From 4714fff3d2cb42faf36ee6c129d138b24232b99d Mon Sep 17 00:00:00 2001 From: Ben Wibking Date: Tue, 10 Sep 2024 07:28:20 -0400 Subject: [PATCH 01/11] Update devcontainer.json to latest CI container (#117) * Update devcontainer.json to latest container Updates the Dev Container image to use `ghcr.io/parthenon-hpc-lab/cuda11.6-noascent`, which updates some of the dependencies. * Update CHANGELOG.md * Update CHANGELOG.md --- .devcontainer/devcontainer.json | 2 +- CHANGELOG.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9341352d..046efd7d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,7 @@ // devcontainer.json { "name": "athenapk-dev", - "image": "ghcr.io/parthenon-hpc-lab/cuda11.6-mpi-hdf5-ascent", + "image": "ghcr.io/parthenon-hpc-lab/cuda11.6-noascent", // disable Dockerfile for now //"build": { // // Path is relative to the devcontainer.json file. diff --git a/CHANGELOG.md b/CHANGELOG.md index e5957e43..369e8579 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ### Fixed (not changing behavior/API/variables/...) ### Infrastructure +- [[PR 117]](https://github.com/parthenon-hpc-lab/athenapk/pull/117) Update devcontainer.json to latest CI container - [[PR 114]](https://github.com/parthenon-hpc-lab/athenapk/pull/114) Bump Parthenon 24.08 and Kokkos to 4.4.00 - [[PR 112]](https://github.com/parthenon-hpc-lab/athenapk/pull/112) Add dev container configuration - [[PR 105]](https://github.com/parthenon-hpc-lab/athenapk/pull/105) Bump Parthenon to latest develop (2024-03-13) From 7ea10bbd55c7353acf5d2f6350ddc1f3c9f515f1 Mon Sep 17 00:00:00 2001 From: Philipp Grete Date: Tue, 10 Sep 2024 16:18:50 +0200 Subject: [PATCH 02/11] Add isotropic viscosity and resistivity (#89) * Add simple anisotropic step function test * Separate FillDerived and EstimateTimestep in driver in prep for STS list * Add diffflux parameter * Add RKL2 STS task list * Add calc of RKL2 stages * Remove unncessary register for rkl2 * Adopt STS RKL2 variable naming * Move calc of dt_diff into PreStep * Make tlim an argument for diff step test * Adjust RKL2 conv test to gaussian profile * Add conv panel to conv plot * auto-format * rename diffusion integrator parameter * Add isotropic thermal conduction * Add isotropic cond to conv test * Add RKL2 conv test * Add new dt max ratio for rkl2 param * Add prolongation and fluxcorrect to RKL2 task list * Use base container as active STS container (workaround some AMR bug for using prolong/restric with non-base containers) * Add isotropic Spitzer thermal conduction timestep * Calc isotropic, non-const thermal diff * Fix calc of saturated heat flux * Add LimO3 limiter * Add limo3 convergence * Fix LimO3 recon * Fix saturated conduction prefactor * Remove calc of saturated conduction from cond coeff * Add upwinded saturated conduction in x-dir * Add saturated conduction prefactor * Add x2 and x3 sat cond fluxes * Increase default rkl2 ratio to 400 and allow flux correction for all integrators * Remove parabolic timestep constraint for saturated conduction limit regime * Add perturb to cloud pgen * Add perturb to B (knowing this is not great...) * Revert "Add perturb to B (knowing this is not great...)" This reverts commit 1d0cb198c92b9eee579d3ecb5617d0629b1f9941. * Revert "Add perturb to cloud pgen" This reverts commit 6df018fe852fe4c285ac5929ed61d5fc67b17ddf. * Limit cooling to upper bound of TFloor and cooling table cutoff * Add oblique B field * Update coords and driver * Fix test cases and add success check * Add isotropic shear viscosity * Add viscosity test problem * Add viscosity convergence test * Add Ohmic resistivity * Remove visc pgen and move to diffusion pgen * Add resis. conv test to diffusion one * Add linwave3d decay diffusion test and fix parabolic dt * Fix test thresholds * Add changelog and readme * Cleanup leftover code * Add doc * Fix interface * Fix more interface * Fix even more interfaces * And fixing a typo * Increase ctest timeout * Address BWO review comments --- .github/workflows/ci.yml | 4 +- CHANGELOG.md | 1 + README.md | 9 +- docs/input.md | 79 +++++ inputs/diffusion.in | 14 +- src/CMakeLists.txt | 4 +- src/hydro/diffusion/conduction.cpp | 4 +- src/hydro/diffusion/diffusion.cpp | 24 +- src/hydro/diffusion/diffusion.hpp | 65 ++++ src/hydro/diffusion/resistivity.cpp | 240 ++++++++++++++ src/hydro/diffusion/viscosity.cpp | 295 ++++++++++++++++++ src/hydro/hydro.cpp | 103 +++++- src/main.cpp | 1 + src/main.hpp | 6 +- src/pgen/diffusion.cpp | 62 +++- src/pgen/linear_wave_mhd.cpp | 34 ++ src/pgen/pgen.hpp | 1 + tst/regression/CMakeLists.txt | 6 + .../aniso_therm_cond_ring_conv.py | 4 + .../aniso_therm_cond_ring_multid.py | 1 + .../test_suites/diffusion/__init__.py | 0 .../test_suites/diffusion/diffusion.py | 217 +++++++++++++ .../diffusion_linwave3d/__init__.py | 0 .../diffusion_linwave3d.py | 183 +++++++++++ 24 files changed, 1325 insertions(+), 32 deletions(-) create mode 100644 src/hydro/diffusion/resistivity.cpp create mode 100644 src/hydro/diffusion/viscosity.cpp create mode 100644 tst/regression/test_suites/diffusion/__init__.py create mode 100644 tst/regression/test_suites/diffusion/diffusion.py create mode 100644 tst/regression/test_suites/diffusion_linwave3d/__init__.py create mode 100644 tst/regression/test_suites/diffusion_linwave3d/diffusion_linwave3d.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d8fbe03..8fcfcba2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: cd build # Pick GPU with most available memory export CUDA_VISIBLE_DEVICES=$(nvidia-smi --query-gpu=memory.free,index --format=csv,nounits,noheader | sort -nr | head -1 | awk '{ print $NF }') - ctest -L ${{ matrix.parallel }} + ctest -L ${{ matrix.parallel }} --timeout 3600 - uses: actions/upload-artifact@v3 if: ${{ always() }} with: @@ -57,6 +57,8 @@ jobs: build/tst/regression/outputs/cluster_tabular_cooling/convergence.png build/tst/regression/outputs/aniso_therm_cond_ring_conv/ring_convergence.png build/tst/regression/outputs/aniso_therm_cond_gauss_conv/cond.png + build/tst/regression/outputs/diffusion/ohm.png + build/tst/regression/outputs/diffusion/visc.png build/tst/regression/outputs/field_loop/field_loop.png build/tst/regression/outputs/riemann_hydro/shock_tube.png build/tst/regression/outputs/turbulence/parthenon.out1.hst diff --git a/CHANGELOG.md b/CHANGELOG.md index 369e8579..65d55b73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Current develop (i.e., `main` branch) ### Added (new features/APIs/variables/...) +- [[PR 89]](https://github.com/parthenon-hpc-lab/athenapk/pull/89) Add viscosity and resistivity - [[PR 1]](https://github.com/parthenon-hpc-lab/athenapk/pull/1) Add isotropic thermal conduction and RKL2 supertimestepping ### Changed (changing behavior/API/variables/...) diff --git a/README.md b/README.md index 13d7280a..fd66e824 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,13 @@ Current features include - HLLE (hydro and MHD), HLLC (hydro), and HLLD (MHD) Riemann solvers - adiabatic equation of state - MHD based on hyperbolic divergence cleaning following Dedner+ 2002 - - isotropic and anisotropic thermal conduction - - operator-split, second-order RKL2 supertimestepping for diffusive terms + - diffusion processes + - isotropic and anisotropic thermal conduction + - viscosity + - resistivity + - diffusion integrator + - unsplit + - operator-split, second-order RKL2 supertimestepping - optically thin cooling based on tabulated cooling tables with either Townsend 2009 exact integration or operator-split subcycling - static and adaptive mesh refinement - problem generators for diff --git a/docs/input.md b/docs/input.md index c51aa8e7..d5e48d4f 100644 --- a/docs/input.md +++ b/docs/input.md @@ -69,6 +69,62 @@ conserved to primitive conversion if both are defined. #### Diffusive processes +Diffusive processes in AthenaPK can be configured in the `` block of the input file. +``` + +integrator = unsplit # alternatively: rkl2 (for rkl2 integrator (operator split integrator) +#rkl2_max_dt_ratio = 100.0 # limits the ratio between the parabolic and hyperbolic timesteps (only used for RKL2 operator split integrator) +#cfl = 1.0 # Additional safety factor applied in the caluclation of the diffusive timestep (used in both unsplit and RKL2 integration schemes). Defaults to hyperbolic cfl. + +conduction = anisotropic # none (disabled), or isotropic, or anisotropic +conduction_coeff = fixed # alternative: spitzer +thermal_diff_coeff_code = 0.01 # fixed coefficent in code units (code_length^2/code_time) +#spitzer_cond_in_erg_by_s_K_cm = 4.6e7 # spitzer coefficient in cgs units (requires definition of a unit system) +#conduction_sat_phi = 0.3 # fudge factor to account for uncertainties in saturated fluxes + + +viscosity = none # none (disabled) or isotropic +viscosity_coeff = fixed +mom_diff_coeff_code = 0.25 # fixed coefficent of the kinetmatic viscosity in code units (code_length^2/code_time) + +resistivity = none # none (disabled) or ohmic +resistivity_coeff = fixed +ohm_diff_coeff_code = 0.25 # fixed coefficent of the magnetic (ohmic) diffusivity code units (code_length^2/code_time) +``` +(An)isotropic thermal conduction (with fixed or Spitzer coefficient), and isotropic viscosity and +resistivity with fixed coefficient are currently implemented. +They can be integrated in an unsplit manner or operator split using a second-order accurate RKL2 +supertimestepping algorithm. +More details are described in the following. + +#### Integrators + +Diffusive processes can be integrated in either an unsplit +fashion (`diffusion/integrator=unsplit`) or operator split using a second-order accurate +RKL2 super timestepping algorithm (`diffusion/integrator=rkl2`) following [^M+14]. + +In the unsplit case, the diffusive processes are included at the end of every stage in +the main integration loop and the global timestep is limited accordingly. +A separate CFL can be set for the diffusive processes via `diffusion/cfl=...`, which +defaults to the hyperbolic value if not set. + +In the RKL2 case, the global timestep is not limited by the diffusive processes by default. +However, as reported by [^V+17] a large number of stages +($`s \approx \sqrt(\Delta t_{hyp}/\Delta t_{par}) \geq 20`$) in the supertimestepping +(in combination with anisotropic, limited diffusion) may lead to a loss in accuracy, which +is why the difference between hyperbolic and parabolic timesteps can be limited by +`diffusion/rkl2_max_dt_ratio=...` and a warning is shown if the ratio is above 400. +Note that if this limit is enforced the `dt=` shown on the terminal might not be the actual +`dt` taken in the code as the limit is currently enforced only after the output +has been printed. + +[^M+14]: + C. D. Meyer, D. S. Balsara, and T. D. Aslam, “A stabilized Runge–Kutta–Legendre method for explicit super-time-stepping of parabolic and mixed equations,” Journal of Computational Physics, vol. 257, pp. 594–626, 2014, doi: https://doi.org/10.1016/j.jcp.2013.08.021. + +[^V+17]: + B. Vaidya, D. Prasad, A. Mignone, P. Sharma, and L. Rickler, “Scalable explicit implementation of anisotropic diffusion with Runge–Kutta–Legendre super-time stepping,” Monthly Notices of the Royal Astronomical Society, vol. 472, no. 3, pp. 3147–3160, 2017, doi: 10.1093/mnras/stx2176. + + ##### Isotropic (hydro and MHD) and anisotropic thermal conduction (only MHD) In the presence of magnetic fields thermal conduction is becoming anisotropic with the flux along the local magnetic field direction typically being much stronger than the flux perpendicular to the magnetic field. @@ -140,6 +196,29 @@ Default value corresponds to the typical value used in literature and goes back [^BM82]: S. A. Balbus and C. F. McKee, “The evaporation of spherical clouds in a hot gas. III - Suprathermal evaporation,” , vol. 252, pp. 529–552, Jan. 1982, doi: https://doi.org/10.1086/159581 +#### Viscosity/Momentum diffusion + +Only isotropic viscosity with a (spatially and temporally) fixed coefficient in code units +(`code_length`^2/`code_time`) is currently implemented. +To enable set (in the `` block) +``` +viscosity = isotropic +viscosity_coeff = fixed +mom_diff_coeff_code = 0.25 # fixed coefficent of the kinetmatic viscosity in code units (code_length^2/code_time) +``` + +#### Resistivity/Ohmic diffusion + +Only resistivity with a (spatially and temporally) fixed coefficient in code units +(`code_length`^2/`code_time`)is currently implemented. +To enable set (in the `` block) +``` +resistivity = ohmic +resistivity_coeff = fixed +ohm_diff_coeff_code = 0.25 # fixed coefficent of the magnetic (ohmic) diffusivity code units (code_length^2/code_time) +``` + + ### Additional MHD options in `` block Parameter: `glmmhd_source` (string) diff --git a/inputs/diffusion.in b/inputs/diffusion.in index 44d02bcb..915dd088 100644 --- a/inputs/diffusion.in +++ b/inputs/diffusion.in @@ -1,9 +1,9 @@ # AthenaPK - a performance portable block structured AMR MHD code -# Copyright (c) 2021, Athena Parthenon Collaboration. All rights reserved. +# Copyright (c) 2021-2024, Athena Parthenon Collaboration. All rights reserved. # Licensed under the BSD 3-Clause License (the "LICENSE"); -problem = Thermal diffusion setup +problem = Diffusion setup (for thermal, momentum and Ohmic diffusion tests) problem_id = diffusion @@ -60,11 +60,17 @@ reconstruction = dc gamma = 2.0 -integrator = unsplit +integrator = rkl2 conduction = anisotropic conduction_coeff = fixed thermal_diff_coeff_code = 0.01 -rkl2_max_dt_ratio = 400.0 +viscosity = none # none (disabled), isotropic, or anisotropic +viscosity_coeff = fixed +mom_diff_coeff_code = 0.25 +resistivity = none # none (disabled) or ohmic +resistivity_coeff = fixed +ohm_diff_coeff_code = 0.25 +rkl2_max_dt_ratio = 100.0 file_type = hdf5 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ddcf09b7..ae2a521a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,9 +7,11 @@ add_executable( eos/adiabatic_glmmhd.cpp units.hpp eos/adiabatic_hydro.cpp + hydro/diffusion/conduction.cpp hydro/diffusion/diffusion.cpp hydro/diffusion/diffusion.hpp - hydro/diffusion/conduction.cpp + hydro/diffusion/resistivity.cpp + hydro/diffusion/viscosity.cpp hydro/hydro_driver.cpp hydro/hydro.cpp hydro/glmmhd/dedner_source.cpp diff --git a/src/hydro/diffusion/conduction.cpp b/src/hydro/diffusion/conduction.cpp index d6e57d30..40256024 100644 --- a/src/hydro/diffusion/conduction.cpp +++ b/src/hydro/diffusion/conduction.cpp @@ -179,8 +179,8 @@ Real EstimateConductionTimestep(MeshData *md) { }, Kokkos::Min(min_dt_cond)); } - - return fac * min_dt_cond; + const auto &cfl_diff = hydro_pkg->Param("cfl_diff"); + return cfl_diff * fac * min_dt_cond; } //--------------------------------------------------------------------------------------- diff --git a/src/hydro/diffusion/diffusion.cpp b/src/hydro/diffusion/diffusion.cpp index 20617ca2..d8b1c59d 100644 --- a/src/hydro/diffusion/diffusion.cpp +++ b/src/hydro/diffusion/diffusion.cpp @@ -1,6 +1,6 @@ //======================================================================================== // AthenaPK - a performance portable block structured AMR astrophysical MHD code. -// Copyright (c) 2021, Athena-Parthenon Collaboration. All rights reserved. +// Copyright (c) 2021-2023, Athena-Parthenon Collaboration. All rights reserved. // Licensed under the 3-clause BSD License, see LICENSE file for details //======================================================================================== //! \file diffusion.cpp @@ -27,5 +27,27 @@ TaskStatus CalcDiffFluxes(StateDescriptor *hydro_pkg, MeshData *md) { ThermalFluxGeneral(md); } } + const auto &viscosity = hydro_pkg->Param("viscosity"); + if (viscosity != Viscosity::none) { + const auto &mom_diff = hydro_pkg->Param("mom_diff"); + + if (viscosity == Viscosity::isotropic && + mom_diff.GetCoeffType() == ViscosityCoeff::fixed) { + MomentumDiffFluxIsoFixed(md); + } else { + MomentumDiffFluxGeneral(md); + } + } + const auto &resistivity = hydro_pkg->Param("resistivity"); + if (resistivity != Resistivity::none) { + const auto &ohm_diff = hydro_pkg->Param("ohm_diff"); + + if (resistivity == Resistivity::ohmic && + ohm_diff.GetCoeffType() == ResistivityCoeff::fixed) { + OhmicDiffFluxIsoFixed(md); + } else { + OhmicDiffFluxGeneral(md); + } + } return TaskStatus::complete; } diff --git a/src/hydro/diffusion/diffusion.hpp b/src/hydro/diffusion/diffusion.hpp index 7d522902..9e03f25a 100644 --- a/src/hydro/diffusion/diffusion.hpp +++ b/src/hydro/diffusion/diffusion.hpp @@ -99,6 +99,71 @@ void ThermalFluxIsoFixed(MeshData *md); //! Calculate thermal conduction (general case incl. anisotropic and saturated) void ThermalFluxGeneral(MeshData *md); +struct MomentumDiffusivity { + private: + Real mbar_, me_, kb_; + Viscosity viscosity_; + ViscosityCoeff viscosity_coeff_type_; + // "free" coefficient/prefactor. Value depends on viscosity set in the constructor. + Real coeff_; + + public: + KOKKOS_INLINE_FUNCTION + MomentumDiffusivity(Viscosity viscosity, ViscosityCoeff viscosity_coeff_type, + Real coeff, Real mbar, Real me, Real kb) + : viscosity_(viscosity), viscosity_coeff_type_(viscosity_coeff_type), coeff_(coeff), + mbar_(mbar), me_(me), kb_(kb) {} + + KOKKOS_INLINE_FUNCTION + Real Get(const Real pres, const Real rho) const; + + KOKKOS_INLINE_FUNCTION + Viscosity GetType() const { return viscosity_; } + + KOKKOS_INLINE_FUNCTION + ViscosityCoeff GetCoeffType() const { return viscosity_coeff_type_; } +}; + +Real EstimateViscosityTimestep(MeshData *md); + +//! Calculate isotropic viscosity with fixed coefficient +void MomentumDiffFluxIsoFixed(MeshData *md); +//! Calculate viscosity (general case incl. anisotropic) +void MomentumDiffFluxGeneral(MeshData *md); + +struct OhmicDiffusivity { + private: + Real mbar_, me_, kb_; + Resistivity resistivity_; + ResistivityCoeff resistivity_coeff_type_; + // "free" coefficient/prefactor. Value depends on resistivity set in the constructor. + Real coeff_; + + public: + KOKKOS_INLINE_FUNCTION + OhmicDiffusivity(Resistivity resistivity, ResistivityCoeff resistivity_coeff_type, + Real coeff, Real mbar, Real me, Real kb) + : resistivity_(resistivity), resistivity_coeff_type_(resistivity_coeff_type), + coeff_(coeff), mbar_(mbar), me_(me), kb_(kb) {} + + KOKKOS_INLINE_FUNCTION + Real Get(const Real pres, const Real rho) const; + + KOKKOS_INLINE_FUNCTION + Resistivity GetType() const { return resistivity_; } + + KOKKOS_INLINE_FUNCTION + ResistivityCoeff GetCoeffType() const { return resistivity_coeff_type_; } +}; + +Real EstimateResistivityTimestep(MeshData *md); + +//! Calculate isotropic resistivity with fixed coefficient +void OhmicDiffFluxIsoFixed(MeshData *md); + +//! Calculate resistivity (general case incl. Spitzer) +void OhmicDiffFluxGeneral(MeshData *md); + // Calculate all diffusion fluxes, i.e., update the .flux views in md TaskStatus CalcDiffFluxes(StateDescriptor *hydro_pkg, MeshData *md); diff --git a/src/hydro/diffusion/resistivity.cpp b/src/hydro/diffusion/resistivity.cpp new file mode 100644 index 00000000..da8bf9ad --- /dev/null +++ b/src/hydro/diffusion/resistivity.cpp @@ -0,0 +1,240 @@ +//======================================================================================== +// AthenaPK - a performance portable block structured AMR astrophysical MHD code. +// Copyright (c) 2024, Athena-Parthenon Collaboration. All rights reserved. +// Licensed under the 3-clause BSD License, see LICENSE file for details +//======================================================================================== +//! \file resistivity.cpp +//! \brief + +// Parthenon headers +#include +#include + +// AthenaPK headers +#include "../../main.hpp" +#include "config.hpp" +#include "diffusion.hpp" +#include "kokkos_abstraction.hpp" +#include "utils/error_checking.hpp" + +using namespace parthenon::package::prelude; + +KOKKOS_INLINE_FUNCTION +Real OhmicDiffusivity::Get(const Real pres, const Real rho) const { + if (resistivity_coeff_type_ == ResistivityCoeff::fixed) { + return coeff_; + } else if (resistivity_coeff_type_ == ResistivityCoeff::spitzer) { + PARTHENON_FAIL("needs impl"); + } else { + PARTHENON_FAIL("Unknown Resistivity coeff"); + } +} + +Real EstimateResistivityTimestep(MeshData *md) { + // get to package via first block in Meshdata (which exists by construction) + auto hydro_pkg = md->GetBlockData(0)->GetBlockPointer()->packages.Get("Hydro"); + const auto &prim_pack = md->PackVariables(std::vector{"prim"}); + + IndexRange ib = md->GetBlockData(0)->GetBoundsI(IndexDomain::interior); + IndexRange jb = md->GetBlockData(0)->GetBoundsJ(IndexDomain::interior); + IndexRange kb = md->GetBlockData(0)->GetBoundsK(IndexDomain::interior); + + Real min_dt_resist = std::numeric_limits::max(); + const auto ndim = prim_pack.GetNdim(); + + Real fac = 0.5; + if (ndim == 2) { + fac = 0.25; + } else if (ndim == 3) { + fac = 1.0 / 6.0; + } + + const auto &ohm_diff = hydro_pkg->Param("ohm_diff"); + + if (ohm_diff.GetType() == Resistivity::ohmic && + ohm_diff.GetCoeffType() == ResistivityCoeff::fixed) { + // TODO(pgrete): once mindx is properly calculated before this loop, we can get rid of + // it entirely. + // Using 0.0 as parameters rho and p as they're not used anyway for a fixed coeff. + const auto ohm_diff_coeff = ohm_diff.Get(0.0, 0.0); + Kokkos::parallel_reduce( + "EstimateResistivityTimestep (ohmic fixed)", + Kokkos::MDRangePolicy>( + DevExecSpace(), {0, kb.s, jb.s, ib.s}, + {prim_pack.GetDim(5), kb.e + 1, jb.e + 1, ib.e + 1}, + {1, 1, 1, ib.e + 1 - ib.s}), + KOKKOS_LAMBDA(const int b, const int k, const int j, const int i, Real &min_dt) { + const auto &coords = prim_pack.GetCoords(b); + min_dt = + fmin(min_dt, SQR(coords.Dxc<1>(k, j, i)) / (ohm_diff_coeff + TINY_NUMBER)); + if (ndim >= 2) { + min_dt = fmin(min_dt, + SQR(coords.Dxc<2>(k, j, i)) / (ohm_diff_coeff + TINY_NUMBER)); + } + if (ndim >= 3) { + min_dt = fmin(min_dt, + SQR(coords.Dxc<3>(k, j, i)) / (ohm_diff_coeff + TINY_NUMBER)); + } + }, + Kokkos::Min(min_dt_resist)); + } else { + PARTHENON_THROW("Needs impl."); + } + + const auto &cfl_diff = hydro_pkg->Param("cfl_diff"); + return cfl_diff * fac * min_dt_resist; +} + +//--------------------------------------------------------------------------------------- +//! Calculate isotropic resistivity with fixed coefficient + +void OhmicDiffFluxIsoFixed(MeshData *md) { + auto pmb = md->GetBlockData(0)->GetBlockPointer(); + IndexRange ib = pmb->cellbounds.GetBoundsI(IndexDomain::interior); + IndexRange jb = pmb->cellbounds.GetBoundsJ(IndexDomain::interior); + IndexRange kb = pmb->cellbounds.GetBoundsK(IndexDomain::interior); + + std::vector flags_ind({Metadata::Independent}); + auto cons_pack = md->PackVariablesAndFluxes(flags_ind); + auto hydro_pkg = pmb->packages.Get("Hydro"); + + auto const &prim_pack = md->PackVariables(std::vector{"prim"}); + + const int ndim = pmb->pmy_mesh->ndim; + + const auto &ohm_diff = hydro_pkg->Param("ohm_diff"); + // Using fixed and uniform coefficient so it's safe to get it outside the kernel. + // Using 0.0 as parameters rho and p as they're not used anyway for a fixed coeff. + const auto eta = ohm_diff.Get(0.0, 0.0); + + parthenon::par_for( + DEFAULT_LOOP_PATTERN, "Resist. X1 fluxes (ohmic)", DevExecSpace(), 0, + cons_pack.GetDim(5) - 1, kb.s, kb.e, jb.s, jb.e, ib.s, ib.e + 1, + KOKKOS_LAMBDA(const int b, const int k, const int j, const int i) { + const auto &coords = prim_pack.GetCoords(b); + auto &cons = cons_pack(b); + const auto &prim = prim_pack(b); + + // Face centered current densities + // j2 = d3B1 - d1B3 + const auto d3B1 = + ndim > 2 ? (0.5 * (prim(IB1, k + 1, j, i - 1) + prim(IB1, k + 1, j, i)) - + 0.5 * (prim(IB1, k - 1, j, i - 1) + prim(IB1, k - 1, j, i))) / + (2.0 * coords.Dxf<3, 1>(k, j, i)) + : 0.0; + + const auto d1B3 = + (prim(IB3, k, j, i) - prim(IB3, k, j, i - 1)) / coords.Dxc<1>(k, j, i); + + const auto j2 = d3B1 - d1B3; + + // j3 = d1B2 - d2B1 + const auto d1B2 = + (prim(IB2, k, j, i) - prim(IB2, k, j, i - 1)) / coords.Dxc<1>(k, j, i); + + const auto d2B1 = + ndim > 1 ? (0.5 * (prim(IB1, k, j + 1, i - 1) + prim(IB1, k, j + 1, i)) - + 0.5 * (prim(IB1, k, j - 1, i - 1) + prim(IB1, k, j - 1, i))) / + (2.0 * coords.Dxf<2, 1>(k, j, i)) + : 0.0; + + const auto j3 = d1B2 - d2B1; + + cons.flux(X1DIR, IB2, k, j, i) += -eta * j3; + cons.flux(X1DIR, IB3, k, j, i) += eta * j2; + cons.flux(X1DIR, IEN, k, j, i) += + 0.5 * eta * + ((prim(IB3, k, j, i - 1) + prim(IB3, k, j, i)) * j2 - + (prim(IB2, k, j, i - 1) + prim(IB2, k, j, i)) * j3); + }); + + if (ndim < 2) { + return; + } + + parthenon::par_for( + DEFAULT_LOOP_PATTERN, "Resist. X2 fluxes (ohmic)", parthenon::DevExecSpace(), 0, + cons_pack.GetDim(5) - 1, kb.s, kb.e, jb.s, jb.e + 1, ib.s, ib.e, + KOKKOS_LAMBDA(const int b, const int k, const int j, const int i) { + const auto &coords = prim_pack.GetCoords(b); + auto &cons = cons_pack(b); + const auto &prim = prim_pack(b); + + // Face centered current densities + // j3 = d1B2 - d2B1 + const auto d1B2 = (0.5 * (prim(IB2, k, j - 1, i + 1) + prim(IB2, k, j, i + 1)) - + 0.5 * (prim(IB2, k, j - 1, i - 1) + prim(IB2, k, j, i - 1))) / + (2.0 * coords.Dxf<1, 2>(k, j, i)); + + const auto d2B1 = + (prim(IB1, k, j, i) - prim(IB1, k, j - 1, i)) / coords.Dxc<2>(k, j, i); + + const auto j3 = d1B2 - d2B1; + + // j1 = d2B3 - d3B2 + const auto d2B3 = + (prim(IB3, k, j, i) - prim(IB3, k, j - 1, i)) / coords.Dxc<2>(k, j, i); + + const auto d3B2 = + ndim > 2 ? (0.5 * (prim(IB2, k + 1, j - 1, i) + prim(IB2, k + 1, j, i)) - + 0.5 * (prim(IB2, k - 1, j - 1, i) + prim(IB2, k - 1, j, i))) / + (2.0 * coords.Dxf<3, 2>(k, j, i)) + : 0.0; + + const auto j1 = d2B3 - d3B2; + + cons.flux(X2DIR, IB1, k, j, i) += eta * j3; + cons.flux(X2DIR, IB3, k, j, i) += -eta * j1; + cons.flux(X2DIR, IEN, k, j, i) += + 0.5 * eta * + ((prim(IB1, k, j - 1, i) + prim(IB1, k, j, i)) * j3 - + (prim(IB3, k, j - 1, i) + prim(IB3, k, j, i)) * j1); + }); + + if (ndim < 3) { + return; + } + + parthenon::par_for( + DEFAULT_LOOP_PATTERN, "Resist. X3 fluxes (ohmic)", parthenon::DevExecSpace(), 0, + cons_pack.GetDim(5) - 1, kb.s, kb.e + 1, jb.s, jb.e, ib.s, ib.e, + KOKKOS_LAMBDA(const int b, const int k, const int j, const int i) { + const auto &coords = prim_pack.GetCoords(b); + auto &cons = cons_pack(b); + const auto &prim = prim_pack(b); + + // Face centered current densities + // j1 = d2B3 - d3B2 + const auto d2B3 = (0.5 * (prim(IB3, k - 1, j + 1, i) + prim(IB3, k, j + 1, i)) - + 0.5 * (prim(IB3, k - 1, j - 1, i) + prim(IB3, k, j - 1, i))) / + (2.0 * coords.Dxf<2, 3>(k, j, i)); + + const auto d3B2 = + (prim(IB2, k, j, i) - prim(IB2, k - 1, j, i)) / coords.Dxc<3>(k, j, i); + + const auto j1 = d2B3 - d3B2; + + // j2 = d3B1 - d1B3 + const auto d3B1 = + (prim(IB1, k, j, i) - prim(IB1, k - 1, j, i)) / coords.Dxc<3>(k, j, i); + + const auto d1B3 = (0.5 * (prim(IB3, k - 1, j, i + 1) + prim(IB3, k, j, i + 1)) - + 0.5 * (prim(IB3, k - 1, j, i - 1) + prim(IB3, k, j, i - 1))) / + (2.0 * coords.Dxf<1, 3>(k, j, i)); + + const auto j2 = d3B1 - d1B3; + + cons.flux(X3DIR, IB1, k, j, i) += -eta * j2; + cons.flux(X3DIR, IB2, k, j, i) += eta * j1; + cons.flux(X3DIR, IEN, k, j, i) += + 0.5 * eta * + ((prim(IB2, k - 1, j, i) + prim(IB2, k, j, i)) * j1 - + (prim(IB1, k - 1, j, i) + prim(IB1, k, j, i)) * j2); + }); +} + +//--------------------------------------------------------------------------------------- +//! TODO(pgrete) Calculate Ohmic diffusion, general case, e.g., with varying (Spitzer) +//! coefficient + +void OhmicDiffFluxGeneral(MeshData *md) { PARTHENON_THROW("Needs impl."); } \ No newline at end of file diff --git a/src/hydro/diffusion/viscosity.cpp b/src/hydro/diffusion/viscosity.cpp new file mode 100644 index 00000000..f222c869 --- /dev/null +++ b/src/hydro/diffusion/viscosity.cpp @@ -0,0 +1,295 @@ +//======================================================================================== +// AthenaPK - a performance portable block structured AMR astrophysical MHD code. +// Copyright (c) 2021-2024, Athena-Parthenon Collaboration. All rights reserved. +// Licensed under the 3-clause BSD License, see LICENSE file for details +//======================================================================================== +// AthenaXXX astrophysical plasma code +// Copyright(C) 2020 James M. Stone and the Athena code team +// Licensed under the 3-clause BSD License (the "LICENSE") +//======================================================================================== +//! \file viscosity.cpp +//! \brief + +// Parthenon headers +#include +#include + +// AthenaPK headers +#include "../../main.hpp" +#include "config.hpp" +#include "diffusion.hpp" +#include "kokkos_abstraction.hpp" +#include "utils/error_checking.hpp" + +using namespace parthenon::package::prelude; + +KOKKOS_INLINE_FUNCTION +Real MomentumDiffusivity::Get(const Real pres, const Real rho) const { + if (viscosity_coeff_type_ == ViscosityCoeff::fixed) { + return coeff_; + } else { + PARTHENON_FAIL("Unknown viscosity coeff"); + } +} + +Real EstimateViscosityTimestep(MeshData *md) { + // get to package via first block in Meshdata (which exists by construction) + auto hydro_pkg = md->GetBlockData(0)->GetBlockPointer()->packages.Get("Hydro"); + const auto &prim_pack = md->PackVariables(std::vector{"prim"}); + + IndexRange ib = md->GetBlockData(0)->GetBoundsI(IndexDomain::interior); + IndexRange jb = md->GetBlockData(0)->GetBoundsJ(IndexDomain::interior); + IndexRange kb = md->GetBlockData(0)->GetBoundsK(IndexDomain::interior); + + Real min_dt_visc = std::numeric_limits::max(); + const auto ndim = prim_pack.GetNdim(); + + Real fac = 0.5; + if (ndim == 2) { + fac = 0.25; + } else if (ndim == 3) { + fac = 1.0 / 6.0; + } + + const auto gm1 = hydro_pkg->Param("AdiabaticIndex"); + const auto &mom_diff = hydro_pkg->Param("mom_diff"); + + if (mom_diff.GetType() == Viscosity::isotropic && + mom_diff.GetCoeffType() == ViscosityCoeff::fixed) { + // TODO(pgrete): once mindx is properly calculated before this loop, we can get rid of + // it entirely. + // Using 0.0 as parameters rho and p as they're not used anyway for a fixed coeff. + const auto mom_diff_coeff = mom_diff.Get(0.0, 0.0); + Kokkos::parallel_reduce( + "EstimateViscosityTimestep (iso fixed)", + Kokkos::MDRangePolicy>( + DevExecSpace(), {0, kb.s, jb.s, ib.s}, + {prim_pack.GetDim(5), kb.e + 1, jb.e + 1, ib.e + 1}, + {1, 1, 1, ib.e + 1 - ib.s}), + KOKKOS_LAMBDA(const int b, const int k, const int j, const int i, Real &min_dt) { + const auto &coords = prim_pack.GetCoords(b); + min_dt = + fmin(min_dt, SQR(coords.Dxc<1>(k, j, i)) / (mom_diff_coeff + TINY_NUMBER)); + if (ndim >= 2) { + min_dt = fmin(min_dt, + SQR(coords.Dxc<2>(k, j, i)) / (mom_diff_coeff + TINY_NUMBER)); + } + if (ndim >= 3) { + min_dt = fmin(min_dt, + SQR(coords.Dxc<3>(k, j, i)) / (mom_diff_coeff + TINY_NUMBER)); + } + }, + Kokkos::Min(min_dt_visc)); + } else { + PARTHENON_THROW("Needs impl."); + } + + const auto &cfl_diff = hydro_pkg->Param("cfl_diff"); + return cfl_diff * fac * min_dt_visc; +} + +//--------------------------------------------------------------------------------------- +//! Calculate isotropic viscosity with fixed coefficient + +void MomentumDiffFluxIsoFixed(MeshData *md) { + auto pmb = md->GetBlockData(0)->GetBlockPointer(); + IndexRange ib = pmb->cellbounds.GetBoundsI(IndexDomain::interior); + IndexRange jb = pmb->cellbounds.GetBoundsJ(IndexDomain::interior); + IndexRange kb = pmb->cellbounds.GetBoundsK(IndexDomain::interior); + + std::vector flags_ind({Metadata::Independent}); + auto cons_pack = md->PackVariablesAndFluxes(flags_ind); + auto hydro_pkg = pmb->packages.Get("Hydro"); + + auto const &prim_pack = md->PackVariables(std::vector{"prim"}); + + const int ndim = pmb->pmy_mesh->ndim; + + const auto &mom_diff = hydro_pkg->Param("mom_diff"); + // Using fixed and uniform coefficient so it's safe to get it outside the kernel. + // Using 0.0 as parameters rho and p as they're not used anyway for a fixed coeff. + const auto nu = mom_diff.Get(0.0, 0.0); + + const int scratch_level = + hydro_pkg->Param("scratch_level"); // 0 is actual scratch (tiny); 1 is HBM + const int nx1 = pmb->cellbounds.ncellsi(IndexDomain::entire); + + size_t scratch_size_in_bytes = parthenon::ScratchPad1D::shmem_size(nx1) * 3; + + parthenon::par_for_outer( + DEFAULT_OUTER_LOOP_PATTERN, "Visc. X1 fluxes (iso)", DevExecSpace(), + scratch_size_in_bytes, scratch_level, 0, cons_pack.GetDim(5) - 1, kb.s, kb.e, jb.s, + jb.e, + KOKKOS_LAMBDA(parthenon::team_mbr_t member, const int b, const int k, const int j) { + const auto &coords = prim_pack.GetCoords(b); + auto &cons = cons_pack(b); + const auto &prim = prim_pack(b); + parthenon::ScratchPad1D fvx(member.team_scratch(scratch_level), nx1); + parthenon::ScratchPad1D fvy(member.team_scratch(scratch_level), nx1); + parthenon::ScratchPad1D fvz(member.team_scratch(scratch_level), nx1); + + // Add [2(dVx/dx)-(2/3)dVx/dx, dVy/dx, dVz/dx] + par_for_inner(member, ib.s, ib.e + 1, [&](const int i) { + fvx(i) = 4.0 * (prim(IV1, k, j, i) - prim(IV1, k, j, i - 1)) / + (3.0 * coords.Dxc<1>(i)); + fvy(i) = (prim(IV2, k, j, i) - prim(IV2, k, j, i - 1)) / coords.Dxc<1>(i); + fvz(i) = (prim(IV3, k, j, i) - prim(IV3, k, j, i - 1)) / coords.Dxc<1>(i); + }); + member.team_barrier(); + + // In 2D/3D Add [(-2/3)dVy/dy, dVx/dy, 0] + if (ndim > 1) { + par_for_inner(member, ib.s, ib.e + 1, [&](const int i) { + fvx(i) -= ((prim(IV2, k, j + 1, i) + prim(IV2, k, j + 1, i - 1)) - + (prim(IV2, k, j - 1, i) + prim(IV2, k, j - 1, i - 1))) / + (6.0 * coords.Dxc<2>(j)); + fvy(i) += ((prim(IV1, k, j + 1, i) + prim(IV1, k, j + 1, i - 1)) - + (prim(IV1, k, j - 1, i) + prim(IV1, k, j - 1, i - 1))) / + (4.0 * coords.Dxc<2>(j)); + }); + member.team_barrier(); + } + + // In 3D Add [(-2/3)dVz/dz, 0, dVx/dz] + if (ndim > 2) { + par_for_inner(member, ib.s, ib.e + 1, [&](const int i) { + fvx(i) -= ((prim(IV3, k + 1, j, i) + prim(IV3, k + 1, j, i - 1)) - + (prim(IV3, k - 1, j, i) + prim(IV3, k - 1, j, i - 1))) / + (6.0 * coords.Dxc<3>(k)); + fvz(i) += ((prim(IV1, k + 1, j, i) + prim(IV1, k + 1, j, i - 1)) - + (prim(IV1, k - 1, j, i) + prim(IV1, k - 1, j, i - 1))) / + (4.0 * coords.Dxc<3>(k)); + }); + member.team_barrier(); + } + + // Sum viscous fluxes into fluxes of conserved variables; including energy fluxes + par_for_inner(member, ib.s, ib.e + 1, [&](const int i) { + Real nud = 0.5 * nu * (prim(IDN, k, j, i) + prim(IDN, k, j, i - 1)); + cons.flux(X1DIR, IV1, k, j, i) -= nud * fvx(i); + cons.flux(X1DIR, IV2, k, j, i) -= nud * fvy(i); + cons.flux(X1DIR, IV3, k, j, i) -= nud * fvz(i); + cons.flux(X1DIR, IEN, k, j, i) -= + 0.5 * nud * + ((prim(IV1, k, j, i - 1) + prim(IV1, k, j, i)) * fvx(i) + + (prim(IV2, k, j, i - 1) + prim(IV2, k, j, i)) * fvy(i) + + (prim(IV3, k, j, i - 1) + prim(IV3, k, j, i)) * fvz(i)); + }); + }); + + if (ndim < 2) { + return; + } + /* Compute viscous fluxes in 2-direction --------------------------------------*/ + parthenon::par_for_outer( + DEFAULT_OUTER_LOOP_PATTERN, "Visc. X2 fluxes (iso)", parthenon::DevExecSpace(), + scratch_size_in_bytes, scratch_level, 0, cons_pack.GetDim(5) - 1, kb.s, kb.e, jb.s, + jb.e + 1, + KOKKOS_LAMBDA(parthenon::team_mbr_t member, const int b, const int k, const int j) { + const auto &coords = prim_pack.GetCoords(b); + auto &cons = cons_pack(b); + const auto &prim = prim_pack(b); + parthenon::ScratchPad1D fvx(member.team_scratch(scratch_level), nx1); + parthenon::ScratchPad1D fvy(member.team_scratch(scratch_level), nx1); + parthenon::ScratchPad1D fvz(member.team_scratch(scratch_level), nx1); + + // Add [(dVx/dy+dVy/dx), 2(dVy/dy)-(2/3)(dVx/dx+dVy/dy), dVz/dy] + par_for_inner(member, ib.s, ib.e, [&](const int i) { + fvx(i) = (prim(IV1, k, j, i) - prim(IV1, k, j - 1, i)) / coords.Dxc<2>(j) + + ((prim(IV2, k, j, i + 1) + prim(IV2, k, j - 1, i + 1)) - + (prim(IV2, k, j, i - 1) + prim(IV2, k, j - 1, i - 1))) / + (4.0 * coords.Dxc<1>(i)); + fvy(i) = (prim(IV2, k, j, i) - prim(IV2, k, j - 1, i)) * 4.0 / + (3.0 * coords.Dxc<2>(j)) - + ((prim(IV1, k, j, i + 1) + prim(IV1, k, j - 1, i + 1)) - + (prim(IV1, k, j, i - 1) + prim(IV1, k, j - 1, i - 1))) / + (6.0 * coords.Dxc<1>(i)); + fvz(i) = (prim(IV3, k, j, i) - prim(IV3, k, j - 1, i)) / coords.Dxc<2>(j); + }); + member.team_barrier(); + + // In 3D Add [0, (-2/3)dVz/dz, dVy/dz] + if (ndim > 2) { + par_for_inner(member, ib.s, ib.e, [&](const int i) { + fvy(i) -= ((prim(IV3, k + 1, j, i) + prim(IV3, k + 1, j - 1, i)) - + (prim(IV3, k - 1, j, i) + prim(IV3, k - 1, j - 1, i))) / + (6.0 * coords.Dxc<3>(k)); + fvz(i) += ((prim(IV2, k + 1, j, i) + prim(IV2, k + 1, j - 1, i)) - + (prim(IV2, k - 1, j, i) + prim(IV2, k - 1, j - 1, i))) / + (4.0 * coords.Dxc<3>(k)); + }); + member.team_barrier(); + } + + // Sum viscous fluxes into fluxes of conserved variables; including energy fluxes + par_for_inner(member, ib.s, ib.e, [&](const int i) { + Real nud = 0.5 * nu * (prim(IDN, k, j, i) + prim(IDN, k, j - 1, i)); + cons.flux(X2DIR, IV1, k, j, i) -= nud * fvx(i); + cons.flux(X2DIR, IV2, k, j, i) -= nud * fvy(i); + cons.flux(X2DIR, IV3, k, j, i) -= nud * fvz(i); + cons.flux(X2DIR, IEN, k, j, i) -= + 0.5 * nud * + ((prim(IV1, k, j - 1, i) + prim(IV1, k, j, i)) * fvx(i) + + (prim(IV2, k, j - 1, i) + prim(IV2, k, j, i)) * fvy(i) + + (prim(IV3, k, j - 1, i) + prim(IV3, k, j, i)) * fvz(i)); + }); + }); + /* Compute heat fluxes in 3-direction, 3D problem ONLY ---------------------*/ + if (ndim < 3) { + return; + } + + parthenon::par_for_outer( + DEFAULT_OUTER_LOOP_PATTERN, "Visc. X3 fluxes (iso)", parthenon::DevExecSpace(), + scratch_size_in_bytes, scratch_level, 0, cons_pack.GetDim(5) - 1, kb.s, kb.e + 1, + jb.s, jb.e, + KOKKOS_LAMBDA(parthenon::team_mbr_t member, const int b, const int k, const int j) { + const auto &coords = prim_pack.GetCoords(b); + auto &cons = cons_pack(b); + const auto &prim = prim_pack(b); + + parthenon::ScratchPad1D fvx(member.team_scratch(scratch_level), nx1); + parthenon::ScratchPad1D fvy(member.team_scratch(scratch_level), nx1); + parthenon::ScratchPad1D fvz(member.team_scratch(scratch_level), nx1); + + // Add [(dVx/dz+dVz/dx), (dVy/dz+dVz/dy), 2(dVz/dz)-(2/3)(dVx/dx+dVy/dy+dVz/dz)] + par_for_inner(member, ib.s, ib.e, [&](const int i) { + fvx(i) = (prim(IV1, k, j, i) - prim(IV1, k - 1, j, i)) / coords.Dxc<3>(k) + + ((prim(IV3, k, j, i + 1) + prim(IV3, k - 1, j, i + 1)) - + (prim(IV3, k, j, i - 1) + prim(IV3, k - 1, j, i - 1))) / + (4.0 * coords.Dxc<1>(i)); + fvy(i) = (prim(IV2, k, j, i) - prim(IV2, k - 1, j, i)) / coords.Dxc<3>(k) + + ((prim(IV3, k, j + 1, i) + prim(IV3, k - 1, j + 1, i)) - + (prim(IV3, k, j - 1, i) + prim(IV3, k - 1, j - 1, i))) / + (4.0 * coords.Dxc<2>(j)); + fvz(i) = (prim(IV3, k, j, i) - prim(IV3, k - 1, j, i)) * 4.0 / + (3.0 * coords.Dxc<3>(k)) - + ((prim(IV1, k, j, i + 1) + prim(IV1, k - 1, j, i + 1)) - + (prim(IV1, k, j, i - 1) + prim(IV1, k - 1, j, i - 1))) / + (6.0 * coords.Dxc<1>(i)) - + ((prim(IV2, k, j + 1, i) + prim(IV2, k - 1, j + 1, i)) - + (prim(IV2, k, j - 1, i) + prim(IV2, k - 1, j - 1, i))) / + (6.0 * coords.Dxc<2>(j)); + }); + member.team_barrier(); + + // Sum viscous fluxes into fluxes of conserved variables; including energy fluxes + par_for_inner(member, ib.s, ib.e, [&](const int i) { + Real nud = 0.5 * nu * (prim(IDN, k, j, i) + prim(IDN, k - 1, j, i)); + cons.flux(X3DIR, IV1, k, j, i) -= nud * fvx(i); + cons.flux(X3DIR, IV2, k, j, i) -= nud * fvy(i); + cons.flux(X3DIR, IV3, k, j, i) -= nud * fvz(i); + cons.flux(X3DIR, IEN, k, j, i) -= + 0.5 * nud * + ((prim(IV1, k - 1, j, i) + prim(IV1, k, j, i)) * fvx(i) + + (prim(IV2, k - 1, j, i) + prim(IV2, k, j, i)) * fvy(i) + + (prim(IV3, k - 1, j, i) + prim(IV3, k, j, i)) * fvz(i)); + }); + }); +} + +//--------------------------------------------------------------------------------------- +//! TODO(pgrete) Calculate momentum diffusion, general case, i.e., anisotropic and/or with +//! varying coefficient + +void MomentumDiffFluxGeneral(MeshData *md) { PARTHENON_THROW("Needs impl."); } diff --git a/src/hydro/hydro.cpp b/src/hydro/hydro.cpp index 54a97b7f..7bc95be7 100644 --- a/src/hydro/hydro.cpp +++ b/src/hydro/hydro.cpp @@ -64,13 +64,28 @@ void PreStepMeshUserWorkInLoop(Mesh *pmesh, ParameterInput *pin, SimTime &tm) { auto hydro_pkg = pmesh->block_list[0]->packages.Get("Hydro"); const auto num_partitions = pmesh->DefaultNumPartitions(); - if ((hydro_pkg->Param("diffint") == DiffInt::rkl2) && - (hydro_pkg->Param("conduction") != Conduction::none)) { + if (hydro_pkg->Param("diffint") == DiffInt::rkl2) { auto dt_diff = std::numeric_limits::max(); - for (auto i = 0; i < num_partitions; i++) { - auto &md = pmesh->mesh_data.GetOrAdd("base", i); + if (hydro_pkg->Param("conduction") != Conduction::none) { + for (auto i = 0; i < num_partitions; i++) { + auto &md = pmesh->mesh_data.GetOrAdd("base", i); - dt_diff = std::min(dt_diff, EstimateConductionTimestep(md.get())); + dt_diff = std::min(dt_diff, EstimateConductionTimestep(md.get())); + } + } + if (hydro_pkg->Param("viscosity") != Viscosity::none) { + for (auto i = 0; i < num_partitions; i++) { + auto &md = pmesh->mesh_data.GetOrAdd("base", i); + + dt_diff = std::min(dt_diff, EstimateViscosityTimestep(md.get())); + } + } + if (hydro_pkg->Param("resistivity") != Resistivity::none) { + for (auto i = 0; i < num_partitions; i++) { + auto &md = pmesh->mesh_data.GetOrAdd("base", i); + + dt_diff = std::min(dt_diff, EstimateResistivityTimestep(md.get())); + } } #ifdef MPI_PARALLEL PARTHENON_MPI_CHECK(MPI_Allreduce(MPI_IN_PLACE, &dt_diff, 1, MPI_PARTHENON_REAL, @@ -544,6 +559,67 @@ std::shared_ptr Initialize(ParameterInput *pin) { } pkg->AddParam<>("conduction", conduction); + auto viscosity = Viscosity::none; + auto viscosity_str = pin->GetOrAddString("diffusion", "viscosity", "none"); + if (viscosity_str == "isotropic") { + viscosity = Viscosity::isotropic; + } else if (viscosity_str != "none") { + PARTHENON_FAIL("Unknown viscosity method. Options are: none, isotropic"); + } + // If viscosity is enabled, process supported coefficients + if (viscosity != Viscosity::none) { + auto viscosity_coeff_str = + pin->GetOrAddString("diffusion", "viscosity_coeff", "none"); + auto viscosity_coeff = ViscosityCoeff::none; + + if (viscosity_coeff_str == "fixed") { + viscosity_coeff = ViscosityCoeff::fixed; + Real mom_diff_coeff_code = pin->GetReal("diffusion", "mom_diff_coeff_code"); + auto mom_diff = MomentumDiffusivity(viscosity, viscosity_coeff, + mom_diff_coeff_code, 0.0, 0.0, 0.0); + pkg->AddParam<>("mom_diff", mom_diff); + + } else { + PARTHENON_FAIL("Viscosity is enabled but no coefficient is set. Please " + "set diffusion/viscosity_coeff to 'fixed' and " + "diffusion/mom_diff_coeff_code to the desired value."); + } + } + pkg->AddParam<>("viscosity", viscosity); + + auto resistivity = Resistivity::none; + auto resistivity_str = pin->GetOrAddString("diffusion", "resistivity", "none"); + if (resistivity_str == "ohmic") { + resistivity = Resistivity::ohmic; + } else if (resistivity_str != "none") { + PARTHENON_FAIL("Unknown resistivity method. Options are: none, ohmic"); + } + // If resistivity is enabled, process supported coefficients + if (resistivity != Resistivity::none) { + auto resistivity_coeff_str = + pin->GetOrAddString("diffusion", "resistivity_coeff", "none"); + auto resistivity_coeff = ResistivityCoeff::none; + + if (resistivity_coeff_str == "spitzer") { + // If this is implemented, check how the Spitzer coeff for thermal conduction is + // handled. + PARTHENON_FAIL("needs impl"); + + } else if (resistivity_coeff_str == "fixed") { + resistivity_coeff = ResistivityCoeff::fixed; + Real ohm_diff_coeff_code = pin->GetReal("diffusion", "ohm_diff_coeff_code"); + auto ohm_diff = OhmicDiffusivity(resistivity, resistivity_coeff, + ohm_diff_coeff_code, 0.0, 0.0, 0.0); + pkg->AddParam<>("ohm_diff", ohm_diff); + + } else { + PARTHENON_FAIL("Resistivity is enabled but no coefficient is set. Please " + "set diffusion/resistivity_coeff to 'fixed' and " + "diffusion/ohm_diff_coeff_code to the desired value."); + } + } + pkg->AddParam<>("resistivity", resistivity); + auto diffint_str = pin->GetOrAddString("diffusion", "integrator", "none"); auto diffint = DiffInt::none; if (diffint_str == "unsplit") { @@ -558,6 +634,10 @@ std::shared_ptr Initialize(ParameterInput *pin) { } if (diffint != DiffInt::none) { pkg->AddParam("dt_diff", 0.0, true); // diffusive timestep constraint + // As in Athena++ a cfl safety factor is also applied to the theoretical limit. + // By default it is equal to the hyperbolic cfl. + auto cfl_diff = pin->GetOrAddReal("diffusion", "cfl", pkg->Param("cfl")); + pkg->AddParam<>("cfl_diff", cfl_diff); } pkg->AddParam<>("diffint", diffint); @@ -790,9 +870,16 @@ Real EstimateTimestep(MeshData *md) { } // For RKL2 STS, the diffusive timestep is calculated separately in the driver - if ((hydro_pkg->Param("diffint") == DiffInt::unsplit) && - (hydro_pkg->Param("conduction") != Conduction::none)) { - min_dt = std::min(min_dt, EstimateConductionTimestep(md)); + if (hydro_pkg->Param("diffint") == DiffInt::unsplit) { + if (hydro_pkg->Param("conduction") != Conduction::none) { + min_dt = std::min(min_dt, EstimateConductionTimestep(md)); + } + if (hydro_pkg->Param("viscosity") != Viscosity::none) { + min_dt = std::min(min_dt, EstimateViscosityTimestep(md)); + } + if (hydro_pkg->Param("resistivity") != Resistivity::none) { + min_dt = std::min(min_dt, EstimateResistivityTimestep(md)); + } } if (ProblemEstimateTimestep != nullptr) { diff --git a/src/main.cpp b/src/main.cpp index 93f984bb..fa0c4dec 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -55,6 +55,7 @@ int main(int argc, char *argv[]) { pman.app_input->InitUserMeshData = linear_wave_mhd::InitUserMeshData; pman.app_input->ProblemGenerator = linear_wave_mhd::ProblemGenerator; pman.app_input->UserWorkAfterLoop = linear_wave_mhd::UserWorkAfterLoop; + Hydro::ProblemInitPackageData = linear_wave_mhd::ProblemInitPackageData; } else if (problem == "cpaw") { pman.app_input->InitUserMeshData = cpaw::InitUserMeshData; pman.app_input->ProblemGenerator = cpaw::ProblemGenerator; diff --git a/src/main.hpp b/src/main.hpp index 033118ad..4c81c307 100644 --- a/src/main.hpp +++ b/src/main.hpp @@ -1,5 +1,5 @@ // AthenaPK - a performance portable block structured AMR MHD code -// Copyright (c) 2020-2021, Athena Parthenon Collaboration. All rights reserved. +// Copyright (c) 2020-2024, Athena Parthenon Collaboration. All rights reserved. // Licensed under the 3-Clause License (the "LICENSE") #ifndef MAIN_HPP_ @@ -37,6 +37,10 @@ enum class Fluid { undefined, euler, glmmhd }; enum class Cooling { none, tabular }; enum class Conduction { none, isotropic, anisotropic }; enum class ConductionCoeff { none, fixed, spitzer }; +enum class Viscosity { none, isotropic }; +enum class ViscosityCoeff { none, fixed }; +enum class Resistivity { none, ohmic }; +enum class ResistivityCoeff { none, fixed, spitzer }; enum class DiffInt { none, unsplit, rkl2 }; enum class Hst { idx, ekin, emag, divb }; diff --git a/src/pgen/diffusion.cpp b/src/pgen/diffusion.cpp index 24ae60f2..269926f8 100644 --- a/src/pgen/diffusion.cpp +++ b/src/pgen/diffusion.cpp @@ -1,6 +1,6 @@ // AthenaPK - a performance portable block structured AMR MHD code -// Copyright (c) 2021-2023, Athena Parthenon Collaboration. All rights reserved. +// Copyright (c) 2021-2024, Athena Parthenon Collaboration. All rights reserved. // Licensed under the 3-Clause License (the "LICENSE") // Parthenon headers @@ -11,11 +11,13 @@ // AthenaPK headers #include "../main.hpp" +#include "utils/error_checking.hpp" namespace diffusion { using namespace parthenon::driver::prelude; void ProblemGenerator(MeshBlock *pmb, ParameterInput *pin) { + auto hydro_pkg = pmb->packages.Get("Hydro"); IndexRange ib = pmb->cellbounds.GetBoundsI(IndexDomain::interior); IndexRange jb = pmb->cellbounds.GetBoundsJ(IndexDomain::interior); IndexRange kb = pmb->cellbounds.GetBoundsK(IndexDomain::interior); @@ -23,24 +25,39 @@ void ProblemGenerator(MeshBlock *pmb, ParameterInput *pin) { auto &mbd = pmb->meshblock_data.Get(); auto &u = mbd->Get("cons").data; + const auto gamma = pin->GetReal("hydro", "gamma"); + const bool mhd_enabled = hydro_pkg->Param("fluid") == Fluid::glmmhd; + const auto Bx = pin->GetOrAddReal("problem/diffusion", "Bx", 0.0); const auto By = pin->GetOrAddReal("problem/diffusion", "By", 0.0); const auto iprob = pin->GetInteger("problem/diffusion", "iprob"); + PARTHENON_REQUIRE_THROWS(mhd_enabled || !(iprob == 0 || iprob == 1 || iprob == 2 || + iprob == 10 || iprob == 20 || iprob == 40), + "Selected iprob for diffusion pgen requires MHD enabled.") Real t0 = 0.5; Real diff_coeff = 0.0; Real amp = 1e-6; - // Get parameters for Gaussian profile - if (iprob == 10) { - diff_coeff = pin->GetReal("diffusion", "thermal_diff_coeff_code"); + // Get common parameters for Gaussian profile + if ((iprob == 10) || (iprob == 30) || (iprob == 40)) { t0 = pin->GetOrAddReal("problem/diffusion", "t0", t0); amp = pin->GetOrAddReal("problem/diffusion", "amp", amp); } + // Heat diffusion of 1D Gaussian + if (iprob == 10) { + diff_coeff = pin->GetReal("diffusion", "thermal_diff_coeff_code"); + // Viscous diffusion of 1D Gaussian + } else if (iprob == 30) { + diff_coeff = pin->GetReal("diffusion", "mom_diff_coeff_code"); + // Ohmic diffusion of 1D Gaussian + } else if (iprob == 40) { + diff_coeff = pin->GetReal("diffusion", "ohm_diff_coeff_code"); + } auto &coords = pmb->coords; pmb->par_for( - "ProblemGenerator: Diffusion Step", kb.s, kb.e, jb.s, jb.e, ib.s, ib.e, + "ProblemGenerator: Diffusion", kb.s, kb.e, jb.s, jb.e, ib.s, ib.e, KOKKOS_LAMBDA(const int k, const int j, const int i) { u(IDN, k, j, i) = 1.0; @@ -48,11 +65,13 @@ void ProblemGenerator(MeshBlock *pmb, ParameterInput *pin) { u(IM2, k, j, i) = 0.0; u(IM3, k, j, i) = 0.0; - u(IB1, k, j, i) = 0.0; - u(IB2, k, j, i) = 0.0; - u(IB3, k, j, i) = 0.0; + if (mhd_enabled) { + u(IB1, k, j, i) = 0.0; + u(IB2, k, j, i) = 0.0; + u(IB3, k, j, i) = 0.0; + } - Real eint; + Real eint = -1.0; // step function x1 if (iprob == 0) { u(IB1, k, j, i) = Bx; @@ -111,12 +130,31 @@ void ProblemGenerator(MeshBlock *pmb, ParameterInput *pin) { u(IB3, k, j, i) = y / r; u(IB1, k, j, i) = -x / r; eint = std::abs(r - 0.6) < 0.1 && std::abs(phi) < M_PI / 12.0 ? 12.0 : 10.0; + // Viscous diffusion of 1D Gaussian + } else if (iprob == 30) { + u(IM2, k, j, i) = + u(IDN, k, j, i) * amp / + std::pow(std::sqrt(4. * M_PI * diff_coeff * t0), 1.0) * + std::exp(-(std::pow(coords.Xc<1>(i), 2.)) / (4. * diff_coeff * t0)); + eint = 1.0 / (gamma * (gamma - 1.0)); // c_s = 1 everywhere + // Ohmic diffusion of 1D Gaussian + } else if (iprob == 40) { + u(IB2, k, j, i) = + amp / std::pow(std::sqrt(4. * M_PI * diff_coeff * t0), 1.0) * + std::exp(-(std::pow(coords.Xc<1>(i), 2.)) / (4. * diff_coeff * t0)); + eint = 1.0 / (gamma * (gamma - 1.0)); // c_s = 1 everywhere } + + PARTHENON_REQUIRE(eint > 0.0, "Missing init of eint"); u(IEN, k, j, i) = u(IDN, k, j, i) * eint + - 0.5 * (SQR(u(IB1, k, j, i)) + SQR(u(IB2, k, j, i)) + SQR(u(IB3, k, j, i)) + - (SQR(u(IM1, k, j, i)) + SQR(u(IM2, k, j, i)) + SQR(u(IM3, k, j, i))) / - u(IDN, k, j, i)); + 0.5 * ((SQR(u(IM1, k, j, i)) + SQR(u(IM2, k, j, i)) + SQR(u(IM3, k, j, i))) / + u(IDN, k, j, i)); + + if (mhd_enabled) { + u(IEN, k, j, i) += + 0.5 * (SQR(u(IB1, k, j, i)) + SQR(u(IB2, k, j, i)) + SQR(u(IB3, k, j, i))); + } }); } } // namespace diffusion diff --git a/src/pgen/linear_wave_mhd.cpp b/src/pgen/linear_wave_mhd.cpp index 67affbe7..6e2c105c 100644 --- a/src/pgen/linear_wave_mhd.cpp +++ b/src/pgen/linear_wave_mhd.cpp @@ -709,4 +709,38 @@ void Eigensystem(const Real d, const Real v1, const Real v2, const Real v3, cons left_eigenmatrix[6][6] = left_eigenmatrix[0][6]; } +// For decaying wave with diffusive processes test problem, dump max V_2 +Real HstMaxV2(MeshData *md) { + auto hydro_pkg = md->GetBlockData(0)->GetBlockPointer()->packages.Get("Hydro"); + + const auto &prim_pack = md->PackVariables(std::vector{"prim"}); + + IndexRange ib = md->GetBlockData(0)->GetBoundsI(IndexDomain::interior); + IndexRange jb = md->GetBlockData(0)->GetBoundsJ(IndexDomain::interior); + IndexRange kb = md->GetBlockData(0)->GetBoundsK(IndexDomain::interior); + + Real max_v2 = 0.0; + + Kokkos::parallel_reduce( + "HstMaxV2", + Kokkos::MDRangePolicy>( + parthenon::DevExecSpace(), {0, kb.s, jb.s, ib.s}, + {prim_pack.GetDim(5), kb.e + 1, jb.e + 1, ib.e + 1}, + {1, 1, 1, ib.e + 1 - ib.s}), + KOKKOS_LAMBDA(const int b, const int k, const int j, const int i, Real &lmax) { + lmax = Kokkos::fmax(lmax, Kokkos::fabs(prim_pack(b, IV2, k, j, i))); + }, + Kokkos::Max(max_v2)); + + return max_v2; +} + +void ProblemInitPackageData(ParameterInput *pin, parthenon::StateDescriptor *pkg) { + if (pin->GetOrAddBoolean("problem/linear_wave", "dump_max_v2", false)) { + auto hst_vars = pkg->Param(parthenon::hist_param_key); + hst_vars.emplace_back(parthenon::HistoryOutputVar( + parthenon::UserHistoryOperation::max, HstMaxV2, "MaxAbsV2")); + pkg->UpdateParam(parthenon::hist_param_key, hst_vars); + } +} } // namespace linear_wave_mhd diff --git a/src/pgen/pgen.hpp b/src/pgen/pgen.hpp index b3e55aad..ddea80f3 100644 --- a/src/pgen/pgen.hpp +++ b/src/pgen/pgen.hpp @@ -25,6 +25,7 @@ void InitUserMeshData(Mesh *mesh, ParameterInput *pin); void ProblemGenerator(MeshBlock *pmb, parthenon::ParameterInput *pin); void UserWorkAfterLoop(Mesh *mesh, parthenon::ParameterInput *pin, parthenon::SimTime &tm); +void ProblemInitPackageData(ParameterInput *pin, parthenon::StateDescriptor *pkg); } // namespace linear_wave_mhd namespace cpaw { diff --git a/tst/regression/CMakeLists.txt b/tst/regression/CMakeLists.txt index c43b4de5..f4fa246e 100644 --- a/tst/regression/CMakeLists.txt +++ b/tst/regression/CMakeLists.txt @@ -49,6 +49,12 @@ setup_test_both("aniso_therm_cond_ring_multid" "--driver ${PROJECT_BINARY_DIR}/b setup_test_both("aniso_therm_cond_gauss_conv" "--driver ${PROJECT_BINARY_DIR}/bin/athenaPK \ --driver_input ${PROJECT_SOURCE_DIR}/inputs/diffusion.in --num_steps 24" "convergence") +setup_test_both("diffusion" "--driver ${PROJECT_BINARY_DIR}/bin/athenaPK \ + --driver_input ${PROJECT_SOURCE_DIR}/inputs/diffusion.in --num_steps 12" "convergence") + + setup_test_both("diffusion_linwave3d" "--driver ${PROJECT_BINARY_DIR}/bin/athenaPK \ + --driver_input ${PROJECT_SOURCE_DIR}/inputs/linear_wave3d.in --num_steps 2" "convergence") + setup_test_both("field_loop" "--driver ${PROJECT_BINARY_DIR}/bin/athenaPK \ --driver_input ${PROJECT_SOURCE_DIR}/inputs/field_loop.in --num_steps 12" "convergence") diff --git a/tst/regression/test_suites/aniso_therm_cond_ring_conv/aniso_therm_cond_ring_conv.py b/tst/regression/test_suites/aniso_therm_cond_ring_conv/aniso_therm_cond_ring_conv.py index ba13129e..41a2dcc6 100644 --- a/tst/regression/test_suites/aniso_therm_cond_ring_conv/aniso_therm_cond_ring_conv.py +++ b/tst/regression/test_suites/aniso_therm_cond_ring_conv/aniso_therm_cond_ring_conv.py @@ -53,6 +53,10 @@ def Prepare(self, parameters, step): "parthenon/meshblock/nx3=1", "problem/diffusion/iprob=20", "parthenon/time/tlim=200.0", + # Work around for RKL2 integrator (that, by default, does not limit the + # timestep, which in newer versions of Parthenon results in triggering + # a fail-safe given the default init value of numeric_limits max. + "parthenon/time/dt_ceil=200.0", "parthenon/output0/dt=200.0", f"parthenon/output0/id={res}", ] diff --git a/tst/regression/test_suites/aniso_therm_cond_ring_multid/aniso_therm_cond_ring_multid.py b/tst/regression/test_suites/aniso_therm_cond_ring_multid/aniso_therm_cond_ring_multid.py index 3f933507..74083a63 100644 --- a/tst/regression/test_suites/aniso_therm_cond_ring_multid/aniso_therm_cond_ring_multid.py +++ b/tst/regression/test_suites/aniso_therm_cond_ring_multid/aniso_therm_cond_ring_multid.py @@ -87,6 +87,7 @@ def Prepare(self, parameters, step): "parthenon/time/tlim=200.0", "parthenon/output0/dt=200.0", f"parthenon/output0/id={step}", + "diffusion/integrator=unsplit", ] return parameters diff --git a/tst/regression/test_suites/diffusion/__init__.py b/tst/regression/test_suites/diffusion/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tst/regression/test_suites/diffusion/diffusion.py b/tst/regression/test_suites/diffusion/diffusion.py new file mode 100644 index 00000000..47a0a8b3 --- /dev/null +++ b/tst/regression/test_suites/diffusion/diffusion.py @@ -0,0 +1,217 @@ +# ======================================================================================== +# AthenaPK - a performance portable block structured AMR MHD code +# Copyright (c) 2023-2024, Athena Parthenon Collaboration. All rights reserved. +# Licensed under the 3-clause BSD License, see LICENSE file for details +# ======================================================================================== + +# Modules +import math +import numpy as np +import matplotlib + +matplotlib.use("agg") +import matplotlib.pylab as plt +import sys +import os +import itertools +import utils.test_case +from scipy.optimize import curve_fit + +# To prevent littering up imported folders with .pyc files or __pycache_ folder +sys.dont_write_bytecode = True + +diff_cfgs = ["visc", "ohm"] +int_cfgs = ["unsplit", "rkl2"] +res_cfgs = [256, 512, 1024] +tlim = 2.0 +diff_coeff = 0.25 + +all_cfgs = list(itertools.product(diff_cfgs, res_cfgs, int_cfgs)) + + +def get_outname(all_cfg): + diff, res, int_cfg = all_cfg + return f"{diff}_{res}_{int_cfg}" + + +class TestCase(utils.test_case.TestCaseAbs): + def Prepare(self, parameters, step): + assert parameters.num_ranks <= 4, "Use <= 4 ranks for diffusion test." + + diff, res, int_cfg = all_cfgs[step - 1] + + outname = get_outname(all_cfgs[step - 1]) + + if diff == "visc": + fluid_ = "euler" + iprob_ = 30 + viscosity_ = "isotropic" + resistivity_ = "none" + elif diff == "ohm": + fluid_ = "glmmhd" + iprob_ = 40 + viscosity_ = "none" + resistivity_ = "ohmic" + + parameters.driver_cmd_line_args = [ + "parthenon/mesh/nx1=%d" % res, + "parthenon/meshblock/nx1=64", + "parthenon/mesh/x1min=-6.0", + "parthenon/mesh/x1max=6.0", + "parthenon/mesh/ix1_bc=outflow", + "parthenon/mesh/ox1_bc=outflow", + "parthenon/mesh/nx2=1", + "parthenon/meshblock/nx2=1", + "parthenon/mesh/x2min=-1.0", + "parthenon/mesh/x2max=1.0", + "parthenon/mesh/nx3=1", + "parthenon/meshblock/nx3=1", + f"parthenon/output0/id={outname}", + f"parthenon/time/tlim={tlim}", + # Work around for RKL2 integrator (that, by default, does not limit the + # timestep, which in newer versions of Parthenon results in triggering + # a fail-safe given the default init value of numeric_limits max. + "parthenon/time/dt_ceil=%f" % tlim, + f"hydro/fluid={fluid_}", + "hydro/gamma=1.4", + "hydro/cfl=0.8", + "hydro/integrator=rk2", + f"problem/diffusion/iprob={iprob_}", + f"problem/diffusion/Bx=0.0", + f"problem/diffusion/By=0.0", + "diffusion/conduction=none", + f"diffusion/viscosity={viscosity_}", + f"diffusion/resistivity={resistivity_}", + # we can set both as their activity is controlled separately + f"diffusion/mom_diff_coeff_code={diff_coeff}", + f"diffusion/ohm_diff_coeff_code={diff_coeff}", + f"diffusion/integrator={int_cfg}", + f"diffusion/rkl2_max_dt_ratio=200", + ] + + return parameters + + def Analyse(self, parameters): + sys.path.insert( + 1, + parameters.parthenon_path + + "/scripts/python/packages/parthenon_tools/parthenon_tools", + ) + + try: + import phdf + except ModuleNotFoundError: + print("Couldn't find module to read Parthenon hdf5 files.") + return False + + tests_passed = True + + def get_ref(x): + return ( + 1e-6 + / np.sqrt(4.0 * np.pi * diff_coeff * (0.5 + tlim)) + * np.exp(-(x**2.0) / (4.0 * diff_coeff * (0.5 + tlim))) + ) + + num_diff = len(diff_cfgs) + for idx_diff in range(num_diff): + num_rows = len(res_cfgs) + num_cols = len(int_cfgs) + fig, p = plt.subplots(num_rows + 1, 2, sharey="row", sharex="row") + + l1_err = np.zeros((len(int_cfgs), len(res_cfgs))) + for step in range(len(all_cfgs)): + diff, res, int_cfg = all_cfgs[step] + # only plot results for this diffusion process + if idx_diff != diff_cfgs.index(diff): + continue + outname = get_outname(all_cfgs[step]) + data_filename = ( + f"{parameters.output_path}/parthenon.{outname}.final.phdf" + ) + data_file = phdf.phdf(data_filename) + # Flatten=true (default) is currently (Sep 24) broken so we manually flatten + components = data_file.GetComponents( + data_file.Info["ComponentNames"], flatten=False + ) + zz, yy, xx = data_file.GetVolumeLocations() + mask = yy == yy[0] + if diff == "visc": + var_name = "prim_velocity_2" + elif diff == "ohm": + var_name = "prim_magnetic_field_2" + else: + print("Unknon diffusion type to process test results!") + return False + + v2 = components[var_name].ravel()[mask] + x = xx[mask] + row = res_cfgs.index(res) + col = int_cfgs.index(int_cfg) + + v2_ref = get_ref(x) + l1 = np.average(np.abs(v2 - v2_ref)) + l1_err[ + int_cfgs.index(int_cfg), + res_cfgs.index(res), + ] = l1 + p[row, col].plot(x, v2, label=f"N={res} L$_1$={l1:.2g}") + + # Plot convergence + for j, int_cfg in enumerate(int_cfgs): + p[0, j].set_title(f"Integrator: {int_cfg}") + + p[-1, j].plot( + res_cfgs, + l1_err[j, :], + label=f"data", + ) + + # Simple convergence estimator + conv_model = lambda log_n, log_a, conv_rate: conv_rate * log_n + log_a + popt, pconv = curve_fit( + conv_model, np.log(res_cfgs), np.log(l1_err[j, :]) + ) + conv_a, conv_measured = popt + # Note that the RKL2 convergence on the plots is currently significantly better + # than expected (<-3) though the L1 errors themself are larger than the unsplit + # integrator (as expected). + # For a more reasonable test (which would take longer), reduce the RKL2 ratio to, + # say, 200 and extend the resolution grid to 1024 (as the first data point at N=128 + # is comparatively worse than at N>128). + if conv_measured > -1.95: + print( + f"!!!\nConvergence for test with {int_cfg} integrator " + f"is worse ({conv_measured}) than expected (-1.95).\n!!!" + ) + tests_passed = False + p[-1, j].plot( + res_cfgs, + np.exp(conv_a) * res_cfgs**conv_measured, + ":", + lw=0.75, + label=f"Measured conv: {conv_measured:.2f}", + ) + + p[-1, 0].set_xscale("log") + p[-1, 0].set_yscale("log") + p[-1, 0].legend(fontsize=6) + p[-1, 1].legend(fontsize=6) + + # Plot reference lines + x = np.linspace(-6, 6, 400) + for i in range(num_rows): + for j in range(num_cols): + y = get_ref(x) + p[i, j].plot(x, y, "-", lw=0.5, color="black", alpha=0.8) + p[i, j].grid() + p[i, j].legend(fontsize=6) + + fig.tight_layout() + fig.savefig( + os.path.join(parameters.output_path, f"{diff_cfgs[idx_diff]}.png"), + bbox_inches="tight", + dpi=300, + ) + + return tests_passed diff --git a/tst/regression/test_suites/diffusion_linwave3d/__init__.py b/tst/regression/test_suites/diffusion_linwave3d/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tst/regression/test_suites/diffusion_linwave3d/diffusion_linwave3d.py b/tst/regression/test_suites/diffusion_linwave3d/diffusion_linwave3d.py new file mode 100644 index 00000000..d684a55c --- /dev/null +++ b/tst/regression/test_suites/diffusion_linwave3d/diffusion_linwave3d.py @@ -0,0 +1,183 @@ +# ======================================================================================== +# AthenaPK - a performance portable block structured AMR MHD code +# Copyright (c) 2023-2024, Athena Parthenon Collaboration. All rights reserved. +# Licensed under the 3-clause BSD License, see LICENSE file for details +# ======================================================================================== + +# Modules +import math +import numpy as np +import matplotlib + +matplotlib.use("agg") +import matplotlib.pylab as plt +import sys +import os +import utils.test_case +from numpy.polynomial import Polynomial + +""" To prevent littering up imported folders with .pyc files or __pycache_ folder""" +sys.dont_write_bytecode = True + +# if this is updated make sure to update the assert statements for the number of MPI ranks, too +lin_res = [16, 32] # resolution for linear convergence + +# Upper bound on relative L1 error for each above nx1: +error_rel_tols = [0.22, 0.05] +# lower bound on convergence rate at final (Nx1=64) asymptotic convergence regime +rate_tols = [2.0] # convergence rate > 3.0 for this particular resolution, sovler + +method = "explicit" + +_nu = 0.01 +_kappa = _nu * 2.0 +_eta = _kappa +_c_s = 0.5 # slow mode wave speed of AthenaPK linear wave configuration + + +class TestCase(utils.test_case.TestCaseAbs): + def Prepare(self, parameters, step): + res = lin_res[(step - 1)] + # make sure we can evenly distribute the MeshBlock sizes + err_msg = "Num ranks must be multiples of 2 for test." + assert parameters.num_ranks == 1 or parameters.num_ranks % 2 == 0, err_msg + # ensure a minimum block size of 4 + assert 2 * res / parameters.num_ranks >= 8, "Use <= 8 ranks for test." + + mb_nx1 = (2 * res) // parameters.num_ranks + # ensure that nx1 is <= 128 when using scratch (V100 limit on test system) + while mb_nx1 > 128: + mb_nx1 //= 2 + + parameters.driver_cmd_line_args = [ + f"parthenon/mesh/nx1={2 * res}", + f"parthenon/meshblock/nx1={mb_nx1}", + f"parthenon/mesh/nx2={res}", + f"parthenon/meshblock/nx2={res}", + f"parthenon/mesh/nx3={res}", + f"parthenon/meshblock/nx3={res}", + "parthenon/mesh/nghost=2", + "parthenon/time/integrator=vl2", + "parthenon/time/tlim=3.0", + "hydro/reconstruction=plm", + "hydro/fluid=glmmhd", + "hydro/riemann=hlld", + # enable history dump to track decay of v2 component + "parthenon/output2/file_type=hst", + "parthenon/output2/dt=0.03", + "problem/linear_wave/dump_max_v2=true", + f"parthenon/job/problem_id={res}", # hack to rename parthenon.hst to res.hst + # setup linear wave (L slow mode) + "job/problem_id=linear_wave_mhd", + "problem/linear_wave/amp=1e-4", + "problem/linear_wave/wave_flag=2", + "problem/linear_wave/compute_error=false", # done here below, not in the pgen + # setup diffusive processes + "diffusion/integrator=unsplit", + "diffusion/conduction=isotropic", + "diffusion/conduction_coeff=fixed", + f"diffusion/thermal_diff_coeff_code={_kappa}", + "diffusion/viscosity=isotropic", + "diffusion/viscosity_coeff=fixed", + f"diffusion/mom_diff_coeff_code={_nu}", + "diffusion/resistivity=ohmic", + "diffusion/resistivity_coeff=fixed", + f"diffusion/ohm_diff_coeff_code={_eta}", + ] + + return parameters + + def Analyse(self, parameters): + analyze_status = True + + # Following test evaluation is adapted from the one in Athena++. + # This also includes the limits/tolerances set above, which are identical to Athena++. + + # Lambda=1 for Athena++'s linear wave setups in 1D, 2D, and 3D: + L = 1.0 + ksqr = (2.0 * np.pi / L) ** 2 + # Equation 3.13 from Ryu, et al. (modified to add thermal conduction term) + # fast mode decay rate = (19\nu/4 + 3\eta + 3(\gamma-1)^2*kappa/gamma/4)*(2/15)*k^2 + # Equation 3.14 from Ryu, et al. (modified to add thermal conduction term) + # slow mode decay rate = (4\nu + 3\eta/4 + 3(\gamma-1)^2*kappa/gamma)*(2/15)*k^2 + slow_mode_rate = ( + (4.0 * _nu + 3.0 * _eta / 4.0 + _kappa * 4.0 / 5.0) * (2.0 / 15.0) * ksqr + ) + + # Equation 3.16 + re_num = (4.0 * np.pi**2 * _c_s) / (L * slow_mode_rate) + analyze_status = True + errors_abs = [] + + for nx, err_tol in zip(lin_res, error_rel_tols): + print( + "[Decaying 3D Linear Wave]: " + "Mesh size {} x {} x {}".format(2 * nx, nx, nx) + ) + filename = os.path.join(parameters.output_path, f"{nx}.out2.hst") + hst_data = np.genfromtxt(filename, names=True, skip_header=1) + + tt = hst_data["1time"] + max_vy = hst_data["13MaxAbsV2"] + # estimate the decay rate from simulation, using weighted least-squares (WLS) + yy = np.log(np.abs(max_vy)) + plt.plot(tt, yy) + plt.show() + p, [resid, rank, sv, rcond] = Polynomial.fit( + tt, yy, 1, w=np.sqrt(max_vy), full=True + ) + resid_normal = np.sum((yy - p(tt)) ** 2) + r2 = 1 - resid_normal / (yy.size * yy.var()) + pnormal = p.convert(domain=(-1, 1)) + fit_rate = -pnormal.coef[-1] + + error_abs = np.fabs(slow_mode_rate - fit_rate) + errors_abs += [error_abs] + error_rel = np.fabs(slow_mode_rate / fit_rate - 1.0) + err_rel_tol_percent = err_tol * 100.0 + + print( + "[Decaying 3D Linear Wave {}]: Reynolds number of slow mode: {}".format( + method, re_num + ) + ) + print( + "[Decaying 3D Linear Wave {}]: R-squared of WLS regression = {}".format( + method, r2 + ) + ) + print( + "[Decaying 3D Linear Wave {}]: Analytic decay rate = {}".format( + method, slow_mode_rate + ) + ) + print( + "[Decaying 3D Linear Wave {}]: Measured decay rate = {}".format( + method, fit_rate + ) + ) + print( + "[Decaying 3D Linear Wave {}]: Decay rate absolute error = {}".format( + method, error_abs + ) + ) + print( + "[Decaying 3D Linear Wave {}]: Decay rate relative error = {}".format( + method, error_rel + ) + ) + + if error_rel > err_tol: + print( + "WARN [Decaying 3D Linear Wave {}]: decay rate disagrees" + " with prediction by >{}%".format(method, err_rel_tol_percent) + ) + analyze_status = False + else: + print( + "[Decaying 3D Linear Wave {}]: decay rate is within " + "{}% of analytic value".format(method, err_rel_tol_percent) + ) + print("") + + return analyze_status From c7273f941ed522002313f7943cfbc2618b73ecc1 Mon Sep 17 00:00:00 2001 From: Philipp Grete Date: Tue, 24 Sep 2024 20:54:12 +0200 Subject: [PATCH 03/11] Fix KHI pgen. Add turbulence pgen doc. (#119) * Move KHI pgen to device * Fix KHI init cond * Add overview README to docs * Add turbulence doc * Address comments --- CHANGELOG.md | 1 + docs/README.md | 36 +++++++++++ docs/img/turb_acc.png | Bin 0 -> 70460 bytes docs/img/turb_evol.png | Bin 0 -> 47808 bytes docs/img/turb_spec.png | Bin 0 -> 18786 bytes docs/pgen.md | 2 +- docs/turbulence.md | 107 +++++++++++++++++++++++++++++++ src/main.cpp | 2 +- src/pgen/kh.cpp | 139 ++++++++++++++++------------------------- src/pgen/pgen.hpp | 2 +- 10 files changed, 202 insertions(+), 87 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/img/turb_acc.png create mode 100644 docs/img/turb_evol.png create mode 100644 docs/img/turb_spec.png create mode 100644 docs/turbulence.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 65d55b73..152bd814 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - [[PR 1]](https://github.com/parthenon-hpc-lab/athenapk/pull/1) Add isotropic thermal conduction and RKL2 supertimestepping ### Changed (changing behavior/API/variables/...) +- [[PR 119]](https://github.com/parthenon-hpc-lab/athenapk/pull/119) Fixed Athena++ paper test case for KHI pgen. Added turbulence pgen doc. - [[PR 97]](https://github.com/parthenon-hpc-lab/athenapk/pull/97) Fixed Schure cooling curve. Removed SD one. Added description of cooling function conventions. - [[PR 84]](https://github.com/parthenon-hpc-lab/athenapk/pull/84) Bump Parthenon to latest develop (2024-02-15) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..018d4b68 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,36 @@ +# AthenaPK documentation + +Note that we're aware the rendering of equations in markdown on GitHub for the documentation +is currently not great. +Eventually, the docs will transition to Read the Docs (or similar). + +## Overview + +The documentation currently includes + +- [Configuring solvers in the input file](input.md) + - [An extended overview of various conventions for optically thin cooling implementations](cooling_notes.md) + - [Notebooks to calculate cooling tables from literature](cooling) +- [Brief notes on developing code for AthenaPK](development.md) +- [How to add a custom/user problem generator](pgen.md) +- Detailed descriptions of more complex problem generators + - [Galaxy Cluster and Cluster-like Problem Setup](cluster.md) + - [Driven turbulence](turbulence.md) + +## Tutorial + +An AthenaPK tutorial was given as part of the **Towards exascale-ready astrophysics** +workshop https://indico3-jsc.fz-juelich.de/event/169/ taking place 25-27 Sep 2024 online. + +The material is currently located at https://github.com/pgrete/athenapk_tutorial + +While the instructions for building the code are specific to the workshop environment +the tutorial itself should translate directly to other environments/systems. + +## Parthenon documenation + +Many paramters/options are directly controlled through the Parthenon framework +(both with regard to building and in the input file). + +While the [Parthenon documenation](https://parthenon-hpc-lab.github.io/parthenon) is +more geared towards developers it also contains useful information for users. \ No newline at end of file diff --git a/docs/img/turb_acc.png b/docs/img/turb_acc.png new file mode 100644 index 0000000000000000000000000000000000000000..b828a66ed86d50ca68783c0a4a229044f8a7996f GIT binary patch literal 70460 zcmcG#Wm6nXv@MKfu)!g?Tkyf%-Q5EWHn;@|Zi6$pySux4fZ!fHK(Ih?mk{oA-h1v( zcz0D-clC$vs;=tQtJhvTT0>191C0a?4h{}OQ9(uv4i299zea=t_;1EmCA9g!4#{0g zQ3vI}@JF$Vf`g-iQKXq6!a=2VN_AtXHYXTs)XV)7M0H`^wjexwr1&_A$cnmrUn`V9R}=bnXn*g~W-4mZp!bSZt-p8jV%_$B z*Sq1rRVU_Oi`GBTZ~V5tvVF=?-!C-tc>J~Ia_&N}p#8Dt))TS($wt@p)xYVbdEiSo zhq`_ZB}Ie&z`%g%+ugj|*NmF?d-0b!jKQF*oi|1fGs+KP!!!HOr{-V#H_qE@-ue@7 zn#~$aO;hpbNf`g!PubV4{&j9&>1=6vv#)t=Q5AIwAE~>@lOx&DJAplnv-No^p=%ctih zuYREO=~t(?_~GI|TFBpyeUB})!si1j@rNWcC6U=U^UPs-iB`@3;5s|aGmc57J+G}^q4S} zIA{)cD-RI&-&I^PweB*|U%np){VOOhE6LpAnKT`J?~|#8`YX9msfJ0iMmfv5OQd=T zD4cSl;tb?2?S*3i;4X6+R1Y(RKC8=@msE*sQVEgI=T%Va5m(%KP#`_j`l7y~i#ot= zdj~2zk6Ene!Gbo8pZQX{dd=P*ZXoxYx1@|Od4U7sJthJ8HkQG3B4|;@1OnXok#Fm# z|IYk9TWZk20Ms3mEx#+g8+1!=U*?z{X-5kr9`nL)Dst0^a*NhO8tmU}XWPTK>J|rG zKe537)spATj|tEacj}0VtSSplDndyZ&*n*`8tpP8slFRBYRs=2r5bHzWO$&iMD2o1F*uLE88(1>2w}# zW{ig2WgHyj43GR@gL}gcrJ0IhejjZffl#6iNB5{&$Zh1)?ey|z;g;o=&H!9{i~DI; z0qsv}o>W8F8X1Y;hh$UMp~P1$`$)R1KQ-ekkNS`CuQTB~Ww8x4LHJ4UXH%#Ao5@zQ zw8Ko>eKIXI$J-j34+v+NOmb`|*B1T|h}j;Mu0) znWqS)Pg3BjqmYdeygYVS zxwH`;Jzz(F?j;FKGm|*FUdLWVEuFmZ##)zx?VWXRyzSb;HWbzWbYfE@`&o0HenlpH z=K{?q4WgA1s-1@8%i-%h@xe|OKAO@yWZ?Jbd#d%;Fg*$U%7#*6)9U<%pyl=ZS?P6a zf7)J-6gw$CR;hkXsxav1P$w=>_8T=#9I!^Z2#H?Kv&u19i=ZLG*#BJMtFtesn@Ea> z6gzZ&kx?J(qfQK>n}!cIN9=@-Iafi{Lgg(xnCrvD(W#xMPn&T-a`jzbCi-3nVKa+@ z?29`m;jmfm+h-NXo4^^$x1E}+TU}bIIV=*OSw}_<$7aon53Mct-lYoM7-U$~KHiCa z`_@x^0v*@Zbg_`(xsF(A-Q3&D&^MzwFUo_hSblrZi~Cey>{cZel-Vz5co#xZo37Ok z0lXX-LhU;0lHOds+IAs?3y+_|jcBXA#g?{?fLtremL z7o%QCliuBHjK5O+yQ6tcLWIfkm9;I;dU6vlVXSBWhhXts#Gb4K!WY(T^S^QnS z8#A!|iDDVDH)uO-)kxqAv-KC4(}8ovn`A(|Wx3B6sCjwPyx=BcmO5iE>NSZ`f{s)gy1}G9#6~J^4H(K(m(t0%)hLIJLSY3KWE)ENjag{>Y z)YSIr{^&kh$(hTn3;APT<_8r3K{Oon*|aa946P#$)&~|0(k#=;xG+4`>ilup#u}1g z!DSN%Q=sgX-wJX}dY6RaKu^>NkyO_l5`UFvgc_9fdZJbfzn zt2-Q=`oW~Z(brtcX9AGlc6&T+9EY%18djE6Dk(^ZS$a-D<%<|nv^c=*@z9~lg4J%m zYz36s$-LNwLeqRzxjRFIJG5NjzA)#kJSWOaFiu6v@-}%MB!~i+u%V{D@$iyUZylFX@L!Ml)U?5^mo2uKK%^#L-PzcLsGXp)g;EXEg*TwFUhL034AcxNZMgc`3C@FEl_vSyxW(VlFr)tS;*Z`pv3cnh0?&fMaWW~*nL zO}16$hUPt!m7}9;!o$7|dBa6>H?0(rUHYz$BrS7opSttGyD08XpG1j3q5b|$C|FfL zxl~O2E8lpPnxoh@ooJ8U4)Ai$>L=VGoB_OuFxmC2ldHcCL+CLN`v5nWLa1->ohbG9 zLtgF@9&WJvuVy5Z`T!x8v4EA5Pl>J8-pi}%Cg3(-pBu-_FzMj z?KJppZggzInPWC$tEHHL0klb6eI>qoxy+v>037}{=tse{32a0i%y8O668LJt_0?vs zrokztOQfgyg-8w;Sp+t;n}uDFnYZ>3Foz-csZ4PY`0yzC|%TYo}gu-m~bG| zt+5FBS_-~(XTAB86=bmOYkp+Fsb0EavEb9L`y~6Z<&OEYUyfKFf656l2H=)P4iVq*N4||M z_+-pz_3jsxE=U$6Pp+6TYzpXppsugy0AePK|&c}z7M|@!{Hq7 zG>Q0T!@h31FMJzqv`(58;45MJJnWc%EvJu{Z~SZD=TcYi4HFkTt@1Ccz8_b4$?lK1 zj;sk8<{KqE%OYZI5Non>lx1;>^x~s8@e|JXawe0LB_=`H0kq3WD+a~7;EP#uXyMSv zr_6P33=kr!SSNB7HSh&(7>rGiIVDKQf&iWHu+t7Fk~&d%f0#zl!eXFbt%w~`^g()S z_CQTB7hKhx^bpC>wLtw?qBb*MOZRLBU(-gx)^XuvfDw}H(yuP~{8r5d!1`+a&A%%J z^;z=JZFFu}IS;O$3{l;lk(rXBu&?!=W}i2|eJ%qd7NsETc<*FEWeCDsY53aAwSDMT?)JrF7xmXi7Ho zB)l(%XYO#S21{pp>LEOW@b@UU<_Q7g0v5>zLf9lS-G=S75jInnb%tL1U)Qehf@IDhs7a zXY0A-Qn+cqk)db3QwrEK53z^e>!WF3`&)kR?QrqQ1YT)+MG=z1~K znpRKHuPHDJ?Mk8(80IPIQ7l&sW~OormGNz6-uk(!z@@-h(C*oF(M|}DXU^EPuO*PH zYq}KqbN@@?Os&@s=`X4H>*d1A$pnS35r}?3d)@TLG)N_@@(qc2i~YevC6BH51?xpc zU=#^Y>1| z0W^7Mrpq~cxlm1$&@2|CW17SB;^V1fZ4mXXbs=wgC>%fYdZz|Q=8u&J<#Yzph=N{e zXb9b@a~;ILnWna;Xnr*{>DD*l|CQ{_AQ6()df*a&ZpijEOK;1Z5|~sbWZ&WCfqHSl9<0um2(}=-lf4 zOGCe{@Q-~bzm2lOL1j{sz@=EJ+_TzhUcx~IEXxrWWZvjtmnBDj%WxJIj+HR6D@dSy z4v-l)0^B}17%Yb^63nU_!Gu>A2|Ui;`%pqXXm!@f zi=)t$J_Vo!EHNsrNx-<*mJ)yQTvneK7IsW8pQeZ!a~kP~j2WBhmiz+@kmQ=LQE+~x!FHCMa3hS~7ILr@%dVY=i9J!>u$ z2W+eW?o4InkY0Z6tdZ-MpVW0)W|=5sHsUqZ=5&8&Ul*38%P-fWDu33PdW-$FC~dEB_^=347Kf=G%H*)Eg#)Hk>o%^1}n> z4Al)YR4)a@C3a6Nrk))pS}2SI=FS;*&rUx#a1zqaKF;5s`g#)8?d@3Jtl=^~)?8E7k2p?U>8C%R%L3Zz3y?zt zi}bGR5NQx?#DD)YT*J$B_#NDUmm%T|){-n{T9q)mRO8p;&IYa-k7@6ZV|p$+6o>pN z#Gc283ohOZ{H6uP>i<^>703Ml=yQXNySadUv`30<98dAYgoSsx<_0oj{xUrsQEAp0 zTay*EIt(kel+ebG>ByLLjb_5Ti_c!G1)}p|Ac56Rf^=bvWf|V+?0Lw*n3~2@{H4Of zRnWR^&68!Ei|oF>lxwtN1YnvLOAaW&999?llCX5`+$xFPpqatDtHW(9OGCkQ^h+*z zI!-6M$Nv36QtCxb|Icb^O91qb)31jaF)}8Ra@d~g?>awc>%lMbDtyu!g zV+Vbjy+0qld;Eq0O$L%KGs#Ic69w*P_JN$Quz`isZXYv-5h~O}t$`{fY=9&vS?;C@ z(|gcr8oc~HBG6RPs6!Q?iwbZDwAeKXjZ+((VbT0n@XY1^gMs-e6E-uJkF)t0=hz@I zR9Nn4g~d5$#|HRb;FmKrcVjf#fM999OrLx2^(z&d$%_a9cqCsJ1#X3{z4HhTuYZqi zGl1v)pQTf}*k})q0vUIRHBoD9xm|{3Z>svw~fWYF0${j>spGl5qQdXosB+8fi3V8zk6MjB# z^u?zY@8`XH?adD3|B?qy!Kpf`F$d*1dXG+E%d6?E7iB^DUnC-4YX6dhGT6I}`ZI$N%f+NGV!E z0KY>Zi0pV2Fgfv}&2KpqcLv|JJW;wFdBT6!()rNKe4o9cJxncy$6>^)bqffk8cX3Mt4E}E+m~|9YWwq7lTXwwxYm7lY0>cgzd=b7uK9&) zlE4B-yLx>2ulY&tt>w2*8niYYkmSqUy3k? zGHVlLTGB1TFGE$RoBH(EeWj5WJhGajj@8>EfdDrL`Z>8B2H!`5>7v)!fJ&j9%N~T1 zkYdQ)P3Y|P3daUj5UY9Pd#wRWr&VE#k$a~M?0d8f=nTW@xR9hn+_ORrtTb>+xb^yk zTIY%*@MnC?44%l3bov{n4{?%`{J@WkODTq$@LI9i2XZ3Jac_}{wnvptwMvs)^cLEm zts9EoF8kb=MSY>!x7qG`x8F-hGgTPGGp~9b-W7S;Gsd6L^$XVjZrPZ-Aa#p=XBt^l zr}{e*7!;2DW*(lal?QSi9z;dl86NCW;`;a+;W1Ce8Ou|1>u9rpP%qqf$&+Q+R|K4Z znAU%kx2f9bQDu_y>CIjqy~3kRV4B?OV;_wt#uK>TYBX{Qhu(f3j*JD%*O({FNZhw5 zUO(r$o7kTQdM_u}lp>cBBjkK?`#oGhE93(agY=QPpKSLehpw5=WT2o#y`kRj$(q&& zrXDMSMK=C|* zE!GUO(p{|tS`N$)U;x2|4yR{K^<8!gwTK3xF^KH@LVNqZ&ewVknA{nh=6%{ za5!YR_W3NbJ~oT!8Vc6_${%9-#w{$Pw64~u*rbT;u~bF@3#YDkI;8S5bTzYyp$-lg zqY};P(B<;J2OOM-C+W{uDkT zJsB#ySOD<OJ|o zb^XEnnzP8A(@K*ch>AER(pXsJ*%KP!AMW8I{%QpGN7A{oJ7ozQ&jOK5 z>J~D7R2~n_iF8$gO=uFIfHEVIwKvT|5w#$LH&eu{k;je0Iv)2_Eu27x5ioiRYKxAc z9om)U#;2fE`_uq$t9d`YNSyq&zBo-I7{fo|t?_ysA_rON=|*!}G1IPod79tE7=LGe zdl0+ApK%Hdw=Q#)A}jJZJVOMcn4DVC+SYYP+KJ>jAF`_Sa7)rxb-qMFM*4tPLY=)L zo-EX)>O)SG7G4iBB?TzhgRT||8i%@Ug^hNsEa}==TyFKU4aJJUoI!2G3lG6 zWS$+B14!#muOPdvdYPag1S5D5))G`?h>63kMAJ80i^DF6??kVbe!v@!5P9Nn0nXg0 zaG_-wnd!FRR&#@N5?=G)9nYSeYqSiO5mdR;ZxqqI6kD8MAT9(TE-JxDZ?%gTQ{h`;eQ_bbQ5#Ty znE7DO6iLmTe!f7|-s_mPIEuv%OSVYQ+XWGceSF$pe>P4!kH?^2{!9x4N(3 zzCqRk_wSir{%?x?X&-b|pA4f9Q%oktJ>?PRDqEA{oH@-ODz_Pc9Q|1zIwgQ;!72Vf zF90`vvW#)y`ugAEllgInF_K!kbgL&Pfh&S5c(pY!Y4{PRei zG7GNT_^i*4BC}ABG#hQmi5)u9)JXbb{HC2mSNC<+3&H5ml7LI?jj3bHAbR;mmgnBz zVIsrX+P7AB+f;3>PD31t9tM*XjPoC!17D|xI&101FW|p@sb$;)AAQVM&aU!rpNTrf zyz{w|6P_UTWT=j#9|?^97LvWr-Gc>8sYBnC!NeTdjYewL&Q`!bI6@j^+}nF@K3WO* z(OZ@JQu9*G@}fJJ6{38vht#_0F;e04mRITZLf2C!1a=$-=$DubVI-6B{H|e{;VASd zOKnP0nl?AIq|CfoV_*6{auNFWfdCaXVld?1A7^AyC8!8rKe|(SmqBv3@ScP8*+Q|6 zrm>5=-*_Cb4W|(sM@h`?a$hA#wR!GNbQs;okdUl5jV`js0_SlFYvb>0()+L4` zR;?Rl+L_fF`DG9`Qj!lnn;L}CB4WYv`T&i;2^s`PE9lGhB&qd%di7u)CqloyzI;RC z=^_)a`9JM!d?~~7(I#WOb{mx7OAK)JAqW{UeqE+5-J zy+#(Kh-8I!-{PG>$P3?wNRC`wUgbQvIre`f;VF>4TAkz7UddV{ zd&~|gIvP=!1`-Nf)Lm4eEe6XMFe(A#?Bz>kmZ zrlFR$j~VCbRdTI>gk=#J={=5!UB;|e&%=gO&Ov=z$0?^)gJW>nD+I6xB)yaR?1^1= zY`IEuqWysF6Asn6HC&Tr0n3e3I5OsTM=65Xx0YFPt?crKL9y##W;iY2m#D1Eo|sd= z4P%ZzFx>4USS_zPq4F&wpdS;)6R{PHhTc6d0nQMg4tK3viDFsTX=I~5#;ISHC+xP; zRnpsZY_$FrpjPOpHv-Z-uGDbUOaOgG2g-7fGYn{gcm6wd_(r+L&AEVjkym|k|9K`Q z)rdyVOeEQGP$HQMQp{P_;IT>KIDzNyZmQxTtIm$FVj({&0}CAOXe((Evuk%DZvMjs z*qL9N;E0h5z<@bVKU)QqhZS8#pEaFdI#kbxRcWtWbWE^NU0HFdri6qQ;KHSJe7 z*ZAVWP>^w6R+=;JY%uFS_%1p1BkZm1;og>V>0fmga9rYds6K{e+hZ7K3ymy3!dbzB z{bUFoq7u?vKhvC?zTU8rs3oqe@ZQ%UoVN``CKkwUL;Zx`@cf5`2kpl2W;5fl7^ z-SO|KeAnCrFe96`Fm{hqDP&(rTZOw}Pf{9$-doPJC=pcDFuSE{fW}~$!AYc?f2@-# z>z4%E0Ou!6ab{XNdbKgTwwfiAom-2OMSKn&Xk-Ge5frl=znXs0!akMkOl%lz!T&CK z;CHyuxAhX-nA2kP0}?xx_qRu&pY9(|f|0L0+THqmfcKByvXOAg@Qb7U-%GduEA>_k z4ZbRIC5Z%!;^F58WOFJ}n*LHilV%So{m0I&KwXs2i|!4n;>mhrjQAPtkWP{eu&$u_Wv2zaLOgD+m|e1WHHEboK@yT9Ci(0@b1b!wA9J0l>aj9*Zm{+p$IK#kx<%E zM=qHuPHCVS;mR5St7V(GimUQwM`orYLNJ(;G*wR)>2(mYhZ|7HXRD$^eDZiu@#|+i z$BhiY@MHoNk{l9E@7OYB3}SwD#Ah9mOMCh>kf8oW&+^!vk6x{k)qtUyvu{DZXiBl# zjeZ9Wk34}cRn~vNUE~oDPTIa@*;vq!YV9`9laIaK2quFVpRyt!8Ut$COV9m>lyJt{ zK?Zx!!t~Y0eU3{B`CNCShc10RL*l2lcT>~<^}dDOHCO$^j0@N06dDWWBg!V#0z2xkh&7?JDx%l| z&A4GJC1dRAHJQ6bnN})6APArWvufqA({ONX@X7~hbp%ek3{VIK!9uii`rj|S?r+Qc zsqg6C-wBCbm3eDq1anVSxSPFRL$(3-MwvY`JeJE9qveTG};{Gz<(IZCsubmZ( z*~+DM$g7MKVB0`Tuf9pFwVjXEXjJn=nsj}eCRqxbZ=E$pE`gRmF$+MBg%`nClc#8y zwn$>=Fj^93Z9aC~@vo!eQSjC*<9|n?p*B+W)#xQfDoc^CnpknkLjt+&w8A?IOD8rA zORAi;cJ61Krc_bgbbtX#&$K$%oiT`t{j^_zUe?qVb83Ap%8*K9uj^rtj=wJC{bH|6E#>#}z6R#s#<}Zt|HZ~VIo~GDK|j1x!kr$2g$oFL5WqCo^?-LQC0vNv z=~*9%Gn-Q&hc&Emc?Zu?tbO=oF)x7?94+;}GG`W%@kc#-!E1v6STe!P6UnR0IR4rJWXios}Y`s4S5@RN4DZCD2gaMEZ7P-h$;nUn?5k6b}BAiCjZIt;7D> zK*Qq!jT(UYI1W2$&dKH08ETZTNAMRJeE@z791ap%>@f?sWnh zkq8~{dZa3lR2uLyTSY3NQ$;>NU7BzijTVE*S^2V$i7m0)k$2A~TPekySVptS`XV$s zHR9HXo9T_``)c*$WN=mDMaC~tj*e4%4!jh*XrH%kAemOPmJ+eWD{rs>N5+qO40yj{t8{BqM-j5kgtrjZg{;TLf`cSY;{c3`4n}g7d|r z62+E3^>i+~C0T$dISyp2=dN(Ei_Ezm8qfRh6qyw`v)M)@{s1YhpXmF!Tqg>dN)1#G z(#0sW1FO&h5+R3rlj8%H&lble{<6-!r8#}V{$n3Uet)%)QOp<_FqtQ9Tre9F9cVMF zuuEc z=Cw5-G>SXrU-%VELlE`x$;$ttGBSAAgr0Z=54GIJ-rWnr5bJ^rzh8NRqRs+tVK*X`rX--J=w>7n~ON%tIKqL0~n~_ zo>Prl6LhoJP{)i7vTe?Nh4OW5n=T)S&47_|YyoWg#Z zO-LK89zn^r>#P6%VIfB3INT~zZyBRJENQp5M9!qok|!Y3J<+|5cf-^=3Ut|rP^-r^ z&(th=bZ_+aO>eZ4d#3)OiMKghPcjPTHtPM1^Gaz3vQGEYei5DhF0@*fdRmX}Am ztYeUlB&(79J{)}RNp7<>Nt`S|cnIC1$LVG>A(HEMo@=y@3RGeIA}E1Ce|DJFn`R8( z^BmdjV8(V|#Gb+Nw@_?bp?D&~jL-ClXN)657Tv7v`nto$>>Puvgk6i`A5W5AqN%{Y zv<9n#N`ulOOL4MQ7P9BgXy}l2;!ogz`DD|fWmn9!;A03#G;o7oHgB0ltQBElh5ToY zI7Ceq1;-?I&%@^Hdw+BY6PMO=L)q;sgcVvPfC-_oFi)%0&DpZiSiLwQIgp%wcQ8QqV}`JrK{@YR5yW z=f9E_zng^Cks-SDjB`|{ea7m?%i%64GdA{6A2ZP*m+Do8 zGH7=)@Y&)_HOCZxH+gf&sHZzme}ME{dh@r)Q5|M7jpIrJso*fi#PZM6w(*N9tbz?Y z^%Zq7W}V2gdvlvyytn;PWRum?5z|wB@n&EA9s&XA!tNlEVuVYS`w{T?=Ru1I=xKf4 zaWIg+mnEf$TXqpJ@wSmiNHk&vVJiEkgofMG-^!X3z))KZ>U5wl)FmT^1O)~5y*z8dOiOcwdCOr)3i2%hLQrd*;b%l@;690rK6|q5?c$a$czjDi_<^MoGglWd%Lm7Qc zHY=C#6G8FU*kwAIWXaI$vIZIoyAi&?@o- z9j+mI>^%=M!dyf;nr4%M$;xJfyLrnBd?_AyZQmD_l$jJ%;wykRZOd9(i6urfr-s6g z1;n4V#u6=K(a$hNSSG4E$JK9C+Jl~6{(76@#rx*{EK2PJ|? zv-N)kzP+piZMtBgCq4pvA!IMtM2fS)eUfr>?Gqwgg$Kw(S+%H@+-|}H&QjfJoYYNIeYj;(y|GZ7tDfy~V((EYk&lCc=k#_Jd zPM`}|F1gpwVPruVJUb>=)&rbw+jQejx$3- z<@OHEBKz@wyb_g>4X%mmUI2TBfMMy;oQLjlakut$7))`x>-eV^=?*O9e&IS714A=e z2(bQ*5OXuk=6#i$Hv&cjrH&D)X|L%`TjBDr8)2TY&B%P>wkEKwssex03@G{L5}L2% z1&8-D8&yEBY4O|5f#pa(0cB~d5q$Y|B7W}vU@Ho(KF|Ds6==ha2>bZ{my zI+N`5l{bx`ErFeH1gRKk8t_Ch{XiIifW#D`9xQOBi2X{ZATpye2pdspU=hj8!dXFG z;Mqa5Xa{*4MmTTA(r0uA&`yA&UGK~JHtk+4ez=1lT4Hpx??UL76m!eVtE?ZUg#&5X zRHC|9CbqU0_cTVitoSTBIy|Bi6N}MU4nn9Xq}+RFX~B{J6hTJ;r7EV-mdb zSPJ#II5@r?Ir6QIFJ)73HK;`CIiLHIHBPL)m}%KK$7+q410En1&nw-+ZA>uFsOh+n zUx};kz4&(=e1*YchtuYPnPBqi3sX&OsWaV$f4E|gOLrIZ~ zjVbM8p$E;|(~?M$SQ7%h;!)QehHrY`k!HVuI-%}m3EitxkSuKl<&lh}i?MAquHRhC zM&#V$Q?I52`S|at+y4~Y%MF#elk_22Q_Zy>RdFD?#V=LwZU$sV4&?TFWn7^n0}}+p zWoIF0DT9&}HJROBhVt&etWq()-?*5ENY@0ms)*zE!&JHPiDm>gBsi$POXgOa$UgaC zH^qLIGZ244&ctYxlCjXLyly_c`j_Q!_slUWF+luR}i|eGd_9*dgYksxtoNM?*-k{7;NFjvFYlB^yg!F#Y)^T3Nm|%`-tRH)2 zeLAAr*Yzd~*LyXN=;?h^A!d)XZHA9F+%G`>x7_j1z{qizKenI)oX$K7a&=12_fEC^ zlkp#=&uk2pk~==@=#yQGw804Kj@HN?y^f?DTB?mLWMr{1abrB9@KUDBuSk8+dUGDk zYz=%lFaMBn^w^hg_!-Q_Q#m3aMi5c*MaiT( z1DW~!d(&1gcS_MOIST+5IibiypD?%EcGrn56!S*X89SGFFAhK$w*4u2l4MpbSj}+X z%wFecxnbcW|CJ~BFGUVE3S#z5H&rkI%RaQql>r2%E3;5kqW$XE%;M=Yb$}!0o=gvv z{c2I>#H(K1gda!)%<B)2Dd;z+j|`C;1Stj_mlaPchcJX}QnJ&Zy$6;M7L#ucS|2CC~po$+t(2En^nI+U7SA4WT#)q9C95s<$KJc76x8UTM=<$@R9>mt0ax# z^oa6K;7y}Sx!s6ZD%zFC*V}5jp8gtzCL&5XHab-_SIQt;*y_*J~nf%N(d8^mNQu{YHFf3_diUo~ShE=s^juYK$-I=V#j-;37~zHrI=8zn`94U2MEs zYXp+iN8h(NmzK%k#r>M5{qFMk{#|*_T_QWWRI54}a%Y z;PMYWkaa;|9)OL8vr;=b{a3g6Jzh1m?`!0x=)0-k*ZvG;(VlVCb?9hcS;g3OVk-fG ze+He#;(ej6>@T_CH+G|TdX6&&wo!|mNs5Xl+dXTa(j3!XIJno)Jwn=DMq%2=3HfHE zQ@1D`?$}}&VnzazLBRBw(#SRmyx}$<;gWUM!yTmvkN#D_DUDs|FSfqY1Za=ojK_Pu zN<;|%H_f)iA3l#@>JcG|R{FxaM6xm&r)Ba(ncoY6SPW>hpmM2 z&|8(t{ytwMK1B6K?PU(N`dZB`LYO0lfbye*GK=sK0TJ~6Z{b6Qt9(VzDb=T2{_hi% z?_5-oLu~Mjak0&#zn@9-o(FdRJwJWd$8;%QaLLv#65k9`(mX8U!Sb5lZFh6J#`X>eU%GarcvQ7nqm& z&nzv6KX!4vE{S~-`AS%y@S|4IGmLdl%`Z4Gef1V=A6LGHse3Te5y>4v#n*Vo`o%1qYoIoY>WHH`SLwu)K)y^? zlgCVTAF&3W!G_h!tO|L#i6_ZXIDZDbVO%|8BQS8!qw62US*{Af=4W_uFIf6gq8d-*DURf|MHi{UqP(i}0ZX6@z$PE0!T?j`!UtLgOhA zATnD=ORu5ve(O|0lt?9WT2v)srOa#4eRKvc;^$Aq+Lr-|nxRhu#I~Z}$K>=mVNRJi zsy^XBIkaaq7YYS|geJ{KeKqHcLvMzRmxh}!qN{pH!`771dyxw-rf4b32-GUM5@YUp56Jr0Txjv zx0RcQRC!g~^`y&{s!bu!uRA1&+qs&sO^ZzE?e*zQ&Z9M#CNd0 z$df#!NXe;J-|mqPs!-)=^QB`qTO~Gz{q;hCWz21VS-KnB=%=|@5$XvSf!-J$>zlLY z6CTsZ@8!qTQcGf`ZT;W>KwAa9vxp@_LQFFzbAFd)!cLtu_K5o=_Wi@MdjB1btM5xY zKrEQ~2TDI^gDCY&9wq;u7vPs>5LtyE<_@IoQ-SA<`EJ&pA^Gs@;}>@^k#KW9PJ9j) z#heurWzNTqdWowfxW1*$K>X!sx+SD@o9=}Sa1II2@-~H)942Cd1&O9)A9_CGqAH*^ z3&x%a`>TXP26w0&@HQ`Rv~@=}_-gS*6dY_f=&cb8FkYfwQ29SJy@Ow-U)#Q&ZQFKT z*>+7ft~|LWyCz$cZBMps+qUiL?fZN0=YLqAbspz_Z2PvDz`{7?C%Gqb4Y-MN|1!G_ z0A+pplx5h}Ap*N_5Cglc9PlS`-aN{=AKle4JSFC0zLHOyEnCt(Adcsl6O${YAG6{? zXR?B&&Lk2nb+X=X)q5je%)2|2UNGGLqu!_yK5SWUx)P!+ilCIc6C^$ec*O4Guljj1 zT+T`gVYnU5B%?(Xb^+NCg)z}c22Xi{JSLL7mr#~NZ+6Qlvw6-~#}YX{k_o$RE2dVQ zCrR6+gE%ozA5Q{CS%f`$Yxv4+rA2U1D1^GboQoeY$ey;=#`(Y z@dqcIay0M+l!K+6$v@jXcLyZzdm_ZewqRlQ(RdrhRbR&-13AyF(EW?%zaC1$RS#hb%r$%wz6S<;%+ zbMJCRcPTRz5^MK?>@lpkpX|$tnJG zR!FTL4|>)sFpKDX5gx(5sY(7B_2-XJFDkP>Ac>7U4J%(1G;|*18P&Vjo=T>F7J(`f zqmi(9$0fs`5NG?Mvh3D~C7QZ_R(|HN!l*#T!kEfA0SEN0CW8oH;eR4DK1hvN-w&4^n@_-_B}0^&bg) zM3j?JxC6TrFUQd$TZhz z_Hq%7A4b>gL)idU4P={O95ro^hWoSv>^|e2+Mw)F$fG*5y_st**bCgvLk^R%PTc)o z|K*G;`#EkU>P>`fa#nY;O74U|2OtGyGCht0anSRQMwogQH%lxYx`RMAE6QoHy0jgF z7e%8tMrTAE0Y}ctkoBmNxpmd7=L1W%H$jkKqCn`k)JTe2#>M@L?(h8o5)715@~lY@ zL+8UzD(WbSj@$&HJY-1!EJ(Bs4$_3#^=if1$v#B-DT7=QNl+TfM)a7veRy)eXbvbZ z;u%;C@8UCQAGvA&PbExa;Q|}Z6vW~*tJvZpZI^PaQYM@zUf%K^{&BsLSivAX(bxqG zJX1i5T3aJb|a-^F<25#ma~C z(rv>1RjyrH5;jpx*@>}Zq9I`w2O&l>)z(rSvG3H>LyWAyUO*8}qJFWayOuD44~@6D zt2Q2U&Rgis`bNZV8-mHLq>l&1rvvwrKm*Stmp8F9agfGp30{q#B34CdHBnAk<|%sB z6)b4{4fre;&FuRM#HrBYe}6tySLpPRMD2K?zAwLqig|wv879}Yiz27>VGL|)1-SZ% zjfbX8&JzA)xL%Rxr0b-H)72|bM#rM6e}rUFA{@Wi{@#r>ppOn(wh>l9kJ+QVO2vAH?;8i~)KGZa(cxNcPVmkWPz@)7>?6@T zK&ux^y}idRtSI#H18iGMVk_3hyrX_@seUPEagWyRCtuzop#Ywscp2me5zMi0BJVr| z(by^;E>-9Is=sC2Fuz=thySbBCy^5 zm2hIzi*kyIE37Ep8P#@ro1T>_;MMnx*=j%qph5+D{^`Xr9`vEjP_gB+YW{HV4M`ow zvC<1134mWz2anN#Un?CMQiV&4yDl}CK4{bdb2@LC35|pYtMMZQ7HkF2&Un7{AV_Z4 z;Sufdb_d5U+MC+MUaM6JV)fhLL97E!dm$PnwRMYk;K9vIB{zQRX&Ody2W6 ziAOMcIG$@gCT-6tuEfUXyJxf1qf`-&(D*60XRp6w84&A{^1zphB}zNH(H>%9!3z0d&`HD59_3*IXr74x&iXlbm;ui1g=rpv6S{ve#myF`HxPR$)CBR!WNSBA1zBpHD zlb#R$3OvGP3N5-F|5Q7c=9(p77yDvx6}ef0ge-9~j1^7J$lZ_DEGhk;f+=>mr(KS% zS~#2gG!8gPvki<5H--xKve1~1f~U4PB1DUD@l_!EH4@P+oK>6WY+Pls1e8=6te116 zD4DR+ZiDQSHlCUXJaA`4HRU34ceQr<+E||qEyl+|x@%;r9I@JkoOpkTPG{c+#>z8D z!_EtYIxRBj$7|r1zG?B^ollkWQOEOVy~Qha$hxqvC!7nv{mPjEZmHEM^)Jq+X&RIY z`vB&0pMEwmW47$jlmH6V*H&*-|NadeZ9eCX;`<~2IrU0YVI>e3=8i2R*?lX4Ysrhu z6S^Rq0jDe{_Qo{IsNck1f8`W&fj~u{b3)&9xNTlbqwaQ>W; zV29we*unH;9@)-mkPzf$$sK-U6fmHZRNy9kQm|>|G^p&$MznBqwo9Bn3NjZIiPHjnkeYWueJaNKdvO@gpX)+w;ClJ_O2?WRh{(N6a2-UTt3&A;x(jw~5Xo ziq#R7_!|qci!lZ#zby{PEutA5lpmOg5^n=P2w3SE?6hDc12gUY2}=o%g&%iS=0$K@ z7GNS%Jg-Dm)Usa;*FC6)ggZT#v%)dtK@^g->cu9>`9ayKCSe!Igxo+yu}&E04n$#x zeFtUf$4Cu^-AOY1*U@-K8QMD;0$77Of|dKMPi@2}O`1SJbdR6ut7{4b<=Q z-xe-IsR8YZydZnV^s1@;oo31cF+Hrcu7U>2i$jk^pl#3^Z%w!`R@$iUh-&GYWE`;c z#96CiuIxD(`B7X8?+EPa0W=Y6>zz> zY84LwY0-sgjcGrMXw8+)rh(DFOEL;6$2M)D!~*+jFs8*#WE$8c2aiv44Y$g3_j%8m z3%UQ#J@OCc|CQa5Ha6O+n>?ouMy$;#TP)_v9MfLE?*d1T%huCmq>w9R?|TVI(lhO` zaJt-8cxV zY?>2p)qb{O0A&q^f5U^u8f^lEeJ^0~m<@>nw-7T)K#uVtu{m?(Xf}hY{tyI)N`>v? z@6s|D{PehLBhL--M?xvO25vsb1w}DQ53)3RYNmF_MyHTPgSQcgWtNS9I?dv)f zb6!53)EG~)UF`kL2&qCEP&UHo0)^XWj@^9&dU(1TPB6+{lS=FzIaQ9gP%2{-Lc|_wiL69PNv&!iHur8wsd1QFbM~9 zmnv6M`l*nma%x4dePBhBV@0Il9auvN3+2>o<<>qE`_w+TS5*ri&*^hiT@!x%00zH>Fz05psqhJWFVS_4R z?11`q{S-C#8aqh3%yR_p|7wG?2%deosP8$E&0J7{pBN81pb6FV5#{rzzYJ*I$roxp z6;rD1oCs2nk|f+VW5z*Ad}T+(fl>#gYrd$xLnqccWoLU?gARIv=R_TGQoL(FQ)ae*B>ne(L0A&pVw#O?DbY^Q3e4qJ=5vQ29VtX zy;efh{XI%VwdED*cU|k~nX1^;6bE|UTb(i-Jqd%)T@qv;|2|G2^(pd+sUY}A z`QvZvD!L%Qa`=>LvVe2@-8rS^Cc~Fl5y4_)g7ZqE8igF-m z@D@#fh~bbDfVH~qiNZ7Y-xulU=HpGGU3R`=^ZzXYzB}E_qe1XG`e?%fDqI`vwHp#> zrD+{cbc+*)?e@D9>RM{3@YuV3T^48BagSDH|1kNd=yVd89Obd;eA7omjMUKr)sZ08 zVj2q}&CS55X9w3452ZzYEqw6(VE9B=oBotdj}p4(uo^E3l!K^5s~6kZs|oC6SZuHH zQJk`zq0S*DL;@*ZusLW`_83@o)XU!JNM8<-_#sBKfPJrj3hWSL(O5bs%Tc$Duwqu0 z#Xx>YdlBRg-sZ+wmo)CsKd~nJJ3U3mLN3Dk=smwZq3zx(Q$L;lT(Y3RBcxGWISIVC zhrBVP4JoD=UFeW_WauE0ju@6mh=u1bLSFR}Z^*tUWUUB_@S+?9*`XL!N~VnMF(HW+ zeNHS&=_zi3}z{FQZIT2an|N`(e}uhB8RJE7b{zfB2w=u^DW3ug8_$u zZ9VFpXixJ!{&xFx-`YD?S^bDHCXA9lJ@VzyLOfrw8GXS}HLXxwpp4btHG_Ix9-8?c zB~Vz%QGsbb>zpR(uzPII1>YduK*php0lRj==R?*KL%ZbG0b2rx(&wg5ptCItz~^#ij{~JzfVl?s>ExD*meL*TI?0Jo>WX|xJgAm zyZKBZ zb{vTl{cMXEg4hFP1WP3-@e8r|H*d2yHr;mE-70HzEC!G`HXI~pk@bmCR1xw#E?d#kKw$&FtPy}FjZpa=j7Y|)`9uY8TGHT`%EjY!ftF!mgHwA@7~jg z@#Fk_J>;K&4B2B=6Uq74yZM{e5p$MDT|#4bw%&=T(DwpC zq8g=CT-oU9!`#Z>j|$u|J!AV5*4^<37(V}t={H%w`-=#GMPT32mX8^2Q)@9gN9ZF0 zxyA|lwr)hvh$U;Ig{>UN(U>g3ls5|>`|o0HV79p2uQF}_Kb#C+bh;}Y^9?z$ma-AF zqm%&LZQ(6LI4l`fixgYkmTzCLOv3BrVphP*)yLNI@MZtYk~2Xyk=o2*(}2ftnXeye zE@BRT7v%iz`+57KZHZ4@0J<*t*@AHcIFQLq8h}C@G+ru>aj4(R)|6jaB3guUzYkg15O{ll4L*%k; zFPFaix-VI-DhJ5&k!ItXPN|sANO1REGtm9j@UTKhmvc-AS_2OM?eq~L&t*jum18Ty z!z2{9eZCVqB?EQK1_zhC33%Y5z);5x&+8r2ghPsY?Ep{7vJdf#A6Ss;ftf(BjIRk% zn6)&2ER)N4FQ+WYBa5eWU~`?fd?f_QviqNx*=?YQ{qf^-`R)2+_-kWvdx~%Ra0?M(N0@V9r&nX%6K}brqdRvTs@{Z%(&dX>T;b>ayTc@%?&~pBiveLZ(Oozb5|#k2`*t zKfAr9KX{`$f95)A66ro{3cyy2@{r=!UgnaDG3HNP&u9;Tym{jP;R zj+tNYXdbIh0mP$iJH4Oa--bTPPuoj+LJ}9sj5MacdFV|)6dJQ9?MtCQrtcebRp^$= zz=B{9jrk*HueYqe3xZI-p#VU35pgf;&zdurAu9Rx%UspZp^#A1vCmr>D90!A_DkCY zMMlJDXf(~Y%?&5Z6n8#WV1;ls>v~7SDKG1K@%~O$Y5`_c^vk7u$AZ;nQF?ttN z+bsE8KpNkV0`JnC!8tu_pXI36bcGJo>B@Q`jqRa5q_rGD>`!^M70U&hwm}4Yt1UkXRgKjwW>mVmoR}Wcu$h5Mt``zMG|91s-=V%c z1|{~Y2uPFdeh9rCJl18HZNYu?4U_q!~Xq$L$q**0v?|)LBZgQwC^#qR(hAA8qj0;AFzdVlKpQw zGgS#((_ja)C^@SsM=OcOi4PBDo)gP+Os4UAVt{l7USTZ+xsjOx*|ZAJ^)vBQ z2x&)QDjhQ&nq?HR5=SA%E(&abrOaGTp!s{Fz!??Dqn1Yrw~J_6e)~L4plcETnW_Xq zZjgWQY3PAuPaWW`AQdy`!_mD!p3icoo1na=3HAB+d2|jlArUBI ztbU{DZs-I_DDfrle2B0O^SzyM-DY00iJU?7;$=!cM-27jGS$e#ooQ%Z%RVkh3?8lN+~ax3i!jA1E==;~F%?tPga5#v0?Pf&*^=77%FzH< zROa^iYJ51tk@>3BRT4#Lv~UKy$~V)6k5(jNpMUtQkrfh`w9;9HTh#q{y#j9hUpX=Z z8W$*vY()isYQD*UZB0#n)1JVQPeWhB9s}S73Ww%he(*veTm=@l- z8o7dbOUzb*<%U+c5%)>^ME_$&#RQ zNs{4nb-nCjRPu-xKfwAYD@r`4%B!f=nR-(3b9nLoKJ2SK_FnPw&Qey`=Gj!NyYE6 zReb;e8-xkom>X^o2w@J8oBKBIeiph1CNF@BXlN6>OKTTan0=Hf70F}~4M;=y9mUDsUfdD;jfBnbgvWksk8AO!Mx88&njlU)7ZM1VWLQFk)r}n?eMbFL)gN}hZ=q=k z!FCArxCv-G7`K-rCE<@6*jlI?oKcTAeHpaoGh7pv8No_+WUy)vnNSSI1;08rN!=DE zSN=YkQ(b-!+H^rLW-RlAkqae%$JKqml*C1N@t4GfQFgQ(RZ47MJFKd$R#HRb?I*BI z)sTDT<4i>vIxI{x${sK($w8OasQakq8Zce-mgeW11_@FL0n$-t^a)@LA7NXgb1uDAv4#E__7xf0IZ?R7{297o zk9bov+^RT{OmEPjS1?OzASNqRE%dMb5a$LLWAT7(aGFmJr``Bm`=7cCkYlXS3_U~T zq!acb<19Tt;WPj^v+&d8cKP0WA!-Ot#&wat`$NAc3KTU6fLcY71Ls~2+BNr$irakF zhu9Jk-GL=m5!*AwQiFWHJKbaWfxI;&pdND?M^lDF#)Hb{*m&mP-zX0$kHI1jNF<)p zCr-+K;$YkJOU6%^;>{UDhc%_Hw=&vY^#|I$|6$GZ|Mlb2wm*-~S3rg(xw#oKzWNka z)4y^F>ye+2D@wLfR4usjx=7Bj+lThuDmXqq%@ht9_{h>~3Tc(gSON?8E>~+Q&aeb< z`jb;Eb)VBE_J%B1O|3MjteVn*nUq28?_BjW{P={E@-paU{Rv7Y-7z}C2_l}BEf4KW zgIAaEmwse5mMoxtQ8)y%L?JJ0^*I(Ia(?6T*0co{MbgU5XxC{~Z1}Gyvd4Kd@pH zjLykXq$L|WK)TB%)Uq$h&6kd5Wao#Ti^;JiXSQJISv=or`6~r2UgW7HQS7@u))M5a z^nwsFy&B5o_0FNb$w+1OOUoN7VINW=JJ3#*D@h@l)XlMvG|jrBUyKYl=dMJJmqjlQ z37jQ7HLgmKea8GblXL0V)v3j3+32KCd>y~`tyT!&0REZ)mBQzDEvXauIxldYwz^-N zY~rjDINM+P$!h($P}fFLLhX@mzYc3vVf&8HnwcVn>#S`=R?|lD4o}`grwZ-oj3~+q zu7Uz!>t*3y!{N>@={3QJtq`hBgK_GQ<_|0l2U4ZueMA~fA%H^l3rAgL-|IDC8m-Gi z9raDw{`1l5oG%Mz|6oj5{*~AFRU*B`4h;9cXi~6O_T)Vp6sZhFfO0&D0MgA}9oz8vj`f3p z^GWvHd1Uv|)KUGbkFjTJQxz*v@ERvMgkK+5UZoNO{7_ue-}lD9(cx~~^@nNQ_)W{g zjiu$~MvgD5$N5+2%k9==U#;jj?KnZJM`(fDofa7{P%hZMZ{M6JuC52mRk|^clbJc2 z_RtbsBKVXC3m&dde^qFStsJ9S5T^0qGoG5069Bix2SAQogHV3uB2>V~mG_#G2Cd(Q zT(@U(VbR9aFdZ(I4zp~`l*2@%vF*4J5W_{{Fd+hFPy}sOyd|1pU`}%06mB#bW!gob zf0_mhc8f`?48AHK{$VeJV2N!jkgUVdi&9OmR&987EHfWCqoP4c77J~B=OCv?sVmQg z$KUgZ>UX4>=t-UbX%hPW0n!Y{k8R4GvJ9EwL!$!JP4^@jqe^~O|M`DSuc@QO6GR`h zZYu*l_ELC7dP8#9oQ-@QQsl>O2cX^hsg}t8YCrX$q8K_{6`Yb#j(Ib0rv7lY1nwe0 z707mWOcr_fb2GecM9%6Ypw3hAM_OKSAeGxor^Gh@N+8I^Hpevc*AuHxdjfG@;ktE5g9WshfGsvdPo`+z-LF)y+g2E@j%Cul3 zaInycAruRa7vAd9X@rE=WAT^UD+fl5{&`Tb9HJ2M;~11ur-U^0g_#!+V*|RNQ=z|5 zmD#bRCHvYQ#ORn)Wk6|eCY#66;B_}_D>ggTnGmQi9olzu3BmeW@+FX;r5QTwO4zt# z>O$J3o0!nkp_D5XVl=S>{j)W5KX$n>6U}U^IW0GOfpxdZKW??=1ahPMIf(+4Y|rNLx`aVIYCJK}$Le0Ox|{PYIKi4q)8yybLyRB%)IvL%~4 zqc&~aGvaGMk+pfSVRA{e;VP=*cwfd*FP1JArBvK60#XWbI6F~ymcFOx+H6W2?etl5 zuHT=U#&Y3p15sWKtv@-_V(S{7i~eRl%4rjDyvmdu`Yae}Y|H&7{doE?fBa9T)NH}U zyR|dqxcz9Pu<5$h@>t(2k%i6Ed^(cB^lvCRNs_7)7C+MSu>HZ zt}NpqP+$C+W|pQEOVv!O0!XY7+!6gvM$#xAY&{>bE8cb^*%x9V4GGX=6qtb0zp_1u zHbQ%s7edAk3cu?tu`9&nPcU7H%P6wMpN`bq9vJSSObFgh16jDtOnEoii1QqpHnKC& zkS|On?Ilb4jh|>5$>!itLq($2)Rs1?Ol6@n8O0=cu?L&yEQv?9+VM6=I^?Mx)VLsbajoG z{EE-r)~hSF*%L=!X+mTa5NlVD=7VsIAAg6z^ariptZqfWRET8R2W0e;{EG>w?8`6+ zl{R#CW$`cH_)Qu(#tEhb!|<)0m6W*aJWHRJB^~1={eg#GSv5C1gfN#k)=Cnl=f|E~ z*~U?DUk*(dl`6Tg$%0eC$qJm3mt5Dc0ifK>R8=cr=?$!+6H?7PNf*g~evY5pz4u6k zWqK?8-We=))E0SAbCrtr&2+Go+>VMO`)RQ%NnrJnTf&snR>SmTH~d;cnJ6aJ^V-?AonM+;;G2Vft)h7=$0z#HpA}W#%xqRN+B+l7 zg)GpxF^cl5nh}HmYVz3`hL06DCUYX*pGs$%Ju2S{4Q4H3bjK76JUb0RZ%eX0-(Q-A zCAHR!m@1Gxk@20eccqTVf=44ji*Ro6HtBDcb_#C(a|AaMG>rhW#d1Wi16()GgcMzyr#WeT+{t~a2+z(Y{;pf&zTSBY4O zqI)@m+12L;2&4Ao0#S8%ZNnKMAd}v6k@a5mVrvP+$$FkZj{QdMw+G+>ga7ma$!a0oI<|d|R^q@sx8==U%fSr{ z&4&(@e&U!*I(CdL&?2Y7n)&CO>(Gvy{jh~kbX1T20c_eVmttgD>lJu$Fv`*rnzntR z+$Q?gDWl@ce?O8V=0uWCS4&Y~ZO*P8jK^t}bYCdww? z{aBnr{kZkSv+zXmcxva;y(lu{19gs_HSOtC2AJHr*7g!4t^tl(5#Jy0H)7?9sFz^rg~sg|#&C}DTW4&Q_3S7XkU+K`l(>qHie zrLYRgj|APqzzq?bce;>5{B?+28Il;Fi5+LwI4oa0PDfRbb0&j*8uR|?rW5pY(e_dT z0yC0gF=}P#ID9tFf(5dXvITxW6&Lk+1X(+;oRh{6P^GMapK{#M;q+6lIBUFlQ@t*2 zJ_wzXtDc43+CTi96xf%z zW7_{$Y=H&$v;U9&3(4w^2R5b24(c&`(XCV+xKml+*dT8=M%;X^R%PD*ebA12U#voC z2$GUve`cJ#xj>Arxx~r}H-UAcKQ-mtnM{y>7-d6A#z4nMK#NYPupqZz2mTrp>Shr0 z;#sp!Sow2k0#uiwQ)wQ3o_E@`HlI;*U7E*=37;T)K^pW6GZcJwBqa~QTGjvOVXCp| zMMVVIAPf09<*Wletw-6a4srVCbH{t0;5Xqf4P9>)gc10#WmkH{8RJ`wxS-99tvkC=ZTaVpM1%(ju~Zd28e>JuRz^7lLp2NUjDH$pc!Ks$ zLnjC-b0j+_#5w0JVY2*}Sk5Ta?Z1?yPA=!z24vM3s=zW8rGkmS#I z?e2}m4pd%>r3R+nK9<~gZVp=r96YPL=+^_B!Z-Y^JcuNPK3Y!%peT8?d4%`X(~gUkO$ zu;oKSqF3Yp2`{sgVkUI7fq=?qZS_f@8iygW1OfLc7%1ZE1Oe#)A z{SZ=kCq$kFBqfDXSy|;88WZdpjUJ9n|2Z{X(dkt&PyS>M0u6IOS%5 zr%MrZXVG=@b26qCLyY5i&qTtkUS{zpa2c~0xZ1~>@+WE&_BREgW;hht5=dKEu$t2_ zO|V0FR5#V>gpqzJ8mvLs*v2BKb00vYG4pjf5R==)hLI`|Og|B+F_r%m_Cc2vQ&XT@ z9BObH+U|ZjrOxV92M?a!1k340n+=41aVP-pf_jncu0FH$hz&`>0uMA`o8a49lYGG-?QRf6d@ z9Sm2JH(xW)<(@C~KZHjXYg2~Z0C)_Hb0-$Tdy+pWdF@Zqp8YQ&K>vRU0WrJN_H!E_ zXK3V6LoDN&WBF01`JYm_2ITnYhRQCzjd8NAa zq*AXG>u^1Va3co*{RboM7BsBRbB3w(wqF12kPW_JFj$sWV?GtgZ6Sy-BvG6p6JdnS z4Z28!&-og|1-ea?34PBYzxVPtzNl8)P>KI@l&c8lMZjlKmNdgin}_0N@=Q0$i+S{@ zHdaEy#~2dDOF<(K<<4e1l_D*DQa>t?67Nl|TaNgLiaZQg8)x*n(pY0JT<5aqB2O94lP1D) zIfc;9BqJDZ^@@LFMGImS^9g#q0hAdD4h{?SEaIBU4&|HDY<&8q^2hpGtETLVl&-VY z5M2^60`YE+rI$KgulIlDkG{72bbKy4g1Hv`gf83 zEhW=W=*MNpM!)KD#_H?_R1o@gTQ>wpahYA=T=U8Qik-YV@CW`X*=Op3#NI8A-t`5E z>DluN)9tpT?;dzv=8;VN^8M zKu1Pbz_&9~KSr;!3Hh8myT7b*Ij&m(|A-wfCj&HN|t7AFfIre(b!=` zD8WAnn7>S0Ye5hUY}-+6_VaiZlfn`UqP-Ki^k#dh03NNcJt|z*!EZgdHD0^Of*@Hz zn8-cw65vaCX*hy~8sPIY;d@k=R8qbE>7<+r4qg#8R4ZUeLzY%?6|pPLG~Dc1_1!CL zL1V+7H0W&z7`>IpCwsO%DB1>6G02-<+~x^PV0(vtbl}FW*l7NHn*P3({94}uuYP}s zN0NMtf^}8W#JbD~@(cTC&v(;#h<;y_3(1y?g#G{#K{ghD7sRHhz?Q}UC@<~U1W^Or zp-&N5M1_`17XgbiA{}-SxUQ*Vw{O)_-p^%MPTWo!1w?}=)9dFi2G7V733yr)=|Oz9 zUOlL&V8`%b5E^TesX)+RBPy|S(kaWglqk#W^)`&dCaypihD&UwI;)TXY-FVxFlWHF zOkFFcAW=LH*=7w{u%inLZYoVr#T~+kT>)r5p`Y-}GV?$49{9=`{D+k2c0$`0p>v@1k?{`^kfZ;^rrQi2A2BDU-!Gj0_&(+9O=mNKPXWtN!Yc` zT9Xv~vW@0plol;SsD*2i^5f8*d^DJ0rWul~f_sktv8$|+*pM8y9jgS|Z=7bO5#hmV zcuc@-t#XfLZ7?sEaf5qerJ%r@w`4#&!n@(gqu}*rt=t>3mkNzOLJqH?`Cfn@WEHLN zoro%#dqhYR7zg{tifGAvE{9?MhI2%{Gt($i*gNI-ud^DeJ7OWTu|q?G0_$H2wefl( zn+K1AK3%4%P`&WcUG=!JI=wz0ALN$A(re-Ea_BZVQ+zqR)q^;;aiY$5o?b$M+!-vN zrl%KxBbs@zeOrj%q`o4icEXXp9k^8p{knY_einD!BPl!2i52vK8WsZ~jZkW_JQKee z2x;}{Z-TnvjA@D8V5V0rhMky0*5}!7oAWNQ9xu;m(Z;Q`5NC!_m=eQ_0M@~2QlNm9 zn`w3e={vl^n00rfF<4;~&LOm-a$~!n9ed5f75%<+QjhBm<(mDPLwkt8ykI^uNNQ9S zkG|U8KlxVeIEBTY6F|mg!@LRXLb#KqwV4!z4Fi9jiDXWQwV(GX!FVCi)FOv!(iE^v z`c~dQM;)6TW4f74d1VtGKks7nt)A$oVzh+a@>?w@T2pD28-K=fS1rAENPpcjC?25_ zM)B{VqF`!K{i9pYHPjJ_kdEJ3wJypaVdDF%pVD=ND^TfT&cfVUVb{-@@N^32O~cHI z;gFCvstl11K3fR|Ee6b4v~ViL!PHQ&WYX>e3gv_Q@P>^;~CD5 z!H1U{ACGG{PgX6^%jP7+O(g2F1*Rw6bqXH6hh#&Z^TK!bDa8<;kCN@{dGa~=9QI;8 zKmaN&A0))Rki0);P8c1|#SbQS)y=aQ7SVF_Sbl77g>$6_F$hB>{5MV@y^R(N)fz9U z$cVJ}(=t$aM+A$)w(v36&UA`|8Nw39kG<4GVt6Vm(q_U+17p&YDW{?O7rjn=EE$^{ z8GmJ)@J~2%3I7l9J&pZ*TL$B#v*vXODwr$mnh}@V`Y@R){ud5U2$~=xl_aFz+X&Iv zpmYLL`ar@&7{pyPW6~bIaP^dV#NjOVM`Ln^U73RO500SHwLk@o*SukwFU@~u1&+V8 zE~N~esNd6vJy3w`C4%9TYT-%`MR4@A6{UJmRzH~PPu@GJ$~^Y3DxmluJ9KO}iZ*OE zBuboposCchSZZU6-&dp69WXTC#*7nu#US!u%rwrQU%zckfikhbQ4OAnuLYFleNOH9 zt+DKspnAu4mo^dj{~#tvyqwr}wCmPjFiGlp0`o&gP72(DBtNqyGB2ts=C-~zY2#n_ zCzCM4AP`!$km}b1lbuA!vOCLI0FhUx1Fdf;h5Z^&A`UeVK67}-PSs2z(z$&nc3=(s zV(vS?U>x{ybawL{wA|-@r~v{KL2buX^%YE6is``V_R7sAiQ*`?Co$~~#zCHwKMly9 zli#S=?R`E*>s#?%>dZ&IQ1Sb0w>_B1G$B{$G^E7|9`C$-{nUu#)D*5Ub@!W=#IOW@ zpBayl=2fQqbUSnV=P|XEJ&zPtPtk^iC_iiH=PEKd zbYQ}>nrvB^`h}#Rv5>@l&xFGS%j|xf<^y+q0DcoKj#e+~q3#?TI;nxJb(Q5n$|c8%div&|y*hTjy}CLJ_T-oD^(7~ zWZB;WV+VU+I}FSCMzK2t3*gkYpV9d*vTXk%-Cu@l4&RuDGYef$>mLBmdb+kOQp!^e z;m?KVr`}fQ3qlKS0`JqWw>Fz3nYvCt9Y%PjsQ0+>Y2^z4R`1fyQ}5I0wxn`);g2su zmh1S~Zz}J?5Kuat{DZbn_mXK3fmyr|bzt0xwHDR2+=~LpB5N+by||8+*X;mTga7(R z&jq&N*fblby3>16BVe9BJLG7ced(;w#`Cl!$#supNQPx1H07=H| zLBq`V8&zB{2qj}L>@BW=;$-+4z#X{u!VgZAY5&2*TK1<4)eR--WhrBuEC$wyRP-P^ zkb1eog`0o+qDk9i zPRHq%u{30&YtyItKs%47-(-Gz3VjK}Sx*|x@=b+_7$jfYFd3S@4vT8TMlHV`PlEqT z8N(-WiZ^Ed|Izf#50!vh*V#DPI+G^56DQlYF}ag%H`O%Ru8EUv+qON~ny-7m_x=md z59?WL@3q#xBJx25*G8=8JLzhfF2Ce-iX@WU7Etsue)@56$Sp~eM$(rd&y@z2*^mi@ zlBc_*mx#drfGBac*Eqk$Q>3wFL-er#G$`Y&NDXs?b37IG0tb%Of&$^G9?$6tuFo*G zPI0ITRup|*7m_ z-g@uiZA`%RSb0UF>wwJjSpq+oH6P3xc`YzPA(&*j2jilXRQx+N5X&JZI6>J{LV(j) z_8%197pMeP+2~{J4<$+G0gg8CIL>4Ab^X1OBS*CW#pxPrX?%*~3b9%aYaC{3DE#=> z*-P6$M&_u{;^TM-+U76K^MSKlUhj`FmJd^GtTFWFmZ%^ZljoV@6}`Ffem(^02uR{_ z0zDZCrc2lrM~%e4icLUMCvGQ6h6LJa&2w3TnB$i+)#KlF8w35wP4*qdfd9H!5

L(tPe>wHd&`~S6cHp(l4SXTo?iQ)d%VPN}_@9XaG`N2_9j;VS7o|0p~jpqRiU-9A8@8eg=>nEZoBMAFaEoEi zs8f|W`G!cL%jGQWnqw@`GP!U|XzW4;oCvvCXJ$T6{b>le;il=&KspAILSh6`f!Daj zvad6cg12>P>KtKR_mL8_%T?0qW}N#morce0ELl?-@G9NHUAc!iqSn1wN@gw@+^!il z`}27kEX$*46(lfTI%0_~Zz_4B#3fBic9W;t^pAs57v>>nV0SRkmXNh5>rx^h#?Dpo z5*?L?HA2l`3yKcXCohXiN)NWYG{bs}6u5<`~^OZclMFw+ z;*?)U&RGQ7Fe_1bJ$TZ|R_x0k%%5liat#9{T<(fM zPZKbFe5se{y27hO)S_=EcrU~GGvsW?I%d%n0cMW^z>9+2itrO<*z9?_DX7#HOinu= z_a=|!nu{NQuZB{X3)XoBTewa>W-WJx%svj3VGeh052qeAKb}g~q9-=~o*c?Y6VNm* zSPgOOaL)1gtT6aH(PVZkxRxY`4Og$!x!NxGfO4_s7(Ilr^D#_jeTKJf&`LASc>_6tOe0sID@+Nq{A>UyDUqbb$K0d@#S_E>%&*eB z#mLwjaemvw@rsG|kQ{DRE>lfrw-C5j(c>{@%&oB4A)sC(+M_sXVD8|>dgxg@ za3Bth{x&ij&BEVlyo9v3!-{PQo9#*>nCwwG>7#$PY1XwY=TELr+$%J9Uut(XF5D4N z&)t&ce2aUraLV`5;|F})-C5n13@K*(J9-#9WOhGr+dF))z4a=PK0Evl4WaI8F739rq6H5iT@z!4J>^WNADza`GJJEvFTsxoLOT^$Hn4pVw zxOaZ%F42F5XnXA+W~4M(`0D+$)|?zITd0B-rOs`uwbp@)e?1|EpNKVOC6;uZG=kJa zbzNgn;HljACGVdA8eHHA669X!k&2fllOJ5mo&K`n);A#2maXy;4s1Cca>YCQ^SXJ8O++;}^+FN?4V_A+#<3Ev>C-|3>j(gd4T$CjuRKQEe8`rIHG^XH4UrSk?M+Y$H%GSm)v$jJ=-iRZGvU!mHq(CnghU1hm4z6tt~W}>{nI-9Oy zKdEz3gyfJhaedpg8PU-j-;+N{N28>ppzayy?g2FC7}z9teXTk;)_`A4O~BB6oUPzN zWTZP;Y!o;#criK**zu@*vI88Yk}6D*r#ff5*h~^CkI=yr>iKfMCPvZrXXKG?^En19 zZacP?P0GIek}tplv38_)Zz?lmC-QH`<9TQ3K6xQTb)aq_0}h=QLq0Yb`sV(JiCc9) zc^v>TgnEA+2w-nyNAUyjj1I`8V4P18V6}*>`|?n?7d-!_5iXF9M80Y{%LH2 zr!IjUFO7Tr)1S_!P1I;*`0eTH)WIUdM~@22g)f%WEyN2`4w$s1Rp~4hzQu=ls8zC0k3u=MiY>H?zTq28!W9@r-cy?Zd;l9uI z@JDnLONjwfyVvcp(&lWwdOFk(9JGoJ^qqxI^aN+Sc8JU6f2)^?lAqT-C zNI6c3kmE4}fTgF9dO-3vDb5=&a_2}(gOCKps{~g#}LJm^F_#ES)PC2EDWze ztqV=HMK(#gmhjC4=$}Mr34FeAdp7cCR~#Zo7~EvczIv6KT7+&Czwd0pD0`BGh~H36 zY<%s3JF{hhiA$^be8}+|=c(*L@{|yS`U3H&%1#3`MF}LUiyb4MPI47o2f1qB$UXy& zkWhBE=l6dwfK}OW_cr)*sEp2V7Bv$3Sr1_U2HT5MW-Yt$sY}fGUOU}_z%vR^qGMKb z!(R*drDTeiz&W+maR35LQB5}CJwWJ8!PQa*rys|}@ZKxHcXH&HqwnrPKx{$-g(dd5 zBJ~mzJ;^AkA>cvy+SvV8hjLcim$Rxg=)>0Y_xrtp}t+nj2|EA`NfoaOVM zOU3)75zQS-r$4gEY8)%#qi^j=42txxApB0x245Z7C|Ft&wm{JxJOr6JJAA7v9B*X~ z3zu|53aw9*^P#av;RRuj3xLF#rCejpvCJPW1Xb3L8PvFqkQ;wM%cJ*uU1vOJdup%C z|IzZxqT5>tfmu#$4!dS-Z7lTOu+o$+0-g?F_QN@P{qYZ*WKlD^MNVRrb@@BCL!-uJ z$_Ni-H;eA-%Lwf8L0m)K&IVrin>EViT4v%XnxoM^30ndB5c&uvYA-yMIyNJ19pWGU zM{;&qRxZ<$vDH{vFZ*K9vz|AB2bK~J7Zl!+naWdKeiz!+%YP3-)wN8coJJ4LT7BC! z*4q0wQ=@{JZ`>Z&=oCOn4_7$z!=@Uq@G-7PK#`I-A&UI z0$n^$Yl{a@g{gTtX@6R@F%9@EZglx)k}PE5t8EzaP{uehLG1W4{!=a^bO?n#mDjY9jp)6z8&r+IrTk z;bxmC)?h0g`Jmt#S4W)#@Qt`5^-B`>iRWpd)MPap)SP{h`FPPki#M=c$xrw79f&|( zTW=UuIfARLIXbe(xj&&QvCQ#OsEkkggZ^5O;KggAx`6vKG=$`6?4HmnGd z5)y77RXANXirx}FD?UHa<{aETW~xKG$!}b`#9df@_$H-#LTl}ZXGDHEA1uKakz`7< z(o$XrTK(P4SI|*b38(e3B4Z%lC~iY)Ih&zzrc&(Yt|sjscGICxdnY4AGU5|9VMSa zr2g1=>Lrmh&35UXWGDAn^wo|E?f%Wg$kt*&!R6JtP-~kG*L+?mYqI2=v+(ZrXJeuE z^nSC@&2sF6vcfZtwYd#FKK|FEf*9;>{*~L-?rhmwt0;5eb)bqATS*3qpSb9vsy3L+I#px#19=(DF~;)0x6!Gnc~O3(h;F$ zZN6q~FEe2KpYbeD*L0pOkC}e){2$P4<2k=NT@0!wD<^LZBGlwCJb35v517q)ZpR40 z_V;ZuFP!1T7~@F&x-B&`52-N`%flFV*>lWL>kCZlFsc}tCn zL;u`M913{+ynucG%(_b}P*%rX3&ud`c_hbgCZ-6o;#E-ADHMY;PSKBOV(~8Uc6W2q z3!QA`GY{N9`5^dS`S1Rp$S;2_QD8SlqA#2Tl9?O{(7Zl(qsNJ4<}nRVky&d8p2Dp^jE-bdC9;qfiDUiaxQl62sIi1oSDHQoq6C#Rm)kuJ277G_F)CTx%$MRuQn+ndcg9vFH++Mga zZeMD*%`iaN;hipIbX8U)EJRa2--%0pcIp{9WnOfzhe#Cs1{)jnyl+QSxccAOll^a( zW`S^?m|%>H4j-TH4CgTMtfcbv{XcN17Pf78@JXn_&FOj1^aR2C^LJmq{`u4pqSmt-50>*)>2&+1(V3jDG%T zXOID*-guK-zlf*zWm3c##xh(pQQtXXhgh|76~5o8Ddg??&5N`1p?5RZP5(BSlCpsF za+CA5(I`*k$2;0R#f9&)9YP`WOXsWm`Zs5H@dYQED=CIeJ}=9`;P@j}bZ28*hd1~Q zZApZ~&VF*B^p43<3zT+RaBL(s$k%5bw6Y%8SbFjzeiVT3A8?a;iP2OTKxb8*UjS*8|Qosi@Y`f&$KHqY*xOU6EaPmD3L-sGoyKYQhe zh4y&?+p4XLT@wipx{6EF1%X?HlV;tqWqSHf!WU@eYMNASUSqC7{B-p9xq+c79X$c<|KCVyheE zF$$qPPWr*FfBw3uKx zAyJgOaKZDowJjKGEZ4--d&C%M8ZnfUl9qqRP*@~4?raS;w--49u=uE%GY*b48?z&g z_QAtyN#A64n?L_KS(0O}SdDHr)!=8?sLi^huY6Lxrrm1yi)#NIt*6c+Tc$Fy8&=9 zIWSUdxI-v15l7&Jjg3kI0CCcux+lB>TVMZ1jJfw;XGTL6vw-M-xxCw`7h&>D-8YS7 zPzHYq3b_y`gO=T+G9Q-l>? zsMj<4KQH_0koC+E!)ZUkiIi)Gp1tdz3i({^1+#Ur>d|ApfBtL2Jp{>J8m{U0$lE9c zkS3&zFLtrv{u?#QFBTcXdMqN}ojzh2gJ72d7xf^St%^e-+xD(7suo8!Y8HLG4TE|% zqnK8V>O48gS>-t^WJ@3$tw&d;pe;F5$X3o~dbf}s!av71SrQFD4@>lSm`q+}=H{AL zw^8B?*xp_P43SK&Ta+_FUcuq+Xma5%a4^*6X4JeD#4~3VxnENuzzq|zh%#7d6sxlu z2i#wt$h)^4`uf3%vWpv-#tv-SXt1I^YBG+a z*2(=wRVI1FKg}y*%H=t_zT$>XF_0g^;)gQw>h5>^RoxgbVQ(G-m-}ZCQSRvg-mBhb zSUk1MXfMd^k9Y3%Lz9oN{a4EWX=k+mXy;Ry?)ddi^gSnHv!`qYh!&@+cnJ=@V#1_ehwiwW7@zxT|%9EPioRH1NUV}9hPkc~A zCb?dOkm@Q&OOiqmUgQmHk(bTIZ+d@;v84ASuGc&nNPc1LKhq_f&D@qpi@$ZR>}!QK zM+(c{L#O*K{qKob3l%C(LG7W?CohiIm~>N(=A)Rrq)TvQ>?2U^s=KX*%f8%~_x#H4 z3PD<`Y4Lnrg;g>aNoIA%7oA=QDwt7iJEy-9QfOM?BEcAuF-Yvj&7+&h0A)2rs}g)4 z_1R;{{IMMYSzT46SkAmfP+Z)}p{o8Mozfat*S*la`)2j84C`q{c@VL`M2wOYpOY!| zwrJuDEkUjh4!inH+~;JoLA%Bpd*BbEWI)~vo_B}7Jdrn|H`K7EFlWZK)Jaxr zgiNc`ASU!L=XmAPNysb|7}|V@{Lw3~Ey5Q-na|KniEFZAoKc6-_KJmYhy{QEqZIih zYZR^+JugKVRv}||g+w#AD7c4t)eAc}ZtbFG)!5)*^1ilt9-97$Q2D)w0(wj$jIrMu z7bFG;J6_fdfuPirnw}5N<^O5zBtit>8BJ?P@n6w z{)uwk7+Q>$8+7*mjTP8=9Lo`yE5E^405dxtf~_36CG6|J)Xf=B_w>9&^tly>+hc2- z#%iY`8~9Tt9D1*p?yJMQpfzCp(H*wf0$HnjS<(MOy)fHc=}!82>VI6^$Oi(TlV#C2^mhmuZL@`SRhkW4Tv zHCw0t)m2>0>zTCI3hK+#csaiM^bMX_$s{MJoIImeHMw(wL6dHHVQHg@fDe$mYkOza zoD9Im$N&&Db7Q5M5Zeu$)fF{AZW101;1AClMZpzK=iM1uag;VyInxxX0HmApfPFn4 z7MB>Rnc+s{P&;gVMq2L6|^sJ0dWX&vGBf$c2G0~Lfy3-0_eXY z2r1fV#CVGm#F79lg^vL1mm_ktFvznY=T=1-1 z067~VXLNkahVoFTy(*L@W+k7igsnAdegUV9%pfKW0W)}}oVCJ0K@9Gq;3Zdr%1$sp zQww0JN+DKG6QW6yIp`_F%&T*ow#SL%LR)|^4rIg745@@j=!E=zXa+~>)*^!o4E9ao zA(lm$?BxA2RbWWR>v_8>{bVbvCvu{D?`Yt~6$s8Wg}a#l^XQ*p(cxbcw<2ov9|OV% zDPZ|4O|Ub0;{w)2{mP}X?up_4;uJ9oMaJs*O_*Gi$*C)2G!y&aZT-TVFgrT2c%;A% zT53%66I%`S zH=sGJF9&P#2in#jcLYU`Gjy6h)?7!HZ{N=DM0jec9j%R_XfR3Zzf?r2%BSkDqV-mE zbemsVup+P{%NxsSq8j>QzK;(3+#A7~T68;5oj&bGZT~n8Ht9+GQi7u4`Aj3->l>eL z8Mu|N6fMc4>gBs%-jbF{1Y5zQ9`u$UAC`~{0L3q+h&|3N7?fEim(e4LPU@8X&kMjm z0{3RaG?-zI<0y*cZgT<5O-x0Vbi((crvG*2d#-w}QuoI~Zs+^>MDA0TJhIRuPDMKe z=CWH4hqE?zV6?K}-F3gGW@Sp3j||sBZ|o8G`ge56t%A4{(9#f#PB%w$w=2cF_ou~% zh9u0jq%IiI_NUM1F(|J5fiv9fndlAmAYrqz1SMs7OEEMCUe1eWxnj{1vvohyCCus2 zeyn%;cDL{qQG-MaPs=}ESTiH+Cn8Gu&0!6(9g_*z{q0?LAvE7QYxpEtq#!TfX#*)d zD1!P^w5Wj4jbC&9pI>}BpCk8_bmc~s^V+}t@(v8vP=lu*4K;-e`$;Aej8@>iLJuOW zm)JL0sriS|!z`7)(O%TUI1W|3^QO-estuXeT#B~gNDFr6L{Q?m}@iz*&NAut8SNW!{Q-P<7gyN<*X{?{c;qT;Aa8+ zVGnmauCGcA@Ctfi>w2e$J-*)fp*_;{QM{J^O#D*l$Uzyl{B#b+!25?AxMLi{pC~QW3G1HOWAtG;Pf}lxua=BhD7KRk$uSe)oSKD!l!uls}rY-e#blV<(92!p~ zL@bTi@mX1B;FST2HTa3i$@&6=VG>`<7(R;;>J(D}~@6Bs#_gO;+9T55g;UFT_=hKfe2*|5$tV_W#f=FAW{ zPML8F9A!9c-cNi_I5qyl=fb9)eUUX4AqfM*cV>+X3S|nnd|xz=w?eB{#qaOQ|4M9; zCN*{Vi%7~;-2y@$QD`LlU`Grg42u_g)7(=>-;$|#Ejt0oXzkkN;s1;2f>HR)Kvy8l zMx!=>Z`aA6k>u|S#!?S(zR;gwDpoe1>iwyzPTMiqsX5VDEAGvJg0LEj?C|=bGla_G z6Bf3024yO7YtBb+3X?k2DX`LDvtwOoqB@TB6_5hn>y|g5vjzpRO6Z4s`RI)~we|v; zvTygu>5L+NV*>XClstMa-%RKBHa1n$ZrMEphoX1u)?kEB%4g-yEi}nZ;GQLEc1;D; z;FBLM2Ru&4Jq2|;)*W@0b?6dh?ls>Og2L*=g=-FjH!Qk^%v?f;wXoHR=-SlbPZscV z%`OGM_aC`0KLk#QRlTG!Y|N~uJZ|?UH;0Jefvsk{Lb{jfNho3qG{oSQ^1ldV87a%WT?AL9 zpTXM_Nqnqak)V}{=>66;vTt^yRyBw{-MX&`$xdtyIO!Op22nh*X(L&r*rI_53u>{Ui5We*G|QJ zP$<{rgjutK`I|djR#Bb8Lu*F&Q`vBD8jyp@zzhG(gq(o~Zjp&u+;KT|d8>sMgzs6# zp0~4RRk&J36#^tE-t`|N}uMWovk>)2E&VQQ2u})C0H?ww3w8#EvMKv-tS%-`i zpX16_)zX=<-8j&4_3P!uCu}~_}99nU7fUm?8vG3=o21IBjlVL zs0d*r%OvWO4wtEr+vh2K@X)wT7K8*rC=*T(xmy+|iun8^({Wh~=zYdb2tN=fYW4JO z4TbF@B%ul;KW#XG6yyCw^)&4si6KnE)Aa?RG8gF_H$0Vy2UIh6B4PUt->RD4`d!9r zBkixyJm}iM0YHVeyUT_f)M2R`Aq9#-l|8&{Eg-E}{=-kp3$8~%iJV{GK{HIQ`SJE{ zG|ss#=4u1(=ltLK5>#%>gC6CTo97!{F{%?W4nUS8+5v|nM}#Cb8!S4)I8ilg{0X}B z_y+}oJXT7}SKJ*UF*iQr^8r-_4&Ha^4)EQyXWt`SM2AfPJ~$C4T^mVS}%uP zHgH@wcV;2`r4iJeE2GcZmM*v(TpkK%d!4UpaUz;8C1@CXCYPm|osjyQ>5KX8X|T}k zgYo~lHr<1}6?oFHTGX_~3exkUNU~Sq)$eSl_iHR6xr$>Ph8PSJIrK2}PzIG?V)Z;{ zoDQPLn|X8+9Dk<;3Gm)3V`d-og#=411?3YQ&;+goxBH%-P)>Sq#cGN6z7?OQ`FbuU z9eRiiU$VBj6K`4GP|5$xUXy1Da?*mt_q9 zDzR6rS~qhui~uv`6lJS;**PE0zD@l|(zldR=T$c9C(#1@w&>gMdw@?4gU^)nAMQvg zA6k)^SXf52z?M&>sERH6i@ieiYQb86W|PPHt;A;{wCeRzIM1Pt=xp zm@X3+h)hHneo>+6e1oj{o{>8{lBlngqUQ>!0Z7K}0SNO{npUKfGjSS|f#3NmzC9Bh zO~Z0RmVv~7H3}t~_-0SJe?#xFz`BOj6l1kVVmVp>0Xd{sFjR^F6T!$DI&o_Pn zBC(SwphBnZsRfPTxN1#O$URb4%9pR0|L{HzUGSe0v5Iybjc6w+s7-zO#`F{7v`YTk zO&qH^Zh)icVXE->gMW~62+M#l&%PtERcM8FQ^xk4cgRHy)#Mq;hBM1{a7JlPOP9EA z$RPgq@-T8qD~pbl!6Jgz1uL6L`tKfiez7&j-FF?L^iNr;#s|Ou^gueMT}m$CdYNWP zw%rnQtj>&8PEY&U3Ct(7ViABb(AMqhIg}<- zI+jXF%Y9&Lx#xxruH=HHj)j+y=a)0mVzutwo&VbgU&RXp+a+efzXTK3O4q0SMwqJp z_S!XXERo!Te$#%6XIV@7d%6{h-?C?58e-CJ81E8DP_r=LsyD$BJJ$KhJY|zhRGn1}U$Sycici5rO$X$H zxy0w%!GMZ!xt}ujpKDzMecQ??1DXM3hm{SaV-MtFjTLdH z2Fmw0C3YnKt7Q2a%2RL0AQjT=ICVVfYdzLYvAV7|qqGm_9;5q$51e}sYSU=5GY_qc zj3)_oHSQx8%9-Xb<4J7ZFLxQZtsqPuwScI+_C$rB^{1C#9pm26RK}Xer(J8D(VW2f z6Fq#1(%{gsHFWtO&mk#P7vg_LacBq7=8m)!4p^Mx=>QBQ_`tk;3qZ=-7CTIJX;7k><)tgjWvbn^`^p5QTGfQWm z3KA!tYDVt<^1u+a6?8ui`9knCS$%%QcyXxN+|P;yW!Up;Yv&2hQgosG1-8lFQa!u0 zvt(nQjB82uvHvmLo*#F-_s_A3)Y6U?yle&YwY(uJA%J~31y+CrQqLh-drorJcjlpihlXJqiSy@-G>%&JV zH1i_=uY$*8&(EACL!-j_Fz0i&hEhdd6)Ev}t8_ETxo%AxqYuP2Q6-xfs%THJ{MprC zB!f3|^4`5nI!=a3gwtgX`o5dz<=JO&Fm=h>TJIv!{{q31(f24+(nrpwO+lf&3MV1Y zLSQ0u#^0OV#!=s32i(%qNz$sGhX!EC1SM?cwOvsQE_hPYk@92BTst7>p`-Y{%8{G1 zM>G#i0#7lNE%WOD*F`zyLHA3b&4{x=5>{1r5jZ|a!d;b!V=+F7>>mdH2q+5@3mg_;ckF||;fFG*gm#sFf06myRO zb7@X}<`<*z8aJz#1^KAiMgb zYXB1tSH-X%hW6#vL4rm8Zgu!3NqlYVg>SPp(9ENI!uVI z+sb%!s^*P$MoCk(KAEb{O=EDr9L5Sg@>|@(x1W)*3Zwb7;%|o!txO!X!00tAd`^aF z8wLgNwRd@=&X%fG>2G5su+E?hXV}O2jJ8r$nGrIVN-916=X*S}#S|2mc1&v|h0cgHg@Mi*OoJ@nT_Iu=CL8&d->m}+2(FG zq(i?h=WiQprjq!P7%T|elQhs22NpL3c{(5l?a>9&M9w_u4F!rm1o>A3lz40FZxK|w z4NH|jPZWnw+TDM(nUTJK?r76AWK#ZMo$7oU9o4=v7SM_)K+|QPeSP7M;3As5v;yu0 z_u}i#5eOXysr*YchvlNGVDZz(L)uv*Ef`o}`Kg}cmh=#gOBDu2Am++42IL~XK=bq?Qy>B>t}e7qXlVFaJ6%1m)<>BMYcjDGKb6~dBE;J zX?Nx+(B;Fv^D*$BY~(q`l|$esF(ZieTTWg`vui51i=EVy>7czX+~0|i&7RoChjY)3 zdy{uY%qqIIkg%jy?PgC;>Z*kzwPjvUvNW~Gc$uC$toJHTJ57xwcWOg7h(j2t7^|}k z&K7U>JS}T)`rs;IJzcr~f@%*;V=V7f4|Fta_s62Ci;!ic>lr~&P~M{b$N2Ox-ZCH! z>99*P!)Qmm6-K|iDO)pt8Sq7T8#`^G)Y+M{oBXN)I3IOj|BNzrs00kdrO`U6V6Ys_ zbYX*Cnhek2!3SYSitHfmug=>U>%?YD+&^5`IC9Sn$Yr8sKRAvn7=e4&X>#1N`!f>K zt_FXXDApg$dheX>4Q52@mY`lF9HLb#=NrIVOhF|=K&gF&z$q!P1Z9u5sn?1o(~q@5Atuk;vYSmgzZ){as5xQ z2(=e8%>cUXdvqp(MLlCdt*Z~+QypHM*g2zzUGGQ#7JMJN-r##`y_scf_zDHjT7N4z zq&V4t@9SGy879^*nJ&4$>FGIqT)%nUqL&zMG?K;s@pZZ2A??R@w4dZr&U{V0&HgyJ z9y`n-6Lk1^{`CFEal1P8GB4p}q!=o2OB%*;(HNEkZ36^mJ~*<*ZBj973%_~zQHi(r zrnwPY;Ss}*$r_zbq0FVo_T1=gUQWu83h;Vf*L2q;QjuWB=!5UR#xjGRXA$w?(RG@a z+#sQ<`AyEc&E{a_{;m0c)%sBUchc2~dhHYRobOmnu0LwO{YPo^DcS`$_3Xoo;!QmZ zaj16(SwClrjLUG+>_@>>Q5;L>)kr>_Tu(`sj2>j1^C;}(p07ZrgI_YVU+=`m$>E8{ z(Do*dAlb%uht;gr;)=Ki6h#)&#~UZxSSW91IFuxzr4Dg@GA2n~9~`tADU$}sVx;)< zvQ$hOT^JaJe?$InTbij~)0jlCumta@uMXn?ddDn8o*YhW-h zC+FDa-*)d-r0##n&?`1?Gil^|XM_=oCChsPnw}l*kjFmFmasAlwVM7Yzmb>=nE|u4 zPjCT3@E`!|2gvBF%-EvsdqD0J_(=(fqa|#xOJe>Pvyaj$&L6m%ryUg|o=h+G{$7 z!Noy)Ru8g>qMOL2hWRwIJOa|MjPAP{@_oapm~Fs!*tUE%BCXO9o(=QvVy=~I1=trN zl76jUBrL{8^XT2jeJktS?>qA(cQXNu?<-xOl_##xb3Qpp#{<09YE@(&If(Jz`n?Mm zhOsJ#X;}15P7<5(10XYWf#^ZFR&gj6wvWKqud<+M9lGX|7=u(e{y@P#O~X1k&1%3!cg!Ka^o*6!&7<)o z@)I(VA~P)VngNa!B%q7XJsqbzq@h}t;K3OlN1y3e8!+29fuwUNyxYC^fy1+nmBVX; zIkFvtiqcta;Rvbu%-pZ%H{-!1W$TWI1la712XWhr^pWcoU6))BUhbF>m#nAXUumJ9 zLzx&Qxxq0Zn?4)YxyzVMZ~jpO-6JD8GkUtKicLI$^fOz<5Voj2;{M66gBLt z^PIYdoNT(uBO6oDd@I_lry@ppuxDB=dA}1?Tauk$PMtTy1;Q^@{yJr?nqnrUyleh% zDX$^Z3h>e8{S0I;O`8bw%KH<}q8eW!z_ZZr4MRaj9KtiVsvmDWI}J(Y3|F^iaW zKqh9&{GuC6a^GPA7P-G8l#fww)(5nL<;XAdA_g7@${FlrH0dD4+_K^QMbKoc0+OV+F zG_g|IG4P;##dM*AX`khFvvn38VZ`5o!(DCDy8#Q zYv^aC<#-Knj-FuO99)_di$M`c27&;SMokSyvL?_ww@EKF(2l5+L;s*FB=qHeBBVy9 zEM=~Ct$VZc3%>#g)TV#ihy5!{{78M_SF~j>YS=$P{M=?4v&mrJj$l#3Yl;s&o~G+; zAaEU|Ii;h1d(9Igt4m6vdQt`Ns;|__(`)Y$5gjhX9pN;_1CMeZ}N5&!4%zJ&g-%X^tG|bn7lGj1F>b*C z%Dk#rb2{ciX3>rwyVbwNm=!@{qgd}9Mj5GXX%C@HOuWAP-{#g`UGz+Y>$1*lJ0>z+ z2WFd4Mp<$eO28ec33GK_J8z8m(i&#sZ9vX5%G*;wK@n;&JJz@lZt@=qhKp|X$ItGA z*9UdZtY-94>x+Q}J0@E967nSV{Tv4s)5W1-b8xKys6Ad z#LfsDgO0p9?S7^dm%3OuSIBob($ECfN7jV|$+D@Yx+mgT3k4a$ZSig7l)Kp&f_3aG zS9s`v7q9XzP4nAN$T+NyNu5bPrVGF&#y-pJ4ZZ{Hoe{v;(NQlK-JF>;UPocb+yN$U3$L# zTuXVJ7sJu}&c$VHUL66EoSQ{$N@IOB4gX|ci)A(-9b>Lo9GNJbc4FFKEUn=?9|!TfgPQ=GO` z@Z0Vdzm|{Rbi9$(`6PcVEq3%9yHi53^GTPe+zFi?_8?-Eo`*-Uf|zGTjM5QW1p2?8 zqJj~Ot0Mh(dq+Gt(RPEr7rtPyXh%#Xgg%|W><>s3(h)&&5_*;zHW8P21J| zuGrccPvf_%tQz_9j@}dTn3R9R~Kh? zYyU5ssOvq+I=ZJb28(WU0%MOAme4NSig&NIPGZtO*szlAMc4`1ZO4|gt+exTKWmCgUzXj7($%~QmTTIxwDo3v zuW^EUvWn|*KdT$O@DnQ@wp5~zw(dB^?k9p@KRjjFZRnS0J>1}C*Am$?*M>SS@s(`8!Eo3+Vr zAdH&LR2q+C!<`8RyiPC(c4FOT{@$f)mT zMx9Se-fP!FD`#mY09RT)&XYe1rp`aQ>znRCP+_?N+OD%B7B#0uuQ_e${SYz)&DJ?w zZP(k$vG9~w*P%zQT}bZ5w%7GoObmlmZ+9y0&Wu3mY0>0#mI!gWk3#`1jQ;sW3E;LA zP7P#spfjn%Eap6LcRw=`bi^N7tGmsfzo|9cJj>dZX;&mfNXMLXPk~Q0Tr%QsOe}04 zU(H{FPGvywjG^H5LSyny^G};+8SD@WiLZf3_)6%y;cBuv#ON#ii%up48c})xTK|wJ z7tXbfANdepiNTU-&?A+n=3P8d7z)?}`5h32@mw<`bkeeB2~*a9ZZ_(-j%Dj>>0HPQ%bz#6ZfYq9kvR|$(2nOYe{uT@d&;?IqZ6W&NbN`f z!+hfI_^fON4~P(9OfprOkn%h8FGF2Mnj!rvp>e?D8XH7JN2_WHHxnpPz5tgqhXf0I0> zuz!ZhK+7C-Ni;CX4HKC*<7F1wR_E$JXoELX42zHr4?0|f*>z^<^`jh!kR3J0N(s4V zTdi4MFN}Qzb$*#64OsQE>~Oo|`8q#l<-<$FpvZ(K)-Eb4(^B@^9`EH2zXGo|{g1~N#= z*c>IzK`SN9rGo;D<3c#eTVQI)vBv%vh>B;qIY;_BVi0F~i^9n7QBnZIxPwBCGljLayu?V+sayTU1ZKTMIz&Wo+9GLxLqBZEJ40ZqqN; zKe1Kdxw_vIz0JIt{WVZ&(_y@LbUyDA?&aqDhMX}(vo1K$px+LH25smbdgvKNaa1E}=xwAN)%8D%kt#47Oz#ZB z&ii_0+>~`sM|16a?%asG|EF4r{g=GSxYU|7{=xM9w-X_!@tE(8=9QpwneYE5KMqzDpDOv$-QT{X#0o&$*L=tBn#$3! zveY6#0JS&Y&D*u@$=fC-dkuj@hK`iNKv`sNj~bqZRL3d6$HtY9kBF zWSZ-L!xLwin%i*x#MjYL)P?wY796`o*fH+FQHHd!aF+u%H9q*F3h(r2-NsrqEruY? zUe632QIMd)ni_{}&Y@}oDp!pp^EfV8GbQq;dD9C6wb4sC#C-jQN!}bg9=WoSvIT{y_nAwC|_@H=YqCrR*+Pr8#6-a$GY$u|&a+X6- zaBENM_vH#Cg(yaTjvKM z1U=#`LJHi^3?A_d`$*~X?HBEbigDD#`r=hvLUATpsZ?t7Gc`uiA}+A<$ys+_DiTt< zq#BR^!Z4oec#uxox#)+$Z`VS+ptxQRYUwzEg)#+QopuEEmz1W=qr}mb*um#4{7m^{{SB!TJR*_Mm_w;ThE)G6wLWeyO*}@~0|(4d zNObevrB28vH69V^->j6ln|-gyn3zn|meK=8!pLafEP(iomURhyfRV$xUsQdhnXtPO%O5Y>Fi)Wz5mOMS##SBGxbsd198$2 zfj}@7?<%|#${~NzHpSA`Ez%ygkM2GYj4}!388mvEun5F9*D1<+!Eg5t7I9!)=>HvV z_?8V4_$CK&8wJAdGtTA|v{XfaQV>s|O~5iYAH!>Owt}Vc7^TZ=)%QZpLKJFUszrf9 zw1uExu0KQ}p{e+^DpEo=*Ze2|#~Rd!k&pp|^PaV-*ZpqSD$lV#U|j8Ik%17825CCc zy~PSAVqnHzeC$cBtPZBW4n>G<@`V6%rW=%wriVqr!PjS_9Gn1TN2@<|>^ab&WXVuz zY~FvYx!(t!O?_T5c^6aC#G^Duk?kiTRdil`(jmWpO0F($e)hF%*(Uaw9(<>7sQel` zTq;#5=q)e7_?5z+hRimg*@Lg?wyfUg^}CVLtG&ORh#UVmj@14eM}bipFT|m&;k=Xv zfa{bZHbY9ftots}-{~+pEF_#SV%bx=_2uHN2L?n#^gL&+vjS$RHfnO>?UBe8AJpJ0 zgq`UgmN%agSZPM!50XWil@h1w#r}a&Z-c|tZRz-)Z)wtgkdO4cDpy%TEvBzNROb5U>{Y*k2msm zs-vBVMg*i!MRFN$Xr~ZxXA^6aLo+9*STQCv7D++u8niTr|Ici(d@o7!M&jZ1$x<$^F#J?%C#-KmvY-bX%r5v zNEOGN&P-DaeQ6$$yWhv1_acznlI|q(7x{F%j-_jTThxKgpY@tR^tsXKam z($s-z58^4Pk)lr*8|l3?*p+!Y1BpHw{Y-q|+^|&y>TlZh6s>x2m*M0(iJ{{7fs$ci zLPc67Y61*U@;l-`*jqWn)1_J<4vh9YjHeb1vpHg#i9W$C8ih0&`VotoF!;}$upbo= zoQMtf&1Lx*ECx=lKb=79_MY-!hTg>5v3d}9$#BrPCpZ&}E}7lI__ymo3>D$q3Uy*z zBRHW7&_%_r^s00U){v|l^_?9d$*q86f%BjsnbL;^K)_7hh(P$)lF64*g0^JDlgWFG z^cbn#?sxrk9Va49UG{u_y?K9A96RZW^m7zy0pg)*kKEKWLOZ*sWEJdE(B`B)(N@^K z8I-W>3+6qJZUgg6mDd9{K~y9ND_-sDAIt4*3R&b>56! zd%5TXOB8FAk)+%nf1t|qE4_#J&3)O3ku$9CttaH%-qG&$SHI8D$yI#?%{4)?PfnV` zyIy;t`0??vBl#%l))dezk#IfSH;qg|fVeQoe18}O4zCAVdSFZ%3ZDb$2RxFRI4oP= znSBwFzcJJ5Av<*0W0&kMw@t>c`C%%LC_Xr`_{x!B#4DS2_@HsGw198muJOTuyStRj z7m3ji1j2M0u))Ab&eSE?49{i9B>i-y53GF8BuM%|Td?m|C$`=sq_FRaQ`)G{#(!o& z3WVR60$-KVE1xLf>BLrgh_;zb06=hEW7kXemOa3bsJV5-{(5p?5GreDW)7Y6q)@D!)e>ymelK#s#O4nKCD+$VX>KU#8rb`_qWxYvsn+90G zT@L)<3gz+7BslP4hbPW*rKtp<|=gQ>q zw)gbni7G0KlAq$rBpQYf^W8&abl+d*wH|lF@;q9~0oGoC?lL}M$gm^~;{f=6c-;Cs z2IUI96MST67{SCa)6)xfRN~7^DLVxN!rCma!R&C5nIuD}ks#HYyjdtgblB)ut*-+1 zkDW7+j#si=b(g8;DvdXHte(Qn$N2u=;xlAMKZ1WOR=N0M{e9_%Id}>0$YjW6&jRUh z^R&J;Lwgz9ffLtdc2Z^U}Q{``##1P58705AfT>`AA~UA z0y$#?i(4aUX@KEznNf<5FFyh6TDa5&vcJx)gJzdY^cA<#yqyr;R5(0t@ENTTv43P@n+mmbm@7|~RZPJ+V^!>1qBSV~!{axb^t=+Fn6-bfM zg1a55x?Dp{NJ`_8{L)>SmSoer!cv$h>FIG3wNG-nutW2xENZuZI&ZhIti8!eRzeuP z^N^Zy;<10GEUux7f*C(eW7kh+D$MlI(x)AtcM(qXxDaR0Tq|~In4sJg zuM-nmBhr(~JBS0Isov+uSz5jNX55NUo}9CXvx4tzxCI@&9vRy{@xQnoE%*`ee+2a* zk`UFi$HaeH_HlR6wSgypH7*bbON-2<)6~htD3@{cQr7NssX>(u(#OV8+sT#Scr>FH z1q8$J1uu5k3@UTLs6UYF^lL2z%{VnRru9Kgx z$z?k-*5&K}y=US2X{g5Ss}kbP8G%WdAAu{jTg0!6UgJ4jnSl&(#sB**EE5|FJ!L;w zbl@_Y9sxz~pY;kcE0#4uDJNmNVnWYYnP^nYCiO~2I&nn7j)#T3=oTSkc?hw#jQ3CG zhk=>OBcAYi72o!sPXOF~#GP{5zYkTktJ_EO;3l85AL*?r{ou9`-Q^MUAZln|$cHfb zK?XA`)=r~8n>9%^tzOl@5|rC|Ej3ktiR4H+R}zk%Vb{#GaZ{DYnGA%y)Ca}Sz(%xC z12d*aZAHHBJt@|8*%&F^9;Leu_VBL+nKvvTo!!?Ph}1(|UlZb`2K7Zs81Gwbg;x4^ zNaw_cR;9!LFRF8iKr=~qX{ER6iN%c`HLiyx!O$OXWm~Z<(Rt{UO1ZJXWY~|Uxa#&) z1coAukLm_je?p_#*dr4XdE42|J$WBenyY8SkcTZna8{$QNm|49HS~~+GafO7$%*J)r6yhG9Y?)c*1z=j5S1wf64ZP(1H8YT zK@<#ZW(%Yi^uA_~i>1@V#QgUEJZc-ick$T4g&PN_9hj$*oFl!G8_kJzNT`Zih4=x| zIu|%a8Twzoy2fpW=a|qjGg?g5mT{`|_J3DDyEgqPteeoE!KoxRYEms&CbMFI($r0P zL!b+EF4CN)OkqI|+%K{`xhcqT1jlIX@D9Yn{h2)lwjDw|R%|r|i`Vn=IjOA_C^pOt z;mROR1eM|Ea69fD`LCvl8OeRR3vI4X*uFtK*-Rkbn(6CA${>&Qnwc{*Udb2Mmv;7A z1To;E%aYsj^R^5c-}mY>QhKs%^d8tGGBw7G>m&@l%Za}I0UU#1br1$W%}JRs$BQ(3 z%J%KDQ+N@&BF=bV^K!jLJ&=^5FSVJF@~dkv?0<<(F;)Z{h+4*|5&Nc4R^R)&gnjZ$c-3*fnWJ zZN3(8R3`gMvwFLHHa^bJ8{rWH-xhO_O0gYDx3+U_^<4Dk`+bq^YJNa9cj+c!mqraze?qw6M)h#cRXJ0A-Y-XrmTA6R0#Go=#M`Cy{U1 zT1T}Oc*Yw#|8Rs9Y0gZwI?)7WXR+5GIUf6+bHU6z~uo?|Ng6kW$9H&M0!C0=AfSrc&7&QH`2>81XVth5btA#}b9ecfAz--Rr72{&eQ-(q0WfTY|7qxYj3Ffw&s3sz_!z7a-lexZM6{NFjS z3;v(Q*6b$&t4S;!p=5&JBlSwYT((%Q`L!8uR(-o|CQr-$04=`K#X4d_#CmHYr#`Rt zkwq#lok13)*tzEK2`AU&E(gZ1kK5r4Qi@Mhx|3%kXLseUi^~`SaL332gW%zDpye$B zzofyBk#fClGTLr!if5bU>a5ZbONtZQ@r4Joi47KZcu$STzEzY78C1ga1RK+$Sxa1q z?ZuRzV^xIndvaWOF0jayrl1oqico-TP1tPjK{p5LrljJvT*7Van$CC9oDyyu{`I!6 z@yS_W^8i|Eam1kPPaP6B1_<9HTC+Qe7R8pN1e{au{7aU$qew#k^|kau17rQ5VHca& z0O9AdVK2NK^^az9vS_?LorW&Z(=1*o#R6wB97@|gA-8O(Ky)@@&ft7Y&>uC%nmy+8 zgVP4oJx__$=p1i4bt{a3pKoaF%pAD<=*X1HbxrmJ`*;M1{RE9U=a=rz?0jm_cE$Z? zTvT*AQ@6?(Ifg%*J8n|P=6Z+-%qCfYX5{(#wFmvy!=-`wcz8G@8u%-LJZt?3-&iV) z=raQs!J$qo^B)WW_Jr6x^mqG~j?Ysx`iU!w_1+*j5YXiTJVt^1jqzwGq0tPc| z_N$N^h4}63$+Ei~7_tyh!HrZ8Tf`Oc!7?=}F))}UVbw_%!>>Lp664g%Fc(+4TODD= zj9!>7VpfA1l8NvjmccRxYYEa+1BLIdy$k|#$G@$|0vi0J07IXXiwxdzcfZ4iTmIpt zZ>kk%_2;q%|IW^hK!sqe0k+Q(c{z^>agF`h>L(>X~@w>K8dPKTmV)$_au_fWYGRp&5+t8 zN7*K`Lm2BPqhp1jx4;ICC9{|zgSXh2KiGR?)$JR4W0TNBqZ^?%Qj}5o_{!nz5FS@% zg4FL<82er@=ZVVs9BLkVe>=aecveG{E!P20m)2(T zLYuou@dqkH!HAj8vkN%*IFV93bRQ%Z%Q>?ks=SgrvtEP`h4#&f1w~XwhWCk_s_LW{ zbkb+b&FZNU$p9QO>a4d@_Y+~CfA0><0A%RbQ1 zs(Y>49fp_b?A{CfYcWFbcL9?Ih5N5x0J0V0(*52SlSG`D=EHYRUmb}XODHstt2y7 z`{k&>ZgGef*pEk#_P2-a;s|)q@3%hZM^!!_Q5_f?*C5ClgiwHew^RUduUn;J%@COc z#+Dh|%R$X*m*v2NR)_hZfi|-Iot{$lw3k3vlM_}w@wP~nJhjQ&zO2v?X$=W{jqvyfb4_`G}~wxzrjK5#VS|<)%@r61q%j{Uk8rRyBisiNt9m zk{Ya_3QugOljWCc(!>FS$na#_6RFg^(ks1)PoLVZ0X5-Yz%^D33*e9)8)+m&4O@*q zL~~!enWn(vTN{0Fob73Hv> zPOq%zT-fr3!gDqPO6mF%#gY8nTAV@bP4=!_EZbO@a)woludnqHJI02$i0oP*;2PP^ zUytKDC9#f{5-Y_W5AnzfUAQ8jE$zsNT8^6J)6+!fq5)y=0ddMOWJ5q}ZQhSIIM@Pv zFIjqKVB%qM0|tR0N8D6HNVeo=JuqLtqKMYyehv}`Vcrqd_+t&Rx+)r3B`&yy>D127 z?qc&%S%wdqEK&9Uc>x&88-zUNg#9{jIu$=gBG*EFQQV|V4uXTd4AM~CXu_mkNZBrc zVR@+MqJYIHgTJ&<5?~D{2}d)*qiDKFnW0V5c%PxB%NJ|;ivexvvJkhBh%|-?(#?#B z;M4;#HXh(`F;|BAyK760k>4wzW18B zI2jbosMe?m*AO{+Z*~6yc-65X0u;K{gHCf?5+Tu%_)(<|Pi#@Rq`-|LJ{$bHUm}d@ zl86fpkq_#00dNlz%GU? zeK9Q{_&`XZ{zp(737eSU`-YVBc^;t^+0}EAAkVR&f>1U}T9=1FcL&i!$>92pw5D9E z1&g~FCT9!6Ejn260y_kkShTRt!f!MDz)vbu_y}&j<=f?yJA)grlg_=6Cnpz%9X8|3 z=S%?NbSsYJnI-WVhDtf;A{A1eHA|qKXIu>OLgf_{%F97 zyTq3E*!6dA%0(b-&N9i06+h+$o--&AYD9W`639cKS^XEGZcrYBw1v-TKG|W>>MZkc~%+@3fb# zUi8H44FxVRj8G?`coX3faUGpUs9U4&m^D}h57z&Mup8Nk{5UIFxn!E>FVKzrVp2B< zLJsUQC&<_sE4$O{RFwBDVv8WcvPcOm`x>V~E`IqHqRAGZHC74(5rFvLy)XFz(*G8w z&^f%%Ws5I)AT=3R8yS6a{1KUu1g1|(~xFkWpC`X_9x02~33O>368-pVv z%@ZSUo#+~CxrqH6LK?AHJ)ifi0fS{Dh=YYp{e^^}U=eFCPbzwg8~Lbz?GG|ZVuyIk zARZ8?RI=6yGWyY=wI3H9gcN{bRnX!l12^s@iVS8VnTnUyb6)17NIuH_3w6&=-+cM1fCg<7IZrO=wA71RU=}3;eG6s zgC#8*_ak#NV(uPym(FeFa2@I3Zb+>-=DrAxD`n+qjJW0P^MbeA@K_mtkg&0xS5Ds@ zR+PcEwhds@Em^byc0A3CWE|Ddzpi#AbnwL8BveEn2P-g5DM5W9nCE%$NwF579atdD zjAkWe&?=pMZ@i8F3Oj_I{J0;cmtk8wj##WZ`r1@I?LBnaa(&P+gIB= zc_^G51$$;i6L_~b9Tn>yWI3>NyZ=PzLV8vYCb9h1aSm9N+z3rd$d%igTGRMBgY0Oj zmdzda=pM(HywzM^-)5Bv9X)M0%?>i>Fnwm;$6pB> zSvay=pECIiPxFnSjdjsh-jq~c;)kS;+#OH4hxgpm+iUz1L!K_iFyQ+M+)javY_Dd62HA}2$|svN~$nb^1X6F z#l2JEd1KTPja>jyh+IS}lBWB;{~)vH^Z9qywQmv25Z1hFotr^wBg@fHxJLLQ+8N_N zmVgh%iki@6jfpI|WTE0~`3G{`RqWaWeKi$PQGL*?KZjC5u9zrvCW}G!b~M%}!vu&G zY0|~{u{g8(`9!FA2k$qfGp`|Fq7f%iGBRPh1kXHhh4zyPy@s+*(kaFLm2afdm?_n; z0uY3Rb4iU*2=;al&ZlYT|lAja6>CgsyjC!W0(-rDa%DOW-Mtql;Oc z6ppBn@;mK|U(ekck&_CVJYMC>)Vy_7RfWu$P%l?{Se1UHtp-`49>!OOnTPW`1uE|Y zzJ78}0T`CisuPITc8HYB+}fL+MsykepohKcJwfn^5UBW1@0l<}dMJ}^ovIpT1}UaU zWJHOLMioc=q+pBc=_F&q3%fhGFXVSf8^XZAXc5_dbo_wmC4pzHLZl(8VZY1HFnm6Z zQDe+BO1g%TGV@&i7rf z?zr9m8wm5mhj5$t-f%w_$7gHqh+}uY4aPR}Z4kg^ONn+2dp$2a zSXAXV5lYDEN1xE^$hdWrHRJ#bg-oK>uwLR#*oN2~u^n)PlT-Czl7c7+1VWQgS0^W; zuZghGH7QPDG4DHhZ@lQya44r{K)Y9DM$qtD&0wI%f^!PfY^K-LJ~8JwicrMB z-_$*e9Y9U=hIQjS(E{g64i0`MrL$J;r|fc5Fnx=m;fHxT&o55V7&mV(yi5>s`0TpY zU(@oGS3UQSiXkVK54780!6ALmq&33@jUvdMAY)F%03~htuoNG%8MR~pTX)f)V{0x# zEVd8<)4J%<^;w8cEf90Tg)_l03zfSf<2mM?Xadx@5L)P@g}Gq8RpzV}tpr`@$#N?$ zo$F(z(D<}i^Y+BDp?cc02AD68_homm_dMD`tSntqa|161_z;Di1DV5nTAE*D?iUigR~);Z;eOoaS2%wae~m*MVcLZ> zbiK|6N51Tfoum{{jeN$6Re6Hm6;6qzJ8~U#o&?wHB>EI3s3*CGNjN zh|L*qeoRURudnY;SJUJq3hVofiC_^Z;lr?)g*><~uwm<29dhwnJNn%6nwgo&;wZ0IDgr$A%OQ$HmnVd%!Fxm8`n3ddy z7E^E)tcasi8kZhi9`j-JQkiPo6cB|g0hly+AKal4KHv;cVZ8)Qs(iD&slZs*cFbH) zcd?#0nw`E6=%~KFVFKOMXgAnERP5k}kMJY_oqv1-Pk;EfBTgb%rK#xcBX`JhqycHd z#0!#r=r2fTGP(m?8fKd(N&^t}NC)3dK=KWV)nFei`YUZAts`@T5VPUWj#_=lwg!MM zld;04YXDFRNosIA=yOeiZpx1|1g``upC5gR8Wi>4Iv5;ZK4Bga3{pU> zalWuvX)cbK{yDzL1(&nJ4LP5s>q8qX;%$HM zYkc1BcfjDa){)_jNA&3BU(f$cGrFX8+Hzuz3#TipI#&MiMds4=VNOxy_Wzd<}^3|V$|Lq92ri+jh;Pd2sJF=lE{OJtou=- zKXS~gnX8?_$UDo>6*LalfP2cA)6-`!1+qA=kM;2M^d8JJ_wa7Mj0 z87zAdF$9oo#U$d_O&t&)Z_0^3jllN}YLYgmtZV)8NTquG`BEK1=E!ogH<;1V6SgZX zUua9~(1NqM9;FAyVezSRtm-A~(2VB5e(lzas$E#UTgagV!q<182@)wjkpC*LS^4+- zWjH)4=TZadn`8-tr8S9uSAy&d^xs}-l`g6Xp5%y3kqv;jX1IMfkpt=28EIW*hA z7HM7IP2~lg>Jo_a@*SeYBey?9~PV)R2-Jo3mEse zzuC$3Afcm9vxR<>)ie7G_|(2PFhQ?PAE=oI&}{ioV2qXNt#Z!`XW(P-+p1E zCnkXbBLTU$C~dhyu}|TNz^@}iNd$p)%%44XH@cwMDxQ7D@-t{eGi3%0S<3B{e%LPBgW`5zexut%S0I~|jQ zJHgC@lH^28V(ojQ#?JUwre}(vtVH=!U_r(me*1poPwp#h*F&9%nrGh$AA!6om}%^d z6Sb;R{`jEK^ZD5(cO2&>mc&D0f8vxDZoG=OEyWp7kIW8Pd4}p+2z7o*8DKv4;6n)> zz;b2Umo~qA5t-IwrN3DqX9{heDZ8aO84&dD0~nK=c_XIUALR;=0VYcG;n&vgwGIt; ziNg^dZS#{Rn_Q8$I}uL!NS{@$<5B47^Oyxgb#b?VRX}>W(;l8shiX+ibT+v+baNXj zD7x>YMu4UTqYH*%Ue$~>K*o9GhDC4jR}XZr_bpS{gV2C`wnJrHD9QQ{dZTQ(Ukmq%5JU=J1DpS`|x^wSO7D6 zrj8os; zJTX}FWjsZ*A3Zg2*Vp87wz!)m@a@eTl7)+e3F&N^29YX|${oP5Unv^2%%*4w*qhbA znC*W64E?0Mx&I6L2Z7lqo=e`%c#p}A(Cw*{0&!xM8+Aw{ZXkzzrw+fuWJVOzB5(fX zW*C<8+OBH?wbh7st9JR%JWZ23MCb)N$k^aHC@GN|UhC`kj>;$10iw0uOR zUQhD_YUEy-V6fgsC`=adW~MR~ezT@p-|AuC$pmMcH+fxni5$xx|5VrMun3 zn}o97wQIN@6+2*`XiGXun(X@mOXk+85e1U0r4r4-I@%RHJ|@aRD-BP7XfMubz4wBQ z_S<~7r5O~?h|Cui{I;1zlV>3rL>9#S?xho$a;^FgoP*CCZVn4fGyHqC%{Za@Z@tIeg3Ip=z)m_%0QHb4NRDg_x(1Y} z7}o&eK5vnTLN6VQznFsNr)a)ZgwIv7H-vN91A=3Z#jdZ|Db0l`AKaKWiZqDv3BZ=T z@N$_6tBl7(EDsC-YifHD3TGb;DR|$E@<9<>WP88b7Y{oY+;LTqS~j;PyCHOkNmG0h zP#9g*h+%LrW|~J%EDnGp78=$@Ww`Vv>@*z`Fl*2-`K{=kh-HK{m#qN*nBlGF*U&{e^L+|z4k5np {+{b!q(7HIB2#PH&Ee;~JtOA6c6sEKS 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 literal 0 HcmV?d00001 diff --git a/docs/img/turb_evol.png b/docs/img/turb_evol.png new file mode 100644 index 0000000000000000000000000000000000000000..de7aa7b7821a918086aec787da8e86195ecedc2f GIT binary patch literal 47808 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 literal 0 HcmV?d00001 diff --git a/docs/img/turb_spec.png b/docs/img/turb_spec.png new file mode 100644 index 0000000000000000000000000000000000000000..e6a8c5653524481bb4c8865c7cabebe51bc04d01 GIT binary patch literal 18786 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 b*HW$;^wK+Qx0A2XP#@o2`@HHriOjzM<3&RL literal 0 HcmV?d00001 diff --git a/docs/pgen.md b/docs/pgen.md index 0b58b9a2..9a2f58e8 100644 --- a/docs/pgen.md +++ b/docs/pgen.md @@ -1,4 +1,4 @@ -# Problem generators +# Custom problem generators Different simulation setups in AthenaPK are controlled via the so-called problem generators. New problem generators can easily be added and we are happy to accept and merge contibuted problem diff --git a/docs/turbulence.md b/docs/turbulence.md new file mode 100644 index 00000000..b51b4b70 --- /dev/null +++ b/docs/turbulence.md @@ -0,0 +1,107 @@ +# Driven turbulence simulations + +The turbulence problem generator uses explicit inverse Fourier transformations (iFTs) +on each meshblock in order to reduce communication during the iFT. +Thus, it is only efficient if comparatively few modes are used (say < 100). + +Quite generally, driven turbulence simulations start from uniform initial conditions +(uniform density and pressure, some initial magnetic field configuration in case of an +MHD setup, and the fluid at rest) and reach a state of stationary, isotropic (or anisotropic +depending on the strength of the background magnetic field) turbulence after one to few +large eddy turnover times (again depending on the background magnetic field strength). +The large eddy turnover time is usually defined as `T = L/U` with `L` being the scale +of the largest eddies and `U` the root mean square Mach number in the stationary regime. + +The current implementation uses the following forcing spectrum +`(k/k_peak)^2 * (2 - (k/k_peak)^2)`. +Here, `k_peak` is the peak wavenumber of the forcing spectrum. It is related the scales of the largest eddies as +`L = 1/k_f` given that a box size of 1 is currently assumed/hardcoded. + +## Problem setup + +An example parameter file can be found in `inputs/turbulence.in`. + +A typical setup contains the following blocks in the input file: + +``` + +problem_id = turbulence + + +rho0 = 1.0 # initial mean density +p0 = 1.0 # initial mean pressure +b0 = 0.01 # initial magnetic field strength +b_config = 0 # 0 - net flux; 1 - no net flux uniform B; 2 - non net flux sin B; 4 - field loop +kpeak = 2.0 # characteristic wavenumber +corr_time = 1.0 # autocorrelation time of the OU forcing process +rseed = 20190729 # random seed of the OU forcing process +sol_weight = 1.0 # solenoidal weight of the acceleration field +accel_rms = 0.5 # root mean square value of the acceleration field +num_modes = 30 # number of wavemodes + + +k_1_0 = +2 +k_1_1 = -1 +k_1_2 = +0 +k_2_0 = +1 +... +``` + +The following parameters can be changed to control both the initial state: + +- `rho0` initial mean density +- `p0` initial mean thermal pressure +- `b0` initial mean magnetic field strength +- `b_config` + - `0`: net flux case (uniform B_x) + - `1`: no net flux case (uniform B_x with changing sign in each half of the box) + - `2`: no net flux with initial sinosoidal B_x field + - `3`: deprecated + - `4`: closed field loop/cylinder in the box (in x-y plane) located at + - `x0=0.5` (default) + - `y0=0.5` (default) + - `z0=0.5` (default) + - and radius `loop_rad=0.25` + +as well as the driving field: + +- `kpeak` peak wavenumber of the forcing spectrum. Make sure to update the wavemodes to match `kpeak`, see below. +- `corr_time` autocorrelation time of the acceleration field (in code units). +Using delta-in-time forcing, i.e., a very low value, is discouraged, see [Grete et al. 2018 ApJL](https://iopscience.iop.org/article/10.3847/2041-8213/aac0f5). +- `rseed` random seed for the OU process. Only change for new simulation, but keep unchanged for restarting simulations. +- `sol_weight` solenoidal weight of the acceleration field. `1.0` is purely solenoidal/rotational and `0.0` is purely dilatational/compressive. Any value between `0.0` and `1.0` is possible. The parameter is related to the resulting rotational power in the 3D acceleration field as +`1. - ((1-sol_weight)^2/(1-2*sol_weight+3*sol_weight^2))`, see eq (9) in [Federrath et al. 2010 A&A]( +https://doi.org/10.1051/0004-6361/200912437). +- `accel_rms` root mean square value of the acceleration (controls the "strength" of the forcing field) +- `num_modes` number of wavemodes that are specified in the `` section of the parameter file. +The modes are specified manually as an explicit inverse FT is performed and only modes set are included (all others are assumed to be 0). +This is done to make the global inverse FT possible without any +expensive communication between blocks but this becomes excessively +expensiv for large number of modes. +Typically using a few tens of modes is a good choice in practice. +In order to generate a set of modes run the `inputs/generate_fmturb_modes.py` script and replace +the corresponding parts of the parameter file with the output of the script. +Within the script, the top three variables (`k_peak`, `k_high`, and `k_low`) need to be adjusted in +order to generate a complete set (i.e., all) of wavemodes. +Important, the `k_peak` in the script should match the `k_peak` set +in the input file. +Alternatively, wavemodes can be chosen/defined manually, e.g., if not all wavemodes are desired or +only individual modes should be forced. + +## Typical results + +The results shown here are obtained from running simulations with the parameters given in the next section. + +### High level temporal evolution +![image](img/turb_evol.png) + +### Power spectra +![image](img/turb_spec.png) + +### Consistency of acceleration field +As each meshblock does a full iFT of all modes the following slices from a run with 8 meshblocks +illustrate that there is no discontinuities at the meshblock boundary. + +Plot shows x-, y-, and z-acceleration (in rows top to bottom) slices in the x-, y-, and z-direction (in columns from left to right). + +![image](img/turb_acc.png) \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index fa0c4dec..f239b197 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -81,7 +81,7 @@ int main(int argc, char *argv[]) { pman.app_input->ProblemGenerator = field_loop::ProblemGenerator; Hydro::ProblemInitPackageData = field_loop::ProblemInitPackageData; } else if (problem == "kh") { - pman.app_input->ProblemGenerator = kh::ProblemGenerator; + pman.app_input->MeshProblemGenerator = kh::ProblemGenerator; } else if (problem == "rand_blast") { pman.app_input->ProblemGenerator = rand_blast::ProblemGenerator; Hydro::ProblemInitPackageData = rand_blast::ProblemInitPackageData; diff --git a/src/pgen/kh.cpp b/src/pgen/kh.cpp index 547f2a2e..68936935 100644 --- a/src/pgen/kh.cpp +++ b/src/pgen/kh.cpp @@ -32,61 +32,32 @@ // AthenaPK headers #include "../main.hpp" +#include "utils/error_checking.hpp" namespace kh { using namespace parthenon::driver::prelude; //---------------------------------------------------------------------------------------- -//! \fn void MeshBlock::ProblemGenerator(ParameterInput *pin) +//! \fn void Mesh::ProblemGenerator(Mesh *pm, ParameterInput *pin, MeshData *md) // \brief Problem Generator for the Kelvin-Helmholtz test -void ProblemGenerator(MeshBlock *pmb, ParameterInput *pin) { +void ProblemGenerator(Mesh *pmesh, ParameterInput *pin, MeshData *md) { auto vflow = pin->GetReal("problem/kh", "vflow"); auto iprob = pin->GetInteger("problem/kh", "iprob"); + // Get pointer to first block (always exists) for common data like loop bounds + auto pmb = md->GetBlockData(0)->GetBlockPointer(); auto ib = pmb->cellbounds.GetBoundsI(IndexDomain::interior); auto jb = pmb->cellbounds.GetBoundsJ(IndexDomain::interior); auto kb = pmb->cellbounds.GetBoundsK(IndexDomain::interior); auto gam = pin->GetReal("hydro", "gamma"); auto gm1 = (gam - 1.0); - // initialize conserved variables - auto &rc = pmb->meshblock_data.Get(); - auto &u_dev = rc->Get("cons").data; - auto &coords = pmb->coords; - // initializing on host - auto u = u_dev.GetHostMirrorAndCopy(); + // Initialize conserved variables + // Get a MeshBlockPack on device with all conserved variables + const auto &cons = md->PackVariables(std::vector{"cons"}); + const auto num_blocks = md->NumBlocks(); - std::mt19937 gen(pmb->gid); // Standard mersenne_twister_engine seeded with gid - std::uniform_real_distribution ran(-0.5, 0.5); - - //--- iprob=1. Uniform stream with density ratio "drat" located in region -1/4GetReal("problem/kh", "drat"); - Real amp = pin->GetReal("problem/kh", "amp"); - for (int k = kb.s; k <= kb.e; k++) { - for (int j = jb.s; j <= jb.e; j++) { - for (int i = ib.s; i <= ib.e; i++) { - u(IDN, k, j, i) = 1.0; - u(IM1, k, j, i) = vflow + amp * ran(gen); - u(IM2, k, j, i) = amp * ran(gen); - u(IM3, k, j, i) = 0.0; - if (std::abs(coords.Xc<2>(j)) < 0.25) { - u(IDN, k, j, i) = drat; - u(IM1, k, j, i) = -drat * (vflow + amp * ran(gen)); - u(IM2, k, j, i) = drat * amp * ran(gen); - } - // Pressure scaled to give a sound speed of 1 with gamma=1.4 - u(IEN, k, j, i) = - 2.5 / gm1 + - 0.5 * (SQR(u(IM1, k, j, i)) + SQR(u(IM2, k, j, i))) / u(IDN, k, j, i); - } - } - } - } + //--- iprob=1. This was the classic, unresolved K-H test. //--- iprob=2. Uniform density medium moving at +/-vflow seperated by a single shear // layer with tanh() profile at y=0 with a single mode perturbation, reflecting BCs at @@ -97,9 +68,11 @@ void ProblemGenerator(MeshBlock *pmb, ParameterInput *pin) { Real amp = pin->GetReal("problem/kh", "amp"); Real a = 0.02; Real sigma = 0.2; - for (int k = kb.s; k <= kb.e; k++) { - for (int j = jb.s; j <= jb.e; j++) { - for (int i = ib.s; i <= ib.e; i++) { + pmb->par_for( + "KHI: iprob2", 0, num_blocks - 1, kb.s, kb.e, jb.s, jb.e, ib.s, ib.e, + KOKKOS_LAMBDA(const int b, const int k, const int j, const int i) { + const auto &u = cons(b); + const auto &coords = cons.GetCoords(b); u(IDN, k, j, i) = 1.0; u(IM1, k, j, i) = vflow * std::tanh((coords.Xc<2>(j)) / a); u(IM2, k, j, i) = amp * std::cos(2.0 * M_PI * coords.Xc<1>(i)) * @@ -108,23 +81,22 @@ void ProblemGenerator(MeshBlock *pmb, ParameterInput *pin) { u(IEN, k, j, i) = 1.0 / gm1 + 0.5 * (SQR(u(IM1, k, j, i)) + SQR(u(IM2, k, j, i))) / u(IDN, k, j, i); - } - } - } - } + }); - //--- iprob=3. Test in SR paper (Beckwith & Stone, ApJS 193, 6, 2011). Gives two - // resolved shear layers with tanh() profiles for velocity and density located at - // y = +/- 0.5, density one in middle and 0.01 elsewhere, single mode perturbation. + } else if (iprob == 3) { + //--- iprob=3. Test in SR paper (Beckwith & Stone, ApJS 193, 6, 2011). Gives two + // resolved shear layers with tanh() profiles for velocity and density located at + // y = +/- 0.5, density one in middle and 0.01 elsewhere, single mode perturbation. - if (iprob == 3) { // Read/set problem parameters Real amp = pin->GetReal("problem/kh", "amp"); Real a = 0.01; Real sigma = 0.1; - for (int k = kb.s; k <= kb.e; k++) { - for (int j = jb.s; j <= jb.e; j++) { - for (int i = ib.s; i <= ib.e; i++) { + pmb->par_for( + "KHI: iprob3", 0, num_blocks - 1, kb.s, kb.e, jb.s, jb.e, ib.s, ib.e, + KOKKOS_LAMBDA(const int b, const int k, const int j, const int i) { + const auto &u = cons(b); + const auto &coords = cons.GetCoords(b); u(IDN, k, j, i) = 0.505 + 0.495 * std::tanh((std::abs(coords.Xc<2>(j)) - 0.5) / a); u(IM1, k, j, i) = vflow * std::tanh((std::abs(coords.Xc<2>(j)) - 0.5) / a); @@ -139,17 +111,15 @@ void ProblemGenerator(MeshBlock *pmb, ParameterInput *pin) { u(IEN, k, j, i) = 1.0 / gm1 + 0.5 * (SQR(u(IM1, k, j, i)) + SQR(u(IM2, k, j, i))) / u(IDN, k, j, i); - } - } - } - } + }); - //--- iprob=4. "Lecoanet" test, resolved shear layers with tanh() profiles for velocity - // and density located at z1=0.5, z2=1.5 two-mode perturbation for fully periodic BCs + } else if (iprob == 4) { + //--- iprob=4. "Lecoanet" test, resolved shear layers with tanh() profiles for + // velocity + // and density located at z1=0.5, z2=1.5 two-mode perturbation for fully periodic BCs - // To promote symmetry of FP errors about midplanes, rescale z' = z - 1. ; x' = x - 0.5 - // so that domain x1 = [-0.5, 0.5] and x2 = [-1.0, 1.0] is centered about origin - if (iprob == 4) { + // To promote symmetry of FP errors about midplanes, rescale z' = z - 1. ; x' = x - + // 0.5 so that domain x1 = [-0.5, 0.5] and x2 = [-1.0, 1.0] is centered about origin // Read/set problem parameters Real amp = pin->GetReal("problem/kh", "amp"); // unstratified problem is the default @@ -164,9 +134,11 @@ void ProblemGenerator(MeshBlock *pmb, ParameterInput *pin) { Real z1 = -0.5; // z1' = z1 - 1.0 Real z2 = 0.5; // z2' = z2 - 1.0 - for (int k = kb.s; k <= kb.e; k++) { - for (int j = jb.s; j <= jb.e; j++) { - for (int i = ib.s; i <= ib.e; i++) { + pmb->par_for( + "KHI: iprob4", 0, num_blocks - 1, kb.s, kb.e, jb.s, jb.e, ib.s, ib.e, + KOKKOS_LAMBDA(const int b, const int k, const int j, const int i) { + const auto &u = cons(b); + const auto &coords = cons.GetCoords(b); // Lecoanet (2015) equation 8a) Real dens = 1.0 + 0.5 * drho_rho0 * (std::tanh((coords.Xc<2>(j) - z1) / a) - @@ -233,40 +205,39 @@ void ProblemGenerator(MeshBlock *pmb, ParameterInput *pin) { P0 / gm1 + 0.5 * (SQR(u(IM1, k, j, i)) + SQR(u(IM2, k, j, i)) + SQR(u(IM3, k, j, i))) / u(IDN, k, j, i); - } - } - } - // copy initialized vars to device - u_dev.DeepCopy(u); - } + }); - //--- iprob=5. Uniform stream with density ratio "drat" located in region -1/4GetReal("problem/kh", "a"); Real sigma = pin->GetReal("problem/kh", "sigma"); Real drat = pin->GetReal("problem/kh", "drat"); Real amp = pin->GetReal("problem/kh", "amp"); - for (int k = kb.s; k <= kb.e; k++) { - for (int j = jb.s; j <= jb.e; j++) { - for (int i = ib.s; i <= ib.e; i++) { + pmb->par_for( + "KHI: iprob5", 0, num_blocks - 1, kb.s, kb.e, jb.s, jb.e, ib.s, ib.e, + KOKKOS_LAMBDA(const int b, const int k, const int j, const int i) { + const auto &u = cons(b); + const auto &coords = cons.GetCoords(b); Real w = (std::tanh((std::abs(coords.Xc<2>(j)) - 0.25) / a) + 1.0) * 0.5; u(IDN, k, j, i) = w + (1.0 - w) * drat; - u(IM1, k, j, i) = w * vflow - (1.0 - w) * vflow * drat; + u(IM1, k, j, i) = u(IDN, k, j, i) * vflow * (w - 0.5); u(IM2, k, j, i) = - u(IDN, k, j, i) * amp * std::sin(2.0 * 2.0 * M_PI * coords.Xc<1>(i)) * + u(IDN, k, j, i) * amp * std::cos(2.0 * 2.0 * M_PI * coords.Xc<1>(i)) * std::exp(-SQR(std::abs(coords.Xc<2>(j)) - 0.25) / (sigma * sigma)); u(IM3, k, j, i) = 0.0; // Pressure scaled to give a sound speed of 1 with gamma=1.4 u(IEN, k, j, i) = 2.5 / gm1 + - 0.25 * (SQR(u(IM1, k, j, i)) + SQR(u(IM2, k, j, i))) / u(IDN, k, j, i); - } - } - } + 0.5 * (SQR(u(IM1, k, j, i)) + SQR(u(IM2, k, j, i))) / u(IDN, k, j, i); + }); + } else { + PARTHENON_FAIL("Unknow iprob for KHI pgen.") } } diff --git a/src/pgen/pgen.hpp b/src/pgen/pgen.hpp index ddea80f3..877a1f13 100644 --- a/src/pgen/pgen.hpp +++ b/src/pgen/pgen.hpp @@ -84,7 +84,7 @@ void ProblemInitPackageData(ParameterInput *pin, parthenon::StateDescriptor *pkg namespace kh { using namespace parthenon::driver::prelude; -void ProblemGenerator(MeshBlock *pmb, parthenon::ParameterInput *pin); +void ProblemGenerator(Mesh *pm, parthenon::ParameterInput *pin, MeshData *md); } // namespace kh namespace rand_blast { using namespace parthenon::driver::prelude; From b7394c09209ab25df6cb05570ae04cc3a11390b8 Mon Sep 17 00:00:00 2001 From: Ben Wibking Date: Mon, 4 Nov 2024 17:01:47 -0500 Subject: [PATCH 04/11] add sqrt(4pi) to cgs B-field units --- src/units.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/units.hpp b/src/units.hpp index 3290e87e..7579354d 100644 --- a/src/units.hpp +++ b/src/units.hpp @@ -90,7 +90,7 @@ class Units { return code_energy_cgs() / kev_cgs * code_length_cgs() * code_length_cgs(); } parthenon::Real code_magnetic_cgs() const { - return sqrt(code_mass_cgs()) / sqrt(code_length_cgs()) / code_time_cgs(); + return std::sqrt(4.0 * M_PI) * sqrt(code_mass_cgs()) / sqrt(code_length_cgs()) / code_time_cgs(); } // Physical Constants in code units From 34be9049dcfa2b5e9008ee82079aed6952b6daf2 Mon Sep 17 00:00:00 2001 From: Philipp Grete Date: Fri, 8 Nov 2024 16:52:49 +0100 Subject: [PATCH 05/11] Add unit doc --- CHANGELOG.md | 1 + README.md | 17 ++------------- docs/README.md | 1 + docs/input.md | 4 ++++ docs/units.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/units.hpp | 3 ++- 6 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 docs/units.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 152bd814..c95a86ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - [[PR 1]](https://github.com/parthenon-hpc-lab/athenapk/pull/1) Add isotropic thermal conduction and RKL2 supertimestepping ### Changed (changing behavior/API/variables/...) +- [[PR 122]](https://github.com/parthenon-hpc-lab/athenapk/pull/122) Fixed sqrt(4pi) factor in CGS Gauss unit and add unit doc - [[PR 119]](https://github.com/parthenon-hpc-lab/athenapk/pull/119) Fixed Athena++ paper test case for KHI pgen. Added turbulence pgen doc. - [[PR 97]](https://github.com/parthenon-hpc-lab/athenapk/pull/97) Fixed Schure cooling curve. Removed SD one. Added description of cooling function conventions. - [[PR 84]](https://github.com/parthenon-hpc-lab/athenapk/pull/84) Bump Parthenon to latest develop (2024-02-15) diff --git a/README.md b/README.md index fd66e824..882d9625 100644 --- a/README.md +++ b/README.md @@ -131,21 +131,8 @@ the `file_type = hdf5` format, see [VisIt](https://wci.llnl.gov/simulation/computer-codes/visit/). In ParaView, select the "XDMF Reader" when prompted. -2. With [yt](https://yt-project.org/) -- though currently through a custom frontend -that is not yet part of the main yt branch and, thus, has to be installed manually, e.g., -as follows: -```bash -cd ~/src # or any other folder of choice -git clone https://github.com/forrestglines/yt.git -cd yt -git checkout parthenon-frontend - -# If you're using conda or virtualenv -pip install -e . -# OR alternatively, if you using the plain Python environment -pip install --user -e . -``` -Afterwards, `*.phdf` files can be read as usual with `yt.load()`. +2. With [yt](https://yt-project.org/) +As of versions >=4.4 `*.phdf` files can be read as usual with `yt.load()`. 3. Using [Ascent](https://github.com/Alpine-DAV/ascent) (for in situ visualization and analysis). This requires Ascent to be installed/available at compile time of AthenaPK. diff --git a/docs/README.md b/docs/README.md index 018d4b68..145cc0f8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,6 +13,7 @@ The documentation currently includes - [Notebooks to calculate cooling tables from literature](cooling) - [Brief notes on developing code for AthenaPK](development.md) - [How to add a custom/user problem generator](pgen.md) +- [Units](units.md) - Detailed descriptions of more complex problem generators - [Galaxy Cluster and Cluster-like Problem Setup](cluster.md) - [Driven turbulence](turbulence.md) diff --git a/docs/input.md b/docs/input.md index d5e48d4f..73f2b23b 100644 --- a/docs/input.md +++ b/docs/input.md @@ -67,6 +67,10 @@ To control the floors, following parameters can be set in the `` block: *Note* the pressure floor will take precedence over the temperature floor in the conserved to primitive conversion if both are defined. +#### Units + +See(here)[units.md]. + #### Diffusive processes Diffusive processes in AthenaPK can be configured in the `` block of the input file. diff --git a/docs/units.md b/docs/units.md new file mode 100644 index 00000000..b26ccf6e --- /dev/null +++ b/docs/units.md @@ -0,0 +1,58 @@ +# AthenaPK units + +## General unit system + +Internally, all calculations are done in "code units" and there are no conversions between +code and physical units during runtime (with excetion like the temperature when cooling is +being used). +Therefore, in general no units need to be prescribed to run a simulation. + +If units are required (e.g., if cooling is used and, thus, a conversion between internal energy +in code units and physical temperature is required) they are configured in the input block +as follows: + +``` + +code_length_cgs = 3.085677580962325e+24 # 1 Mpc in cm +code_mass_cgs = 1.98841586e+47 # 1e14 Msun in g +code_time_cgs = 3.15576e+16 # 1 Gyr in s +``` + +This information will also be used by postprocessing tools (like yt) to convert between +code units and a physical unit system (like cgs). + +Moreover, internally a set of factors from code to cgs units are available to process conversions +if required (e.g., from the input file). + +For example, for an input parameter (in the input file) like + +``` + +r0_cgs = 3.085677580962325e+20 # 100 pc +``` + +the conversion should happen in the problem generator lik + +```c++ + r_cloud = pin->GetReal("problem/cloud", "r0_cgs") / units.code_length_cgs(); +``` + +so that the resulting quantity is internally in code units (here code length). + +It is highly recommended to be *very* explicit/specific about units everywhere (as it is +a common source of confusion) like adding the `_cgs` suffix to the parameter in the +input file above. + +## Magnetic units + +Internally, AthenaPK (and almost all MHD codes) use +[Heaviside-Lorentz units](https://en.wikipedia.org/wiki/Heaviside%E2%80%93Lorentz_units), +where the magnetic field is transformed from $B \rightarrow B / \sqrt{4 \pi}$. +(See also the note in the +[Castro documentation](https://amrex-astro.github.io/Castro/docs/mhd.html) about this.) + +So when converting from CGS-Gaussian units to code units, it is necessary to divide +by $\sqrt{4 \pi}$ (in addition to the base dimensional factors). +This is automatically handled by the `units.code_magnetic_cgs()` factors. + + diff --git a/src/units.hpp b/src/units.hpp index 7579354d..fc6a2412 100644 --- a/src/units.hpp +++ b/src/units.hpp @@ -90,7 +90,8 @@ class Units { return code_energy_cgs() / kev_cgs * code_length_cgs() * code_length_cgs(); } parthenon::Real code_magnetic_cgs() const { - return std::sqrt(4.0 * M_PI) * sqrt(code_mass_cgs()) / sqrt(code_length_cgs()) / code_time_cgs(); + return std::sqrt(4.0 * M_PI) * sqrt(code_mass_cgs()) / sqrt(code_length_cgs()) / + code_time_cgs(); } // Physical Constants in code units From 580e5739d1119c409ec6ed22878a52bbab1a4bcc Mon Sep 17 00:00:00 2001 From: Philipp Grete Date: Sat, 9 Nov 2024 12:15:49 +0100 Subject: [PATCH 06/11] Bump Kokkos 4.4.1, Parth vov-fix --- external/Kokkos | 2 +- external/parthenon | 2 +- inputs/cloud.in | 2 +- src/main.cpp | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/external/Kokkos b/external/Kokkos index 08ceff92..15dc143e 160000 --- a/external/Kokkos +++ b/external/Kokkos @@ -1 +1 @@ -Subproject commit 08ceff92bcf3a828844480bc1e6137eb74028517 +Subproject commit 15dc143e5f39949eece972a798e175c4b463d4b8 diff --git a/external/parthenon b/external/parthenon index ec61c9cb..e00017c6 160000 --- a/external/parthenon +++ b/external/parthenon @@ -1 +1 @@ -Subproject commit ec61c9cb102d6a67a4dd03e7d7fa4e10c82c1032 +Subproject commit e00017c6ece547f3adf9a16b94ddf0bdf8bb4773 diff --git a/inputs/cloud.in b/inputs/cloud.in index 002febd0..e7e05f69 100644 --- a/inputs/cloud.in +++ b/inputs/cloud.in @@ -22,7 +22,7 @@ ox1_bc = outflow nx2 = 320 x2min = -400 x2max = 2100 -ix2_bc = user +ix2_bc = cloud_inflow_x2 ox2_bc = outflow nx3 = 192 diff --git a/src/main.cpp b/src/main.cpp index f239b197..23cdb825 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -63,8 +63,8 @@ int main(int argc, char *argv[]) { } else if (problem == "cloud") { pman.app_input->InitUserMeshData = cloud::InitUserMeshData; pman.app_input->ProblemGenerator = cloud::ProblemGenerator; - pman.app_input->boundary_conditions[parthenon::BoundaryFace::inner_x2] = - cloud::InflowWindX2; + pman.app_input->RegisterBoundaryCondition(parthenon::BoundaryFace::inner_x2, + "cloud_inflow_x2", cloud::InflowWindX2); Hydro::ProblemCheckRefinementBlock = cloud::ProblemCheckRefinementBlock; } else if (problem == "blast") { pman.app_input->InitUserMeshData = blast::InitUserMeshData; From 9a7ede7423123d4b0d62f44777dbcb2cbfe34c4b Mon Sep 17 00:00:00 2001 From: Philipp Grete Date: Sat, 9 Nov 2024 19:09:54 +0100 Subject: [PATCH 07/11] Store and restore dt_X and centralize dt_diff calc --- docs/input.md | 3 -- src/hydro/hydro.cpp | 91 +++++++++++++++++--------------------- src/hydro/hydro_driver.cpp | 16 +++++-- 3 files changed, 52 insertions(+), 58 deletions(-) diff --git a/docs/input.md b/docs/input.md index 73f2b23b..0395a448 100644 --- a/docs/input.md +++ b/docs/input.md @@ -118,9 +118,6 @@ However, as reported by [^V+17] a large number of stages (in combination with anisotropic, limited diffusion) may lead to a loss in accuracy, which is why the difference between hyperbolic and parabolic timesteps can be limited by `diffusion/rkl2_max_dt_ratio=...` and a warning is shown if the ratio is above 400. -Note that if this limit is enforced the `dt=` shown on the terminal might not be the actual -`dt` taken in the code as the limit is currently enforced only after the output -has been printed. [^M+14]: C. D. Meyer, D. S. Balsara, and T. D. Aslam, “A stabilized Runge–Kutta–Legendre method for explicit super-time-stepping of parabolic and mixed equations,” Journal of Computational Physics, vol. 257, pp. 594–626, 2014, doi: https://doi.org/10.1016/j.jcp.2013.08.021. diff --git a/src/hydro/hydro.cpp b/src/hydro/hydro.cpp index 7bc95be7..3861e53a 100644 --- a/src/hydro/hydro.cpp +++ b/src/hydro/hydro.cpp @@ -4,6 +4,7 @@ // Licensed under the BSD 3-Clause License (the "LICENSE"). //======================================================================================== +#include #include #include #include @@ -29,6 +30,7 @@ #include "diffusion/diffusion.hpp" #include "glmmhd/glmmhd.hpp" #include "hydro.hpp" +#include "interface/params.hpp" #include "outputs/outputs.hpp" #include "prolongation/custom_ops.hpp" #include "rsolvers/rsolvers.hpp" @@ -60,44 +62,7 @@ parthenon::Packages_t ProcessPackages(std::unique_ptr &pin) { // the task list is constructed (versus when the task list is being executed). // TODO(next person touching this function): If more/separate feature are required // please separate concerns. -void PreStepMeshUserWorkInLoop(Mesh *pmesh, ParameterInput *pin, SimTime &tm) { - auto hydro_pkg = pmesh->block_list[0]->packages.Get("Hydro"); - const auto num_partitions = pmesh->DefaultNumPartitions(); - - if (hydro_pkg->Param("diffint") == DiffInt::rkl2) { - auto dt_diff = std::numeric_limits::max(); - if (hydro_pkg->Param("conduction") != Conduction::none) { - for (auto i = 0; i < num_partitions; i++) { - auto &md = pmesh->mesh_data.GetOrAdd("base", i); - - dt_diff = std::min(dt_diff, EstimateConductionTimestep(md.get())); - } - } - if (hydro_pkg->Param("viscosity") != Viscosity::none) { - for (auto i = 0; i < num_partitions; i++) { - auto &md = pmesh->mesh_data.GetOrAdd("base", i); - - dt_diff = std::min(dt_diff, EstimateViscosityTimestep(md.get())); - } - } - if (hydro_pkg->Param("resistivity") != Resistivity::none) { - for (auto i = 0; i < num_partitions; i++) { - auto &md = pmesh->mesh_data.GetOrAdd("base", i); - - dt_diff = std::min(dt_diff, EstimateResistivityTimestep(md.get())); - } - } -#ifdef MPI_PARALLEL - PARTHENON_MPI_CHECK(MPI_Allreduce(MPI_IN_PLACE, &dt_diff, 1, MPI_PARTHENON_REAL, - MPI_MIN, MPI_COMM_WORLD)); -#endif - hydro_pkg->UpdateParam("dt_diff", dt_diff); - const auto max_dt_ratio = hydro_pkg->Param("rkl2_max_dt_ratio"); - if (max_dt_ratio > 0.0 && tm.dt / dt_diff > max_dt_ratio) { - tm.dt = max_dt_ratio * dt_diff; - } - } -} +void PreStepMeshUserWorkInLoop(Mesh *pmesh, ParameterInput *pin, SimTime &tm) {} template Real HydroHst(MeshData *md) { @@ -252,17 +217,20 @@ std::shared_ptr Initialize(ParameterInput *pin) { auto glmmhd_alpha = pin->GetOrAddReal("hydro", "glmmhd_alpha", 0.1); pkg->AddParam("glmmhd_alpha", glmmhd_alpha); calc_c_h = true; - pkg->AddParam("c_h", 0.0, true); // hyperbolic divergence cleaning speed - // global minimum dx (used to calc c_h) - pkg->AddParam("mindx", std::numeric_limits::max(), true); - // hyperbolic timestep constraint - pkg->AddParam("dt_hyp", std::numeric_limits::max(), true); + pkg->AddParam( + "c_h", 0.0, Params::Mutability::Restart); // hyperbolic divergence cleaning speed } else { PARTHENON_FAIL("AthenaPK hydro: Unknown fluid method."); } pkg->AddParam<>("fluid", fluid); pkg->AddParam<>("nhydro", nhydro); pkg->AddParam<>("calc_c_h", calc_c_h); + // global minimum dx (used to calc c_h) + pkg->AddParam("mindx", std::numeric_limits::max(), + Params::Mutability::Restart); + // hyperbolic timestep constraint + pkg->AddParam("dt_hyp", std::numeric_limits::max(), + Params::Mutability::Restart); const auto recon_str = pin->GetString("hydro", "reconstruction"); int recon_need_nghost = 3; // largest number for the choices below @@ -633,12 +601,13 @@ std::shared_ptr Initialize(ParameterInput *pin) { "Options are: none, unsplit, rkl2"); } if (diffint != DiffInt::none) { - pkg->AddParam("dt_diff", 0.0, true); // diffusive timestep constraint // As in Athena++ a cfl safety factor is also applied to the theoretical limit. // By default it is equal to the hyperbolic cfl. auto cfl_diff = pin->GetOrAddReal("diffusion", "cfl", pkg->Param("cfl")); pkg->AddParam<>("cfl_diff", cfl_diff); } + pkg->AddParam("dt_diff", std::numeric_limits::max(), + Params::Mutability::Restart); // diffusive timestep constraint pkg->AddParam<>("diffint", diffint); if (fluid == Fluid::euler) { @@ -855,9 +824,12 @@ Real EstimateTimestep(MeshData *md) { // get to package via first block in Meshdata (which exists by construction) auto hydro_pkg = md->GetBlockData(0)->GetBlockPointer()->packages.Get("Hydro"); auto min_dt = std::numeric_limits::max(); + auto dt_hyp = std::numeric_limits::max(); - if (hydro_pkg->Param("calc_dt_hyp")) { - min_dt = std::min(min_dt, EstimateHyperbolicTimestep(md)); + const auto calc_dt_hyp = hydro_pkg->Param("calc_dt_hyp"); + if (calc_dt_hyp) { + dt_hyp = EstimateHyperbolicTimestep(md); + min_dt = std::min(min_dt, dt_hyp); } const auto &enable_cooling = hydro_pkg->Param("enable_cooling"); @@ -869,17 +841,34 @@ Real EstimateTimestep(MeshData *md) { min_dt = std::min(min_dt, tabular_cooling.EstimateTimeStep(md)); } - // For RKL2 STS, the diffusive timestep is calculated separately in the driver - if (hydro_pkg->Param("diffint") == DiffInt::unsplit) { + auto dt_diff = std::numeric_limits::max(); + if (hydro_pkg->Param("diffint") != DiffInt::none) { if (hydro_pkg->Param("conduction") != Conduction::none) { - min_dt = std::min(min_dt, EstimateConductionTimestep(md)); + dt_diff = std::min(dt_diff, EstimateConductionTimestep(md)); } if (hydro_pkg->Param("viscosity") != Viscosity::none) { - min_dt = std::min(min_dt, EstimateViscosityTimestep(md)); + dt_diff = std::min(dt_diff, EstimateViscosityTimestep(md)); } if (hydro_pkg->Param("resistivity") != Resistivity::none) { - min_dt = std::min(min_dt, EstimateResistivityTimestep(md)); + dt_diff = std::min(dt_diff, EstimateResistivityTimestep(md)); + } + + // For unsplit ingegration use strict limit + if (hydro_pkg->Param("diffint") == DiffInt::unsplit) { + min_dt = std::min(min_dt, dt_diff); + // and for RKL2 integration use limit taking into account the maxium ratio + // or not constrain limit further (which is why RKL2 is there in first place) + } else if (hydro_pkg->Param("diffint") == DiffInt::rkl2) { + const auto max_dt_ratio = hydro_pkg->Param("rkl2_max_dt_ratio"); + if (calc_dt_hyp && max_dt_ratio > 0.0 && dt_hyp / dt_diff > max_dt_ratio) { + min_dt = std::min(min_dt, max_dt_ratio * dt_diff); + } + } else { + PARTHENON_THROW("Looks like a a new diffusion integrator was implemented without " + "taking into accout timestep contstraints yet."); } + auto dt_diff_param = hydro_pkg->Param("dt_diff"); + hydro_pkg->UpdateParam("dt_diff", std::min(dt_diff, dt_diff_param)); } if (ProblemEstimateTimestep != nullptr) { diff --git a/src/hydro/hydro_driver.cpp b/src/hydro/hydro_driver.cpp index 36bdce91..eeb29d3d 100644 --- a/src/hydro/hydro_driver.cpp +++ b/src/hydro/hydro_driver.cpp @@ -447,7 +447,10 @@ TaskCollection HydroDriver::MakeTaskCollection(BlockList_t &blocks, int stage) { // Calculate hyperbolic divergence cleaning speed // TODO(pgrete) Calculating mindx is only required after remeshing. Need to find a clean // solution for this one-off global reduction. - if (hydro_pkg->Param("calc_c_h") && (stage == 1)) { + // TODO(PG) move this to PreStepMeshUserWorkInLoop + if ((hydro_pkg->Param("calc_c_h") || + hydro_pkg->Param("diffint") != DiffInt::none) && + (stage == 1)) { // need to make sure that there's only one region in order to MPI_reduce to work TaskRegion &single_task_region = tc.AddRegion(1); auto &tl = single_task_region[0]; @@ -468,14 +471,16 @@ TaskCollection HydroDriver::MakeTaskCollection(BlockList_t &blocks, int stage) { reduce_c_h = tl.AddTask( prev_task, [](StateDescriptor *hydro_pkg) { - Real mins[2]; + Real mins[3]; mins[0] = hydro_pkg->Param("mindx"); mins[1] = hydro_pkg->Param("dt_hyp"); - PARTHENON_MPI_CHECK(MPI_Allreduce(MPI_IN_PLACE, mins, 2, MPI_PARTHENON_REAL, + mins[2] = hydro_pkg->Param("dt_diff"); + PARTHENON_MPI_CHECK(MPI_Allreduce(MPI_IN_PLACE, mins, 3, MPI_PARTHENON_REAL, MPI_MIN, MPI_COMM_WORLD)); hydro_pkg->UpdateParam("mindx", mins[0]); hydro_pkg->UpdateParam("dt_hyp", mins[1]); + hydro_pkg->UpdateParam("dt_diff", mins[2]); return TaskStatus::complete; }, hydro_pkg.get()); @@ -657,7 +662,9 @@ TaskCollection HydroDriver::MakeTaskCollection(BlockList_t &blocks, int stage) { // Single task in single (serial) region to reset global vars used in reductions in the // first stage. - if (stage == integrator->nstages && hydro_pkg->Param("calc_c_h")) { + if (stage == integrator->nstages && + (hydro_pkg->Param("calc_c_h") || + hydro_pkg->Param("diffint") != DiffInt::none)) { TaskRegion &reset_reduction_vars_region = tc.AddRegion(1); auto &tl = reset_reduction_vars_region[0]; tl.AddTask( @@ -665,6 +672,7 @@ TaskCollection HydroDriver::MakeTaskCollection(BlockList_t &blocks, int stage) { [](StateDescriptor *hydro_pkg) { hydro_pkg->UpdateParam("mindx", std::numeric_limits::max()); hydro_pkg->UpdateParam("dt_hyp", std::numeric_limits::max()); + hydro_pkg->UpdateParam("dt_diff", std::numeric_limits::max()); return TaskStatus::complete; }, hydro_pkg.get()); From 867fb3a2da10e9881be788f37fc177084b211ebe Mon Sep 17 00:00:00 2001 From: Philipp Grete Date: Sun, 10 Nov 2024 11:45:30 +0100 Subject: [PATCH 08/11] Enforce max ratio (i.e., limit number of stages) even without dt_hyp --- src/hydro/hydro.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hydro/hydro.cpp b/src/hydro/hydro.cpp index 3861e53a..4e2e5eec 100644 --- a/src/hydro/hydro.cpp +++ b/src/hydro/hydro.cpp @@ -860,7 +860,7 @@ Real EstimateTimestep(MeshData *md) { // or not constrain limit further (which is why RKL2 is there in first place) } else if (hydro_pkg->Param("diffint") == DiffInt::rkl2) { const auto max_dt_ratio = hydro_pkg->Param("rkl2_max_dt_ratio"); - if (calc_dt_hyp && max_dt_ratio > 0.0 && dt_hyp / dt_diff > max_dt_ratio) { + if (max_dt_ratio > 0.0 && dt_hyp / dt_diff > max_dt_ratio) { min_dt = std::min(min_dt, max_dt_ratio * dt_diff); } } else { From fcb5f397aca9032410050da03d45985148cd91a3 Mon Sep 17 00:00:00 2001 From: Philipp Grete Date: Sun, 10 Nov 2024 12:47:59 +0100 Subject: [PATCH 09/11] Ensure c_h is present --- src/hydro/hydro.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/hydro/hydro.cpp b/src/hydro/hydro.cpp index 4e2e5eec..5720346c 100644 --- a/src/hydro/hydro.cpp +++ b/src/hydro/hydro.cpp @@ -217,14 +217,17 @@ std::shared_ptr Initialize(ParameterInput *pin) { auto glmmhd_alpha = pin->GetOrAddReal("hydro", "glmmhd_alpha", 0.1); pkg->AddParam("glmmhd_alpha", glmmhd_alpha); calc_c_h = true; - pkg->AddParam( - "c_h", 0.0, Params::Mutability::Restart); // hyperbolic divergence cleaning speed } else { PARTHENON_FAIL("AthenaPK hydro: Unknown fluid method."); } pkg->AddParam<>("fluid", fluid); pkg->AddParam<>("nhydro", nhydro); pkg->AddParam<>("calc_c_h", calc_c_h); + // Following params should (currently) be present independent of solver because + // they're all used in the main loop. + // TODO(pgrete) think about which approach (selective versus always is preferable) + pkg->AddParam( + "c_h", 0.0, Params::Mutability::Restart); // hyperbolic divergence cleaning speed // global minimum dx (used to calc c_h) pkg->AddParam("mindx", std::numeric_limits::max(), Params::Mutability::Restart); From f4d3b824b0a55fd749a013cc84fa125ff6b0eaa3 Mon Sep 17 00:00:00 2001 From: Philipp Grete Date: Sun, 10 Nov 2024 17:43:58 +0100 Subject: [PATCH 10/11] Add Changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c95a86ae..b1d49a38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### Fixed (not changing behavior/API/variables/...) ### Infrastructure +- [[PR 124]](https://github.com/parthenon-hpc-lab/athenapk/pull/124) Bump Kokkos 4.4.1 (and Parthenon to include view-of-view fix) - [[PR 117]](https://github.com/parthenon-hpc-lab/athenapk/pull/117) Update devcontainer.json to latest CI container - [[PR 114]](https://github.com/parthenon-hpc-lab/athenapk/pull/114) Bump Parthenon 24.08 and Kokkos to 4.4.00 - [[PR 112]](https://github.com/parthenon-hpc-lab/athenapk/pull/112) Add dev container configuration @@ -24,6 +25,8 @@ ### Removed (removing behavior/API/varaibles/...) ### Incompatibilities (i.e. breaking changes) +- [[PR 124]](https://github.com/parthenon-hpc-lab/athenapk/pull/124) Enrolling custom boundary conditions changed + - Boundary conditions can now be enrolled using a string that can be subsequently be used in the input file (see, e.g., cloud problem generator) - [[PR 114]](https://github.com/parthenon-hpc-lab/athenapk/pull/114) Bump Parthenon 24.08 and Kokkos to 4.4.00 - Changed signature of `UserWorkBeforeOutput` to include `SimTime` as last paramter - Fixes bitwise idential restarts for AMR simulations (the derefinement counter is now included) From f8497c5f39473c557b84c7078e1eb696246e139c Mon Sep 17 00:00:00 2001 From: Philipp Grete Date: Sat, 30 Nov 2024 01:29:43 +0100 Subject: [PATCH 11/11] Fix various `dt_diff` issues recently introduced and bump Parthenon (for `dn` outputs) (#129) * Fix reset of dt_diff * Ensure dt_diff is set when STS tasks are added * Bump Parth for dncycle * Lessen restriction on mutability requirements for dt params * Add Changelog --- CHANGELOG.md | 2 + external/parthenon | 2 +- src/hydro/hydro.cpp | 110 +++++++++++++++++++++++++++----- src/hydro/hydro_driver.cpp | 124 +++++-------------------------------- 4 files changed, 112 insertions(+), 126 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1d49a38..fc2af035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,10 @@ - [[PR 84]](https://github.com/parthenon-hpc-lab/athenapk/pull/84) Bump Parthenon to latest develop (2024-02-15) ### Fixed (not changing behavior/API/variables/...) +- [[PR 128]](https://github.com/parthenon-hpc-lab/athenapk/pull/128) Fixed `dt_diff` in RKL2 ### Infrastructure +- [[PR 128]](https://github.com/parthenon-hpc-lab/athenapk/pull/128) Bump Parthenon to support `dn` based outputs - [[PR 124]](https://github.com/parthenon-hpc-lab/athenapk/pull/124) Bump Kokkos 4.4.1 (and Parthenon to include view-of-view fix) - [[PR 117]](https://github.com/parthenon-hpc-lab/athenapk/pull/117) Update devcontainer.json to latest CI container - [[PR 114]](https://github.com/parthenon-hpc-lab/athenapk/pull/114) Bump Parthenon 24.08 and Kokkos to 4.4.00 diff --git a/external/parthenon b/external/parthenon index e00017c6..07948ef1 160000 --- a/external/parthenon +++ b/external/parthenon @@ -1 +1 @@ -Subproject commit e00017c6ece547f3adf9a16b94ddf0bdf8bb4773 +Subproject commit 07948ef150b7146acdd270b70c43701f3f26dd4c diff --git a/src/hydro/hydro.cpp b/src/hydro/hydro.cpp index 5720346c..64c5d4a3 100644 --- a/src/hydro/hydro.cpp +++ b/src/hydro/hydro.cpp @@ -57,12 +57,87 @@ parthenon::Packages_t ProcessPackages(std::unique_ptr &pin) { return packages; } +// Calculate mininum dx, which is used in calculating the divergence cleaning speed c_h +// TODO(PG) eventually move to calculating the timestep once the timestep calc +// has been moved to be done before Step() +Real CalculateGlobalMinDx(MeshData *md) { + auto *pmb = md->GetBlockData(0)->GetBlockPointer(); + auto hydro_pkg = pmb->packages.Get("Hydro"); + + const auto &prim_pack = md->PackVariables(std::vector{"prim"}); + + IndexRange ib = md->GetBlockData(0)->GetBoundsI(IndexDomain::interior); + IndexRange jb = md->GetBlockData(0)->GetBoundsJ(IndexDomain::interior); + IndexRange kb = md->GetBlockData(0)->GetBoundsK(IndexDomain::interior); + + Real mindx = std::numeric_limits::max(); + + bool nx2 = prim_pack.GetDim(2) > 1; + bool nx3 = prim_pack.GetDim(3) > 1; + pmb->par_reduce( + "CalculateGlobalMinDx", 0, prim_pack.GetDim(5) - 1, kb.s, kb.e, jb.s, jb.e, ib.s, + ib.e, + KOKKOS_LAMBDA(const int b, const int k, const int j, const int i, Real &lmindx) { + const auto &coords = prim_pack.GetCoords(b); + lmindx = fmin(lmindx, coords.Dxc<1>(k, j, i)); + if (nx2) { + lmindx = fmin(lmindx, coords.Dxc<2>(k, j, i)); + } + if (nx3) { + lmindx = fmin(lmindx, coords.Dxc<3>(k, j, i)); + } + }, + Kokkos::Min(mindx)); + + return mindx; +} + // Using this per cycle function to populate various variables in // Params that require global reduction *and* need to be set/known when // the task list is constructed (versus when the task list is being executed). // TODO(next person touching this function): If more/separate feature are required // please separate concerns. -void PreStepMeshUserWorkInLoop(Mesh *pmesh, ParameterInput *pin, SimTime &tm) {} +void PreStepMeshUserWorkInLoop(Mesh *pmesh, ParameterInput *pin, SimTime &tm) { + auto hydro_pkg = pmesh->packages.Get("Hydro"); + + // Calculate hyperbolic divergence cleaning speed + // TODO(pgrete) Calculating mindx is only required after remeshing. Need to + // find a clean solution for this one-off global reduction. + if (hydro_pkg->Param("calc_c_h") || + hydro_pkg->Param("diffint") != DiffInt::none) { + + Real mindx = std::numeric_limits::max(); + // Going over default partitions. Not using a (new) single partition containing + // all blocks here as this (default) split is also used main Step() function and + // thus does not create an overhead (such as creating a new MeshBlockPack that is just + // used here). All partitions are executed sequentially. Given that a par_reduce to a + // host var is blocking it's save to dirctly use the return value. + const int num_partitions = pmesh->DefaultNumPartitions(); + for (int i = 0; i < num_partitions; i++) { + auto &mu0 = pmesh->mesh_data.GetOrAdd("base", i); + mindx = std::min(mindx, CalculateGlobalMinDx(mu0.get())); + } +#ifdef MPI_PARALLEL + Real mins[3]; + mins[0] = mindx; + mins[1] = hydro_pkg->Param("dt_hyp"); + mins[2] = hydro_pkg->Param("dt_diff"); + PARTHENON_MPI_CHECK(MPI_Allreduce(MPI_IN_PLACE, mins, 3, MPI_PARTHENON_REAL, MPI_MIN, + MPI_COMM_WORLD)); + + hydro_pkg->UpdateParam("mindx", mins[0]); + hydro_pkg->UpdateParam("dt_hyp", mins[1]); + hydro_pkg->UpdateParam("dt_diff", mins[2]); +#else + hydro_pkg->UpdateParam("mindx", mindx); + // dt_hyp and dt_diff are already set directly in Params when they're calculated +#endif + // Finally update c_h + const auto &cfl_hyp = hydro_pkg->Param("cfl"); + const auto &dt_hyp = hydro_pkg->Param("dt_hyp"); + hydro_pkg->UpdateParam("c_h", cfl_hyp * mindx / dt_hyp); + } +} template Real HydroHst(MeshData *md) { @@ -227,13 +302,13 @@ std::shared_ptr Initialize(ParameterInput *pin) { // they're all used in the main loop. // TODO(pgrete) think about which approach (selective versus always is preferable) pkg->AddParam( - "c_h", 0.0, Params::Mutability::Restart); // hyperbolic divergence cleaning speed + "c_h", 0.0, Params::Mutability::Mutable); // hyperbolic divergence cleaning speed // global minimum dx (used to calc c_h) pkg->AddParam("mindx", std::numeric_limits::max(), - Params::Mutability::Restart); + Params::Mutability::Mutable); // hyperbolic timestep constraint pkg->AddParam("dt_hyp", std::numeric_limits::max(), - Params::Mutability::Restart); + Params::Mutability::Mutable); const auto recon_str = pin->GetString("hydro", "reconstruction"); int recon_need_nghost = 3; // largest number for the choices below @@ -610,7 +685,7 @@ std::shared_ptr Initialize(ParameterInput *pin) { pkg->AddParam<>("cfl_diff", cfl_diff); } pkg->AddParam("dt_diff", std::numeric_limits::max(), - Params::Mutability::Restart); // diffusive timestep constraint + Params::Mutability::Mutable); // diffusive timestep constraint pkg->AddParam<>("diffint", diffint); if (fluid == Fluid::euler) { @@ -1189,10 +1264,10 @@ TaskStatus FirstOrderFluxCorrect(MeshData *u0_data, MeshData *u1_dat const auto &u0_prim = u0_prim_pack(b); auto &u0_cons = u0_cons_pack(b); - // In principle, the u_cons.fluxes could be updated in parallel by a different - // thread resulting in a race conditon here. - // However, if the fluxes of a cell have been updated (anywhere) then the entire - // kernel will be called again anyway, and, at that point the already fixed + // In principle, the u_cons.fluxes could be updated in parallel by a + // different thread resulting in a race conditon here. However, if the + // fluxes of a cell have been updated (anywhere) then the entire kernel will + // be called again anyway, and, at that point the already fixed // u0_cons.fluxes will automaticlly be used here. Real new_cons[NVAR]; for (auto v = 0; v < NVAR; v++) { @@ -1220,13 +1295,13 @@ TaskStatus FirstOrderFluxCorrect(MeshData *u0_data, MeshData *u1_dat lnum_need_floor += 1; return; } - // In principle, there could be a racecondion as this loop goes over all k,j,i - // and we updating the i+1 flux here. - // However, the results are idential because u0_prim is never updated in this - // kernel so we don't worry about it. - // TODO(pgrete) as we need to keep the function signature idential for now (due - // to Cuda compiler bug) we could potentially template these function and get - // rid of the `if constexpr` + // In principle, there could be a racecondion as this loop goes over all + // k,j,i and we updating the i+1 flux here. However, the results are + // idential because u0_prim is never updated in this kernel so we don't + // worry about it. + // TODO(pgrete) as we need to keep the function signature idential for now + // (due to Cuda compiler bug) we could potentially template these function + // and get rid of the `if constexpr` riemann.Solve(eos, k, j, i, IV1, u0_prim, u0_cons, c_h); riemann.Solve(eos, k, j, i + 1, IV1, u0_prim, u0_cons, c_h); @@ -1243,7 +1318,8 @@ TaskStatus FirstOrderFluxCorrect(MeshData *u0_data, MeshData *u1_dat Kokkos::Sum(num_corrected), Kokkos::Sum(num_need_floor)); // TODO(pgrete) make this optional and global (potentially store values in Params) - // std::cout << "[" << parthenon::Globals::my_rank << "] Attempt: " << num_attempts + // std::cout << "[" << parthenon::Globals::my_rank << "] Attempt: " << + // num_attempts // << " Corrected (center): " << num_corrected // << " Failed (will rely on floor): " << num_need_floor << std::endl; num_attempts += 1; diff --git a/src/hydro/hydro_driver.cpp b/src/hydro/hydro_driver.cpp index eeb29d3d..3ded1d6b 100644 --- a/src/hydro/hydro_driver.cpp +++ b/src/hydro/hydro_driver.cpp @@ -37,46 +37,6 @@ HydroDriver::HydroDriver(ParameterInput *pin, ApplicationInput *app_in, Mesh *pm pin->CheckDesired("parthenon/time", "cfl"); } -// Calculate mininum dx, which is used in calculating the divergence cleaning speed c_h -TaskStatus CalculateGlobalMinDx(MeshData *md) { - auto pmb = md->GetBlockData(0)->GetBlockPointer(); - auto hydro_pkg = pmb->packages.Get("Hydro"); - - const auto &prim_pack = md->PackVariables(std::vector{"prim"}); - - IndexRange ib = md->GetBlockData(0)->GetBoundsI(IndexDomain::interior); - IndexRange jb = md->GetBlockData(0)->GetBoundsJ(IndexDomain::interior); - IndexRange kb = md->GetBlockData(0)->GetBoundsK(IndexDomain::interior); - - Real mindx = std::numeric_limits::max(); - - bool nx2 = prim_pack.GetDim(2) > 1; - bool nx3 = prim_pack.GetDim(3) > 1; - pmb->par_reduce( - "CalculateGlobalMinDx", 0, prim_pack.GetDim(5) - 1, kb.s, kb.e, jb.s, jb.e, ib.s, - ib.e, - KOKKOS_LAMBDA(const int b, const int k, const int j, const int i, Real &lmindx) { - const auto &coords = prim_pack.GetCoords(b); - lmindx = fmin(lmindx, coords.Dxc<1>(k, j, i)); - if (nx2) { - lmindx = fmin(lmindx, coords.Dxc<2>(k, j, i)); - } - if (nx3) { - lmindx = fmin(lmindx, coords.Dxc<3>(k, j, i)); - } - }, - Kokkos::Min(mindx)); - - // Reduction to host var is blocking and only have one of this tasks run at the same - // time so modifying the package should be safe. - auto mindx_pkg = hydro_pkg->Param("mindx"); - if (mindx < mindx_pkg) { - hydro_pkg->UpdateParam("mindx", mindx); - } - - return TaskStatus::complete; -} - // Sets all fluxes to 0 TaskStatus ResetFluxes(MeshData *md) { auto pmb = md->GetBlockData(0)->GetBlockPointer(); @@ -444,60 +404,6 @@ TaskCollection HydroDriver::MakeTaskCollection(BlockList_t &blocks, int stage) { } } - // Calculate hyperbolic divergence cleaning speed - // TODO(pgrete) Calculating mindx is only required after remeshing. Need to find a clean - // solution for this one-off global reduction. - // TODO(PG) move this to PreStepMeshUserWorkInLoop - if ((hydro_pkg->Param("calc_c_h") || - hydro_pkg->Param("diffint") != DiffInt::none) && - (stage == 1)) { - // need to make sure that there's only one region in order to MPI_reduce to work - TaskRegion &single_task_region = tc.AddRegion(1); - auto &tl = single_task_region[0]; - // Adding one task for each partition. Not using a (new) single partition containing - // all blocks here as this (default) split is also used for the following tasks and - // thus does not create an overhead (such as creating a new MeshBlockPack that is just - // used here). Given that all partitions are in one task list they'll be executed - // sequentially. Given that a par_reduce to a host var is blocking it's also save to - // store the variable in the Params for now. - auto prev_task = none; - for (int i = 0; i < num_partitions; i++) { - auto &mu0 = pmesh->mesh_data.GetOrAdd("base", i); - auto new_mindx = tl.AddTask(prev_task, CalculateGlobalMinDx, mu0.get()); - prev_task = new_mindx; - } - auto reduce_c_h = prev_task; -#ifdef MPI_PARALLEL - reduce_c_h = tl.AddTask( - prev_task, - [](StateDescriptor *hydro_pkg) { - Real mins[3]; - mins[0] = hydro_pkg->Param("mindx"); - mins[1] = hydro_pkg->Param("dt_hyp"); - mins[2] = hydro_pkg->Param("dt_diff"); - PARTHENON_MPI_CHECK(MPI_Allreduce(MPI_IN_PLACE, mins, 3, MPI_PARTHENON_REAL, - MPI_MIN, MPI_COMM_WORLD)); - - hydro_pkg->UpdateParam("mindx", mins[0]); - hydro_pkg->UpdateParam("dt_hyp", mins[1]); - hydro_pkg->UpdateParam("dt_diff", mins[2]); - return TaskStatus::complete; - }, - hydro_pkg.get()); -#endif - // Finally update c_h - auto update_c_h = tl.AddTask( - reduce_c_h, - [](StateDescriptor *hydro_pkg) { - const auto &mindx = hydro_pkg->Param("mindx"); - const auto &cfl_hyp = hydro_pkg->Param("cfl"); - const auto &dt_hyp = hydro_pkg->Param("dt_hyp"); - hydro_pkg->UpdateParam("c_h", cfl_hyp * mindx / dt_hyp); - return TaskStatus::complete; - }, - hydro_pkg.get()); - } - // calculate magnetic tower scaling if ((stage == 1) && hydro_pkg->AllParams().hasKey("magnetic_tower_power_scaling") && hydro_pkg->Param("magnetic_tower_power_scaling")) { @@ -660,8 +566,24 @@ TaskCollection HydroDriver::MakeTaskCollection(BlockList_t &blocks, int stage) { pmesh->multilevel); } + TaskRegion &single_tasklist_per_pack_region_3 = tc.AddRegion(num_partitions); + for (int i = 0; i < num_partitions; i++) { + auto &tl = single_tasklist_per_pack_region_3[i]; + auto &mu0 = pmesh->mesh_data.GetOrAdd("base", i); + auto fill_derived = + tl.AddTask(none, parthenon::Update::FillDerived>, mu0.get()); + } + const auto &diffint = hydro_pkg->Param("diffint"); + // If any tasks modify the conserved variables before this place and after FillDerived, + // then the STS tasks should be updated to not assume prim and cons are in sync. + if (diffint == DiffInt::rkl2 && stage == integrator->nstages) { + AddSTSTasks(&tc, pmesh, blocks, 0.5 * tm.dt); + } + // Single task in single (serial) region to reset global vars used in reductions in the // first stage. + // TODO(pgrete) check if we logically need this reset or if we can reset within the + // timestep task if (stage == integrator->nstages && (hydro_pkg->Param("calc_c_h") || hydro_pkg->Param("diffint") != DiffInt::none)) { @@ -678,20 +600,6 @@ TaskCollection HydroDriver::MakeTaskCollection(BlockList_t &blocks, int stage) { hydro_pkg.get()); } - TaskRegion &single_tasklist_per_pack_region_3 = tc.AddRegion(num_partitions); - for (int i = 0; i < num_partitions; i++) { - auto &tl = single_tasklist_per_pack_region_3[i]; - auto &mu0 = pmesh->mesh_data.GetOrAdd("base", i); - auto fill_derived = - tl.AddTask(none, parthenon::Update::FillDerived>, mu0.get()); - } - const auto &diffint = hydro_pkg->Param("diffint"); - // If any tasks modify the conserved variables before this place and after FillDerived, - // then the STS tasks should be updated to not assume prim and cons are in sync. - if (diffint == DiffInt::rkl2 && stage == integrator->nstages) { - AddSTSTasks(&tc, pmesh, blocks, 0.5 * tm.dt); - } - if (stage == integrator->nstages) { TaskRegion &tr = tc.AddRegion(num_partitions); for (int i = 0; i < num_partitions; i++) {