diff --git a/lettuce/_flow.py b/lettuce/_flow.py index 8fb9fbf5..5a38a044 100644 --- a/lettuce/_flow.py +++ b/lettuce/_flow.py @@ -6,7 +6,8 @@ from typing import Optional, List, Union, Callable, AnyStr from abc import ABC, abstractmethod -from . import * +from . import TorchStencil +from .util import torch_gradient, torch_jacobi from .cuda_native import NativeEquilibrium __all__ = ['Equilibrium', 'Flow', 'Boundary'] @@ -67,6 +68,8 @@ class Flow(ABC): torch_stencil: 'TorchStencil' equilibrium: 'Equilibrium' boundaries: List['Boundary'] + initialize_pressure: bool = False + initialize_fneq: bool = False # current physical state i: int @@ -76,7 +79,6 @@ class Flow(ABC): def __init__(self, context: 'Context', resolution: List[int], units: 'UnitConversion', stencil: 'Stencil', equilibrium: 'Equilibrium'): - self.context = context self.resolution = resolution self.units = units @@ -108,8 +110,16 @@ def initialize(self): self.units.convert_pressure_pu_to_density_lu(initial_p)) initial_u = self.context.convert_to_tensor( self.units.convert_velocity_to_lu(initial_u)) - self.f = self.context.convert_to_tensor( - self.equilibrium(self, rho=initial_rho, u=initial_u)) + if self.initialize_pressure: + initial_rho = pressure_poisson( + self.units, + initial_u, + initial_rho + ) + self.f = self.equilibrium(self, rho=initial_rho, u=initial_u) + self.f = self.equilibrium(self, rho=initial_rho, u=initial_u) + if self.initialize_fneq: + self.f = initialize_f_neq(self) @property def f_next(self) -> torch.Tensor: @@ -139,19 +149,22 @@ def p_pu(self) -> torch.Tensor: def u_pu(self): return self.units.convert_velocity_to_pu(self.u()) - def j(self) -> torch.Tensor: + def j(self, f: Optional[torch.Tensor] = None) -> torch.Tensor: """momentum""" - return self.einsum("qd,q->d", [self.torch_stencil.e, self.f]) + return self.einsum("qd,q->d", + [self.torch_stencil.e, self.f if f is None else f]) - def u(self, rho=None, acceleration=None) -> torch.Tensor: + def u(self, f: Optional[torch.Tensor] = None, rho=None, acceleration=None + ) -> torch.Tensor: """velocity; the `acceleration` is used to compute the correct velocity in the presence of a forcing scheme.""" - rho = self.rho() if rho is None else rho - v = self.j() / rho + rho = self.rho(f=f) if rho is None else rho + v = self.j(f=f) / rho # apply correction due to forcing, which effectively averages the pre- # and post-collision velocity - correction = 0.0 - if acceleration is not None: + if acceleration is None: + correction = 0.0 + else: if len(acceleration.shape) == 1: index = [Ellipsis] + [None] * self.stencil.d acceleration = acceleration[index] @@ -162,9 +175,10 @@ def u(self, rho=None, acceleration=None) -> torch.Tensor: def velocity(self): return self.j() / self.rho() - def incompressible_energy(self) -> torch.Tensor: + def incompressible_energy(self, f: Optional[torch.Tensor] = None + ) -> torch.Tensor: """incompressible kinetic energy""" - return 0.5 * self.einsum("d,d->", [self.u(), self.u()]) + return 0.5 * self.einsum("d,d->", [self.u(f), self.u(f)]) def entropy(self) -> torch.Tensor: """entropy according to the H-theorem""" @@ -177,11 +191,13 @@ def pseudo_entropy_global(self) -> torch.Tensor: f_w = self.einsum("q,q->q", [self.f, 1 / self.torch_stencil.w]) return self.rho() - self.einsum("q,q->", [self.f, f_w]) - def pseudo_entropy_local(self) -> torch.Tensor: + def pseudo_entropy_local(self, f: Optional[torch.Tensor] = None + ) -> torch.Tensor: """pseudo_entropy derived by a Taylor expansion around the local equilibrium""" - f_feq = self.f / self.equilibrium(self) - return self.rho() - self.einsum("q,q->", [self.f, f_feq]) + f = self.f if f is None else f + f_feq = f / self.equilibrium(self) + return self.rho(f) - self.einsum("q,q->", [f, f_feq]) def shear_tensor(self, f: Optional[torch.Tensor] = None) -> torch.Tensor: """computes the shear tensor of a given self.f in the sense @@ -191,10 +207,6 @@ def shear_tensor(self, f: Optional[torch.Tensor] = None) -> torch.Tensor: shear = self.einsum("q,qab->ab", [self.f if f is None else f, shear]) return shear - def mv(self, m, v) -> torch.Tensor: - """matrix-vector multiplication""" - return self.einsum("ij,j->i", [m, v]) - def einsum(self, equation, fields, *args) -> torch.Tensor: """Einstein summation on local fields.""" inputs, output = equation.split("->") @@ -222,3 +234,103 @@ def load(self, filename): if self.context.use_native: self._f_next = self.context.empty_tensor(self.f.shape) + + +def pressure_poisson(units: 'UnitConversion', u, rho0, tol_abs=1e-10, + max_num_steps=100000): + """ + Solve the pressure poisson equation using a jacobi scheme. + + Parameters + ---------- + units : lettuce.UnitConversion + The flow instance. + u : torch.Tensor + The velocity tensor. + rho0 : torch.Tensor + Initial guess for the density (i.e., pressure). + tol_abs : float + The tolerance for pressure convergence. + + + Returns + ------- + rho : torch.Tensor + The converged density (i.e., pressure). + """ + # convert to physical units + dx = units.convert_length_to_pu(1.0) + u = units.convert_velocity_to_pu(u) + p = units.convert_density_lu_to_pressure_pu(rho0) + + # compute laplacian + with torch.no_grad(): + u_mod = torch.zeros_like(u[0]) + dim = u.shape[0] + for i in range(dim): + for j in range(dim): + derivative = torch_gradient( + torch_gradient(u[i] * u[j], dx)[i], + dx + )[j] + u_mod -= derivative + # TODO(@MCBs): still not working in 3D + + p_mod = torch_jacobi( + u_mod, + p[0], + dx, + dim=2, + tol_abs=tol_abs, + max_num_steps=max_num_steps + )[None, ...] + + return units.convert_pressure_pu_to_density_lu(p_mod) + + +def initialize_pressure_poisson(flow: 'Flow', + max_num_steps=100000, + tol_pressure=1e-6): + """Reinitialize equilibrium distributions with pressure obtained by a + Jacobi solver. Note that this method has to be called before + initialize_f_neq. + """ + u = flow.u() + rho = pressure_poisson( + flow.units, + u, + flow.rho(), + tol_abs=tol_pressure, + max_num_steps=max_num_steps + ) + return flow.equilibrium(flow=flow, rho=rho, u=u) + + +def initialize_f_neq(flow: 'Flow'): + """Initialize the distribution function values. The f^(1) contributions are + approximated by finite differences. See Krüger et al. (2017). + """ + rho = flow.rho() + u = flow.u() + + grad_u0 = torch_gradient(u[0], dx=1, order=6)[None, ...] + grad_u1 = torch_gradient(u[1], dx=1, order=6)[None, ...] + S = torch.cat([grad_u0, grad_u1]) + + if flow.stencil.d == 3: + grad_u2 = torch_gradient(u[2], dx=1, order=6)[None, ...] + S = torch.cat([S, grad_u2]) + + Pi_1 = (1.0 * flow.units.relaxation_parameter_lu * rho * S + / flow.torch_stencil.cs ** 2) + print(flow.torch_stencil.e.device) + Q = (torch.einsum('ia,ib->iab', + [flow.torch_stencil.e, flow.torch_stencil.e]) + - torch.eye(flow.stencil.d, device=flow.torch_stencil.e.device) + * flow.stencil.cs ** 2) + Pi_1_Q = flow.einsum('ab,iab->i', [Pi_1, Q]) + fneq = flow.einsum('i,i->i', [flow.torch_stencil.w, Pi_1_Q]) + + feq = flow.equilibrium(flow, rho, u) + + return feq - fneq diff --git a/lettuce/cli.py b/lettuce/cli.py index 281da2c2..a104dace 100644 --- a/lettuce/cli.py +++ b/lettuce/cli.py @@ -165,13 +165,16 @@ def convergence(ctx, use_cuda_native): error_u_old = error_u error_p_old = error_p - print(f"{resolution:15} {error_u:15.2e} {factor_u / 2:15.1f} " - f"{error_p:15.2e} {factor_p / 2:15.1f} {mlups:15.2f}") - if factor_u / 2 < (2 - 1e-6): - print("FAILED: Velocity convergence order < 2.") + print(f"{resolution:15} {error_u:15.2e} {factor_u / 2:15.2f} " + f"{error_p:15.2e} {factor_p / 2:15.2f} {mlups:15.2f}") + tol = 1e-1 + if not (2 - tol) < factor_u / 2 < (2 + tol): + print(f"FAILED: Velocity convergence order {factor_u / 2} is not in " + f"[1.9, 2.1") sys.exit(1) - if factor_p / 2 < (1 - 1e-6): - print("FAILED: Pressure convergence order < 1.") + if not (1 - tol) < factor_p / 2 < (1 + tol): + print(f"FAILED: Pressure convergence order {factor_p / 2} is not in " + f"[0.9, 1.1].") sys.exit(1) else: return 0 diff --git a/lettuce/ext/_boundary/anti_bounce_back_outlet.py b/lettuce/ext/_boundary/anti_bounce_back_outlet.py index 75796390..0b46a444 100644 --- a/lettuce/ext/_boundary/anti_bounce_back_outlet.py +++ b/lettuce/ext/_boundary/anti_bounce_back_outlet.py @@ -3,8 +3,9 @@ import numpy as np import torch +from ... import Collision +from .._collision import BGKCollision from ... import Boundary, Context -from ... import Stencil __all__ = ['AntiBounceBackOutlet'] @@ -18,27 +19,28 @@ class AntiBounceBackOutlet(Boundary): [0, -1, 0] is negative y-direction in 3D; [0, -1] for the same in 2D """ - def __init__(self, direction: [List[int]], stencil: 'Stencil', - context: 'Context' = None): - context = Context() if context is None else context - direction = context.convert_to_ndarray(direction) + def __init__(self, direction: [List[int]], flow: 'Flow', + collision: 'Collision' = None): + self.collision = BGKCollision(tau=flow.units.relaxation_parameter_lu) \ + if collision is None else collision + context = flow.context assert len(direction) in [1, 2, 3], \ (f"Invalid direction parameter. Expected direction of of length " f"1, 2 or 3 but got {len(direction)}.") - assert ((direction.tolist().count(0) == (len(direction) - 1)) + assert ((direction.count(0) == (len(direction) - 1)) and ((1 in direction) ^ (-1 in direction))), \ (f"Invalid direction parameter. Expected direction with all " f"entries 0 except one 1 or -1 but got {direction}.") - self.stencil = stencil + self.stencil = flow.torch_stencil # select velocities to be bounced (the ones pointing in "direction") - # self.velocities = torch.argwhere( - # torch.eq(torch.matmul(stencil.e, direction), 1))[None, :] - self.velocities = np.concatenate(np.argwhere( - np.matmul(context.convert_to_ndarray(stencil.e), direction) > - 1 - 1e-6), axis=0) + # needs to be np, because it is a list of indices instead of + # torchean bool tensor + self.velocities = np.concatenate( + np.argwhere(np.matmul(context.convert_to_ndarray(flow.stencil.e), + direction) > 1 - 1e-6), axis=0) # build indices of u and f that determine the side of the domain self.index = [] @@ -55,10 +57,7 @@ def __init__(self, direction: [List[int]], stencil: 'Stencil', self.neighbor.append(1) # construct indices for einsum and get w in proper shape for the # calculation in each dimension - print(stencil.w) - print(self.velocities) - print(context.convert_to_tensor(stencil.w)) - w = context.convert_to_tensor(stencil.w)[self.velocities] + w = flow.torch_stencil.w[self.velocities] if len(direction) == 3: self.dims = 'dc, cxy -> dxy' self.w = w.view(1, -1).t().unsqueeze(1) @@ -72,7 +71,7 @@ def __init__(self, direction: [List[int]], stencil: 'Stencil', def __call__(self, flow: 'Flow'): # not 100% sure about this. But collision seem to stabilize the # boundary. - flow.collision(flow) + # self.collision(flow) # actual algorithm u = flow.u() @@ -80,16 +79,14 @@ def __call__(self, flow: 'Flow'): + 0.5 * (u[[slice(None)] + self.index] - u[[slice(None)] + self.neighbor])) f = flow.f - flow.f[[np.array(flow.stencil.opposite)[self.velocities]] - + self.index] = ( + f[[flow.context.convert_to_ndarray( + flow.torch_stencil.opposite)[self.velocities]] + self.index] = ( - flow.f[[self.velocities] + self.index] - + self.w - * flow.rho()[[slice(None)] + self.index] - * (2 - + torch.einsum(self.dims, - flow.torch_stencil.e[self.velocities], - u_w) ** 2 / flow.stencil.cs ** 4 - - (torch.norm(u_w, dim=0) / flow.stencil.cs) ** 2) + + self.w * flow.rho()[[slice(None)] + self.index] + * (2 + torch.einsum(self.dims, + flow.torch_stencil.e[self.velocities], + u_w) ** 2 / flow.torch_stencil.cs ** 4 + - (torch.norm(u_w, dim=0) / flow.torch_stencil.cs) ** 2) ) return f diff --git a/lettuce/ext/_boundary/equilibrium_outlet_p.py b/lettuce/ext/_boundary/equilibrium_outlet_p.py index db034e89..1d283836 100644 --- a/lettuce/ext/_boundary/equilibrium_outlet_p.py +++ b/lettuce/ext/_boundary/equilibrium_outlet_p.py @@ -13,11 +13,11 @@ class EquilibriumOutletP(AntiBounceBackOutlet): """Equilibrium outlet with constant pressure. """ - def __init__(self, flow: 'Flow', context: 'Context', direction: List[int], + def __init__(self, direction: List[int], flow: 'Flow', rho_outlet: float = 1.0): - super().__init__(direction, flow.stencil) - self.rho_outlet = context.convert_to_tensor(rho_outlet) - self.context = context + super(EquilibriumOutletP, self).__init__(direction, flow) + self.context = flow.context + self.rho_outlet = self.context.convert_to_tensor(rho_outlet) assert len(direction) in [1, 2, 3], \ (f"Invalid direction parameter. Expected direction of of length 1," @@ -61,13 +61,16 @@ def __init__(self, flow: 'Flow', context: 'Context', direction: List[int], self.w = flow.torch_stencil.w[self.velocities] def __call__(self, flow: 'Flow'): - outlet = [slice(None)] + self.index - neighbor = [slice(None)] + self.neighbor - rho_outlet = self.rho_outlet * torch.ones_like(flow.rho()[outlet]) - feq = flow.equilibrium(flow, rho_outlet[..., None], - flow.u()[neighbor][..., None]) - return flow.einsum("q,q->q", [feq, - torch.ones_like(flow.f)]) + here = [slice(None)] + self.index + other = [slice(None)] + self.neighbor + rho = flow.rho() + u = flow.u() + rho_w = self.rho_outlet * torch.ones_like(rho[here]) + u_w = u[other] + feq = flow.f + feq[here] = flow.equilibrium(flow, rho_w[..., None], u_w[..., None])[ + ..., 0] + return flow.einsum("q,q->q", [feq, torch.ones_like(flow.f)]) def make_no_streaming_mask(self, shape: List[int], context: 'Context' ) -> Optional[torch.Tensor]: diff --git a/lettuce/ext/_collision/__init__.py b/lettuce/ext/_collision/__init__.py index c0374af0..481180e1 100644 --- a/lettuce/ext/_collision/__init__.py +++ b/lettuce/ext/_collision/__init__.py @@ -1,5 +1,4 @@ from .bgk_collision import BGKCollision -from .bgk_initialization import BGKInitialization from .kbc_collision import KBCCollision3D, KBCCollision2D, KBCCollision from .mrt_collision import MRTCollision from .no_collision import NoCollision @@ -9,7 +8,6 @@ __all__ = [ 'BGKCollision', - 'BGKInitialization', 'KBCCollision2D', 'KBCCollision3D', 'KBCCollision', diff --git a/lettuce/ext/_collision/bgk_collision.py b/lettuce/ext/_collision/bgk_collision.py index a7ce1ebe..4bb7364e 100644 --- a/lettuce/ext/_collision/bgk_collision.py +++ b/lettuce/ext/_collision/bgk_collision.py @@ -15,10 +15,9 @@ def __init__(self, tau, force: Optional['Force'] = None): self.force = force def __call__(self, flow: 'Flow') -> torch.Tensor: - rho = flow.rho() u_eq = 0 if self.force is None else self.force.u_eq(flow) - u = flow.u(rho=rho) + u_eq - feq = flow.equilibrium(flow, rho, u) + u = flow.u() + u_eq + feq = flow.equilibrium(flow, u=u) si = self.force.source_term(u) if self.force is not None else 0 return flow.f - 1.0 / self.tau * (flow.f - feq) + si diff --git a/lettuce/ext/_collision/bgk_initialization.py b/lettuce/ext/_collision/bgk_initialization.py deleted file mode 100644 index 67d49c90..00000000 --- a/lettuce/ext/_collision/bgk_initialization.py +++ /dev/null @@ -1,39 +0,0 @@ -from .. import Force -from ... import Flow, Collision -from ...util.moments import Transform - -__all__ = ['BGKInitialization'] - - -class BGKInitialization(Collision): - """ - Keep velocity constant. - """ - - def __init__(self, flow: 'Flow', moment_transformation: Transform): - self.tau = flow.units.relaxation_parameter_lu - self.moment_transformation = moment_transformation - p_pu, u_pu = flow.initial_pu() - self.u = flow.units.convert_velocity_to_lu( - flow.context.convert_to_tensor(u_pu) - ) - self.rho0 = flow.units.characteristic_density_lu - momentum_names = tuple([f"j{x}" for x in "xyz"[:flow.stencil.d]]) - self.momentum_indices = moment_transformation[momentum_names] - - def __call__(self, flow: 'Flow'): - rho = flow.rho() - feq = flow.equilibrium(flow, rho, self.u) - m = self.moment_transformation.transform(flow.f) - meq = self.moment_transformation.transform(feq) - mnew = m - 1.0 / self.tau * (m - meq) - mnew[0] = m[0] - 1.0 / (self.tau + 1) * (m[0] - meq[0]) - mnew[self.momentum_indices] = rho * self.u - f = self.moment_transformation.inverse_transform(mnew) - return f - - def native_available(self) -> bool: - return True - - def native_generator(self) -> 'NativeCollision': - pass diff --git a/lettuce/ext/_collision/mrt_collision.py b/lettuce/ext/_collision/mrt_collision.py index d901f5ac..f4104474 100644 --- a/lettuce/ext/_collision/mrt_collision.py +++ b/lettuce/ext/_collision/mrt_collision.py @@ -20,7 +20,7 @@ def __init__(self, transform: 'Transform', relaxation_parameters: list, def __call__(self, flow: 'Flow'): m = self.transform.transform(flow.f) - meq = self.transform.equilibrium(m) + meq = self.transform.equilibrium(m, flow) m = m - flow.einsum("q,q->q", [1 / self.relaxation_parameters, m - meq]) f = self.transform.inverse_transform(m) diff --git a/lettuce/ext/_flows/couette.py b/lettuce/ext/_flows/couette.py index 06d9573f..776f0aee 100644 --- a/lettuce/ext/_flows/couette.py +++ b/lettuce/ext/_flows/couette.py @@ -15,11 +15,13 @@ class CouetteFlow2D(ExtFlow): - def __init__(self, resolution: Union[int, List[int]], reynolds_number, - mach_number, context: 'Context'): - super().__init__(context, resolution, reynolds_number, - mach_number) + def __init__(self, context: 'Context', resolution: Union[int, List[int]], + reynolds_number, mach_number, + stencil: Optional['Stencil'] = None, + equilibrium: Optional['Equilibrium'] = None): self.u0 = 0 # background velocity + super().__init__(context, resolution, reynolds_number, + mach_number, stencil, equilibrium) def make_resolution(self, resolution: Union[int, List[int]], stencil: Optional['Stencil'] = None) -> List[int]: @@ -35,7 +37,7 @@ def make_units(self, reynolds_number, mach_number, resolution: List[int] mach_number=mach_number, characteristic_length_lu=resolution[0], characteristic_length_pu=1, - characteristic_velocity_pu=1 + characteristic_velocity_pu=self.u0 ) def analytic_solution(self): @@ -45,9 +47,10 @@ def analytic_solution(self): return u def initial_pu(self): - return (torch.zeros(self.resolution), - torch.stack([torch.zeros(self.resolution), - torch.zeros(self.resolution)])) + zeros = self.context.zero_tensor(self.resolution) + p = zeros[None, ...] + u = torch.stack([zeros, zeros], dim=0) + return p, u @property def grid(self): diff --git a/lettuce/ext/_flows/decayingturbulence.py b/lettuce/ext/_flows/decayingturbulence.py index d87334fc..d0f607f7 100644 --- a/lettuce/ext/_flows/decayingturbulence.py +++ b/lettuce/ext/_flows/decayingturbulence.py @@ -13,6 +13,7 @@ import torch from ... import UnitConversion +from .._stencil import D1Q3, D2Q9, D3Q19 from . import ExtFlow @@ -24,18 +25,29 @@ class DecayingTurbulence(ExtFlow): def __init__(self, context: 'Context', resolution: Union[int, List[int]], reynolds_number, mach_number, k0=20, ic_energy=0.5, stencil: Optional['Stencil'] = None, - equilibrium: Optional['Equilibrium'] = None): + equilibrium: Optional['Equilibrium'] = None, + initialize_pressure: bool = True, + initialize_fneq: bool = True, + randseed: Optional[int] = None): + self.initialize_pressure = initialize_pressure + self.initialize_fneq = initialize_fneq + self.randseed = randseed self.k0 = k0 self.ic_energy = ic_energy self.wavenumbers = [] self.spectrum = [] - ExtFlow.__init__(self, context, resolution, reynolds_number, + default_stencils = [D1Q3(), D2Q9(), D3Q19()] + stencil = stencil or default_stencils[len(resolution) - 1] + stencil = stencil() if callable(stencil) else stencil + if stencil.d != 2: + self.initialize_pressure = False + super().__init__(context, resolution, reynolds_number, mach_number, stencil, equilibrium) def make_resolution(self, resolution: Union[int, List[int]], stencil: Optional['Stencil'] = None) -> List[int]: if isinstance(resolution, int): - return [resolution] * (stencil.d or self.stencil.d) + return [resolution] * stencil.d else: return resolution @@ -75,6 +87,7 @@ def _generate_spectrum(self): def _generate_initial_velocity(self, ek, wavenumber): dx = self.units.convert_length_to_pu(1.0) + np.random.seed(self.randseed) u = np.random.random(np.array(wavenumber).shape) * 2 * np.pi + 0j u = [np.fft.fftn(u[dim], axes=tuple((np.arange(self.stencil.d)))) for diff --git a/lettuce/ext/_flows/doublyshear.py b/lettuce/ext/_flows/doublyshear.py index 476365f4..a27ca9df 100644 --- a/lettuce/ext/_flows/doublyshear.py +++ b/lettuce/ext/_flows/doublyshear.py @@ -13,20 +13,29 @@ __all__ = ['DoublyPeriodicShear2D'] +from .._stencil import D2Q9 + class DoublyPeriodicShear2D(ExtFlow): - def __init__(self, context: 'Context', resolution, reynolds_number, - mach_number, shear_layer_width=80, - initial_perturbation_magnitude=0.05): - super().__init__(context, resolution, reynolds_number, mach_number) + def __init__(self, context: 'Context', resolution: Union[int, List[int]], + reynolds_number, mach_number, + stencil: Optional['Stencil'] = None, + equilibrium: Optional['Equilibrium'] = None, + shear_layer_width=80, + initial_perturbation_magnitude=0.05, + initialize_fneq: bool = True): + self.initialize_fneq = initialize_fneq self.initial_perturbation_magnitude = initial_perturbation_magnitude self.shear_layer_width = shear_layer_width + self.stencil = D2Q9() if stencil is None else stencil + super().__init__(context, resolution, reynolds_number, mach_number, + self.stencil, equilibrium) def make_resolution(self, resolution: Union[int, List[int]], stencil: Optional['Stencil'] = None) -> List[int]: if isinstance(resolution, int): - return [resolution] * 2 + return [resolution] * self.stencil.d else: assert len(resolution) == 2, 'expected 2-dimensional resolution' return resolution @@ -35,23 +44,22 @@ def make_units(self, reynolds_number, mach_number, resolution: List[int]) -> 'UnitConversion': return UnitConversion( reynolds_number=reynolds_number, mach_number=mach_number, - characteristic_length_lu=resolution, characteristic_length_pu=1, + characteristic_length_lu=resolution[0], characteristic_length_pu=1, characteristic_velocity_pu=1 ) - def analytic_solution(self, x, t=0): + def analytic_solution(self, t=0): raise NotImplementedError def initial_pu(self) -> (float, Union[np.array, torch.Tensor]): pert = self.initial_perturbation_magnitude w = self.shear_layer_width - u1 = self.context.convert_to_tensor(np.choose( + u1 = self.context.convert_to_tensor(torch.where( self.grid[1] > 0.5, - [np.tanh(w * (self.grid[1] - 0.25)), - np.tanh(w * (0.75 - self.grid[1]))] + torch.tanh(w * (self.grid[1] - 0.25)), + torch.tanh(w * (0.75 - self.grid[1])) )) - u2 = self.context.convert_to_tensor( - pert * np.sin(2 * np.pi * (self.grid[0] + 0.25))) + u2 = pert * torch.sin(2 * torch.pi * (self.grid[0] + 0.25)) u = torch.stack([u1, u2]) p = torch.zeros_like(u1)[None, ...] return p, u diff --git a/lettuce/ext/_flows/obstacle.py b/lettuce/ext/_flows/obstacle.py index c14ead33..088a8a4d 100644 --- a/lettuce/ext/_flows/obstacle.py +++ b/lettuce/ext/_flows/obstacle.py @@ -114,10 +114,10 @@ def boundaries(self): characteristic_velocity_pu * self._unit_vector() ), - AntiBounceBackOutlet(self._unit_vector(), - self.torch_stencil), - # EquilibriumOutletP(self, self.context, - # self._unit_vector().tolist(), 0), + AntiBounceBackOutlet(self._unit_vector().tolist(), + self), + # EquilibriumOutletP(direction=self._unit_vector().tolist(), + # self, rho_outlet=0), BounceBackBoundary(self.mask) ] diff --git a/lettuce/ext/_flows/poiseuille.py b/lettuce/ext/_flows/poiseuille.py index 96aadfa1..fb65d6e6 100644 --- a/lettuce/ext/_flows/poiseuille.py +++ b/lettuce/ext/_flows/poiseuille.py @@ -12,51 +12,63 @@ __all__ = ['PoiseuilleFlow2D'] +from .._stencil import D2Q9 + class PoiseuilleFlow2D(ExtFlow): - def __init__(self, context: 'Context', resolution, reynolds_number, - mach_number, initialize_with_zeros=True): - super().__init__(context, resolution, reynolds_number, mach_number) + def __init__(self, context: 'Context', resolution: Union[int, List[int]], + reynolds_number, mach_number, + stencil: Optional['Stencil'] = None, + equilibrium: Optional['Equilibrium'] = None, + initialize_with_zeros=True): + self.stencil = D2Q9() if stencil is None else stencil self.initialize_with_zeros = initialize_with_zeros + super().__init__(context, resolution, reynolds_number, mach_number, + self.stencil, equilibrium) - def analytic_solution(self, grid): + def analytic_solution(self, t=0) -> (torch.Tensor, torch.Tensor): half_lattice_spacing = 0.5 / self.resolution[0] - x, y = grid + x, y = self.grid nu = self.units.viscosity_pu rho = 1 - u = self.context.convert_to_tensor([ - self.acceleration[0] / (2 * rho * nu) - * ((y - half_lattice_spacing) * (1 - half_lattice_spacing - y)), - np.zeros(x.shape) - ]) - p = self.context.convert_to_tensor( - [y * 0 + self.units.convert_density_lu_to_pressure_pu(rho)]) + ux = (self.acceleration[0] / (2 * rho * nu) + * ((y - half_lattice_spacing) * (1 - half_lattice_spacing - y))) + uy = self.context.zero_tensor(self.resolution) + u = torch.stack([ux, uy], dim=0) + p = y * 0 + self.units.convert_density_lu_to_pressure_pu(rho) return p, u def initial_pu(self): if self.initialize_with_zeros: - p = np.array([0 * self.grid[0]], dtype=float) - u = np.array([0 * self.grid[0], 0 * self.grid[1]], dtype=float) + zeros = self.context.zero_tensor(self.resolution) + p = zeros[None, ...] + u = torch.stack(2*[zeros], dim=0) return p, u else: - return self.analytic_solution(self.grid) + return self.analytic_solution() def make_units(self, reynolds_number, mach_number, resolution: List[int]) -> 'UnitConversion': return UnitConversion( reynolds_number=reynolds_number, mach_number=mach_number, - characteristic_length_lu=resolution, characteristic_length_pu=1, + characteristic_length_lu=resolution[0]-1, + characteristic_length_pu=1, characteristic_velocity_pu=1 ) def make_resolution(self, resolution: Union[int, List[int]], stencil: Optional['Stencil'] = None) -> List[int]: - pass + if isinstance(resolution, list): + assert len(resolution) == self.stencil.d + if isinstance(resolution, int): + resolution = [resolution] * self.stencil.d + return resolution @property def grid(self): - xyz = tuple(torch.linspace(0, 1, steps=n, + xyz = tuple(torch.linspace(0, 1, + steps=n, device=self.context.device, dtype=self.context.dtype) for n in self.resolution) @@ -64,7 +76,7 @@ def grid(self): @property def boundaries(self): - mask = self.context.zero_tensor(self.grid[0].shape, dtype=bool) + mask = self.context.zero_tensor(self.resolution, dtype=bool) mask[:, [0, -1]] = True boundary = BounceBackBoundary(mask=mask) return [boundary] diff --git a/lettuce/ext/_flows/taylorgreen.py b/lettuce/ext/_flows/taylorgreen.py index f5d607ca..cbfd8e3e 100644 --- a/lettuce/ext/_flows/taylorgreen.py +++ b/lettuce/ext/_flows/taylorgreen.py @@ -7,6 +7,7 @@ import torch from ... import UnitConversion +from .._stencil import D2Q9 from . import ExtFlow __all__ = ['TaylorGreenVortex', 'TaylorGreenVortex2D', 'TaylorGreenVortex3D'] @@ -16,23 +17,23 @@ class TaylorGreenVortex(ExtFlow): def __init__(self, context: 'Context', resolution: Union[int, List[int]], reynolds_number, mach_number, stencil: Optional['Stencil'] = None, - equilibrium: Optional['Equilibrium'] = None): + equilibrium: Optional['Equilibrium'] = None, + initialize_fneq: bool = True): + self.initialize_fneq = initialize_fneq if stencil is None and not isinstance(resolution, list): warnings.warn("Requiring information about dimensionality!" " Either via stencil or resolution. Setting " "dimension to 2.", UserWarning) - self.d = 2 + self.stencil = D2Q9() else: self.stencil = stencil() if callable(stencil) else stencil - self.d = self.stencil.d if self.stencil is not None else len( - resolution) ExtFlow.__init__(self, context, resolution, reynolds_number, mach_number, stencil, equilibrium) def make_resolution(self, resolution: Union[int, List[int]], stencil: Optional['Stencil'] = None) -> List[int]: if isinstance(resolution, int): - return [resolution] * self.d + return [resolution] * self.stencil.d else: assert len(resolution) in [2, 3], ('the resolution of a ' 'taylor-green-vortex ' @@ -44,8 +45,8 @@ def make_units(self, reynolds_number, mach_number, return UnitConversion( reynolds_number=reynolds_number, mach_number=mach_number, - characteristic_length_lu=resolution[0] / (2 * torch.pi), - characteristic_length_pu=1, + characteristic_length_lu=resolution[0], + characteristic_length_pu=2 * torch.pi, characteristic_velocity_pu=1) @property @@ -56,7 +57,7 @@ def grid(self): steps=self.resolution[n], device=self.context.device, dtype=self.context.dtype) - for n in range(self.d)) + for n in range(self.stencil.d)) return torch.meshgrid(*xyz, indexing='ij') def initial_pu(self) -> (torch.Tensor, torch.Tensor): diff --git a/lettuce/ext/_force/_force.py b/lettuce/ext/_force/_force.py index a1e959c3..e1a1e1aa 100644 --- a/lettuce/ext/_force/_force.py +++ b/lettuce/ext/_force/_force.py @@ -4,6 +4,10 @@ class Force(ABC): + @abstractmethod + def __init__(self, flow: 'Flow', tau, acceleration): + ... + @abstractmethod def source_term(self, u): ... diff --git a/lettuce/ext/_force/guo.py b/lettuce/ext/_force/guo.py index 9bf21417..ebdb7544 100644 --- a/lettuce/ext/_force/guo.py +++ b/lettuce/ext/_force/guo.py @@ -5,23 +5,37 @@ class Guo(Force): - def __init__(self, lattice, tau, acceleration): - self.lattice = lattice + + def __init__(self, flow, tau, acceleration): + self.flow = flow self.tau = tau - self.acceleration = lattice.convert_to_tensor(acceleration) + self.acceleration = flow.context.convert_to_tensor(acceleration) def source_term(self, u): - emu = append_axes(self.lattice.e, self.lattice.D) - u - eu = self.lattice.einsum("ib,b->i", [self.lattice.e, u]) - eeu = self.lattice.einsum("ia,i->ia", [self.lattice.e, eu]) - emu_eeu = emu / (self.lattice.cs ** 2) + eeu / (self.lattice.cs ** 4) - emu_eeuF = self.lattice.einsum("ia,a->i", [emu_eeu, self.acceleration]) - weemu_eeuF = append_axes(self.lattice.w, self.lattice.D) * emu_eeuF + emu = append_axes(self.flow.torch_stencil.e, + self.flow.torch_stencil.d) - u + eu = self.flow.einsum("ib,b->i", [self.flow.torch_stencil.e, u]) + eeu = self.flow.einsum("ia,i->ia", [self.flow.torch_stencil.e, eu]) + emu_eeu = (emu / (self.flow.torch_stencil.cs ** 2) + + eeu / (self.flow.torch_stencil.cs ** 4)) + emu_eeuF = self.flow.einsum("ia,a->i", [emu_eeu, self.acceleration]) + weemu_eeuF = (append_axes(self.flow.torch_stencil.w, + self.flow.torch_stencil.d) + * emu_eeuF) return (1 - 1 / (2 * self.tau)) * weemu_eeuF - def u_eq(self, f): - return self.ueq_scaling_factor * append_axes(self.acceleration, self.lattice.D) / self.lattice.rho(f) + def u_eq(self, flow: 'Flow' = None): + flow = self.flow if flow is None else flow + return (self.ueq_scaling_factor + * append_axes(self.acceleration, flow.torch_stencil.d) + / flow.rho()) @property def ueq_scaling_factor(self): return 0.5 + + def native_available(self) -> bool: + return False + + def native_generator(self) -> 'NativeForce': + pass diff --git a/lettuce/ext/_force/shan_chen.py b/lettuce/ext/_force/shan_chen.py index b9679f79..3dc259f6 100644 --- a/lettuce/ext/_force/shan_chen.py +++ b/lettuce/ext/_force/shan_chen.py @@ -6,9 +6,9 @@ class ShanChen(Force): - def __init__(self, context, tau, acceleration): + def __init__(self, flow, tau, acceleration): self.tau = tau - self.acceleration = context.convert_to_tensor(acceleration) + self.acceleration = flow.context.convert_to_tensor(acceleration) def source_term(self, u): return 0 diff --git a/lettuce/ext/_reporter/error_reporter.py b/lettuce/ext/_reporter/error_reporter.py index 512dec2b..0a0edd6a 100644 --- a/lettuce/ext/_reporter/error_reporter.py +++ b/lettuce/ext/_reporter/error_reporter.py @@ -21,8 +21,9 @@ def __call__(self, simulation: 'Simulation'): simulation.flow.i), simulation.flow.f if i % self.interval == 0: - pref, uref = [simulation.flow.context.convert_to_tensor( - _) for _ in self.analytical_solution(t=t)] + pref, uref = self.analytical_solution(t=t) + pref = simulation.flow.context.convert_to_tensor(pref) + uref = simulation.flow.context.convert_to_tensor(uref) p = simulation.flow.p_pu u = simulation.flow.u_pu diff --git a/lettuce/ext/_reporter/observable_reporter.py b/lettuce/ext/_reporter/observable_reporter.py index 9cd4df39..59805b14 100644 --- a/lettuce/ext/_reporter/observable_reporter.py +++ b/lettuce/ext/_reporter/observable_reporter.py @@ -1,4 +1,7 @@ import sys +from abc import ABC, abstractmethod +from typing import Optional + import torch import numpy as np @@ -11,26 +14,27 @@ 'Mass'] -class Observable: - def __init__(self, flow: Flow): +class Observable(ABC): + def __init__(self, flow: 'Flow'): self.context = flow.context self.flow = flow - def __call__(self, f): - raise NotImplementedError + @abstractmethod + def __call__(self, f: Optional[torch.Tensor] = None): + ... class MaximumVelocity(Observable): """Maximum velocitiy""" - def __call__(self, f): + def __call__(self, f: Optional[torch.Tensor] = None): return torch.norm(self.flow.u_pu, dim=0).max() class IncompressibleKineticEnergy(Observable): """Total kinetic energy of an incompressible flow.""" - def __call__(self, f): + def __call__(self, f: Optional[torch.Tensor] = None): dx = self.flow.units.convert_length_to_pu(1.0) kinE = self.flow.units.convert_incompressible_energy_to_pu( torch.sum(self.flow.incompressible_energy())) @@ -46,7 +50,7 @@ class Enstrophy(Observable): The function only works for periodic domains """ - def __call__(self, f): + def __call__(self, f: Optional[torch.Tensor] = None): u0 = self.flow.units.convert_velocity_to_pu(self.flow.u()[0]) u1 = self.flow.units.convert_velocity_to_pu(self.flow.u()[1]) dx = self.flow.units.convert_length_to_pu(1.0) @@ -70,7 +74,7 @@ class EnergySpectrum(Observable): def __init__(self, flow: Flow): super(EnergySpectrum, self).__init__(flow) self.dx = self.flow.units.convert_length_to_pu(1.0) - self.dimensions = self.flow.grid[0].shape + self.dimensions = self.flow.resolution frequencies = [self.context.convert_to_tensor( np.fft.fftfreq(dim, d=1 / dim) ) for dim in self.dimensions] @@ -92,7 +96,7 @@ def __init__(self, flow: Flow): + 0.5) ) - def __call__(self, f): + def __call__(self, f: Optional[torch.Tensor] = None): u = self.flow.u() return self.spectrum_from_u(u) @@ -147,7 +151,7 @@ def __init__(self, flow: Flow, no_mass_mask=None): super(Mass, self).__init__(flow) self.mask = no_mass_mask - def __call__(self, f): + def __call__(self, f: Optional[torch.Tensor] = None): mass = f[..., 1:-1, 1:-1].sum() if self.mask is not None: mass -= (f * self.mask.to(dtype=torch.float)).sum() diff --git a/lettuce/ext/_reporter/vtk_reporter.py b/lettuce/ext/_reporter/vtk_reporter.py index 365aeeab..52544bb6 100644 --- a/lettuce/ext/_reporter/vtk_reporter.py +++ b/lettuce/ext/_reporter/vtk_reporter.py @@ -52,10 +52,10 @@ def output_mask(self, simulation: 'Simulation'): Usage: vtk_reporter.output_mask(simulation.no_collision_mask)""" point_dict = dict() if simulation.flow.stencil.d == 2: - point_dict["mask"] = simulation.flow.context.convert_to_numpy( + point_dict["mask"] = simulation.flow.context.convert_to_ndarray( simulation.no_collision_mask)[..., None].astype(int) else: - point_dict["mask"] = simulation.flow.context.convert_to_numpy( + point_dict["mask"] = simulation.flow.context.convert_to_ndarray( simulation.no_collision_mask).astype(int) vtk.gridToVTK(self.filename_base + "_mask", np.arange(0, point_dict["mask"].shape[0]), diff --git a/lettuce/util/__init__.py b/lettuce/util/__init__.py index 202015bf..263f3685 100644 --- a/lettuce/util/__init__.py +++ b/lettuce/util/__init__.py @@ -3,27 +3,29 @@ ''' from .utility import * +# not importing moments due to cyclic import issue # from .moments import * from .datautils import * from .utility import get_subclasses -__all__ = ['get_subclasses', - 'LettuceException', - 'LettuceWarning', - 'InefficientCodeWarning', - 'ExperimentalWarning', - 'torch_gradient', - 'grid_fine_to_coarse', - 'torch_jacobi', - 'pressure_poisson', - 'append_axes', - 'HDF5Reporter', - 'LettuceDataset', ] -# 'moment_tensor', -# 'get_default_moment_transform', -# 'Moments', -# 'Transform', -# 'D1Q3Transform', -# 'D2Q9Lallemand', -# 'D2Q9Dellar', -# 'D3Q27Hermite'] +__all__ = [ + 'get_subclasses', + 'LettuceException', + 'LettuceWarning', + 'InefficientCodeWarning', + 'ExperimentalWarning', + 'torch_gradient', + 'grid_fine_to_coarse', + 'torch_jacobi', + 'append_axes', + 'HDF5Reporter', + 'LettuceDataset', + # 'moment_tensor', + # 'get_default_moment_transform', + # 'Moments', + # 'Transform', + # 'D1Q3Transform', + # 'D2Q9Lallemand', + # 'D2Q9Dellar', + # 'D3Q27Hermite' +] diff --git a/lettuce/util/datautils.py b/lettuce/util/datautils.py index 74c22dba..a8158c2c 100644 --- a/lettuce/util/datautils.py +++ b/lettuce/util/datautils.py @@ -8,12 +8,13 @@ import pickle import io import numpy as np +from lettuce._simulation import Reporter __all__ = ["HDF5Reporter", "LettuceDataset"] -class HDF5Reporter: +class HDF5Reporter(Reporter): """ HDF5 _reporter for distribution function f in lettuce containing metadata of the simulation. @@ -32,13 +33,13 @@ class HDF5Reporter: -------- Create a HDF5 _reporter. >>> import lettuce as lt - >>> lattice = lt.Lattice(lt.D3Q27, device="cpu") - >>> flow = lt.TaylorGreenVortex3D(50, 300, 0.1, lattice) + >>> context = Context() + >>> flow = lt.TaylorGreenVortex(context, [50, 50], 300, 0.1) >>> _collision = ... >>> simulation = ... >>> hdf5_reporter = lt.HDF5Reporter( + >>> context=context, >>> flow=flow, - >>> lattice=lattice, >>> _collision=_collision, >>> interval= 100, >>> filebase="./h5_output") @@ -46,7 +47,7 @@ class HDF5Reporter: """ def __init__(self, flow, collision, interval, filebase='./output', metadata=None): - self.lattice = flow.units.lattice + self.context = flow.context self.interval = interval self.filebase = filebase fs = h5py.File(self.filebase + '.h5', 'w') @@ -56,19 +57,20 @@ def __init__(self, flow, collision, interval, filebase='./output', metadata=None if metadata: for attr in metadata: fs.attrs[attr] = metadata[attr] - self.shape = (self.lattice.Q, *flow.grid[0].shape) + self.shape = (flow.stencil.q, *flow.grid[0].shape) fs.create_dataset(name="f", shape=(0, *self.shape), maxshape=(None, *self.shape)) fs.close() - def __call__(self, i, t, f): - if i % self.interval == 0: + def __call__(self, simulation: 'Simulation'): # i, t, f): + if simulation.flow.i % self.interval == 0: with h5py.File(self.filebase + '.h5', 'r+') as fs: fs["f"].resize(fs["f"].shape[0] + 1, axis=0) - fs["f"][-1, ...] = self.lattice.convert_to_numpy(f) + fs["f"][-1, ...] = self.context.convert_to_ndarray( + simulation.flow.f) fs.attrs['data'] = str(fs["f"].shape[0]) - fs.attrs['steps'] = str(i) + fs.attrs['steps'] = str(simulation.flow.i) @staticmethod def _pickle_to_h5(instance): @@ -116,7 +118,7 @@ def __init__(self, filebase, transform=None, target=False, skip_idx_to_target=1) self.fs = h5py.File(self.filebase, "r") self.shape = self.fs["f"].shape self.keys = list(self.fs.keys()) - self.lattice = self._unpickle_from_h5(self.fs.attrs["flow"]).units.lattice + self.context = self._unpickle_from_h5(self.fs.attrs["flow"]).context def __str__(self): for attr, value in self.fs.attrs.items(): @@ -144,7 +146,7 @@ def __del__(self): self.fs.close() def get_data(self, idx): - return self.lattice.convert_to_tensor(self.fs["f"][idx]) + return self.context.convert_to_tensor(self.fs["f"][idx]) def get_attr(self, attr): return self.fs.attrs[attr] diff --git a/lettuce/util/moments.py b/lettuce/util/moments.py index 077845d9..7cc704c3 100644 --- a/lettuce/util/moments.py +++ b/lettuce/util/moments.py @@ -3,7 +3,11 @@ """ import warnings +from typing import List + import torch +from lettuce._flow import Flow + import lettuce from lettuce.util import (LettuceException, InefficientCodeWarning, get_subclasses, ExperimentalWarning) @@ -27,21 +31,21 @@ _ALL_STENCILS = get_subclasses(Stencil, module=lettuce) -def moment_tensor(e, multiindex): +def moment_tensor(e: List[List[int]], multiindex): if isinstance(e, torch.Tensor): return torch.prod(torch.pow(e, multiindex[..., None, :]), dim=-1) else: return np.prod(np.power(e, multiindex[..., None, :]), axis=-1) -def get_default_moment_transform(lattice): - if lattice.stencil == D1Q3: - return D1Q3Transform(lattice) - if lattice.stencil == D2Q9: - return D2Q9Lallemand(lattice) +def get_default_moment_transform(stencil: 'Stencil', context: 'Context'): + if stencil == D1Q3 or isinstance(stencil, D1Q3): + return D1Q3Transform(stencil, context) + if stencil == D2Q9 or isinstance(stencil, D2Q9): + return D2Q9Lallemand(stencil, context) else: raise LettuceException(f"No default moment transform for lattice " - f"{lattice}.") + f"{stencil}.") class Moments: @@ -58,10 +62,11 @@ class Transform: transforms. """ - def __init__(self, lattice, names=None): - self.lattice = lattice - self.names = [f"m{i}" for i in range(lattice.Q)]\ + def __init__(self, stencil: 'Stencil', context: 'Context', names=None): + self.context = context + self.names = [f"m{i}" for i in range(stencil.q)]\ if names is None else names + self.stencil = stencil def __getitem__(self, moment_names): if not isinstance(moment_names, tuple): @@ -74,7 +79,7 @@ def transform(self, f): def inverse_transform(self, m): return m - def equilibrium(self, m): + def equilibrium(self, m: torch.Tensor, flow: 'Flow'): """A very inefficient and basic implementation of the equilibrium moments. """ @@ -85,9 +90,29 @@ def equilibrium(self, m): InefficientCodeWarning ) f = self.inverse_transform(m) - feq = self.lattice.equilibrium(self.lattice.rho(f), self.lattice.u(f)) + feq = flow.equilibrium(flow, flow.rho(None, f), flow.u(None, f)) return self.transform(feq) + def einsum(self, equation, fields, *args) -> torch.Tensor: + """Einstein summation on local fields.""" + inputs, output = equation.split("->") + inputs = inputs.split(",") + for i, inp in enumerate(inputs): + if len(inp) == len(fields[i].shape): + pass + elif len(inp) == len(fields[i].shape) - self.stencil.d: + inputs[i] += "..." + if not output.endswith("..."): + output += "..." + else: + assert False, "Bad dimension." + equation = ",".join(inputs) + "->" + output + return torch.einsum(equation, fields, *args) + + def mv(self, m, v) -> torch.Tensor: + """matrix-vector multiplication""" + return self.einsum("ij,j->i", [m, v]) + class D1Q3Transform(Transform): matrix = np.array([ @@ -103,16 +128,16 @@ class D1Q3Transform(Transform): names = ["rho", "j", "e"] supported_stencils = [D1Q3] - def __init__(self, lattice): - super(D1Q3Transform, self).__init__(lattice, self.names) - self.matrix = self.lattice.convert_to_tensor(self.matrix) - self.inverse = self.lattice.convert_to_tensor(self.inverse) + def __init__(self, stencil: 'Stencil', context: 'Context'): + super(D1Q3Transform, self).__init__(stencil, context, self.names) + self.matrix = self.context.convert_to_tensor(self.matrix) + self.inverse = self.context.convert_to_tensor(self.inverse) def transform(self, f): - return self.lattice.mv(self.matrix, f) + return self.mv(self.matrix, f) def inverse_transform(self, m): - return self.lattice.mv(self.inverse, m) + return self.mv(self.inverse, m) # def _equilibrium(self, m): # # TODO @@ -149,20 +174,18 @@ class D2Q9Dellar(Transform): names = ['rho', 'jx', 'jy', 'Pi_xx', 'Pi_xy', 'PI_yy', 'N', 'Jx', 'Jy'] supported_stencils = [D2Q9] - def __init__(self, lattice): - super(D2Q9Dellar, self).__init__( - lattice, self.names - ) - self.matrix = self.lattice.convert_to_tensor(self.matrix) - self.inverse = self.lattice.convert_to_tensor(self.inverse) + def __init__(self, stencil: 'Stencil', context: 'Context'): + super(D2Q9Dellar, self).__init__(stencil, context, self.names) + self.matrix = self.context.convert_to_tensor(self.matrix) + self.inverse = self.context.convert_to_tensor(self.inverse) def transform(self, f): - return self.lattice.mv(self.matrix, f) + return self.mv(self.matrix, f) def inverse_transform(self, m): - return self.lattice.mv(self.inverse, m) + return self.mv(self.inverse, m) - def equilibrium(self, m): + def equilibrium(self, m, flow: 'Flow'): warnings.warn("I am not 100% sure if this equilibrium is correct.", ExperimentalWarning) meq = torch.zeros_like(m) @@ -207,20 +230,18 @@ class D2Q9Lallemand(Transform): names = ['rho', 'jx', 'jy', 'pxx', 'pxy', 'e', 'qx', 'qy', 'eps'] supported_stencils = [D2Q9] - def __init__(self, lattice): - super(D2Q9Lallemand, self).__init__( - lattice, self.names - ) - self.matrix = self.lattice.convert_to_tensor(self.matrix) - self.inverse = self.lattice.convert_to_tensor(self.inverse) + def __init__(self, stencil: 'Stencil', context: 'Context'): + super(D2Q9Lallemand, self).__init__(stencil, context, self.names) + self.matrix = self.context.convert_to_tensor(self.matrix) + self.inverse = self.context.convert_to_tensor(self.inverse) def transform(self, f): - return self.lattice.mv(self.matrix, f) + return self.mv(self.matrix, f) def inverse_transform(self, m): - return self.lattice.mv(self.inverse, m) + return self.mv(self.inverse, m) - def equilibrium(self, m): + def equilibrium(self, m, flow: 'Flow'): """From Lallemand and Luo""" warnings.warn("I am not 100% sure if this equilibrium is correct.", ExperimentalWarning) @@ -511,20 +532,18 @@ class D3Q27Hermite(Transform): 'J_xxyyz', 'J_xxyzz', 'J_xyyzz', 'J_xyxzyz'] supported_stencils = [D3Q27] - def __init__(self, lattice): - super(D3Q27Hermite, self).__init__( - lattice, self.names - ) - self.matrix = self.lattice.convert_to_tensor(self.matrix) - self.inverse = self.lattice.convert_to_tensor(self.inverse) + def __init__(self, stencil: 'Stencil', context: 'Context'): + super(D3Q27Hermite, self).__init__(stencil, context, self.names) + self.matrix = self.context.convert_to_tensor(self.matrix) + self.inverse = self.context.convert_to_tensor(self.inverse) def transform(self, f): - return self.lattice.mv(self.matrix, f) + return self.mv(self.matrix, f) def inverse_transform(self, m): - return self.lattice.mv(self.inverse, m) + return self.mv(self.inverse, m) - def equilibrium(self, m): + def equilibrium(self, m, flow: 'Flow'): meq = torch.zeros_like(m) rho = m[0] jx = m[1] diff --git a/lettuce/util/utility.py b/lettuce/util/utility.py index 52e8f4cb..ee1bae5d 100644 --- a/lettuce/util/utility.py +++ b/lettuce/util/utility.py @@ -9,7 +9,6 @@ , 'torch_gradient', 'grid_fine_to_coarse', 'torch_jacobi', - 'pressure_poisson', 'append_axes'] @@ -100,18 +99,24 @@ def torch_gradient(f, dx=1, order=2): return out -def grid_fine_to_coarse(lattice, f_fine, tau_fine, tau_coarse): +def grid_fine_to_coarse(flow: 'Flow', f_fine, tau_fine, tau_coarse): if f_fine.shape.__len__() == 3: - f_eq = lattice.equilibrium(lattice.rho(f_fine[:, ::2, ::2]), lattice.u(f_fine[:, ::2, ::2])) + f_eq = flow.equilibrium(flow, + rho=flow.rho(f_fine[:, ::2, ::2]), + u=flow.u(f_fine[:, ::2, ::2])) f_neq = f_fine[:, ::2, ::2] - f_eq - if f_fine.shape.__len__() == 4: - f_eq = lattice.equilibrium(lattice.rho(f_fine[:, ::2, ::2, ::2]), lattice.u(f_fine[:, ::2, ::2, ::2])) + elif f_fine.shape.__len__() == 4: + f_eq = flow.equilibrium(flow, + rho=flow.rho(f_fine[:, ::2, ::2, ::2]), + u=flow.u(f_fine[:, ::2, ::2, ::2])) f_neq = f_fine[:, ::2, ::2, ::2] - f_eq + else: + raise LettuceException("Invalid dimension!") f_coarse = f_eq + 2 * tau_coarse / tau_fine * f_neq return f_coarse -def torch_jacobi(f, p, dx, device, dim, tol_abs=1e-10, max_num_steps=100000): +def torch_jacobi(f, p, dx, dim, tol_abs=1e-10, max_num_steps=100000): """Jacobi solver for the Poisson pressure equation""" ## transform to torch.tensor @@ -151,55 +156,6 @@ def torch_jacobi(f, p, dx, device, dim, tol_abs=1e-10, max_num_steps=100000): return p -def pressure_poisson(units, u, rho0, tol_abs=1e-10, max_num_steps=100000): - """ - Solve the pressure poisson equation using a jacobi scheme. - - Parameters - ---------- - units : lettuce.UnitConversion - The flow instance. - u : torch.Tensor - The velocity tensor. - rho0 : torch.Tensor - Initial guess for the density (i.e., pressure). - tol_abs : float - The tolerance for pressure convergence. - - - Returns - ------- - rho : torch.Tensor - The converged density (i.e., pressure). - """ - # convert to physical units - dx = units.convert_length_to_pu(1.0) - u = units.convert_velocity_to_pu(u) - p = units.convert_density_lu_to_pressure_pu(rho0) - - # compute laplacian - with _torch.no_grad(): - u_mod = _torch.zeros_like(u[0]) - dim = u.shape[0] - for i in range(dim): - for j in range(dim): - derivative = torch_gradient(torch_gradient(u[i] * u[j], dx)[i], dx)[j] - u_mod -= derivative - # TODO(@MCBs): still not working in 3D - - p_mod = torch_jacobi( - u_mod, - p[0], - dx, - units.lattice.device, - dim=units.lattice.D, - tol_abs=tol_abs, - max_num_steps=max_num_steps - )[None, ...] - - return units.convert_pressure_pu_to_density_lu(p_mod) - - def append_axes(array, n): index = (Ellipsis,) + (None,) * n return array[index] diff --git a/old_tests/conftest.py b/old_tests/conftest.py deleted file mode 100644 index a997edf7..00000000 --- a/old_tests/conftest.py +++ /dev/null @@ -1,122 +0,0 @@ -""" -Fixtures for unit tests. -""" -import pytest - -import numpy as np -import torch - -from lettuce import * -from lettuce import _stencil -from lettuce.util import moments -from lettuce.util.moments import Transform - -STENCILS = list(get_subclasses(Stencil, _stencil)) -TRANSFORMS = list(get_subclasses(Transform, moments)) - - -@pytest.fixture( - params=["cpu", - pytest.param("cuda:0", - marks=pytest.mark.skipif( - not torch.cuda.is_available(), - reason="CUDA not available."))]) -def device(request): - """Run a test case for all available devices.""" - return request.param - - -@pytest.fixture(params=[torch.float32, torch.float64]) -# not testing torch.float16 (half precision is not precise enough) -def dtype_device(request, device): - """Run a test case for all available devices and data types available on - the device.""" - if device == "cpu" and request.param == torch.float16: - pytest.skip("Half precision is only available on GPU.") - if device == "cuda:0" and request.param == torch.float32: - pytest.skip("Tolerances are not yet adapted to CUDA single precision.") - return request.param, device - - -@pytest.fixture(params=STENCILS) -def stencil(request): - """Run a test for all stencil.""" - return request.param - - -@pytest.fixture( - params=((torch.float64, "cpu", ""), - (torch.float32, "cpu", ""), - (torch.float64, "cuda:0", ""), - (torch.float32, "cuda:0", ""), - (torch.float64, "cuda:0", "cuda_native"), - (torch.float32, "cuda:0", "cuda_native")), - ids=("cpu64", "cpu32", "cu64", "cu32", "native64", "native32")) -def lattice(request, stencil): - """Run a test for all lattices (all stencil, devices and data types - available on the device.)""" - dtype, device, native = request.param - if device == "cuda:0" and not torch.cuda.is_available(): - pytest.skip(reason="CUDA not available.") - if device == "cuda:0" and dtype == torch.float32: - pytest.skip("TODO: loosen tolerances") - return Lattice(stencil, device=device, dtype=dtype, - use_native=(native == "cuda_native")) - - -@pytest.fixture( - params=((torch.float64, "cpu", "", "cuda:0", ""), - (torch.float32, "cpu", "", "cuda:0", ""), - (torch.float64, "cpu", "", "cuda:0", "cuda_native"), - (torch.float32, "cpu", "", "cuda:0", "cuda_native"), - (torch.float64, "cuda:0", "", "cuda:0", "cuda_native"), - (torch.float32, "cuda:0", "", "cuda:0", "cuda_native")), - ids=("cpu_cu_64", "cpu_cu_32", "cpu_native_64", "cpu_native_32", - "cu_native_64", "cu_native_32")) -def lattice2(request, stencil): - """Run a test for all lattices (all stencil, devices and data types - available on the device.)""" - dtype, device, native, device2, native2 = request.param - if device == "cuda:0" and not torch.cuda.is_available(): - pytest.skip(reason="CUDA not available.") - if device == "cuda:0" and dtype == torch.float32: - pytest.skip("TODO: loosen tolerances") - if device2 == "cuda:0" and not torch.cuda.is_available(): - pytest.skip(reason="CUDA not available.") - if device2 == "cuda:0" and dtype == torch.float32: - pytest.skip("TODO: loosen tolerances") - return (Lattice(stencil, device=device, dtype=dtype, - use_native=(native == "cuda_native")), - Lattice(stencil, device=device2, dtype=dtype, - use_native=(native2 == "cuda_native"))) - - -@pytest.fixture() -def f_lattice(lattice): - """Run a test for all lattices; return a grid with 3^D sample distribution - functions alongside the lattice.""" - np.random.seed(1) # arbitrary, but deterministic - return lattice.convert_to_tensor(np.random.random( - [lattice.Q] + [3] * lattice.D)), lattice - - -@pytest.fixture(params=[Lattice]) -def f_all_lattices(request, lattice): - """Run a test for all lattices and lattices-of-vector; - return a grid with 3^D sample distribution functions alongside the lattice. - """ - np.random.seed(1) - f = np.random.random([lattice.Q] + [3] * lattice.D) - Ltc = request.param - ltc = Ltc(lattice.stencil, lattice.device, lattice.dtype) - return ltc.convert_to_tensor(f), ltc - - -@pytest.fixture(params=TRANSFORMS) -def f_transform(request, f_all_lattices): - Transform = request.param - f, lattice = f_all_lattices - if lattice.stencil in Transform.supported_stencils: - return f, Transform(lattice) - else: - pytest.skip("Stencil not supported for this transform.") diff --git a/old_tests/test_boundaries.py b/old_tests/test_boundaries.py deleted file mode 100644 index 978f427c..00000000 --- a/old_tests/test_boundaries.py +++ /dev/null @@ -1,195 +0,0 @@ -""" -Test boundary conditions. -""" - -from lettuce import * -from lettuce.ext import * - -import pytest - -from copy import copy -import numpy as np -import torch - - -def test_bounce_back_boundary(f_lattice): - f, lattice = f_lattice - f_old = copy(f) - mask = (f[0] > 0).cpu().numpy() # will contain all points - bounce_back = BounceBackBoundary(mask, lattice) - f = bounce_back(f) - assert (f[lattice.stencil.opposite].cpu().numpy() == - pytest.approx(f_old.cpu().numpy())) - - -def test_bounce_back_boundary_not_applied_if_mask_empty(f_lattice): - f, lattice = f_lattice - f_old = copy(f) - mask = (f[0] < 0).cpu().numpy() # will not contain any points - bounce_back = BounceBackBoundary(mask, lattice) - f = bounce_back(f) - assert f.cpu().numpy() == pytest.approx(f_old.cpu().numpy()) - - -def test_equilibrium_boundary_pu(f_lattice): - f, lattice = f_lattice - mask = (f[0] > 0).cpu().numpy() # will contain all points - units = UnitConversion(lattice, reynolds_number=1) - pressure = 0 - velocity = 0.1 * np.ones(lattice.D) - feq = lattice.equilibrium( - lattice.convert_to_tensor(units.convert_pressure_pu_to_density_lu( - pressure)), - lattice.convert_to_tensor(units.convert_velocity_to_lu(velocity)) - ) - feq_field = torch.einsum("q,q...->q...", feq, torch.ones_like(f)) - - eq_boundary = EquilibriumBoundaryPU(mask, lattice, units, - velocity=velocity, pressure=pressure) - f = eq_boundary(f) - - assert f.cpu().numpy() == pytest.approx(feq_field.cpu().numpy()) - # assert f.cpu().numpy() == pytest.approx(f_old.cpu().numpy()) - - -def test_anti_bounce_back_outlet(f_lattice): - """Compares the result of the application of the boundary to f to the - result using the formula taken from page 195 - of "The lattice Boltzmann method" (2016 by Krüger et al.) if both are - similar it is assumed to be working fine.""" - f, lattice = f_lattice - # generates reference value of f using non-dynamic formula - f_ref = f - u = lattice.u(f) - D = lattice.stencil.D() - Q = lattice.stencil.Q() - - if D == 3: - direction = [1, 0, 0] - - if Q == 27: - u_w = u[:, -1, :, :] + 0.5 * (u[:, -1, :, :] - u[:, -2, :, :]) - u_w_norm = torch.norm(u_w, dim=0) - - for i in [1, 11, 13, 15, 17, 19, 21, 23, 25]: - stencil_e_tensor = torch.tensor(lattice.stencil.e[i], - device=f.device, dtype=f.dtype) - - f_ref[lattice.stencil.opposite[i], -1, :, :] = ( - - f_ref[i, -1, :, :] - + (lattice.stencil.w[i] * lattice.rho(f)[0, -1, :, :] - * (2 + torch.einsum( - 'c, cyz -> yz', stencil_e_tensor, u_w - ) ** 2 - / lattice.stencil.cs ** 4 - - (u_w_norm / lattice.stencil.cs) ** 2)) - ) - - if Q == 19: - u_w = u[:, -1, :, :] + 0.5 * (u[:, -1, :, :] - u[:, -2, :, :]) - u_w_norm = torch.norm(u_w, dim=0) - - for i in [1, 11, 13, 15, 17]: - stencil_e_tensor = torch.tensor(lattice.stencil.e[i], - device=f.device, dtype=f.dtype) - - f_ref[lattice.stencil.opposite[i], -1, :, :] = ( - - f_ref[i, -1, :, :] - + (lattice.stencil.w[i] - * lattice.rho(f)[0, -1, :, :] - * (2 + torch.einsum('c, cyz -> yz', - stencil_e_tensor, - u_w) ** 2 - / lattice.stencil.cs ** 4 - - (u_w_norm / lattice.stencil.cs) ** 2))) - - if D == 2 and Q == 9: - direction = [1, 0] - u_w = u[:, -1, :] + 0.5 * (u[:, -1, :] - u[:, -2, :]) - u_w_norm = torch.norm(u_w, dim=0) - - for i in [1, 5, 8]: - stencil_e_tensor = torch.tensor(lattice.stencil.e[i], - device=f.device, dtype=f.dtype) - - f_ref[lattice.stencil.opposite[i], -1, :] = - f_ref[i, -1, :] + ( - lattice.stencil.w[i] * lattice.rho(f)[0, -1, :] - * (2 + torch.einsum('c, cy -> y', stencil_e_tensor, - u_w) ** 2 - / lattice.stencil.cs ** 4 - ( - u_w_norm / lattice.stencil.cs) ** 2)) - - if D == 1 and Q == 3: - direction = [1] - u_w = u[:, -1] + 0.5 * (u[:, -1] - u[:, -2]) - u_w_norm = torch.norm(u_w, dim=0) - - for i in [1]: - stencil_e_tensor = torch.tensor(lattice.stencil.e[i], - device=f.device, dtype=f.dtype) - - f_ref[lattice.stencil.opposite[i], -1] = - f_ref[i, -1] + ( - lattice.stencil.w[i] * lattice.rho(f)[0, -1] - * (2 + torch.einsum('c, x -> x', stencil_e_tensor, - u_w) ** 2 - / lattice.stencil.cs ** 4 - ( - u_w_norm / lattice.stencil.cs) ** 2)) - - # generates value from actual boundary implementation - abb_outlet = AntiBounceBackOutlet(lattice, direction) - f = abb_outlet(f) - assert f.cpu().numpy() == pytest.approx(f_ref.cpu().numpy()) - - -def test_masks(dtype_device): - """test if masks are applied from boundary conditions""" - dtype, device = dtype_device - lattice = Lattice(D2Q9, dtype=dtype, device=device) - flow = Obstacle((16, 16), 100, 0.1, lattice, 2) - flow.mask[1, 1] = 1 - streaming = StandardStreaming(lattice) - collision = BGKCollision(lattice, 1.0) - simulation = Simulation(flow, lattice, collision, streaming) - assert simulation.streaming.no_stream_mask.any() - assert simulation.collision.no_collision_mask.any() - - -def test_equilibrium_pressure_outlet(dtype_device): - dtype, device = dtype_device - lattice = Lattice(D2Q9, dtype=dtype, device=device) - - class MyObstacle(Obstacle): - @property - def boundaries(self, *args): - x, y = self.grid - return [ - EquilibriumBoundaryPU( - np.abs(x) < 1e-6, self.units.lattice, self.units, - np.array([self.units.characteristic_velocity_pu, 0]) - ), - EquilibriumOutletP(self.units.lattice, [0, -1]), - EquilibriumOutletP(self.units.lattice, [0, 1]), - EquilibriumOutletP(self.units.lattice, [1, 0]), - BounceBackBoundary(self.mask, self.units.lattice) - ] - - flow = MyObstacle((32, 32), reynolds_number=10, mach_number=0.1, - lattice=lattice, domain_length_x=3) - mask = np.zeros_like(flow.grid[0], dtype=bool) - mask[10:20, 10:20] = 1 - flow.mask = mask - simulation = Simulation(flow, lattice, - RegularizedCollision( - lattice, - flow.units.relaxation_parameter_lu), - StandardStreaming(lattice)) - simulation.step(30) - rho = lattice.rho(simulation.f) - u = lattice.u(simulation.f) - feq = lattice.equilibrium(torch.ones_like(rho), u) - p = flow.units.convert_density_lu_to_pressure_pu(rho) - zeros = torch.zeros_like(p[0, -1, :]) - assert torch.allclose(zeros, p[:, -1, :], rtol=0, atol=1e-4) - assert torch.allclose(zeros, p[:, :, 0], rtol=0, atol=1e-4) - assert torch.allclose(zeros, p[:, :, -1], rtol=0, atol=1e-4) - assert torch.allclose(feq[:, -1, 1:-1], feq[:, -2, 1:-1]) diff --git a/old_tests/test_collision.py b/old_tests/test_collision.py deleted file mode 100644 index 7d9b7092..00000000 --- a/old_tests/test_collision.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -Test functions for collision models and related functions. -""" - -from copy import copy -import torch -import pytest -import numpy as np -from lettuce import * -from tests.common import DummyFlow - - -@pytest.mark.parametrize("Collision", - [BGKCollision, KBCCollision2D, KBCCollision3D, - TRTCollision, RegularizedCollision, - SmagorinskyCollision]) -def test_collision_conserves_mass(Collision, f_all_lattices): - f, lattice = f_all_lattices - if ((Collision == KBCCollision2D and lattice.stencil != D2Q9) or - (Collision == KBCCollision3D and lattice.stencil != D3Q27)): - pytest.skip() - f_old = copy(f) - collision = Collision(lattice, 0.51) - f = collision(f) - assert (lattice.rho(f).cpu().numpy() - == pytest.approx(lattice.rho(f_old).cpu().numpy())) - - -@pytest.mark.parametrize("Collision", - [BGKCollision, KBCCollision2D, KBCCollision3D, - TRTCollision, RegularizedCollision, - SmagorinskyCollision]) -def test_collision_conserves_momentum(Collision, f_all_lattices): - f, lattice = f_all_lattices - if ((Collision == KBCCollision2D and lattice.stencil != D2Q9) or ( - (Collision == KBCCollision3D and lattice.stencil != D3Q27))): - pytest.skip() - f_old = copy(f) - collision = Collision(lattice, 0.51) - f = collision(f) - assert (lattice.j(f).cpu().numpy() - == pytest.approx(lattice.j(f_old).cpu().numpy(), abs=1e-5)) - - -@pytest.mark.parametrize("Collision", [BGKCollision]) -def test_collision_fixpoint_2x(Collision, f_all_lattices): - f, lattice = f_all_lattices - f_old = copy(f) - collision = Collision(lattice, 0.5) - f = collision(collision(f)) - assert f.cpu().numpy() == pytest.approx(f_old.cpu().numpy(), abs=1e-5) - - -@pytest.mark.parametrize("Collision", - [BGKCollision, TRTCollision, KBCCollision2D, - KBCCollision3D, RegularizedCollision]) -def test_collision_relaxes_shear_moments(Collision, f_all_lattices): - """checks whether the collision models relax the shear moments according - to the prescribed relaxation time""" - f, lattice = f_all_lattices - if ((Collision == KBCCollision2D and lattice.stencil != D2Q9) or ( - (Collision == KBCCollision3D and lattice.stencil != D3Q27))): - pytest.skip() - rho = lattice.rho(f) - u = lattice.u(f) - feq = lattice.equilibrium(rho, u) - shear_pre = lattice.shear_tensor(f) - shear_eq_pre = lattice.shear_tensor(feq) - tau = 0.6 - coll = Collision(lattice, tau) - f_post = coll(f) - shear_post = lattice.shear_tensor(f_post) - assert (shear_post.cpu().numpy() == - pytest.approx((shear_pre - - 1 / tau * (shear_pre - shear_eq_pre) - ).cpu().numpy(), abs=1e-5)) - - -@pytest.mark.parametrize("Collision", [KBCCollision2D, KBCCollision3D]) -def test_collision_optimizes_pseudo_entropy(Collision, f_all_lattices): - """checks if the pseudo-entropy of the KBC collision model is at least - higher than the BGK pseudo-entropy""" - f, lattice = f_all_lattices - if ((Collision == KBCCollision2D and lattice.stencil != D2Q9) or ( - (Collision == KBCCollision3D and lattice.stencil != D3Q27))): - pytest.skip() - tau = 0.5003 - coll_kbc = Collision(lattice, tau) - coll_bgk = BGKCollision(lattice, tau) - f_kbc = coll_kbc(f) - f_bgk = coll_bgk(f) - entropy_kbc = lattice.pseudo_entropy_local(f_kbc) - entropy_bgk = lattice.pseudo_entropy_local(f_bgk) - assert (entropy_bgk.cpu().numpy() < entropy_kbc.cpu().numpy()).all() - - -@pytest.mark.parametrize("Transform", [D2Q9Lallemand, D2Q9Dellar]) -def test_collision_fixpoint_2x_MRT(Transform, dtype_device): - dtype, device = dtype_device - context = Context(device=device, dtype=dtype) - np.random.seed(1) # arbitrary, but deterministic - stencil = D2Q9() - f = context.convert_to_tensor(np.random.random([stencil.q] + [3] * - stencil.d)) - f_old = copy(f) - flow = DummyFlow(context, 1) - collision = MRTCollision(Transform(stencil), np.array([0.5] * 9)) - f = collision(collision(flow)) - print(f.cpu().numpy(), f_old.cpu().numpy()) - assert f.cpu().numpy() == pytest.approx(f_old.cpu().numpy(), abs=1e-5) - - -def test_bgk_collision_devices(lattice2): - if lattice2[0].stencil.D() != 2 and lattice2[0].stencil.D() != 3: - pytest.skip("Test for 2D and 3D only!") - - def simulate(lattice): - Flow = TaylorGreenVortex2D if lattice2[0].stencil.D() == 2 else ( - TaylorGreenVortex3D) - flow = Flow(resolution=16, reynolds_number=10, mach_number=0.05, - lattice=lattice) - - collision = BGKCollision(lattice, - tau=flow.units.relaxation_parameter_lu) - streaming = NoStreaming(lattice) - simulation = Simulation(flow=flow, lattice=lattice, - collision=collision, streaming=streaming) - simulation.step(4) - - return simulation.f - - lattice0, lattice1 = lattice2 - f0 = simulate(lattice0).to(torch.device("cpu")) - f1 = simulate(lattice1).to(torch.device("cpu")) - error = torch.abs(f0 - f1).sum().data - assert float(error) < 1.0e-8 diff --git a/old_tests/test_equilibrium.py b/old_tests/test_equilibrium.py deleted file mode 100644 index 6a69b52d..00000000 --- a/old_tests/test_equilibrium.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -Tests for equilibria -""" - -import pytest -from lettuce._equilibrium import * - - -@pytest.mark.parametrize("Equilibrium", [QuadraticEquilibrium]) -def test_equilibrium_conserves_mass(f_all_lattices, Equilibrium): - f, lattice = f_all_lattices - equilibrium = Equilibrium(lattice) - feq = equilibrium(rho=lattice.rho(f), u=lattice.u(f)) - assert lattice.rho(feq).cpu().numpy() == pytest.approx(lattice.rho(f).cpu().numpy()) - - -@pytest.mark.parametrize("Equilibrium", [QuadraticEquilibrium]) -def test_equilibrium_conserves_momentum(f_all_lattices, Equilibrium): - f, lattice = f_all_lattices - equilibrium = Equilibrium(lattice) - feq = equilibrium(rho=lattice.rho(f), u=lattice.u(f)) - assert lattice.j(feq).cpu().numpy() == pytest.approx(lattice.j(f).cpu().numpy(), abs=1e-6) diff --git a/old_tests/test_flows.py b/old_tests/test_flows.py deleted file mode 100644 index 55a452fa..00000000 --- a/old_tests/test_flows.py +++ /dev/null @@ -1,86 +0,0 @@ -import pytest -import numpy as np -import torch -from lettuce import TaylorGreenVortex2D, TaylorGreenVortex3D, CouetteFlow2D, D2Q9, D3Q27, DoublyPeriodicShear2D -from lettuce import torch_gradient, DecayingTurbulence -from lettuce import Lattice, Simulation, BGKCollision, BGKInitialization, StandardStreaming -from lettuce import Obstacle2D, Obstacle3D -from lettuce.flows.poiseuille import PoiseuilleFlow2D -from lettuce import Context - -# Flows to test -INCOMPRESSIBLE_2D = [TaylorGreenVortex2D, CouetteFlow2D, PoiseuilleFlow2D, DoublyPeriodicShear2D, DecayingTurbulence] -INCOMPRESSIBLE_3D = [TaylorGreenVortex3D, DecayingTurbulence] - - -@pytest.mark.parametrize("IncompressibleFlow", INCOMPRESSIBLE_2D) -def test_flow_2d(IncompressibleFlow, dtype_device): - dtype, device = dtype_device - context = Context(dtype=dtype, device=device, use_native=False) - flow = IncompressibleFlow(context=context, resolution=16, - reynolds_number=1, mach_number=0.05) - collision = BGKCollision(tau=flow.units.relaxation_parameter_lu) - streaming = StandardStreaming(lattice) - simulation = Simulation(flow=flow, lattice=lattice, collision=collision, streaming=streaming) - simulation.step(1) - - -@pytest.mark.parametrize("IncompressibleFlow", INCOMPRESSIBLE_3D) -def test_flow_3d(IncompressibleFlow, dtype_device): - dtype, device = dtype_device - lattice = Lattice(D3Q27, dtype=dtype, device=device) - flow = IncompressibleFlow(16, 1, 0.05, lattice=lattice) - collision = BGKCollision(lattice, tau=flow.units.relaxation_parameter_lu) - streaming = StandardStreaming(lattice) - simulation = Simulation(flow=flow, lattice=lattice, collision=collision, streaming=streaming) - simulation.step(1) - - -@pytest.mark.parametrize("_stencil", [D2Q9, D3Q27]) -def test_divergence(stencil, dtype_device): - dtype, device = dtype_device - lattice = Lattice(stencil, dtype=dtype, device=device) - flow = DecayingTurbulence(50, 1, 0.05, lattice=lattice, ic_energy=0.5) - collision = BGKCollision(lattice, tau=flow.units.relaxation_parameter_lu) - streaming = StandardStreaming(lattice) - simulation = Simulation(flow=flow, lattice=lattice, collision=collision, streaming=streaming) - ekin = flow.units.convert_incompressible_energy_to_pu( - torch.sum(lattice.incompressible_energy(simulation.f))) * flow.units.convert_length_to_pu(1.0) ** lattice.D - - u0 = flow.units.convert_velocity_to_pu(lattice.u(simulation.f)[0]) - u1 = flow.units.convert_velocity_to_pu(lattice.u(simulation.f)[1]) - dx = flow.units.convert_length_to_pu(1.0) - grad_u0 = torch_gradient(u0, dx=dx, order=6).cpu().numpy() - grad_u1 = torch_gradient(u1, dx=dx, order=6).cpu().numpy() - divergence = np.sum(grad_u0[0] + grad_u1[1]) - - if lattice.D == 3: - u2 = flow.units.convert_velocity_to_pu(lattice.u(simulation.f)[2]) - grad_u2 = torch_gradient(u2, dx=dx, order=6).cpu().numpy() - divergence += np.sum(grad_u2[2]) - assert (flow.ic_energy == pytest.approx(lattice.convert_to_numpy(ekin), rel=1)) - assert (0 == pytest.approx(divergence, abs=2e-3)) - - -@pytest.mark.parametrize("_stencil", [D2Q9, D3Q27]) -def test_obstacle(stencil, dtype_device): - dtype, device = dtype_device - lattice = Lattice(stencil, dtype=dtype, device=device) - - nx = 32 - ny = 16 - nz = 16 - - if stencil is D2Q9: - mask = np.zeros([nx, ny]) - mask[3:6, 3:6] = 1 - flow = Obstacle2D(nx, ny, 100, 0.1, lattice=lattice, char_length_lu=3) - if stencil is D3Q27: - mask = np.zeros([nx, ny, nz]) - mask[3:6, 3:6, :] = 1 - flow = Obstacle3D(nx, ny, nz, 100, 0.1, lattice=lattice, char_length_lu=3) - collision = BGKCollision(lattice, tau=flow.units.relaxation_parameter_lu) - flow.mask = mask != 0 - streaming = StandardStreaming(lattice) - simulation = Simulation(flow, lattice, collision, streaming) - simulation.step(2) diff --git a/old_tests/test_force.py b/old_tests/test_force.py deleted file mode 100644 index 813677d4..00000000 --- a/old_tests/test_force.py +++ /dev/null @@ -1,29 +0,0 @@ -import pytest -import torch -import numpy as np -from lettuce import D2Q9 -from lettuce import Lattice, Simulation, BGKCollision, StandardStreaming -from lettuce.flows.poiseuille import PoiseuilleFlow2D -from lettuce.util.force import Guo, ShanChen - - -@pytest.mark.parametrize("ForceType", [Guo, ShanChen]) -def test_force(ForceType, device): - dtype = torch.double - lattice = Lattice(D2Q9, dtype=dtype, device=device, use_native=False) # TODO use_native Fails here - flow = PoiseuilleFlow2D(resolution=16, reynolds_number=1, mach_number=0.02, lattice=lattice, - initialize_with_zeros=True) - acceleration_lu = flow.units.convert_acceleration_to_lu(flow.acceleration) - force = ForceType(lattice, tau=flow.units.relaxation_parameter_lu, - acceleration=acceleration_lu) - collision = BGKCollision(lattice, tau=flow.units.relaxation_parameter_lu, force=force) - streaming = StandardStreaming(lattice) - simulation = Simulation(flow=flow, lattice=lattice, collision=collision, streaming=streaming) - simulation.step(1000) - # compare with reference solution - u_sim = lattice.u(simulation.f, acceleration=torch.as_tensor(acceleration_lu, device=device)) - u_sim = flow.units.convert_velocity_to_pu(lattice.convert_to_numpy(u_sim)) - _, u_ref = flow.analytic_solution(flow.grid) - fluidnodes = np.where(np.logical_not(flow.boundaries[0].mask.cpu())) - assert u_ref[0].max() == pytest.approx(u_sim[0].max(), rel=0.01) - assert u_ref[0][fluidnodes] == pytest.approx(u_sim[0][fluidnodes], rel=None, abs=0.01 * u_ref[0].max()) diff --git a/old_tests/test_lattices.py b/old_tests/test_lattices.py deleted file mode 100644 index 5d5595a6..00000000 --- a/old_tests/test_lattices.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Tests for lattices. -""" diff --git a/old_tests/test_moments.py b/old_tests/test_moments.py deleted file mode 100644 index 24a67386..00000000 --- a/old_tests/test_moments.py +++ /dev/null @@ -1,99 +0,0 @@ -import numpy as np -import pytest -from lettuce.util.moments import * -from lettuce._stencil import * -from lettuce.lattices import Lattice - - -def test_moments_density_array(stencil): - rho_tensor = moment_tensor(stencil.e, np.array([0] * stencil.D())) - assert rho_tensor == pytest.approx(np.ones((stencil.Q()))) - - -def test_more_moments_density_array(stencil): - rho_tensor = moment_tensor(stencil.e, np.array([[0] * stencil.D()])) - assert rho_tensor == pytest.approx(np.ones((1, stencil.Q()))) - - -def test_moments_density_tensor(lattice): - rho_tensor = moment_tensor(lattice.e, lattice.convert_to_tensor(([0] * lattice.D))) - assert rho_tensor.shape == (lattice.Q,) - assert rho_tensor.cpu().numpy() == pytest.approx(np.ones((lattice.Q))) - - -def test_more_moments_density_tensor(lattice): - rho_tensor = moment_tensor(lattice.e, lattice.convert_to_tensor(([[0] * lattice.D]))) - assert rho_tensor.shape == (1, lattice.Q) - assert rho_tensor.cpu().numpy() == pytest.approx(np.ones((1, lattice.Q))) - - -@pytest.mark.parametrize("MomentSet", (D2Q9Dellar, D2Q9Lallemand)) -def test_conserved_moments_d2q9(MomentSet): - multiindices = np.array([ - [0, 0], [1, 0], [0, 1] - ]) - m = moment_tensor(D2Q9.e, multiindices) - assert m == pytest.approx(MomentSet.matrix[:3, :]) - - -def test_inverse_transform(f_transform): - f, transform = f_transform - lattice = transform.lattice - retransformed = lattice.convert_to_numpy(transform.inverse_transform(transform.transform(f))) - original = lattice.convert_to_numpy(f) - assert retransformed == pytest.approx(original, abs=1e-5) - - -def test_getitem(dtype_device): - dtype, device = dtype_device - moments = D2Q9Lallemand(Lattice(D2Q9, device, dtype)) - assert moments["jx", "jy"] == [1, 2] - assert moments["rho"] == [0] - - -def test_moment_equilibrium_dellar(dtype_device): - dtype, device = dtype_device - lattice = Lattice(D2Q9, device, dtype) - moments = D2Q9Dellar(lattice) - np.random.seed(1) - f = lattice.convert_to_tensor(np.random.random([lattice.Q] + [3] * lattice.D)) - meq1 = lattice.convert_to_numpy(moments.transform(lattice.equilibrium(lattice.rho(f), lattice.u(f)))) - meq2 = lattice.convert_to_numpy(moments.equilibrium(moments.transform(f))) - assert meq1 == pytest.approx(meq2, abs=1e-5) - - -def test_moment_equilibrium_lallemand(dtype_device): - dtype, device = dtype_device - lattice = Lattice(D2Q9, device, dtype) - moments = D2Q9Lallemand(lattice) - np.random.seed(1) - f = lattice.convert_to_tensor(np.random.random([lattice.Q] + [3] * lattice.D)) - meq1 = lattice.convert_to_numpy(moments.transform(lattice.equilibrium(lattice.rho(f), lattice.u(f)))) - meq2 = lattice.convert_to_numpy(moments.equilibrium(moments.transform(f))) - same_moments = moments["rho", "jx", "jy", "qx", "qy"] - assert meq1[same_moments] == pytest.approx(meq2[same_moments], abs=1e-5) - - -def test_moment_equilibrium_D3Q27Hermite(dtype_device): - dtype, device = dtype_device - lattice = Lattice(D3Q27, device, dtype) - moments = D3Q27Hermite(lattice) - np.random.seed(1) - f = lattice.convert_to_tensor(np.random.random([lattice.Q] + [3] * lattice.D)) - meq1 = lattice.convert_to_numpy(moments.transform(lattice.equilibrium(lattice.rho(f), lattice.u(f)))) - meq2 = lattice.convert_to_numpy(moments.equilibrium(moments.transform(f))) - same_moments = moments['rho', 'jx', 'jy', 'jz', 'Pi_xx', 'Pi_xy', 'PI_xz', 'PI_yy', 'PI_yz', 'PI_zz'] - assert meq1[same_moments] == pytest.approx(meq2[same_moments], abs=1e-5) - - -@pytest.mark.parametrize("MomentSet", (D2Q9Dellar, D2Q9Lallemand, D3Q27Hermite)) -def test_orthogonality(dtype_device, MomentSet): - dtype, device = dtype_device - lattice = Lattice(MomentSet.supported_stencils[0], device, dtype) - moments = MomentSet(lattice) - M = Lattice.convert_to_numpy(moments.matrix) - if MomentSet == D2Q9Lallemand: - Md = np.round(M @ M.T, 4) - else: - Md = np.round(M @ np.diag(lattice.stencil.w) @ M.T, 4) - assert np.where(np.diag(np.ones(lattice.stencil.Q())), Md != 0.0, Md == 0.0).all() diff --git a/old_tests/test_native.py b/old_tests/test_native.py deleted file mode 100644 index 1ed072e1..00000000 --- a/old_tests/test_native.py +++ /dev/null @@ -1,72 +0,0 @@ -import pytest -import torch - -from lettuce import StandardStreaming, Lattice, Obstacle2D, D2Q9, NoCollision, BGKCollision, NoStreaming, Simulation - - -def test_native_no_streaming_mask(): - """test if """ - - if not torch.cuda.is_available(): - pytest.skip("Native test skipped") - - dtype = torch.float32 - device = "cuda:0" - - lattice = Lattice(D2Q9, dtype=dtype, device=device) - flow = Obstacle2D(16, 16, 128, 0.1, lattice, 2) - flow.mask[5, 5] = 1 - - streaming = StandardStreaming(lattice) - collision = NoCollision(lattice) - - simulation = Simulation(flow, lattice, collision, streaming) - simulation.step(64) - - lattice_n = Lattice(D2Q9, dtype=dtype, device=device) - flow_n = Obstacle2D(16, 16, 128, 0.1, lattice_n, 2) - flow_n.mask[5, 5] = 1 - - streaming_n = StandardStreaming(lattice_n) - collision_n = NoCollision(lattice_n) - - simulation_n = Simulation(flow_n, lattice_n, collision_n, streaming_n) - assert not (simulation_n.step_ == simulation_n.pipeline_steps_) - simulation_n.step(64) - - error = torch.abs(simulation.f - simulation_n.f).sum().data - assert error == 0.0 - - -def test_native_no_collision_mask(): - """test if """ - - if not torch.cuda.is_available(): - pytest.skip("Native test skipped") - - dtype = torch.float32 - device = "cuda:0" - - lattice = Lattice(D2Q9, dtype=dtype, device=device) - flow = Obstacle2D(16, 16, 128, 0.1, lattice, 2) - flow.mask[5, 5] = 1 - - streaming = NoStreaming(lattice) - collision = BGKCollision(lattice, 1.0) - - simulation = Simulation(flow, lattice, collision, streaming) - simulation.step(64) - - lattice_n = Lattice(D2Q9, dtype=dtype, device=device) - flow_n = Obstacle2D(16, 16, 128, 0.1, lattice_n, 2) - flow_n.mask[5, 5] = 1 - - streaming_n = NoStreaming(lattice_n) - collision_n = BGKCollision(lattice_n, 1.0) - - simulation_n = Simulation(flow_n, lattice_n, collision_n, streaming_n) - assert not (simulation_n.step_ == simulation_n.pipeline) - simulation_n.step(64) - - error = torch.abs(simulation.f - simulation_n.f).sum().data - assert error < 1.0e-24 diff --git a/old_tests/test_observables.py b/old_tests/test_observables.py deleted file mode 100644 index 9162f3fa..00000000 --- a/old_tests/test_observables.py +++ /dev/null @@ -1,32 +0,0 @@ -import pytest - -import numpy as np -from lettuce import Lattice, D2Q9, D3Q27, BGKCollision, DecayingTurbulence, StandardStreaming, Simulation -from lettuce.flows.taylorgreen import TaylorGreenVortex3D, TaylorGreenVortex2D -from lettuce.util.observables import EnergySpectrum, IncompressibleKineticEnergy - - -@pytest.mark.parametrize("Flow", - [TaylorGreenVortex2D, TaylorGreenVortex3D, 'DecayingTurbulence2D', 'DecayingTurbulence3D']) -def test_energy_spectrum(tmpdir, Flow): - lattice = Lattice(D2Q9, device='cpu') - if Flow == TaylorGreenVortex3D or Flow == 'DecayingTurbulence3D': - lattice = Lattice(D3Q27, device='cpu') - if Flow == 'DecayingTurbulence2D' or Flow == 'DecayingTurbulence3D': - Flow = DecayingTurbulence - flow = Flow(resolution=20, reynolds_number=1600, mach_number=0.01, lattice=lattice) - collision = BGKCollision(lattice, tau=flow.units.relaxation_parameter_lu) - streaming = StandardStreaming(lattice) - simulation = Simulation(flow, lattice, collision, streaming) - spectrum = lattice.convert_to_numpy(EnergySpectrum(lattice, flow)(simulation.f)) - energy = IncompressibleKineticEnergy(lattice, flow)(simulation.f).item() - - if Flow == DecayingTurbulence: - # check that the reported spectrum agrees with the spectrum used for initialization - ek_ref, _ = flow.energy_spectrum - assert (spectrum == pytest.approx(ek_ref, rel=0.0, abs=0.1)) - if Flow == TaylorGreenVortex2D or Flow == TaylorGreenVortex3D: - # check that flow has only one mode - ek_max = sorted(spectrum, reverse=True) - assert ek_max[0] * 1e-5 > ek_max[1] - assert (energy == pytest.approx(np.sum(spectrum), rel=0.1, abs=0.0)) diff --git a/old_tests/test_readme.py b/old_tests/test_readme.py deleted file mode 100644 index 203a32d3..00000000 --- a/old_tests/test_readme.py +++ /dev/null @@ -1,19 +0,0 @@ -def test_readme(): - """Whenever you have to change this test, the example in README.rst has to change, too. - Note differences in the device + number of steps. - """ - - import torch - from lettuce import BGKCollision, StandardStreaming, Lattice, D2Q9, TaylorGreenVortex2D, Simulation - - device = "cpu" - dtype = torch.float32 - - lattice = Lattice(D2Q9, device, dtype) - flow = TaylorGreenVortex2D(resolution=256, reynolds_number=10, mach_number=0.05, lattice=lattice) - collision = BGKCollision(lattice, tau=flow.units.relaxation_parameter_lu) - streaming = StandardStreaming(lattice) - simulation = Simulation(flow, lattice, collision, streaming) - mlups = simulation.step(num_steps=1) - - print("Performance in MLUPS:", mlups) diff --git a/old_tests/test_reporters.py b/old_tests/test_reporters.py deleted file mode 100644 index be5dba9a..00000000 --- a/old_tests/test_reporters.py +++ /dev/null @@ -1,119 +0,0 @@ -import pytest -import os -from lettuce import (TaylorGreenVortex2D, TaylorGreenVortex3D, - PoiseuilleFlow2D, Lattice, D3Q27, D2Q9, write_image, - BGKCollision, StandardStreaming, Simulation, Context) -from lettuce.util.reporters import write_vtk, VTKReporter, ObservableReporter -from lettuce.util.datautils import HDF5Reporter, LettuceDataset -from lettuce.util.observables import (Enstrophy, EnergySpectrum, - MaximumVelocity, - IncompressibleKineticEnergy, Mass) -import numpy as np -import torch - - -def test_write_image(tmpdir): - pytest.skip("matplotlib not working") - lattice = Lattice(D2Q9, "cpu") - flow = TaylorGreenVortex2D(resolution=16, reynolds_number=10, - mach_number=0.05, lattice=lattice) - p, u = flow.initial_pu(flow.grid) - write_image(tmpdir / "p.png", p[0]) - print(tmpdir / "p.png") - assert os.path.isfile(tmpdir / "p.png") - - -@pytest.mark.parametrize("Observable", [Enstrophy, EnergySpectrum, - MaximumVelocity, - IncompressibleKineticEnergy, Mass]) -@pytest.mark.parametrize("Case", [TaylorGreenVortex2D, TaylorGreenVortex3D]) -def test_generic_reporters(Observable, Case, dtype_device): - dtype, device = dtype_device - lattice = Lattice(D2Q9, dtype=dtype, device=device, use_native=False) - flow = Case(32, 10000, 0.05, lattice=lattice) - if Case == TaylorGreenVortex3D: - lattice = Lattice(D3Q27, dtype=dtype, device=device, use_native=False) - collision = BGKCollision(lattice, tau=flow.units.relaxation_parameter_lu) - streaming = StandardStreaming(lattice) - simulation = Simulation(flow, lattice, collision, streaming) - reporter = ObservableReporter(Observable(lattice, flow), interval=1, - out=None) - simulation.reporters.append(reporter) - simulation.step(2) - values = np.asarray(reporter.out) - if Observable is EnergySpectrum: - assert values[1, 2:] == pytest.approx(values[0, 2:], rel=0.0, - abs=values[0, 2:].sum() / 10) - else: - assert values[1, 2] == pytest.approx(values[0, 2], rel=0.05) - - -def test_write_vtk(tmpdir): - lattice = Lattice(D2Q9, "cpu") - flow = TaylorGreenVortex2D(resolution=16, reynolds_number=10, - mach_number=0.05, lattice=lattice) - p, u = flow.initial_pu(flow.grid) - point_dict = {"p": p[0, ..., None]} - write_vtk(point_dict, id=1, filename_base=tmpdir / "output") - assert os.path.isfile(tmpdir / "output_00000001.vtr") - - -def test_vtk_reporter_no_mask(tmpdir): - lattice = Lattice(D2Q9, "cpu") - flow = TaylorGreenVortex2D(resolution=16, reynolds_number=10, - mach_number=0.05, lattice=lattice) - collision = BGKCollision(lattice, tau=flow.units.relaxation_parameter_lu) - streaming = StandardStreaming(lattice) - simulation = Simulation(flow, lattice, collision, streaming) - vtk_reporter = VTKReporter(lattice, flow, interval=1, - filename_base=tmpdir / "output") - simulation.reporters.append(vtk_reporter) - simulation.step(2) - assert os.path.isfile(tmpdir / "output_00000000.vtr") - assert os.path.isfile(tmpdir / "output_00000001.vtr") - - -def test_vtk_reporter_mask(tmpdir): - flow = PoiseuilleFlow2D(context=Context(), resolution=16, - reynolds_number=10, mach_number=0.05) - collision = BGKCollision(tau=flow.units.relaxation_parameter_lu) - simulation = Simulation(flow, collision, []) - vtk_reporter = VTKReporter(interval=1, filename_base=tmpdir / "output") - simulation.reporter.append(vtk_reporter) - vtk_reporter.output_mask(simulation) - simulation(2) - assert os.path.isfile(tmpdir / "output_mask.vtr") - assert os.path.isfile(tmpdir / "output_00000000.vtr") - assert os.path.isfile(tmpdir / "output_00000001.vtr") - - -def test_HDF5Reporter(tmpdir): - step = 3 - lattice = Lattice(D2Q9, "cpu") - flow = TaylorGreenVortex2D(resolution=16, reynolds_number=10, - mach_number=0.05, lattice=lattice) - collision = BGKCollision(lattice=lattice, - tau=flow.units.relaxation_parameter_lu) - streaming = StandardStreaming(lattice=lattice) - simulation = Simulation(flow=flow, - lattice=lattice, - collision=collision, - streaming=streaming) - hdf5_reporter = HDF5Reporter( - flow=flow, - collision=collision, - interval=step, - filebase=tmpdir / "output") - simulation.reporters.append(hdf5_reporter) - simulation.step(step) - assert os.path.isfile(tmpdir / "output.h5") - - dataset_train = LettuceDataset( - filebase=tmpdir / "output.h5", - target=True) - train_loader = torch.utils.data.DataLoader(dataset_train, shuffle=False) - print(dataset_train) - for (f, target, idx) in train_loader: - assert idx in (0, 1, 2) - assert f.shape == (1, 9, 16, 16) - assert target.shape == (1, 9, 16, 16) diff --git a/old_tests/test_simulation.py b/old_tests/test_simulation.py deleted file mode 100644 index 5029e286..00000000 --- a/old_tests/test_simulation.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Tests for simulation""" - -import pytest -import numpy as np -from lettuce import ( - Simulation, TaylorGreenVortex2D, TaylorGreenVortex3D, Lattice, - D2Q9, D3Q27, BGKCollision, StandardStreaming, ErrorReporter, - DecayingTurbulence -) -import torch - - -# Note: Simulation is also implicitly tested in test_flows - - -def test_save_and_load(dtype_device, tmpdir): - dtype, device = dtype_device - lattice = Lattice(D2Q9, device, dtype) - flow = TaylorGreenVortex2D(resolution=16, reynolds_number=10, - mach_number=0.05, lattice=lattice) - collision = BGKCollision(lattice, tau=flow.units.relaxation_parameter_lu) - streaming = StandardStreaming(lattice) - simulation = Simulation(flow, lattice, collision, streaming) - simulation.step(10) - simulation.save_checkpoint(tmpdir / "checkpoint.pic") - simulation2 = Simulation(flow, lattice, collision, streaming) - simulation2.load_checkpoint(tmpdir / "checkpoint.pic") - assert (lattice.convert_to_numpy(simulation2.f) - == pytest.approx(lattice.convert_to_numpy(simulation.f))) - - -@pytest.mark.parametrize("use_jacobi", [True, False]) -def test_initialization(dtype_device, use_jacobi): - dtype, device = dtype_device - lattice = Lattice(D2Q9, device, dtype) - flow = TaylorGreenVortex2D(resolution=24, reynolds_number=10, - mach_number=0.05, lattice=lattice) - collision = BGKCollision(lattice, tau=flow.units.relaxation_parameter_lu) - streaming = StandardStreaming(lattice) - simulation = Simulation(flow, lattice, collision, streaming) - # set initial pressure to 0 everywhere - p, u = flow.initial_pu(flow.grid) - u0 = lattice.convert_to_tensor(flow.units.convert_velocity_to_lu(u)) - rho0 = lattice.convert_to_tensor(np.ones_like(u0[0, ...].cpu())) - simulation.f = lattice.equilibrium(rho0, u0) - if use_jacobi: - simulation.initialize_pressure(1000, 1e-6) - num_iterations = 0 - else: - num_iterations = simulation.initialize(500, 1e-3) - piter = lattice.convert_to_numpy( - flow.units.convert_density_lu_to_pressure_pu( - lattice.rho(simulation.f))) - # assert that pressure is converged up to 0.05 (max p - assert piter == pytest.approx(p, rel=0.0, abs=5e-2) - assert num_iterations < 500 - - -@pytest.mark.parametrize("Case", [TaylorGreenVortex2D, TaylorGreenVortex3D, - DecayingTurbulence]) -def test_initialize_fneq(Case, dtype_device): - dtype, device = dtype_device - lattice = Lattice(D2Q9, device, dtype, use_native=False) - if "3D" in Case.__name__: - lattice = Lattice(D3Q27, dtype=dtype, device=device, use_native=False) - flow = Case(resolution=32, reynolds_number=1000, mach_number=0.1, - lattice=lattice) - collision = BGKCollision(lattice, tau=flow.units.relaxation_parameter_lu) - streaming = StandardStreaming(lattice) - simulation_neq = Simulation(flow, collision, []) - - pre_rho = lattice.rho(simulation_neq.f) - pre_u = lattice.u(simulation_neq.f) - pre_ke = lattice.incompressible_energy(simulation_neq.f) - - simulation_neq.initialize_f_neq() - - post_rho = lattice.rho(simulation_neq.f) - post_u = lattice.u(simulation_neq.f) - post_ke = lattice.incompressible_energy(simulation_neq.f) - tol = 1e-6 - assert torch.allclose(pre_rho, post_rho, rtol=0.0, atol=tol) - assert torch.allclose(pre_u, post_u, rtol=0.0, atol=tol) - assert torch.allclose(pre_ke, post_ke, rtol=0.0, atol=tol) - - if Case is TaylorGreenVortex2D: - error_reporter_neq = ErrorReporter(flow.analytic_solution, interval=1, - out=None) - error_reporter_eq = ErrorReporter(flow.analytic_solution, - interval=1, out=None) - simulation_eq = Simulation(flow, collision, []) - simulation_neq.reporter.append(error_reporter_neq) - simulation_eq.reporter.append(error_reporter_eq) - - simulation_neq(10) - simulation_eq(10) - error_u, error_p = np.mean(np.abs(error_reporter_neq.out), - axis=0).tolist() - error_u_eq, error_p_eq = np.mean(np.abs(error_reporter_eq.out), - axis=0).tolist() - - assert error_u < error_u_eq diff --git a/old_tests/test_stencils.py b/old_tests/test_stencils.py deleted file mode 100644 index 04c2f1de..00000000 --- a/old_tests/test_stencils.py +++ /dev/null @@ -1,22 +0,0 @@ -import pytest -import numpy as np - - -def test_opposite(lattice): - """Test if the opposite field holds the index of the opposite direction.""" - assert lattice.e[lattice.stencil.opposite].cpu().numpy() == pytest.approx(-lattice.e.cpu().numpy()) - - -def test_symmetry(lattice): - """Test if the stencil is symmetric""" - assert sum(sum(lattice.e.cpu().numpy())) == pytest.approx(0.0) - - -def test_weights(lattice): - """Test if the sum of all weights equals one.""" - assert sum(lattice.w.cpu().numpy()) == pytest.approx(1.0) - - -def test_first_zero(lattice): - """Test that the zeroth velocity is 0.""" - assert lattice.stencil.e[0] == pytest.approx(np.zeros_like(lattice.stencil.e[0])) diff --git a/old_tests/test_streaming.py b/old_tests/test_streaming.py deleted file mode 100644 index 2bef71db..00000000 --- a/old_tests/test_streaming.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -Tests for streaming operators. -""" - -from lettuce import * -from lettuce.flows import * - -import torch -import pytest -import copy - - -def test_standard_streaming_x3(f_all_lattices): - """Streaming three times on a 3^D grid gives the original distribution functions.""" - f, lattice = f_all_lattices - f_old = copy.copy(f.cpu().numpy()) - streaming = StandardStreaming(lattice) - f = streaming(streaming(streaming(f))) - assert f.cpu().numpy() == pytest.approx(f_old) - - -def test_standard_streaming_hardcoded_2D(lattice): - if not (lattice.stencil.D() == 2): - pytest.skip("Custom Test for 2D stencil") - - dummy_flow = TaylorGreenVortex2D(resolution=16, reynolds_number=10, mach_number=0.05, lattice=lattice) - collision = NoCollision(lattice) - streaming = StandardStreaming(lattice) - simulation = Simulation(dummy_flow, lattice, collision, streaming) - - if hasattr(simulation, 'f_next'): - simulation.f_next = torch.zeros((lattice.stencil.Q(), 16, 16), dtype=lattice.dtype, device=lattice.device) - - assert hasattr(simulation, 'f') - f = torch.zeros((lattice.stencil.Q(), 16, 16), dtype=lattice.dtype, device=lattice.device) - for q in range(lattice.stencil.Q()): - f[q, 1, 1] = q + 1 - simulation.f = f - - simulation.step_(simulation) - f = simulation.f - - for q in range(lattice.stencil.Q()): - assert f[q, 1 + lattice.stencil.e[q, 0], 1 + lattice.stencil.e[q, 1]] == q + 1 - - -def test_standard_streaming_hardcoded_3D(lattice): - if not (lattice.stencil.D() == 3): - pytest.skip("Custom Test for 3D stencil") - - dummy_flow = TaylorGreenVortex3D(resolution=16, reynolds_number=10, mach_number=0.05, lattice=lattice) - collision = NoCollision(lattice) - streaming = StandardStreaming(lattice) - simulation = Simulation(dummy_flow, lattice, collision, streaming) - - if hasattr(simulation, 'f_next'): - simulation.f_next = torch.zeros((lattice.stencil.Q(), 16, 16, 16), dtype=lattice.dtype, device=lattice.device) - - assert hasattr(simulation, 'f') - f = torch.zeros((lattice.stencil.Q(), 16, 16, 16), dtype=lattice.dtype, device=lattice.device) - for q in range(lattice.stencil.Q()): - f[q, 1, 1, 1] = q + 1 - simulation.f = f - - simulation.step_(simulation) - f = simulation.f - - for q in range(lattice.stencil.Q()): - assert f[q, 1 + lattice.stencil.e[q, 0], 1 + lattice.stencil.e[q, 1], 1 + lattice.stencil.e[q, 2]] == q + 1 - - -def test_standard_streaming_devices(lattice2): - if lattice2[0].stencil.D() != 2 and lattice2[0].stencil.D() != 3: - pytest.skip("Test for 2D and 3D only!") - - def simulate(lattice): - Flow = TaylorGreenVortex2D if lattice2[0].stencil.D() == 2 else TaylorGreenVortex3D - flow = Flow(resolution=16, reynolds_number=10, mach_number=0.05, lattice=lattice) - - collision = NoCollision(lattice) - streaming = StandardStreaming(lattice) - simulation = Simulation(flow=flow, lattice=lattice, collision=collision, streaming=streaming) - simulation.step(4) - - return simulation.f - - lattice0, lattice1 = lattice2 - f0 = simulate(lattice0).to(torch.device("cpu")) - f1 = simulate(lattice1).to(torch.device("cpu")) - - error = torch.abs(f0 - f1).sum().data - assert float(error) == 0.0 diff --git a/old_tests/test_unit.py b/old_tests/test_unit.py deleted file mode 100644 index a4131716..00000000 --- a/old_tests/test_unit.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -Test unit conversion. -""" - -from lettuce import UnitConversion - -import pytest - -import numpy as np - - -def create_default_unit_conversion(lattice): - return UnitConversion( - lattice, - reynolds_number=1000, - mach_number=0.05, - characteristic_length_lu=100, - characteristic_length_pu=2 * np.pi, - characteristic_velocity_pu=2, - characteristic_density_pu=0.7) - - -def test_reynolds_number_consistent(lattice): - units = create_default_unit_conversion(lattice) - re_lu = units.characteristic_velocity_lu * units.characteristic_length_lu / units.viscosity_lu - re_pu = units.characteristic_velocity_pu * units.characteristic_length_pu / units.viscosity_pu - assert re_lu == pytest.approx(re_pu) - - -def test_conversion_reversible(lattice): - approx_two = pytest.approx(2.0) - units = create_default_unit_conversion(lattice) - - assert approx_two == units.convert_velocity_to_lu(units.convert_velocity_to_pu(2.0)) - assert approx_two == units.convert_time_to_lu(units.convert_time_to_pu(2.0)) - assert approx_two == units.convert_length_to_lu(units.convert_length_to_pu(2.0)) - assert approx_two == units.convert_density_to_lu(units.convert_density_to_pu(2.0)) - assert approx_two == units.convert_pressure_to_lu(units.convert_pressure_to_pu(2.0)) - assert approx_two == units.convert_density_lu_to_pressure_pu(units.convert_pressure_pu_to_density_lu(2.0)) - assert approx_two == units.convert_energy_to_lu(units.convert_energy_to_pu(2.0)) - assert approx_two == units.convert_incompressible_energy_to_lu(units.convert_incompressible_energy_to_pu(2.0)) - - -def test_consistency(lattice): - rho = 0.9 - u = 0.1 - units = create_default_unit_conversion(lattice) - - assert ( - units.convert_density_to_pu(rho) * units.convert_velocity_to_pu(u) ** 2 - == pytest.approx(units.convert_energy_to_pu(rho * u * u)) - ) - assert units.convert_velocity_to_pu(u) ** 2 == pytest.approx(units.convert_incompressible_energy_to_pu(u * u)) diff --git a/old_tests/test_util.py b/old_tests/test_util.py deleted file mode 100644 index a82c881b..00000000 --- a/old_tests/test_util.py +++ /dev/null @@ -1,119 +0,0 @@ -import numpy as np -import torch -from lettuce import ( - Lattice, D2Q9, D3Q27, TaylorGreenVortex2D, - TaylorGreenVortex3D, torch_gradient, grid_fine_to_coarse, - BGKCollision, Simulation, StandardStreaming -) -from lettuce.util import pressure_poisson -import pytest - - -@pytest.mark.parametrize("order", [2, 4, 6]) -def test_torch_gradient_2d(order): - lattice = Lattice(D2Q9, device='cpu', ) - flow = TaylorGreenVortex2D(resolution=100, reynolds_number=1, mach_number=0.05, lattice=lattice) - grid = flow.grid - p, u = flow.initial_pu(grid) - dx = flow.units.convert_length_to_pu(1.0) - u0_grad = torch_gradient(lattice.convert_to_tensor(u[0]), dx=dx, order=order).numpy() - u0_grad_np = np.array(np.gradient(u[0], dx)) - u0_grad_analytic = np.array([ - -np.sin(grid[0]) * np.sin(grid[1]), - np.cos(grid[0]) * np.cos(grid[1]), - ]) - assert (u0_grad_analytic == pytest.approx(u0_grad, rel=0.0, abs=1e-3)) - assert (u0_grad_np[:, 2:-2, 2:-2] == pytest.approx(u0_grad[:, 2:-2, 2:-2], rel=0.0, abs=1e-3)) - - -@pytest.mark.parametrize("order", [2, 4, 6]) -def test_torch_gradient_3d(order): - lattice = Lattice(D3Q27, device='cpu', ) - flow = TaylorGreenVortex3D(resolution=100, reynolds_number=1, mach_number=0.05, lattice=lattice) - grid = flow.grid - p, u = flow.initial_pu(grid) - dx = flow.units.convert_length_to_pu(1.0) - u0_grad = torch_gradient(lattice.convert_to_tensor(u[0]), dx=dx, order=order).numpy() - u0_grad_np = np.array(np.gradient(u[0], dx)) - u0_grad_analytic = np.array([ - np.cos(grid[0]) * np.cos(grid[1]) * np.cos(grid[2]), - np.sin(grid[0]) * np.sin(grid[1]) * (-np.cos(grid[2])), - np.sin(grid[0]) * (-np.cos(grid[1])) * np.sin(grid[2]) - ]) - assert np.allclose(u0_grad_analytic, u0_grad, rtol=0.0, atol=1e-3) - assert np.allclose(u0_grad_np[:, 2:-2, 2:-2, 2:-2], u0_grad[:, 2:-2, 2:-2, 2:-2], rtol=0.0, atol=1e-3) - - -def test_grid_fine_to_coarse_2d(): - lattice = Lattice(D2Q9, 'cpu', dtype=torch.double) - # streaming = StandardStreaming(lattice) - - flow_f = TaylorGreenVortex2D(40, 1600, 0.15, lattice) - collision_f = BGKCollision(lattice, tau=flow_f.units.relaxation_parameter_lu) - streaming_f = StandardStreaming(lattice) - sim_f = Simulation(flow_f, lattice, collision_f, streaming_f) - - flow_c = TaylorGreenVortex2D(20, 1600, 0.15, lattice) - collision_c = BGKCollision(lattice, tau=flow_c.units.relaxation_parameter_lu) - streaming_c = StandardStreaming(lattice) - sim_c = Simulation(flow_c, lattice, collision_c, streaming_c) - - f_c = grid_fine_to_coarse(lattice, sim_f.f, flow_f.units.relaxation_parameter_lu, - flow_c.units.relaxation_parameter_lu) - - p_init, u_init = flow_c.initial_pu(flow_c.grid) - rho_init = lattice.convert_to_tensor(flow_c.units.convert_pressure_pu_to_density_lu(p_init)) - u_init = lattice.convert_to_tensor(flow_c.units.convert_velocity_to_lu(u_init)) - shear_c_init = lattice.shear_tensor(sim_c.f) - shear_c = lattice.shear_tensor(f_c) - - assert torch.isclose(lattice.u(f_c), u_init).all() - assert torch.isclose(lattice.rho(f_c), rho_init).all() - assert torch.isclose(f_c, sim_c.f).all() - assert torch.isclose(shear_c_init, shear_c).all() - - -def test_grid_fine_to_coarse_3d(): - lattice = Lattice(D3Q27, 'cpu', dtype=torch.double) - - flow_f = TaylorGreenVortex3D(40, 1600, 0.15, lattice) - collision_f = BGKCollision(lattice, tau=flow_f.units.relaxation_parameter_lu) - sim_f = Simulation(flow_f, lattice, collision_f) - - flow_c = TaylorGreenVortex3D(20, 1600, 0.15, lattice) - collision_c = BGKCollision(lattice, tau=flow_c.units.relaxation_parameter_lu) - sim_c = Simulation(flow_c, lattice, collision_c) - - f_c = grid_fine_to_coarse( - lattice, - sim_f.f, - flow_f.units.relaxation_parameter_lu, - flow_c.units.relaxation_parameter_lu - ) - - p_c_init, u_c_init = flow_c.initial_pu(flow_c.grid) - rho_c_init = flow_c.units.convert_pressure_pu_to_density_lu(p_c_init) - u_c_init = flow_c.units.convert_velocity_to_lu(u_c_init) - shear_c_init = lattice.shear_tensor(sim_c.f) - shear_c = lattice.shear_tensor(f_c) - - assert np.isclose(lattice.u(f_c).cpu().numpy(), u_c_init).all() - assert np.isclose(lattice.rho(f_c).cpu().numpy(), rho_c_init).all() - assert torch.isclose(f_c, sim_c.f).all() - assert torch.isclose(shear_c_init, shear_c).all() - - -@pytest.mark.parametrize("Stencil", [D2Q9, D3Q27]) -def test_pressure_poisson(dtype_device, Stencil): - dtype, device = dtype_device - lattice = Lattice(Stencil(), device, dtype) - flow_class = TaylorGreenVortex2D if Stencil is D2Q9 else TaylorGreenVortex3D - flow = flow_class(resolution=32, reynolds_number=100, mach_number=0.05, lattice=lattice) - p0, u = flow.initial_pu(flow.grid) - u = flow.units.convert_velocity_to_lu(lattice.convert_to_tensor(u)) - rho0 = flow.units.convert_pressure_pu_to_density_lu(lattice.convert_to_tensor(p0)) - rho = pressure_poisson(flow.units, u, torch.ones_like(rho0)) - pfinal = flow.units.convert_density_lu_to_pressure_pu(rho).cpu().numpy() - print(p0.max(), p0.min(), p0.mean(), pfinal.max(), pfinal.min(), pfinal.mean()) - print((p0 - pfinal).max(), (p0 - pfinal).min()) - assert p0 == pytest.approx(pfinal, rel=0.0, abs=0.05) diff --git a/tests/__init__.py b/tests/__init__.py index dce40d92..36b4f543 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1,3 @@ """Top-level package for tests.""" -__all__ = ["common"] +__all__ = ["conftest"] diff --git a/tests/boundary/test_antibounceback_outlet_bc.py b/tests/boundary/test_antibounceback_outlet_bc.py new file mode 100644 index 00000000..5265da9c --- /dev/null +++ b/tests/boundary/test_antibounceback_outlet_bc.py @@ -0,0 +1,103 @@ +from copy import copy + +from tests.conftest import * + + +def test_anti_bounce_back_outlet(fix_configuration, fix_stencil): + """Compares the result of the application of the boundary to f to the + result using the formula taken from page 195 + of "The lattice Boltzmann method" (2016 by Krüger et al.) if both are + similar it is assumed to be working fine.""" + device, dtype, use_native = fix_configuration + if use_native: + pytest.skip("This test does not depend on the native implementation.") + context = Context(device=device, dtype=dtype, use_native=False) + flow = TestFlow(context, resolution=fix_stencil.d * [16], + reynolds_number=1, mach_number=0.1, stencil=fix_stencil) + f = flow.f + + # generates reference value of f using non-dynamic formula + f_ref = copy(f) + u = flow.u() + D = flow.stencil.d + Q = flow.stencil.q + + if D == 3: + direction = [1, 0, 0] + + if Q == 27: + u_w = u[:, -1, :, :] + 0.5 * (u[:, -1, :, :] - u[:, -2, :, :]) + u_w_norm = torch.norm(u_w, dim=0) + + for i in [1, 11, 13, 15, 17, 19, 21, 23, 25]: + stencil_e_tensor = torch.tensor(flow.stencil.e[i], + device=f.device, dtype=f.dtype) + + f_ref[flow.stencil.opposite[i], -1, :, :] = ( + - f_ref[i, -1, :, :] + + (flow.stencil.w[i] * flow.rho()[0, -1, :, :] + * (2 + torch.einsum( + 'c, cyz -> yz', stencil_e_tensor, u_w + ) ** 2 + / flow.stencil.cs ** 4 + - (u_w_norm / flow.stencil.cs) ** 2)) + ) + + if Q == 19: + u_w = u[:, -1, :, :] + 0.5 * (u[:, -1, :, :] - u[:, -2, :, :]) + u_w_norm = torch.norm(u_w, dim=0) + + for i in [1, 11, 13, 15, 17]: + stencil_e_tensor = torch.tensor(flow.stencil.e[i], + device=f.device, dtype=f.dtype) + + f_ref[flow.stencil.opposite[i], -1, :, :] = ( + - f_ref[i, -1, :, :] + + (flow.stencil.w[i] + * flow.rho()[0, -1, :, :] + * (2 + torch.einsum('c, cyz -> yz', + stencil_e_tensor, + u_w) ** 2 + / flow.stencil.cs ** 4 + - (u_w_norm / flow.stencil.cs) ** 2))) + + if D == 2 and Q == 9: + direction = [1, 0] + u_w = u[:, -1, :] + 0.5 * (u[:, -1, :] - u[:, -2, :]) + u_w_norm = torch.norm(u_w, dim=0) + + for i in [1, 5, 8]: + stencil_e_tensor = torch.tensor(flow.stencil.e[i], + device=f.device, dtype=f.dtype) + + f_ref[flow.stencil.opposite[i], -1, :] = - f_ref[i, -1, :] + ( + flow.stencil.w[i] * flow.rho()[0, -1, :] + * (2 + torch.einsum('c, cy -> y', stencil_e_tensor, + u_w) ** 2 + / flow.stencil.cs ** 4 - ( + u_w_norm / flow.stencil.cs) ** 2)) + + if D == 1 and Q == 3: + direction = [1] + u_w = u[:, -1] + 0.5 * (u[:, -1] - u[:, -2]) + u_w_norm = torch.norm(u_w, dim=0) + + for i in [1]: + stencil_e_tensor = torch.tensor(flow.stencil.e[i], + device=f.device, dtype=f.dtype) + + f_ref[flow.stencil.opposite[i], -1] = ( + - f_ref[i, -1] + + (flow.stencil.w[i] + * flow.rho()[0, -1] + * (2 + torch.einsum('c, x -> x', stencil_e_tensor, + u_w) ** 2 + / flow.stencil.cs ** 4 + - (u_w_norm / flow.stencil.cs) ** 2) + ) + ) + + # generates value from actual boundary implementation + abb_outlet = AntiBounceBackOutlet(direction=direction, flow=flow) + f = abb_outlet(flow) + assert f.cpu().numpy() == pytest.approx(f_ref.cpu().numpy()) diff --git a/tests/boundary/test_bc_masks.py b/tests/boundary/test_bc_masks.py new file mode 100644 index 00000000..c39d92ef --- /dev/null +++ b/tests/boundary/test_bc_masks.py @@ -0,0 +1,19 @@ +from tests.conftest import * + + +def test_masks(fix_configuration): + """test if masks are applied from boundary conditions""" + device, dtype, native = fix_configuration + context = Context(device=device, dtype=dtype, use_native=native) + flow = Obstacle(context=context, resolution=[16, 16], reynolds_number=100, + mach_number=0.1, domain_length_x=2) + all_native_boundaries_in_Obstacle = sum([ + _.native_available() for _ in flow.boundaries]) == flow.boundaries + if native and not all_native_boundaries_in_Obstacle: + pytest.skip("Some boundaries in Obstacle are still not available for " + "cuda_native (probably AntiBounceBackOutlet)") + flow.mask[1, 1] = 1 + collision = BGKCollision(1.0) + simulation = Simulation(flow, collision, []) + assert simulation.no_streaming_mask.any() + assert simulation.no_collision_mask.any() diff --git a/tests/boundary/test_bounceback_bc.py b/tests/boundary/test_bounceback_bc.py new file mode 100644 index 00000000..15960ee9 --- /dev/null +++ b/tests/boundary/test_bounceback_bc.py @@ -0,0 +1,36 @@ +from tests.conftest import * + +from copy import copy + + +def test_bounce_back_boundary(fix_stencil, fix_configuration): + device, dtype, use_native = fix_configuration + if use_native: + pytest.skip("This test does not depend on the native implementation.") + context = Context(device=device, dtype=dtype, use_native=False) + flow = TestFlow(context, resolution=fix_stencil.d * [16], + reynolds_number=1, mach_number=0.1, stencil=fix_stencil) + f_old = copy(flow.f) + mask = context.one_tensor(flow.resolution, dtype=bool) # will contain all + # points + bounce_back = BounceBackBoundary(mask) + f_bounced = bounce_back(flow) + assert (f_old[flow.stencil.opposite].cpu().numpy() == + pytest.approx(f_bounced.cpu().numpy())) + + +def test_bounce_back_boundary_not_applied_if_mask_empty(fix_stencil, + fix_configuration): + device, dtype, use_native = fix_configuration + if use_native: + pytest.skip("This test does not depend on the native implementation.") + context = Context(device=device, dtype=dtype, use_native=False) + flow = TestFlow(context, resolution=fix_stencil.d * [16], + reynolds_number=1, mach_number=0.1, stencil=fix_stencil) + f_old = copy(flow.f) + mask = context.zero_tensor(flow.resolution, dtype=bool) # will not contain + # any points + bounce_back = BounceBackBoundary(mask) + bounce_back(flow) + assert (flow.f.cpu().numpy() == + pytest.approx(f_old.cpu().numpy())) diff --git a/tests/boundary/test_equilibrium_outlet_p.py b/tests/boundary/test_equilibrium_bc_outlet_p.py similarity index 67% rename from tests/boundary/test_equilibrium_outlet_p.py rename to tests/boundary/test_equilibrium_bc_outlet_p.py index 71069fbe..650fdf32 100644 --- a/tests/boundary/test_equilibrium_outlet_p.py +++ b/tests/boundary/test_equilibrium_bc_outlet_p.py @@ -1,4 +1,4 @@ -from tests.common import * +from tests.conftest import * # TODO: Implement cuda_native generator and test suite @@ -10,20 +10,16 @@ def test_equilibrium_outlet_p_algorithm(fix_stencil, fix_configuration): equilibrium outlet pressure by comparing its output to manually calculated equilibrium values. """ - - device, dtype, native = fix_configuration - if native: - pytest.skip("TODO: native_available for equilibrium_outlet_p at the " - "moment False") - context = Context(device=torch.device(device), dtype=dtype, - use_native=native) - stencil = fix_stencil # TorchStencil(stencil=fix_stencil, context=context) + device, dtype, use_native = fix_configuration + if use_native: + pytest.skip("This test does not depend on the native implementation.") + context = Context(device=device, dtype=dtype, use_native=False) + stencil = fix_stencil flow = TestFlow(context, stencil=stencil, resolution=16, reynolds_number=1, mach_number=0.1) direction = [0] * (stencil.d - 1) + [1] - boundary_cpu = EquilibriumOutletP(flow=flow, context=context, - direction=direction, + boundary_cpu = EquilibriumOutletP(flow=flow, direction=direction, rho_outlet=1.2) f_post_boundary = boundary_cpu(flow)[..., -1] u_slice = [stencil.d, *flow.resolution[:stencil.d - 1], 1] diff --git a/tests/boundary/test_equilibrium_pu.py b/tests/boundary/test_equilibrium_bc_pu.py similarity index 72% rename from tests/boundary/test_equilibrium_pu.py rename to tests/boundary/test_equilibrium_bc_pu.py index cc3321b3..df25aa25 100644 --- a/tests/boundary/test_equilibrium_pu.py +++ b/tests/boundary/test_equilibrium_bc_pu.py @@ -1,4 +1,4 @@ -from tests.common import * +from tests.conftest import * def moment_dims_params(): @@ -59,8 +59,8 @@ def boundaries(self) -> List['Boundary']: flow_1 = DummyEQBC(context, resolution=fix_stencil.d * [16], reynolds_number=1, mach_number=0.1, stencil=fix_stencil) - flow_2 = TestFlow(context, resolution=fix_stencil.d * [16], - reynolds_number=1, mach_number=0.1, stencil=fix_stencil) + flow_2 = TestFlow(context, resolution=16, reynolds_number=1, + mach_number=0.1, stencil=fix_stencil) simulation = Simulation(flow=flow_1, collision=NoCollision(), reporter=[]) simulation(num_steps=1) @@ -81,6 +81,47 @@ def boundaries(self) -> List['Boundary']: context.convert_to_ndarray(flow_2.f)) +def test_equilibrium_boundary_pu_tgv(fix_stencil, fix_configuration): + if fix_stencil.d not in [2, 3]: + pytest.skip("TGV Test can only be done in 2D or 3D.") + device, dtype, native = fix_configuration + context = Context(device=device, dtype=dtype, use_native=native) + + class DummyEQBC(TaylorGreenVortex): + @property + def boundaries(self): + # u = self.context.one_tensor(self.stencil.d) * 0.1 + u = self.context.one_tensor(self.stencil.d) * 0.1 + p = 0 # self.context.zero_tensor([1, 1, 1]) + m = self.context.zero_tensor(self.resolution, dtype=bool) + m[..., :1] = True + return [TestEquilibriumBoundary( + self.context, m, u, p)] + + flow_1 = DummyEQBC(context, resolution=16, reynolds_number=1, + mach_number=0.1, stencil=fix_stencil) + flow_2 = DummyTGV(context, resolution=16, reynolds_number=1, + mach_number=0.1, stencil=fix_stencil) + + simulation = Simulation(flow=flow_1, collision=NoCollision(), reporter=[]) + simulation(num_steps=1) + + pressure = 0 + velocity = 0.1 * np.ones(flow_2.stencil.d) + + feq = flow_2.equilibrium( + flow_2, + context.convert_to_tensor( + flow_2.units.convert_pressure_pu_to_density_lu(pressure)), + context.convert_to_tensor( + flow_2.units.convert_velocity_to_lu(velocity)) + ) + flow_2.f[..., :1] = torch.einsum("q,q...->q...", feq, + torch.ones_like(flow_2.f))[..., :1] + + assert flow_1.f.cpu().numpy() == pytest.approx(flow_2.f.cpu().numpy()) + + def test_equilibrium_boundary_pu_native(fix_stencil_x_moment_dims, fix_dtype): if not torch.cuda.is_available(): pytest.skip(reason="CUDA is not available on this machine.") diff --git a/tests/boundary/test_equilibrium_pressure_outlet.py b/tests/boundary/test_equilibrium_pressure_outlet.py new file mode 100644 index 00000000..f8747525 --- /dev/null +++ b/tests/boundary/test_equilibrium_pressure_outlet.py @@ -0,0 +1,69 @@ +from tests.conftest import * + + +def test_equilibrium_pressure_outlet(fix_configuration, fix_stencil): + pytest.skip("TODO rtol is too big or simulation time too short!") + if fix_stencil.d not in [2, 3]: + pytest.skip("Obstacle to test EQ P outlet only implemented for 2D " + "and 3D") + outlet_directions = [[0, -1], + [0, 1], + [1, 0]] + if fix_stencil.d == 3: + outlet_directions = [direction + [0] + for direction in outlet_directions] + device, dtype, native = fix_configuration + context = Context(device=device, dtype=dtype, use_native=native) + + class MyObstacle(Obstacle): + @property + def boundaries(self, *args): + x, y, *z = self.grid + z = z if z else None + outlets = [EquilibriumOutletP(_, self) for _ in outlet_directions] + inflow = [self.units.characteristic_velocity_pu, 0] \ + if self.stencil.d == 2 \ + else [self.units.characteristic_velocity_pu, 0, 0] + return [ + EquilibriumBoundaryPU( + self.context, + np.abs(x) < 1e-6, + inflow + ), + *outlets, + BounceBackBoundary(self.mask) + ] + + flow = MyObstacle(context=context, + resolution=[32] * fix_stencil.d, + reynolds_number=10, + mach_number=0.1, + domain_length_x=3, + stencil=fix_stencil) + all_native_boundaries_in_MyObstacle = sum([ + _.native_available() for _ in flow.boundaries]) == flow.boundaries + if native and not all_native_boundaries_in_MyObstacle: + pytest.skip("Some boundaries in Obstacle are still not available for " + "cuda_native (probably EquilibriumOutletP)") + + # mask = np.zeros_like(flow.grid[0], dtype=bool) + # mask[10:20, 10:20] = 1 + flow.mask[10:20, 10:20] = True + simulation = Simulation(flow, + RegularizedCollision( + flow.units.relaxation_parameter_lu), + [VTKReporter(1, + filename_base=f"./data/output_" + f"D{fix_stencil.d}Q" + f"{fix_stencil.q}")]) + simulation(30) + rho = flow.rho() + u = flow.u() + feq = flow.equilibrium(flow, torch.ones_like(rho), u) + p = flow.units.convert_density_lu_to_pressure_pu(rho) + zeros = torch.zeros_like(p[0, -1, :]) + # TODO rtol is too big or simulation time too short! + assert torch.allclose(zeros, p[:, -1, :], rtol=0, atol=1e-4) + assert torch.allclose(zeros, p[:, :, 0], rtol=0, atol=1e-4) + assert torch.allclose(zeros, p[:, :, -1], rtol=0, atol=1e-4) + assert torch.allclose(feq[:, -1, 1:-1], feq[:, -2, 1:-1]) diff --git a/tests/collision/test_collision_conserves_mass.py b/tests/collision/test_collision_conserves_mass.py new file mode 100644 index 00000000..25cc4d08 --- /dev/null +++ b/tests/collision/test_collision_conserves_mass.py @@ -0,0 +1,20 @@ +from tests.conftest import * + + +def test_collision_conserves_mass(fix_conserving_collision, + fix_configuration, + fix_stencil): + if (fix_conserving_collision == KBCCollision and type(fix_stencil) not + in [D2Q9, D3Q27]): + pytest.skip("KBCCollision only implemented for D2Q9 and D3Q27") + device, dtype, use_native = fix_configuration + context = Context(device=device, dtype=dtype, use_native=use_native) + flow = TestFlow(context=context, + resolution=[16] * fix_stencil.d, + reynolds_number=100, + mach_number=0.1, + stencil=fix_stencil) + collision = fix_conserving_collision(tau=0.51) + f_collided = collision(flow) + assert (flow.rho().cpu().numpy() + == pytest.approx(flow.rho(f_collided).cpu().numpy())) diff --git a/tests/collision/test_collision_conserves_momentum.py b/tests/collision/test_collision_conserves_momentum.py new file mode 100644 index 00000000..7b2aa440 --- /dev/null +++ b/tests/collision/test_collision_conserves_momentum.py @@ -0,0 +1,22 @@ +from tests.conftest import * + + +def test_collision_conserves_momentum(fix_conserving_collision, + fix_configuration, + fix_stencil): + if (fix_conserving_collision == KBCCollision and type(fix_stencil) not + in [D2Q9, D3Q27]): + pytest.skip("KBCCollision only implemented for D2Q9 and D3Q27") + device, dtype, use_native = fix_configuration + if use_native: + pytest.skip("This test does not depend on the native implementation.") + context = Context(device=device, dtype=dtype, use_native=False) + flow = TestFlow(context=context, + resolution=[16] * fix_stencil.d, + reynolds_number=100, + mach_number=0.1, + stencil=fix_stencil) + collision = fix_conserving_collision(tau=0.51) + f_collided = collision(flow) + assert (flow.j().cpu().numpy() + == pytest.approx(flow.j(f_collided).cpu().numpy(), abs=1e-5)) diff --git a/tests/collision/test_collision_fixpoint_2x.py b/tests/collision/test_collision_fixpoint_2x.py new file mode 100644 index 00000000..a2b994e1 --- /dev/null +++ b/tests/collision/test_collision_fixpoint_2x.py @@ -0,0 +1,21 @@ +from tests.conftest import * + + +@pytest.mark.parametrize("Collision", [BGKCollision]) +def test_collision_fixpoint_2x(Collision, + fix_configuration, + fix_stencil): + device, dtype, use_native = fix_configuration + if use_native: + pytest.skip("This test does not depend on the native implementation.") + context = Context(device=device, dtype=dtype, use_native=False) + flow = TestFlow(context=context, + resolution=[16] * fix_stencil.d, + reynolds_number=100, + mach_number=0.1, + stencil=fix_stencil) + collision = Collision(0.5) + f_old = copy(flow.f) + flow.f = collision(flow) + flow.f = collision(flow) + assert f_old.cpu().numpy() == pytest.approx(flow.f.cpu().numpy(), abs=1e-5) diff --git a/tests/collision/test_collision_fixpoint_2x_MRT.py b/tests/collision/test_collision_fixpoint_2x_MRT.py new file mode 100644 index 00000000..182ef20b --- /dev/null +++ b/tests/collision/test_collision_fixpoint_2x_MRT.py @@ -0,0 +1,24 @@ +from lettuce.util.moments import D2Q9Lallemand, D2Q9Dellar +from tests.conftest import * + + +@pytest.mark.parametrize("Transform", [D2Q9Lallemand, D2Q9Dellar]) +def test_collision_fixpoint_2x_MRT(Transform, fix_configuration): + # TODO: Migrate lattice methods to be independend of Flow or pass Flow + # to Transform + pytest.skip("Transform() currently still uses Lattice methods.") + device, dtype, use_native = fix_configuration + if use_native: + pytest.skip("This test does not depend on the native implementation.") + context = Context(device=device, dtype=dtype, use_native=False) + np.random.seed(1) # arbitrary, but deterministic + stencil = D2Q9() + f = context.convert_to_tensor(np.random.random([stencil.q] + [3] * + stencil.d)) + f_old = copy(f) + flow = DummyFlow(context, 1) + collision = MRTCollision(Transform(stencil), + [0.5] * 9, + context=context) + f = collision(collision(flow)) + assert f.cpu().numpy() == pytest.approx(f_old.cpu().numpy(), abs=1e-5) diff --git a/tests/collision/test_collision_optimizes_pseudo_entropy.py b/tests/collision/test_collision_optimizes_pseudo_entropy.py new file mode 100644 index 00000000..1cd91491 --- /dev/null +++ b/tests/collision/test_collision_optimizes_pseudo_entropy.py @@ -0,0 +1,29 @@ +from tests.conftest import * + + +def test_collision_optimizes_pseudo_entropy(fix_configuration, + fix_stencil): + """checks if the pseudo-entropy of the KBC collision model is at least + higher than the BGK pseudo-entropy""" + if type(fix_stencil) not in [D2Q9, D3Q27]: + pytest.skip("KBCCollision only implemented for D2Q9 and D3Q27.") + device, dtype, use_native = fix_configuration + if use_native: + pytest.skip("This test does not depend on the native implementation.") + context = Context(device=device, dtype=dtype, use_native=False) + flow = TestFlow(context=context, + resolution=[16] * fix_stencil.d, + reynolds_number=100, + mach_number=0.1, + stencil=fix_stencil) + np.random.seed(1) # arbitrary, but deterministic + flow.f = flow.context.convert_to_tensor(np.random.random( + [flow.stencil.q] + [3] * flow.stencil.d)) + tau = 0.5003 + coll_kbc = KBCCollision(tau) + coll_bgk = BGKCollision(tau) + f_kbc = coll_kbc(flow) + f_bgk = coll_bgk(flow) + entropy_kbc = flow.pseudo_entropy_local(f_kbc) + entropy_bgk = flow.pseudo_entropy_local(f_bgk) + assert (entropy_bgk < entropy_kbc).all() diff --git a/tests/collision/test_collision_relaxes_shear_moments.py b/tests/collision/test_collision_relaxes_shear_moments.py new file mode 100644 index 00000000..a7f43227 --- /dev/null +++ b/tests/collision/test_collision_relaxes_shear_moments.py @@ -0,0 +1,33 @@ +from tests.conftest import * + + +@pytest.mark.parametrize("Collision", + [BGKCollision, TRTCollision, KBCCollision, + RegularizedCollision]) +def test_collision_relaxes_shear_moments(Collision, + fix_configuration, + fix_stencil): + """checks whether the collision models relax the shear moments according + to the prescribed relaxation time""" + if Collision == KBCCollision and type(fix_stencil) not in [D2Q9, D3Q27]: + pytest.skip("KBCCollision only implemented for D2Q9 and D3Q27") + device, dtype, use_native = fix_configuration + if use_native: + pytest.skip("This test does not depend on the native implementation.") + context = Context(device=device, dtype=dtype, use_native=False) + flow = TestFlow(context=context, + resolution=[16] * fix_stencil.d, + reynolds_number=100, + mach_number=0.1, + stencil=fix_stencil) + feq = flow.equilibrium(flow) + shear_pre = flow.shear_tensor() + shear_eq_pre = flow.shear_tensor(feq) + tau = 0.6 + coll = Collision(tau) + f_post = coll(flow) + shear_post = flow.shear_tensor(f_post) + assert (shear_post.cpu().numpy() == + pytest.approx((shear_pre + - 1 / tau * (shear_pre - shear_eq_pre) + ).cpu().numpy(), abs=1e-5)) diff --git a/tests/collision/test_force.py b/tests/collision/test_force.py new file mode 100644 index 00000000..d315f19e --- /dev/null +++ b/tests/collision/test_force.py @@ -0,0 +1,53 @@ +from tests.conftest import * +import matplotlib.pyplot as plt + + +@pytest.mark.parametrize("ForceType", [Guo, ShanChen]) +def test_force(ForceType, fix_device): + if fix_device.type == 'cuda' and not torch.cuda.is_available(): + pytest.skip('CUDA not available') + context = Context(device=fix_device, dtype=torch.float64, use_native=False) + # TODO cuda_native Fails here, probably because this test requires uneven + # grid points + flow = PoiseuilleFlow2D(context=context, + resolution=17, + reynolds_number=1, + mach_number=0.02, + initialize_with_zeros=True) + acceleration_lu = flow.units.convert_acceleration_to_lu(flow.acceleration) + force = ForceType(flow=flow, + tau=flow.units.relaxation_parameter_lu, + acceleration=acceleration_lu) + collision = BGKCollision(tau=flow.units.relaxation_parameter_lu, + force=force) + report_interval = 100 + errorreporter = ErrorReporter(flow.analytic_solution, + interval=report_interval, + out=None) + simulation = Simulation(flow=flow, + collision=collision, + reporter=[errorreporter]) + simulation(1000) + + # compare with reference solution + u_sim = flow.u(acceleration=acceleration_lu) + u_sim = flow.units.convert_velocity_to_pu(u_sim) + _, u_ref = flow.analytic_solution() + + fluidnodes = torch.eq(simulation.no_collision_mask, 0) + # np.where(np.logical_not(flow.boundaries[0].mask.cpu())) + + print() + print(("{:>15} " * 3).format("iterations", "error (u)", "error (p)")) + for i in range(len(errorreporter.out)): + error_u, error_p = np.abs(errorreporter.out[i]).tolist() + print(f"{i * report_interval:15} {error_u:15.2e} {error_p:15.2e}") + + for dim in range(2): + u_sim_i = context.convert_to_ndarray(u_sim[dim][fluidnodes]) + u_ref_i = context.convert_to_ndarray(u_ref[dim][fluidnodes]) + assert u_sim_i.max() == pytest.approx(u_ref_i.max(), + rel=0.01) + assert u_sim_i == pytest.approx(u_ref_i, + rel=None, + abs=0.01 * u_ref[0].max()) diff --git a/tests/common.py b/tests/common.py deleted file mode 100644 index aedb4d93..00000000 --- a/tests/common.py +++ /dev/null @@ -1,197 +0,0 @@ -import pytest -import numpy as np -import torch - -from lettuce import * - - -def dtype_params(): - return [torch.float64, torch.float32] - - -def dtype_ids(): - return ['Float64', 'Float32'] - - -def stencil1d_params(): - return [D1Q3()] - - -def stencil2d_params(): - return [D2Q9()] - - -def stencil3d_params(): - return [D3Q15(), D3Q19(), D3Q27()] - - -def stencil_params(): - return stencil1d_params() + stencil2d_params() + stencil3d_params() - - -def stencil1d_ids(): - return [p.__class__.__name__ for p in stencil1d_params()] - - -def stencil2d_ids(): - return [p.__class__.__name__ for p in stencil2d_params()] - - -def stencil3d_ids(): - return [p.__class__.__name__ for p in stencil3d_params()] - - -def stencil_ids(): - return stencil1d_ids() + stencil2d_ids() + stencil3d_ids() - - -def device_params(): - return [torch.device('cpu'), torch.device('cuda')] - - -def device_ids(): - return ['CPU', 'CUDA'] - - -def native_params(): - return [True, False] - - -def native_ids(): - return ['Native', 'NonNative'] - - -def configuration_params(): - for device in device_params(): - for dtype in dtype_params(): - for native in native_params(): - if not (device == torch.device('cpu') and native): - yield device, dtype, native - - -def configuration_ids(): - buffer = [] - for device in device_ids(): - for dtype in dtype_ids(): - for native in native_ids(): - if not (device == 'CPU' and native == 'Native'): - if native == 'Native': - buffer.append(f"{device}-{dtype}-{native}") - else: - buffer.append(f"{device}-{dtype}") - return buffer - - -@pytest.fixture(params=dtype_params(), ids=dtype_ids()) -def fix_dtype(request): - return request.param - - -@pytest.fixture(params=stencil1d_params(), ids=stencil1d_ids()) -def fix_stencil1d(request): - return request.param - - -@pytest.fixture(params=stencil2d_params(), ids=stencil2d_ids()) -def fix_stencil2d(request): - return request.param - - -@pytest.fixture(params=stencil3d_params(), ids=stencil3d_ids()) -def fix_stencil3d(request): - return request.param - - -@pytest.fixture(params=stencil_params(), ids=stencil_ids()) -def fix_stencil(request): - return request.param - - -@pytest.fixture(params=device_params(), ids=device_ids()) -def fix_device(request): - return request.param - - -@pytest.fixture(params=native_params(), ids=native_ids()) -def fix_native(request): - return request.param - - -@pytest.fixture(params=configuration_params(), ids=configuration_ids()) -def fix_configuration(request): - if 'cuda' in request.param[0].type and not torch.cuda.is_available(): - pytest.skip(reason="CUDA is not available on this machine.") - return request.param - - -class TestFlow(ExtFlow): - def __init__(self, context: 'Context', resolution: Union[int, List[int]], - reynolds_number, mach_number, - stencil: Optional['Stencil'] = None, - equilibrium: Optional['Equilibrium'] = None): - self._boundaries = [] - super().__init__(context, resolution, reynolds_number, mach_number, - stencil, equilibrium) - - def make_resolution(self, resolution: List[int], - stencil: Optional['Stencil'] = None) -> List[int]: - if isinstance(resolution, int): - if stencil is None: - return [resolution] - else: - return [resolution] * stencil.d - else: - return resolution - - def make_units(self, reynolds_number, mach_number, - resolution: List[int]) -> 'UnitConversion': - return UnitConversion(reynolds_number, mach_number, - characteristic_length_lu=resolution[0]) - - def initial_pu(self) -> (float, Union[np.array, torch.Tensor]): - print(self.resolution) - u = 1.01 * np.ones([self.stencil.d] + self.resolution) - p = 0.01 * np.ones([1] + self.resolution) - return p, u - - @property - def boundaries(self) -> List['Boundary']: - return self._boundaries - - @boundaries.setter - def boundaries(self, boundaries: List['Boundary']): - self._boundaries = boundaries - - -def DummyTGV(context: 'Context', resolution: Union[int, List[int]], - reynolds_number, mach_number, - stencil: Optional['Stencil'] = None, - equilibrium: Optional['Equilibrium'] = None): - return TaylorGreenVortex(context, resolution, reynolds_number, mach_number, - stencil, equilibrium) - - -class DummyFlow(ExtFlow): - - def __init__(self, context: Context, resolution: int = 16): - ExtFlow.__init__(self, context, resolution, 1.0, 1.0) - - def make_resolution(self, resolution: Union[int, List[int]], - stencil: Optional['Stencil'] = None) -> List[int]: - return [resolution, resolution] if isinstance(resolution, int)\ - else resolution - - def make_units(self, reynolds_number, mach_number, _: List[int] - ) -> 'UnitConversion': - return UnitConversion(reynolds_number=reynolds_number, - mach_number=mach_number) - - def initial_pu(self) -> (float, List[float]): - ... - - def initialize(self): - ... - - @property - def boundaries(self) -> List['Boundary']: - return [] diff --git a/tests/conftest.py b/tests/conftest.py index e8e31ac7..dd232963 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,33 +1,266 @@ -""" -Fixtures for unit tests. -""" import pytest - +import numpy as np import torch +from copy import copy from lettuce import * -from lettuce.ext import _stencil +from lettuce.util.moments import * + + +def dtype_params(): + return [torch.float64, torch.float32] + + +def dtype_ids(): + return ['Float64', 'Float32'] + + +def stencil1d_params(): + return [D1Q3()] + + +def stencil2d_params(): + return [D2Q9()] + + +def stencil3d_params(): + return [D3Q15(), D3Q19(), D3Q27()] + + +def stencil_params(): + return stencil1d_params() + stencil2d_params() + stencil3d_params() + + +def stencil1d_ids(): + return [p.__class__.__name__ for p in stencil1d_params()] + + +def stencil2d_ids(): + return [p.__class__.__name__ for p in stencil2d_params()] + + +def stencil3d_ids(): + return [p.__class__.__name__ for p in stencil3d_params()] + + +def stencil_ids(): + return stencil1d_ids() + stencil2d_ids() + stencil3d_ids() + -STENCILS = list(get_subclasses(Stencil, _stencil)) +def device_params(): + return [torch.device('cpu'), torch.device('cuda')] -@pytest.fixture( - params=((torch.float64, "cpu", "no_native"), - (torch.float32, "cpu", "no_native"), - (torch.float64, "cuda", "no_native"), - (torch.float32, "cuda", "no_native"), - (torch.float64, "cuda", "cuda_native"), - (torch.float32, "cuda", "cuda_native")), - ids=("cpu64", "cpu32", "cu64", "cu32", "native64", "native32")) -def configurations(request): - dtype, device, native = request.param - if device == "cuda:0" and not torch.cuda.is_available(): - pytest.skip(reason="CUDA not available.") - return dtype, device, native +def device_ids(): + return ['CPU', 'CUDA'] -@pytest.fixture(params=STENCILS) -def stencils(request): - """Run a test for all stencil.""" +def native_params(): + return [True, False] + + +def native_ids(): + return ['Native', 'NonNative'] + + +def configuration_params(): + for device in device_params(): + for dtype in dtype_params(): + for native in native_params(): + if not (device == torch.device('cpu') and native): + yield device, dtype, native + + +def configuration_ids(): + buffer = [] + for device in device_ids(): + for dtype in dtype_ids(): + for native in native_ids(): + if not (device == 'CPU' and native == 'Native'): + if native == 'Native': + buffer.append(f"{device}-{dtype}-{native}") + else: + buffer.append(f"{device}-{dtype}") + return buffer + + +def transform_params(): + Transforms = [ + D1Q3Transform, + D2Q9Dellar, + D2Q9Lallemand, + D3Q27Hermite + ] + Stencils = [ + D1Q3, + D2Q9, + D2Q9, + D3Q27 + ] + return zip(Transforms, Stencils) + + +def transform_ids(): + return ["D1Q3", "D2Q9Dellar", "D2Q9Lallemand", "D3Q27"] + + +@pytest.fixture(params=transform_params(), ids=transform_ids()) +def fix_transform(request): + return request.param + + +COLLISIONS = list(get_subclasses(Collision, lettuce.ext._collision)) + + +@pytest.fixture(params=COLLISIONS) +def fix_collision(request): return request.param + +def conserving_collision_params(): + return [ + BGKCollision, + KBCCollision, + TRTCollision, + RegularizedCollision, + SmagorinskyCollision + ] + + +def conserving_collision_ids(): + return [ + 'BGKCollision', + 'KBCCollision', + 'TRTCollision', + 'RegularizedCollision', + 'SmagorinskyCollision' + ] + + +@pytest.fixture(params=dtype_params(), ids=dtype_ids()) +def fix_dtype(request): + return request.param + + +@pytest.fixture(params=stencil1d_params(), ids=stencil1d_ids()) +def fix_stencil1d(request): + return request.param + + +@pytest.fixture(params=stencil2d_params(), ids=stencil2d_ids()) +def fix_stencil2d(request): + return request.param + + +@pytest.fixture(params=stencil3d_params(), ids=stencil3d_ids()) +def fix_stencil3d(request): + return request.param + + +@pytest.fixture(params=stencil_params(), ids=stencil_ids()) +def fix_stencil(request): + return request.param + + +@pytest.fixture(params=device_params(), ids=device_ids()) +def fix_device(request): + if 'cuda' in request.param.type and not torch.cuda.is_available(): + pytest.skip(reason="CUDA is not available on this machine.", + allow_module_level=True) + return request.param + + +@pytest.fixture(params=native_params(), ids=native_ids()) +def fix_native(request): + if request.param[0] and not torch.cuda.is_available(): + pytest.skip(reason="CUDA is not available on this machine.", + allow_module_level=True) + return request.param + + +@pytest.fixture(params=configuration_params(), ids=configuration_ids()) +def fix_configuration(request): + if 'cuda' in request.param[0].type and not torch.cuda.is_available(): + pytest.skip(reason="CUDA is not available on this machine.", + allow_module_level=True) + return request.param + + +@pytest.fixture(params=conserving_collision_params(), + ids=conserving_collision_ids()) +def fix_conserving_collision(request): + return request.param + + +class TestFlow(ExtFlow): + __test__ = False + + def __init__(self, context: 'Context', resolution: Union[int, List[int]], + reynolds_number, mach_number, + stencil: Optional['Stencil'] = None, + equilibrium: Optional['Equilibrium'] = None): + self._boundaries = [] + super().__init__(context, resolution, reynolds_number, mach_number, + stencil, equilibrium) + + def make_resolution(self, resolution: List[int], + stencil: Optional['Stencil'] = None) -> List[int]: + if isinstance(resolution, int): + if stencil is None: + return [resolution] + else: + return [resolution] * stencil.d + else: + return resolution + + def make_units(self, reynolds_number, mach_number, + resolution: List[int]) -> 'UnitConversion': + return UnitConversion(reynolds_number, mach_number, + characteristic_length_lu=resolution[0]) + + def initial_pu(self) -> (float, Union[np.array, torch.Tensor]): + u = 1.01 * np.ones([self.stencil.d] + self.resolution) + p = 0.01 * np.ones([1] + self.resolution) + return p, u + + @property + def boundaries(self) -> List['Boundary']: + return self._boundaries + + @boundaries.setter + def boundaries(self, boundaries: List['Boundary']): + self._boundaries = boundaries + + +def DummyTGV(context: 'Context', resolution: Union[int, List[int]], + reynolds_number, mach_number, + stencil: Optional['Stencil'] = None, + equilibrium: Optional['Equilibrium'] = None): + return TaylorGreenVortex(context, resolution, reynolds_number, mach_number, + stencil, equilibrium) + + +class DummyFlow(ExtFlow): + + def __init__(self, context: Context, resolution: int = 16): + ExtFlow.__init__(self, context, resolution, 1.0, 1.0) + + def make_resolution(self, resolution: Union[int, List[int]], + stencil: Optional['Stencil'] = None) -> List[int]: + return [resolution, resolution] if isinstance(resolution, int)\ + else resolution + + def make_units(self, reynolds_number, mach_number, _: List[int] + ) -> 'UnitConversion': + return UnitConversion(reynolds_number=reynolds_number, + mach_number=mach_number) + + def initial_pu(self) -> (float, List[float]): + ... + + def initialize(self): + ... + + @property + def boundaries(self) -> List['Boundary']: + return [] diff --git a/tests/flow/test_divergence.py b/tests/flow/test_divergence.py new file mode 100644 index 00000000..a873fa76 --- /dev/null +++ b/tests/flow/test_divergence.py @@ -0,0 +1,34 @@ +from tests.conftest import * + + +@pytest.mark.parametrize("stencil2d3d", [D2Q9(), D3Q27()]) +def test_divergence(stencil2d3d, fix_configuration): + device, dtype, use_native = fix_configuration + context = Context(device=device, dtype=dtype, use_native=use_native) + flow = DecayingTurbulence(context=context, + resolution=[50] * stencil2d3d.d, + reynolds_number=1, + mach_number=0.05, + ic_energy=0.5) + collision = BGKCollision(tau=flow.units.relaxation_parameter_lu) + simulation = Simulation(flow=flow, + collision=collision, + reporter=[]) + ekin = (flow.units.convert_incompressible_energy_to_pu( + torch.sum(flow.incompressible_energy())) * + flow.units.convert_length_to_pu(1.0) ** stencil2d3d.d) + + u0 = flow.u_pu[0] + u1 = flow.u_pu[1] + dx = flow.units.convert_length_to_pu(1.0) + grad_u0 = torch_gradient(u0, dx=dx, order=6).cpu().numpy() + grad_u1 = torch_gradient(u1, dx=dx, order=6).cpu().numpy() + divergence = np.sum(grad_u0[0] + grad_u1[1]) + + if stencil2d3d.d == 3: + u2 = flow.u_pu[2] + grad_u2 = torch_gradient(u2, dx=dx, order=6).cpu().numpy() + divergence += np.sum(grad_u2[2]) + assert (flow.ic_energy == + pytest.approx(context.convert_to_ndarray(ekin), rel=1)) + assert (0 == pytest.approx(divergence, abs=2e-3)) diff --git a/tests/flow/test_flow.py b/tests/flow/test_flow.py new file mode 100644 index 00000000..4901ad4b --- /dev/null +++ b/tests/flow/test_flow.py @@ -0,0 +1,17 @@ +from tests.conftest import * + + +@pytest.mark.parametrize("flowname", flow_by_name.keys()) +def test_flow(flowname, fix_configuration): + device, dtype, use_native = fix_configuration + if use_native: + pytest.skip("This test does not depend on the native implementation.") + context = Context(device=device, dtype=dtype, use_native=False) + IncompressibleFlow, stencil = flow_by_name[flowname] + stencil = stencil() if callable(stencil) else stencil + flow = IncompressibleFlow(context=context, resolution=[16] * stencil.d, + reynolds_number=1, mach_number=0.05, + stencil=stencil) + collision = BGKCollision(tau=flow.units.relaxation_parameter_lu) + simulation = Simulation(flow=flow, collision=collision, reporter=[]) + simulation(1) diff --git a/tests/flow/test_initialize_fneq.py b/tests/flow/test_initialize_fneq.py new file mode 100644 index 00000000..50dd6b46 --- /dev/null +++ b/tests/flow/test_initialize_fneq.py @@ -0,0 +1,93 @@ +"""Testing that initializing fneq improves TGV solution""" +import pytest + +from tests.conftest import * + + +@pytest.mark.parametrize("Case", [DecayingTurbulence, + TaylorGreenVortex, + DoublyPeriodicShear2D]) +def test_initialize_fneq(fix_configuration, fix_stencil, Case): + # fixture setup + if fix_stencil.d == 1: + pytest.skip("Testflows not working for 1D") + if Case is DoublyPeriodicShear2D and fix_stencil.d != 2: + pytest.skip("DoublyPeriodicShear2D only working for 2D") + device, dtype, use_native = fix_configuration + # if dtype == torch.float32: + # pytest.skip("TGV is not accurate enough for single precision.") + context = Context(device, dtype, use_native) + + # setting up flows with and without fneq + if Case is DecayingTurbulence: + randseed = np.random.randint(1) + flow_neq = Case(context=context, resolution=32, reynolds_number=1000, + mach_number=0.1, stencil=fix_stencil, + initialize_pressure=False, + randseed=randseed) + flow_eq = Case(context=context, resolution=32, reynolds_number=1000, + mach_number=0.1, stencil=fix_stencil, + initialize_pressure=False, + initialize_fneq=False, + randseed=randseed) + else: + flow_neq = Case(context=context, resolution=32, reynolds_number=1000, + mach_number=0.1, stencil=fix_stencil) + flow_eq = Case(context=context, resolution=32, reynolds_number=1000, + mach_number=0.1, stencil=fix_stencil, + initialize_fneq=False) + + # initializing with and without fneq + flow_eq.initialize() + flow_neq.initialize() + + # comparing densitiy, velocity, and kinetic energy with and without fneq + # (should be equal) + rho_eq = flow_eq.rho() + u_eq = flow_eq.u() + ke_eq = flow_eq.incompressible_energy() + + rho_neq = flow_neq.rho() + u_neq = flow_neq.u() + ke_neq = flow_neq.incompressible_energy() + + print(u_eq) + print(u_neq) + + tol = 1e-6 + assert (context.convert_to_ndarray(rho_neq) + == pytest.approx(context.convert_to_ndarray(rho_eq), + rel=0.0, + abs=tol)) + assert (context.convert_to_ndarray(u_neq) + == pytest.approx(context.convert_to_ndarray(u_eq), + rel=0.0, + abs=tol)) + assert (context.convert_to_ndarray(ke_neq) + == pytest.approx(context.convert_to_ndarray(ke_eq), + rel=0.0, + abs=tol)) + + # comparing to analytic solution of TGV + if Case is TaylorGreenVortex and fix_stencil.d == 2: + collision = BGKCollision(tau=flow_neq.units.relaxation_parameter_lu) + + error_reporter_neq = ErrorReporter(flow_neq.analytic_solution, + interval=1, + out=None) + simulation_neq = Simulation(flow_neq, collision, [error_reporter_neq]) + + error_reporter_eq = ErrorReporter(flow_eq.analytic_solution, + interval=1, + out=None) + simulation_eq = Simulation(flow_eq, collision, [error_reporter_eq]) + + simulation_neq(10) + simulation_eq(10) + + error_u, error_p = np.mean(np.abs(error_reporter_neq.out), + axis=0).tolist() + error_u_eq, error_p_eq = np.mean(np.abs(error_reporter_eq.out), + axis=0).tolist() + + assert error_u < error_u_eq diff --git a/tests/flow/test_initialize_pressure.py b/tests/flow/test_initialize_pressure.py new file mode 100644 index 00000000..b5780a95 --- /dev/null +++ b/tests/flow/test_initialize_pressure.py @@ -0,0 +1,41 @@ +from lettuce._flow import pressure_poisson +from tests.conftest import * + + +def test_initialize_pressure(fix_configuration): + device, dtype, use_native = fix_configuration + context = Context(device=device, dtype=dtype, use_native=use_native) + flow = TaylorGreenVortex(context=context, + resolution=[32] * 2, + reynolds_number=10, + mach_number=0.05, + initialize_fneq=False) + # getting analytical p and u + p0, u0 = flow.analytic_solution(t=0) + # get TGV velocity field + u_lu = flow.units.convert_velocity_to_lu(u0) + # manually setting rho=1 (p=0) + rho_lu = torch.ones_like(p0) + + # initializing as if not knowing analytic pressure solution + f_before = flow.equilibrium(flow, rho=rho_lu, u=u_lu) + p_before = flow.units.convert_density_lu_to_pressure_pu(flow.rho(f_before)) + + # getting poisson calculation for better f's + rho_poisson = pressure_poisson(flow.units, + u_lu, + rho_lu, + tol_abs=1e-6, + max_num_steps=1000 + ) + f_after = flow.equilibrium(flow, rho=rho_poisson, u=u_lu) + p_after = flow.units.convert_density_lu_to_pressure_pu(flow.rho(f_after)) + + # assert that pressure is much closer to analytic solution + assert (p_after - p0).abs().sum() < 5e-2 * (p_before - p0).abs().sum() + + # translating to numpy + p0, p_before, p_after = [context.convert_to_ndarray(_) + for _ in [p0, p_before, p_after]] + # assert that pressure is converged up to 0.05 (max p) + assert p_after == pytest.approx(p0, rel=0.0, abs=5e-2) diff --git a/tests/flow/test_obstacle.py b/tests/flow/test_obstacle.py new file mode 100644 index 00000000..e4f7e078 --- /dev/null +++ b/tests/flow/test_obstacle.py @@ -0,0 +1,32 @@ +from tests.conftest import * + + +@pytest.mark.parametrize("stencil2d3d", [D2Q9(), D3Q27()]) +def test_divergence(stencil2d3d, fix_configuration): + device, dtype, use_native = fix_configuration + context = Context(device=device, dtype=dtype, use_native=use_native) + + nx = 32 + ny = 16 + nz = 16 + + resolution = [nx, ny, nz] if stencil2d3d.d == 3 else [nx, ny] + mask = np.zeros(resolution) + if stencil2d3d.d == 3: + mask[3:6, 3:6, :] = 1 + else: + mask[3:6, 3:6] = 1 + flow = Obstacle(context=context, + resolution=resolution, + reynolds_number=100, + mach_number=0.1, + domain_length_x=3) + all_native_boundaries_in_Obstacle = sum([ + _.native_available() for _ in flow.boundaries]) == flow.boundaries + if use_native and not all_native_boundaries_in_Obstacle: + pytest.skip("Some boundaries in Obstacle are still not available for " + "cuda_native (probably AntiBounceBackOutlet)") + collision = BGKCollision(tau=flow.units.relaxation_parameter_lu) + flow.mask = mask != 0 + simulation = Simulation(flow, collision, []) + simulation(2) diff --git a/tests/flow/test_pressure_poisson.py b/tests/flow/test_pressure_poisson.py new file mode 100644 index 00000000..f17a36e1 --- /dev/null +++ b/tests/flow/test_pressure_poisson.py @@ -0,0 +1,22 @@ +from lettuce._flow import pressure_poisson +from tests.conftest import * + + +@pytest.mark.parametrize("Stencil", [D2Q9, D3Q27]) +def test_pressure_poisson(fix_configuration, Stencil): + if Stencil == D3Q27: + pytest.skip("D3Q27 pressure_poisson does not work.") + device, dtype, use_native = fix_configuration + context = Context(device, dtype, use_native) + flow = TaylorGreenVortex(context=context, + resolution=32, + reynolds_number=100, + mach_number=0.05, + stencil=Stencil) + p0, u = flow.initial_pu() + u = flow.units.convert_velocity_to_lu(u) + rho0 = flow.units.convert_pressure_pu_to_density_lu(p0) + rho = pressure_poisson(flow.units, u, torch.ones_like(rho0)) + pfinal = context.convert_to_ndarray( + flow.units.convert_density_lu_to_pressure_pu(rho)) + assert pfinal == pytest.approx(p0, rel=0.0, abs=0.05) diff --git a/tests/moments/test_conserved_moments_d2q9.py b/tests/moments/test_conserved_moments_d2q9.py new file mode 100644 index 00000000..acb664fd --- /dev/null +++ b/tests/moments/test_conserved_moments_d2q9.py @@ -0,0 +1,11 @@ +from lettuce.util.moments import D2Q9Dellar, D2Q9Lallemand, moment_tensor +from tests.conftest import * + + +@pytest.mark.parametrize("MomentSet", (D2Q9Dellar, D2Q9Lallemand)) +def test_conserved_moments_d2q9(MomentSet): + multiindices = np.array([ + [0, 0], [1, 0], [0, 1] + ]) + m = moment_tensor(D2Q9().e, multiindices) + assert m == pytest.approx(MomentSet.matrix[:3, :]) diff --git a/tests/moments/test_getitem.py b/tests/moments/test_getitem.py new file mode 100644 index 00000000..ffc3c6e0 --- /dev/null +++ b/tests/moments/test_getitem.py @@ -0,0 +1,7 @@ +from tests.conftest import * + + +def test_getitem(fix_device, fix_dtype): + moments = D2Q9Lallemand(D2Q9(), Context(fix_device, fix_dtype)) + assert moments["jx", "jy"] == [1, 2] + assert moments["rho"] == [0] diff --git a/tests/moments/test_inverse_transform.py b/tests/moments/test_inverse_transform.py new file mode 100644 index 00000000..11ec26c0 --- /dev/null +++ b/tests/moments/test_inverse_transform.py @@ -0,0 +1,15 @@ +from tests.conftest import * + + +def test_inverse_transform(fix_transform): + Transform, Stencil = fix_transform + stencil = Stencil() + context = Context() + torch_stencil = TorchStencil(stencil, context) + transform = Transform(torch_stencil, context) + f = context.convert_to_tensor( + np.random.random([stencil.q] + [3] * stencil.d)) + original = context.convert_to_ndarray(f) + retransformed = context.convert_to_ndarray( + transform.inverse_transform(transform.transform(f))) + assert retransformed == pytest.approx(original, abs=1e-5) diff --git a/tests/moments/test_moment_equilibrium_D3Q27Hermite.py b/tests/moments/test_moment_equilibrium_D3Q27Hermite.py new file mode 100644 index 00000000..bc4132dd --- /dev/null +++ b/tests/moments/test_moment_equilibrium_D3Q27Hermite.py @@ -0,0 +1,14 @@ +from tests.conftest import * + + +def test_moment_equilibrium_D3Q27Hermite(fix_device, fix_dtype): + context = Context(fix_device, fix_dtype) + stencil = D3Q27() + moments = D3Q27Hermite(stencil, context) + flow = TestFlow(context, 10, 1, 0.1, stencil=stencil) + meq1 = context.convert_to_ndarray(moments.transform(flow.equilibrium( + flow))) + meq2 = context.convert_to_ndarray(moments.equilibrium(moments.transform( + flow.f), flow)) + same_moments = moments['rho', 'jx', 'jy', 'jz', 'Pi_xx', 'Pi_xy', 'PI_xz', 'PI_yy', 'PI_yz', 'PI_zz'] + assert meq1[same_moments] == pytest.approx(meq2[same_moments], abs=1e-5) diff --git a/tests/moments/test_moment_equilibrium_dellar.py b/tests/moments/test_moment_equilibrium_dellar.py new file mode 100644 index 00000000..b89e7220 --- /dev/null +++ b/tests/moments/test_moment_equilibrium_dellar.py @@ -0,0 +1,13 @@ +from tests.conftest import * + + +def test_moment_equilibrium_dellar(fix_device, fix_dtype): + context = Context(fix_device, fix_dtype) + stencil = D2Q9() + moments = D2Q9Dellar(stencil, context) + flow = TestFlow(context, 10, 1, 0.1, stencil=stencil) + meq1 = context.convert_to_ndarray(moments.transform(flow.equilibrium( + flow))) + meq2 = context.convert_to_ndarray(moments.equilibrium(moments.transform( + flow.f), flow)) + assert meq1 == pytest.approx(meq2, abs=1e-5) diff --git a/tests/moments/test_moment_equilibrium_lallemand.py b/tests/moments/test_moment_equilibrium_lallemand.py new file mode 100644 index 00000000..02aa58a8 --- /dev/null +++ b/tests/moments/test_moment_equilibrium_lallemand.py @@ -0,0 +1,14 @@ +from tests.conftest import * + + +def test_moment_equilibrium_lallemand(fix_device, fix_dtype): + context = Context(fix_device, fix_dtype) + stencil = D2Q9() + moments = D2Q9Lallemand(stencil, context) + flow = TestFlow(context, 10, 1, 0.1, stencil=stencil) + meq1 = context.convert_to_ndarray(moments.transform(flow.equilibrium( + flow))) + meq2 = context.convert_to_ndarray(moments.equilibrium(moments.transform( + flow.f), flow)) + same_moments = moments["rho", "jx", "jy", "qx", "qy"] + assert meq1[same_moments] == pytest.approx(meq2[same_moments], abs=1e-5) diff --git a/tests/moments/test_moments_density.py b/tests/moments/test_moments_density.py new file mode 100644 index 00000000..050cc9b7 --- /dev/null +++ b/tests/moments/test_moments_density.py @@ -0,0 +1,32 @@ +from lettuce.util.moments import moment_tensor +from tests.conftest import * + + +def test_moments_density_array(fix_stencil): + rho_tensor = moment_tensor(fix_stencil.e, np.array([0] * fix_stencil.d)) + assert rho_tensor == pytest.approx(np.ones(fix_stencil.q)) + + +def test_more_moments_density_array(fix_stencil): + rho_tensor = moment_tensor(fix_stencil.e, np.array([[0] * fix_stencil.d])) + assert rho_tensor == pytest.approx(np.ones((1, fix_stencil.q))) + + +def test_moments_density_tensor(fix_stencil, fix_device): + context = Context(fix_device) + rho_tensor = moment_tensor(context.convert_to_tensor(fix_stencil.e), + context.convert_to_tensor(([0] + * fix_stencil.d))) + assert rho_tensor.shape == (fix_stencil.q,) + assert (context.convert_to_ndarray(rho_tensor) + == pytest.approx(np.ones((fix_stencil.q)))) + + +def test_more_moments_density_tensor(fix_stencil, fix_device): + context = Context(fix_device) + rho_tensor = moment_tensor(context.convert_to_tensor(fix_stencil.e), + context.convert_to_tensor(([[0] + * fix_stencil.d]))) + assert rho_tensor.shape == (1, fix_stencil.q) + assert (context.convert_to_ndarray(rho_tensor) + == pytest.approx(np.ones((1, fix_stencil.q)))) diff --git a/tests/moments/test_orthogonality.py b/tests/moments/test_orthogonality.py new file mode 100644 index 00000000..87bfd3e3 --- /dev/null +++ b/tests/moments/test_orthogonality.py @@ -0,0 +1,16 @@ +from tests.conftest import * + + +def test_orthogonality(fix_device, fix_dtype, fix_transform): + Transform, Stencil = fix_transform + stencil = Stencil() + if stencil.d == 1: + pytest.skip("No othogonality for 1D") + context = Context(fix_device, fix_dtype) + tranform = Transform(stencil, context) + M = context.convert_to_ndarray(tranform.matrix) + if Transform is D2Q9Lallemand: + Md = np.round(M @ M.T, 4) + else: + Md = np.round(M @ np.diag(stencil.w) @ M.T, 4) + assert np.where(np.diag(np.ones(stencil.q)), Md != 0.0, Md == 0.0).all() diff --git a/tests/native/__init__.py b/tests/native/__init__.py new file mode 100644 index 00000000..a44cde65 --- /dev/null +++ b/tests/native/__init__.py @@ -0,0 +1,6 @@ +from tests.conftest import * + +if not torch.cuda.is_available(): + pytest.skip(reason="CUDA is not available on this machine, " + "so cuda_native will not be tested..", + allow_module_level=True) diff --git a/tests/test_native_bgk_collision.py b/tests/native/test_native_bgk_collision.py similarity index 96% rename from tests/test_native_bgk_collision.py rename to tests/native/test_native_bgk_collision.py index 7bcb92e4..68c10f43 100644 --- a/tests/test_native_bgk_collision.py +++ b/tests/native/test_native_bgk_collision.py @@ -33,8 +33,6 @@ def boundaries(self) -> List['Boundary']: def test_native_bgk_collision(): - if not torch.cuda.is_available(): - pytest.skip(reason="CUDA is not available on this machine.") cpu_context = Context('cpu') cpu_flow = DummyBGK(cpu_context) diff --git a/tests/test_native_bounce_back.py b/tests/native/test_native_bounce_back.py similarity index 93% rename from tests/test_native_bounce_back.py rename to tests/native/test_native_bounce_back.py index 47033088..70e8ad5d 100644 --- a/tests/test_native_bounce_back.py +++ b/tests/native/test_native_bounce_back.py @@ -5,7 +5,7 @@ from lettuce import Context, Simulation from lettuce.ext import NoCollision, BounceBackBoundary -from tests.common import DummyFlow +from tests.conftest import DummyFlow class MyBounceBackBoundary(BounceBackBoundary): @@ -31,8 +31,6 @@ def boundaries(self) -> List['Boundary']: def test_native_bounce_back(): - if not torch.cuda.is_available(): - pytest.skip(reason="CUDA is not available on this machine.") cpu_context = Context(torch.device('cpu'), use_native=False) cpu_flow = DummyBBBC(cpu_context) diff --git a/tests/test_native_equilibrium_pu.py b/tests/native/test_native_equilibrium_pu.py similarity index 94% rename from tests/test_native_equilibrium_pu.py rename to tests/native/test_native_equilibrium_pu.py index f9e18376..dddb7906 100644 --- a/tests/test_native_equilibrium_pu.py +++ b/tests/native/test_native_equilibrium_pu.py @@ -4,7 +4,7 @@ from lettuce import * from lettuce.ext import * -from .common import DummyTGV +from tests.conftest import DummyTGV import pytest @@ -28,8 +28,6 @@ def make_no_streaming_mask(self, shape: List[int], context: 'Context' def test_equilibrium_boundary_pu_native(): - if not torch.cuda.is_available(): - pytest.skip(reason="CUDA is not available on this machine.") context_native = Context(device=torch.device('cuda'), dtype=torch.float64, use_native=True) context_cpu = Context(device=torch.device('cpu'), dtype=torch.float64, diff --git a/tests/native/test_native_no_streaming_mask.py b/tests/native/test_native_no_streaming_mask.py new file mode 100644 index 00000000..4de5a709 --- /dev/null +++ b/tests/native/test_native_no_streaming_mask.py @@ -0,0 +1,22 @@ +from tests.conftest import * + + +def test_native_no_streaming_mask(): + """test if """ + + if not torch.cuda.is_available(): + pytest.skip("Native test skipped") + + context = Context(dtype=torch.float32, device=torch.device("cuda"), + use_native=True) + flow = TestFlow(context, 16, 1, 0.01) + collision = NoCollision() + simulation = Simulation(flow, collision, []) + simulation.no_streaming_mask = context.zero_tensor(flow.resolution, + dtype=bool) + + f0 = copy(flow.f) + simulation(64) + f1 = flow.f + + assert torch.isclose(f0, f1).all() diff --git a/tests/test_native_streaming.py b/tests/native/test_native_streaming.py similarity index 92% rename from tests/test_native_streaming.py rename to tests/native/test_native_streaming.py index 5dddff28..e977ca87 100644 --- a/tests/test_native_streaming.py +++ b/tests/native/test_native_streaming.py @@ -3,7 +3,7 @@ from lettuce import Context, Simulation from lettuce.ext import NoCollision -from tests.common import DummyFlow +from tests.conftest import DummyFlow class DummyStreaming(DummyFlow): @@ -22,8 +22,6 @@ def initialize(self): def test_native_streaming(): - if not torch.cuda.is_available(): - pytest.skip(reason="CUDA is not available on this machine.") cpu_context = Context(torch.device('cpu'), use_native=False) cpu_flow = DummyStreaming(cpu_context) diff --git a/tests/reporter/test_HDF5Reporter.py b/tests/reporter/test_HDF5Reporter.py new file mode 100644 index 00000000..1ed090eb --- /dev/null +++ b/tests/reporter/test_HDF5Reporter.py @@ -0,0 +1,31 @@ +import os +from tests.conftest import * + + +def test_HDF5Reporter(tmpdir): + step = 3 + context = Context(device='cpu') + flow = TaylorGreenVortex(context=context, + resolution=[16, 16], + reynolds_number=10, + mach_number=0.05) + collision = BGKCollision(tau=flow.units.relaxation_parameter_lu) + simulation = Simulation(flow=flow, + collision=collision, + reporter=[]) + hdf5_reporter = HDF5Reporter(flow=flow, + collision=collision, + interval=step, + filebase=tmpdir / "output") + simulation.reporter.append(hdf5_reporter) + simulation(step) + assert os.path.isfile(tmpdir / "output.h5") + + dataset_train = LettuceDataset(filebase=tmpdir / "output.h5", + target=True) + train_loader = torch.utils.data.DataLoader(dataset_train, shuffle=False) + print(dataset_train) + for (f, target, idx) in train_loader: + assert idx in (0, 1, 2) + assert f.shape == (1, 9, 16, 16) + assert target.shape == (1, 9, 16, 16) diff --git a/tests/reporter/test_energy_spectrum.py b/tests/reporter/test_energy_spectrum.py new file mode 100644 index 00000000..bcc7907a --- /dev/null +++ b/tests/reporter/test_energy_spectrum.py @@ -0,0 +1,28 @@ +from tests.conftest import * + + +@pytest.mark.parametrize("flowname", flow_by_name.keys()) +def test_energy_spectrum(tmpdir, flowname): + context = Context(device=torch.device('cpu')) + IncompressibleFlow, stencil = flow_by_name[flowname] + if IncompressibleFlow is CouetteFlow2D: + pytest.skip("CouetteFlow2D has nan energy spectrum") + stencil = stencil() if callable(stencil) else stencil + flow = IncompressibleFlow(context=context, resolution=[20] * stencil.d, + reynolds_number=1600, + mach_number=0.01, + stencil=stencil) + collision = BGKCollision(tau=flow.units.relaxation_parameter_lu) + spectrum = context.convert_to_ndarray(EnergySpectrum(flow)()) + energy = IncompressibleKineticEnergy(flow)().item() + + if Flow == DecayingTurbulence: + # check that the reported spectrum agrees with the spectrum used for + # initialization + ek_ref, _ = flow.energy_spectrum + assert (spectrum == pytest.approx(ek_ref, rel=0.0, abs=0.1)) + if Flow == TaylorGreenVortex: + # check that flow has only one mode + ek_max = sorted(spectrum, reverse=True) + assert ek_max[0] * 1e-5 > ek_max[1] + assert (energy == pytest.approx(np.sum(spectrum), rel=0.1, abs=0.0)) diff --git a/tests/reporter/test_generic_reporters.py b/tests/reporter/test_generic_reporters.py new file mode 100644 index 00000000..7ce70db0 --- /dev/null +++ b/tests/reporter/test_generic_reporters.py @@ -0,0 +1,25 @@ +from tests.conftest import * + + +@pytest.mark.parametrize("Observable", [Enstrophy, + EnergySpectrum, + MaximumVelocity, + IncompressibleKineticEnergy, + Mass]) +@pytest.mark.parametrize("Case", [[32]*2, [32]*3]) +def test_generic_reporters(Observable, Case, fix_configuration): + device, dtype, use_native = fix_configuration + context = Context(device=device, dtype=dtype, use_native=use_native) + flow = TaylorGreenVortex(context, Case, 10000, 0.05) + collision = BGKCollision(tau=flow.units.relaxation_parameter_lu) + reporter = ObservableReporter(Observable(flow), + interval=1, + out=None) + simulation = Simulation(flow, collision, [reporter]) + simulation(2) + values = np.asarray(reporter.out) + if Observable is EnergySpectrum: + assert values[1, 2:] == pytest.approx(values[0, 2:], rel=0.0, + abs=values[0, 2:].sum() / 10) + else: + assert values[1, 2] == pytest.approx(values[0, 2], rel=0.05) diff --git a/tests/reporter/test_vtk_reporter_mask.py b/tests/reporter/test_vtk_reporter_mask.py new file mode 100644 index 00000000..c74d2ea9 --- /dev/null +++ b/tests/reporter/test_vtk_reporter_mask.py @@ -0,0 +1,18 @@ +import os + +from tests.conftest import * + + +def test_vtk_reporter_mask(tmpdir): + flow = PoiseuilleFlow2D(context=Context(), resolution=16, + reynolds_number=10, mach_number=0.05) + collision = BGKCollision(tau=flow.units.relaxation_parameter_lu) + simulation = Simulation(flow, collision, []) + vtk_reporter = VTKReporter(interval=1, + filename_base=tmpdir / "output") + simulation.reporter.append(vtk_reporter) + vtk_reporter.output_mask(simulation) + simulation(2) + assert os.path.isfile(tmpdir / "output_mask.vtr") + assert os.path.isfile(tmpdir / "output_00000000.vtr") + assert os.path.isfile(tmpdir / "output_00000001.vtr") diff --git a/tests/reporter/test_vtk_reporter_no_mask.py b/tests/reporter/test_vtk_reporter_no_mask.py new file mode 100644 index 00000000..0f0bbd31 --- /dev/null +++ b/tests/reporter/test_vtk_reporter_no_mask.py @@ -0,0 +1,16 @@ +import os +from tests.conftest import * + + +def test_vtk_reporter_no_mask(tmpdir): + context = Context(device="cpu") + flow = TaylorGreenVortex(context=context, resolution=[16, 16], + reynolds_number=10, mach_number=0.05) + collision = BGKCollision(tau=flow.units.relaxation_parameter_lu) + simulation = Simulation(flow, collision, []) + vtk_reporter = VTKReporter(interval=1, + filename_base=tmpdir / "output") + simulation.reporter.append(vtk_reporter) + simulation(2) + assert os.path.isfile(tmpdir / "output_00000000.vtr") + assert os.path.isfile(tmpdir / "output_00000001.vtr") diff --git a/tests/reporter/test_write_image.py b/tests/reporter/test_write_image.py new file mode 100644 index 00000000..3d9323eb --- /dev/null +++ b/tests/reporter/test_write_image.py @@ -0,0 +1,15 @@ +from tests.conftest import * +import os + + +def test_write_image(tmpdir): + # pytest.skip("matplotlib not working") + context = Context() + flow = TaylorGreenVortex(context=context, + resolution=[16, 16], + reynolds_number=10, + mach_number=0.05) + p, _ = flow.initial_pu() + write_image(tmpdir / "p.png", context.convert_to_ndarray(p[0])) + print(tmpdir / "p.png") + assert os.path.isfile(tmpdir / "p.png") diff --git a/tests/reporter/test_write_vtk.py b/tests/reporter/test_write_vtk.py new file mode 100644 index 00000000..cecdca84 --- /dev/null +++ b/tests/reporter/test_write_vtk.py @@ -0,0 +1,13 @@ +from tests.conftest import * +from lettuce.ext._reporter.vtk_reporter import write_vtk +import os + + +def test_write_vtk(tmpdir): + context = Context(device='cpu') + flow = TaylorGreenVortex(context, resolution=[16, 16], reynolds_number=10, + mach_number=0.05) + p, u = flow.initial_pu() + point_dict = {"p": context.convert_to_ndarray(p[0, ..., None])} + write_vtk(point_dict, id=1, filename_base=tmpdir / "output") + assert os.path.isfile(tmpdir / "output_00000001.vtr") diff --git a/tests/stencil/test_first_zero.py b/tests/stencil/test_first_zero.py new file mode 100644 index 00000000..092a8506 --- /dev/null +++ b/tests/stencil/test_first_zero.py @@ -0,0 +1,7 @@ +from tests.conftest import * + + +def test_first_zero(fix_stencil): + """Test that the zeroth velocity is 0.""" + assert (fix_stencil.e[0] + == pytest.approx(np.zeros_like(fix_stencil.e[0]))) diff --git a/tests/stencil/test_opposite.py b/tests/stencil/test_opposite.py new file mode 100644 index 00000000..59fd45ea --- /dev/null +++ b/tests/stencil/test_opposite.py @@ -0,0 +1,8 @@ +from tests.conftest import * + +def test_opposite(fix_stencil): + """Test if the opposite field holds the index of the opposite direction.""" + context = Context() + torch_stencil = TorchStencil(fix_stencil, context) + assert torch.isclose(torch_stencil.e[fix_stencil.opposite], + -torch_stencil.e).all() diff --git a/tests/stencil/test_symmetry.py b/tests/stencil/test_symmetry.py new file mode 100644 index 00000000..aa421486 --- /dev/null +++ b/tests/stencil/test_symmetry.py @@ -0,0 +1,6 @@ +from tests.conftest import * + + +def test_symmetry(fix_stencil): + """Test if the stencil is symmetric""" + assert np.array(fix_stencil.e).sum() == pytest.approx(0.0) diff --git a/tests/stencil/test_weights.py b/tests/stencil/test_weights.py new file mode 100644 index 00000000..4f25c681 --- /dev/null +++ b/tests/stencil/test_weights.py @@ -0,0 +1,6 @@ +from tests.conftest import * + + +def test_weights(fix_stencil): + """Test if the sum of all weights equals one.""" + assert np.array(fix_stencil.w).sum() == pytest.approx(1.0) diff --git a/tests/test_boundary.py b/tests/test_boundary.py deleted file mode 100644 index c0ce3734..00000000 --- a/tests/test_boundary.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -Test boundary conditions. -""" - -from lettuce import * -from lettuce.ext import * -from .common import DummyTGV - -import pytest - -import numpy as np -import torch - - -class my_equilibrium_boundary_mask(EquilibriumBoundaryPU): - - def make_no_collision_mask(self, shape: List[int], context: 'Context' - ) -> Optional[torch.Tensor]: - a = context.one_tensor(shape, dtype=bool) - return a - - def make_no_streaming_mask(self, shape: List[int], context: 'Context' - ) -> Optional[torch.Tensor]: - return context.one_tensor(shape, dtype=bool) - - -class DummyEQBC(TaylorGreenVortex): - @property - def boundaries(self): - u = self.context.one_tensor([2, 1, 1]) * 0.1 - p = self.context.zero_tensor([1, 1, 1]) - boundary = my_equilibrium_boundary_mask(self.context, - torch.ones(self.resolution), - u, p) - return [boundary] - - -def test_equilibrium_boundary_pu(): - context = Context(device=torch.device('cpu'), dtype=torch.float64, - use_native=False) - - flow_1 = DummyEQBC(context, resolution=[16, 16], reynolds_number=1, - mach_number=0.1) - flow_2 = DummyTGV(context, resolution=[16, 16], reynolds_number=1, - mach_number=0.1) - - simulation = Simulation(flow=flow_1, collision=NoCollision(), reporter=[]) - simulation(num_steps=1) - - pressure = 0 - velocity = 0.1 * np.ones(flow_2.stencil.d) - - feq = flow_2.equilibrium( - flow_2, - context.convert_to_tensor( - flow_2.units.convert_pressure_pu_to_density_lu(pressure)), - context.convert_to_tensor( - flow_2.units.convert_velocity_to_lu(velocity)) - ) - flow_2.f = torch.einsum("q,q...->q...", feq, torch.ones_like(flow_2.f)) - - assert flow_1.f.cpu().numpy() == pytest.approx(flow_2.f.cpu().numpy()) diff --git a/tests/test_checkpoint.py b/tests/test_checkpoint.py new file mode 100644 index 00000000..ef9e04d5 --- /dev/null +++ b/tests/test_checkpoint.py @@ -0,0 +1,53 @@ +from copy import deepcopy +from tests.conftest import * + + +def test_checkpoint(tmpdir): + context = Context() + flow = PoiseuilleFlow2D(context=context, + resolution=[512, 512], + reynolds_number=1, + mach_number=0.02, + initialize_with_zeros=False) + f0 = deepcopy(flow.f) + filename = tmpdir / 'PoiseuilleFlow2D' + flow.dump(filename) + simulation = Simulation(flow, + BGKCollision(flow.units.relaxation_parameter_lu), + []) + simulation(10) + flow.load(filename) + assert torch.eq(f0, flow.f).all() + + """ + This could be a way to test a dump-load workflow which stores all flow + attributes. I did not get it to work, yet. + context = Context('cuda') + flow = PoiseuilleFlow2D(context=context, + resolution=16, + reynolds_number=1, + mach_number=0.02, + initialize_with_zeros=False) + f0 = deepcopy(flow.f) + filename = './PoiseuilleFlow2D' + flow.dump(filename) + + simulation = Simulation(flow, + BGKCollision(flow.units.relaxation_parameter_lu), + []) + simulation(10) + + context2 = Context('cpu') + flow2 = PoiseuilleFlow2D(context=context2, + resolution=32, + reynolds_number=10, + mach_number=0.01, + initialize_with_zeros=True) + flow2.load(filename) + + assert flow2.resolution == flow.resolution + assert flow2.units.reynolds_number == flow.units.reynolds_number + assert flow2.units.mach_number == flow.units.mach_number + assert flow2.initialize_with_zeros == flow.initialize_with_zeros + assert torch.eq(f0, flow2.f).all() + """ diff --git a/old_tests/test_cli.py b/tests/test_cli.py similarity index 100% rename from old_tests/test_cli.py rename to tests/test_cli.py diff --git a/tests/test_equilibrium.py b/tests/test_equilibrium.py new file mode 100644 index 00000000..618db5cd --- /dev/null +++ b/tests/test_equilibrium.py @@ -0,0 +1,39 @@ +from tests.conftest import * + + +@pytest.mark.parametrize("fix_equilibrium", [QuadraticEquilibrium]) +def test_equilibrium_conserves_mass(fix_equilibrium, + fix_configuration, + fix_stencil): + device, dtype, use_native = fix_configuration + if use_native: + pytest.skip("This test does not depend on the native implementation.") + context = Context(device=device, dtype=dtype, use_native=False) + flow = TestFlow(context=context, + resolution=[16] * fix_stencil.d, + reynolds_number=100, + mach_number=0.1, + stencil=fix_stencil) + equilibrium = fix_equilibrium() + feq = equilibrium(flow) + assert (flow.rho(feq).cpu().numpy() + == pytest.approx(flow.rho().cpu().numpy())) + + +@pytest.mark.parametrize("fix_equilibrium", [QuadraticEquilibrium]) +def test_equilibrium_conserves_momentum(fix_equilibrium, + fix_configuration, + fix_stencil): + device, dtype, use_native = fix_configuration + if use_native: + pytest.skip("This test does not depend on the native implementation.") + context = Context(device=device, dtype=dtype, use_native=False) + flow = TestFlow(context=context, + resolution=[16] * fix_stencil.d, + reynolds_number=100, + mach_number=0.1, + stencil=fix_stencil) + equilibrium = fix_equilibrium() + feq = equilibrium(flow) + assert (flow.j(feq).cpu().numpy() + == pytest.approx(flow.j().cpu().numpy(), abs=1e-6)) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 00000000..55c02360 --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1,11 @@ +from tests.conftest import * + + +def create_default_unit_conversion(): + return UnitConversion( + reynolds_number=1000, + mach_number=0.05, + characteristic_length_pu=2 * np.pi, + characteristic_velocity_pu=2, + characteristic_length_lu=100, + characteristic_density_pu=0.7) diff --git a/tests/unit/test_consistency.py b/tests/unit/test_consistency.py new file mode 100644 index 00000000..6f587549 --- /dev/null +++ b/tests/unit/test_consistency.py @@ -0,0 +1,15 @@ +from tests.conftest import * +from tests.unit import create_default_unit_conversion + + +def test_consistency(): + rho = 0.9 + u = 0.1 + units = create_default_unit_conversion() + + assert (units.convert_density_to_pu(rho) + * units.convert_velocity_to_pu(u) ** 2 + == pytest.approx(units.convert_energy_to_pu(rho * u * u)) + ) + assert (units.convert_velocity_to_pu(u) ** 2 + == pytest.approx(units.convert_incompressible_energy_to_pu(u * u))) diff --git a/tests/unit/test_conversion_reversible.py b/tests/unit/test_conversion_reversible.py new file mode 100644 index 00000000..96aacf21 --- /dev/null +++ b/tests/unit/test_conversion_reversible.py @@ -0,0 +1,16 @@ +from tests.conftest import * +from tests.unit import create_default_unit_conversion + + +def test_conversion_reversible(): + approx_two = pytest.approx(2.0) + units = create_default_unit_conversion() + + assert approx_two == units.convert_velocity_to_lu(units.convert_velocity_to_pu(2.0)) + assert approx_two == units.convert_time_to_lu(units.convert_time_to_pu(2.0)) + assert approx_two == units.convert_length_to_lu(units.convert_length_to_pu(2.0)) + assert approx_two == units.convert_density_to_lu(units.convert_density_to_pu(2.0)) + assert approx_two == units.convert_pressure_to_lu(units.convert_pressure_to_pu(2.0)) + assert approx_two == units.convert_density_lu_to_pressure_pu(units.convert_pressure_pu_to_density_lu(2.0)) + assert approx_two == units.convert_energy_to_lu(units.convert_energy_to_pu(2.0)) + assert approx_two == units.convert_incompressible_energy_to_lu(units.convert_incompressible_energy_to_pu(2.0)) diff --git a/tests/unit/test_reynolds_number_consistent.py b/tests/unit/test_reynolds_number_consistent.py new file mode 100644 index 00000000..8763c6dc --- /dev/null +++ b/tests/unit/test_reynolds_number_consistent.py @@ -0,0 +1,9 @@ +from tests.conftest import * +from tests.unit import create_default_unit_conversion + + +def test_reynolds_number_consistent(): + units = create_default_unit_conversion() + re_lu = units.characteristic_velocity_lu * units.characteristic_length_lu / units.viscosity_lu + re_pu = units.characteristic_velocity_pu * units.characteristic_length_pu / units.viscosity_pu + assert re_lu == pytest.approx(re_pu) diff --git a/tests/util/test_grid_fine_to_coarse.py b/tests/util/test_grid_fine_to_coarse.py new file mode 100644 index 00000000..193a7f46 --- /dev/null +++ b/tests/util/test_grid_fine_to_coarse.py @@ -0,0 +1,25 @@ +from tests.conftest import * + + +@pytest.mark.parametrize("dims", [2, 3]) +def test_grid_fine_to_coarse(dims): + context = Context(device='cpu', dtype=torch.float64) + flow_f = TaylorGreenVortex(context, [40] * dims, 1600, 0.15) + + flow_c = TaylorGreenVortex(context, [20] * dims, 1600, 0.15) + + f_c = grid_fine_to_coarse(flow_f, + flow_f.f, + flow_f.units.relaxation_parameter_lu, + flow_c.units.relaxation_parameter_lu) + + p_init, u_init = flow_c.initial_pu() + rho_init = flow_c.units.convert_pressure_pu_to_density_lu(p_init) + u_init = flow_c.units.convert_velocity_to_lu(u_init) + shear_c_init = flow_c.shear_tensor(flow_c.f) + shear_c = flow_c.shear_tensor(f_c) + + assert torch.isclose(flow_c.u(f_c), u_init).all() + assert torch.isclose(flow_c.rho(f_c), rho_init).all() + assert torch.isclose(f_c, flow_c.f).all() + assert torch.isclose(shear_c_init, shear_c).all() diff --git a/tests/util/test_torch_gradient.py b/tests/util/test_torch_gradient.py new file mode 100644 index 00000000..d92a5e87 --- /dev/null +++ b/tests/util/test_torch_gradient.py @@ -0,0 +1,48 @@ +from tests.conftest import * + + +@pytest.mark.parametrize("dims", [2, 3]) +@pytest.mark.parametrize("order", [2, 4, 6]) +def test_torch_gradient(dims, order): + context = Context() + flow = TaylorGreenVortex(context=context, + resolution=[100] * dims, + reynolds_number=1, + mach_number=0.05) + + _, u = flow.initial_pu() + + dx = flow.units.convert_length_to_pu(1.0) + u0_grad = torch_gradient(u[0], + dx=dx, + order=order) + + u = context.convert_to_ndarray(u) + u0_grad = context.convert_to_ndarray(u0_grad) + + u0_grad_np = np.array(np.gradient(u[0], dx)) + if dims == 2: + x, y = [context.convert_to_ndarray(x) for x in flow.grid] + u0_grad_analytic = np.array([ + -np.sin(x) * np.sin(y), + np.cos(x) * np.cos(y), + ]) + elif dims == 3: + x, y, z = [context.convert_to_ndarray(x) for x in flow.grid] + u0_grad_analytic = np.array([ + np.cos(x) * np.cos(y) * np.cos(z), + np.sin(x) * np.sin(y) * (-np.cos(z)), + np.sin(x) * (-np.cos(y)) * np.sin(z) + ]) + else: + return + assert np.allclose(u0_grad_analytic, + u0_grad, rtol=0.0, atol=1e-3) + if dims == 2: + assert (u0_grad_np[:, 2:-2, 2:-2] + == pytest.approx(u0_grad[:, 2:-2, 2:-2], + rel=0.0, abs=1e-3)) + if dims == 3: + assert (u0_grad_np[:, 2:-2, 2:-2, 2:-2] + == pytest.approx(u0_grad[:, 2:-2, 2:-2, 2:-2], + rel=0.0, abs=1e-3))