Skip to Content

Schrödingerization Method

Overview

The unitarylab_algorithms.schrodingerization package implements algorithms that convert the solution of partial differential equations (PDEs) into quantum simulations via the Schrödingerization technique — embedding non-Hermitian (non-unitary) dynamics into a larger quantum system governed by the Schrödinger equation, thereby enabling unitary simulation on a quantum computer.

Core idea: a classical PDE (where is not necessarily anti-Hermitian) is embedded, via a change of variables, into a quantum system described by an equivalent Schrödinger equation.

EquationModuleAlgorithm ClassDescription
1D Advection Equationequation_advectionAdvectionEquationAlgorithmLinear transport equation
1D Heat Equationequation_heatHeatEquationAlgorithmDiffusion equation
2D Heat Equationequation_heat2dHeat2dEquationAlgorithm2D heat diffusion

This module uses a standalone PDE algorithm interface and setup.json configuration. In the return value, circuit is a list of circuit-diagram information, and plot is the solution’s visualization information. The advection equation supports classical and trotter; the heat equation’s block option is currently computed using the classical method. See the corresponding sections below for the parameters and output structure supported by each method.


Base Class: unitarylab_algorithms.schrodingerization.base

All Schrödingerization algorithms inherit from unitarylab_algorithms.schrodingerization.base.BaseAlgorithm.

Abstract Interface

class BaseAlgorithm(ABC): def __init__(self): self.color = "#DBB924" self.algo_dir = None self.logger = None @abstractmethod def run(self, params: str) -> Dict[str, Any]: """Execute the algorithm logic. params is in JSON string format.""" ... def parse_params(self, params) -> Any: """Parse parameters: tries json.loads first, falling back to ast.literal_eval on failure; compatible with JSON strings, Python literal strings, and already-parsed dict/list objects.""" ...

The run(self, params: str) signature declared on the abstract base class is only a placeholder declaration — Python does not check signature compatibility for methods overridden in subclasses. The signature actually implemented by the three concrete subclasses (and actually used by __main__ and external callers) is:

def run(self, params=None, algo_dir: str = None, backend='torch', device='cpu', dtype=np.complex128) -> Dict[str, Any]:

When params=None, default parameters are automatically loaded from setup.json in the algorithm module’s directory; passing a dict/list uses it directly; passing a JSON string causes it to be parsed internally by parse_equation (first via json.loads; if the result contains a "params" key, ["params"] is taken).

Logger Utility

The create_algorithm_logger function creates a separate rotating file logger for each algorithm, writing to algo_dir/algorithm.log (and a separate algorithm_error.log):

from unitarylab_algorithms.schrodingerization.base import create_algorithm_logger logger = create_algorithm_logger(__file__) logger.info("Algorithm started")

Parameters:

ParameterTypeDefaultDescription
algorithm_filestrTypically pass the algorithm module’s __file__; used to determine the default log directory
log_namestr | NoneNoneLogger name (defaults to the algorithm folder name)
log_dirstr | NoneNoneLog directory (defaults to the directory containing the algorithm file)
console_outputboolTrueWhether to also output to the console
max_bytesint50 * 1024 * 1024 (50 MB)Maximum size in bytes of a single log file
backup_countint5Number of backup log files retained
force_reconfigureboolFalseForce re-initialization (used during hot reload)

This function detects sandbox environments (determined by the simultaneous presence of the TASK_ID and ALGO_PARAMS environment variables); in that case, even if console_output=True, the console StreamHandler is skipped and only the file is written, to avoid duplicate log collection by the host process.

Plotting Helper Methods

BaseAlgorithm also provides protected helper methods for subclasses to call inside their _solve_* methods, including set_plt(color='white') (uniformly sets matplotlib text/axis colors), _generate_solution_plot_1d(...) (1D solution curve plot), and _generate_circuit_plots(name, qc, H1=None, H2=None, format='svg') (generates quantum circuit diagrams — the full circuit diagram corresponding to qc is always generated, and when H1/H2 are not None, one additional decomposed circuit diagram is appended for each, so the returned list length is between 1 and 3). Not every subclass calls _generate_solution_plot_1d via its own _generate_solution_plot wrapper — the 1D advection equation and 1D heat equation directly inherit the base class’s _generate_solution_plot (which internally calls _generate_solution_plot_1d), whereas the 2D heat equation completely overrides _generate_solution_plot, using its own independently implemented 3D surface plot (ax.plot_surface(...)) instead, and never calls _generate_solution_plot_1d — see the algorithm-specific sections below for details.


1D Advection Equation

Background

The linear advection equation describes the transport of a scalar field at constant velocity :

The Schrödingerization method encodes as a quantum state and evolves it under the equivalent Schrödinger equation (with , ), thereby achieving unitary quantum simulation of this non-unitary classical PDE.

