Quantum
Quest

Algorithms, Math, and Physics

Implementing complex absorbing potentials in 2d Schrödinger simulation

In my work on 2d Schrödinger simulations, boundary conditions play a critical role when simulating systems with finite spatial domains. One of the challenges is ensuring that the wave function is properly absorbed at the boundaries to prevent unphysical reflections that would otherwise affect the results. This is where complex absorbing potentials (CAPs) come into play.

An absorbing condition introduces a potential at the boundaries of the simulation space that gradually absorbs the wave function, simulating the effect of an open system where the particle can escape. Without proper absorption, a wave function that reaches the edge would reflect back, interfering with the ongoing simulation and producing inaccurate results. There are several methods to implement absorbing boundary conditions, and the choice of method can have a significant impact on the quality of the simulation.

Simple absorbing boundary

One of the basic methods I’ve implemented to absorb the wave function is by adding an imaginary component to the potential using the \tanh function. The \tanh function provides a smooth transition for absorbing the wave function as it approaches the boundaries. The imaginary potential has the effect of gradually damping the amplitude of the wave function without reflecting it back.

Here’s how I implemented this method in code:


V_imag = np.zeros_like(x)
V_imag += strength * (1 - np.tanh((x - self.x_min) / width_x)**2)
V_imag += strength * (1 - np.tanh((self.x_max - x) / width_x)**2)
V_imag += strength * (1 - np.tanh((y - self.y_min) / width_y)**2)
V_imag += strength * (1 - np.tanh((self.y_max - y) / width_y)**2)

This method is straightforward and works in most cases, but it has some limitations in terms of reflection suppression, particularly when the wave function has high momentum components.

Polynomial complex absorbing potentials (CAPs)

To improve absorption efficiency, I implemented CAPs based on polynomial functions. CAPs are defined in such a way that they absorb the wave function in the outer regions while minimizing the reflection back into the simulation domain. The most common choices for polynomial CAPs are quadratic, cubic, and quartic forms. These provide increasing levels of smoothness as the power increases, with quartic CAPs generally giving the smoothest transition.

The general form of a polynomial CAP is:

V(x) = -i \eta \left( \frac{x}{L} \right)^n

where \eta is the strength of the potential, L is the width of the absorbing region, and n is the power (typically 2, 3, or 4).

In my simulation, I incorporated these different polynomial CAPs as follows:


def cap(z, z_min, z_max, width):
    z_range = z_max - z_min
    cap_width = width * z_range
    left = np.maximum(0, (z_min + cap_width - z) / cap_width)
    right = np.maximum(0, (z - (z_max - cap_width)) / cap_width)
    match cfg.cap_type:
        case 0:
            # quadratic
            return cfg.absorbing_strength * (left**2 + right**2)
        case 1:
            # cubic
            return cfg.absorbing_strength * (left**3 + right**3)
        case 2:
            # quartic
            return cfg.absorbing_strength * (left**4 + right**4)
        case _:
            raise ValueError("Unknown CAP type")

The advantage of using these polynomial forms is that they provide greater flexibility in shaping the absorbing potential, reducing the amount of unwanted reflections at the boundary.

Optimal CAP Using WKB Approximation

Beyond polynomial CAPs, I also implemented an optimal CAP based on the Wentzel-Kramers-Brillouin (WKB) approximation. Research has shown that this form of CAP offers better absorption with minimal reflections, making it ideal for simulations that require high precision.

The potential is defined as:

V(x) = -i \eta \left[ 1 + \operatorname{erf}\left(\frac{a x}{L} - 1\right)\right]

where \operatorname{erf} is the error function and a is a parameter, typically set to 2.62. This approach provides smoother absorption than the polynomial forms, with fewer reflections, especially when handling high-energy wave functions. The implementation is as follows:


def cap(z, z_min, z_max, width):
    z_range = z_max - z_min
    cap_width = width * z_range
    left = np.maximum(0, (z_min + cap_width - z) / cap_width)
    right = np.maximum(0, (z - (z_max - cap_width)) / cap_width)
    a = cfg.cap_opt_a
    left_optimal = 0.5 * cfg.absorbing_strength * (1 + erf(a * left - 1))
    right_optimal = 0.5 * cfg.absorbing_strength * (1 + erf(a * right - 1))
    return left_optimal + right_optimal

By comparing these methods, I found that the optimal CAP derived from the WKB approximation produces the best results in terms of absorption efficiency. However, the polynomial CAPs still offer a more customizable approach for those looking to fine-tune their absorbing boundaries.

Conclusion

Through my implementation of different CAP methods—ranging from \tanh functions to polynomial and WKB-based forms—I’ve been able to enhance the accuracy and realism of my 2d Schrödinger simulations. These CAP methods effectively absorb the wave function at the boundaries, allowing for cleaner, more precise simulations of quantum systems.

For more insights into this topic, you can find the details here.