PyQTorch
Fast differentiable statevector emulator based on PyTorch. The code is open source, hosted on Github (external) and maintained by Pasqal.
Backend(name=BackendName.PYQTORCH, supports_ad=True, support_bp=True, supports_adjoint=True, is_remote=False, with_measurements=True, native_endianness=Endianness.BIG, engine=Engine.TORCH, with_noise=False, config=Configuration())
dataclass
Section titled “
Backend(name=BackendName.PYQTORCH, supports_ad=True, support_bp=True, supports_adjoint=True, is_remote=False, with_measurements=True, native_endianness=Endianness.BIG, engine=Engine.TORCH, with_noise=False, config=Configuration())
dataclass
”
Bases: Backend
PyQTorch backend.
circuit(circuit)
Section titled “
circuit(circuit)
”Return the converted circuit.
Note that to get a representation with noise, noise should be passed within the config.
| PARAMETER | DESCRIPTION |
|---|---|
circuit
|
Original circuit
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
ConvertedCircuit
|
ConvertedCircuit instance for backend.
TYPE:
|
Source code in qadence/backends/pyqtorch/backend.py
98 99100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141def circuit(self, circuit: QuantumCircuit) -> ConvertedCircuit: """Return the converted circuit.
Note that to get a representation with noise, noise should be passed within the config.
Args: circuit (QuantumCircuit): Original circuit
Returns: ConvertedCircuit: ConvertedCircuit instance for backend. """ passes = self.config.transpilation_passes if passes is None: passes = default_passes(self.config)
original_circ = circuit if len(passes) > 0: circuit = transpile(*passes)(circuit) # Setting noise in the circuit. if self.config.noise: set_noise(circuit, self.config.noise)
ops = convert_block(circuit.block, n_qubits=circuit.n_qubits, config=self.config) readout_noise = ( convert_readout_noise(circuit.n_qubits, self.config.noise) if self.config.noise else None ) if self.config.dropout_probability == 0: native = pyq.QuantumCircuit( circuit.n_qubits, ops, readout_noise, ) else: native = pyq.DropoutQuantumCircuit( circuit.n_qubits, ops, readout_noise, dropout_prob=self.config.dropout_probability, dropout_mode=self.config.dropout_mode, ) return ConvertedCircuit(native=native, abstract=circuit, original=original_circ)
convert(circuit, observable=None)
Section titled “
convert(circuit, observable=None)
”Convert an abstract circuit and an optional observable to their native representation.
Additionally, this function constructs an embedding function which maps from user-facing parameters to device parameters (read more on parameter embedding here).
Source code in qadence/backend.py
188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253def convert( self, circuit: QuantumCircuit, observable: list[AbstractBlock] | AbstractBlock | None = None) -> Converted: """Convert an abstract circuit and an optional observable to their native representation.
Additionally, this function constructs an embedding function which maps from user-facing parameters to device parameters (read more on parameter embedding [here][qadence.blocks.embedding.embedding]). """
def check_observable(obs_obj: Any) -> AbstractBlock: if isinstance(obs_obj, QubitOperator): from qadence.blocks.manipulate import from_openfermion
assert len(obs_obj.terms) > 0, "Make sure to give a non-empty qubit hamiltonian"
return from_openfermion(obs_obj)
elif isinstance(obs_obj, (CompositeBlock, PrimitiveBlock, ScaleBlock)): from qadence.blocks.utils import block_is_qubit_hamiltonian
assert block_is_qubit_hamiltonian( obs_obj ), "Make sure the QubitHamiltonian consists only of Pauli operators X, Y, Z, I" return obs_obj raise TypeError( "qubit_hamiltonian should be a Pauli-like AbstractBlock or a QubitOperator" )
conv_circ = self.circuit(circuit) circ_params, circ_embedding_fn = embedding( conv_circ.abstract.block, self.config._use_gate_params, self.engine ) params = circ_params if observable is not None: observable = observable if isinstance(observable, list) else [observable] conv_obs = [] obs_embedding_fns = []
for obs in observable: obs = check_observable(obs) c_obs = self.observable(obs, max(circuit.n_qubits, obs.n_qubits)) obs_params, obs_embedding_fn = embedding( c_obs.abstract, self.config._use_gate_params, self.engine ) params.update(obs_params) obs_embedding_fns.append(obs_embedding_fn) conv_obs.append(c_obs)
def embedding_fn_dict(a: dict, b: dict) -> dict: if "circuit" in b or "observables" in b: embedding_dict = {"circuit": circ_embedding_fn(a, b), "observables": dict()} for obs_embedding_fn in obs_embedding_fns: embedding_dict["observables"].update(obs_embedding_fn(a, b)) else: embedding_dict = circ_embedding_fn(a, b) for obs_embedding_fn in obs_embedding_fns: embedding_dict.update(obs_embedding_fn(a, b)) return embedding_dict
return Converted(conv_circ, conv_obs, embedding_fn_dict, params)
def embedding_fn(a: dict, b: dict) -> dict: return circ_embedding_fn(a, b)
return Converted(conv_circ, None, embedding_fn, params)
set_block_and_readout_noises(circuit, noise, config)
Section titled “
set_block_and_readout_noises(circuit, noise, config)
”Add noise on blocks and readout on circuit.
We first start by adding noise to the abstract blocks. Then we do a conversion to their native representation. Finally, we add readout.
| PARAMETER | DESCRIPTION |
|---|---|
circuit
|
Input circuit.
TYPE:
|
noise
|
Noise to add.
TYPE:
|
Source code in qadence/backends/pyqtorch/backend.py
64656667686970717273747576777879def set_block_and_readout_noises( circuit: ConvertedCircuit, noise: NoiseHandler | None, config: Configuration) -> None: """Add noise on blocks and readout on circuit.
We first start by adding noise to the abstract blocks. Then we do a conversion to their native representation. Finally, we add readout.
Args: circuit (ConvertedCircuit): Input circuit. noise (NoiseHandler | None): Noise to add. """ if noise: set_noise(circuit, noise) set_noise_abstract_to_native(circuit, config) set_readout_noise(circuit, noise)
set_noise_abstract_to_native(circuit, config)
Section titled “
set_noise_abstract_to_native(circuit, config)
”Set noise in native blocks from the abstract ones with noise.
| PARAMETER | DESCRIPTION |
|---|---|
circuit
|
Input converted circuit.
TYPE:
|
Source code in qadence/backends/pyqtorch/backend.py
4243444546474849def set_noise_abstract_to_native(circuit: ConvertedCircuit, config: Configuration) -> None: """Set noise in native blocks from the abstract ones with noise.
Args: circuit (ConvertedCircuit): Input converted circuit. """ ops = convert_block(circuit.abstract.block, n_qubits=circuit.native.n_qubits, config=config) circuit.native = pyq.QuantumCircuit(circuit.native.n_qubits, ops, circuit.native.readout_noise)
set_readout_noise(circuit, noise)
Section titled “
set_readout_noise(circuit, noise)
”Set readout noise in place in native.
| PARAMETER | DESCRIPTION |
|---|---|
circuit
|
Input converted circuit.
TYPE:
|
noise
|
Noise.
TYPE:
|
Source code in qadence/backends/pyqtorch/backend.py
52535455565758596061def set_readout_noise(circuit: ConvertedCircuit, noise: NoiseHandler) -> None: """Set readout noise in place in native.
Args: circuit (ConvertedCircuit): Input converted circuit. noise (NoiseHandler | None): Noise. """ readout = convert_readout_noise(circuit.abstract.n_qubits, noise) if readout: circuit.native.readout_noise = readout
Configuration(_use_gate_params=True, use_sparse_observable=False, use_gradient_checkpointing=False, use_single_qubit_composition=False, transpilation_passes=None, algo_hevo=AlgoHEvo.EXP, ode_solver=SolverType.DP5_SE, n_steps_hevo=100, loop_expectation=False, noise=None, dropout_probability=0.0, dropout_mode=DropoutMode.ROTATIONAL, n_eqs=None, shift_prefac=0.5, gap_step=1.0, lb=None, ub=None)
dataclass
Section titled “
Configuration(_use_gate_params=True, use_sparse_observable=False, use_gradient_checkpointing=False, use_single_qubit_composition=False, transpilation_passes=None, algo_hevo=AlgoHEvo.EXP, ode_solver=SolverType.DP5_SE, n_steps_hevo=100, loop_expectation=False, noise=None, dropout_probability=0.0, dropout_mode=DropoutMode.ROTATIONAL, n_eqs=None, shift_prefac=0.5, gap_step=1.0, lb=None, ub=None)
dataclass
”
Bases: BackendConfiguration
algo_hevo = AlgoHEvo.EXP
class-attribute
instance-attribute
Section titled “
algo_hevo = AlgoHEvo.EXP
class-attribute
instance-attribute
”Determine which kind of Hamiltonian evolution algorithm to use.
dropout_mode = DropoutMode.ROTATIONAL
class-attribute
instance-attribute
Section titled “
dropout_mode = DropoutMode.ROTATIONAL
class-attribute
instance-attribute
”Type of quantum dropout to perform.
dropout_probability = 0.0
class-attribute
instance-attribute
Section titled “
dropout_probability = 0.0
class-attribute
instance-attribute
”Quantum dropout probability (0 means no dropout).
gap_step = 1.0
class-attribute
instance-attribute
Section titled “
gap_step = 1.0
class-attribute
instance-attribute
”Step between generated pseudo-gaps when using aGPSR algorithm.
lb = None
class-attribute
instance-attribute
Section titled “
lb = None
class-attribute
instance-attribute
”Lower bound of optimal shift value search interval.
loop_expectation = False
class-attribute
instance-attribute
Section titled “
loop_expectation = False
class-attribute
instance-attribute
”When computing batches of expectation values, only allocate one wavefunction.
Loop over the batch of parameters to only allocate a single wavefunction at any given time.
n_eqs = None
class-attribute
instance-attribute
Section titled “
n_eqs = None
class-attribute
instance-attribute
”Number of equations to use in aGPSR calculations.
n_steps_hevo = 100
class-attribute
instance-attribute
Section titled “
n_steps_hevo = 100
class-attribute
instance-attribute
”Default number of steps for the Hamiltonian evolution.
noise = None
class-attribute
instance-attribute
Section titled “
noise = None
class-attribute
instance-attribute
”NoiseHandler containing readout noise applied in backend.
ode_solver = SolverType.DP5_SE
class-attribute
instance-attribute
Section titled “
ode_solver = SolverType.DP5_SE
class-attribute
instance-attribute
”Determine which ODE solver to use for time-dependent blocks.
shift_prefac = 0.5
class-attribute
instance-attribute
Section titled “
shift_prefac = 0.5
class-attribute
instance-attribute
”Prefactor governing the magnitude of parameter shift values.
Select smaller value if spectral gaps are large.
ub = None
class-attribute
instance-attribute
Section titled “
ub = None
class-attribute
instance-attribute
”Upper bound of optimal shift value search interval.
use_gradient_checkpointing = False
class-attribute
instance-attribute
Section titled “
use_gradient_checkpointing = False
class-attribute
instance-attribute
”Use gradient checkpointing.
Recommended for higher-order optimization tasks.
use_single_qubit_composition = False
class-attribute
instance-attribute
Section titled “
use_single_qubit_composition = False
class-attribute
instance-attribute
”Composes chains of single qubit gates into a single matmul if possible.
supported_gates = list(set(OpName.list()) - set([OpName.TDAGGER]))
module-attribute
Section titled “
supported_gates = list(set(OpName.list()) - set([OpName.TDAGGER]))
module-attribute
”The set of supported gates.
Tdagger is currently not supported.
convert_block(block, n_qubits=None, config=None)
Section titled “
convert_block(block, n_qubits=None, config=None)
”Convert block to native Pyqtorch representation.
| PARAMETER | DESCRIPTION |
|---|---|
block
|
Block to convert.
TYPE:
|
n_qubits
|
Number of qubits. Defaults to None.
TYPE:
|
config
|
Backend configuration instance. Defaults to None.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
NotImplementedError
|
For non supported blocks. |
| RETURNS | DESCRIPTION |
|---|---|
Sequence[Module | Tensor | str | Expr]
|
Sequence[Module | Tensor | str | sympy.Expr]: List of native operations. |
Source code in qadence/backends/pyqtorch/convert_ops.py
177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351def convert_block( block: AbstractBlock, n_qubits: int = None, config: Configuration = None,) -> Sequence[Module | Tensor | str | sympy.Expr]: """Convert block to native Pyqtorch representation.
Args: block (AbstractBlock): Block to convert. n_qubits (int, optional): Number of qubits. Defaults to None. config (Configuration, optional): Backend configuration instance. Defaults to None.
Raises: NotImplementedError: For non supported blocks.
Returns: Sequence[Module | Tensor | str | sympy.Expr]: List of native operations. """ if isinstance(block, (Tensor, str, sympy.Expr)): # case for hamevo generators if isinstance(block, Tensor): block = block.permute(1, 2, 0) # put batch size in the back return [block] qubit_support = block.qubit_support if n_qubits is None: n_qubits = max(qubit_support) + 1
if config is None: config = Configuration()
noise: NoiseHandler | None = None if hasattr(block, "noise") and block.noise: noise = convert_digital_noise(block.noise)
if isinstance(block, ScaleBlock): scaled_ops = convert_block(block.block, n_qubits, config) scale = extract_parameter(block, config=config)
# replace underscore by dot when underscore is between two numbers in string if isinstance(scale, str): scale = replace_underscore_floats(scale)
if isinstance(scale, str) and not config._use_gate_params: param = sympy_to_pyq(sympy.parse_expr(scale)) else: param = scale
return [pyq.Scale(pyq.Sequence(scaled_ops), param)]
elif isinstance(block, TimeEvolutionBlock): duration = block.duration # type: ignore [attr-defined] if getattr(block.generator, "is_time_dependent", False): config._use_gate_params = False duration = config.get_param_name(block)[1] generator = convert_block(block.generator, config=config)[0] # type: ignore [arg-type] elif isinstance(block.generator, sympy.Basic): generator = config.get_param_name(block)[1]
elif isinstance(block.generator, Tensor): m = block.generator.to(dtype=cdouble) generator = convert_block( MatrixBlock( m, qubit_support=qubit_support, check_unitary=False, check_hermitian=True, ) )[0] else: generator = convert_block(block.generator, n_qubits, config)[0] # type: ignore[arg-type] time_param = config.get_param_name(block)[0]
# convert noise operators here noise_operators: list = [ convert_block(noise_block, config=config)[0] for noise_block in block.noise_operators ] if len(noise_operators) > 0: # squeeze batch size for noise operators noise_operators = [ pyq_op.tensor(full_support=qubit_support).squeeze(-1) for pyq_op in noise_operators ]
return [ pyq.HamiltonianEvolution( qubit_support=qubit_support, generator=generator, time=time_param, cache_length=0, duration=duration, solver=config.ode_solver, steps=config.n_steps_hevo, noise=noise_operators if len(noise_operators) > 0 else None, ) ]
elif isinstance(block, MatrixBlock): return [pyq.primitives.Primitive(block.matrix, block.qubit_support, noise=noise)] elif isinstance(block, CompositeBlock): ops = list(flatten(*(convert_block(b, n_qubits, config) for b in block.blocks))) if isinstance(block, AddBlock): return [pyq.Add(ops)] # add elif is_single_qubit_chain(block) and config.use_single_qubit_composition: return [pyq.Merge(ops)] # for chains of single qubit ops on the same qubit else: return [pyq.Sequence(ops)] # for kron and chain elif isinstance(block, tuple(non_unitary_gateset)): if isinstance(block, ProjectorBlock): projector = getattr(pyq, block.name) if block.name == OpName.N: return [projector(target=qubit_support, noise=noise)] else: return [ projector( qubit_support=qubit_support, ket=block.ket, bra=block.bra, noise=noise, ) ] else: return [getattr(pyq, block.name)(qubit_support[0])] elif isinstance(block, tuple(single_qubit_gateset)): pyq_cls = getattr(pyq, block.name) if isinstance(block, ParametricBlock): if isinstance(block, U): op = pyq_cls( qubit_support[0], *config.get_param_name(block), noise=noise, ) else: param = extract_parameter(block, config) op = pyq_cls(qubit_support[0], param, noise=noise) else: op = pyq_cls(qubit_support[0], noise=noise) # type: ignore [attr-defined] return [op] elif isinstance(block, tuple(two_qubit_gateset)): pyq_cls = getattr(pyq, block.name) if isinstance(block, ParametricBlock): op = pyq_cls( qubit_support[0], qubit_support[1], extract_parameter(block, config), noise=noise, ) else: op = pyq_cls( qubit_support[0], qubit_support[1], noise=noise # type: ignore [attr-defined] ) return [op] elif isinstance(block, tuple(three_qubit_gateset) + tuple(multi_qubit_gateset)): block_name = block.name[1:] if block.name.startswith("M") else block.name pyq_cls = getattr(pyq, block_name) if isinstance(block, ParametricBlock): op = pyq_cls( qubit_support[:-1], qubit_support[-1], extract_parameter(block, config), noise=noise, ) else: if "CSWAP" in block_name: op = pyq_cls( qubit_support[:-2], qubit_support[-2:], noise=noise # type: ignore [attr-defined] ) else: op = pyq_cls( qubit_support[:-1], qubit_support[-1], noise=noise # type: ignore [attr-defined] ) return [op] else: raise NotImplementedError( f"Non supported operation of type {type(block)}. " "In case you are trying to run an `AnalogBlock`, make sure you " "specify the `device_specs` in your `Register` first." )
convert_digital_noise(noise)
Section titled “
convert_digital_noise(noise)
”Convert the digital noise into pyqtorch NoiseProtocol.
| PARAMETER | DESCRIPTION |
|---|---|
noise
|
Noise to convert.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
DigitalNoiseProtocol | None
|
pyq.noise.DigitalNoiseProtocol | None: Pyqtorch native noise protocol if there are any digital noise protocols. |
Source code in qadence/backends/pyqtorch/convert_ops.py
354355356357358359360361362363364365366367368369370371372def convert_digital_noise(noise: NoiseHandler) -> pyq.noise.DigitalNoiseProtocol | None: """Convert the digital noise into pyqtorch NoiseProtocol.
Args: noise (NoiseHandler): Noise to convert.
Returns: pyq.noise.DigitalNoiseProtocol | None: Pyqtorch native noise protocol if there are any digital noise protocols. """ digital_part = noise.filter(NoiseProtocol.DIGITAL) if digital_part is None: return None return pyq.noise.DigitalNoiseProtocol( [ pyq.noise.DigitalNoiseProtocol(proto, option.get("error_probability")) for proto, option in zip(digital_part.protocol, digital_part.options) ] )
convert_readout_noise(n_qubits, noise)
Section titled “
convert_readout_noise(n_qubits, noise)
”Convert the readout noise into pyqtorch ReadoutNoise.
| PARAMETER | DESCRIPTION |
|---|---|
n_qubits
|
Number of qubits
TYPE:
|
noise
|
Noise to convert.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
ReadoutNoise | None
|
pyq.noise.ReadoutNoise | None: Pyqtorch native ReadoutNoise instance if readout is is noise. |
Source code in qadence/backends/pyqtorch/convert_ops.py
375376377378379380381382383384385386387388389390391392393def convert_readout_noise(n_qubits: int, noise: NoiseHandler) -> pyq.noise.ReadoutNoise | None: """Convert the readout noise into pyqtorch ReadoutNoise.
Args: n_qubits (int): Number of qubits noise (NoiseHandler): Noise to convert.
Returns: pyq.noise.ReadoutNoise | None: Pyqtorch native ReadoutNoise instance if readout is is noise. """ readout_part = noise.filter(NoiseProtocol.READOUT) if readout_part is None: return None
if readout_part.protocol[0] == NoiseProtocol.READOUT.INDEPENDENT: return pyq.noise.ReadoutNoise(n_qubits, **readout_part.options[0]) else: return pyq.noise.CorrelatedReadoutNoise(**readout_part.options[0])
extract_parameter(block, config)
Section titled “
extract_parameter(block, config)
”Extract the parameter as string or its tensor value.
| PARAMETER | DESCRIPTION |
|---|---|
block
|
Block to extract parameter from.
TYPE:
|
config
|
Configuration instance.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
str | Tensor
|
str | Tensor: Parameter value or symbol. |
Source code in qadence/backends/pyqtorch/convert_ops.py
81828384858687888990919293949596979899def extract_parameter(block: ScaleBlock | ParametricBlock, config: Configuration) -> str | Tensor: """Extract the parameter as string or its tensor value.
Args: block (ScaleBlock | ParametricBlock): Block to extract parameter from. config (Configuration): Configuration instance.
Returns: str | Tensor: Parameter value or symbol. """ if not block.is_parametric: tensor_val = tensor([block.parameters.parameter], dtype=complex64) return ( tensor([block.parameters.parameter], dtype=float64) if torch.all(tensor_val.imag == 0) else tensor_val )
return config.get_param_name(block)[0]
replace_underscore_floats(s)
Section titled “
replace_underscore_floats(s)
”Replace underscores with periods for all floats in given string.
Needed for correct parsing of string by sympy parser.
| PARAMETER | DESCRIPTION |
|---|---|
s
|
string expression
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
str
|
transformed string expression
TYPE:
|
Source code in qadence/backends/pyqtorch/convert_ops.py
102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132def replace_underscore_floats(s: str) -> str: """Replace underscores with periods for all floats in given string.
Needed for correct parsing of string by sympy parser.
Args: s (str): string expression
Returns: str: transformed string expression """
# Regular expression to match floats written with underscores instead of dots float_with_underscore_pattern = r""" (?<!\w) # Negative lookbehind to ensure not part of a word -? # Optional negative sign \d+ # One or more digits (before underscore) _ # The underscore acting as decimal separator \d+ # One or more digits (after underscore) ([eE][-+]?\d+)? # Optional exponent part for scientific notation (?!\w) # Negative lookahead to ensure not part of a word """
# Function to replace the underscore with a dot def underscore_to_dot(match: re.Match) -> Any: return match.group(0).replace("_", ".")
# Compile the regular expression pattern = re.compile(float_with_underscore_pattern, re.VERBOSE)
return pattern.sub(underscore_to_dot, s)
sympy_to_pyq(expr)
Section titled “
sympy_to_pyq(expr)
”Convert sympy expression to pyqtorch ConcretizedCallable object.
| PARAMETER | DESCRIPTION |
|---|---|
expr
|
sympy expression
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
ConcretizedCallable
|
expression encoded as ConcretizedCallable
TYPE:
|
Source code in qadence/backends/pyqtorch/convert_ops.py
135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174def sympy_to_pyq(expr: sympy.Expr) -> ConcretizedCallable | Tensor: """Convert sympy expression to pyqtorch ConcretizedCallable object.
Args: expr (sympy.Expr): sympy expression
Returns: ConcretizedCallable: expression encoded as ConcretizedCallable """
# base case - independent argument if len(expr.args) == 0: try: res = torch.as_tensor(float(expr)) except Exception as e: res = str(expr)
if "/" in res: # Found a rational res = torch.as_tensor(float(sympy.Rational(res).evalf())) return res
# Recursively iterate through current function arguments all_results = [] for arg in expr.args: res = sympy_to_pyq(arg) all_results.append(res)
# deal with multi-argument (>2) sympy functions: converting to nested # ConcretizedCallable objects if len(all_results) > 2:
def fn(x: str | ConcretizedCallable, y: str | ConcretizedCallable) -> Callable: return partial(ConcretizedCallable, call_name=SYMPY_TO_PYQ_MAPPING[expr.func])( # type: ignore [no-any-return] abstract_args=[x, y] )
concretized_callable = reduce(fn, all_results) else: concretized_callable = ConcretizedCallable(SYMPY_TO_PYQ_MAPPING[expr.func], all_results) return concretized_callable