diff --git a/lbmpy_tests/test_fluctuating_lb.py b/lbmpy_tests/test_fluctuating_lb.py index 1317568a1d67a88a970b7b1d5813f83e61983c9f..7ce27c90db7608f3ae0d0db5b94f45b82affddac 100644 --- a/lbmpy_tests/test_fluctuating_lb.py +++ b/lbmpy_tests/test_fluctuating_lb.py @@ -1,90 +1,196 @@ -""" -This tests that for the thermalized LB (MRT with 15 equal relaxation times), -the correct temperature is obtained and the velocity distribution matches -the Maxwell-Boltzmann distribution -""" +"""Tests velocity and stress fluctuations for thermalized LB""" import pystencils as ps -from lbmpy.lbstep import LatticeBoltzmannStep -from lbmpy.creationfunctions import create_lb_collision_rule -from lbmpy.relaxationrates import relaxation_rate_from_lattice_viscosity, relaxation_rate_from_magic_number +from lbmpy.creationfunctions import * +from lbmpy.macroscopic_value_kernels import macroscopic_values_setter import numpy as np -import pickle -import gzip -from time import time +from lbmpy.moments import is_bulk_moment, is_shear_moment, get_order -def single_component_maxwell(x1, x2, kT): +def single_component_maxwell(x1, x2, kT, mass): """Integrate the probability density from x1 to x2 using the trapezoidal rule""" x = np.linspace(x1, x2, 1000) - return np.trapz(np.exp(-x**2 / (2. * kT)), x) / np.sqrt(2. * np.pi * kT) - - -def run_scenario(scenario, steps): - scenario.pre_run() - for t in range(scenario.time_steps_run, scenario.time_steps_run + steps): - scenario.kernel_params['time_step'] = t - scenario.time_step() - scenario.post_run() - scenario.time_steps_run += steps - - -def create_scenario(domain_size, temperature=None, viscosity=None, seed=2, target='cpu', openmp=4, num_rel_rates=None): - rr = [relaxation_rate_from_lattice_viscosity(viscosity)] - rr = rr*num_rel_rates - cr = create_lb_collision_rule( - stencil='D3Q19', compressible=True, - method='mrt', weighted=True, relaxation_rates=rr, - fluctuating={'temperature': temperature, 'seed': seed}, - optimization={'cse_global': True, 'split': False, - 'cse_pdfs': True, 'vectorization': True} + return np.trapz(np.exp(-mass * x**2 / (2. * kT)), x) / np.sqrt(2. * np.pi * kT/mass) + + +def rr_getter(moment_group): + """Mapos moments to 4 relaxation rates for shear, bulk, odd, even modes""" + is_shear = [is_shear_moment(m, 3) for m in moment_group] + is_bulk = [is_bulk_moment(m, 3) for m in moment_group] + order = [get_order(m) for m in moment_group] + assert min(order) == max(order) + order = order[0] + + if order < 2: + return 0 + elif any(is_bulk): + assert all(is_bulk) + return sp.Symbol("omega_bulk") + elif any(is_shear): + assert all(is_shear) + return sp.Symbol("omega_shear") + elif order % 2 == 0: + assert order > 2 + return sp.Symbol("omega_even") + else: + return sp.Symbol("omega_odd") + + +def second_order_moment_tensor_assignments(function_values, stencil, output_field): + """Assignments for calculating the pressure tensor""" + assert len(function_values) == len(stencil) + dim = len(stencil[0]) + return [ps.Assignment(output_field(i, j), + sum(c[i] * c[j] * f for f, c in zip(function_values, stencil))) + for i in range(dim) for j in range(dim)] + + +def add_pressure_output_to_collision_rule(collision_rule, pressure_field): + pressure_ouput = second_order_moment_tensor_assignments(collision_rule.method.pre_collision_pdf_symbols, + collision_rule.method.stencil, pressure_field) + collision_rule.main_assignments = collision_rule.main_assignments + pressure_ouput + + +def test(): + + # Parameters + stencil = get_stencil('D3Q19') + L = [60]*3 + kT = 4E-4 + rho_0 = 0.8 + omega_shear = 0.8 + omega_bulk = 0.3 + omega_even = 0.5 + omega_odd = 0.4 + target = 'cpu' + + # Setup data handling + dh = ps.create_data_handling(L, periodicity=True, default_target=target) + src = dh.add_array('src', values_per_cell=len(stencil), layout='f') + dst = dh.add_array_like('dst', 'src') + rho = dh.add_array('rho', layout='f', latex_name='\\rho') + u = dh.add_array('u', values_per_cell=dh.dim, layout='f') + pressure_field = dh.add_array('pressure', values_per_cell=( + 3, 3), layout='f', gpu=target == 'gpu') + force_field = dh.add_array( + 'force', values_per_cell=3, layout='f', gpu=target == 'gpu') + + # Method setup + method = create_mrt_orthogonal( + stencil=get_stencil('D3Q19'), + compressible=True, + weighted=True, + relaxation_rate_getter=rr_getter, + force_model=force_model_from_string('luo', force_field.center_vector)) + collision_rule = create_lb_collision_rule( + method, + fluctuating={ + 'temperature': kT + }, + optimization={'cse_global': True} ) - return LatticeBoltzmannStep(periodicity=(True, True, True), domain_size=domain_size, compressible=True, stencil='D3Q19', collision_rule=cr, optimization={'target': target, 'openmp': openmp}) - - -def test_fluctuating_mrt(): - # Unit conversions (MD to lattice) for parameters known to work with Espresso - agrid = 1. - m = 1. # mass per node - tau = 0.01 # time step - temperature = 4. / (m * agrid**2/tau**2) - viscosity = 3. * tau / agrid**2 - n = 8 - sc = create_scenario((n, n, n), viscosity=viscosity, temperature=temperature, - target='cpu', openmp=4, num_rel_rates=15) - assert np.average(sc.velocity[:, :, :]) == 0. - - # Warmup - run_scenario(sc, steps=500) - # sampling: - steps = 20000 - v = np.zeros((steps, n, n, n, 3)) - for i in range(steps): - run_scenario(sc, steps=2) - v[i, :, :, :, :] = np.copy(sc.velocity[:, :, :, :]) - - v = v.reshape((steps*n*n*n, 3)) - np.testing.assert_allclose(np.mean(v, axis=0), [0, 0, 0], atol=6E-7) - np.testing.assert_allclose( - np.var(v, axis=0), [temperature, temperature, temperature], rtol=1E-2) - v_hist, v_bins = np.histogram(v, bins=11, range=(-.08, .08), density=True) - - # Calculate expected values from single - v_expected = [] - for i in range(len(v_hist)): - # Maxwell distribution - res = np.exp(-v_bins[i]**2/(2.*temperature)) / \ - np.sqrt(2*np.pi*temperature) - res = 1./(v_bins[i+1]-v_bins[i]) * \ - single_component_maxwell(v_bins[i], v_bins[i+1], temperature) - v_expected.append(res) - v_expected = np.array(v_expected) - - # 8% accuracy on the entire histogram - np.testing.assert_allclose(v_hist, v_expected, rtol=0.08) - # 0.5% accuracy on the middle part - remove = 3 - np.testing.assert_allclose( - v_hist[remove:-remove], v_expected[remove:-remove], rtol=0.005) + add_pressure_output_to_collision_rule(collision_rule, pressure_field) + + collision = create_lb_update_rule(collision_rule=collision_rule, + stencil=stencil, + method=method, + compressible=True, + kernel_type='collide_only', + optimization={'symbolic_field': src}) + stream = create_stream_pull_with_output_kernel(collision.method, src, dst, + {'density': rho, 'velocity': u}) + + opts = {'cpu_openmp': True, + 'cpu_vectorize_info': None, + 'target': dh.default_target} + + # Compile kernels + stream_kernel = ps.create_kernel(stream, **opts).compile() + collision_kernel = ps.create_kernel(collision, **opts).compile() + + sync_pdfs = dh.synchronization_function([src.name]) + + # Initialization + init = macroscopic_values_setter(collision.method, velocity=(0,)*dh.dim, + pdfs=src.center_vector, density=rho.center) + init_kernel = ps.create_kernel(init, ghost_layers=0).compile() + + dh.fill(rho.name, rho_0) + dh.fill(u.name, np.nan, ghost_layers=True, inner_ghost_layers=True) + dh.fill(u.name, 0) + dh.run_kernel(init_kernel) + + # time loop + def time_loop(start, steps): + dh.all_to_gpu() + for i in range(start, start+steps): + dh.run_kernel(collision_kernel, omega_shear=omega_shear, omega_bulk=omega_bulk, + omega_odd=omega_odd, omega_even=omega_even, seed=42, time_step=i) + + sync_pdfs() + dh.run_kernel(stream_kernel) + + dh.swap(src.name, dst.name) + return start+steps + + # Test + t = 0 + # warm up + t = time_loop(t, 10) + + # Measurement + for i in range(10): + t = time_loop(t, 5) + + res_u = dh.gather_array("u").reshape((-1, 3)) + res_rho = dh.gather_array("rho").reshape((-1,)) + + # mass conservation + np.testing.assert_allclose(np.mean(res_rho), rho_0, atol=3E-12) + + # momentum conservation + momentum = np.dot(res_rho, res_u) + np.testing.assert_allclose(momentum, [0, 0, 0], atol=1E-10) + + # temperature + kinetic_energy = 1/2*np.dot(res_rho, res_u*res_u)/np.product(L) + np.testing.assert_allclose( + kinetic_energy, [kT/2]*3, atol=kT*0.01) + + # velocity distribution + v_hist, v_bins = np.histogram( + res_u, bins=11, range=(-.075, .075), density=True) + + # Calculate expected values from single + v_expected = [] + for j in range(len(v_hist)): + # Maxwell distribution + res = 1./(v_bins[j+1]-v_bins[j]) * \ + single_component_maxwell( + v_bins[j], v_bins[j+1], kT, rho_0) + v_expected.append(res) + v_expected = np.array(v_expected) + + # 10% accuracy on the entire histogram + np.testing.assert_allclose(v_hist, v_expected, rtol=0.1) + # 1% accuracy on the middle part + remove = 3 + np.testing.assert_allclose( + v_hist[remove:-remove], v_expected[remove:-remove], rtol=0.01) + + # pressure tensor against expressions from + # Duenweg, Schiller, Ladd, https://arxiv.org/abs/0707.1581 + + res_pressure = dh.gather_array("pressure").reshape((-1, 3, 3)) + + c_s = np.sqrt(1/3) # speed of sound + + # average of pressure tensor + # Diagonal elements are rho c_s^22 +<u,u>. When the fluid is + # thermalized, the expectation value of <u,u> = kT due to the + # equi-partition theorem. + p_av_expected = np.diag([rho_0*c_s**2 + kT]*3) + np.testing.assert_allclose( + np.mean(res_pressure, axis=0), p_av_expected, atol=c_s**2/2000) diff --git a/lbmpy_tests/test_fluctuation.py b/lbmpy_tests/test_fluctuation.py deleted file mode 100644 index ded0db9149789bc95af08829a7e68b171752252e..0000000000000000000000000000000000000000 --- a/lbmpy_tests/test_fluctuation.py +++ /dev/null @@ -1,12 +0,0 @@ -import numpy as np - -from lbmpy.scenarios import create_channel - - -def test_fluctuating_generation_pipeline(): - ch = create_channel((10, 10), stencil='D2Q9', method='mrt', weighted=True, relaxation_rates=[1.5] * 5, force=1e-5, - force_model='luo', fluctuating={'temperature': 1e-9}, kernel_params={'time_step': 1, 'seed': 312}, - optimization={'cse_global': True}) - - ch.run(10) - assert np.max(ch.velocity[:, :]) < 0.1