hugr 0.1.0a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hugr-0.1.0a1/PKG-INFO ADDED
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.1
2
+ Name: hugr
3
+ Version: 0.1.0a1
4
+ Summary: Quantinuum's common representation for quantum programs
5
+ Home-page: https://github.com/CQCL/hugr
6
+ License: Apache-2.0
7
+ Author: TKET development team
8
+ Author-email: tket-support@cambridgequantum.com
9
+ Maintainer: TKET development team
10
+ Maintainer-email: tket-support@cambridgequantum.com
11
+ Requires-Python: >=3.10
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Operating System :: MacOS :: MacOS X
17
+ Classifier: Operating System :: Microsoft :: Windows
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Scientific/Engineering
24
+ Requires-Dist: pydantic (>=2.6.4,<3.0.0)
25
+ Project-URL: Repository, https://github.com/CQCL/hugr
26
+ Description-Content-Type: text/markdown
27
+
28
+ hugr
29
+ ===============
30
+
31
+ [![build_status][]](https://github.com/CQCL/hugr/actions)
32
+ [![codecov][]](https://codecov.io/gh/CQCL/hugr)
33
+
34
+ The Hierarchical Unified Graph Representation (HUGR, pronounced _hugger_) is the
35
+ common representation of quantum circuits and operations in the Quantinuum
36
+ ecosystem.
37
+
38
+ This library provides a pure-python implementation of the HUGR data model, and
39
+ a low-level API for constructing HUGR objects.
40
+
41
+ This library is intended to be used as a dependency for other high-level tools.
42
+ See [`guppylang`][] and [`tket2`][] for examples of such tools.
43
+
44
+ The HUGR specification is [here](https://github.com/CQCL/hugr/blob/main/specification/hugr.md).
45
+
46
+ [`guppylang`]: https://pypi.org/project/guppylang/
47
+ [`tket2`]: https://github.com/CQCL/tket2
48
+
49
+
50
+ ## Installation
51
+
52
+ The package name is `hugr`. It can be installed from PyPI:
53
+ ```bash
54
+ pip install hugr
55
+ ```
56
+
57
+ The current releases are in alpha stage, and the API is subject to change.
58
+
59
+ ## Usage
60
+
61
+ TODO
62
+
63
+ ## Recent Changes
64
+
65
+ TODO
66
+
67
+ ## Development
68
+
69
+ TODO
70
+
71
+ ## License
72
+
73
+ This project is licensed under Apache License, Version 2.0 ([LICENSE][] or http://www.apache.org/licenses/LICENSE-2.0).
74
+
75
+ [build_status]: https://github.com/CQCL/hugr/actions/workflows/ci-py.yml/badge.svg?branch=main
76
+ [codecov]: https://img.shields.io/codecov/c/gh/CQCL/hugr?logo=codecov
77
+ [LICENSE]: https://github.com/CQCL/hugr/blob/main/LICENCE
78
+ [CHANGELOG]: https://github.com/CQCL/hugr/blob/main/hugr-py/CHANGELOG.md
79
+
hugr-0.1.0a1/README.md ADDED
@@ -0,0 +1,51 @@
1
+ hugr
2
+ ===============
3
+
4
+ [![build_status][]](https://github.com/CQCL/hugr/actions)
5
+ [![codecov][]](https://codecov.io/gh/CQCL/hugr)
6
+
7
+ The Hierarchical Unified Graph Representation (HUGR, pronounced _hugger_) is the
8
+ common representation of quantum circuits and operations in the Quantinuum
9
+ ecosystem.
10
+
11
+ This library provides a pure-python implementation of the HUGR data model, and
12
+ a low-level API for constructing HUGR objects.
13
+
14
+ This library is intended to be used as a dependency for other high-level tools.
15
+ See [`guppylang`][] and [`tket2`][] for examples of such tools.
16
+
17
+ The HUGR specification is [here](https://github.com/CQCL/hugr/blob/main/specification/hugr.md).
18
+
19
+ [`guppylang`]: https://pypi.org/project/guppylang/
20
+ [`tket2`]: https://github.com/CQCL/tket2
21
+
22
+
23
+ ## Installation
24
+
25
+ The package name is `hugr`. It can be installed from PyPI:
26
+ ```bash
27
+ pip install hugr
28
+ ```
29
+
30
+ The current releases are in alpha stage, and the API is subject to change.
31
+
32
+ ## Usage
33
+
34
+ TODO
35
+
36
+ ## Recent Changes
37
+
38
+ TODO
39
+
40
+ ## Development
41
+
42
+ TODO
43
+
44
+ ## License
45
+
46
+ This project is licensed under Apache License, Version 2.0 ([LICENSE][] or http://www.apache.org/licenses/LICENSE-2.0).
47
+
48
+ [build_status]: https://github.com/CQCL/hugr/actions/workflows/ci-py.yml/badge.svg?branch=main
49
+ [codecov]: https://img.shields.io/codecov/c/gh/CQCL/hugr?logo=codecov
50
+ [LICENSE]: https://github.com/CQCL/hugr/blob/main/LICENCE
51
+ [CHANGELOG]: https://github.com/CQCL/hugr/blob/main/hugr-py/CHANGELOG.md
@@ -0,0 +1,32 @@
1
+ [tool.poetry]
2
+ classifiers = [
3
+ "Environment :: Console",
4
+ "Programming Language :: Python :: 3.10",
5
+ "Programming Language :: Python :: 3.11",
6
+ "Programming Language :: Python :: 3.12",
7
+ "License :: OSI Approved :: Apache Software License",
8
+ "Operating System :: MacOS :: MacOS X",
9
+ "Operating System :: POSIX :: Linux",
10
+ "Operating System :: Microsoft :: Windows",
11
+ "Intended Audience :: Developers",
12
+ "Intended Audience :: Science/Research",
13
+ "Topic :: Scientific/Engineering",
14
+ ]
15
+ name = "hugr"
16
+ version = "0.1.0a1"
17
+ description = "Quantinuum's common representation for quantum programs"
18
+ #keywords = []
19
+ authors = ["TKET development team <tket-support@cambridgequantum.com>"]
20
+ maintainers = ["TKET development team <tket-support@cambridgequantum.com>"]
21
+ license = "Apache-2.0"
22
+ readme = "README.md"
23
+ homepage = "https://github.com/CQCL/hugr"
24
+ repository = "https://github.com/CQCL/hugr"
25
+
26
+ [tool.poetry.dependencies]
27
+ python = ">=3.10"
28
+ pydantic = "^2.6.4"
29
+
30
+ [tool.pytest.ini_options]
31
+ # Lark throws deprecation warnings for `src_parse` and `src_constants`.
32
+ filterwarnings = "ignore::DeprecationWarning:lark.*"
@@ -0,0 +1,8 @@
1
+ """`hugr` is a Python package for the Quantinuum HUGR common
2
+ representation.
3
+ """
4
+
5
+
6
+ def it_works() -> str:
7
+ """Return a string to confirm that the package is installed and working."""
8
+ return "It works!"
@@ -0,0 +1,3 @@
1
+ from .serial_hugr import SerialHugr
2
+
3
+ __all__ = ["SerialHugr"]
@@ -0,0 +1,557 @@
1
+ import inspect
2
+ import sys
3
+ from abc import ABC
4
+ from typing import Any, Literal, cast
5
+
6
+ from pydantic import BaseModel, Field, RootModel
7
+
8
+ from . import tys
9
+ from .tys import (
10
+ ExtensionId,
11
+ ExtensionSet,
12
+ FunctionType,
13
+ PolyFuncType,
14
+ Type,
15
+ TypeRow,
16
+ )
17
+
18
+ NodeID = int
19
+
20
+
21
+ class BaseOp(ABC, BaseModel):
22
+ """Base class for ops that store their node's input/output types"""
23
+
24
+ # Parent node index of node the op belongs to, used only at serialization time
25
+ parent: NodeID
26
+ input_extensions: ExtensionSet = Field(default_factory=ExtensionSet)
27
+
28
+ def insert_port_types(self, in_types: TypeRow, out_types: TypeRow) -> None:
29
+ """Hook to insert type information from the input and output ports into the
30
+ op"""
31
+
32
+ def insert_child_dfg_signature(self, inputs: TypeRow, outputs: TypeRow) -> None:
33
+ """Hook to insert type information from a child dataflow graph"""
34
+
35
+ def display_name(self) -> str:
36
+ """Name of the op for visualisation"""
37
+ return self.__class__.__name__
38
+
39
+
40
+ # ----------------------------------------------------------
41
+ # --------------- Module level operations ------------------
42
+ # ----------------------------------------------------------
43
+
44
+
45
+ class Module(BaseOp):
46
+ """The root of a module, parent of all other `ModuleOp`s."""
47
+
48
+ op: Literal["Module"] = "Module"
49
+
50
+
51
+ class FuncDefn(BaseOp):
52
+ """A function definition. Children nodes are the body of the definition."""
53
+
54
+ op: Literal["FuncDefn"] = "FuncDefn"
55
+
56
+ name: str
57
+ signature: PolyFuncType = Field(default_factory=PolyFuncType.empty)
58
+
59
+ def insert_port_types(self, in_types: TypeRow, out_types: TypeRow) -> None:
60
+ assert len(in_types) == 0
61
+ assert len(out_types) == 1
62
+ out = out_types[0]
63
+ assert isinstance(out, PolyFuncType)
64
+ self.signature = out # TODO: Extensions
65
+
66
+
67
+ class FuncDecl(BaseOp):
68
+ """External function declaration, linked at runtime."""
69
+
70
+ op: Literal["FuncDecl"] = "FuncDecl"
71
+ name: str
72
+ signature: PolyFuncType = Field(default_factory=PolyFuncType.empty)
73
+
74
+ def insert_port_types(self, in_types: TypeRow, out_types: TypeRow) -> None:
75
+ assert len(in_types) == 0
76
+ assert len(out_types) == 1
77
+ out = out_types[0]
78
+ assert isinstance(out, PolyFuncType)
79
+ self.signature = out
80
+
81
+
82
+ class ConstBase(BaseOp):
83
+ """A constant operation definition."""
84
+
85
+ op: Literal["Const"] = "Const"
86
+
87
+
88
+ CustomConst = Any # TODO
89
+
90
+
91
+ class ExtensionConst(ConstBase):
92
+ """An extension constant value, that can check it is of a given [CustomType]."""
93
+
94
+ c: Literal["Extension"] = Field("Extension", title="ConstTag")
95
+ e: CustomConst = Field(title="CustomConst")
96
+
97
+ class Config:
98
+ json_schema_extra = {
99
+ "required": ["parent", "op", "c", "e"],
100
+ }
101
+
102
+
103
+ class FunctionConst(ConstBase):
104
+ """A higher-order function value."""
105
+
106
+ c: Literal["Function"] = Field("Function", title="ConstTag")
107
+ hugr: Any # TODO
108
+
109
+ class Config:
110
+ json_schema_extra = {
111
+ "required": ["parent", "op", "c", "hugr"],
112
+ }
113
+
114
+
115
+ class Tuple(ConstBase):
116
+ """A constant tuple value."""
117
+
118
+ c: Literal["Tuple"] = Field("Tuple", title="ConstTag")
119
+ vs: list["Const"]
120
+
121
+ class Config:
122
+ json_schema_extra = {
123
+ "required": ["parent", "op", "c", "vs"],
124
+ }
125
+
126
+
127
+ class Sum(ConstBase):
128
+ """A Sum variant
129
+
130
+ For any Sum type where this value meets the type of the variant indicated by the tag
131
+ """
132
+
133
+ c: Literal["Sum"] = Field("Sum", title="ConstTag")
134
+ tag: int
135
+ typ: Type
136
+ vs: list["Const"]
137
+
138
+ class Config:
139
+ # Needed to avoid random '\n's in the pydantic description
140
+ json_schema_extra = {
141
+ "description": (
142
+ "A Sum variant For any Sum type where this value meets the type "
143
+ "of the variant indicated by the tag."
144
+ ),
145
+ "required": ["parent", "op", "c", "tag", "typ", "vs"],
146
+ }
147
+
148
+
149
+ class Const(RootModel):
150
+ """A constant operation."""
151
+
152
+ root: ExtensionConst | FunctionConst | Tuple | Sum = Field(discriminator="c")
153
+
154
+
155
+ # -----------------------------------------------
156
+ # --------------- BasicBlock types ------------------
157
+ # -----------------------------------------------
158
+
159
+
160
+ class DataflowBlock(BaseOp):
161
+ """A CFG basic block node. The signature is that of the internal Dataflow
162
+ graph."""
163
+
164
+ op: Literal["DataflowBlock"] = "DataflowBlock"
165
+ inputs: TypeRow = Field(default_factory=list)
166
+ other_outputs: TypeRow = Field(default_factory=list)
167
+ sum_rows: list[TypeRow] = Field(default_factory=list)
168
+ extension_delta: ExtensionSet = Field(default_factory=list)
169
+
170
+ def insert_port_types(self, in_types: TypeRow, out_types: TypeRow) -> None:
171
+ num_cases = len(out_types)
172
+ self.sum_rows = [[] for _ in range(num_cases)]
173
+
174
+ def insert_child_dfg_signature(self, inputs: TypeRow, outputs: TypeRow) -> None:
175
+ self.inputs = inputs
176
+ pred = outputs[0]
177
+ assert isinstance(pred, tys.UnitSum | tys.GeneralSum)
178
+ if isinstance(pred, tys.UnitSum):
179
+ self.sum_rows = [[] for _ in range(cast(tys.UnitSum, pred).size)]
180
+ else:
181
+ self.sum_rows = []
182
+ for variant in pred.rows:
183
+ self.sum_rows.append(variant)
184
+ self.other_outputs = outputs[1:]
185
+
186
+ class Config:
187
+ # Needed to avoid random '\n's in the pydantic description
188
+ json_schema_extra = {
189
+ "required": [
190
+ "parent",
191
+ "op",
192
+ "inputs",
193
+ "other_outputs",
194
+ "sum_rows",
195
+ "extension_delta",
196
+ ],
197
+ "description": "A CFG basic block node. The signature is that of the internal Dataflow graph.",
198
+ }
199
+
200
+
201
+ class ExitBlock(BaseOp):
202
+ """The single exit node of the CFG, has no children, stores the types of
203
+ the CFG node output."""
204
+
205
+ op: Literal["ExitBlock"] = "ExitBlock"
206
+ cfg_outputs: TypeRow
207
+
208
+ class Config:
209
+ # Needed to avoid random '\n's in the pydantic description
210
+ json_schema_extra = {
211
+ "description": "The single exit node of the CFG, has no children, stores the types of the CFG node output."
212
+ }
213
+
214
+
215
+ # ---------------------------------------------
216
+ # --------------- DataflowOp ------------------
217
+ # ---------------------------------------------
218
+
219
+
220
+ class DataflowOp(BaseOp):
221
+ pass
222
+
223
+
224
+ class Input(DataflowOp):
225
+ """An input node. The outputs of this node are the inputs to the parent node."""
226
+
227
+ op: Literal["Input"] = "Input"
228
+ types: TypeRow = Field(default_factory=list)
229
+
230
+ def insert_port_types(self, in_types: TypeRow, out_types: TypeRow) -> None:
231
+ assert len(in_types) == 0
232
+ self.types = list(out_types)
233
+
234
+
235
+ class Output(DataflowOp):
236
+ """An output node. The inputs are the outputs of the function."""
237
+
238
+ op: Literal["Output"] = "Output"
239
+ types: TypeRow = Field(default_factory=list)
240
+
241
+ def insert_port_types(self, in_types: TypeRow, out_types: TypeRow) -> None:
242
+ assert len(out_types) == 0
243
+ self.types = list(in_types)
244
+
245
+
246
+ class Call(DataflowOp):
247
+ """
248
+ Call a function directly.
249
+
250
+ The first port is connected to the def/declare of the function being called
251
+ directly, with a `ConstE<Graph>` edge. The signature of the remaining ports matches
252
+ the function being called.
253
+ """
254
+
255
+ op: Literal["Call"] = "Call"
256
+ signature: FunctionType = Field(default_factory=FunctionType.empty)
257
+
258
+ def insert_port_types(self, in_types: TypeRow, out_types: TypeRow) -> None:
259
+ # The constE edge comes after the value inputs
260
+ fun_ty = in_types[-1]
261
+ assert isinstance(fun_ty, PolyFuncType)
262
+ poly_func = cast(PolyFuncType, fun_ty)
263
+ assert len(poly_func.params) == 0
264
+ self.signature = poly_func.body
265
+
266
+ class Config:
267
+ # Needed to avoid random '\n's in the pydantic description
268
+ json_schema_extra = {
269
+ "description": (
270
+ "Operation to call a function directly. The first port is "
271
+ "connected to the def/declare of the function being called directly, "
272
+ "with a `Static<FunctionType>` edge. The signature of the remaining "
273
+ "ports matches the function being called."
274
+ )
275
+ }
276
+
277
+
278
+ class CallIndirect(DataflowOp):
279
+ """Call a function indirectly.
280
+
281
+ Like call, but the first input is a standard dataflow graph type."""
282
+
283
+ op: Literal["CallIndirect"] = "CallIndirect"
284
+ signature: FunctionType = Field(default_factory=FunctionType.empty)
285
+
286
+ def insert_port_types(self, in_types: TypeRow, out_types: TypeRow) -> None:
287
+ fun_ty = in_types[0]
288
+ assert isinstance(fun_ty, PolyFuncType)
289
+ poly_func = cast(PolyFuncType, fun_ty)
290
+ assert len(poly_func.params) == 0
291
+ assert len(poly_func.body.input) == len(in_types) - 1
292
+ assert len(poly_func.body.output) == len(out_types)
293
+ self.signature = poly_func.body
294
+
295
+
296
+ class LoadConstant(DataflowOp):
297
+ """An operation that loads a static constant in to the local dataflow graph."""
298
+
299
+ op: Literal["LoadConstant"] = "LoadConstant"
300
+ datatype: Type
301
+
302
+
303
+ class LeafOpBase(DataflowOp):
304
+ """Simple operation that has only value inputs+outputs and (potentially) StateOrder
305
+ edges."""
306
+
307
+ op: Literal["LeafOp"] = "LeafOp"
308
+
309
+
310
+ class DFG(DataflowOp):
311
+ """A simply nested dataflow graph."""
312
+
313
+ op: Literal["DFG"] = "DFG"
314
+ signature: FunctionType = Field(default_factory=FunctionType.empty)
315
+
316
+ def insert_child_dfg_signature(self, inputs: TypeRow, outputs: TypeRow) -> None:
317
+ self.signature = FunctionType(
318
+ input=list(inputs), output=list(outputs), extension_reqs=ExtensionSet([])
319
+ )
320
+
321
+
322
+ # ------------------------------------------------
323
+ # --------------- ControlFlowOp ------------------
324
+ # ------------------------------------------------
325
+
326
+
327
+ class Conditional(DataflowOp):
328
+ """Conditional operation, defined by child `Case` nodes for each branch."""
329
+
330
+ op: Literal["Conditional"] = "Conditional"
331
+ other_inputs: TypeRow = Field(default_factory=list) # Remaining input types
332
+ outputs: TypeRow = Field(default_factory=list) # Output types
333
+ sum_rows: list[TypeRow] = Field(description="The possible rows of the Sum input")
334
+ # Extensions used to produce the outputs
335
+ extension_delta: ExtensionSet = Field(default_factory=list)
336
+
337
+ def insert_port_types(self, in_types: TypeRow, out_types: TypeRow) -> None:
338
+ # First port is a predicate, i.e. a sum of tuple types. We need to unpack
339
+ # those into a list of type rows
340
+ pred = in_types[0]
341
+ if isinstance(pred, tys.UnitSum):
342
+ self.sum_rows = [[] for _ in range(cast(tys.UnitSum, pred).size)]
343
+ else:
344
+ assert isinstance(pred, tys.GeneralSum)
345
+ self.sum_rows = []
346
+ for ty in pred.rows:
347
+ self.sum_rows.append(ty)
348
+ self.other_inputs = list(in_types[1:])
349
+ self.outputs = list(out_types)
350
+
351
+
352
+ class Case(BaseOp):
353
+ """Case ops - nodes valid inside Conditional nodes."""
354
+
355
+ op: Literal["Case"] = "Case"
356
+ # The signature of the contained dataflow graph.
357
+ signature: FunctionType = Field(default_factory=FunctionType.empty)
358
+
359
+ def insert_child_dfg_signature(self, inputs: TypeRow, outputs: TypeRow) -> None:
360
+ self.signature = tys.FunctionType(
361
+ input=list(inputs), output=list(outputs), extension_reqs=ExtensionSet([])
362
+ )
363
+
364
+
365
+ class TailLoop(DataflowOp):
366
+ """Tail-controlled loop."""
367
+
368
+ op: Literal["TailLoop"] = "TailLoop"
369
+ just_inputs: TypeRow = Field(default_factory=list) # Types that are only input
370
+ just_outputs: TypeRow = Field(default_factory=list) # Types that are only output
371
+ # Types that are appended to both input and output:
372
+ rest: TypeRow = Field(default_factory=list)
373
+
374
+ def insert_port_types(self, in_types: TypeRow, out_types: TypeRow) -> None:
375
+ assert in_types == out_types
376
+ # self.just_inputs = list(in_types)
377
+ # self.just_outputs = list(out_types)
378
+ self.rest = list(in_types)
379
+
380
+
381
+ class CFG(DataflowOp):
382
+ """A dataflow node which is defined by a child CFG."""
383
+
384
+ op: Literal["CFG"] = "CFG"
385
+ signature: FunctionType = Field(default_factory=FunctionType.empty)
386
+
387
+ def insert_port_types(self, inputs: TypeRow, outputs: TypeRow) -> None:
388
+ self.signature = FunctionType(
389
+ input=list(inputs), output=list(outputs), extension_reqs=ExtensionSet([])
390
+ )
391
+
392
+
393
+ ControlFlowOp = Conditional | TailLoop | CFG
394
+
395
+
396
+ # -----------------------------------------
397
+ # --------------- LeafOp ------------------
398
+ # -----------------------------------------
399
+
400
+
401
+ class CustomOp(LeafOpBase):
402
+ """A user-defined operation that can be downcasted by the extensions that define
403
+ it."""
404
+
405
+ lop: Literal["CustomOp"] = "CustomOp"
406
+ extension: ExtensionId
407
+ op_name: str
408
+ signature: tys.FunctionType = Field(default_factory=tys.FunctionType.empty)
409
+ description: str = ""
410
+ args: list[tys.TypeArg] = Field(default_factory=list)
411
+
412
+ def insert_port_types(self, in_types: TypeRow, out_types: TypeRow) -> None:
413
+ self.signature = tys.FunctionType(input=list(in_types), output=list(out_types))
414
+
415
+ def display_name(self) -> str:
416
+ return self.op_name
417
+
418
+ class Config:
419
+ # Needed to avoid random '\n's in the pydantic description
420
+ json_schema_extra = {
421
+ "description": (
422
+ "A user-defined operation that can be downcasted by the extensions that "
423
+ "define it."
424
+ )
425
+ }
426
+
427
+
428
+ class Noop(LeafOpBase):
429
+ """A no-op operation."""
430
+
431
+ lop: Literal["Noop"] = "Noop"
432
+ ty: Type
433
+
434
+ def insert_port_types(self, in_types: TypeRow, out_types: TypeRow) -> None:
435
+ assert len(in_types) == 1
436
+ assert len(out_types) == 1
437
+ assert in_types[0] == out_types[0]
438
+ self.ty = in_types[0]
439
+
440
+
441
+ class MakeTuple(LeafOpBase):
442
+ """An operation that packs all its inputs into a tuple."""
443
+
444
+ lop: Literal["MakeTuple"] = "MakeTuple"
445
+ tys: TypeRow = Field(default_factory=list)
446
+
447
+ def insert_port_types(self, in_types: TypeRow, out_types: TypeRow) -> None:
448
+ # If we have a single order edge as input, this is a unit
449
+ if in_types == [None]:
450
+ in_types = []
451
+ self.tys = list(in_types)
452
+
453
+
454
+ class UnpackTuple(LeafOpBase):
455
+ """An operation that packs all its inputs into a tuple."""
456
+
457
+ lop: Literal["UnpackTuple"] = "UnpackTuple"
458
+ tys: TypeRow = Field(default_factory=list)
459
+
460
+ def insert_port_types(self, in_types: TypeRow, out_types: TypeRow) -> None:
461
+ self.tys = list(out_types)
462
+
463
+
464
+ class Tag(LeafOpBase):
465
+ """An operation that creates a tagged sum value from one of its variants."""
466
+
467
+ lop: Literal["Tag"] = "Tag"
468
+ tag: int # The variant to create.
469
+ variants: TypeRow # The variants of the sum type.
470
+
471
+
472
+ class TypeApply(LeafOpBase):
473
+ """Fixes some TypeParams of a polymorphic type by providing TypeArgs."""
474
+
475
+ lop: Literal["TypeApply"] = "TypeApply"
476
+ ta: "TypeApplication"
477
+
478
+
479
+ class TypeApplication(BaseModel):
480
+ """Records details of an application of a PolyFuncType to some TypeArgs and the
481
+ result (a less-, but still potentially-, polymorphic type).
482
+ """
483
+
484
+ input: PolyFuncType
485
+ args: list[tys.TypeTypeArg]
486
+ output: PolyFuncType
487
+
488
+ class Config:
489
+ # Needed to avoid random '\n's in the pydantic description
490
+ json_schema_extra = {
491
+ "description": (
492
+ "Records details of an application of a PolyFuncType to some TypeArgs "
493
+ "and the result (a less-, but still potentially-, polymorphic type)."
494
+ )
495
+ }
496
+
497
+
498
+ class LeafOp(RootModel):
499
+ """A constant operation."""
500
+
501
+ root: CustomOp | Noop | MakeTuple | UnpackTuple | Tag | TypeApply = Field(
502
+ discriminator="lop"
503
+ )
504
+
505
+
506
+ class OpType(RootModel):
507
+ """A constant operation."""
508
+
509
+ root: (
510
+ Module
511
+ | Case
512
+ | FuncDefn
513
+ | FuncDecl
514
+ | Const
515
+ | DataflowBlock
516
+ | ExitBlock
517
+ | Conditional
518
+ | TailLoop
519
+ | CFG
520
+ | Input
521
+ | Output
522
+ | Call
523
+ | CallIndirect
524
+ | LoadConstant
525
+ | LeafOp
526
+ | DFG
527
+ ) = Field(discriminator="op")
528
+
529
+
530
+ # --------------------------------------
531
+ # --------------- OpDef ----------------
532
+ # --------------------------------------
533
+
534
+
535
+ class OpDef(BaseOp, populate_by_name=True):
536
+ """Serializable definition for dynamically loaded operations."""
537
+
538
+ name: str # Unique identifier of the operation.
539
+ description: str # Human readable description of the operation.
540
+ inputs: list[tuple[str | None, Type]]
541
+ outputs: list[tuple[str | None, Type]]
542
+ misc: dict[str, Any] # Miscellaneous data associated with the operation.
543
+ def_: str | None = Field(
544
+ ..., alias="def"
545
+ ) # (YAML?)-encoded definition of the operation.
546
+ extension_reqs: ExtensionSet # Resources required to execute this operation.
547
+
548
+
549
+ # Now that all classes are defined, we need to update the ForwardRefs in all type
550
+ # annotations. We use some inspect magic to find all classes defined in this file.
551
+ classes = inspect.getmembers(
552
+ sys.modules[__name__],
553
+ lambda member: inspect.isclass(member) and member.__module__ == __name__,
554
+ )
555
+ for _, c in classes:
556
+ if issubclass(c, BaseModel):
557
+ c.model_rebuild()
@@ -0,0 +1,36 @@
1
+ from typing import Any, Literal
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from .ops import NodeID, OpType
6
+
7
+ Port = tuple[NodeID, int | None] # (node, offset)
8
+ Edge = tuple[Port, Port]
9
+
10
+
11
+ class SerialHugr(BaseModel):
12
+ """A serializable representation of a Hugr."""
13
+
14
+ version: Literal["v1"] = "v1"
15
+ nodes: list[OpType]
16
+ edges: list[Edge]
17
+
18
+ def to_json(self) -> str:
19
+ """Return a JSON representation of the Hugr."""
20
+ return self.model_dump_json()
21
+
22
+ @classmethod
23
+ def load_json(cls, json: dict[Any, Any]) -> "SerialHugr":
24
+ """Decode a JSON-encoded Hugr."""
25
+ return cls(**json)
26
+
27
+ @classmethod
28
+ def get_version(cls) -> str:
29
+ """Return the version of the schema."""
30
+ return cls(nodes=[], edges=[]).version
31
+
32
+ class Config:
33
+ title = "Hugr"
34
+ json_schema_extra = {
35
+ "required": ["version", "nodes", "edges"],
36
+ }
@@ -0,0 +1,320 @@
1
+ import inspect
2
+ import sys
3
+ from enum import Enum
4
+ from typing import Annotated, Any, Literal, Optional, Union
5
+
6
+ from pydantic import (
7
+ BaseModel,
8
+ Field,
9
+ RootModel,
10
+ ValidationError,
11
+ ValidationInfo,
12
+ ValidatorFunctionWrapHandler,
13
+ WrapValidator,
14
+ )
15
+ from pydantic_core import PydanticCustomError
16
+
17
+
18
+ def _json_custom_error_validator(
19
+ value: Any, handler: ValidatorFunctionWrapHandler, _info: ValidationInfo
20
+ ) -> Any:
21
+ """Simplify the error message to avoid a gross error stemming
22
+ from exhaustive checking of all union options.
23
+
24
+ As suggested at
25
+ https://docs.pydantic.dev/latest/concepts/types/#named-recursive-types
26
+
27
+
28
+ Used to define named recursive alias types.
29
+ """
30
+ try:
31
+ return handler(value)
32
+ except ValidationError as err:
33
+ raise PydanticCustomError(
34
+ "invalid_json",
35
+ "Input is not valid json",
36
+ ) from err
37
+
38
+
39
+ ExtensionId = str
40
+
41
+
42
+ class ExtensionSet(RootModel):
43
+ """A set of extensions ids."""
44
+
45
+ root: Optional[list[ExtensionId]] = Field(default=None)
46
+
47
+
48
+ # --------------------------------------------
49
+ # --------------- TypeParam ------------------
50
+ # --------------------------------------------
51
+
52
+
53
+ class TypeTypeParam(BaseModel):
54
+ tp: Literal["Type"] = "Type"
55
+ b: "TypeBound"
56
+
57
+
58
+ class BoundedNatParam(BaseModel):
59
+ tp: Literal["BoundedNat"] = "BoundedNat"
60
+ bound: int | None
61
+
62
+
63
+ class OpaqueParam(BaseModel):
64
+ tp: Literal["Opaque"] = "Opaque"
65
+ ty: "Opaque"
66
+
67
+
68
+ class ListParam(BaseModel):
69
+ tp: Literal["List"] = "List"
70
+ param: "TypeParam"
71
+
72
+
73
+ class TupleParam(BaseModel):
74
+ tp: Literal["Tuple"] = "Tuple"
75
+ params: list["TypeParam"]
76
+
77
+
78
+ class TypeParam(RootModel):
79
+ """A type parameter."""
80
+
81
+ root: Annotated[
82
+ TypeTypeParam | BoundedNatParam | OpaqueParam | ListParam | TupleParam,
83
+ WrapValidator(_json_custom_error_validator),
84
+ ] = Field(discriminator="tp")
85
+
86
+
87
+ # ------------------------------------------
88
+ # --------------- TypeArg ------------------
89
+ # ------------------------------------------
90
+
91
+
92
+ class CustomTypeArg(BaseModel):
93
+ typ: None # TODO
94
+ value: str
95
+
96
+
97
+ class TypeTypeArg(BaseModel):
98
+ tya: Literal["Type"] = "Type"
99
+ ty: "Type"
100
+
101
+
102
+ class BoundedNatArg(BaseModel):
103
+ tya: Literal["BoundedNat"] = "BoundedNat"
104
+ n: int
105
+
106
+
107
+ class OpaqueArg(BaseModel):
108
+ tya: Literal["Opaque"] = "Opaque"
109
+ arg: CustomTypeArg
110
+
111
+
112
+ class SequenceArg(BaseModel):
113
+ tya: Literal["Sequence"] = "Sequence"
114
+ args: list["TypeArg"]
115
+
116
+
117
+ class ExtensionsArg(BaseModel):
118
+ tya: Literal["Extensions"] = "Extensions"
119
+ es: ExtensionSet
120
+
121
+
122
+ class TypeArg(RootModel):
123
+ """A type argument."""
124
+
125
+ root: Annotated[
126
+ TypeTypeArg | BoundedNatArg | OpaqueArg | SequenceArg | ExtensionsArg,
127
+ WrapValidator(_json_custom_error_validator),
128
+ ] = Field(discriminator="tya")
129
+
130
+
131
+ # --------------------------------------------
132
+ # --------------- Container ------------------
133
+ # --------------------------------------------
134
+
135
+
136
+ class MultiContainer(BaseModel):
137
+ ty: "Type"
138
+
139
+
140
+ class Array(MultiContainer):
141
+ """Known size array whose elements are of the same type."""
142
+
143
+ t: Literal["Array"] = "Array"
144
+ len: int
145
+
146
+
147
+ class UnitSum(BaseModel):
148
+ """Simple predicate where all variants are empty tuples."""
149
+
150
+ t: Literal["Sum"] = "Sum"
151
+
152
+ s: Literal["Unit"] = "Unit"
153
+ size: int
154
+
155
+
156
+ class GeneralSum(BaseModel):
157
+ """General sum type that explicitly stores the types of the variants."""
158
+
159
+ t: Literal["Sum"] = "Sum"
160
+
161
+ s: Literal["General"] = "General"
162
+ rows: list["TypeRow"]
163
+
164
+
165
+ class SumType(RootModel):
166
+ root: Union[UnitSum, GeneralSum] = Field(discriminator="s")
167
+
168
+
169
+ # ----------------------------------------------
170
+ # --------------- ClassicType ------------------
171
+ # ----------------------------------------------
172
+
173
+
174
+ class Variable(BaseModel):
175
+ """A type variable identified by a de Bruijn index."""
176
+
177
+ t: Literal["V"] = "V"
178
+ i: int
179
+ b: "TypeBound"
180
+
181
+
182
+ class USize(BaseModel):
183
+ """Unsigned integer size type."""
184
+
185
+ t: Literal["I"] = "I"
186
+
187
+
188
+ class FunctionType(BaseModel):
189
+ """A graph encoded as a value. It contains a concrete signature and a set of
190
+ required resources."""
191
+
192
+ input: "TypeRow" # Value inputs of the function.
193
+ output: "TypeRow" # Value outputs of the function.
194
+ # The extension requirements which are added by the operation
195
+ extension_reqs: "ExtensionSet" = Field(default_factory=list)
196
+
197
+ @classmethod
198
+ def empty(cls) -> "FunctionType":
199
+ return FunctionType(input=[], output=[], extension_reqs=ExtensionSet([]))
200
+
201
+ class Config:
202
+ # Needed to avoid random '\n's in the pydantic description
203
+ json_schema_extra = {
204
+ "description": (
205
+ "A graph encoded as a value. It contains a concrete signature and "
206
+ "a set of required resources."
207
+ )
208
+ }
209
+
210
+
211
+ class PolyFuncType(BaseModel):
212
+ """A graph encoded as a value. It contains a concrete signature and a set of
213
+ required resources."""
214
+
215
+ t: Literal["G"] = "G"
216
+
217
+ # The declared type parameters, i.e., these must be instantiated with the same
218
+ # number of TypeArgs before the function can be called. Note that within the body,
219
+ # variable (DeBruijn) index 0 is element 0 of this array, i.e. the variables are
220
+ # bound from right to left.
221
+ params: list[TypeParam]
222
+
223
+ # Template for the function. May contain variables up to length of `params`
224
+ body: FunctionType
225
+
226
+ @classmethod
227
+ def empty(cls) -> "PolyFuncType":
228
+ return PolyFuncType(params=[], body=FunctionType.empty())
229
+
230
+ class Config:
231
+ # Needed to avoid random '\n's in the pydantic description
232
+ json_schema_extra = {
233
+ "description": (
234
+ "A graph encoded as a value. It contains a concrete signature and "
235
+ "a set of required resources."
236
+ )
237
+ }
238
+
239
+
240
+ class TypeBound(Enum):
241
+ Eq = "E"
242
+ Copyable = "C"
243
+ Any = "A"
244
+
245
+ @staticmethod
246
+ def join(*bs: "TypeBound") -> "TypeBound":
247
+ """Computes the least upper bound for a sequence of bounds."""
248
+ res = TypeBound.Eq
249
+ for b in bs:
250
+ if b == TypeBound.Any:
251
+ return TypeBound.Any
252
+ if res == TypeBound.Eq:
253
+ res = b
254
+ return res
255
+
256
+
257
+ class Opaque(BaseModel):
258
+ """An opaque operation that can be downcasted by the extensions that define it."""
259
+
260
+ t: Literal["Opaque"] = "Opaque"
261
+ extension: ExtensionId
262
+ id: str # Unique identifier of the opaque type.
263
+ args: list[TypeArg]
264
+ bound: TypeBound
265
+
266
+
267
+ # ----------------------------------------------
268
+ # --------------- LinearType -------------------
269
+ # ----------------------------------------------
270
+
271
+
272
+ class Qubit(BaseModel):
273
+ """A qubit."""
274
+
275
+ t: Literal["Q"] = "Q"
276
+
277
+
278
+ class Type(RootModel):
279
+ """A HUGR type."""
280
+
281
+ root: Annotated[
282
+ Qubit | Variable | USize | PolyFuncType | Array | SumType | Opaque,
283
+ WrapValidator(_json_custom_error_validator),
284
+ ] = Field(discriminator="t")
285
+
286
+
287
+ # -------------------------------------------
288
+ # --------------- TypeRow -------------------
289
+ # -------------------------------------------
290
+
291
+ TypeRow = list[Type]
292
+
293
+
294
+ # -------------------------------------------
295
+ # --------------- Signature -----------------
296
+ # -------------------------------------------
297
+
298
+
299
+ class Signature(BaseModel):
300
+ """Describes the edges required to/from a node.
301
+
302
+ This includes both the concept of "signature" in the spec, and also the target
303
+ (value) of a call (constant).
304
+ """
305
+
306
+ signature: "PolyFuncType" # The underlying signature
307
+
308
+ # The extensions which are associated with all the inputs and carried through
309
+ input_extensions: ExtensionSet
310
+
311
+
312
+ # Now that all classes are defined, we need to update the ForwardRefs in all type
313
+ # annotations. We use some inspect magic to find all classes defined in this file.
314
+ classes = inspect.getmembers(
315
+ sys.modules[__name__],
316
+ lambda member: inspect.isclass(member) and member.__module__ == __name__,
317
+ )
318
+ for _, c in classes:
319
+ if issubclass(c, BaseModel):
320
+ c.model_rebuild()