Skip to content
Pasqal Documentation

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.

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: QuantumCircuit

RETURNS DESCRIPTION
ConvertedCircuit

ConvertedCircuit instance for backend.

TYPE: ConvertedCircuit

Source code in qadence/backends/pyqtorch/backend.py
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141def 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 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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253def 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: ConvertedCircuit

noise

Noise to add.

TYPE: NoiseHandler | None

Source code in qadence/backends/pyqtorch/backend.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79def 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: ConvertedCircuit

Source code in qadence/backends/pyqtorch/backend.py
42
43
44
45
46
47
48
49def 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 in place in native.

PARAMETER DESCRIPTION
circuit

Input converted circuit.

TYPE: ConvertedCircuit

noise

Noise.

TYPE: NoiseHandler | None

Source code in qadence/backends/pyqtorch/backend.py
52
53
54
55
56
57
58
59
60
61def 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: AbstractBlock

n_qubits

Number of qubits. Defaults to None.

TYPE: int DEFAULT: None

config

Backend configuration instance. Defaults to None.

TYPE: Configuration DEFAULT: None

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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351def 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 the digital noise into pyqtorch NoiseProtocol.

PARAMETER DESCRIPTION
noise

Noise to convert.

TYPE: NoiseHandler

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
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372def 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 the readout noise into pyqtorch ReadoutNoise.

PARAMETER DESCRIPTION
n_qubits

Number of qubits

TYPE: int

noise

Noise to convert.

TYPE: NoiseHandler

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
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393def 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 the parameter as string or its tensor value.

PARAMETER DESCRIPTION
block

Block to extract parameter from.

TYPE: ScaleBlock | ParametricBlock

config

Configuration instance.

TYPE: Configuration

RETURNS DESCRIPTION
str | Tensor

str | Tensor: Parameter value or symbol.

Source code in qadence/backends/pyqtorch/convert_ops.py
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99def 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 underscores with periods for all floats in given string.

Needed for correct parsing of string by sympy parser.

PARAMETER DESCRIPTION
s

string expression

TYPE: str

RETURNS DESCRIPTION
str

transformed string expression

TYPE: str

Source code in qadence/backends/pyqtorch/convert_ops.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132def 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)

Convert sympy expression to pyqtorch ConcretizedCallable object.

PARAMETER DESCRIPTION
expr

sympy expression

TYPE: Expr

RETURNS DESCRIPTION
ConcretizedCallable

expression encoded as ConcretizedCallable

TYPE: ConcretizedCallable | Tensor

Source code in qadence/backends/pyqtorch/convert_ops.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174def 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