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.
| Equation | Module | Algorithm Class | Description |
|---|---|---|---|
| 1D Advection Equation | equation_advection | AdvectionEquationAlgorithm | Linear transport equation |
| 1D Heat Equation | equation_heat | HeatEquationAlgorithm | Diffusion equation |
| 2D Heat Equation | equation_heat2d | Heat2dEquationAlgorithm | 2D 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 fromsetup.jsonin the algorithm module’s directory; passing a dict/list uses it directly; passing a JSON string causes it to be parsed internally byparse_equation(first viajson.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:
| Parameter | Type | Default | Description |
|---|---|---|---|
algorithm_file | str | — | Typically pass the algorithm module’s __file__; used to determine the default log directory |
log_name | str | None | None | Logger name (defaults to the algorithm folder name) |
log_dir | str | None | None | Log directory (defaults to the directory containing the algorithm file) |
console_output | bool | True | Whether to also output to the console |
max_bytes | int | 50 * 1024 * 1024 (50 MB) | Maximum size in bytes of a single log file |
backup_count | int | 5 | Number of backup log files retained |
force_reconfigure | bool | False | Force re-initialization (used during hot reload) |
This function detects sandbox environments (determined by the simultaneous presence of the
TASK_IDandALGO_PARAMSenvironment variables); in that case, even ifconsole_output=True, the consoleStreamHandleris 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]| Parameter | Type | Default | Description |
|---|---|---|---|
params | None | str | dict | list | None | When 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_dir | str | None | None | Output directory for results and logs; when None, automatically creates ./results/schrodingerization/equation_advection/ |
backend | str | 'torch' | Only takes effect when method='trotter', forwarded as-is to the underlying schro_trotter |
device | str | 'cpu' | Same as above |
dtype | numpy dtype | np.complex128 | Same as above |
Key fixed parameters of the default equation configuration in setup.json (the equation_advection block):
| Parameter | Default | Range | Description |
|---|---|---|---|
a | 0.1 | [-100, 100] | Advection velocity; the sign determines the direction of the upwind scheme |
L | 16 | (0, 100] | Length of the computational domain, interval |
T | 1 | (0, 100] | Final time |
nx | 4 | [1, 10] | Number of spatial qubits |
Solution method configuration (the solutionMethod_classical block):
| Parameter | Default | Description |
|---|---|---|
na | 8 | Number of auxiliary qubits, controlling the discretization precision of the Schrödingerization parameter |
R | 4 | The truncation interval for is |
p (point) | 1 | After 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):
| Parameter | Default | Range | Description |
|---|---|---|---|
a | 1 | (0, 100] | Diffusion coefficient, requires a > 0 |
L | 17 | (0, 100] | Length of the computational domain |
T | 1 | (0, 100] | Final time |
nx | 5 | [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 tomethod='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’smethod_listlists"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 Heat2dEquationAlgorithmNote that the class name capitalization is
Heat2d(notHeat2D) — bothfrom .algorithm import Heat2dEquationAlgorithmin__init__.pyand the package-levelALGORITHM_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):
| Parameter | Default | Range | Description |
|---|---|---|---|
a1 | 1 | (0, 100] | Diffusion coefficient in the direction |
a2 | 1 | (0, 100] | Diffusion coefficient in the direction |
L | 17 | (0, 100] | Length of the computational domain (shared by and ) |
T | 1 | (0, 100] | Final time |
nx | 4 | [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.jsondoes not declare theblockmethod, the code ofHeat2dEquationAlgorithm.run()still dispatchesmethod == '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 customparams(rather than the defaultsetup.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, orself.status; do not assume the fields of the returned dict are consistent with other chapters. - When
params=None, thesetup.jsonin the algorithm’s own directory is automatically loaded to solve a default example once — this is also the behavior at the end of eachalgorithm.pyfile’sif __name__ == "__main__": result = XxxAlgorithm().run(). methodis determined by thetypefield of thesolverblock insetup.json(or the passed-in parameters) (corresponding to the portion of thetypevalue — such assolutionMethod_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_blockis currently a “fall back to the classical method” placeholder implementation in bothequation_heatandequation_heat2d, whileequation_advectiondoes 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 attemptingjson.loads, falling back toast.literal_evalon failure). - These algorithms are computationally expensive for fine spatial grids (the matrix dimension grows as
2**nx, or2**(2*nx)in the 2D case). When validating your environment configuration, it is recommended to start with a smallernx(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 toalgo_dir/algorithm.log(andalgorithm_error.log), and console output is automatically skipped in sandbox environments to avoid duplicate logs.