Using and reusing circuits

A Circuit owns diagram storage. A transformation moves that storage into its result; the old Python object remains present but is marked consumed. Queries such as model_count(), is_sat(), and equivalent() borrow the circuit and can be repeated.

>>> from tididi import Vtree, literal, ConsumedCircuitError
>>> vtree = Vtree.balanced(2)
>>> a = literal(vtree, 1)
>>> b = literal(vtree, 2)
>>> result = a & b
>>> print(result.model_count(), a.is_consumed, b.is_consumed)
1 True True
>>> try:
...     a.model_count()
... except ConsumedCircuitError as error:
...     print(error)
Circuit has been consumed. Copy it before passing it to a consuming operation.

Copies and aliases

a.copy() duplicates the circuit’s storage and shares its vtree. Python’s copy.copy(a) and copy.deepcopy(a) have the same behavior: the vtree stays shared so the copies can still be combined.

Assignment creates an alias, not a copy:

>>> a = literal(vtree, 1)
>>> alias = a
>>> saved = a.copy()
>>> negated = ~a
>>> print(alias.is_consumed, saved.model_count(), negated.model_count())
True 2 2

The same object cannot occupy two consuming operand positions. This includes a & a, aliases of a, and repeated entries in or_many. Use a.copy() & a when you need two occurrences. Validation rejects duplicates before consuming any operand. Borrowing queries such as a.equivalent(a) accept aliases.

Errors and resource limits

Both operators and named functions raise Python exceptions. A wrong argument type, a consumed operand, mismatched vtrees, or an invalid variable is rejected before the wrapper transfers valid operands. Once the Rust computation starts, a consuming operation keeps its inputs consumed even if it fails. Copy before the call if you need the original for a retry.

MemoryError reports a refused allocation or byte budget; tididi.ResourceLimitError reports a deadline or output-node cap; ValueError reports invalid input; OverflowError reports an exhausted index range. Borrowing queries leave their inputs usable on failure.

circuit = circuit.minimize() and circuit = circuit.update(...) follow the same consuming convention. A counter takes ownership with counter = circuit.counter(); circuit = counter.finish() returns the original diagram and discards the counter’s observations and cache.

Threaded callers

Native construction, Boolean operations, counting, and weighted evaluation release the interpreter lock while they run. Each operation uses the calling thread; tididi starts no worker threads. Use independent circuit objects for concurrent transformations. Attempts to mutate an object already borrowed by another operation raise a runtime borrowing error. Python algebra callbacks run with the interpreter attached and propagate their exceptions.