class Obstacle: """ Flow class to simulate the flow around an object (mask). It consists of one inflow (equilibrium boundary) and one outflow (anti-bounce-back-boundary), leading to a flow in positive x direction. Parameters ---------- shape : Tuple[int]: Grid resolution. domain_length_x : float Length of the domain in physical units. Attributes ---------- mask : np.array with dtype = np.bool Boolean mask to define the obstacle. The shape of this object is the shape of the grid. Initially set to zero (no obstacle). Examples -------- Initialization of flow around a cylinder: >>> from lettuce import Lattice, D2Q9 >>> flow = Obstacle( >>> shape=(101, 51), >>> reynolds_number=100, >>> mach_number=0.1, >>> lattice=lattice, >>> domain_length_x=10.1 >>> ) >>> x, y = flow.grid >>> condition = np.sqrt((x-2.5)**2+(y-2.5)**2) < 1. >>> flow.mask[np.where(condition)] = 1 """ def __init__(self, shape, reynolds_number, mach_number, lattice, domain_length_x, char_length=1, char_velocity=1): if len(shape) != lattice.D: raise ValueError( f"{lattice.D}-dimensional lattice requires {lattice.D}-dimensional `shape`" ) self.shape = shape char_length_lu = shape[0] / domain_length_x * char_length self.units = UnitConversion(lattice, reynolds_number=reynolds_number, mach_number=mach_number, characteristic_length_lu=char_length_lu, characteristic_length_pu=char_length, characteristic_velocity_pu=char_velocity) self._mask = np.zeros(shape=self.shape, dtype=np.bool) @property def mask(self): return self._mask @mask.setter def mask(self, m): assert isinstance(m, np.ndarray) and m.shape == self.shape self._mask = m.astype(np.bool) def initial_solution(self, x): p = np.zeros_like(x[0], dtype=float)[None, ...] u_char = self.units.characteristic_velocity_pu * self._unit_vector() u_char = append_axes(u_char, self.units.lattice.D) u = (1 - self.mask) * u_char return p, u @property def grid(self): xyz = tuple( self.units.convert_length_to_pu(np.arange(n)) for n in self.shape) return np.meshgrid(*xyz, indexing='ij') @property def boundaries(self): x = self.grid[0] return [ EquilibriumBoundaryPU( np.abs(x) < 1e-6, self.units.lattice, self.units, self.units.characteristic_velocity_pu * self._unit_vector()), AntiBounceBackOutlet(self.units.lattice, self._unit_vector().tolist()), BounceBackBoundary(self.mask, self.units.lattice) ] def _unit_vector(self, i=0): return np.eye(self.units.lattice.D)[i]
class DecayingTurbulence: def __init__(self, resolution, reynolds_number, mach_number, lattice, k0=20, ic_energy=0.5): self.k0 = k0 self.ic_energy = ic_energy self.resolution = resolution self.units = UnitConversion( lattice, reynolds_number=reynolds_number, mach_number=mach_number, characteristic_length_lu=resolution, characteristic_length_pu=2 * np.pi, characteristic_velocity_pu=None ) self.wavenumbers = [] self.spectrum = [] def analytic_solution(self, x, t=0): return def _generate_wavenumbers(self): self.dimensions = self.grid[0].shape frequencies = [np.fft.fftfreq(dim, d=1 / dim) for dim in self.dimensions] wavenumber = np.meshgrid(*frequencies) wavenorms = np.linalg.norm(wavenumber, axis=0) self.wavenumbers = np.arange(int(np.max(wavenorms))) wavemask = (wavenorms[..., None] > self.wavenumbers - 0.5) & (wavenorms[..., None] <= self.wavenumbers + 0.5) return wavenorms, wavenumber, wavemask def _generate_spectrum(self): wavenorms, wavenumber, wavemask = self._generate_wavenumbers() ek = (wavenorms) ** 4 * np.exp(-2 * (wavenorms / self.k0) ** 2) ek /= np.sum(ek) ek *= self.ic_energy self.spectrum = ek[..., None] * wavemask self.spectrum = np.sum(self.spectrum, axis=tuple((np.arange(self.units.lattice.D)))) return ek, wavenumber def _generate_initial_velocity(self, ek, wavenumber): dx = self.units.convert_length_to_pu(1.0) u = np.random.random(np.array(wavenumber).shape) * 2 * np.pi + 0j u = [np.fft.fftn(u[dim], axes=tuple((np.arange(self.units.lattice.D)))) for dim in range(self.units.lattice.D)] u_real = [u[dim].real for dim in range(self.units.lattice.D)] u_imag = [u[dim].imag for dim in range(self.units.lattice.D)] for dim in range(self.units.lattice.D): u_real[dim].ravel()[0] = 0 u_imag[dim].ravel()[0] = 0 u_real_h = [np.sqrt(2 / self.units.lattice.D * ek / (u_imag[dim] ** 2 + u_real[dim] ** 2 + 1.e-15)) * u_real[dim] for dim in range(self.units.lattice.D)] u_imag_h = [np.sqrt(2 / self.units.lattice.D * ek / (u_imag[dim] ** 2 + u_real[dim] ** 2 + 1.e-15)) * u_imag[dim] for dim in range(self.units.lattice.D)] for dim in range(self.units.lattice.D): u_real_h[dim].ravel()[0] = 0 u_imag_h[dim].ravel()[0] = 0 ### Remove divergence # modified wave number sin(k*dx) is used, as the gradient below uses second order cental differences # Modify if other schemes are used or use kx, ky if you don't know the modified wavenumber !!! wavenumber_modified = [np.sin(wavenumber[dim] * dx) / dx for dim in range(self.units.lattice.D)] wavenorm_modified = np.linalg.norm(wavenumber_modified, axis=0) + 1e-16 divergence_real = np.zeros(self.dimensions) divergence_imag = np.zeros(self.dimensions) for dim in range(self.units.lattice.D): divergence_real += wavenumber_modified[dim] * u_real_h[dim] divergence_imag += wavenumber_modified[dim] * u_imag_h[dim] u_real = [u_real_h[dim] - divergence_real * wavenumber_modified[dim] / wavenorm_modified ** 2 for dim in range(self.units.lattice.D)] u_imag = [u_imag_h[dim] - divergence_imag * wavenumber_modified[dim] / wavenorm_modified ** 2 for dim in range(self.units.lattice.D)] for dim in range(self.units.lattice.D): u_real[dim].ravel()[0] = 0 u_imag[dim].ravel()[0] = 0 ### Scale velocity field to achieve the desired inicial energy e_kin = [np.sum(u_real[dim] ** 2 + u_imag[dim] ** 2) for dim in range(self.units.lattice.D)] e_kin = np.sum(e_kin) * .5 factor = np.sqrt(self.ic_energy / e_kin) u_real = [u_real[dim] * factor for dim in range(self.units.lattice.D)] u_imag = [u_imag[dim] * factor for dim in range(self.units.lattice.D)] ### Backtransformation to physical space norm = ((self.resolution * dx ** (1 - self.units.lattice.D) * np.sqrt(self.units.characteristic_length_pu)) if self.units.lattice.D == 3 else (self.resolution / dx)) u = np.asarray([ (np.fft.ifftn(u_real[dim] + u_imag[dim] * 1.0j, axes=tuple((np.arange(self.units.lattice.D)))) * norm).real for dim in range(self.units.lattice.D)]) return u def _compute_initial_pressure(self): return np.zeros(self.dimensions)[None, ...] def initial_solution(self, x): """Return initial solution. Note: this function sets the characteristic velocity in phyiscal units.""" ek, wavenumber = self._generate_spectrum() u = self._generate_initial_velocity(ek, wavenumber) p = self._compute_initial_pressure() self.units.characteristic_velocity_pu = np.linalg.norm(u, axis=0).max() return p, u @property def energy_spectrum(self): return self.spectrum, self.wavenumbers @property def grid(self): grid = [np.linspace(0, 2 * np.pi, num=self.resolution, endpoint=False) for _ in range(self.units.lattice.D)] return np.meshgrid(*grid) @property def boundaries(self): return []