Source code for circuitry.circuitry

"""
Embedded domain-specific combinator library for assembling abstract definitions
of logical circuits and for automatically synthesizing circuits from those
definitions.

This library makes it possible both to construct circuits **programmatically** (see
the documentation for the :obj:`bit` class) and to synthesize circuits **in an
automated manner** from Python function definitions (see the documentation for
the :obj:`synthesize` function).
"""
from __future__ import annotations
from typing import Sequence, Union, Optional, Callable
import doctest
import inspect
from parts import parts
from circuit import op, circuit, signature

[docs]class bit: """ Class for representing an abstract bit. Such a bit can be interpreted concretely as a value, but it is also used to keep track of relationships between operators and to represent the individual wires within a circuit that is programmatically built up from gates that correspond to those operators. **The documentation for this class describes how circuits can be synthesized programmatically using the methods made available by this class. Consult the documentation for the** :obj:`synthesize` **function to learn how circuits can be synthesized automatically from Python function definitions.** When constructing a circuit programmatically, it is first necessary to introduce a new circuit object and to designate this object as the circuit that is being constructed. >>> c = circuit() >>> bit.circuit(c) Subsequently, it is possible to instantiate subclasses of :obj:`bit` that serve specific roles (such as :obj:`input`, which represents an input value into the overall circuit being constructed). An operator method such as :obj:`bit.and_` can be used to operate on :obj:`bit` instances (and, at the same time to introduce a gate into the circuit). >>> b = output(input(1).and_(input(1))) At this point, constructed circuit can be retrieved and evaluated on a vector of bit values (wherein each value is represented using the integer ``0`` or the integer ``1``). >>> b.value == bit.circuit().evaluate([1, 1])[0] True It is possible to add custom hook functions that are applied whenever an operator is introduced using the :obj:`bit.hook_operation` method. >>> def make_hook_that_prints_created_gates(bit_): ... def hook(o, v, *args): ... print('created gate with operation "' + o.name() + '"') ... return bit_.constructor(*args)(v, bit_.gate(o, [a.gate for a in args])) ... return hook >>> bit.hook_operation(make_hook_that_prints_created_gates(bit)) >>> bit.circuit(circuit()) >>> b = output(input(0).and_(input(1)).not_()) created gate with operation "and" created gate with operation "not" Note that a hook must be removed if its effects are not desired when subsequent circuits are constructed. >>> bit.hook_operation() """ # pylint: disable=too-many-public-methods _circuit = None _hook_operation = None def __init__( self: bit, value: int, gate: Optional[gate] = None # pylint: disable=redefined-outer-name ): """ Create an instance with the specified value and (if one is supplied) designate an associated gate object. """ self.value = value if bit._circuit is not None: self.gate = bit._circuit.gates if gate is None else gate
[docs] @staticmethod def circuit( circuit: Optional[circuit] = None # pylint: disable=redefined-outer-name ) -> Optional[circuit]: """ Designate the circuit object that is under construction. Any invocation of the :obj:`bit` constructor adds a gate to the circuit designated using this method. >>> c = circuit() >>> bit.circuit(c) >>> b = output(input(1).and_(input(1))) >>> b.value == bit.circuit().evaluate([1, 1])[0] True Invoking this method with no argument retrieves *and removes* the circuit object (leaving no designated circuit object). Note that because of the invocation of this method at the end of the above example, the invocation below returns ``None``. >>> bit.circuit() is None True """ if circuit is not None: bit._circuit = circuit return None if bit._circuit is not None: bit._circuit.prune_and_topological_sort_stable() c = bit._circuit bit._circuit = None return c return None
[docs] @staticmethod def hook_operation(hook: Optional[Callable] = None): """ Assign a function that is invoked whenever the :obj:`bit.operation` method is used to create a new instance of :obj:`bit`. >>> def make_hook_that_prints_created_gates(bit_): ... def hook(o, v, *args): ... print('created gate with operation "' + o.name() + '"') ... return bit_.constructor(*args)(v, bit_.gate(o, [a.gate for a in args])) ... return hook >>> bit.hook_operation(make_hook_that_prints_created_gates(bit)) >>> bit.circuit(circuit()) >>> b = output(input(0).and_(input(1)).not_()) created gate with operation "and" created gate with operation "not" >>> b.value == bit.circuit().evaluate([0, 0])[0] True If a hook function returns ``None``, then the default instance of :obj:`bit` is returned when an operation is applied. >>> bit.hook_operation(lambda o, v, *args: None) >>> bit.circuit(circuit()) >>> b = output(input(1).and_(input(1))) >>> b.value == bit.circuit().evaluate([1, 1])[0] True It is only possible to assign one hook function at a time. A hook must be removed if its effects are not desired when subsequent circuits are constructed. >>> bit.hook_operation() """ bit._hook_operation = hook
[docs] @staticmethod def operation(o: Callable, *args) -> bit: """ Apply the supplied operation method to zero or more :obj:`bit` arguments. Gate operations are represented using instances of the :obj:`~logical.logical.logical` class exported by the `logical <https://pypi.org/project/logical/>`_ library. This module indirectly imports the :obj:`~logical.logical.logical` class via the :obj:`~circuit.circuit.op` synonym defined in the `circuit <https://pypi.org/project/circuit/>`_ library, enabling the more concise syntax used in the example below. >>> bit.circuit(circuit()) >>> b = output(bit.operation(op.and_, input(1), input(1))) >>> b.value == bit.circuit().evaluate([1, 1])[0] True Arguments that are instances of :obj:`output` are not permitted. >>> bit.circuit(circuit()) >>> b0 = output(input(0).not_()) >>> b1 = b0.not_() Traceback (most recent call last): ... TypeError: cannot supply an output as an argument to an operation >>> _ = bit.circuit() # Remove designated circuit. """ # Ensure second argument is a `bit`. args = list(args) if len(args) == 2: args[1] = constant(args[1]) if isinstance(args[1], int) else args[1] # Ensure none of the arguments are outputs. for a in args: if isinstance(a, output): raise TypeError( 'cannot supply an output as an argument to an operation' ) # Compute the value of the result of the operation on the arguments. v = o(*[a.value for a in args]) # Return output from hook if it exists and if # it returns an output. if bit._hook_operation is not None: r = bit._hook_operation(o, v, *args) # pylint: disable=not-callable if r is not None: return r return bit.constructor(*args)(v, bit.gate(o, [a.gate for a in args]))
[docs] @staticmethod def constructor( b1: Optional[bit] = None, b2: Optional[bit] = None # pylint: disable=unused-argument ) -> type: """ Return the constructor to use for instantiating a :obj:`bit` object. This method is used by the :obj:`bit.operation` and :obj:`bits.from_byte` methods. It is provided primarily to aid in the implementation of custom hooks that can be set using the :obj:`bit.hook_operation` method. """ return bit
[docs] @staticmethod def gate(operation: op, igs: Sequence[gate]) -> Optional[gate]: # pylint: disable=method-hidden """ Add a gate to the designated circuit object that is under construction. This method is primarily provided to aid in the implementation of custom hooks that can be set using the :obj:`bit.hook_operation` method. Gate operations are represented using instances of the :obj:`~logical.logical.logical` class exported by the `logical <https://pypi.org/project/logical/>`_ library (which is indirectly imported into this module as the :obj:`~circuit.circuit.op` synonym from the `circuit <https://pypi.org/project/circuit/>`_ library). >>> def make_hook(bit_): ... def hook(o, v, *args): ... return bit_.constructor(*args)(v, bit_.gate(o, [a.gate for a in args])) ... return hook >>> bit.hook_operation(make_hook(bit)) >>> bit.circuit(circuit()) >>> b = output(input(0).and_(input(0))) >>> b.value == bit.circuit().evaluate([0, 0])[0] True """ return ( bit._circuit.gate(operation, igs) if bit._circuit is not None else None )
[docs] def __int__(self: bit) -> int: """ Convert this :obj:`bit` instance into the integer representation of its value. >>> int(bit(1)) 1 """ return self.value
[docs] def id_(self: bit) -> bit: """ Logical operation for an individual :obj:`bit` instance. >>> results = [] >>> for x in [0, 1]: ... bit.circuit(circuit()) ... b = output(input(x).id_()) ... results.append(int(b) == bit.circuit().evaluate([x])[0]) >>> all(results) True """ return bit.operation(op.id_, self)
[docs] def not_(self: bit) -> bit: """ Logical operation for an individual :obj:`bit` instance. >>> results = [] >>> for x in [0, 1]: ... bit.circuit(circuit()) ... b = output(input(x).not_()) ... results.append(int(b) == bit.circuit().evaluate([x])[0]) >>> all(results) True """ return bit.operation(op.not_, self)
[docs] def __invert__(self: bit) -> bit: """ Logical operation for an individual :obj:`bit` instance. >>> results = [] >>> for x in [0, 1]: ... bit.circuit(circuit()) ... b = output(~input(x)) ... results.append(int(b) == bit.circuit().evaluate([x])[0]) >>> all(results) True """ return bit.operation(op.not_, self)
[docs] def __rsub__(self: bit, other: bit) -> bit: """ Arithmetic operation involving an individual :obj:`bit` instance that is often used to represent the logical negation operation. This method is provided primarily as a convenience enabling use of the syntax ``1 - b`` to represent logical negation of a bit value ``b``; it **does not enable subtraction of one** :obj:`bit` **instance from another**. >>> results = [] >>> for x in [0, 1]: ... bit.circuit(circuit()) ... b = output(1 - input(x)) ... results.append(int(b) == bit.circuit().evaluate([x])[0]) >>> all(results) True >>> 2 - input(0) Traceback (most recent call last): ... ValueError: can only subtract a bit from the integer 1 """ if other == 1: return bit.operation(op.not_, self) raise ValueError('can only subtract a bit from the integer 1')
[docs] def and_(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x).and_(input(y))) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.and_, self, other)
[docs] def __and__(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x) & input(y)) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.and_, self, other)
[docs] def __rand__(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> b = 0 & constant(1) >>> b.value 0 """ return self & (constant(other) if isinstance(other, int) else other)
[docs] def nimp(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x).nimp(input(y))) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.nimp_, self, other)
[docs] def nimp_(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x).nimp_(input(y))) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.nimp_, self, other)
[docs] def __gt__(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x) > input(y)) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return self.nimp(other)
[docs] def nif(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x).nif(input(y))) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.nif_, self, other)
[docs] def nif_(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x).nif_(input(y))) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.nif_, self, other)
[docs] def __lt__(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x) < input(y)) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return self.nif(other)
[docs] def xor(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x).xor(input(y))) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.xor_, self, other)
[docs] def xor_(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x).xor_(input(y))) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.xor_, self, other)
[docs] def __xor__(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x) ^ input(y)) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.xor_, self, other)
[docs] def __rxor__(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> b = 1 ^ constant(0) >>> b.value 1 """ return self ^ (constant(other) if isinstance(other, int) else other)
[docs] def or_(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x).or_(input(y))) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.or_, self, other)
[docs] def __or__(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x) | input(y)) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.or_, self, other)
[docs] def __ror__(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> b = 1 | constant(0) >>> b.value 1 """ return self | (constant(other) if isinstance(other, int) else other)
[docs] def nor(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x).nor(input(y))) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.nor_, self, other)
[docs] def nor_(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x).nor_(input(y))) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.nor_, self, other)
[docs] def __mod__(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x) % input(y)) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.nor_, self, other)
[docs] def xnor(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x).xnor(input(y))) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.xnor_, self, other)
[docs] def xnor_(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x).xnor_(input(y))) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.xnor_, self, other)
[docs] def __eq__(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x) == input(y)) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.xnor_, self, other)
[docs] def if_(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x).if_(input(y))) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.if_, self, other)
[docs] def __ge__(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x) >= input(y)) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.if_, self, other)
[docs] def imp(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x).imp(input(y))) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.imp_, self, other)
[docs] def imp_(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x).imp_(input(y))) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.imp_, self, other)
[docs] def __le__(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x) <= input(y)) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.imp_, self, other)
[docs] def nand(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x).nand(input(y))) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.nand_, self, other)
[docs] def nand_(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x).nand_(input(y))) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.nand_, self, other)
[docs] def __matmul__(self: bit, other: bit) -> bit: """ Logical operation for individual :obj:`bit` instances. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... b = output(input(x) @ input(y)) ... results.append(int(b) == bit.circuit().evaluate([x, y])[0]) >>> all(results) True """ return bit.operation(op.nand_, self, other)
[docs]class constant(bit): """ Instance of a :obj:`bit` that is designated as a constant value. >>> constant(1).value 1 When a :obj:`constant` instance is introduced during circuit construction, a gate with a nullary operator (*i.e.*, one of the two appearing in :obj:`~logical.logical.logical.nullary`) is added to the circuit. >>> bit.circuit(circuit()) >>> _ = output(constant(0)) >>> c = bit.circuit() >>> c.count() 2 >>> c.evaluate([]) [0] """ def __init__(self: bit, value: int): """ Instantiate an instance that is designated as a constant input. """ super().__init__(value) self.value = value if bit._circuit is not None: self.gate = bit._circuit.gate(op.nf_ if self.value == 0 else op.nt_)
[docs]class input(bit): # pylint: disable=redefined-builtin """ Instance of a :obj:`bit` that is designated as a variable input. When an :obj:`input` instance is introduced during circuit construction, a gate is added to the circuit that is explicitly marked as an input gate (as defined in the `circuit <https://pypi.org/project/circuit/>`_ library). >>> b0 = output(input(1).not_()) >>> b0.value 0 """ def __init__(self: bit, value: int): """ Instantiate an instance that is designated as a variable input. """ super().__init__(value) self.value = value if bit._circuit is not None: self.gate = bit._circuit.gate(op.id_, is_input=True)
[docs]class input_one(input): """ Instance of a :obj:`bit` that is designated as a variable input from one source. """
[docs]class input_two(input): """ Instance of a :obj:`bit` that is designated as a variable input from a second source """
[docs]class output(bit): """ Instance of a :obj:`bit` that is designated as an output. When an :obj:`output` instance is introduced during circuit construction, a gate is added to the circuit that is explicitly marked as an output gate (as defined in the `circuit <https://pypi.org/project/circuit/>`_ library). >>> bit.circuit(circuit()) >>> b0 = input(0).not_() >>> b1 = output(b0.not_()) >>> [b0.value, b1.value] [1, 0] >>> bit.circuit().evaluate([1]) [1] It is not possible to apply an operation to an :obj:`output` instance. Any attempt to do so raises an exception (see implementation of :obj:`bit.operation` for more details). >>> bit.circuit(circuit()) >>> b0 = output(input(0).not_()) >>> b1 = b0.not_() Traceback (most recent call last): ... TypeError: cannot supply an output as an argument to an operation If the value represented by a :obj:`bit` instance must be both an argument into another operation and an output, an :obj:`bit.id_` operation can be introduced. >>> bit.circuit(circuit()) >>> b0 = input(1).not_() >>> b1 = output(b0.id_()) >>> b2 = output(b0.not_()) >>> [b0.value, b1.value, b2.value] [0, 0, 1] >>> bit.circuit().evaluate([0]) [1, 0] """ def __init__(self: bit, b: bit): """ Instantiate a bit that is designated as an output. """ super().__init__(b) self.value = b.value if bit._circuit is not None: self.gate = bit._circuit.gate(op.id_, [b.gate], is_output=True)
class bits_type(int): # pylint: disable=R0903 """ Class for representing an input or output type of a function decorated for automated synthesis. This class is employed internally within the :obj:`bits` constructor implementation and is not intended to be used directly. It is not exported. """ def __str__(self): """ Return representation that reflects the meaning and the manner of construction of this instance. >>> str(bits_type(32)) 'bits(32)' """ return 'bits(' + str(int(self)) + ')' def __repr__(self): """ Return representation that reflects the meaning and the manner of construction of this instance. >>> bits_type(32) bits(32) """ return 'bits(' + str(int(self)) + ')'
[docs]class bits(list): """ Class for representing a *bit vector* (*i.e.*, a list of abstract :obj:`bit` instances). >>> bs = bits([constant(1)] * 3) >>> [b.value for b in bs] [1, 1, 1] """ # pylint: disable=too-many-public-methods def __new__(cls, argument=None) -> bits: """ Instantiate bit vector object given the supplied argument. """ return ( bits_type(argument) if isinstance(argument, int) else list.__new__(cls, argument) )
[docs] @staticmethod def from_byte(b: int, constructor=bit) -> bits: """ Convert an integer representing a single byte into a corresponding bit vector. >>> [b.value for b in bits.from_byte(255)] [1, 1, 1, 1, 1, 1, 1, 1] """ return bits([ constructor(bit_) for bit_ in reversed([(b >> i) % 2 for i in range(8)]) ])
[docs] @staticmethod def from_bytes(bs: Union[bytes, bytearray], constructor=bit) -> bits: """ Convert a vector of bytes into a corresponding bit vector. >>> [b.value for b in bits.from_bytes(bytes([255]))] [1, 1, 1, 1, 1, 1, 1, 1] >>> [b.value for b in bits.from_bytes(bytes([11, 0]))] [0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0] """ return bits([ bit_ for byte_ in bs for bit_ in bits.from_byte(byte_, constructor) ])
[docs] @staticmethod def zeros(n: int) -> bits: """ Create a vector (of the specified length) of constant zero bits. >>> bit.circuit(circuit()) >>> xs = bits.zeros(3) >>> ys = outputs(xs.not_()) >>> [y.value for y in ys] [1, 1, 1] >>> _ = bit.circuit() # Remove designated circuit. """ return bits([constant(0)]*n)
[docs] def __int__(self: bits) -> int: """ Convert a bit vector into an integer. >>> bit.circuit(circuit()) >>> xs = constants([0, 0, 0]) >>> ys = outputs(xs.not_()) >>> int(ys) 7 >>> _ = bit.circuit() # Remove designated circuit. """ return sum(int(b)*(2**i) for (i, b) in zip(range(len(self)), reversed(self)))
[docs] def not_(self: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for x in [0, 1]: ... bit.circuit(circuit()) ... xs = inputs([x, x, x]) ... ys = outputs(xs.not_()) ... ns = [int(y) for y in ys] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x])) >>> all(results) True """ return bits([x.not_() for x in self])
[docs] def __invert__(self: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for x in [0, 1]: ... bit.circuit(circuit()) ... xs = inputs([x, x, x]) ... ys = outputs(~xs) ... ns = [int(y) for y in ys] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x])) >>> all(results) True """ return bits([x.not_() for x in self])
[docs] def and_(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs.and_(ys)) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.and_(y) for (x, y) in zip(self, other)])
[docs] def __and__(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs & ys) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.and_(y) for (x, y) in zip(self, other)])
[docs] def nimp(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs.nimp(ys)) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.nimp_(y) for (x, y) in zip(self, other)])
[docs] def nimp_(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs.nimp_(ys)) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.nimp_(y) for (x, y) in zip(self, other)])
[docs] def __gt__(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs > ys) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.nimp_(y) for (x, y) in zip(self, other)])
[docs] def nif(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs.nif(ys)) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.nif_(y) for (x, y) in zip(self, other)])
[docs] def nif_(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs.nif_(ys)) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.nif_(y) for (x, y) in zip(self, other)])
[docs] def __lt__(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs < ys) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.nif_(y) for (x, y) in zip(self, other)])
[docs] def xor(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs.xor(ys)) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.xor_(y) for (x, y) in zip(self, other)])
[docs] def xor_(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs.xor_(ys)) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.xor_(y) for (x, y) in zip(self, other)])
[docs] def __xor__(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs ^ ys) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.xor_(y) for (x, y) in zip(self, other)])
[docs] def or_(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs.or_(ys)) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.or_(y) for (x, y) in zip(self, other)])
[docs] def __or__(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs | ys) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.or_(y) for (x, y) in zip(self, other)])
[docs] def nor(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs.nor(ys)) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.nor_(y) for (x, y) in zip(self, other)])
[docs] def nor_(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs.nor_(ys)) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.nor_(y) for (x, y) in zip(self, other)])
[docs] def __mod__(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs % ys) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.nor_(y) for (x, y) in zip(self, other)])
[docs] def xnor(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs.xnor(ys)) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.xnor_(y) for (x, y) in zip(self, other)])
[docs] def xnor_(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs.xnor_(ys)) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.xnor_(y) for (x, y) in zip(self, other)])
[docs] def __eq__(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs == ys) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.xnor_(y) for (x, y) in zip(self, other)])
[docs] def if_(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs.if_(ys)) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.if_(y) for (x, y) in zip(self, other)])
[docs] def __ge__(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs >= ys) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.if_(y) for (x, y) in zip(self, other)])
[docs] def imp(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs.imp(ys)) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.imp_(y) for (x, y) in zip(self, other)])
[docs] def imp_(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs.imp_(ys)) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.imp_(y) for (x, y) in zip(self, other)])
[docs] def __le__(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs <= ys) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.imp_(y) for (x, y) in zip(self, other)])
[docs] def nand(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs.nand(ys)) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.nand_(y) for (x, y) in zip(self, other)])
[docs] def nand_(self: bits, other: bits) -> bits: """ Logical operation for bit vectors. >>> results = [] >>> for (x, y) in [(0, 0), (0, 1), (1, 0), (1, 1)]: ... bit.circuit(circuit()) ... (xs, ys) = (inputs([x, x, x]), inputs([y, y, y])) ... zs = outputs(xs.nand_(ys)) ... ns = [int(z) for z in zs] ... c = bit.circuit() ... results.append(ns == c.evaluate([x, x, x, y, y, y])) >>> all(results) True """ return bits([x.nand_(y) for (x, y) in zip(self, other)])
[docs] def __rshift__(self: bits, other: Union[int, set]) -> bits: """ Overloaded operator for performing rotation and shift operations on bit vectors. When the second parameter is an integer, a shift operation is performed. When the second parameter is a set containing a single integer, a rotation operation is performed. >>> bs = bits(map(bit, [1, 1, 1, 1, 0, 0, 0, 0])) >>> bs = bs >> 3 >>> [b.value for b in bs] [0, 0, 0, 1, 1, 1, 1, 0] >>> bs = bits(map(bit, [0, 0, 0, 0, 1, 1, 1, 1])) >>> bs = bs >> {3} >>> [b.value for b in bs] [1, 1, 1, 0, 0, 0, 0, 1] """ # Rotation. if isinstance(other, set) and isinstance(list(other)[0], int): quantity = list(other)[0] return bits(self[len(self)-quantity:]) ** bits(self[0:len(self)-quantity]) # Shift. return bits([constant(0)]*other) ** bits(self[0:len(self)-other])
[docs] def __lshift__(self: bits, other: int) -> bits: """ Overloaded operator for performing shift operations on bit vectors. >>> bs = bits(map(bit, [1, 1, 1, 1, 0, 0, 0, 0])) >>> bs = bs << 3 >>> [b.value for b in bs] [1, 0, 0, 0, 0, 0, 0, 0] """ return bits(self[other:]) ** bits([constant(0) for _ in range(other)])
[docs] def __truediv__(self: bits, other: [Union, set, list]) -> Sequence[bits]: """ Overloaded operator for splitting a bit vector into a collection of smaller bit vectors. Three operations are possible depending on the type of the second parameter. When the second parameter is an integer, this instance is split into that number of bit vectors. If the size of the vector is not an exact multiple of the integer, the parts in the result may not all be the same length. >>> bs = bits(map(bit, [1, 1, 1, 1, 0, 0, 0, 0])) >>> bss = list(bs / 2) >>> ([b.value for b in bss[0]], [b.value for b in bss[1]]) ([1, 1, 1, 1], [0, 0, 0, 0]) >>> bs = bits(map(bit, [1, 1, 1, 1, 0, 0, 0, 0])) >>> [len(bs) for bs in list(bs / 3)] [2, 3, 3] When the second parameter is a set containing a single integer, this instance is split into parts of the size specified by that integer. >>> bs = bits(map(bit, [1, 1, 1, 1, 0, 0, 0, 0])) >>> bss = list(bs / {2}) >>> [[b.value for b in bs] for bs in bss] [[1, 1], [1, 1], [0, 0], [0, 0]] >>> bs = bits(map(bit, [1, 1, 1, 1, 0, 0, 0, 0])) >>> bss = list(bs / {1}) >>> [[b.value for b in bs] for bs in bss] [[1], [1], [1], [1], [0], [0], [0], [0]] When the second parameter is a list of integers, this instance is split into parts the sizes of which correspond to the entries (in order) of the list of integers. >>> bs = bits(map(bit, [1, 1, 1, 1, 0, 0, 0, 0])) >>> bss = list(bs / [1, 3, 4]) >>> [[b.value for b in bs] for bs in bss] [[1], [1, 1, 1], [0, 0, 0, 0]] """ if isinstance(other, list) and len(other) > 0 and isinstance(other[0], int): return map(bits, parts(self, length=other)) # Sequence of lengths. if isinstance(other, set) and len(other) == 1 and isinstance(list(other)[0], int): return self / (len(self)//list(other)[0]) # Parts of length `other`. return map(bits, parts(self, other)) # Number of parts is `other`.
[docs] def __add__(self: bits, other: Union[bits, Sequence[int]]) -> bits: """ Overloaded operator for concatenating bit vectors. >>> bs = bits(map(bit, [1, 1])) + bits(map(bit, [0, 0])) >>> [b.value for b in bs] [1, 1, 0, 0] """ result = list(self) result.extend(list(other)) return bits(result)
[docs] def __pow__(self: bits, other: Union[bits, Sequence[int]]) -> bits: """ Overloaded operator for concatenating bit vectors. >>> bs = bits(map(bit, [1, 1])) ** bits(map(bit, [0, 0])) >>> [b.value for b in bs] [1, 1, 0, 0] """ return self + other
[docs]def constants(l: Sequence[int]) -> bits: """ Synonym for concisely constructing a vector of :obj:`bit` objects that are designated as constants. >>> bit.circuit(circuit()) >>> xs = constants([0, 0, 0]) >>> ys = outputs(xs.not_()) >>> int(ys) 7 >>> _ = bit.circuit() # Remove designated circuit. """ return bits(map(constant, l))
[docs]def inputs(l: Sequence[int]) -> bits: """ Synonym for concisely constructing a vector of :obj:`bit` objects that are designated as inputs. >>> results = [] >>> bit.circuit(circuit()) >>> xs = inputs([1, 1, 1]) >>> ys = outputs(xs.not_()) >>> ns = [int(y) for y in ys] >>> c = bit.circuit() >>> ns == c.evaluate([1, 1, 1]) True """ return bits(map(input, l))
[docs]def outputs(l: Sequence[int]) -> bits: """ Synonym for concisely constructing a vector of :obj:`bit` objects that are designated as outputs. >>> bit.circuit(circuit()) >>> xs = bits.zeros(3) >>> ys = outputs(xs.not_()) >>> [y.value for y in ys] [1, 1, 1] >>> _ = bit.circuit() # Remove designated circuit. """ return bits(map(output, l))
[docs]def synthesize(function: Callable, in_type=None, out_type=None) -> Callable: """ Decorator for automatically synthesizing a circuit from a function that takes only :obj:`bit` and/or :obj:`bits` objects as its arguments and returns an output of type :obj:`bit` or :obj:`bits` (or a tuple or list thereof). In the example below, a function ``equal`` is defined that determines whether two bits are equivalent. The use of this decorator causes a circuit that implements this function to be constructed. >>> @synthesize ... def equal(x: bit, y: bit) -> bit: ... return (x & y) | ((1 - x) & (1 - y)) The synthesized :obj:`~circuit.circuit.circuit` object is introduced as an attribute of the function and can be evaluated on two bit values (as indicated by the function's type annotation). >>> [equal.circuit.evaluate([[x], [y]]) for x in (0, 1) for y in (0, 1)] [[[1]], [[0]], [[0]], [[1]]] Note that the function itself can still be invoked on its own in the usual manner if the supplied inputs are integers or :obj:`bit` instances. When the function is invoked in this way, the output of the function corresponds to its output type annotation. >>> equal(0, 1) 0 >>> b = equal(bit(0), bit(1)) >>> isinstance(b, bit) True >>> int(b) 0 This decorator can also be applied to functions that are defined explicitly as operating on bit vectors (in the form of :obj:`bits` objects). >>> @synthesize ... def conjunction(xy: bits(2)) -> bits(2): ... return (xy[0], xy[0] & xy[1]) >>> [conjunction.circuit.evaluate([[x, y]]) for x in (0, 1) for y in (0, 1)] [[[0, 0]], [[0, 0]], [[1, 0]], [[1, 1]]] If the decorated function returns multiple outputs, the output type annotation should indicate this. >>> @synthesize ... def swap(x: bit, y: bit) -> (bit, bit): ... return (y, x) >>> [swap.circuit.evaluate([[x], [y]]) for x in (0, 1) for y in (0, 1)] [[[0], [0]], [[1], [0]], [[0], [1]], [[1], [1]]] Functions to which this decorator is applied must have valid type annotations that specify the lengths of the input and output bit vectors. >>> @synthesize ... def equal(x, y): ... return x & y Traceback (most recent call last): ... ValueError: function must have a type annotation for every argument >>> @synthesize ... def equal(x: bit, y: bit): ... return x & y Traceback (most recent call last): ... ValueError: function must have an output type annotation >>> @synthesize ... def equal(x: 'ABC', y: 'ABC') -> bit: ... return x & y Traceback (most recent call last): ... TypeError: input type annotations must be specified using bit or bits >>> @synthesize ... def swap(x: bit, y: bit) -> ('ABC', 'ABC'): ... return (y, x) Traceback (most recent call last): ... TypeError: output type annotation components must be specified using bit or bits >>> @synthesize ... def swap(x: bit, y: bit) -> 'ABC': ... return (y, x) Traceback (most recent call last): ... TypeError: output type must be specified using bit/bits, or a list/tuple thereof If an exception occurs during the execution (for the purpose of circuit synthesis) of the decorated function, then synthesis will fail. >>> @synthesize ... def equal(x: bit, y: bit) -> bit: ... return 1 / 0 # Run-time error. Traceback (most recent call last): ... RuntimeError: automated circuit synthesis failed To support programmatic synthesis (*e.g.*, of circuit variants of many different sizes from the same function definition), this function can accept input and output type information via two optional parameters. >>> def conjunction(xy): ... return (xy[0], xy[0] & xy[1]) >>> c = synthesize(conjunction, {'xy': bits(2)}, bits(2)) >>> [c.evaluate([[x, y]]) for x in (0, 1) for y in (0, 1)] [[[0, 0]], [[0, 0]], [[1, 0]], [[1, 1]]] >>> def swap(x: bit, y: bit) -> (bit, bit): ... return (y, x) >>> c = synthesize(swap, (bit, bit), (bit, bit)) >>> [c.evaluate([[x], [y]]) for x in (0, 1) for y in (0, 1)] [[[0], [0]], [[1], [0]], [[0], [1]], [[1], [1]]] When synthesizing programmatically, the input type information must be supplied either (1) as a dictionary that maps input type parameter names to types or (2) as a tuple or list (the length of which matches the number of parameters). The output type information must follow the same conventions as an output type annotation. >>> def equal(x, y): ... return (x & y) | ((1 - x) & (1 - y)) >>> c = synthesize(equal, (bit, bit), bit) >>> [c.evaluate([[x], [y]]) for x in (0, 1) for y in (0, 1)] [[[1]], [[0]], [[0]], [[1]]] >>> def swap(xy): ... return [xy[1], xy[0]] >>> c = synthesize(swap, bits(2), bits(2)) >>> [c.evaluate([[x, y]]) for x in (0, 1) for y in (0, 1)] [[[0, 0]], [[1, 0]], [[0, 1]], [[1, 1]]] If the supplied type information does not have the correct type or is incomplete, an exception is raised. >>> c = synthesize(equal, (bit, bit)) Traceback (most recent call last): ... ValueError: must include input and output types when supplying type information via parameters >>> c = synthesize(equal, bits(2), bit) Traceback (most recent call last): ... ValueError: number of input type components does not match number of function arguments >>> c = synthesize(equal, ('ABC', 'ABC'), bit) Traceback (most recent call last): ... TypeError: input type components must be specified using bit or bits >>> c = synthesize(equal, 'bits(2)', bit) Traceback (most recent call last): ... TypeError: input type must be specified using bit/bits, or a dict/list/tuple thereof >>> c = synthesize(equal, {'x': 'ABC', 'y': 'ABC'}, bit) Traceback (most recent call last): ... TypeError: input type components must be specified using bit or bits >>> c = synthesize(equal, (bit, bit), 'ABC') Traceback (most recent call last): ... TypeError: output type must be specified using bit/bits, or a list/tuple thereof >>> c = synthesize(equal, (bit, bit), ('ABC', bit)) Traceback (most recent call last): ... TypeError: output type components must be specified using bit or bits """ # pylint: disable=R0912,R0915 # Assume the type information is supplied via type annotations for the function until # it is determined otherwise. type_supplied_via_annotation = True # For forward-compatibility with PEP 563. eval_ = lambda a: eval(a) if isinstance(a, str) else a # pylint: disable=W0123 # If the type information is supplied via parameters, then both input and output # types must be supplied. if (in_type is None and out_type is not None) or (in_type is not None and out_type is None): raise ValueError( 'must include input and output types when supplying type information via parameters' ) # If the type information is supplied via parameters, then both input and output # types must be supplied. if (in_type is not None) and (out_type is not None): type_supplied_via_annotation = False # Ensure that the input type is specified in a valid way. if in_type is bit or isinstance(in_type, bits_type): in_type = (in_type,) elif isinstance(in_type, (tuple, list)): if not all(t is bit or isinstance(t, bits_type) for t in in_type): raise TypeError('input type components must be specified using bit or bits') elif isinstance(in_type, dict): if not all(t is bit or isinstance(t, bits_type) for t in in_type.values()): raise TypeError('input type components must be specified using bit or bits') else: raise TypeError( 'input type must be specified using bit/bits, or a dict/list/tuple thereof' ) # If the type information is supplied via parameters, ensure that the input type # information is converted into a dictionary that maps the function's input variables # to the corresponding type information (*i.e.*, if the user supplied a tuple or list # and not a dictionary for the input type information). if isinstance(in_type, (tuple, list)): if len(in_type) != len(inspect.getfullargspec(function).args): raise ValueError( 'number of input type components does not match number of function arguments' ) in_type = dict(zip(inspect.getfullargspec(function).args, in_type)) # Ensure that the output type is specified in a valid way. if out_type is bit or isinstance(out_type, bits_type): out_type = (out_type,) elif isinstance(out_type, (tuple, list)): if not all(t is bit or isinstance(t, bits_type) for t in out_type): raise TypeError('output type components must be specified using bit or bits') else: raise TypeError( 'output type must be specified using bit/bits, or a list/tuple thereof' ) # If the type information is supplied the function's annotation, ensure that its structure # is valid (and extract it for later use). if type_supplied_via_annotation: # Ensure that every one of the function's arguments and its output has a corresponding # type annotation. if ( len([() for k in function.__annotations__ if k != 'return']) != len(inspect.getfullargspec(function).args) ): print(function.__code__.co_varnames) print(function.__annotations__) raise ValueError('function must have a type annotation for every argument') if 'return' not in function.__annotations__: raise ValueError('function must have an output type annotation') # Extract the type annotations and evaluate them for later processing. in_type = {k: eval_(a) for (k, a) in function.__annotations__.items() if k != 'return'} out_type = eval_(function.__annotations__['return']) # Ensure that every argument's type annotation is specified in a valid way. if not all(t is bit or isinstance(t, bits_type) for t in in_type.values()): raise TypeError('input type annotations must be specified using bit or bits') # Ensure that the output type annotation is specified in a valid way. if out_type is bit or isinstance(out_type, bits_type): out_type = (out_type,) elif isinstance(out_type, (tuple, list)): if not all(t is bit or isinstance(t, bits_type) for t in out_type): raise TypeError( 'output type annotation components must be specified using bit or bits' ) else: raise TypeError( 'output type must be specified using bit/bits, or a list/tuple thereof' ) # Designate the circuit to be synthesized. bit.circuit(circuit()) # Construct the input bit vector(s) and output wrapper based on the function's # type annotation or type information supplied via the parameters. in_values = {k: (input(0) if a is bit else inputs([0] * a)) for (k, a) in in_type.items()} out_values = output if out_type == (bit,) else outputs # Construct a signature to the circuit based on the type annotation of the # decorated function. signature_ = signature( [(1 if a is bit else a) for a in in_type.values()], [(1 if a is bit else a) for a in out_type] ) # Synthesize the circuit by evaluating the function, add it to the function as an # attribute, and give the circuit its signature. try: out_values(function(**in_values)) function_circuit = bit.circuit() function_circuit.signature = signature_ if type_supplied_via_annotation: function.circuit = function_circuit except Exception as e: raise RuntimeError('automated circuit synthesis failed') from e # Return the original function if the function is being used as a decorator, or # the circuit if the function is being used programmatically (*i.e.*, with type # information supplied via its parameters). return function if type_supplied_via_annotation else function_circuit
if __name__ == '__main__': doctest.testmod() # pragma: no cover