diff --git a/README.md b/README.md index 61495e8..08443c5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # QM_sim -Python library for simulation of quantum mechanical systems. +Python library for simulation of quantum mechanical systems. The documentation is available on [GitHub pages](https://viljarjf.github.io/QM_sim/). [![Build docs](https://github.com/viljarjf/QM_sim/actions/workflows/build_docs.yml/badge.svg)](https://github.com/viljarjf/QM_sim/actions/workflows/build_docs.yml) diff --git a/docs/source/conf.py b/docs/source/conf.py index 19094da..c428aeb 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -9,7 +9,7 @@ project = 'QM sim' copyright = '2023, Viljar Femoen' author = 'Viljar Femoen' -release = '0.1.1' +release = '0.1.2' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/source/examples.rst b/docs/source/examples.rst deleted file mode 100644 index 8b0eb32..0000000 --- a/docs/source/examples.rst +++ /dev/null @@ -1,44 +0,0 @@ -Examples -======== - -No potential ------------- - -.. literalinclude:: ../../examples/01_zero_potential.py - :language: python - -Quadratic potential -------------------- - -.. literalinclude:: ../../examples/02_quadratic_potential.py - :language: python - -2D system ---------- - -.. literalinclude:: ../../examples/03_2D_potential.py - :language: python - -Adiabatic evolution -------------------- - -.. literalinclude:: ../../examples/04_adiabatic_evolution.py - :language: python - -Temporal evolution ------------------- - -.. literalinclude:: ../../examples/05_temporal_evolution.py - :language: python - -Temporal evolution of a 2D system ---------------------------------- - -.. literalinclude:: ../../examples/06_2D_temporal_evolution.py - :language: python - -Hydrogen atom ---------------------------------- - -.. literalinclude:: ../../examples/07_hydrogen_atom.py - :language: python diff --git a/docs/source/index.rst b/docs/source/index.rst index 267493e..5ef2949 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -19,7 +19,6 @@ A python library for simulation of quantum mechanical systems. usage api - examples contribution Indices and tables diff --git a/docs/source/usage.rst b/docs/source/usage.rst index ce7f385..358561b 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -1,5 +1,5 @@ Usage -===== +======== Installation ------------ @@ -10,3 +10,52 @@ To use QM sim, first install it using pip: $ pip install qm-sim + +To run calculations on a GPU, install the PyTorch version for your system at the +`PyTorch website `_ as well + + +Examples +-------- + +No potential +------------ + +.. literalinclude:: ../../examples/01_zero_potential.py + :language: python + +Quadratic potential +------------------- + +.. literalinclude:: ../../examples/02_quadratic_potential.py + :language: python + +2D system +--------- + +.. literalinclude:: ../../examples/03_2D_potential.py + :language: python + +Adiabatic evolution +------------------- + +.. literalinclude:: ../../examples/04_adiabatic_evolution.py + :language: python + +Temporal evolution +------------------ + +.. literalinclude:: ../../examples/05_temporal_evolution.py + :language: python + +Temporal evolution of a 2D system +--------------------------------- + +.. literalinclude:: ../../examples/06_2D_temporal_evolution.py + :language: python + +Hydrogen atom +------------- + +.. literalinclude:: ../../examples/07_hydrogen_atom.py + :language: python diff --git a/examples/02_quadratic_potential.py b/examples/02_quadratic_potential.py index 349008c..34314e6 100644 --- a/examples/02_quadratic_potential.py +++ b/examples/02_quadratic_potential.py @@ -17,7 +17,7 @@ H = Hamiltonian(N, L, m) # Set potential -x = np.linspace(-L/2, L/2, N) +x, = H.get_coordinate_arrays() def V(t): k = 200 diff --git a/examples/04_adiabatic_evolution.py b/examples/04_adiabatic_evolution.py index 529ac55..6c25749 100644 --- a/examples/04_adiabatic_evolution.py +++ b/examples/04_adiabatic_evolution.py @@ -17,8 +17,7 @@ H = Hamiltonian(N, L, m) # Set potential. Here, a square well is used, with dV = 0.15 eV -x,y = np.linspace(-L[0]/2,L[0]/2, N[0]), np.linspace(-L[0]/2,L[0]/2, N[1]) -X, Y = np.meshgrid(x,y) +X, Y = H.get_coordinate_arrays() # Smoothed rectangular time-dependent potential, assymetric to avoid degeneracy def V(t): diff --git a/examples/05_temporal_evolution.py b/examples/05_temporal_evolution.py index 69f9322..bde8782 100644 --- a/examples/05_temporal_evolution.py +++ b/examples/05_temporal_evolution.py @@ -14,7 +14,7 @@ H = Hamiltonian(N, L, m, temporal_scheme="leapfrog") # Set the potential to a quadratic potential oscilating from side to side -z = np.linspace(-L/2, L/2, N) +z, = H.get_coordinate_arrays() H.V = lambda t: 6*z**2 + 3*z*np.abs(z)*np.sin(4e15*t) # Plot diff --git a/examples/06_2D_temporal_evolution.py b/examples/06_2D_temporal_evolution.py index 7134b0b..06fdb33 100644 --- a/examples/06_2D_temporal_evolution.py +++ b/examples/06_2D_temporal_evolution.py @@ -16,9 +16,7 @@ # Set the potential. Use a cool elipse cross that rotates over time a = 2e-9 b = 5e-9 -x = np.linspace(-L[0]/2, L[0]/2, N[0]) -y = np.linspace(-L[1]/2, L[1]/2, N[1]) -X, Y = np.meshgrid(x, y) +X, Y = H.get_coordinate_arrays() def V(theta: float) -> np.ndarray: ct, st = np.cos(theta), np.sin(theta) diff --git a/examples/07_hydrogen_atom.py b/examples/07_hydrogen_atom.py index 9a67abe..4412ca4 100644 --- a/examples/07_hydrogen_atom.py +++ b/examples/07_hydrogen_atom.py @@ -20,7 +20,7 @@ H = Hamiltonian(N, L, m) # Define the potential: -e^2 / (4*pi*𝜀_0*r) -x, y, z = np.meshgrid(*(np.linspace(-L[i]/2, L[i]/2, N[i]) for i in range(3))) +x, y, z = H.get_coordinate_arrays() r = (x**2 + y**2 + z**2)**0.5 H.V = -e_0**2 / (4 * np.pi * 𝜀_0 * r) diff --git a/pyproject.toml b/pyproject.toml index 27951a5..1a51a96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "qm_sim" -version = "0.1.1" +version = "0.1.2" authors = [ { name="Viljar Femoen", email="author@example.com" }, ] diff --git a/src/qm_sim/hamiltonian/spatial_hamiltonian.py b/src/qm_sim/hamiltonian/spatial_hamiltonian.py index 455be31..d6a36df 100644 --- a/src/qm_sim/hamiltonian/spatial_hamiltonian.py +++ b/src/qm_sim/hamiltonian/spatial_hamiltonian.py @@ -4,7 +4,6 @@ """ -from collections.abc import Iterable from typing import Any, Callable import numpy as np @@ -16,7 +15,11 @@ from ..eigensolvers import get_eigensolver from ..nature_constants import h_bar from ..spatial_derivative import get_scheme_order -from ..spatial_derivative.cartesian import laplacian, nabla +from ..spatial_derivative.cartesian import ( + CartesianDiscretization, + laplacian, + nabla, +) from ..temporal_solver import TemporalSolver, get_temporal_solver @@ -84,29 +87,11 @@ def __init__( Defaults to "zero" :type boundary_condition: str, optional """ - # 1D inputs - if isinstance(N, int): - N = (N,) - if isinstance(L, (float, int)): - L = (L,) - - # Allow any iterable that can be converted to a tuple - if isinstance(N, Iterable): - N = tuple(N) - if isinstance(L, Iterable): - L = tuple(L) - - # Check type - if not isinstance(N, tuple) or not all(isinstance(i, int) for i in N): - raise ValueError(f"Param `N` must be int or tuple of ints, got {type(N)}") - if not isinstance(L, tuple) or not all(isinstance(i, (float, int)) for i in L): - raise ValueError(f"Param `L` must be float or tuple, got {type(L)}") - - if len(N) != len(L): - raise ValueError("`N`and `L`must have same length") - - self.N = N - self.L = L + # Creating this object performs the necessary type checking + self.discretization = CartesianDiscretization(L, N) + self.N = self.discretization.N + self.L = self.discretization.L + self.deltas = self.discretization.dx self.eigensolver = get_eigensolver(eigensolver) if self.eigensolver is None: @@ -116,9 +101,6 @@ def __init__( if order is None: raise ValueError("Requested finite difference is invalid") - self._dim = len(N) - self.delta = [Li / Ni for Li, Ni in zip(L, N)] - # Handle non-isotropic effective mass if isinstance(m, np.ndarray) and np.all(m == m.flat[0]): m = m.flatten()[0] @@ -130,8 +112,12 @@ def __init__( ) m_inv = 1 / m.flatten() - _n = nabla(N, L, order=order, boundary_condition=boundary_condition) - _n2 = laplacian(N, L, order=order, boundary_condition=boundary_condition) + _n = nabla( + self.discretization, order=order, boundary_condition=boundary_condition + ) + _n2 = laplacian( + self.discretization, order=order, boundary_condition=boundary_condition + ) # nabla m_inv nabla + m_inv nabla^2 _n.data *= _n @ m_inv # First term @@ -139,7 +125,7 @@ def __init__( self.mat = _n + _n2 else: self.mat = laplacian( - N, L, order=order, boundary_condition=boundary_condition + self.discretization, order=order, boundary_condition=boundary_condition ) self.mat *= 1 / m @@ -427,3 +413,10 @@ def asarray(self) -> np.ndarray: :rtype: np.ndarray """ return self.mat.toarray() + + def get_coordinate_arrays(self) -> tuple[np.ndarray]: + return self.discretization.get_coordinate_arrays() + + get_coordinate_arrays.__doc__ = ( + CartesianDiscretization.get_coordinate_arrays.__doc__ + ) diff --git a/src/qm_sim/spatial_derivative/cartesian.py b/src/qm_sim/spatial_derivative/cartesian.py index f804bbc..9911896 100644 --- a/src/qm_sim/spatial_derivative/cartesian.py +++ b/src/qm_sim/spatial_derivative/cartesian.py @@ -1,10 +1,121 @@ +from typing import Iterable + import numpy as np from scipy import sparse as sp +from typing_extensions import Self + + +class CartesianDiscretization: + """Helper class for discretization of cartesian space""" + + def __init__(self, L: float | tuple[float], N: int | tuple[int]): + """Initialize a discretization objects. Allows non-tuple inputs + + :param L: System size along each dimension + :type L: float | tuple[float] + :param N: Discretization count along each dimension + :type N: int | tuple[int] + """ + # 1D inputs + if isinstance(N, int): + N = (N,) + if isinstance(L, (float, int)): + L = (L,) + + # Allow any iterable that can be converted to a tuple + if isinstance(N, Iterable): + N = tuple(N) + if isinstance(L, Iterable): + L = tuple(L) + + # Check type + if not isinstance(N, tuple) or not all(isinstance(i, int) for i in N): + raise ValueError(f"Param `N` must be int or tuple of ints, got {type(N)}") + if not isinstance(L, tuple) or not all(isinstance(i, (float, int)) for i in L): + raise ValueError(f"Param `L` must be float or tuple, got {type(L)}") + + if len(N) != len(L): + raise ValueError("Inputs must have same length") + + self.N = N + self.L = L + self.dx = (Li / Ni for Li, Ni in zip(self.L, self.N)) + + @classmethod + def from_dx(cls, dx: float | tuple[float], N: int | tuple[int]) -> Self: + """Initialize a discretization objects. Allows non-tuple inputs + + :param dx: Discretzation length along each dimension, + i.e. distance between discretization points + :type dx: float | tuple[float] + :param N: Discretization count along each dimension + :type N: int | tuple[int] + """ + if isinstance(N, int): + N = (N,) + if isinstance(dx, (float, int)): + dx = (dx,) + + # Allow any iterable that can be converted to a tuple + if isinstance(N, Iterable): + N = tuple(N) + if isinstance(dx, Iterable): + dx = tuple(dx) + + # Check type + if not isinstance(N, tuple) or not all(isinstance(i, int) for i in N): + raise ValueError(f"Param `N` must be int or tuple of ints, got {type(N)}") + if not isinstance(dx, tuple) or not all( + isinstance(i, (float, int)) for i in dx + ): + raise ValueError(f"Param `dx` must be float or tuple, got {type(dx)}") + + if len(N) != len(dx): + raise ValueError("Inputs must have same length") + + L = (Ni * dxi for Ni, dxi in zip(N, dx)) + return cls(L, N) + + def get_coordinate_axes(self, centering: str = "middle") -> tuple[np.ndarray]: + """Return an array of shape (N_i,) for the ith dimension, with corresponding coordinates + + :param centering: Where to place the origin. + Options: + + - "middle" [default]: Center of system + - "first": First element in the array + + :type centering: str, optional + :return: Coordinate axes + :rtype: tuple[np.ndarray] + """ + if centering == "middle": + return (np.linspace(-Li / 2, Li / 2, Ni) for Li, Ni in zip(self.L, self.N)) + elif centering == "first": + return ( + np.linspace(0, Li, Ni, endpoint=False) for Li, Ni in zip(self.L, self.N) + ) + else: + raise ValueError("Invalid centering parameters") + + def get_coordinate_arrays(self, centering: str = "middle") -> tuple[np.ndarray]: + """Return arrays with shape `N` of coordinates for each point in the system. + + :param centering: Where to place the origin. + Options: + + - "middle" [default]: Center of system + - "first": First element in the array + + :type centering: str, optional + :return: Coordinate arrays + :rtype: tuple[np.ndarray] + """ + return np.meshgrid(*self.get_coordinate_axes(centering)) def nabla( - N: tuple[int], - L: tuple[float], + discretization: CartesianDiscretization, order: int = 2, dtype: type = np.float64, boundary_condition: str = "zero", @@ -14,20 +125,19 @@ def nabla( Example: approximate the derivative of sin(x). - >>> from qm_sim.spatial_derivative.cartesian import nabla + >>> from qm_sim.spatial_derivative.cartesian import nabla, CartesianDiscretization >>> import numpy as np - >>> N = (1000,) - >>> L = (2*np.pi,) - >>> n = nabla( N, L, boundary_condition="periodic") - >>> x = np.linspace( 0, L[0], N[0], endpoint=False ) + >>> disc = CartesianDiscretization(2*np.pi, 1000) + >>> n = nabla( disc, boundary_condition="periodic") + >>> x, = disc.get_coordinate_arrays( centering="first" ) >>> y = np.sin(x) >>> np.allclose(n @ y, np.cos(x)) # The analytical solution is cos(x) True :param N: Discretization count along each axis - :type N: tuple[int] + :type N: tuple[int] | int :param L: System size along each axis - :type L: tuple[float] + :type L: tuple[float] | float :param order: Numerical order of the differentiation scheme. Options are: @@ -72,12 +182,13 @@ def nabla( stencil += [0] stencil = _mirror_sign_list(stencil) + N = discretization.N + L = discretization.L return _matrix_from_central_stencil(stencil, 1, N, L, dtype, boundary_condition) def laplacian( - N: tuple[int], - L: tuple[float], + discretization: CartesianDiscretization, order: int = 2, dtype: type = np.float64, boundary_condition: str = "zero", @@ -87,20 +198,19 @@ def laplacian( Example: approximate the second derivative of sin(x). - >>> from qm_sim.spatial_derivative.cartesian import laplacian + >>> from qm_sim.spatial_derivative.cartesian import laplacian, CartesianDiscretization >>> import numpy as np - >>> N = (1000,) - >>> L = (2*np.pi,) - >>> n = laplacian( N, L, boundary_condition="periodic" ) - >>> x = np.linspace( 0, L[0], N[0], endpoint=False) + >>> disc = CartesianDiscretization(2*np.pi, 1000) + >>> n = laplacian( disc, boundary_condition="periodic") + >>> x, = disc.get_coordinate_arrays( centering="first" ) >>> y = np.sin(x) >>> np.allclose(n @ y, -np.sin(x)) # The analytical solution is -sin(x) True :param N: Discretization count along each axis - :type N: tuple[int] + :type N: tuple[int] | int :param L: System size along each axis - :type L: tuple[float] + :type L: tuple[float] | float :param order: Numerical order of the differentiation scheme. Options are: @@ -142,6 +252,8 @@ def laplacian( ) stencil = _mirror_list(stencil) + N = discretization.N + L = discretization.L return _matrix_from_central_stencil(stencil, 2, N, L, dtype, boundary_condition) @@ -154,7 +266,7 @@ def _matrix_from_central_stencil( boundary_condition: str = "zero", ) -> sp.dia_matrix: """ - Creates a full matrix from a central stencil. Determines indices from stencil + Creates a full matrix from a central stencil. Determines indices from stencil. """ available_boundary_conditions = ["zero", "periodic"]