Import

from unitarylab_algorithms import AdvectionEquationAlgorithm

.run() Parameters

def run(self, params=None, algo_dir: str = None, backend='torch', device='cpu', dtype=np.complex128) -> Dict[str, Any]
ParameterTypeDefaultDescription
paramsNone | str | dict | listNoneWhen None, automatically loads setup.json within the module; a string can be either JSON content or a path pointing to a JSON file (a file-path branch unique to AdvectionEquationAlgorithm); an already-parsed dict/list may also be passed directly
algo_dirstr | NoneNoneOutput directory for results and logs; when None, automatically creates ./results/schrodingerization/equation_advection/
backendstr'torch'Only takes effect when method='trotter', forwarded as-is to the underlying schro_trotter
devicestr'cpu'Same as above
dtypenumpy dtypenp.complex128Same as above

Key fixed parameters of the default equation configuration in setup.json (the equation_advection block):

ParameterDefaultRangeDescription
a0.1[-100, 100]Advection velocity; the sign determines the direction of the upwind scheme
L16(0, 100]Length of the computational domain, interval
T1(0, 100]Final time
nx4[1, 10]Number of spatial qubits

Solution method configuration (the solutionMethod_classical block):

ParameterDefaultDescription
na8Number of auxiliary qubits, controlling the discretization precision of the Schrödingerization parameter
R4The truncation interval for is
p (point)1After Schrödingerization, the solution to the original equation is recovered at this point

The boundary condition defaults to boundaryCondition_periodic (periodic boundary), and the initial value defaults to initialCondition_discontinuous (a step initial value: 0 where and 1 elsewhere); the discretization scheme defaults to discreteFormat_upwind (upwind scheme; when scheme='upwind', _solve_classical automatically switches to 'forward'/'backward' based on the sign of a).

Return Value

Both _solve_classical and _solve_trotter return:

