├── .gitignore
├── LICENSE
├── README.md
├── api_reference.md
├── conftest.py
├── generate_doc.py
├── operations_reference.md
├── requirements_dev.txt
├── setup.py
├── stac
├── __init__.py
├── annotation.py
├── circuit.py
├── code.py
├── commoncodes.py
├── concatenation.py
├── instruction.py
├── instructionblock.py
├── measurementrecord.py
├── operation.py
├── qubit.py
├── register.py
├── supportedinstructions.py
├── timepoint.py
└── topologicalcodes
│ ├── __init__.py
│ ├── colorcode.py
│ └── primallattice.py
└── tests
└── circuit_test.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # stac
2 | Stac allows you to generate and simulate quantum stabilizer codes. It comes with
3 | its own quantum circuit library that has been designed to make the process of
4 | algorithmically creating quantum error-correction circuits and fault-tolerence
5 | circuits as painless as possible.
6 |
7 | Stac also includes a stabilizer code library. If you give it a generator matrix
8 | of any stabilizer code, you construct a code object. Code objects include
9 | algorithms for generating
10 |
11 | * encoding circuits,
12 | * decoding circuits,
13 | * syndrome measurment circuits,
14 |
15 | for the code, among other useful functions.
16 |
17 | For these circuits or any other circuits you build using the circuit library,
18 | you can
19 |
20 | * draw the circuits.
21 | * annotate the circuit with errors. This is useful for reasoning about how
22 | errors effect the circuits of quantum codes.
23 | * simulate circuits using stim.
24 | * export to qasm, stim or quirk.
25 |
26 | One of the goals of stac (not there yet) is to compute the fault-tolerance
27 | thresholds of simple stabilizer codes in "one-click".
28 |
29 | ## Getting started
30 | To install stac, run
31 |
32 | ```
33 | pip install git+https://github.com/abdullahkhalids/stac
34 | ```
35 |
36 | Please refer to my [mini-book](https://abdullahkhalid.com/qecft/index.html) which
37 | illustrates basic usage in action.
38 |
39 | A short guide on more advanced Stac circuits is available
40 | [here](https://github.com/abdullahkhalids/stac/wiki/guide).
41 |
42 | ## Development version
43 | Stac is currently undergoing a complete overwrite to make it suitable for
44 | constructing fault-tolerant circuits. The goals and progress
45 |
46 | * [x] There is an intrinsic notion of encoded qubits at any concatenation
47 | level in the circuit. User can create registers of such qubits.
48 | * [x] The `append` function can apply a logical operation to qubits at any level
49 | of concatentation. The resultant operation is automatically compiled down
50 | to the physical qubits.
51 | * [ ] The user can construct a fault-tolerant circuit for any stabilizer
52 | code using a few lines of code. (Works for codes with k=1 currently)
53 | * [ ] Provide the user with a rich assembly language to construct custom
54 | fault-tolerant circuits (basic functionality present but needs improvement)
55 | * [ ] Export a stim circuit that can be used to compute the threshold of the
56 | code.
57 |
58 |
59 | ## Credits
60 | Thanks for Unitary Fund for funding part of the development of this project.
61 | [](https://unitary.fund)
62 |
--------------------------------------------------------------------------------
/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | import stac
4 | @pytest.fixture(autouse=True)
5 | def add_doctest_imports(doctest_namespace):
6 | doctest_namespace['stac'] = stac
7 |
--------------------------------------------------------------------------------
/generate_doc.py:
--------------------------------------------------------------------------------
1 | #!/bin/python
2 | import subprocess
3 |
4 | filename = "api_reference.md"
5 |
6 | config = '''{
7 | 'loaders': [{'type': 'python'}],
8 | 'processors': [
9 | {'type': 'filter',
10 | 'expression': 'not name in ["stac", "stac.supportedoperations"] and default()',
11 | 'do_not_filter_modules': False
12 | },
13 | {'type': 'smart'},
14 | {'type': 'crossref'}],
15 | 'renderer': {
16 | 'type': 'markdown',
17 | 'render_toc': True,
18 | 'render_toc_title': "Api Reference",
19 | 'add_module_prefix': False,
20 | 'add_full_prefix': False,
21 | 'render_module_header': False,
22 | 'docstrings_as_blockquote': True,
23 | 'add_method_class_prefix': True,
24 | 'descriptive_class_title': False
25 | }
26 | }
27 | '''
28 |
29 | output = subprocess.run(['pydoc-markdown',
30 | '-I', '.',
31 | config],
32 | capture_output=True)
33 |
34 | so = output.stdout.decode()
35 | L = so.splitlines(True)
36 |
37 | b = False
38 | i = 0
39 | while i < len(L):
40 | line = L[i]
41 | if line == '```\n' and not b:
42 | L[i+1] += '```\n'
43 | b = True
44 | elif line[0:2] == '>> import stac
53 |
54 | ```
55 |
56 | '''
57 |
58 | for i, line in enumerate(L):
59 | if line[0:2] != '=4.6.4
2 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | from setuptools import find_packages, setup
4 |
5 | with open("README.md", "r") as f:
6 | long_description = f.read()
7 |
8 | setup(
9 | name="stac",
10 | version="0.2",
11 | author="Abdullah Khalid",
12 | author_email="abdullah@abdullahkhalid.com",
13 | description="A python library to play with stabilizer codes.",
14 | long_description=long_description,
15 | long_description_content_type="text/markdown",
16 | url="https://github.com/abdullahkhalids/stac",
17 | python_requires=">=3.10",
18 | packages=find_packages(),
19 | include_package_data=True,
20 | install_requires=["ipython",
21 | "numpy",
22 | "stim",
23 | "tabulate",
24 | "svg.py",
25 | "bidict",
26 | "networkx",
27 | "matplotlib",
28 | "ordered-set"]
29 | )
30 |
--------------------------------------------------------------------------------
/stac/__init__.py:
--------------------------------------------------------------------------------
1 | """Initiatization for stac."""
2 |
3 | from .qubit import PhysicalQubit, VirtualQubit
4 | from .register import Register, QubitRegister, RegisterRegister
5 | from .operation import Operation
6 | from .timepoint import Timepoint
7 | from .annotation import Annotation, AnnotationSlice
8 | from .measurementrecord import MeasurementRecord
9 | from .supportedinstructions import instructions
10 | from .circuit import Circuit
11 | from .code import print_matrix, print_paulis, print_paulis_indexed, Code
12 | from .commoncodes import CommonCodes
13 | from .concatenation import ConcatCode
14 | from .instructionblock import InstructionBlock
15 | from .topologicalcodes.colorcode import ColorCode
16 |
--------------------------------------------------------------------------------
/stac/annotation.py:
--------------------------------------------------------------------------------
1 | """Module to provide annotations."""
2 | from typing import Union, Iterator
3 | from .instruction import Instruction
4 |
5 |
6 | class Annotation(Instruction):
7 | """Class to represent circuit annotations."""
8 |
9 | def __init__(self,
10 | name: str,
11 | targets: list = []
12 | ) -> None:
13 | """
14 | Construct annotation object.
15 |
16 | Parameters
17 | ----------
18 | name : str
19 | Name of annotation.
20 | targets : list, optional
21 | Any targets this annotation has. The default is [].
22 |
23 | """
24 | self.name = name
25 | self.targets = targets.copy()
26 |
27 | def __repr__(self) -> str:
28 | """Return a representation of the object."""
29 | s = self.name
30 | s += ' ' + ' '.join([str(t) for t in self.targets])
31 | return s
32 |
33 | def __str__(self) -> str:
34 | """Return a string representation of the object."""
35 | return self.__repr__()
36 |
37 | def copy(self) -> 'Annotation':
38 | """Return copy of object."""
39 | copied_ann = Annotation.__new__(Annotation)
40 |
41 | copied_ann.name = self.name
42 | copied_ann.targets = self.targets
43 |
44 | return copied_ann
45 |
46 |
47 | class AnnotationSlice():
48 | """Class to create and manipulate annotation slices."""
49 |
50 | def __init__(self,
51 | new_ann: Annotation = None) -> None:
52 | """
53 | Construct an AnnotationSlice.
54 |
55 | Parameters
56 | ----------
57 | new_ann : Annotation, optional
58 | This annotation will be appended to this slice. The default is
59 | None.
60 | """
61 | self.elements: list[Annotation] = []
62 |
63 | if new_ann is not None:
64 | self.append(new_ann)
65 |
66 | def __repr__(self) -> str:
67 | """Return a representation of the object."""
68 | return '\n'.join([str(ann) for ann in self.elements])
69 |
70 | def __str__(self) -> str:
71 | """Return a string representation of the object."""
72 | return self.__repr__()
73 |
74 | def __iter__(self) -> Iterator[Annotation]:
75 | """Return iterator of the AnnotationSlice."""
76 | return self.elements.__iter__()
77 |
78 | def __getitem__(self, ind) -> Union[Annotation, list[Annotation]]:
79 | """Make Timepoint subscriptable."""
80 | return self.elements.__getitem__(ind)
81 |
82 | def __len__(self) -> int:
83 | """Return number of annotations in the AnnotationSlice."""
84 | return len(self.elements)
85 |
86 | def copy(self) -> 'AnnotationSlice':
87 | """Return a copy of the AnnotationSlice."""
88 | copied_anns = AnnotationSlice()
89 | for ann in self.elements:
90 | copied_anns.append(ann.copy())
91 |
92 | return copied_anns
93 |
94 | def append(self,
95 | new_ann: Annotation) -> None:
96 | """
97 | Append operation to this AnnotationSlice.
98 |
99 | Parameters
100 | ----------
101 | new_ann : Annotation
102 | Annotation to append.
103 |
104 | """
105 | self.elements.append(new_ann.copy())
106 |
107 | def __add__(self,
108 | other: 'AnnotationSlice') -> 'AnnotationSlice':
109 | """
110 | Create sum of this AnnotationSlice and other AnnotationSlice.
111 |
112 | Parameters
113 | ----------
114 | other : AnnotationSlice
115 | AnnotationSlice to be added.
116 |
117 | Returns
118 | -------
119 | anns : AnnotationSlice
120 | Summed AnnotationSlice.
121 |
122 | """
123 | anns = self.copy()
124 |
125 | for ann in other:
126 | anns.append(ann.copy())
127 |
128 | return anns
129 |
130 | def __iadd__(self,
131 | other: 'AnnotationSlice') -> 'AnnotationSlice':
132 | """
133 | Add other AnnotationSlice to this AnnotationSlice.
134 |
135 | Parameters
136 | ----------
137 | other : AnnotationSlice
138 | AnnotationSlice to be added.
139 |
140 | Returns
141 | -------
142 | AnnotationSlice
143 | Summed AnnotationSlice.
144 |
145 | """
146 | for ann in other:
147 | self.append(ann.copy())
148 |
149 | return self
150 |
--------------------------------------------------------------------------------
/stac/code.py:
--------------------------------------------------------------------------------
1 | """Stac is a stabilizer code module."""
2 | from typing import Any, Optional
3 |
4 | from itertools import combinations
5 | from IPython.display import display, Math
6 | import numpy as np
7 | from random import randint
8 |
9 |
10 | # from .operation import Operation
11 | # from .timepoint import Timepoint
12 | # from .qubit import PhysicalQubit, VirtualQubit
13 | from .register import QubitRegister, RegisterRegister
14 | from .circuit import Circuit
15 | from .supportedinstructions import instructions
16 |
17 |
18 | def print_matrix(array: Any,
19 | augmented: bool = False
20 | ) -> None:
21 | """
22 | Display an array using latex.
23 |
24 | If augmented=True, then a line is placed
25 | in the center of the matrix, which is useful
26 | for printing the stabilizer generator matrix.
27 | """
28 | # If array is a list
29 | if array.ndim == 1:
30 | array = np.array([array])
31 |
32 | data = ''
33 | for line in array:
34 | for element in line:
35 | data += str(element) + ' & '
36 | data = data[:-3]
37 | data += r' \\' + '\n'
38 |
39 | if augmented:
40 | matname = '{array}'
41 | (nrows, ncols) = array.shape
42 | n = int(ncols/2)
43 | c = ''.join(['c' for i in range(n)])
44 | colalign = '{' + c + '|' + c + '}'
45 | display(Math(
46 | f'''\
47 | \\left(\\begin{matname}{colalign}
48 | {data}
49 | \\end{matname}\\right)\
50 | '''))
51 | else:
52 | matname = '{pmatrix}'
53 | display(Math(f'\\begin{matname}\n {data}\\end{matname}'))
54 |
55 |
56 | def print_paulis(G: Any) -> None:
57 | """Print a set of Paulis as I,X,Y,Z."""
58 | if G.ndim == 1:
59 | m = 1
60 | n = int(G.shape[0]/2)
61 | G = np.array([G])
62 | else:
63 | m = G.shape[0]
64 | n = int(G.shape[1]/2)
65 |
66 | for i in range(m):
67 | pauli_str = ''
68 | for j in range(n):
69 | if G[i, j] == 0 and G[i, n+j] == 0:
70 | pauli_str += 'I'
71 | elif G[i, j] and G[i, n+j]:
72 | pauli_str += 'Y'
73 | elif G[i, j]:
74 | pauli_str += 'X'
75 | elif G[i, n+j]:
76 | pauli_str += 'Z'
77 |
78 | display(Math(f'${pauli_str}$'))
79 |
80 |
81 | def print_paulis_indexed(G: Any) -> None:
82 | """Print a set of Paulis as indexed X,Y,Z."""
83 | if len(G.shape) == 1:
84 | m = 1
85 | n = int(G.shape[0]/2)
86 | G = np.array([G])
87 | else:
88 | m = G.shape[0]
89 | n = int(G.shape[1]/2)
90 |
91 | for i in range(m):
92 | pauli_str = ''
93 | for j in range(n):
94 | if G[i, j] == 0 and G[i, n+j] == 0:
95 | pass
96 | elif G[i, j] and G[i, n+j]:
97 | pauli_str += 'Y_{{ {0} }}'.format(j)
98 | elif G[i, j]:
99 | pauli_str += 'X_{{ {} }}'.format(j)
100 | elif G[i, n+j]:
101 | pauli_str += 'Z_{{ {} }}'.format(j)
102 |
103 | if pauli_str != '':
104 | display(Math(f'${pauli_str}$'))
105 |
106 |
107 | def _rref(A: Any,
108 | colswap: bool = True) -> tuple[Any, int, list]:
109 | """
110 | Produce reduced row echelon form (RREF) of a matrix.
111 |
112 | Parameters
113 | ----------
114 | A : numpy.array
115 | The matrix to reduce.
116 | colswap : bool, optional
117 | Whether to swap the columns to place identity at the left. The default
118 | is True.
119 |
120 | Returns
121 | -------
122 | M : numpy.array
123 | The reduced matrix.
124 | r : int
125 | The rank of the matrix.
126 | ops: list
127 | The set of elementary row operations to reduce A to M. Each operation
128 | is a list of length three. The first entry is one of {colswap, rowswap
129 | addrow}. The next two arguments determine which rows to swap or add.
130 |
131 | """
132 | M = np.copy(A)
133 | (nrow, ncol) = M.shape
134 |
135 | if nrow == 0 or ncol == 0:
136 | return M, 0, []
137 |
138 | ops = []
139 | cur_col = 0
140 |
141 | # iterate over each row and find pivot
142 | for cur_row in range(nrow):
143 |
144 | # determine the first non-zero col
145 | new_row = cur_row
146 | new_col = cur_col
147 | while M[new_row, new_col] == 0:
148 | new_row += 1
149 | if new_row == nrow:
150 | new_row = cur_row
151 | new_col += 1
152 | # if rest of matrix is zero
153 | if new_col == ncol:
154 | return M, cur_row, ops
155 |
156 | # the first non-zero entry is M[cur_row, new_col]
157 | # swap cols to bring it forward
158 | if cur_col != new_col and colswap:
159 | M[:, [cur_col, new_col]] = M[:, [new_col, cur_col]]
160 | ops.append(["colswap", cur_col, new_col])
161 |
162 | # Move it to the top
163 | if new_row != cur_row:
164 | M[[cur_row, new_row], :] = M[[new_row, cur_row], :]
165 | ops.append(["rowswap", cur_row, new_row])
166 |
167 | # now non-zero entry is at M[r, cur_col]
168 |
169 | # place zeros above and below the pivot position
170 | for r in range(nrow):
171 | if r != cur_row and M[r, cur_col]:
172 | M[r, :] = (M[r, :] + M[cur_row, :]) % 2
173 | ops.append(["addrow", r, cur_row])
174 |
175 | cur_col += 1
176 |
177 | # if we are are done with all cols
178 | if cur_col == ncol:
179 | break
180 |
181 | # rank is how far down we have gone
182 | rank = cur_row+1
183 |
184 | return M, rank, ops
185 |
186 |
187 | def _perform_row_operations(A: Any,
188 | ops: list,
189 | start_row: int = 0) -> Any:
190 | """
191 | Perform elementary operations on a matrix.
192 |
193 | Parameters
194 | ----------
195 | A : numpy.array
196 | The matrix on which to do the operations.
197 | ops : list
198 | The set of elementary row operations to reduce A to M. Each operation
199 | is a list of length three. The first entry is one of {colswap, rowswap
200 | addrow}. The next two arguments determine which rows to swap or add.
201 | start_row : int, optional
202 | Shift the start row and column of the operations. The default is 0.
203 |
204 | Returns
205 | -------
206 | M : numpy.array
207 | The matrix on which all operations have been performed.
208 |
209 | """
210 | M = np.copy(A)
211 |
212 | for op in ops:
213 | if op[0] == "colswap":
214 | M[:,[start_row + op[1], start_row + op[2]]] = \
215 | M[:,[start_row + op[2], start_row + op[1]]]
216 | elif op[0] == "rowswap":
217 | M[[start_row + op[1], start_row + op[2]], :] = \
218 | M[[start_row + op[2], start_row + op[1]], :]
219 | elif op[0] == "addrow":
220 | M[start_row + op[1], :] = (M[start_row + op[1], :]
221 | + M[start_row + op[2], :]) % 2
222 |
223 | return M
224 |
225 |
226 | def _inner_product(v: Any, w: Any) -> int:
227 | """
228 | Compute the symplectic inner product between two vectors.
229 |
230 | Parameters
231 | ----------
232 | v : numpy.array
233 | Should have one row. Columns should be even.
234 | w : numpy.array
235 | Should have one row. Columns should be even.
236 |
237 | Returns
238 | -------
239 | int
240 | The symplectic inner product between v and w.
241 |
242 | """
243 | n = int(len(v)/2)
244 | return (v[:n]@w[n:] + v[n:]@w[:n]) % 2
245 |
246 |
247 | class Code:
248 | """Class for creating stabilizer codes."""
249 |
250 | def __init__(self, *args: Any) -> None:
251 | """
252 | Construct a stabilizer code.
253 |
254 | Parameters
255 | ----------
256 | There are multiple choices for construction. One choice is
257 |
258 | generator_matrix : numpy.array
259 | The Code is constructed using this generator matrix.
260 |
261 | Another option is,
262 |
263 | generators_x : numpy.array
264 | generators_z : numpy.array
265 | Pass two matrices of the same shape, that describe the X part and
266 | the Z part of the code.
267 |
268 | """
269 | if len(args) == 1:
270 | self.generator_matrix = args[0]
271 |
272 | self.num_data_qubits = int(self.generator_matrix.shape[1]/2)
273 |
274 | self.generators_x = \
275 | self.generator_matrix[:, :self.num_data_qubits]
276 | self.generators_z = \
277 | self.generator_matrix[:, self.num_data_qubits:]
278 |
279 | elif len(args) == 2:
280 | if args[0].shape != args[1].shape:
281 | print("The shape of the matrices don't match")
282 | return
283 |
284 | self.generators_x = args[0]
285 | self.generators_z = args[1]
286 |
287 | self.generator_matrix = np.concatenate(
288 | (self.generators_x, self.generators_z), axis=1)
289 |
290 | self.num_data_qubits = self.generators_x.shape[1]
291 |
292 | self.num_generators = self.generators_x.shape[0]
293 |
294 | self.num_logical_qubits = self.num_data_qubits \
295 | - self.num_generators
296 |
297 | self.distance: Optional[int] = None
298 |
299 | self.rankx = None
300 |
301 | self.standard_generators_x = None
302 | self.standard_generators_z = None
303 | self.standard_generator_matrix = None
304 |
305 | self.destab_gen_mat = None
306 |
307 | self.logical_xs = None
308 | self.logical_zs = None
309 | self.logical_circuits: dict[str, Optional[Circuit]] = dict()
310 | for op in instructions:
311 | self.logical_circuits[op] = None
312 |
313 | self.encoding_circuit = None
314 | self.decoding_circuit = None
315 |
316 | def __repr__(self) -> str:
317 | """Return a representation of the object."""
318 | return f'Code(\n{self.generator_matrix}\n)'
319 |
320 | def __str__(self) -> str:
321 | """Return a string representation of the object."""
322 | if self.distance:
323 | return 'A [[{}, {}, {}]] code'.format(self.num_data_qubits,
324 | self.num_logical_qubits,
325 | self. distance)
326 | else:
327 | return 'A [[{}, {}]] code'.format(self.num_data_qubits,
328 | self.num_logical_qubits)
329 |
330 | def check_valid_code(self) -> bool:
331 | """
332 | Check if code generators commute.
333 |
334 | Returns
335 | -------
336 | bool
337 | True if the code generators commute, false otherwise.
338 |
339 | Examples
340 | --------
341 | >>> generator_matrix = np.array([[1, 1, 0, 0, 0, 0],
342 | ... [0, 1, 1, 0, 0, 0]])
343 | >>> cd = stac.Code(generator_matrix)
344 | >>> cd.check_valid_code()
345 | True
346 | """
347 | is_valid = True
348 | for i in range(self.num_generators-1):
349 | for j in range(i+1, self.num_generators):
350 | if (self.generators_x[i]@self.generators_z[j]
351 | + self.generators_x[j]@self.generators_z[i]) % 2:
352 | print("Generators {} and {} don't commute".format(i, j))
353 | print(np.append(self.generators_x[i],
354 | self.generators_z[i]))
355 | print(np.append(self.generators_x[j],
356 | self.generators_z[j]))
357 | is_valid = False
358 |
359 | return is_valid
360 |
361 | def check_in_normalizer(self,
362 | operator: Any
363 | ) -> bool:
364 | """
365 | Check if an operator is in the normalizer of the stabilizer code.
366 |
367 | Checks if the operator commutes with every generator.
368 |
369 | Parameters
370 | ----------
371 | operator : numpy.array
372 | A 2n length numpy array of of the operator.
373 |
374 | Returns
375 | -------
376 | bool
377 | True if operator in normalizer, else False.
378 | """
379 | for s in self.generator_matrix:
380 | if _inner_product(s, operator):
381 | return False
382 | return True
383 |
384 | def construct_standard_form(self) -> (Any, Any, int):
385 | """
386 | Construct the standard form a stabilizer matrix.
387 |
388 | Returns
389 | -------
390 | standard_generators_x: numpy.array
391 | The X part of the standard generator matrix.
392 | standard_generators_z: numpy.array
393 | The Z part of a standard generator matix.
394 | rankx: int
395 | The rank of the X part of the generator matrix..
396 |
397 | """
398 | # Find RREF of X stabs
399 | (standard_generators_x, self.rankx, opsA) = _rref(self.generators_x)
400 |
401 | standard_generators_z = _perform_row_operations(self.generators_z,
402 | opsA)
403 |
404 | # now extract E' and reduce
405 | (rEp, rankEp, opsEp) = _rref(standard_generators_z[self.rankx:,
406 | self.rankx:])
407 |
408 | # perform same operations on full matrices
409 | self.standard_generators_x = _perform_row_operations(
410 | standard_generators_x,
411 | opsEp,
412 | self.rankx)
413 |
414 | self.standard_generators_z = _perform_row_operations(
415 | standard_generators_z,
416 | opsEp,
417 | self.rankx)
418 |
419 | self.standard_generator_matrix = np.concatenate(
420 | (self.standard_generators_x,
421 | self.standard_generators_z),
422 | axis=1)
423 |
424 | return self.standard_generators_x,\
425 | self.standard_generators_z,\
426 | self.rankx
427 |
428 | def construct_logical_operators(self,
429 | method: str = "gottesman"
430 | ) -> (Any, Any):
431 | """
432 | Construct logical operators for the code.
433 |
434 | Parameters
435 | ----------
436 | method : str
437 | Method to construct logical operators. Uses Gottesman's method by
438 | default.
439 |
440 | Returns
441 | -------
442 | logical_xs: numpy.array
443 | Array of logical xs. Each row is an operator.
444 | logical_zs: numpy.array
445 | Array of logical xs. Each row is an operator.
446 | """
447 | if method != "gottesman":
448 | raise Exception("Only method=gottesman is supported.")
449 | if self.standard_generators_x is None:
450 | self.construct_standard_form()
451 |
452 | n = self.num_data_qubits
453 | k = self.num_logical_qubits
454 | r = self.rankx
455 |
456 | # The relevant parts of the reduced generator matrix are
457 | A2 = self.standard_generators_x[0:r, (n-k):n]
458 | C1 = self.standard_generators_z[0:r, r:(n-k)]
459 | C2 = self.standard_generators_z[0:r, (n-k):n]
460 | E = self.standard_generators_z[r:(n-k), (n-k):n]
461 |
462 | # Construct the logical X operators
463 | self.logical_xs = np.concatenate((
464 | np.zeros((k, r), dtype=int),
465 | E.transpose(),
466 | np.identity(k, dtype=int),
467 | (E.transpose()@C1.transpose() + C2.transpose()) % 2,
468 | np.zeros((k, n-r), dtype=int)
469 | ), axis=1, dtype=int)
470 |
471 | # Construct the logical Z operators
472 | self.logical_zs = np.concatenate((
473 | np.zeros((k, n), dtype=int),
474 | A2.transpose(),
475 | np.zeros((k, n-k-r), dtype=int),
476 | np.identity(k, dtype=int)
477 | ), axis=1, dtype=int)
478 |
479 | return self.logical_xs, self.logical_zs
480 |
481 | def _equivalent_operators(self,
482 | operator: Any
483 | ) -> Any:
484 | """
485 | Given operator, return generator of all equivalent ones.
486 |
487 | Equivalence is defined upto multiplication by stabilizers.
488 |
489 | Parameters
490 | ----------
491 | operator : Any
492 | Operator to find equivalences of.
493 |
494 | Yields
495 | ------
496 | Any
497 | DESCRIPTION.
498 |
499 | """
500 | m = self.num_generators
501 | for i in range(0, 2**m):
502 | d = np.binary_repr(i, m)
503 | sel = np.array(list(d), dtype=int).astype(bool)
504 | op = (np.sum(self.generator_matrix[sel, :], axis=0) + operator) % 2
505 | yield op
506 |
507 | def construct_logical_gate_circuits(
508 | self,
509 | syndrome_measurement_type: str = 'non_ft'
510 | ):
511 | """
512 | Create the circuits that implement logical circuits for the code.
513 |
514 | Results are storted in logical_circuits.
515 |
516 | Parameters
517 | ----------
518 | syndrome_measurement_type: str
519 | Options are 'non_ft', 'cat'
520 |
521 | """
522 | if self.logical_xs is None:
523 | self.construct_logical_operators()
524 |
525 | n = self.num_data_qubits
526 |
527 | for name, operators in [('X', self.logical_xs),
528 | ('Z', self.logical_zs)]:
529 | circ = Circuit()
530 | circ.append_register(
531 | self.construct_encoded_qubit_register(
532 | 0,
533 | syndrome_measurement_type))
534 |
535 | for pauli in operators:
536 | for i in range(n):
537 | if pauli[i] and pauli[n+i]:
538 | circ.append('Y', (0, 0, 0, i))
539 | elif pauli[i]:
540 | circ.append('X', (0, 0, 0, i))
541 | elif pauli[n+i]:
542 | circ.append('Z', (0, 0, 0, i))
543 |
544 | self.logical_circuits[name] = circ
545 |
546 | circ = Circuit()
547 | circ.append_register(
548 | self.construct_encoded_qubit_register(
549 | 0, syndrome_measurement_type))
550 | for i in range(n):
551 | circ.append('H', (0, 0, 0, i))
552 | self.logical_circuits['H'] = circ
553 |
554 | circ = Circuit()
555 | circ.append_register(
556 | self.construct_encoded_qubit_register(
557 | 0, syndrome_measurement_type))
558 | circ.append("H", (0, 0, 0, 0))
559 | for i in range(6):
560 | circ.append("CX", (0, 0, 0, i), (0, 0, 0, i+1))
561 | self.logical_circuits['CAT'] = circ
562 |
563 | for name in ['M', 'R', 'MR']:
564 | circ = Circuit()
565 | circ.append_register(
566 | self.construct_encoded_qubit_register(
567 | 0, syndrome_measurement_type))
568 | for i in range(self.num_data_qubits):
569 | circ.append(name, (0, 0, 0, i))
570 | self.logical_circuits[name] = circ
571 |
572 | for name in ['CX', 'CZ']:
573 | circ = Circuit()
574 | circ.append_register(
575 | self.construct_encoded_qubit_register(
576 | 0, syndrome_measurement_type))
577 | circ.append_register(
578 | self.construct_encoded_qubit_register(
579 | 0, syndrome_measurement_type))
580 | for i in range(n):
581 | circ.append(name, (0, 0, 0, i), (0, 1, 0, i))
582 |
583 | self.logical_circuits[name] = circ
584 |
585 | def find_destabilizers(self):
586 | """
587 | Find the destabilizers of the standard form generators.
588 |
589 | Find the destabilizers of the standard form generators by exhaustive
590 | search. This will be slow for large codes but has the advantage that
591 | it will find the lowest weight destabilizers.
592 |
593 | Returns
594 | -------
595 | destab_gen_mat: numpy.array
596 | Array of shape m x 2n where each row is a destabilizer
597 |
598 | """
599 | if self.standard_generators_x is None:
600 | self.construct_standard_form()
601 |
602 | n = self.num_data_qubits
603 |
604 | destabs = np.empty((self.num_generators, 2*n), dtype=int)
605 | destab_found = [False for i in range(self.num_generators)]
606 | i = 0
607 | b = False
608 |
609 | # these for loops create binary vector arrays
610 | # in increasing number of 1s.
611 | for k in range(1, 2*n):
612 | if b:
613 | break
614 | for comb in combinations(np.arange(2*n), k):
615 | v = np.array([1 if i in comb else 0 for i in range(2*n)])
616 |
617 | # v should anti-commute with only one generator
618 | # to be a destabilizer
619 | num_anti_commute = 0
620 | for i in range(self.num_generators):
621 | ip = _inner_product(self.standard_generator_matrix[i], v)
622 | if ip:
623 | num_anti_commute += ip
624 | if num_anti_commute > 1:
625 | break
626 | else:
627 | destab_for_gen = i
628 | else:
629 | if not destab_found[destab_for_gen]:
630 | destabs[destab_for_gen] = v
631 | destab_found[destab_for_gen] = True
632 |
633 | if np.all(destab_found):
634 | b = True
635 | break
636 |
637 | self.destab_gen_mat = np.array(destabs)
638 | return self.destab_gen_mat
639 |
640 | def construct_data_register(
641 | self,
642 | level: int
643 | ) -> RegisterRegister:
644 | """
645 | Create a data qubit register for this code.
646 |
647 | Parameters
648 | ----------
649 | level : int
650 | The concatenation level of the qubit.
651 |
652 | Returns
653 | -------
654 | RegisterRegister
655 | The data qubit register.
656 |
657 | """
658 | return QubitRegister('d', level, self.num_data_qubits)
659 |
660 | def construct_syndrome_measurement_register(
661 | self,
662 | level: int,
663 | syndrome_measurement_type: str = 'non_ft'
664 | ) -> RegisterRegister:
665 | """
666 | Create a register appropriate for doing syndrome measurements.
667 |
668 | Parameters
669 | ----------
670 | level : int
671 | The concatenation level of the qubit.
672 | syndrome_measurement_type : str
673 | Options are 'non_ft',
674 | 'cat',
675 | 'cat_standard'.
676 | With the 'standard' postfix uses the standard form of the
677 | generators. 'non_ft' is Default.
678 |
679 | Returns
680 | -------
681 | RegisterRegister
682 | Register for encoded qubit.
683 | """
684 | if syndrome_measurement_type == 'cat_standard':
685 | genmat = self.standard_generator_matrix
686 | else:
687 | genmat = self.generator_matrix
688 |
689 | if syndrome_measurement_type == 'non_ft':
690 | genregs = [QubitRegister('g', level, 1)
691 | for i in range(self.num_generators)]
692 | elif syndrome_measurement_type in ['cat', 'cat_standard']:
693 | genregs = []
694 | for g in genmat:
695 | cat_reg = QubitRegister('c', level, sum(g))
696 | detect_reg = QubitRegister('f', level, 1)
697 | genregs.append(
698 | RegisterRegister('g',
699 | level,
700 | subregisters=(cat_reg, detect_reg)))
701 | else:
702 | raise Exception("Unknown syndrome_measurement_type")
703 | return RegisterRegister('s', level, genregs, code=self)
704 |
705 | def construct_encoded_qubit_register(
706 | self,
707 | level: int,
708 | syndrome_measurement_type: str = 'non_ft'
709 | ) -> RegisterRegister:
710 | """
711 | Create a register appropriate for creating an encoded qubit.
712 |
713 | Parameters
714 | ----------
715 | level : int
716 | The concatenation level of the qubit.
717 | syndrome_measurement_type : str
718 | Options are 'non_ft',
719 | 'cat',
720 | 'cat_standard'.
721 | With the 'standard' postfix uses the standard form of the
722 | generators. 'non_ft' is Default.
723 |
724 | Returns
725 | -------
726 | RegisterRegister
727 | Register for encoded qubit.
728 |
729 | """
730 | n = self.num_data_qubits
731 | datareg = QubitRegister('d', level, n)
732 | if syndrome_measurement_type == 'non_ft':
733 | genregs = [QubitRegister('g', level, 1)
734 | for i in range(self.num_generators)]
735 | elif syndrome_measurement_type == 'cat':
736 | genregs = []
737 | for g in self.generator_matrix:
738 | cat_reg = QubitRegister('c', level, sum(g))
739 | detect_reg = QubitRegister('a', level, 1)
740 | genregs.append(
741 | RegisterRegister('g',
742 | level,
743 | subregisters=(cat_reg, detect_reg)))
744 | else:
745 | raise Exception("Unknown 'syndrome_measurement_type'")
746 | syndreg = RegisterRegister('s', level, genregs)
747 | return RegisterRegister('e',
748 | level,
749 | subregisters=(datareg, syndreg),
750 | code=self)
751 |
752 | def construct_encoding_circuit(self,
753 | syndrome_measurement_type: str = 'none'
754 | ) -> Circuit:
755 | """
756 | Construct an encoding circuit for the code using Gottesman's method.
757 |
758 | Parameters
759 | ----------
760 | syndrome_measurement_type : str, optional
761 | Possible types are
762 | * 'none': Creates a simple data register only. Default.
763 | * 'non_ft',
764 | * 'cat',
765 | * 'cat_standard'.
766 | With the 'standard' postfix uses the standard form of the
767 | generators.
768 | The syndrome registers will be empty, but useful if the circuit
769 | is part of a larget circuit.
770 |
771 | Returns
772 | -------
773 | encoding_circuit : Circuit
774 | The encoding circuit.
775 |
776 | """
777 | if self.logical_xs is None:
778 | self.construct_logical_operators()
779 |
780 | n = self.num_data_qubits
781 | k = self.num_logical_qubits
782 | r = self.rankx
783 |
784 | self.encoding_circuit = Circuit()
785 | if syndrome_measurement_type == 'none':
786 | reg = self.construct_data_register(0)
787 | self.encoding_circuit.base_address = (0, 0)
788 | else:
789 | reg = self.construct_encoded_qubit_register(
790 | 0,
791 | syndrome_measurement_type)
792 | self.encoding_circuit.base_address = (0, 0, 0)
793 |
794 | self.encoding_circuit.append_register(reg)
795 | for i in range(k):
796 | for j in range(r, n-k):
797 | if self.logical_xs[i, j]:
798 | self.encoding_circuit.append("CX", n-k+i, j)
799 |
800 | for i in range(r):
801 | self.encoding_circuit.append("H", i)
802 | for j in range(n):
803 | if i == j:
804 | continue
805 | if (self.standard_generators_x[i, j]
806 | and self.standard_generators_z[i, j]):
807 |
808 | self.encoding_circuit.append("CX", i, j)
809 | self.encoding_circuit.append("CZ", i, j)
810 | elif self.standard_generators_x[i, j]:
811 | self.encoding_circuit.append("CX", i, j)
812 | elif self.standard_generators_z[i, j]:
813 | self.encoding_circuit.append("CZ", i, j)
814 |
815 | return self.encoding_circuit
816 |
817 | def construct_decoding_circuit(self) -> Circuit:
818 | """
819 | Construct an decoding circuit for the code using Gottesman's method.
820 |
821 | Returns
822 | -------
823 | decoding_circuit : Circuit
824 | The decoding circuit.
825 |
826 | """
827 | if self.logical_xs is None:
828 | self.construct_logical_operators()
829 |
830 | n = self.num_data_qubits
831 | k = self.num_logical_qubits
832 |
833 | self.decoding_circuit = Circuit()
834 | reg = self.construct_data_register(0)
835 | self.decoding_circuit.append_register(reg)
836 | self.decoding_circuit.append_register(QubitRegister('a', 0, k))
837 | self.decoding_circuit.base_address = (0,)
838 |
839 | # Note, we will need num_logical_qubits ancilla
840 | for i in range(len(self.logical_zs)):
841 | for j in range(n):
842 | if self.logical_zs[i, n+j]:
843 | self.decoding_circuit.append("CX", (0, j), (1, i))
844 |
845 | for i in range(len(self.logical_xs)):
846 | for j in range(n):
847 | if self.logical_xs[i, j] and self.logical_xs[i, n+j]:
848 | self.decoding_circuit.append("CZ", (1, i), (0, j))
849 | self.decoding_circuit.append("CX", (1, i), (0, j))
850 | elif self.logical_xs[i, j]:
851 | self.decoding_circuit.append("CX", (1, i), (0, j))
852 | elif self.logical_xs[i, n+j]:
853 | self.decoding_circuit.append("CZ", (1, i), (0, j))
854 |
855 | return self.decoding_circuit
856 |
857 | def construct_syndrome_circuit(self,
858 | syndrome_measurement_type: str = 'non_ft',
859 | assign_circuit: bool = True
860 | ) -> Circuit:
861 | """
862 | Construct a circuit to measure the stabilizers of the code.
863 |
864 | ----------
865 | syndrome_measurement_type : str
866 | Options are 'non_ft',
867 | 'non_ft_standard',
868 | 'cat',
869 | 'cat_standard'.
870 | With the 'standard' postfix uses the standard form of the
871 | generators. If no argument, then 'non_ft' is Default.
872 | assign_circuit : bool, optional
873 | If true, the circuit is assigned to self.syndrome_circuit. The
874 | default is True.
875 |
876 | Returns
877 | -------
878 | syndrome_circuit : Circuit
879 | The circuit for measuring the stabilizers.
880 |
881 | """
882 | if syndrome_measurement_type == 'non_ft':
883 | syndrome_circuit = \
884 | self._construct_syndrome_circuit_simple(self.generators_x,
885 | self.generators_z)
886 | elif syndrome_measurement_type == 'non_ft_standard':
887 | if self.standard_generators_x is None:
888 | self.construct_standard_form()
889 | syndrome_circuit = \
890 | self._construct_syndrome_circuit_simple(
891 | self.standard_generators_x,
892 | self.standard_generators_z)
893 |
894 | elif syndrome_measurement_type == 'cat':
895 | syndrome_circuit = \
896 | self._construct_syndrome_circuit_cat(self.generators_x,
897 | self.generators_z)
898 | elif syndrome_measurement_type == 'cat_standard':
899 | if self.standard_generators_x is None:
900 | self.construct_standard_form()
901 | syndrome_circuit = \
902 | self._construct_syndrome_circuit_cat(
903 | self.standard_generators_x,
904 | self.standard_generators_z)
905 |
906 | if assign_circuit:
907 | self.syndrome_circuit = syndrome_circuit
908 |
909 | return syndrome_circuit
910 |
911 | def _construct_syndrome_circuit_simple(self,
912 | generators_x: Any,
913 | generators_z: Any
914 | ) -> Circuit:
915 | """
916 | Construct a non-fault tolerant syndrome circuit.
917 |
918 | Parameters
919 | ----------
920 | generators_x : numpy.array
921 | generators_z : numpy.array
922 | The X and Z generators.
923 |
924 | Returns
925 | -------
926 | Circuit
927 | The syndrome circuit.
928 |
929 | """
930 | syndrome_circuit = Circuit()
931 | rg = self.construct_encoded_qubit_register(0, 'non_ft')
932 | syndrome_circuit.append_register(rg)
933 |
934 | for i in range(self.num_generators):
935 | syndrome_circuit.append("H", (0, 0, 1, i, 0))
936 |
937 | for i in range(self.num_generators):
938 | for j in range(self.num_data_qubits):
939 | if generators_x[i, j] and generators_z[i, j]:
940 | syndrome_circuit.append("CX",
941 | (0, 0, 1, i, 0),
942 | (0, 0, 0, j))
943 | syndrome_circuit.append("CZ",
944 | (0, 0, 1, i, 0),
945 | (0, 0, 0, j))
946 | elif generators_x[i, j]:
947 | syndrome_circuit.append("CX",
948 | (0, 0, 1, i, 0),
949 | (0, 0, 0, j))
950 | elif generators_z[i, j]:
951 | syndrome_circuit.append("CZ",
952 | (0, 0, 1, i, 0),
953 | (0, 0, 0, j))
954 | syndrome_circuit.cur_time += 1
955 |
956 | for i in range(0, self.num_generators):
957 | syndrome_circuit.append("H", (0, 0, 1, i, 0))
958 |
959 | for i in range(0, self.num_generators):
960 | syndrome_circuit.append('MR', (0, 0, 1, i, 0))
961 |
962 | return syndrome_circuit
963 |
964 | def _construct_syndrome_circuit_cat(self,
965 | generators_x: Any,
966 | generators_z: Any
967 | ) -> Circuit:
968 | """
969 | Construct a fault tolerant syndrome circuit.
970 |
971 | Parameters
972 | ----------
973 | generators_x : numpy.array
974 | generators_z : numpy.array
975 | The X and Z generators.
976 |
977 | Returns
978 | -------
979 | Circuit
980 | The syndrome circuit.
981 |
982 | """
983 | syndrome_circuit = Circuit()
984 | rg = self.construct_encoded_qubit_register(0, 'cat')
985 | syndrome_circuit.append_register(rg)
986 |
987 | # create cat state check
988 | for i in range(self.num_generators):
989 | syndrome_circuit.cur_time = 0
990 | syndrome_circuit.append('H', (0, 0, 1, i, 0, 0))
991 | ng = syndrome_circuit.register[0, 0, 1, i, 0].num_qubits
992 | for j in range(ng-1):
993 | syndrome_circuit.append(
994 | 'CX', (0, 0, 1, i, 0, j), (0, 0, 1, i, 0, j+1))
995 | syndrome_circuit.append(
996 | 'CX', (0, 0, 1, i, 0, 0), (0, 0, 1, i, 1, 0))
997 | syndrome_circuit.append(
998 | 'CX', (0, 0, 1, i, 0, ng-1), (0, 0, 1, i, 1, 0))
999 | syndrome_circuit.append('MR', (0, 0, 1, i, 1, 0))
1000 |
1001 | syndrome_circuit.cur_time += 1
1002 |
1003 | # measure each generator
1004 | for i in range(self.num_generators):
1005 | k = 0
1006 | for j in range(self.num_data_qubits):
1007 | if generators_x[i, j] and generators_z[i, j]:
1008 | syndrome_circuit.append("CX",
1009 | (0, 0, 1, i, 0, k),
1010 | (0, 0, 0, j))
1011 | syndrome_circuit.append("CZ",
1012 | (0, 0, 1, i, 0, k),
1013 | (0, 0, 0, j))
1014 | k += 1
1015 | elif generators_x[i, j]:
1016 | syndrome_circuit.append("CX",
1017 | (0, 0, 1, i, 0, k),
1018 | (0, 0, 0, j))
1019 | k += 1
1020 | elif generators_z[i, j]:
1021 | syndrome_circuit.append("CZ",
1022 | (0, 0, 1, i, 0, k),
1023 | (0, 0, 0, j))
1024 | k += 1
1025 | syndrome_circuit.cur_time += 1
1026 |
1027 | w = len(syndrome_circuit.instructions)-1
1028 | for i in range(self.num_generators):
1029 | syndrome_circuit.cur_time = w
1030 | ng = syndrome_circuit.register[0, 0, 1, i, 0].num_qubits
1031 | for j in range(ng-1-1, -1, -1):
1032 | syndrome_circuit.append(
1033 | 'CX', (0, 0, 1, i, 0, j), (0, 0, 1, i, 0, j+1))
1034 | syndrome_circuit.append('H', (0, 0, 1, i, 0, 0))
1035 | syndrome_circuit.append('MR', (0, 0, 1, i, 0, 0))
1036 |
1037 | return syndrome_circuit
1038 |
1039 | def construct_encoded_qubit(self,
1040 | J: int,
1041 | syndrome_measurement_type: str = 'non_ft'
1042 | ) -> Circuit:
1043 | """
1044 | Create an encoded qubit at the Jth concatenation level.
1045 |
1046 | Parameters
1047 | ----------
1048 | J : int
1049 | Concatenation level.
1050 | syndrome_measurement_type : str, optional
1051 | Options are 'non_ft',
1052 | 'non_ft_standard',
1053 | 'cat',
1054 | 'cat_standard'.
1055 | With the 'standard' postfix uses the standard form of the
1056 | generators. If no argument, then 'non_ft' is Default.
1057 |
1058 | Returns
1059 | -------
1060 | Circuit
1061 | DESCRIPTION.
1062 |
1063 | """
1064 | self.construct_logical_gate_circuits()
1065 |
1066 | syndcirc = self.construct_syndrome_circuit(syndrome_measurement_type,
1067 | assign_circuit=False)
1068 | for tp in syndcirc.instructions:
1069 | for op in tp:
1070 | if op.name == 'H':
1071 | op.name = 'CAT'
1072 | circ = Circuit()
1073 |
1074 | # add one qubit at level J
1075 | address = circ.append_register(QubitRegister('l', J, 1))
1076 | next_addresses = []
1077 | next_addresses.append(address)
1078 |
1079 | for j in range(J-1, -1, -1):
1080 | prev_addresses = next_addresses.copy()
1081 | next_addresses = []
1082 | for paddress in prev_addresses:
1083 | for qubit in circ.register[paddress].qubits('l'):
1084 | address = circ.append_register(
1085 | self.construct_encoded_qubit_register(
1086 | j, syndrome_measurement_type))
1087 | qubit.constituent_register = address
1088 | qubit.index_in_constituent_register = 0
1089 | next_addresses.append(address)
1090 | for qubit in circ.register[paddress].qubits('d'):
1091 | address = circ.append_register(
1092 | self.construct_encoded_qubit_register(
1093 | j, syndrome_measurement_type))
1094 | qubit.constituent_register = address
1095 | qubit.index_in_constituent_register = 0
1096 | next_addresses.append(address)
1097 | for qubit in circ.register[paddress].qubits('s'):
1098 | sreg = RegisterRegister(
1099 | 's',
1100 | j,
1101 | QubitRegister('g', j, self.num_data_qubits))
1102 | sreg.code = self
1103 | address = circ.append_register(sreg)
1104 | qubit.constituent_register = address
1105 | qubit.index_in_constituent_register = 0
1106 | next_addresses.append(address)
1107 |
1108 | for j in range(0, J):
1109 | w = len(circ.instructions)
1110 |
1111 | for reg in circ.register[j]:
1112 | if reg.register_type != 'e':
1113 | continue
1114 | circ.cur_time = w
1115 | for tp in syndcirc.instructions:
1116 | for op in tp:
1117 | circ.append(op.rebase_qubits((reg.level, reg.index)))
1118 | circ.cur_time += 1
1119 | return circ
1120 |
1121 | def generate_error(self,
1122 | error_type: str = 'X',
1123 | weight: int = 1
1124 | ) -> (np.ndarray, set):
1125 | """
1126 | Create a pure X or pure Z error as a binary vector of length 2n.
1127 |
1128 | Parameters
1129 | ----------
1130 | error_type : str, optional
1131 | Either 'X' or 'Z'. The default is 'X'.
1132 | weight : int, optional
1133 | The weight of the error. The default is 1.
1134 |
1135 | Returns
1136 | -------
1137 | error: np.ndarray
1138 | The error.
1139 | """
1140 | error = np.zeros(2*self.num_data_qubits, dtype=int)
1141 | error_locations = set()
1142 | if error_type == 'X':
1143 | while len(error_locations) < weight:
1144 | error_locations.add(randint(0, self.num_data_qubits-1))
1145 |
1146 | for loc in error_locations:
1147 | error[loc] = 1
1148 | elif error_type == 'Z':
1149 | while len(error_locations) < weight:
1150 | error_locations.add(randint(self.num_data_qubits,
1151 | 2*self.num_data_qubits-1))
1152 | for loc in error_locations:
1153 | error[self.num_data_qubits + loc] = 1
1154 |
1155 | return error
1156 |
1157 | def compute_syndrome(self,
1158 | error: np.ndarray
1159 | ) -> np.ndarray:
1160 | """
1161 | Compute the syndrome of an error.
1162 |
1163 | Parameters
1164 | ----------
1165 | error : numpy.ndarray
1166 | A binary vector of length 2n.
1167 |
1168 | Returns
1169 | -------
1170 | syndrome : numpy.ndarray
1171 | A binary vector of length m.
1172 |
1173 | """
1174 | swapped_vector = np.append(
1175 | error[self.num_data_qubits: 2*self.num_data_qubits],
1176 | error[0: self.num_data_qubits]
1177 | )
1178 | syndrome = self.generator_matrix @ swapped_vector % 2
1179 |
1180 | return syndrome
1181 |
1182 | def verify_correction(self,
1183 | error: np.ndarray,
1184 | correction: np.ndarray,
1185 | print_result: bool = True
1186 | ) -> bool:
1187 | """
1188 | Verify if a correction corrects the given error.
1189 |
1190 | Parameters
1191 | ----------
1192 | error : numpy.ndarray
1193 | A binary vector of length 2n.
1194 | correction : numpy.ndarray
1195 | A binary vector of length 2n.
1196 | print_result : bool, optional
1197 | Print why correction fails or if it is valid. The default is True.
1198 |
1199 | Returns
1200 | -------
1201 | bool
1202 | True if correction valid, else False.
1203 |
1204 | """
1205 | corrected_error = (error + correction) % 2
1206 | corrected_syndrome = self.compute_syndrome(corrected_error)
1207 |
1208 | if any(corrected_syndrome):
1209 | if print_result:
1210 | print("Corrected state is not in stabilizer.")
1211 | return False
1212 | elif _inner_product(self.logical_zs[0], corrected_error) == 1:
1213 | if print_result:
1214 | print("Corrected state is logically incorrect.")
1215 | else:
1216 | if print_result:
1217 | print("Correction is valid.")
1218 | return True
1219 |
--------------------------------------------------------------------------------
/stac/commoncodes.py:
--------------------------------------------------------------------------------
1 | """Provides collection of some codes."""
2 | import numpy as np
3 | from .code import Code
4 |
5 |
6 | class CommonCodes:
7 | """Class to provide some common codes."""
8 |
9 | def __init__(self):
10 | """Use the generate_code method to create codes."""
11 | pass
12 |
13 | @classmethod
14 | def generate_code(cls,
15 | codename: str) -> Code:
16 | """
17 | Generate an internally stored Code.
18 |
19 | Parameters
20 | ----------
21 | codename : str
22 | Can be one of the following.
23 | * [[7,1,3]]
24 | * [[5,1,3]]
25 | * [[4,2,2]]
26 | * [[8,3,3]]
27 | * [[6,4,2]]
28 |
29 | Raises
30 | ------
31 | Exception
32 | If codename is not recognized.
33 |
34 | Returns
35 | -------
36 | Code
37 | The corresponding code.
38 |
39 | """
40 | if codename == '[[7,1,3]]':
41 | return cls._Steane()
42 | elif codename == '[[5,1,3]]':
43 | return cls._Code513()
44 | elif codename == '[[4,2,2]]':
45 | return cls._Code422()
46 | elif codename == '[[8,3,3]]':
47 | return cls._Code833()
48 | elif codename == '[[6,4,2]]':
49 | return cls._Code642()
50 | else:
51 | raise Exception("Code not found. See method help.")
52 |
53 | @classmethod
54 | def _Steane(cls) -> Code:
55 | hamming = np.array([
56 | [1, 1, 1, 1, 0, 0, 0],
57 | [1, 1, 0, 0, 1, 1, 0],
58 | [1, 0, 1, 0, 1, 0, 1]
59 | ], dtype=int)
60 |
61 | zeroM = np.zeros(hamming.shape, dtype=int)
62 |
63 | Sx = np.concatenate((hamming, zeroM))
64 | Sz = np.concatenate((zeroM, hamming))
65 |
66 | c = Code(Sx, Sz)
67 | c.distance = 3
68 |
69 | return c
70 |
71 | @classmethod
72 | def _Code513(cls) -> Code:
73 |
74 | Sx = np.array([
75 | [1, 0, 0, 1, 0],
76 | [0, 1, 0, 0, 1],
77 | [1, 0, 1, 0, 0],
78 | [0, 1, 0, 1, 0]
79 | ], dtype=int)
80 |
81 | Sz = np.array([
82 | [0, 1, 1, 0, 0],
83 | [0, 0, 1, 1, 0],
84 | [0, 0, 0, 1, 1],
85 | [1, 0, 0, 0, 1]
86 | ], dtype=int)
87 |
88 | c = Code(Sx, Sz)
89 | c.distance = 3
90 |
91 | return c
92 |
93 | @classmethod
94 | def _Code833(cls) -> Code:
95 | Sx = np.array([
96 | [1, 1, 1, 1, 1, 1, 1, 1],
97 | [0, 0, 0, 0, 0, 0, 0, 0],
98 | [0, 1, 0, 1, 1, 0, 1, 0],
99 | [0, 1, 0, 1, 0, 1, 0, 1],
100 | [0, 1, 1, 0, 1, 0, 0, 1],
101 | ], dtype=int)
102 | Sz = np.array([
103 | [0, 0, 0, 0, 0, 0, 0, 0],
104 | [1, 1, 1, 1, 1, 1, 1, 1],
105 | [0, 0, 0, 0, 1, 1, 1, 1],
106 | [0, 0, 1, 1, 0, 0, 1, 1],
107 | [0, 1, 0, 1, 0, 1, 0, 1],
108 | ], dtype=int)
109 |
110 | c = Code(Sx, Sz)
111 | c.distance = 3
112 |
113 | return c
114 |
115 | @classmethod
116 | def _Code422(cls) -> Code:
117 |
118 | Sx = np.array([
119 | [1, 0, 0, 1],
120 | [1, 1, 1, 1]
121 | ], dtype=int)
122 |
123 | Sz = np.array([
124 | [0, 1, 1, 0],
125 | [1, 0, 0, 1]
126 | ], dtype=int)
127 |
128 | c = Code(Sx, Sz)
129 | c.distance = 2
130 |
131 | return c
132 |
133 | @classmethod
134 | def _Code642(cls) -> Code:
135 |
136 | Sx = np.array([
137 | [1, 1, 1, 1, 1, 1],
138 | [0, 0, 0, 0, 0, 0]
139 | ], dtype=int)
140 |
141 | Sz = np.array([
142 | [0, 0, 0, 0, 0, 0],
143 | [1, 1, 1, 1, 1, 1]
144 | ], dtype=int)
145 |
146 | c = Code(Sx, Sz)
147 | c.distance = 2
148 |
149 | return c
150 |
--------------------------------------------------------------------------------
/stac/concatenation.py:
--------------------------------------------------------------------------------
1 | """Provide a module to concatenate quantum codes."""
2 | from typing import Any
3 | import numpy as np
4 | from .code import Code
5 |
6 |
7 | class ConcatCode(Code):
8 | """Class to create concatenated codes."""
9 |
10 | def __init__(self, *args: Any) -> None:
11 | """
12 | Construct a concatenated code.
13 |
14 | Parameters
15 | ----------
16 | *args:
17 | Can be on eof the following
18 | tuple[Code]
19 | A tuple of codes that will be concatenated in order.
20 | tuple[Code, int]
21 | The Code will be concatenated with itself.
22 |
23 | Raises
24 | ------
25 | TypeError
26 | DESCRIPTION.
27 |
28 |
29 | """
30 | if len(args) == 1 and type(args[0]) == tuple:
31 | self.concat_sequence = args[0]
32 | elif (len(args) == 2
33 | and type(args[0]) == Code
34 | and type(args[1]) == int):
35 | self.concat_sequence = tuple([args[0]]*args[1])
36 | else:
37 | raise TypeError
38 |
39 | for cd in self.concat_sequence:
40 | if cd.logical_xs is None or cd.logical_zs is None:
41 | cd.construct_logical_operators()
42 |
43 | # create generator matrix
44 | self._construct_generator_matrix()
45 |
46 | # after you have constructed the generator matrix for the concat
47 | # code, we can call the parent init
48 | super().__init__(self.generator_matrix)
49 |
50 | def _construct_generator_matrix(self):
51 |
52 | # assign the first two codes as code 1 and 2
53 | code1 = self.concat_sequence[0]
54 |
55 | for code2 in self.concat_sequence[1:]:
56 | # code2 = self.concat_sequence[1]
57 |
58 | # construct the concat generator matrix
59 | if code1.num_data_qubits % code2.num_logical_qubits == 0:
60 | concat_generator_matrix = \
61 | self._construct_generator_matrix_concat_k2_divides_n1(
62 | code1, code2)
63 | else:
64 | concat_generator_matrix = \
65 | self._construct_generator_matrix_concat_k2_not_divides_n1(
66 | code1, code2)
67 |
68 | # create a code using the concat_generator_matrix
69 | code1 = Code(concat_generator_matrix)
70 |
71 | self.generator_matrix = concat_generator_matrix
72 | return self.generator_matrix
73 |
74 | def _construct_generator_matrix_concat_k2_divides_n1(self,
75 | code1: Code,
76 | code2: Code
77 | ) -> Any:
78 | """
79 | Construct concatenatenated generators when k_2 divides n_1.
80 |
81 | Parameters
82 | ----------
83 | code1 : Code
84 | First code.
85 | code2 : Code
86 | Second code.
87 |
88 | Returns
89 | -------
90 | numpy.ndarray
91 | The generator matrix of the concatenated code.
92 |
93 | """
94 | n1 = code1.num_data_qubits
95 | k1 = code1.num_logical_qubits
96 | m1 = code1.num_generators
97 | n2 = code2.num_data_qubits
98 | k2 = code2.num_logical_qubits
99 | m2 = code2.num_generators
100 | # physical qubits of concatenated code
101 | n = int(n1*n2/k2)
102 |
103 | new_gens_x = np.zeros((int(n - k1), n), dtype=int)
104 | new_gens_z = np.zeros((int(n - k1), n), dtype=int)
105 |
106 | # First for each block of qubits, we associate the generators of code 2
107 | # number of blocks
108 | nB = int(n1/k2)
109 | # size of each block
110 | sB = n2
111 | # for each block
112 | for b in range(nB):
113 | # for each generator in C2
114 | for i in range(m2):
115 | # place it in a shifted manner
116 | new_gens_x[m2*b + i, sB*b:sB*(b+1)] = code2.generators_x[i]
117 | new_gens_z[m2*b + i, sB*b:sB*(b+1)] = code2.generators_z[i]
118 |
119 | # create the incomplete generator matrix
120 | new_gens = np.concatenate((new_gens_x, new_gens_z), axis=1)
121 |
122 | # Now we want to add the encoded generators of code1
123 | nB = int(n1/k2)
124 | sB = k2
125 | # for each generator
126 | for i in range(m1):
127 | g = code1.generator_matrix[i]
128 |
129 | encoded_g = np.zeros(2*n, dtype=int)
130 |
131 | # break into blocks
132 | for b in range(nB):
133 | gb_x = g[sB*b:sB*(b+1)]
134 | gb_z = g[n1 + sB*b:n1 + sB*(b+1)]
135 |
136 | # iterate over the entries
137 | for j in range(sB):
138 | # first create shifted logical operators for each block
139 | shifted_op_x = np.zeros(2*n, dtype=int)
140 | shifted_op_x[n2*b:n2*(b+1)] = code2.logical_xs[j][:n2]
141 | shifted_op_x[n+n2*b:n+n2*(b+1)] = code2.logical_xs[j][n2:]
142 |
143 | shifted_op_z = np.zeros(2*n, dtype=int)
144 | shifted_op_z[n2*b:n2*(b+1)] = code2.logical_zs[j][:n2]
145 | shifted_op_z[n+n2*b:n+n2*(b+1)] = code2.logical_zs[j][n2:]
146 |
147 | # Depending on what operator is at g[j], we include the
148 | # correct logical operator into the encoded operator
149 | if gb_x[j] and gb_z[j]:
150 | encoded_g = (encoded_g + shifted_op_x +
151 | shifted_op_z) % 2
152 | elif gb_x[j]:
153 | encoded_g = (encoded_g + shifted_op_x) % 2
154 | else:
155 | encoded_g = (encoded_g + shifted_op_z) % 2
156 |
157 | new_gens[n-n1+i] = encoded_g
158 |
159 | return new_gens
160 |
161 | def _construct_generator_matrix_concat_k2_not_divides_n1(
162 | self,
163 | code1: Code,
164 | code2: Code
165 | ) -> Any:
166 | """
167 | Construct concatenatenated generators when k_2 does not divide n_1.
168 |
169 | Parameters
170 | ----------
171 | code1 : Code
172 | First code.
173 | code2 : Code
174 | Second code.
175 |
176 | Returns
177 | -------
178 | numpy.ndarray
179 | The generator matrix of the concatenated code.
180 |
181 | """
182 | n1 = code1.num_data_qubits
183 | k1 = code1.num_logical_qubits
184 | m1 = code1.num_generators
185 | n2 = code2.num_data_qubits
186 | k2 = code2.num_logical_qubits
187 | m2 = code2.num_generators
188 |
189 | n = int(n1*n2)
190 | k = int(k1*k2)
191 |
192 | new_gens_x = np.zeros((n-k, n), dtype=int)
193 | new_gens_z = np.zeros((n-k, n), dtype=int)
194 |
195 | nB = n1
196 | sB = n2
197 | # for each block
198 | for b in range(nB):
199 | # for each generator in C2
200 | for i in range(m2):
201 | # print(b*cd2.num_generators + i, sB*b,sB*(b+1)-1 )
202 | new_gens_x[b*m2 + i, sB*b:sB*(b+1)] = code2.generators_x[i]
203 | new_gens_z[b*m2 + i, sB*b:sB*(b+1)] = code2.generators_z[i]
204 |
205 | new_gens = np.concatenate((new_gens_x, new_gens_z), axis=1)
206 |
207 | for c in range(k2):
208 | for j in range(m1):
209 | g = code1.generator_matrix[j]
210 | encoded_g = np.zeros(2*n, dtype=int)
211 | for k in range(n1):
212 | shifted_op_x = np.zeros(2*n, dtype=int)
213 | shifted_op_x[n2*k:n2*(k+1)] = code2.logical_xs[c][:n2]
214 | shifted_op_x[n+n2*k:n+n2*(k+1)] = code2.logical_xs[c][n2:]
215 |
216 | shifted_op_z = np.zeros(2*n, dtype=int)
217 | shifted_op_z[n2*k:n2*(k+1)] = code2.logical_zs[c][:n2]
218 | shifted_op_z[n+n2*k:n+n2*(k+1)] = code2.logical_zs[c][n2:]
219 |
220 | if g[k] and g[n1+k]:
221 | encoded_g = (encoded_g + shifted_op_x +
222 | shifted_op_z) % 2
223 | elif g[k]:
224 | encoded_g = (encoded_g + shifted_op_x) % 2
225 | elif g[n1+k]:
226 | encoded_g = (encoded_g + shifted_op_z) % 2
227 |
228 | new_gens[n-n1*k2+c*code1.num_generators+j] = encoded_g
229 |
230 | return new_gens
231 |
--------------------------------------------------------------------------------
/stac/instruction.py:
--------------------------------------------------------------------------------
1 | """Module to provide the instruction class."""
2 |
3 |
4 | class Instruction:
5 | """Class to represent all circuit operations, annotations etc."""
6 |
7 | pass
8 |
--------------------------------------------------------------------------------
/stac/instructionblock.py:
--------------------------------------------------------------------------------
1 | """Provide module for blocks of circuit operations."""
2 | from typing import Iterator, Union, Any
3 |
4 |
5 | class InstructionBlock:
6 | """Class for creating and manipulating blocks of circuit instructions."""
7 |
8 | def __init__(self):
9 | self.elements: list = []
10 |
11 | def __repr__(self) -> str:
12 | """Return a representation of the block."""
13 | return '\n'.join([str(el) for el in self.elements])
14 |
15 | def __str__(self) -> str:
16 | """Return a string representation of the block."""
17 | return self.__repr__()
18 |
19 | def __iter__(self) -> Iterator:
20 | """Return iterator of the block."""
21 | return self.elements.__iter__()
22 |
23 | def __getitem__(self, ind) -> Union[Any, list[Any]]:
24 | """Make Timepoint subscriptable."""
25 | return self.elements.__getitem__(ind)
26 |
27 | def __len__(self) -> int:
28 | """Return number of operations in the block."""
29 | return len(self.elements)
30 |
31 | def insert(self, i, ins) -> int:
32 | """Insert instruction at particular index."""
33 | self.elements.insert(i, ins)
34 |
35 | def copy(self) -> 'InstructionBlock':
36 | """Return a copy of the block."""
37 | copied_ib = InstructionBlock()
38 | for el in self.elements:
39 | copied_ib.append(el.copy())
40 |
41 | return copied_ib
42 |
43 | def append(self, obj) -> None:
44 | """Append object."""
45 | self.elements.append(obj)
46 |
47 |
48 | class AnnotationBlock(InstructionBlock):
49 | """Class to create blocks that hold annotations."""
50 |
51 | pass
52 |
53 |
54 | class RepetitionBlock(InstructionBlock):
55 | """Class to create blocks of repeating instructions."""
56 |
57 | def __init__(self,
58 | repetitions: int
59 | ) -> None:
60 | self.repetitions = repetitions
61 | super().__init__()
62 |
63 |
64 | class IfBlock(InstructionBlock):
65 | """Class to store conditional instructions."""
66 |
67 | def __init__(self):
68 | pass
69 |
--------------------------------------------------------------------------------
/stac/measurementrecord.py:
--------------------------------------------------------------------------------
1 | """Provides class for measurement records."""
2 |
3 | class MeasurementRecord:
4 | def __init__(self,
5 | address: tuple,
6 | index: int
7 | ) -> None:
8 | self.address = address
9 | self.index = index
10 |
11 | def __repr__(self) -> str:
12 | return f'MR[{self.address}, {self.index}]'
13 |
--------------------------------------------------------------------------------
/stac/operation.py:
--------------------------------------------------------------------------------
1 | """Provide a class for operations in circuits."""
2 | # from typing import Optional
3 | from .instruction import Instruction
4 |
5 |
6 | class Operation(Instruction):
7 | """Class to represent operations in circuits."""
8 |
9 | def __init__(self,
10 | name: str,
11 | targets: list = [],
12 | parameters: list[float] | None = None
13 | ) -> None:
14 | """
15 | Construct Operation object.
16 |
17 | Parameters
18 | ----------
19 | name : str
20 | Name of operation.
21 | targets : list[tuple]
22 | List of addresses that the operation targets.
23 | controls : Optional[list[tuple]], optional
24 | If this is a quantum-controlled operation, then this is a list of
25 | addresses that control the operation. The default is None.
26 | classical_control : None, optional
27 | This parameter is unused at the moment. The default is None.
28 |
29 | This contructor does no checks on whether the name, controls or targets
30 | are valid. These checks should be done before appending the operation
31 | to the circuit.
32 |
33 | Examples
34 | --------
35 | >>> op = stac.Operation('H', [(0, 0, 3)])
36 | >>> op
37 | H (0, 0, 3)
38 |
39 | >>> op = stac.Operation('CX', [(0, 0, 3), (0, 0, 2)])
40 | >>> op
41 | CX (0, 0, 3) (0, 0, 2)
42 |
43 | >>> op = stac.Operation('RX', [(0, 0, 1)], [0.5])
44 | >>> op
45 | RX(0.5) (0, 0, 1)
46 | """
47 | # Todo Add classical controls
48 | # Any changes here should be reflected in copy()
49 | self.name = name
50 |
51 | self.targets = targets.copy()
52 | self.num_affected_qubits = len(self.targets)
53 | self.affected_qubits = set(self.targets)
54 |
55 | if parameters is not None:
56 | self.is_parameterized = True
57 | self.parameters = parameters
58 | self.num_parameters = len(parameters)
59 | else:
60 | self.is_parameterized = False
61 |
62 | def __repr__(self) -> str:
63 | """Return a representation of the object."""
64 | s = self.name
65 | if self.is_parameterized:
66 | s += '(' + str(self.parameters)[1:-1] + ')'
67 | s += ' ' + ' '.join([str(t) for t in self.targets])
68 | return s
69 |
70 | def __str__(self) -> str:
71 | """Return a string representation of the object."""
72 | return self.__repr__()
73 |
74 | def __eq__(self,
75 | other: 'Operation') -> bool:
76 | """Determine if two operations are equal."""
77 | if self.name != other.name:
78 | return False
79 | if len(self.targets) != len(other.targets):
80 | return False
81 | for i, q in enumerate(self.targets):
82 | if q != other.targets[i]:
83 | return False
84 | if self.is_parameterized != other.is_parameterized:
85 | return False
86 | if self.is_parameterized:
87 | if len(self.parameters) != len(other.parameters):
88 | return False
89 | for i, p in enumerate(self.parameters):
90 | if q != other.parameters[i]:
91 | return False
92 | return True
93 |
94 | def __hash__(self) -> int:
95 | """Return a hash of the object."""
96 | return hash(repr(self))
97 |
98 | def copy(self) -> 'Operation':
99 | """Return copy of class."""
100 | copied_op = Operation.__new__(Operation)
101 |
102 | copied_op.name = self.name
103 | copied_op.targets = self.targets
104 | copied_op.num_affected_qubits = self.num_affected_qubits
105 | copied_op.affected_qubits = self.affected_qubits
106 |
107 | copied_op.is_parameterized = self.is_parameterized
108 | if self.is_parameterized:
109 | copied_op.parameters = self.parameters
110 | copied_op.num_parameters = self.num_parameters
111 |
112 | return copied_op
113 |
114 | def rebase_qubits(self,
115 | new_base: tuple) -> 'Operation':
116 | """
117 | Create Operation with new base address of the controls and targets.
118 |
119 | Parameters
120 | ----------
121 | new_base : tuple
122 | The base address to replace the existing base. This can be any
123 | length shorter than the length of the smallest address within the
124 | controls and targets.
125 |
126 | Returns
127 | -------
128 | Operation
129 | A new Operation with new base address.
130 |
131 | Examples
132 | --------
133 | >>> op = stac.Operation('CX', [(0, 0, 3), (0, 0, 2)])
134 | >>> op.rebase_qubits((0,1))
135 | CX (0, 1, 3) (0, 1, 2)
136 | """
137 | L = len(new_base)
138 | new_targets = [new_base + q[L:] for q in self.targets]
139 | return Operation(self.name, new_targets)
140 |
--------------------------------------------------------------------------------
/stac/qubit.py:
--------------------------------------------------------------------------------
1 | """Provides classes to create and manipulate qubits."""
2 | from typing import Union
3 |
4 |
5 | class PhysicalQubit:
6 | """Class to create and manipulate physical qubits."""
7 |
8 | def __init__(self,
9 | index: int,
10 | coordinates: Union[int, tuple],
11 | interactable_qubits: list[Union[int, tuple]]
12 | ) -> None:
13 | """
14 | Construct a physical qubit.
15 |
16 | Parameters
17 | ----------
18 | index : int
19 | Index of qubits within its Register.
20 | coordinates : Union[int, tuple]
21 | The coordinate of the qubit.
22 | interactable_qubits : list[Union[int, tuple]]
23 | The qubits this qubit can interact with.
24 |
25 | """
26 | self.index = index
27 | self.coordinates = coordinates
28 | self.interactable_qubits = interactable_qubits
29 |
30 |
31 | class VirtualQubit:
32 | """Class to create and manipulate virtual qubits."""
33 |
34 | def __init__(self,
35 | level: int,
36 | index_in_assigned_register: int,
37 | assigned_register: tuple = None,
38 | index_in_constituent_register: int = None,
39 | constituent_register: tuple = None
40 | ) -> None:
41 | """
42 | Construct a virtual qubit.
43 |
44 | Parameters
45 | ----------
46 | level : int
47 | The level of the Circuit this qubit is at.
48 | index_in_assigned_register : int
49 | The index within its assigned register.
50 | assigned_register : tuple, optional
51 | The address of the Register this qubit is part of. The default is
52 | None.
53 | index_in_constituent_register : int, optional
54 | The index within its constituent register. The default is None.
55 | constituent_register : tuple, optional
56 | Encoded qubits at level > 1 are made of a Register. This points to
57 | the address of that Register. The default is None.
58 |
59 | """
60 | self.level = level
61 |
62 | # The register this qubit is part of
63 | self.assigned_register = assigned_register
64 | self.index = index_in_assigned_register
65 |
66 | # The register this qubit is made of
67 | self.constituent_register = constituent_register
68 | self.index_in_constituent_register = index_in_constituent_register
69 |
70 | self.register_type = 'q'
71 |
72 | @property
73 | def index_in_assigned_register(self) -> int:
74 | """
75 | Get index in assigned register.
76 |
77 | Returns
78 | -------
79 | int
80 | Index in assigned register.
81 |
82 | """
83 | return self._index
84 |
85 | @index_in_assigned_register.setter
86 | def index_in_assigned_register(self,
87 | value: int) -> None:
88 | """
89 | Set index in assigned register.
90 |
91 | Parameters
92 | ----------
93 | value : int
94 | Value to set.
95 |
96 | """
97 | self._index = value
98 |
99 | @property
100 | def index(self):
101 | """
102 | Get index in assigned register.
103 |
104 | Returns
105 | -------
106 | int
107 | Index in assigned register.
108 |
109 | """
110 | return self._index
111 |
112 | @index.setter
113 | def index(self,
114 | value: int) -> None:
115 | """
116 | Set index in assigned register.
117 |
118 | Parameters
119 | ----------
120 | value : int
121 | Value to set.
122 |
123 | """
124 | self._index = value
125 |
126 | def copy(self) -> 'VirtualQubit':
127 | """
128 | Create copy of this register.
129 |
130 | Returns
131 | -------
132 | VirtualQubit
133 | The copy of self.
134 |
135 | """
136 | vq = VirtualQubit.__new__(VirtualQubit)
137 | vq.level = self.level
138 |
139 | vq.assigned_register = self.assigned_register
140 | vq.index = self.index_in_assigned_register
141 |
142 | vq.constituent_register = self.constituent_register
143 | vq.index_in_constituent_register = self.index_in_constituent_register
144 |
145 | vq.register_type = 'q'
146 |
147 | return vq
148 |
--------------------------------------------------------------------------------
/stac/register.py:
--------------------------------------------------------------------------------
1 | """Provides a set of classes to define registers of qubits."""
2 | from typing import Union, Optional, Iterator, Any, Sequence
3 | from .qubit import VirtualQubit
4 |
5 |
6 | class Register:
7 | """
8 | Class to create and manipulate registers.
9 |
10 | Registers have a type that determines how the register is functionally
11 | used within the fault-tolerant circuit. Stac recognizes the following
12 | types:
13 | d : Data registers store encoded qubits. For a [[n,k,d]] code, the
14 | size of such registers should be n.
15 | g : Stabilizer generator measurement registers have the ancilla qubits
16 | used to measure one stabilizer generator of a code. The size of
17 | such registers is usually equal to the weight of the generator.
18 | s : Syndrome measurement registers are a collection of g-type
19 | registers.
20 | e : Encoded qubit registers usually contain one d-type register and
21 | one s-type register.
22 | However, registers can be given any type.
23 | """
24 |
25 | def __init__(self) -> None:
26 | """
27 | Construct a Register.
28 |
29 | This class is generally not used directly, but its subclasses are.
30 | """
31 | self.index: Union[int, None] = None
32 | self.register_type: Union[str, None] = None
33 |
34 | self.elements: Sequence[Union['Register', VirtualQubit]] = []
35 |
36 | self.level: Union[int, None] = None
37 |
38 | def copy(self) -> 'Register':
39 | """
40 | Create a copy of this register.
41 |
42 | Returns
43 | -------
44 | Register
45 | The copy of this register.
46 |
47 | """
48 | reg = Register.__new__(Register)
49 | reg.index = self.index
50 | reg.register_type = self.register_type
51 | reg.elements = [r.copy() for r in self.elements]
52 | reg.level = self.level
53 |
54 | return reg
55 |
56 | def __repr__(self) -> str:
57 | """Return a representation of the object."""
58 | if type(self) is QubitRegister:
59 | t = 'QubitRegister'
60 | else:
61 | t = 'RegisterRegister'
62 | return ''.join([t,
63 | f'(register_type={self.register_type}, ',
64 | f'level={self.level}, ',
65 | f'len={self.__len__()})'])
66 |
67 | def __str__(self) -> str:
68 | """Return a string representation of the object."""
69 | return self.__repr__()
70 |
71 | def __len__(self) -> int:
72 | """Return number of objects in the register."""
73 | return len(self.elements)
74 |
75 | def __iter__(self) -> Iterator:
76 | """Return iterator of the Register."""
77 | return self.elements.__iter__()
78 |
79 | def __getitem__(self,
80 | s: Union[int, tuple]
81 | ) -> Union['Register', VirtualQubit]:
82 | """
83 | Make Register subscriptable.
84 |
85 | Parameters
86 | ----------
87 | s : Union[int, tuple]
88 | Address of register to return.
89 |
90 | Returns
91 | -------
92 | Register
93 | The Register at s.
94 |
95 | Raises
96 | ------
97 | IndexError
98 | If s is not a valid address.
99 | """
100 | if type(s) is int:
101 | return self.elements.__getitem__(s)
102 | elif (type(s) is tuple
103 | and all(isinstance(v, int) for v in s[:-1])): # type: ignore
104 | reg: Any = self
105 | for t in s:
106 | try:
107 | reg = reg.elements[t]
108 | except IndexError:
109 | error_message = f'The register does not contain a \
110 | subregister or qubit at {s}.'
111 | raise IndexError(error_message)
112 | return reg
113 | else:
114 | raise TypeError('Cannot recognize subscript.')
115 |
116 | def __ge__(self,
117 | other: 'Register') -> bool:
118 | """
119 | Determine if this Register contains qubits at every address as other.
120 |
121 | Parameters
122 | ----------
123 | other : Register
124 | The Register to compare self to.
125 |
126 | Returns
127 | -------
128 | bool
129 | True if this Register contains qubits as every address as other,
130 | otherwise False.
131 |
132 | This is an important check when adding circuits.
133 | """
134 | self_addresses = set(self.qubit_addresses())
135 | other_addresses = set(other.qubit_addresses())
136 |
137 | if other_addresses.issubset(self_addresses):
138 | return True
139 | else:
140 | return False
141 |
142 | def append(self,
143 | *registers: Union['Register', list['Register']]
144 | ) -> None:
145 | """
146 | Append one or more registers to this register.
147 |
148 | Parameters
149 | ----------
150 | *registers : Register
151 | Either a list of Registers, or pass one or more Registers as
152 | arguments.
153 |
154 | Raises
155 | ------
156 | TypeError
157 | If args are not a Register.
158 |
159 | """
160 | if (len(registers) == 1
161 | and issubclass(type(registers[0]), Register)):
162 | registers_list = [registers[0]]
163 | elif (len(registers) == 1 and type(registers[0]) is list
164 | and all(issubclass(type(r), Register) for r in registers[0])):
165 | registers_list = registers[0] # type: ignore
166 | elif all(issubclass(type(r), Register) for r in registers):
167 | registers_list = list(registers)
168 | else:
169 | raise TypeError("Args must be registers or a list of registers.")
170 |
171 | for register in registers_list:
172 | register.index = len(self) # type: ignore
173 | self.elements.append(register) # type: ignore
174 |
175 | @ property
176 | def num_qubits(self) -> int:
177 | """
178 | Determine number of qubits in this Register recursively.
179 |
180 | Returns
181 | -------
182 | int
183 | Number of qubits.
184 |
185 | """
186 | if type(self) is QubitRegister:
187 | return len(self.elements)
188 | else:
189 | return sum([register.num_qubits # type: ignore
190 | for register in self.elements])
191 |
192 | def structure(self,
193 | depth: int = -1) -> None:
194 | """
195 | Print the register structure.
196 |
197 | Parameters
198 | ----------
199 | max_depth : int, optional
200 | Determine structure to this depth. The default is -1, which goes to
201 | max depth.
202 |
203 | """
204 | if depth == -1:
205 | depth = 4294967295
206 |
207 | def determine_structure(register, indent, d):
208 | if d > depth:
209 | return ''
210 | s = ' '*indent
211 | s += ' '.join([str(register.index),
212 | register.register_type,
213 | '×',
214 | str(len(register)),
215 | '\n'])
216 | if type(register) is not QubitRegister:
217 | for child_register in register.elements:
218 | s += determine_structure(child_register, indent+3, d+1)
219 | return s
220 | print(determine_structure(self, 0, 0))
221 |
222 | def check_address(self,
223 | address: tuple) -> bool:
224 | """
225 | Determine if address is a valid qubit address in this Register.
226 |
227 | Parameters
228 | ----------
229 | address : tuple
230 | Address to be checked.
231 |
232 | Raises
233 | ------
234 | Exception
235 | If address not found, or if address does not point to a Qubit.
236 |
237 | Returns
238 | -------
239 | bool
240 | Only returns True if valid address, else raises Exception.
241 |
242 | """
243 | truncated_address: tuple = address[:-1] # type: ignore
244 | try:
245 | self[truncated_address]
246 | except KeyError:
247 | raise Exception('Address not found.')
248 |
249 | if type(self[truncated_address]) is not QubitRegister:
250 | raise Exception(f'{self[truncated_address]} is not a qubit \
251 | register.')
252 |
253 | try:
254 | self[address]
255 | except KeyError:
256 | raise Exception('Address not found.')
257 |
258 | return True
259 |
260 | def qubit_addresses(self,
261 | my_address: Optional[tuple] = tuple()
262 | ) -> list[tuple]:
263 | """
264 | Determine all qubit addresses within this Register, or its subregister.
265 |
266 | Parameters
267 | ----------
268 | my_address : tuple, optional
269 | The address of the subregister within which to search. The default
270 | is tuple(), which searches from the root of this Register.
271 |
272 | Returns
273 | -------
274 | address_list : list[tuple]
275 | List of addresses of qubits.
276 |
277 | """
278 | address_list = []
279 |
280 | def determine_structure(register, my_address):
281 | if type(register) is RegisterRegister:
282 | for i, child_register in enumerate(register.elements):
283 | determine_structure(child_register, my_address + (i,))
284 | else:
285 | for i, qubit in enumerate(register.elements):
286 | address_list.append(my_address + (i,))
287 | determine_structure(self, tuple())
288 | return address_list
289 |
290 | def qubits(self,
291 | register_type: Optional[str] = None
292 | ) -> Iterator:
293 | """
294 | Create generator for qubits within this Register.
295 |
296 | Parameters
297 | ----------
298 | register_type : str, optional
299 | Only generates qubits who or their parent register have this type.
300 | The default is None, in which case all qubits are generated.
301 |
302 | Yields
303 | ------
304 | Iterator
305 | Generator for qubits.
306 |
307 | """
308 | def iterator(register, certain_yield=False):
309 | if type(register) is RegisterRegister:
310 | for child_register in register:
311 | if (child_register.register_type == register_type
312 | or certain_yield):
313 | yield from iterator(child_register, True)
314 | else:
315 | yield from iterator(child_register)
316 | elif (register_type is None
317 | or certain_yield
318 | or register.register_type == register_type):
319 | for qubit in register:
320 | yield qubit
321 | if self.register_type == register_type:
322 | return iterator(self, True)
323 | else:
324 | return iterator(self)
325 |
326 |
327 | class QubitRegister(Register):
328 | """Class to manipulate registers made out of virtual qubits."""
329 |
330 | def __init__(self,
331 | register_type: str,
332 | level: int,
333 | num_qubits: int,
334 | index: Optional[int] = None) -> None:
335 | """
336 | Construct a register to store qubits.
337 |
338 | Parameters
339 | ----------
340 | register_type : str
341 | The type of the Register. See Register class documenation.
342 | level : int
343 | The level within a Circuit at which this Register is present.
344 | num_qubits : int
345 | Number of qubits to create within this Register.
346 | index : int, optional
347 | The index of the Register within its parent. The default is None.
348 |
349 | """
350 | self.register_type = register_type
351 | self.level = level
352 | qubit_list = []
353 | for i in range(num_qubits):
354 | q = VirtualQubit(self.level,
355 | i)
356 | qubit_list.append(q)
357 |
358 | self.elements = qubit_list
359 |
360 | self.index = index
361 |
362 | def copy(self) -> 'QubitRegister':
363 | """
364 | Create a copy of this register.
365 |
366 | Returns
367 | -------
368 | QubitRegister
369 | A copy of this register.
370 |
371 | """
372 | reg = QubitRegister.__new__(QubitRegister)
373 | reg.register_type = self.register_type
374 | reg.level = self.level
375 | reg.elements = [q.copy() for q in self.elements]
376 | reg.index = self.index
377 |
378 | return reg
379 |
380 | @ property # type: ignore
381 | def index(self) -> Union[int, None]:
382 | """
383 | Get index of this Register.
384 |
385 | Returns
386 | -------
387 | int
388 | Index of this Register. Is None if not set.
389 |
390 | """
391 | return self._index
392 |
393 | @ index.setter
394 | def index(self,
395 | value: int) -> None:
396 | """
397 | Set index of this Register.
398 |
399 | Also sets the assigned_register property of each qubit within this
400 | Register.
401 |
402 | Parameters
403 | ----------
404 | value : int
405 | Value to set.
406 |
407 | """
408 | self._index = value
409 | # for qubit in self.elements:
410 | # qubit.assigned_register = self._index
411 |
412 |
413 | class RegisterRegister(Register):
414 | """Class to manipulate registers made out of subregisters."""
415 |
416 | def __init__(self,
417 | register_type: str,
418 | level: int,
419 | subregisters: Optional[Iterator[Union[Register,
420 | VirtualQubit]]] = None,
421 | code: Optional[Any] = None) -> None:
422 | """
423 | Construct a register to hold registers.
424 |
425 | Parameters
426 | ----------
427 | register_type : str
428 | The type of the register. See Register class documenation.
429 | level : int
430 | The level within a Circuit at which this Register is present.
431 | subregisters : Iterator, optional
432 | If provided, these are appended to this Register. The default is
433 | None.
434 | code : Code, optional
435 | The code to attach to this Register. The default is None.
436 |
437 | """
438 | self.register_type = register_type
439 | self.level = level
440 | self.elements = []
441 |
442 | if type(subregisters) is list or type(subregisters) is tuple:
443 | self.elements = list(subregisters) # type: ignore
444 | elif (type(subregisters) is QubitRegister
445 | or type(subregisters) is RegisterRegister):
446 | self.elements = list([subregisters]) # type: ignore
447 | for i, register in enumerate(self.elements):
448 | register.index = i
449 |
450 | self.code = code
451 |
452 | self._index: Union[int, None] = None
453 |
454 | def copy(self) -> 'RegisterRegister':
455 | """
456 | Create a copy of this register.
457 |
458 | Returns
459 | -------
460 | RegisterRegister
461 | The copied register.
462 |
463 | """
464 | reg = RegisterRegister.__new__(RegisterRegister)
465 |
466 | reg.register_type = self.register_type
467 | reg.level = self.level
468 | reg.elements = [r.copy() for r in self.elements]
469 | reg.code = self.code
470 | reg._index = self._index
471 |
472 | return reg
473 |
474 | @ property # type: ignore
475 | def index(self) -> Optional[int]:
476 | """
477 | Get index of this Register.
478 |
479 | Returns
480 | -------
481 | int
482 | Index of this Register. Is None if not set.
483 |
484 | """
485 | return self._index
486 |
487 | @ index.setter
488 | def index(self,
489 | value: int) -> None:
490 | """
491 | Set index of this Register.
492 |
493 | Parameters
494 | ----------
495 | value : int
496 | Value to set.
497 |
498 | """
499 | self._index = value
500 |
501 | def constituent_register_mapping(self) -> list[list]:
502 | """
503 | Determine the constituent register of every qubit within this Register.
504 |
505 | Returns
506 | -------
507 | list[list]
508 | List of lists, where each sublist is a
509 | [address, constituent_reigster] pair.
510 |
511 | """
512 | mapping = []
513 |
514 | for address in reversed(self.qubit_addresses()):
515 | if self[address].constituent_register is not None: # type: ignore
516 | mapping.append([
517 | address,
518 | self[address].constituent_register]) # type: ignore
519 |
520 | return mapping
521 |
--------------------------------------------------------------------------------
/stac/supportedinstructions.py:
--------------------------------------------------------------------------------
1 | """
2 | The instructions supported by stac.
3 |
4 | ins_type: int
5 | Instruction type. 0 for operation, 1 for annotation
6 | num_targets: int
7 | If positive, indicates the exact number of targets. If -1, the
8 | instruction can take any number of targets.
9 | """
10 |
11 | instructions = dict()
12 |
13 | for name in ['I', 'X', 'Y', 'Z', 'H', 'S', 'T', 'CAT']:
14 | instructions[name] = {
15 | 'ins_type': 0,
16 | 'num_targets': 1,
17 | 'control_targets': set(),
18 | 'num_parameters': 0,
19 | 'draw_text': [name],
20 | 'draw_img': [name],
21 | 'stim_str': name,
22 | 'qasm_str': name.lower() + ' q[{t0}];\n'
23 | }
24 | instructions['I']['qasm_str'] = 'id q[{t0}];\n'
25 | instructions['CAT']['stim_str'] = 'H'
26 | instructions['CAT']['draw_text'] = 'H'
27 | instructions['CAT']['draw_img'] = 'H'
28 |
29 | for name in ['RX', 'RY', 'RZ']:
30 | instructions[name] = {
31 | 'ins_type': 0,
32 | 'num_targets': 1,
33 | 'control_targets': set(),
34 | 'num_parameters': 1,
35 | 'draw_text': [name],
36 | 'draw_img': [name],
37 | 'stim_str': name,
38 | 'qasm_str': name.lower() + '({p0}) q[{t0}];\n'
39 | }
40 |
41 | for name in ['CX', 'CY', 'CZ']:
42 | instructions[name] = {
43 | 'ins_type': 0,
44 | 'num_targets': 2,
45 | 'control_targets': {0},
46 | 'num_parameters': 0,
47 | 'draw_text': ['●', name[1]],
48 | 'draw_img': ['●', name[1]],
49 | 'stim_str': name,
50 | 'qasm_str': name.lower() + ' q[{t0}],q[{t1}];\n'
51 | }
52 | instructions['CX']['draw_text'][1] = '⊕'
53 | instructions['CZ']['draw_text'][1] = '●'
54 |
55 | for name in ['R', 'M', 'MR']:
56 | instructions[name] = {
57 | 'ins_type': 0,
58 | 'num_targets': 1,
59 | 'control_targets': set(),
60 | 'num_parameters': 0,
61 | 'draw_text': [name],
62 | 'draw_img': [name],
63 | 'stim_str': name,
64 | 'qasm_str': ''
65 | }
66 | instructions['MR']['draw_text'][0] = 'm'
67 | instructions['R']['qasm_str'] = 'reset q[{t0}];\n'
68 | instructions['M']['qasm_str'] = 'measure q[{t0}] -> c[{t0}];\n'
69 | instructions['MR']['qasm_str'] = 'measure q[{t0}] -> c[{t0}];\n'
70 |
71 | # annotations
72 | instructions['TICK'] = {
73 | 'ins_type': 1,
74 | 'num_targets': 0,
75 | 'control_targets': set(),
76 | 'num_parameters': 0,
77 | 'stim_str': name,
78 | 'qasm_str': 'barrier q;\n'
79 | }
80 |
81 | instructions['DETECTOR'] = {
82 | 'ins_type': 1,
83 | 'num_targets': -1,
84 | 'control_targets': set(),
85 | 'num_parameters': 0,
86 | 'draw_text': None,
87 | 'draw_img': None,
88 | 'stim_str': None,
89 | 'qasm_str': None
90 | }
91 |
--------------------------------------------------------------------------------
/stac/timepoint.py:
--------------------------------------------------------------------------------
1 | """Provides class for creating and manipulating timepoints in circuits."""
2 | from typing import Union, Iterator
3 | from .operation import Operation
4 |
5 |
6 | class Timepoint:
7 | """Class to create and manipulate timepoints."""
8 |
9 | def __init__(self,
10 | new_op: Operation = None) -> None:
11 | """
12 | Construct a Timepoint.
13 |
14 | Parameters
15 | ----------
16 | new_op : Operation, optional
17 | This operation will be appended to the Timepoint. The default is
18 | None.
19 | """
20 | self.operations: list[Operation] = []
21 | self.affected_qubits: set[tuple] = set()
22 | self.repeat_start = False
23 | self.repeat_end = False
24 | self.repeat_repetitions = None
25 |
26 | if new_op is not None:
27 | self.append(new_op)
28 |
29 | def __repr__(self) -> str:
30 | """Return a representation of the object."""
31 | return '\n'.join([str(op) for op in self.operations])
32 |
33 | def __str__(self) -> str:
34 | """Return a string representation of the object."""
35 | return self.__repr__()
36 |
37 | def __iter__(self) -> Iterator[Operation]:
38 | """Return iterator of the Timepoint."""
39 | return self.operations.__iter__()
40 |
41 | def __getitem__(self, ind) -> Union[Operation, list[Operation]]:
42 | """Make Timepoint subscriptable."""
43 | return self.operations.__getitem__(ind)
44 |
45 | def __len__(self) -> int:
46 | """Return number of operations in the Timepoint."""
47 | return len(self.operations)
48 |
49 | def copy(self) -> 'Timepoint':
50 | """Return a copy of the Timepoint."""
51 | copied_tp = Timepoint()
52 | for op in self.operations:
53 | copied_tp.append(op.copy())
54 |
55 | return copied_tp
56 |
57 | def append(self,
58 | new_op: Operation) -> None:
59 | """
60 | Append operation to this Timepoint.
61 |
62 | Parameters
63 | ----------
64 | new_op : Operation
65 | Operation to append.
66 |
67 | Raises
68 | ------
69 | Exception
70 | If new_op can't be appended to current Timepoint.
71 |
72 | """
73 | possible_intersections = self.affected_qubits \
74 | & new_op.affected_qubits
75 | if len(possible_intersections) == 0:
76 | self.affected_qubits |= new_op.affected_qubits
77 |
78 | self.operations.append(new_op.copy())
79 | else:
80 | raise Exception("Operations affects qubits already affected by\
81 | this timepoint.")
82 |
83 | def can_append(self,
84 | new_op: Operation) -> bool:
85 | """
86 | Check if an Operation can be appended to this Timepoint.
87 |
88 | Parameters
89 | ----------
90 | new_op : Operation
91 | Operation to be checked.
92 |
93 | Returns
94 | -------
95 | bool
96 | True if Operation can be appended, otherwise False.
97 |
98 | """
99 | possible_intersections = self.affected_qubits \
100 | & new_op.affected_qubits
101 | if len(possible_intersections) == 0:
102 | return True
103 | else:
104 | return False
105 |
106 | def rebase_qubits(self,
107 | new_base: tuple) -> 'Timepoint':
108 | """
109 | Create Timepoint with new base address for all controls and targets.
110 |
111 | Parameters
112 | ----------
113 | new_base : tuple
114 | New base address. Must have length smaller than the shortest
115 | address within all controls and targets within qubits.
116 |
117 | Returns
118 | -------
119 | tp : Timepoint
120 | Timepoint with new base address.
121 |
122 | """
123 | tp = Timepoint()
124 | for op in self.operations:
125 | tp.append(op.rebase_qubits(new_base))
126 | return tp
127 |
128 | def can_add(self,
129 | other: 'Timepoint') -> bool:
130 | """
131 | Check if a Timepoint can be added to this Timepoint.
132 |
133 | Parameters
134 | ----------
135 | other : Timepoint
136 | The Timepoint to be checked.
137 |
138 | Returns
139 | -------
140 | bool
141 | True if other can be added, otherwise False.
142 |
143 | """
144 | for op in other:
145 | if not self.can_append(op):
146 | return False
147 | else:
148 | return True
149 |
150 | def __add__(self,
151 | other: 'Timepoint') -> 'Timepoint':
152 | """
153 | Create Timepoint that is sum of other Timepoint and this Timepoint.
154 |
155 | Parameters
156 | ----------
157 | other : Timepoint
158 | Timepoint to be added.
159 |
160 | Returns
161 | -------
162 | tp : Timepoint
163 | DESCRIPTION.
164 |
165 | """
166 | tp = self.copy()
167 |
168 | if self.can_add(other):
169 | for op in other:
170 | tp.append(op.copy())
171 |
172 | return tp
173 |
174 | def __iadd__(self,
175 | other: 'Timepoint') -> 'Timepoint':
176 | """
177 | Add other Timepoint to this Timepoint.
178 |
179 | Parameters
180 | ----------
181 | other : Timepoint
182 | Timepoint to be added.
183 |
184 | Returns
185 | -------
186 | Timepoint
187 | Summed Timepoints.
188 |
189 | """
190 | if self.can_add(other):
191 | for op in other:
192 | self.append(op.copy())
193 |
194 | return self
195 |
--------------------------------------------------------------------------------
/stac/topologicalcodes/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abdullahkhalids/stac/2428f7c5e31123f4a1768fdbaa18e50ab471c805/stac/topologicalcodes/__init__.py
--------------------------------------------------------------------------------
/stac/topologicalcodes/colorcode.py:
--------------------------------------------------------------------------------
1 | """Module to provide color code."""
2 | from typing import Any, Optional
3 | from functools import partial
4 | import numpy as np
5 | import networkx as nx
6 | import bidict
7 | from ordered_set import OrderedSet
8 | import matplotlib.pyplot as plt
9 |
10 | from stac import Circuit
11 | from ..code import Code
12 | from .primallattice import PrimalLattice
13 |
14 |
15 | class ColorCode(Code):
16 | """Class for creating triangular color codes."""
17 |
18 | def __init__(self,
19 | distance: int,
20 | geometry: str = "hexagonal",
21 | color_order: list[str] = ['g', 'r', 'b']
22 | ) -> None:
23 | """
24 | Construct the color code of some geometry and distance.
25 |
26 | Parameters
27 | ----------
28 | distance : int
29 | The distance of the code.
30 | geometry : str, optional
31 | Describes the shape of the primal lattice. The default and only
32 | option currently is "hexagonal".
33 | color_order: str, optional
34 | Order of colors in the lattice.
35 | """
36 | self.color_order = color_order
37 | # lattice length
38 | L = int((distance-1)/2)
39 |
40 | # rows of hexagons
41 | rows = 3*L
42 |
43 | # primal graph
44 | self.primal_graph = nx.Graph()
45 | self.primal_graph.faces = dict()
46 | self.primal_graph.boundaries = dict()
47 | self.primal_graph.draw = self._primal_graph_draw
48 |
49 | # determine nodes and edges of primal graph
50 | self.primal_graph.nodes_index = bidict.bidict()
51 | kk = 0
52 | heights = [i for i in range(2, rows, 3)] + \
53 | [i for i in range(rows + 1, 0, -3)]
54 | row_nodes = [[] for i in range(rows+1)]
55 | for i in range(distance):
56 | for j in range(heights[i]):
57 | self.primal_graph.add_node((i, j),
58 | faces=OrderedSet())
59 | self.primal_graph.nodes_index[(i, j)] = kk
60 | kk += 1
61 | row_nodes[j].append((i, j))
62 | if j != 0:
63 | self.primal_graph.add_edge((i, j), (i, j-1))
64 |
65 | b = 0
66 | for row in row_nodes:
67 | for i in range(len(row)-1):
68 | if b:
69 | self.primal_graph.add_edge(row[i], row[i+1])
70 | b = (b+1) % 2
71 | b = (b+1) % 2
72 |
73 | # boundaries
74 | self.primal_graph.boundaries[2] = \
75 | OrderedSet([(i, 0) for i in range(distance)])
76 | self.primal_graph.boundaries[1] = \
77 | OrderedSet([row[0] for i, row in enumerate(row_nodes)
78 | if i % 3 in [0, 1]])
79 | self.primal_graph.boundaries[0] = \
80 | OrderedSet([row[-1] for i, row in enumerate(row_nodes)
81 | if i % 3 in [0, 2]])
82 |
83 | # faces
84 | f_heights = [i+1 for i in range(2, 3*L, 3)] + \
85 | [i+1 for i in range(3*L-2, 0, -3)]
86 | self.primal_graph.faces_index = bidict.bidict()
87 | kk = 0
88 |
89 | c_bot = 0
90 | c = 0
91 | for i in range(2*L):
92 | for j in range(c_bot, f_heights[i], 2):
93 | self.primal_graph.faces[(i, j)] = dict()
94 | if j == 0 and c == 0:
95 | self.primal_graph.faces[(i, j)]['form'] = 'bottom'
96 | elif i < L and j == f_heights[i]-1:
97 | self.primal_graph.faces[(i, j)]['form'] = 'right'
98 | elif i >= L and j == f_heights[i]-1:
99 | self.primal_graph.faces[(i, j)]['form'] = 'left'
100 | else:
101 | self.primal_graph.faces[(i, j)]['form'] = 'full'
102 |
103 | nodes = OrderedSet()
104 |
105 | for di, dj in [(1, 1), (1, 0), (1, -1),
106 | (0, -1), (0, 0), (0, 1)]:
107 | if j+dj < 0 or j+dj >= heights[i+di]:
108 | continue
109 | nodes.add((i+di, j+dj))
110 |
111 | self.primal_graph.faces[(i, j)]['nodes'] = nodes
112 | for v in nodes:
113 | self.primal_graph.nodes[v]['faces'].add((i, j))
114 |
115 | self.primal_graph.faces[(i, j)]['color'] = c
116 | c = (c+2) % 3
117 |
118 | self.primal_graph.faces_index[(i, j)] = kk
119 | kk += 1
120 |
121 | c_bot = (c_bot+1) % 2
122 | c = c_bot
123 |
124 | # add color to edges
125 | all_boundary_nodes = self.primal_graph.boundaries[0] | \
126 | self.primal_graph.boundaries[1] | \
127 | self.primal_graph.boundaries[2]
128 |
129 | # add color to all edges that terminate on boundaries
130 | for j in range(3):
131 | for v in self.primal_graph.boundaries[j]:
132 | for e in self.primal_graph.edges(v):
133 | if e[1] not in all_boundary_nodes:
134 | self.primal_graph.edges[e]['color'] = j
135 | # add color to edges from corner
136 | self.primal_graph.edges[(L, rows), (L, rows-1)]['color'] = 1
137 | self.primal_graph.edges[(0, 0), (0, 1)]['color'] = 2
138 | self.primal_graph.edges[(2*L, 0), (2*L-1, 0)]['color'] = 0
139 |
140 | # add color to edges in bulk
141 | for e in self.primal_graph.edges:
142 | if 'color' in self.primal_graph.edges[e]:
143 | continue
144 | face = self.primal_graph.nodes[e[0]]['faces'].difference(
145 | self.primal_graph.nodes[e[1]]['faces'])[0]
146 | self.primal_graph.edges[e]['color'] = \
147 | self.primal_graph.faces[face]['color']
148 |
149 | # corner colors
150 | self.primal_graph.corners = dict()
151 | self.primal_graph.corners[0] = (0, 0)
152 | self.primal_graph.corners[1] = (2*L, 0)
153 | self.primal_graph.corners[2] = (L, 3*L)
154 |
155 | self.primal_lattice = PrimalLattice(distance,
156 | self.primal_graph,
157 | color_order)
158 |
159 | # create the generator matrix
160 | n = int(3*(distance-1)*(distance+1)/4+1)
161 | mhalf = int((n-1)/2)
162 | H = np.zeros((mhalf, n), dtype=int)
163 |
164 | for i, f in self.primal_graph.faces_index.inv.items():
165 | for v in self.primal_graph.faces[f]['nodes']:
166 | v_ind = self.primal_graph.nodes_index[v]
167 | H[i, v_ind] = 1
168 |
169 | generator_matrix = np.zeros((2*mhalf, 2*n), dtype=int)
170 | generator_matrix[:mhalf, :n] = H
171 | generator_matrix[mhalf:, n:] = H
172 |
173 | super().__init__(generator_matrix)
174 | self.num_generators_x = mhalf
175 | self.num_generators_z = mhalf
176 | self.distance = distance
177 |
178 | def construct_logical_operators(self,
179 | method: str = "boundary: blue"
180 | ) -> (Any, Any):
181 | """
182 | Construct logical operators of the code.
183 |
184 | Parameters
185 | ----------
186 | method : str, optional
187 | With boundaries with color 0, 1, 2. The options are:
188 | "boundary: green"
189 | "boundary: red"
190 | "boundary: blue" (default)
191 | "gottesman" (generic method)
192 |
193 | Returns
194 | -------
195 | logical_xs: numpy.array
196 | Array of logical xs. Each row is an operator.
197 | logical_zs: numpy.array
198 | Array of logical xs. Each row is an operator.
199 | """
200 | if method == "boundary: green":
201 | c = self.primal_lattice.color_order.index('g')
202 | elif method == "boundary: red":
203 | c = self.primal_lattice.color_order.index('r')
204 | elif method == "boundary: blue":
205 | c = self.primal_lattice.color_order.index('b')
206 | else:
207 | return super().construct_logical_operators(method)
208 |
209 | oper_x = np.zeros(2*self.num_data_qubits, dtype=int)
210 | oper_z = np.zeros(2*self.num_data_qubits, dtype=int)
211 | for node in self.primal_graph.boundaries[c]:
212 | oper_x[self.primal_graph.nodes_index[node]] = 1
213 | oper_z[self.num_data_qubits +
214 | self.primal_graph.nodes_index[node]] = 1
215 |
216 | self.logical_xs = np.array([oper_x])
217 | self.logical_zs = np.array([oper_z])
218 |
219 | return self.logical_xs, self.logical_zs
220 |
221 | def _primal_graph_draw(self,
222 | draw_face_labels: bool = True
223 | ) -> None:
224 | """
225 | Draw the primal graph.
226 |
227 | Parameters
228 | ----------
229 | draw_face_labels : bool, optional
230 | Draw the face labels. The default is True.
231 | """
232 | # nicer labels
233 | if not hasattr(self.primal_graph, '_node_labels'):
234 | self.primal_graph._node_labels = dict()
235 | for node in self.primal_graph.nodes:
236 | self.primal_graph._node_labels[node] = str(node)[1:-1]
237 |
238 | nx.draw(self.primal_graph,
239 | pos=nx.get_node_attributes(self.primal_graph, 'pos_graph'),
240 | node_size=450,
241 | font_size=7,
242 | labels=self.primal_graph._node_labels,
243 | with_labels=True)
244 |
245 | if draw_face_labels:
246 | # nicer labels
247 | if not hasattr(self.primal_graph, '_face_labels'):
248 | self.primal_graph._face_labels = dict()
249 | for face in self.primal_graph.faces:
250 | self.primal_graph._face_labels[face] = str(face)[1:-1]
251 |
252 | plt.axis('off')
253 | pos = {f: val['pos_graph']
254 | for f, val in self.primal_graph.faces.items()}
255 |
256 | nx.draw_networkx_labels(self.primal_graph,
257 | pos=pos,
258 | labels=self.primal_graph._face_labels,
259 | font_size=7)
260 |
261 | def construct_dual_graph(self):
262 | """
263 | Construct the dual graph of the code.
264 |
265 | In the dual graph, the stabilizers are mapped onto the vertices and
266 | the qubits are mapped onto the faces. The stabilizers refer to both
267 | the set of pure X stabilizers of the code, and the pure Z ones. The
268 | vertices are colored, like the faces of the primal lattice.
269 | """
270 | self.dual_graph = nx.Graph()
271 | # nodes of dual graph are faces of primal
272 | for face, val in self.primal_graph.faces.items():
273 | pos = (-self.primal_lattice.x_shift + val['pos_lat'][0],
274 | self.primal_lattice.y_shift - val['pos_lat'][1])
275 | self.dual_graph.add_node(face,
276 | color=val['color'],
277 | pos_graph=pos,
278 | faces=val['nodes'])
279 | self.dual_graph.nodes_index = self.primal_graph.faces_index
280 |
281 | # now add edges between the new nodes
282 | for face in self.primal_graph.faces:
283 | for d in [(-1, -1), (-1, 1), (0, -2), (0, 2), (1, -1), (1, 1)]:
284 | connected_face = (face[0] + d[0], face[1] + d[1])
285 | if connected_face in self.primal_graph.faces:
286 | self.dual_graph.add_edge(face, connected_face)
287 |
288 | # color of nodes has to be a list for draw function
289 | self.dual_graph.node_colors = \
290 | [self.primal_lattice.color_map[self.primal_lattice.color_order[c]]
291 | for f, c in nx.get_node_attributes(
292 | self.dual_graph, 'color').items()]
293 |
294 | # now add faces, which are triangles
295 | self.dual_graph.faces = dict()
296 | self.dual_graph._face_labels = dict()
297 | for node in self.primal_graph.nodes:
298 | self.dual_graph.faces[node] = dict()
299 | self.dual_graph.faces[node]['nodes'] = \
300 | self.primal_graph.nodes[node]['faces']
301 | self.dual_graph.faces[node]['pos_graph'] = \
302 | self.primal_graph.nodes[node]['pos_graph']
303 | self.dual_graph._face_labels[node] = str(node)[1:-1]
304 | self.dual_graph.faces_index = self.primal_graph.nodes_index
305 |
306 | # edges index
307 | self.dual_graph.edges_index = bidict.bidict()
308 | kk = 0
309 | for e in self.dual_graph.edges:
310 | self.dual_graph.edges_index[frozenset(e)] = kk
311 | kk += 1
312 |
313 | # nicer labels
314 | self.dual_graph._node_labels = dict()
315 | for node in self.dual_graph.nodes:
316 | self.dual_graph._node_labels[node] = str(node)[1:-1]
317 | # draw
318 | self.dual_graph.draw = self._dual_graph_draw
319 |
320 | def _dual_graph_draw(self,
321 | draw_vertex_labels: bool = True,
322 | draw_face_labels: bool = True,
323 | edge_list: Optional[list] = None,
324 | highlight_nodes: Optional[list] = None,
325 | highlight_faces: Optional[list] = None
326 | ) -> None:
327 | """
328 | Draw the dual graph.
329 |
330 | Parameters
331 | ----------
332 | draw_vertex_labels : bool, optional
333 | Draw the vertex labels. The default is True.
334 | draw_face_labels : bool, optional
335 | Draw the face labels. The default is False.
336 | edge_list: list, optional
337 | List of edges to draw
338 | highlight_nodes: list, optional
339 | List of nodes to highlight.
340 | highlight_faces: list, optional
341 | List of faces to highlight.
342 | """
343 | plt.figure(figsize=(10, 8))
344 | plt.axis('off')
345 |
346 | pos = nx.get_node_attributes(self.dual_graph, 'pos_graph')
347 | if highlight_nodes:
348 | nx.draw_networkx_nodes(self.dual_graph,
349 | pos=pos,
350 | nodelist=highlight_nodes,
351 | node_size=450,
352 | node_shape='s',
353 | node_color='orange')
354 |
355 | if highlight_faces:
356 | pos_faces = {f: val['pos_graph']
357 | for f, val in self.dual_graph.faces.items()
358 | if f in highlight_faces}
359 | lbls = {f: ' '*len(self.dual_graph._face_labels[f])
360 | for f in highlight_faces}
361 | nx.draw_networkx_labels(self.dual_graph,
362 | pos=pos_faces,
363 | labels=lbls,
364 | bbox=dict(facecolor='cyan',
365 | linewidth=0),
366 | font_size=7)
367 |
368 | if not edge_list:
369 | edge_list = list(self.dual_graph.edges())
370 |
371 | nx.draw(self.dual_graph,
372 | pos=pos,
373 | node_color=self.dual_graph.node_colors,
374 | node_size=450,
375 | font_size=7,
376 | labels=self.dual_graph._node_labels,
377 | with_labels=True,
378 | edgelist=edge_list)
379 |
380 | if draw_face_labels:
381 | plt.axis('off')
382 | pos = {f: val['pos_graph']
383 | for f, val in self.dual_graph.faces.items()}
384 | # nx.draw_networkx_nodes(self.dual_graph,
385 | # pos=pos,
386 | # node_color='white')
387 | nx.draw_networkx_labels(self.dual_graph,
388 | pos=pos,
389 | labels=self.dual_graph._face_labels,
390 | font_size=7)
391 |
392 | def construct_restricted_graphs(self):
393 | """
394 | Construct the restricted graphs.
395 |
396 | There are three restricted graphs. Each is built by omitting vertices
397 | of one color from the dual graph.
398 |
399 | The graphs are stored in the dictionary `self.restricted_graphs`. There
400 | are three keys for this dictionary, (0, 1), (0, 2), and (1, 2),
401 | referring to the colors that are included in the graph.
402 | """
403 | self.restricted_graphs = dict()
404 | for c1, c2 in [(0, 1), (0, 2), (1, 2)]:
405 | self.restricted_graphs[c1, c2] = nx.Graph()
406 |
407 | # add nodes
408 | for node, val in self.dual_graph.nodes.items():
409 | if val['color'] in [c1, c2]:
410 | self.restricted_graphs[c1, c2].add_node(node, **val)
411 |
412 | # add edges
413 | for node in self.dual_graph.nodes:
414 | if self.dual_graph.nodes[node]['color'] != c1:
415 | continue
416 | for e in nx.edges(self.dual_graph, node):
417 | if self.dual_graph.nodes[e[1]]['color'] == c2:
418 | self.restricted_graphs[c1, c2].add_edge(*e)
419 | self.restricted_graphs[c1, c2].edges[e]['faces'] = \
420 | self.primal_graph.faces[e[0]]['nodes'] & \
421 | self.primal_graph.faces[e[1]]['nodes']
422 |
423 | self.restricted_graphs[c1, c2].node_colors = \
424 | [self.primal_lattice.color_map[
425 | self.primal_lattice.color_order[c]]
426 | for f, c in nx.get_node_attributes(
427 | self.restricted_graphs[c1, c2], 'color').items()]
428 |
429 | self.restricted_graphs[c1, c2].draw = partial(
430 | self._restricted_graph_draw, (c1, c2))
431 | self.restricted_graphs[c1, c2].draw.__doc__ = \
432 | """\
433 | Draw this restricted graph.
434 |
435 | Parameters
436 | ----------
437 | draw_edge_labels : bool, optional
438 | Draw the edge labels. The default is False.
439 | """
440 |
441 | def _restricted_graph_draw(self,
442 | label: tuple,
443 | draw_edge_labels: bool = False):
444 | """
445 | Draw a restricted graph.
446 |
447 | Parameters
448 | ----------
449 | label : tuple
450 | The label of the restricted graph: (0, 1), (0, 2) or (1, 2).
451 | draw_edge_labels : bool, optional
452 | Draw the edge labels. The default is False.
453 | """
454 | node_labels = dict()
455 | for node in self.restricted_graphs[label].nodes:
456 | node_labels[node] = str(node)[1:-1]
457 |
458 | pos = nx.get_node_attributes(
459 | self.restricted_graphs[label], 'pos_graph')
460 |
461 | nx.draw(self.restricted_graphs[label],
462 | pos=pos,
463 | node_color=self.restricted_graphs[label].node_colors,
464 | node_size=450,
465 | font_size=7,
466 | labels=node_labels,
467 | with_labels=True)
468 |
469 | if draw_edge_labels:
470 | edge_labels = dict()
471 |
472 | for e, val in self.restricted_graphs[label].edges.items():
473 | fs = list(val['faces'])
474 | edge_labels[e] = str(fs[0])[1:-1] + '\n' + str(fs[1])[1:-1]
475 |
476 | nx.draw_networkx_edge_labels(self.restricted_graphs[label],
477 | pos=pos,
478 | edge_labels=edge_labels,
479 | font_size=7,
480 | verticalalignment='center_baseline')
481 |
--------------------------------------------------------------------------------
/stac/topologicalcodes/primallattice.py:
--------------------------------------------------------------------------------
1 | """Module to provide primal lattice for color codes."""
2 | from typing import Optional
3 | import numpy as np
4 | import svg
5 | from IPython.display import display, SVG
6 |
7 |
8 | class PrimalLattice:
9 | """Primal lattice for color codes."""
10 |
11 | def __init__(self,
12 | distance,
13 | primal_graph,
14 | color_order):
15 | self.color_order = color_order
16 | self.primal_graph = primal_graph
17 |
18 | # lattice length
19 | self.lattice_length = int((distance-1)/2)
20 |
21 | # cols and rows in the grid
22 | self.face_cols = 2*self.lattice_length
23 | self.face_rows = 3*self.lattice_length
24 |
25 | # size of hexagon
26 | self.hexagon_size = 40
27 |
28 | # horizontal separation between hexagons
29 | self.hor_sep = self.hexagon_size*3/2
30 | # vertical separation between centers of diagonally stacked hexagons
31 | self.ver_sep = self.hexagon_size*np.sqrt(3)/2
32 |
33 | self.length_scale = np.linalg.norm((self.hor_sep, self.ver_sep))
34 |
35 | # shift grid to keep in svg frame
36 | self.x_shift = self.hexagon_size*2
37 | self.y_shift = 3*self.lattice_length*self.ver_sep + self.hexagon_size/2
38 |
39 | # color map
40 | self.color_map = {'r': '#FA8072', 'g': '#33FF93', 'b': '#069AF3'}
41 |
42 | # face coordinates
43 | for f in primal_graph.faces:
44 | x = self.hexagon_size + self.hor_sep*f[0]
45 | y = self.ver_sep*f[1]
46 | primal_graph.faces[f]['pos_graph'] = (x, y)
47 | primal_graph.faces[f]['pos_lat'] = \
48 | (self.x_shift + x,
49 | self.y_shift - y)
50 |
51 | # vertex coordinates
52 | def xe(i):
53 | return self.hexagon_size * (-1)**i * (-1 + (-1)**i * (1 + 6*i))/4
54 | for node in primal_graph.nodes:
55 | x = xe(node[0])
56 | if node[1] % 2 == 1:
57 | x = x + self.hexagon_size/2 if node[0] % 2 == 0 \
58 | else x - self.hexagon_size/2
59 | y = node[1] * self.ver_sep
60 | primal_graph.nodes[node]['pos_graph'] = (x, y)
61 | primal_graph.nodes[node]['pos_lat'] = \
62 | (self.x_shift + x, self.y_shift - y)
63 |
64 | def _create_face_svg(self,
65 | face):
66 | """
67 | Create an svg.Polygon object for a face.
68 |
69 | Parameters
70 | ----------
71 | x0 : float
72 | Horizontal position of center.
73 | y0 : float
74 | Vertical position of center.
75 | color : str
76 | Options are r, g or b.
77 |
78 | Returns
79 | -------
80 | pg : svg.Polygon
81 | The svg.Polygon object of the hexagon.
82 |
83 | """
84 | coords = [self.primal_graph.nodes[node]['pos_lat']
85 | for node in self.primal_graph.faces[face]['nodes']]
86 |
87 | pts = [a for p in coords for a in p]
88 |
89 | c = self.color_order[self.primal_graph.faces[face]['color']]
90 |
91 | pg = svg.Polygon(
92 | points=pts,
93 | fill=self.color_map[c],
94 | stroke="black",
95 | stroke_width=1,
96 | stroke_linejoin="round",
97 | )
98 | return pg
99 |
100 | def setup_draw(self,
101 | draw_boundaries: bool = False,
102 | draw_vertex_labels: Optional[int] = None,
103 | draw_face_labels: Optional[int] = None
104 | ) -> None:
105 | """
106 | Set the options for drawing the primal lattice.
107 |
108 | The `draw` function can be used to display the lattice.
109 |
110 | Parameters
111 | ----------
112 | draw_boundaries : bool, optional
113 | Draw the boundaries of the lattice. The default is False.
114 | draw_vertex_labels : Optional[int], optional
115 | Draw the vertex labels. The default is None.
116 | draw_face_labels : Optional[int], optional
117 | Draw the face labels. The default is None.
118 | """
119 | self._svg_els = []
120 |
121 | # draw the boundaries first
122 | if draw_boundaries:
123 | right_corner_x = self.face_cols * self.hor_sep + self.x_shift
124 | pts = [self.x_shift, self.y_shift+5,
125 | self.x_shift, self.y_shift,
126 | right_corner_x, self.y_shift,
127 | right_corner_x, self.y_shift+5]
128 | # bottom boundary
129 | self._svg_els.append(
130 | svg.Polygon(
131 | points=pts,
132 | fill='blue',
133 | stroke="black",
134 | stroke_width=1,
135 | stroke_linejoin="round",
136 |
137 | ))
138 | # left boundary
139 | self._svg_els.append(
140 | svg.Polygon(
141 | points=pts,
142 | fill='red',
143 | stroke="black",
144 | stroke_width=1,
145 | stroke_linejoin="round",
146 | transform=f'rotate(-60 {self.x_shift-5} {self.y_shift})'
147 | ))
148 | # right boundary
149 | self._svg_els.append(
150 | svg.Polygon(
151 | points=pts,
152 | fill='green',
153 | stroke="black",
154 | stroke_width=1,
155 | stroke_linejoin="round",
156 | transform=f'rotate(60 {right_corner_x+5} {self.y_shift})'
157 | ))
158 | for face in self.primal_graph.faces:
159 | self._svg_els.append(
160 | self._create_face_svg(face)
161 | )
162 |
163 | if type(draw_vertex_labels) is int:
164 | face_color = [0, 1, 2] if draw_vertex_labels == 3 \
165 | else [draw_vertex_labels]
166 | included_faces = [f for f in self.primal_graph.faces
167 | if self.primal_graph.faces[f]['color']
168 | in face_color]
169 | for face in included_faces:
170 | for node in self.primal_graph.faces[face]['nodes']:
171 | node_pos = self.primal_graph.nodes[node]['pos_lat']
172 | self._svg_els.append(
173 | svg.Text(x=node_pos[0], y=node_pos[1],
174 | text=f'{node[0]},{node[1]}',
175 | font_size=8,
176 | text_anchor='middle'))
177 |
178 | if type(draw_face_labels) is int:
179 | face_color = [0, 1, 2] if draw_face_labels == 3 \
180 | else [draw_face_labels]
181 | included_faces = [f for f in self.primal_graph.faces
182 | if self.primal_graph.faces[f]['color']
183 | in face_color]
184 | for face in included_faces:
185 | face_pos = self.primal_graph.faces[face]['pos_lat']
186 | self._svg_els.append(svg.Text(x=face_pos[0], y=face_pos[1],
187 | text=f'{face[0]},{face[1]}',
188 | font_size=10,
189 | text_anchor='middle'))
190 |
191 | def label_vertex(self,
192 | label: str,
193 | node: tuple) -> None:
194 | """
195 | Label a vertex on the lattice to be drawn.
196 |
197 | Parameters
198 | ----------
199 | label : str
200 | Label to include. One character for nice display.
201 | node : tuple
202 | The address of the node at which to place the label..
203 | """
204 | node_pos = self.primal_graph.nodes[node]['pos_lat']
205 | self._svg_els.append(
206 | svg.Circle(cx=node_pos[0], cy=node_pos[1],
207 | r=8,
208 | fill='white',
209 | stroke=None))
210 | self._svg_els.append(
211 | svg.Text(x=node_pos[0], y=node_pos[1]+2.5,
212 | text=label,
213 | font_size=10,
214 | text_anchor='middle'))
215 |
216 | def label_operator(self,
217 | operator: np.ndarray
218 | ) -> None:
219 | """
220 | Label an operator on the lattice to be drawn.
221 |
222 | Parameters
223 | ----------
224 | operator : np.ndarray
225 | A one-dimensional numpy array of the operator. with length twice
226 | the number of qubits in the code. Entries should be 0 or 1.
227 | """
228 |
229 | n = int(len(operator)/2)
230 | for i in range(n):
231 | if operator[i] and not operator[n+i]:
232 | self.label_vertex('X', self.primal_graph.nodes_index.inv[i])
233 | elif operator[i] and operator[n+i]:
234 | self.label_vertex('Y', self.primal_graph.nodes_index.inv[i])
235 | elif not operator[i] and operator[n+i]:
236 | self.label_vertex('Z', self.primal_graph.nodes_index.inv[i])
237 |
238 | def label_face(self,
239 | label: str,
240 | face: tuple
241 | ) -> None:
242 | """
243 | Label a face on the lattice to be drawn.
244 |
245 | Parameters
246 | ----------
247 | label : str
248 | The string to be placed on the face.
249 | face : tuple
250 | The address of the face which is to be labelled.
251 | """
252 | node_pos = self.primal_graph.faces[face]['pos_lat']
253 | self._svg_els.append(
254 | svg.Rect(x=node_pos[0]-5, y=node_pos[1]-5,
255 | width=10, height=10,
256 | fill='white',
257 | stroke=None))
258 | self._svg_els.append(
259 | svg.Text(x=node_pos[0], y=node_pos[1]+2.5,
260 | text=label,
261 | font_size=10,
262 | text_anchor='middle'))
263 |
264 | def label_syndrome(self,
265 | syndrome: np.ndarray
266 | ) -> None:
267 | """
268 | Label a syndrome on the lattice to be drawn.
269 |
270 | Parameters
271 | ----------
272 | syndrome : np.ndarray
273 | A one-dimensional numpy array. Length should be equal to the number
274 | of generators of the code. Entries should be 0 or 1.
275 | """
276 | m = int(len(syndrome)/2)
277 | for i in range(m):
278 | if syndrome[i] and not syndrome[m+i]:
279 | self.label_face('X', self.primal_graph.faces_index.inv[i])
280 | elif syndrome[i] and syndrome[m+i]:
281 | self.label_face('Y', self.primal_graph.faces_index.inv[i])
282 | elif not syndrome[i] and syndrome[m+i]:
283 | self.label_face('Z', self.primal_graph.faces_index.inv[i])
284 |
285 | def draw(self
286 | ) -> None:
287 | """Display the primal lattice with any labels put on it."""
288 | img = svg.SVG(
289 | width=self.face_cols*self.hor_sep + 3*self.hexagon_size,
290 | height=self.face_rows*self.ver_sep + 1*self.hexagon_size,
291 | elements=self._svg_els)
292 | display(SVG(img.as_str()))
293 |
--------------------------------------------------------------------------------
/tests/circuit_test.py:
--------------------------------------------------------------------------------
1 | import stac
2 |
3 |
4 | def test_circuit_init():
5 | circ = stac.Circuit()
6 | assert type(circ) == stac.circuit.Circuit
7 |
8 |
9 | def test_circuit_init2():
10 | circ = stac.Circuit.simple(3)
11 | assert type(circ) == stac.circuit.Circuit
12 |
13 |
14 | cd = stac.CommonCodes.generate_code('[[7,1,3]]')
15 | circ = stac.Circuit()
16 | circ.append_register(cd.construct_data_register(0))
17 | circ.append_register(cd.construct_syndrome_measurement_register(0, 'non_ft'))
18 |
19 |
20 | def test_append_basic():
21 | circ.append('H', (0, 0, 0))
22 | circ.append('CX', (0, 0, 1), (0, 1, 0, 0))
23 | assert circ.__repr__() == '0 H (0, 0, 0)\n CX (0, 0, 1) (0, 1, 0, 0)'
24 |
25 |
26 | def test_append_next_timepoint():
27 | circ.append('H', (0, 0, 1))
28 | circ.append('X', (0, 0, 2), time=[1])
29 | assert circ.__repr__() == '0 H (0, 0, 0)\n CX (0, 0, 1) (0, 1, 0, 0)\n1 H (0, 0, 1)\n2 X (0, 0, 2)'
30 |
31 |
32 | def test_append_past():
33 | circ.append('Y', (0, 0, 3), time=1)
34 | assert circ.__repr__() == '0 H (0, 0, 0)\n CX (0, 0, 1) (0, 1, 0, 0)\n1 H (0, 0, 1)\n Y (0, 0, 3)\n2 X (0, 0, 2)'
35 |
36 |
37 | def test_append_future():
38 | circ.append('Z', (0, 0, 4), time=6)
39 | assert circ.__repr__() == '0 H (0, 0, 0)\n CX (0, 0, 1) (0, 1, 0, 0)\n1 H (0, 0, 1)\n Y (0, 0, 3)\n2 X (0, 0, 2)\n3\n4\n5\n6 Z (0, 0, 4)'
40 |
41 |
42 | def test_append_set_cur_time():
43 | circ.cur_time = 4
44 | circ.append('CY', (0, 0, 2), (0, 0, 5))
45 | assert circ.__repr__() == '0 H (0, 0, 0)\n CX (0, 0, 1) (0, 1, 0, 0)\n1 H (0, 0, 1)\n Y (0, 0, 3)\n2 X (0, 0, 2)\n3\n4 CY (0, 0, 2) (0, 0, 5)\n5\n6 Z (0, 0, 4)'
46 |
47 |
48 | def test_append_parameterized_gate():
49 | circ.append('RX', (0, 0, 5), 0.3)
50 | assert circ.__repr__() == '0 H (0, 0, 0)\n CX (0, 0, 1) (0, 1, 0, 0)\n1 H (0, 0, 1)\n Y (0, 0, 3)\n2 X (0, 0, 2)\n3\n4 CY (0, 0, 2) (0, 0, 5)\n5 RX(0.3) (0, 0, 5)\n6 Z (0, 0, 4)'
51 |
--------------------------------------------------------------------------------