{ "status": "ok", "message": "Advection equation solved", "grid": {"n_points": 2**nx, "dx": dx}, # the trotter branch additionally includes "dt", "nt" "x": [...], # spatial grid coordinates (list) "u": [...], # solution at the final time (list) "circuit": [{"format": "svg", "filename": "..."}], # always exactly 1 full circuit diagram; neither `_solve_*` branch passes H1/H2 to `_generate_circuit_plots` "plot": {"format": "svg", "filename": "..."}, # visualization of the solution (single dict) }

Example

from unitarylab_algorithms import AdvectionEquationAlgorithm algo = AdvectionEquationAlgorithm() result = algo.run() # params=None, uses the setup.json default parameters (classical method) print(result["status"], result["message"]) print("Number of circuit diagrams:", len(result["circuit"]))

When passing custom parameters (as a dict, corresponding directly to the params array structure in setup.json), construct the equation and solutionMethod_* blocks according to the full schema of setup.json; alternatively, first json.load the default setup.json, modify the par_fix/value fields, and then pass it in.


1D Heat Equation

Background

The 1D heat equation describes the diffusion process of a scalar field:

where is the thermal diffusivity, which must satisfy . The operator is a negative semi-definite non-Hamiltonian type, requiring a Schrödingerization embedding (, ) for unitary simulation.

Import

from unitarylab_algorithms import HeatEquationAlgorithm

.run() Parameters

def run(self, params=None, algo_dir: str = None, backend='torch', device='cpu', dtype=np.complex128) -> Dict[str, Any]

The meaning of the parameters is the same as for the advection equation, but the file-path form of the params string is not supported — only None, an already-parsed object, or a directly parseable configuration string are accepted. The current version’s Trotter path uses the default execution backend configuration.

Default equation configuration in setup.json (the equation_heat block):

ParameterDefaultRangeDescription
a1(0, 100]Diffusion coefficient, requires a > 0
L17(0, 100]Length of the computational domain
T1(0, 100]Final time
nx5[1, 10]Number of spatial qubits
f(x) (source term)"a+x"Parsed by sympy; the variable x and the constants a, L are available

The boundary condition defaults to boundaryCondition_dirichlet (), and the initial value defaults to initialCondition_custom (a custom must be supplied via par_func).

method='block' is currently equivalent to method='classical': the full implementation of _solve_block(self, eq) is:

def _solve_block(self, eq): self.logger.info('Block encoding will be supported soon! Now falling back to classical method!') return self._solve_classical(eq)

Yet setup.json’s method_list lists "solutionMethod_block", which can easily give the impression that block encoding has already been implemented. Selecting this method actually yields the result of the classical matrix-exponential method — do not use its performance or circuit structure as representative of a true block-encoding algorithm.

Return Value

The field shape is the same as for the advection equation (see above), and the message field is "Heat equation solved", but the length of the circuit list depends on the solution method: with method='classical', only 1 full circuit diagram is generated; with method='trotter', the non-empty H1 and H2 are also passed to _generate_circuit_plots(...), so 3 circuit diagrams are generated (the full circuit, H1, and H2); with method='block', which currently falls back to classical, it is likewise 1 diagram.

Example

from unitarylab_algorithms import HeatEquationAlgorithm algo = HeatEquationAlgorithm() result = algo.run() # defaults to the classical method print(result["status"], result["grid"])

2D Heat Equation

Background

The 2D heat equation extends scalar diffusion to two spatial dimensions:

Quantum simulation requires spatial qubits ( each for the and directions), and the Schrödingerization embedding is similar to the 1D case (, ).

Import

from unitarylab_algorithms import Heat2dEquationAlgorithm

Note that the class name capitalization is Heat2d (not Heat2D) — both from .algorithm import Heat2dEquationAlgorithm in __init__.py and the package-level ALGORITHM_NAME = "heat2d" follow this spelling.

.run() Parameters

def run(self, params=None, algo_dir: str = None, backend='torch', device='cpu', dtype=np.complex128) -> Dict[str, Any]

The file-path form of params is likewise not supported. With method='trotter', the execution device can be selected via device; the backend and numerical precision use the default configuration.

Default equation configuration in setup.json (the equation_heat2d block):

ParameterDefaultRangeDescription
a11(0, 100]Diffusion coefficient in the direction
a21(0, 100]Diffusion coefficient in the direction
L17(0, 100]Length of the computational domain (shared by and )
T1(0, 100]Final time
nx4[1, 10]Number of spatial qubits per direction (total spatial qubits is 2*nx)

The boundary condition defaults to boundaryCondition_dirichlet, the initial value defaults to initialCondition_custom, with a default of . method_list lists only ["solutionMethod_classical", "solutionMethod_trotter"] (not including block).

Even though setup.json does not declare the block method, the code of Heat2dEquationAlgorithm.run() still dispatches method == 'block' to _solve_block(self, eq), whose implementation is identical to that of the 1D heat equation — it logs "Block encoding will be supported soon!..." and then directly calls _solve_classical(eq). Therefore, when "solutionMethod_block" is explicitly specified via custom params (rather than the default setup.json), the result obtained is likewise only that of the classical method.

Return Value

{ "status": "ok", "message": "2D Heat equation solved", "grid": {"n_points": 2**nx, "dx": dx}, # the trotter branch additionally includes "dt", "nt" "x": [...], "y": [...], # unique to the 2D heat equation: additional grid coordinates in the y direction "u": [[...], ...], # a 2D solution of shape (2**nx, 2**nx), serialized as a list of lists "circuit": [{"format": "svg", "filename": "..."}, ...], "plot": {"format": "svg", "filename": "..."}, # a 3D surface plot (plot_surface), not a 1D line plot }

Example

from unitarylab_algorithms import Heat2dEquationAlgorithm algo = Heat2dEquationAlgorithm() result = algo.run() print(result["status"], "grid:", result["grid"]) print("Number of grid points in the y direction:", len(result["y"]))

General Notes

  • None of the three algorithms use unitarylab_algorithms.algo_base.BaseAlgorithm, and therefore they do not have mechanisms shared by other packages such as _build_return_dict(), self.output, or self.status; do not assume the fields of the returned dict are consistent with other chapters.
  • When params=None, the setup.json in the algorithm’s own directory is automatically loaded to solve a default example once — this is also the behavior at the end of each algorithm.py file’s if __name__ == "__main__": result = XxxAlgorithm().run().
  • method is determined by the type field of the solver block in setup.json (or the passed-in parameters) (corresponding to the portion of the type value — such as solutionMethod_classical/solutionMethod_trotter/solutionMethod_block — remaining after removing the prefix, e.g. 'classical'/'trotter'/'block'); the values supported by the three algorithms are not entirely identical — see their respective sections for details.
  • _solve_block is currently a “fall back to the classical method” placeholder implementation in both equation_heat and equation_heat2d, while equation_advection does not accept 'block' at all; none of the three has a true block-encoding circuit implementation.
  • Before processing external input, algo.parse_params(params) can be used to safely deserialize parameters (first attempting json.loads, falling back to ast.literal_eval on failure).
  • These algorithms are computationally expensive for fine spatial grids (the matrix dimension grows as 2**nx, or 2**(2*nx) in the 2D case). When validating your environment configuration, it is recommended to start with a smaller nx (e.g., 4–6) and then scale up gradually.
  • Each algorithm internally uses create_algorithm_logger(algo_dir) to configure logging; log files are written to algo_dir/algorithm.log (and algorithm_error.log), and console output is automatically skipped in sandbox environments to avoid duplicate logs.
Last updated on