From 5c1b0062fdec15808248d53b485760c3c65cf118 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 30 Oct 2025 13:47:18 +0100 Subject: [PATCH 01/83] Added a basic template for the Github actions (#72) Redo of https://github.com/struphy-hub/struphy/pull/6 --- .github/actions/compile/action.yml | 19 +++ .../install-struphy-editable/action.yml | 14 ++ .../install/install-struphy/action.yml | 21 +++ .../actions/install/macos-latest/action.yml | 37 +++++ .../actions/install/ubuntu-latest/action.yml | 21 +++ .github/actions/tests/models/action.yml | 16 +++ .github/actions/tests/quickstart/action.yml | 17 +++ .github/actions/tests/unit/action.yml | 9 ++ .github/workflows/docs.yml | 85 +++++++++++ .github/workflows/publish.yml | 33 +++++ .github/workflows/static_analysis.yml | 136 ++++++++++++++++++ .github/workflows/testing.yml | 95 ++++++++++++ pyproject.toml | 9 +- .../massless_kernels_control_variate.py | 1 + .../set_release_dependencies.py | 2 +- 15 files changed, 509 insertions(+), 6 deletions(-) create mode 100644 .github/actions/compile/action.yml create mode 100644 .github/actions/install/install-struphy-editable/action.yml create mode 100644 .github/actions/install/install-struphy/action.yml create mode 100644 .github/actions/install/macos-latest/action.yml create mode 100644 .github/actions/install/ubuntu-latest/action.yml create mode 100644 .github/actions/tests/models/action.yml create mode 100644 .github/actions/tests/quickstart/action.yml create mode 100644 .github/actions/tests/unit/action.yml create mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/static_analysis.yml create mode 100644 .github/workflows/testing.yml rename {src/struphy/utils => utils}/set_release_dependencies.py (100%) diff --git a/.github/actions/compile/action.yml b/.github/actions/compile/action.yml new file mode 100644 index 000000000..46657900f --- /dev/null +++ b/.github/actions/compile/action.yml @@ -0,0 +1,19 @@ +name: "Compile kernels with pyccel" + +runs: + using: composite + steps: + - name: Checkout pyccel version + shell: bash + run: | + pyccel --version + + - name: Compile kernels + shell: bash + run: | + struphy compile --status + struphy compile -y --language ${{ matrix.compile-language }} || ( + echo "Initial compile failed. Removing compiled kernels and trying again..." && + struphy compile -d -y && + struphy compile -y --language ${{ matrix.compile-language }} + ) diff --git a/.github/actions/install/install-struphy-editable/action.yml b/.github/actions/install/install-struphy-editable/action.yml new file mode 100644 index 000000000..49d871e15 --- /dev/null +++ b/.github/actions/install/install-struphy-editable/action.yml @@ -0,0 +1,14 @@ +name: "Clone and install struphy" + +runs: + using: composite + steps: + + - name: Install struphy + shell: bash + run: | + pip install --upgrade pip + pip uninstall -y gvec + pip install -e ".[dev,mpi]" + pip list + struphy -h diff --git a/.github/actions/install/install-struphy/action.yml b/.github/actions/install/install-struphy/action.yml new file mode 100644 index 000000000..bce677589 --- /dev/null +++ b/.github/actions/install/install-struphy/action.yml @@ -0,0 +1,21 @@ +name: "Clone and install struphy" + +runs: + using: composite + steps: + + - name: Install struphy + shell: bash + run: | + echo $FC + echo $CC + echo $CXX + echo "----------------" + which gfortran + which gcc + which g++ + pip install --upgrade pip + pip uninstall -y gvec + pip install ".[phys,mpi]" + pip list + struphy -h diff --git a/.github/actions/install/macos-latest/action.yml b/.github/actions/install/macos-latest/action.yml new file mode 100644 index 000000000..6e0593685 --- /dev/null +++ b/.github/actions/install/macos-latest/action.yml @@ -0,0 +1,37 @@ +name: "Install MacOS prereqs" + +runs: + using: composite + steps: + - name: Install dependencies + shell: bash + run: | + set -euo pipefail + # ensure Homebrew bin is first + export PATH="/opt/homebrew/bin:$PATH" + brew update + # brew install python3 + # brew install gcc + brew install openblas + brew install lapack + brew install open-mpi + brew install pkgconf + brew install libomp + brew install git + brew install pandoc + brew install hdf5 + brew install netcdf-fortran + brew install cmake + brew link --overwrite cmake + cmake --version + make -v + system_profiler SPHardwareDataType + which gfortran || echo "could not find gfortran" + which gcc || echo "could not find gcc" + which g++ || echo "could not find g++" + # Update environment variables + echo "FC=$(which gfortran)" >> $GITHUB_ENV # for gvec + echo "CC=$(which gcc)" >> $GITHUB_ENV # for gvec + echo "CXX=$(which g++)" >> $GITHUB_ENV # for gvec + echo "----------------" + diff --git a/.github/actions/install/ubuntu-latest/action.yml b/.github/actions/install/ubuntu-latest/action.yml new file mode 100644 index 000000000..abcaba465 --- /dev/null +++ b/.github/actions/install/ubuntu-latest/action.yml @@ -0,0 +1,21 @@ +name: "Install ubuntu prereqs" + +runs: + using: composite + steps: + - name: Install dependencies + shell: bash + run: | + sudo apt install -y software-properties-common + sudo add-apt-repository -y ppa:deadsnakes/ppa + sudo apt update -y + sudo apt install -y python3-pip + sudo apt install -y python3-venv + sudo apt install -y gfortran gcc + sudo apt install -y liblapack-dev libopenmpi-dev + sudo apt install -y libblas-dev openmpi-bin + sudo apt install -y libomp-dev libomp5 + sudo apt install -y git + sudo apt install -y pandoc + sudo apt install -y libnetcdf-dev + sudo apt install -y g++ liblapack3 cmake cmake-curses-gui zlib1g-dev libnetcdf-dev libnetcdff-dev diff --git a/.github/actions/tests/models/action.yml b/.github/actions/tests/models/action.yml new file mode 100644 index 000000000..0a9fafc16 --- /dev/null +++ b/.github/actions/tests/models/action.yml @@ -0,0 +1,16 @@ +name: "Run model tests" + +runs: + using: composite + steps: + - name: Install dependencies + shell: bash + run: | + struphy compile --status + struphy compile --status + struphy test models --fast + struphy test models --fast --mpi 2 + struphy test models --fast --verification --mpi 1 + struphy test models --fast --verification --mpi 4 + struphy test models --fast --verification --mpi 4 --nclones 2 + struphy test DriftKineticElectrostaticAdiabatic --mpi 2 --nclones 2 \ No newline at end of file diff --git a/.github/actions/tests/quickstart/action.yml b/.github/actions/tests/quickstart/action.yml new file mode 100644 index 000000000..b78ade5e7 --- /dev/null +++ b/.github/actions/tests/quickstart/action.yml @@ -0,0 +1,17 @@ +name: "Run quickstart" + +runs: + using: composite + steps: + - name: Run quickstart + shell: bash + run: | + struphy -p + struphy -h + struphy params VlasovAmpereOneSpecies -y + ls -1a + mv params_VlasovAmpereOneSpecies.py test.py + python3 test.py + ls sim_1/ + mpirun -n 2 python3 test.py + LINE_PROFILE=1 mpirun -n 2 python3 test.py diff --git a/.github/actions/tests/unit/action.yml b/.github/actions/tests/unit/action.yml new file mode 100644 index 000000000..2d0e70a2d --- /dev/null +++ b/.github/actions/tests/unit/action.yml @@ -0,0 +1,9 @@ +name: "Run unit tests" + +runs: + using: composite + steps: + - name: Run unit tests + shell: bash + run: | + struphy test unit diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..79caa16e7 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,85 @@ +name: Deploy docs to GitHub Pages + +on: + push: + branches: ["devel", "main"] # TODO: Set to main only after release + workflow_dispatch: + +defaults: + run: + shell: bash + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + container: + image: texlive/texlive:latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install pandoc + run: | + apt-get update + apt-get install -y pandoc + + # - name: Install TeX Live with pdflatex + # run: | + # sudo apt-get update + # sudo apt-get install -y texlive-full + # sudo apt-get install -y texlive texlive-latex-base texlive-latex-extra texlive-fonts-recommended texlive-science + + - name: Check if pdflatex is available + run: | + which pdflatex + pdflatex --version + + - name: Set up virtual environment and install project + run: | + python -m venv env + source env/bin/activate + pip install --upgrade pip + pip install ".[test, dev, docs]" + + - name: Build Sphinx docs + run: | + source env/bin/activate + cd docs + make html + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload built docs + uses: actions/upload-pages-artifact@v3 + with: + path: docs/build/html/ + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..02f9fc5ea --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,33 @@ +name: Publish Python Package + +on: + push: + branches: + - main + +jobs: + build-and-publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install build tools + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build the package + run: python -m build + + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ # Use API token + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: twine upload dist/* diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml new file mode 100644 index 000000000..5dc140173 --- /dev/null +++ b/.github/workflows/static_analysis.yml @@ -0,0 +1,136 @@ +name: Static analysis + +on: + push: + branches: + - main + - devel + pull_request: + branches: + - main + - devel + +defaults: + run: + shell: bash + +jobs: + cloc: + runs-on: ubuntu-latest + steps: + - name: Checkout the code + uses: actions/checkout@v4 + + - name: Download and run cloc + run: | + curl -s https://raw.githubusercontent.com/AlDanial/cloc/master/cloc > cloc + chmod +x cloc + ./cloc --version + ./cloc $(git ls-files) + + struphy_lint_all: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + # You can test your matrix by printing the current Python version + - name: Display Python version + run: python -c "import sys; print(sys.version)" + + # Cache pip dependencies + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install prerequisites (Ubuntu) + uses: ./.github/actions/install/ubuntu-latest + + - name: Install struphy + uses: ./.github/actions/install/install-struphy-editable + + - name: Lint the repo + run: | + struphy lint all --output-format plain --verbose + + # black: + # runs-on: ubuntu-latest + # continue-on-error: true + # steps: + # - name: Checkout the code + # uses: actions/checkout@v4 + + # - name: Code formatting with black + # run: | + # pip install black "black[jupyter]" + # # black --check src/ + # # black --check tutorials/ + + isort: + runs-on: ubuntu-latest + continue-on-error: true + steps: + - name: Checkout the code + uses: actions/checkout@v4 + + - name: Code formatting with isort + run: | + pip install isort + isort --check src/ + isort --check doc/tutorials/ + + # mypy: + # runs-on: ubuntu-latest + # continue-on-error: true + # steps: + # - name: Checkout the code + # uses: actions/checkout@v4 + + # - name: Type checking with mypy + # run: | + # pip install mypy + # mypy src/ || true + + # prospector: + # runs-on: ubuntu-latest + # continue-on-error: true + # steps: + # - name: Checkout the code + # uses: actions/checkout@v4 + + # - name: Code analysis with prospector + # run: | + # pip install prospector + # prospector src/ || true + + ruff: + runs-on: ubuntu-latest + steps: + - name: Checkout the code + uses: actions/checkout@v4 + + - name: Linting with ruff + run: | + pip install ruff + ruff check --select I src/**/*.py + + # pylint: + # runs-on: ubuntu-latest + # continue-on-error: true + # steps: + # - name: Checkout the code + # uses: actions/checkout@v4 + + # - name: Linting with pylint + # run: | + # pip install pylint + # pylint src/ || true diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 000000000..3404f6348 --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,95 @@ +name: Testing + +on: + push: + branches: + - main + - devel + pull_request: + branches: + - main + - devel + +jobs: + test: + runs-on: ${{ matrix.os }} + env: + OMPI_MCA_rmaps_base_oversubscribe: 1 # Linux + PRRTE_MCA_rmaps_base_oversubscribe: 1 # MacOS + strategy: + fail-fast: false + matrix: + python-version: ["3.12"] + os: ["ubuntu-latest"] #, "macos-latest"] + compile-language: ["fortran", "c"] + test-type: ["unit", "model"] #, "quickstart"] + + steps: + # Checkout the repository + - name: Checkout code + uses: actions/checkout@v5 + + # https://docs.github.com/en/actions/tutorials/build-and-test-code/python + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + # You can test your matrix by printing the current Python version + - name: Display Python version + run: python -c "import sys; print(sys.version)" + + # Cache pip dependencies + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-pip- + + # Install prereqs + # I don't think it's possible to use a single action for this because + # we can't use ${matrix.os} in an if statement, so we have to use two different actions. + - name: Install prerequisites (Ubuntu) + if: matrix.os == 'ubuntu-latest' + uses: ./.github/actions/install/ubuntu-latest + + - name: Install prerequisites (macOS) + if: matrix.os == 'macos-latest' + uses: ./.github/actions/install/macos-latest + + # Check that mpirun oversubscribing works, doesn't work unless OMPI_MCA_rmaps_base_oversubscribe==1 + - name: Test mpirun + run: | + echo $OMPI_MCA_rmaps_base_oversubscribe + echo $PRRTE_MCA_rmaps_base_oversubscribe + pip install mpi4py -U + which mpirun + mpirun --version + mpirun --oversubscribe --report-bindings -n 4 python -c "from mpi4py import MPI; comm=MPI.COMM_WORLD; print(f'Hello from rank {comm.Get_rank()} of {comm.Get_size()}'); assert comm.Get_size()==4" + + # Clone struphy-ci-testing + - name: Install struphy + uses: ./.github/actions/install/install-struphy + env: + FC: ${{ env.FC }} + CC: ${{ env.CC }} + CXX: ${{ env.CXX }} + + # Compile + - name: Compile kernels + uses: ./.github/actions/compile + + # Run tests + - name: Run unit tests + if: matrix.test-type == 'unit' + uses: ./.github/actions/tests/unit + + - name: Run model tests + if: matrix.test-type == 'model' + uses: ./.github/actions/tests/models + + #- name: Run quickstart tests + # if: matrix.test-type == 'quickstart' + # uses: ./.github/actions/tests/quickstart diff --git a/pyproject.toml b/pyproject.toml index cee825cb0..85dbbebf1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -138,14 +138,13 @@ ignore = [ [tool.isort] profile = "black" line_length = 120 -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -combine_as_imports = true +# multi_line_output = 3 +# include_trailing_comma = true +# force_grid_wrap = 0 +# combine_as_imports = true [tool.pylint] max-line-length = 120 -quote-style = "single" [tool.ruff] line-length = 120 diff --git a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/massless_kernels_control_variate.py b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/massless_kernels_control_variate.py index c56b67711..28ce7bbb2 100644 --- a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/massless_kernels_control_variate.py +++ b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/massless_kernels_control_variate.py @@ -27,6 +27,7 @@ def uvpre( bn3: "float[:,:,:,:]", ): from numpy import empty, exp, zeros + # -- removed omp: #$ omp parallel # -- removed omp: #$ omp do private (ie1, ie2, ie3, q1, q2, q3, il1, il2, il3, value) diff --git a/src/struphy/utils/set_release_dependencies.py b/utils/set_release_dependencies.py similarity index 100% rename from src/struphy/utils/set_release_dependencies.py rename to utils/set_release_dependencies.py index a08717060..96e29343a 100644 --- a/src/struphy/utils/set_release_dependencies.py +++ b/utils/set_release_dependencies.py @@ -1,8 +1,8 @@ import importlib.metadata import re +import tomllib import tomli_w -import tomllib def get_min_bound(entry): From df11ff29c906da457194d47cfba4d34f923c26f5 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 30 Oct 2025 16:59:36 +0100 Subject: [PATCH 02/83] Issue and PR template fixes (#82) Redo of https://github.com/struphy-hub/struphy/pull/46 and https://github.com/struphy-hub/struphy/pull/44 --- .../pull_request_template.md | 15 +++++++++++++++ README.md | 10 ++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE/pull_request_template.md diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 000000000..87f9ff3cc --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,15 @@ +**Solves the following issue(s):** + +Closes #... + +**Core changes:** + +None + +**Model-specific changes:** + +None + +**Documentation changes:** + +None \ No newline at end of file diff --git a/README.md b/README.md index 6a25a91a7..1730f5e00 100755 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ -# Struphy - Structure-preserving hybrid codes +![Struphy header](https://raw.githubusercontent.com/struphy-hub/.github/refs/heads/main/profile/struphy_header_with_subs.png) -A Python package for plasma physics PDEs. +# Welcome! + +Struphy is a Python package for plasma physics PDEs. Join the [Struphy mailing list](https://listserv.gwdg.de/mailman/listinfo/struphy) and stay informed on updates. ## Documentation -See the [Struphy pages](https://struphy.pages.mpcdf.de/struphy/index.html) for details regarding installation, tutorials, use and development. +See the [Struphy pages](https://struphy.pages.mpcdf.de/struphy/index.html) for details regarding installation, tutorials, use, and development. ## Quick install @@ -52,4 +54,4 @@ Struphy tutorials are available in the form of [Jupyter notebooks](https://gitla * Stefan Possanner [stefan.possanner@ipp.mpg.de](mailto:spossann@ipp.mpg.de) * Eric Sonnendrücker [eric.sonnendruecker@ipp.mpg.de](mailto:eric.sonnendruecker@ipp.mpg.de) -* Xin Wang [xin.wang@ipp.mpg.de](mailto:xin.wang@ipp.mpg.de) \ No newline at end of file +* Xin Wang [xin.wang@ipp.mpg.de](mailto:xin.wang@ipp.mpg.de) From fedb0a8b217c23d1a047f925aa527cdeb8263949 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 30 Oct 2025 17:00:10 +0100 Subject: [PATCH 03/83] Parameter files as py (#83) Redo of https://github.com/struphy-hub/struphy/pull/42 Original MR on Gitlab: https://gitlab.mpcdf.mpg.de/struphy/struphy/-/merge_requests/734 **Solves the following issue(s):** Closes [#318 ](https://gitlab.mpcdf.mpg.de/struphy/struphy/-/issues/318) **Outline:** **This MR is a big refactoring of the high-level interface of Struphy.** The lower level routines like kernels or Derham stuff are not touched. The goal is to address the disparity between the console interface and the Python API (as used in the notebook tutorials) of Struphy. In order to avoid confusion - as there has been for some users - we will shift towards the API and get rid of some console commands, such as `struphy run`. In this MR, we design a `.py` file as a sort of parameter file for the main point of interaction with the user. This file imports all necessary (mostly new) classes for setting up a model and a simulation. Through an IDE like VScode one can then inspect all possible options of a model by clicking or hovering. This is tied to the second point of this MR - replacing `dicts` with `Classes` wherever possible. This will lead to more Pythonic code and easier documentation and coding. Some of the models I have already ported to the new standard. To get familiar with the new ideas, the easiest way is to generate a default `.py` parameter file via ``` struphy params Vlasov ``` This will generate `params_Vlasov.py` in the current working dir. Try to inspect it in an advanced editor like VCcode in order to grasp the new concept. You can run the model via ``` python params_Vlasov.py ``` Parallel runs are started with ``` mpirun -n 2 python params_Vlasov.py ``` Line profiling can be turned on with ``` LINE_PROFILE=1 mpirun -n 2 python params_Vlasov.py ``` Moreover, three tutorials have been ported and moved from `doc/tutorials` to `tutorials/`. You can check these out to learn more about some of the new classes used in the parameter file. **TODOS:** **This MR is now open for improvements from anybody.** We shall discuss in the next meetings. **I have protected this branch - please checkout from this branch and merge back into it!** Models that have already been ported can be seen below. You can try these out as described above. Additionally, the following model tests can be performed: ``` python src/struphy/models/tests/test_Maxwell.py python src/struphy/models/tests/test_LinearMHD.py ``` **Workflow:** * I suggest that everybody tries to port his/her model to the new framework, see the unresolved threads below. * Reminder: **I have protected this branch - please checkout from this branch and merge back into it!** * Once you have ported a model, please add it to the list of models to be tested, here: https://gitlab.mpcdf.mpg.de/struphy/struphy/-/blob/318-parameter-file-as-py/src/struphy/models/tests/test_models.py?ref_type=heads#L16 * Any help is welcome - thanks! Stefan --------- Co-authored-by: Stefan Possanner Co-authored-by: Byung Kyu Na --- .../install-struphy-editable/action.yml | 2 +- .../install/install-struphy/action.yml | 11 +- .github/actions/tests/models/action.yml | 21 +- .github/actions/tests/quickstart/action.yml | 2 +- .github/actions/tests/tutorials/action.yml | 12 + .github/actions/tests/unit/action.yml | 12 + .github/workflows/static_analysis.yml | 2 +- .github/workflows/testing.yml | 12 +- .gitignore | 2 + .gitlab-ci.yml | 85 +- .pre-commit-config.yaml | 3 +- doc/conf.py | 22 +- doc/sections/tutorials.rst | 16 +- pyproject.toml | 16 + src/struphy/bsplines/bsplines.py | 58 +- .../bsplines/tests/test_bsplines_kernels.py | 51 +- .../bsplines/tests/test_eval_spline_mpi.py | 159 +- src/struphy/compile_struphy.mk | 2 +- src/struphy/conftest.py | 16 +- src/struphy/console/main.py | 81 +- src/struphy/console/params.py | 31 +- src/struphy/console/profile.py | 10 +- src/struphy/console/test.py | 117 +- src/struphy/console/tests/test_console.py | 9 +- src/struphy/diagnostics/console_diagn.py | 6 +- src/struphy/diagnostics/continuous_spectra.py | 34 +- src/struphy/diagnostics/diagn_tools.py | 234 +- .../diagnostics/paraview/mesh_creator.py | 67 +- src/struphy/dispersion_relations/analytic.py | 86 +- src/struphy/dispersion_relations/base.py | 11 +- src/struphy/dispersion_relations/utilities.py | 5 +- src/struphy/eigenvalue_solvers/derivatives.py | 9 +- .../legacy/MHD_eigenvalues_cylinder_1D.py | 372 +- .../control_variates/control_variate.py | 42 +- .../fB_massless_control_variate.py | 4 +- .../fnB_massless_control_variate.py | 7 +- .../massless_control_variate.py | 7 +- .../legacy/emw_operators.py | 8 +- .../legacy/inner_products_1d.py | 11 +- .../legacy/inner_products_2d.py | 38 +- .../legacy/inner_products_3d.py | 38 +- .../eigenvalue_solvers/legacy/l2_error_1d.py | 11 +- .../eigenvalue_solvers/legacy/l2_error_2d.py | 50 +- .../eigenvalue_solvers/legacy/l2_error_3d.py | 50 +- .../legacy/mass_matrices_3d_pre.py | 74 +- .../legacy/massless_operators/fB_arrays.py | 206 +- .../fB_massless_linear_operators.py | 102 +- .../legacy/massless_operators/fB_vv_kernel.py | 2 +- .../legacy/mhd_operators_MF.py | 226 +- .../pro_local/mhd_operators_3d_local.py | 500 +- .../pro_local/projectors_local.py | 632 +-- .../shape_function_projectors_L2.py | 122 +- .../shape_function_projectors_local.py | 220 +- .../eigenvalue_solvers/mass_matrices_1d.py | 26 +- .../eigenvalue_solvers/mass_matrices_2d.py | 92 +- .../eigenvalue_solvers/mass_matrices_3d.py | 92 +- .../mhd_axisymmetric_main.py | 8 +- .../mhd_axisymmetric_pproc.py | 11 +- .../eigenvalue_solvers/mhd_operators.py | 40 +- .../eigenvalue_solvers/mhd_operators_core.py | 374 +- .../eigenvalue_solvers/projectors_global.py | 270 +- .../eigenvalue_solvers/spline_space.py | 343 +- src/struphy/examples/_draw_parallel.py | 10 +- .../examples/restelli2018/callables.py | 66 +- src/struphy/feec/basis_projection_ops.py | 42 +- src/struphy/feec/linear_operators.py | 36 +- src/struphy/feec/mass.py | 154 +- src/struphy/feec/preconditioner.py | 42 +- src/struphy/feec/projectors.py | 114 +- src/struphy/feec/psydac_derham.py | 637 ++- src/struphy/feec/tests/test_basis_ops.py | 68 +- src/struphy/feec/tests/test_derham.py | 18 +- src/struphy/feec/tests/test_eval_field.py | 227 +- src/struphy/feec/tests/test_field_init.py | 338 +- src/struphy/feec/tests/test_l2_projectors.py | 46 +- .../feec/tests/test_local_projectors.py | 312 +- .../feec/tests/test_lowdim_nel_is_1.py | 54 +- src/struphy/feec/tests/test_mass_matrices.py | 123 +- .../feec/tests/test_toarray_struphy.py | 28 +- .../feec/tests/test_tosparse_struphy.py | 16 +- src/struphy/feec/tests/xx_test_preconds.py | 14 +- src/struphy/feec/utilities.py | 37 +- .../feec/utilities_local_projectors.py | 105 +- src/struphy/feec/variational_utilities.py | 118 +- src/struphy/fields_background/base.py | 116 +- .../fields_background/coil_fields/base.py | 7 +- .../coil_fields/coil_fields.py | 7 +- src/struphy/fields_background/equils.py | 226 +- src/struphy/fields_background/generic.py | 8 + .../fields_background/projected_equils.py | 7 +- .../tests/test_desc_equil.py | 69 +- .../tests/test_generic_equils.py | 28 +- .../tests/test_mhd_equils.py | 82 +- .../tests/test_numerical_mhd_equil.py | 72 +- src/struphy/geometry/base.py | 226 +- src/struphy/geometry/domains.py | 55 +- src/struphy/geometry/evaluation_kernels.py | 24 +- src/struphy/geometry/tests/test_domain.py | 123 +- src/struphy/geometry/utilities.py | 94 +- src/struphy/geometry/utilities_kernels.py | 8 +- src/struphy/initial/base.py | 47 + src/struphy/initial/eigenfunctions.py | 38 +- src/struphy/initial/perturbations.py | 933 ++-- .../initial/tests/test_init_perturbations.py | 168 +- src/struphy/initial/utilities.py | 4 +- src/struphy/io/inp/params_Maxwell.py | 63 + src/struphy/io/inp/params_Maxwell_lw.py | 66 + .../io/inp/verification/Maxwell_coaxial.py | 60 + src/struphy/io/options.py | 363 ++ src/struphy/io/output_handling.py | 7 +- src/struphy/io/setup.py | 481 +- src/struphy/kinetic_background/base.py | 413 +- src/struphy/kinetic_background/maxwellians.py | 386 +- .../kinetic_background/moment_functions.py | 6 +- .../kinetic_background/tests/test_base.py | 42 +- .../tests/test_maxwellians.py | 666 ++- src/struphy/linear_algebra/linalg_kron.py | 9 +- src/struphy/linear_algebra/saddle_point.py | 104 +- src/struphy/linear_algebra/schur_solver.py | 22 +- src/struphy/linear_algebra/solver.py | 37 + .../tests/test_saddle_point_propagator.py | 133 +- .../tests/test_saddlepoint_massmatrices.py | 61 +- .../tests/test_stencil_dot_kernels.py | 36 +- .../tests/test_stencil_transpose_kernels.py | 30 +- src/struphy/main.py | 808 ++- src/struphy/models/__init__.py | 148 +- src/struphy/models/base.py | 2400 ++++---- src/struphy/models/fluid.py | 3179 ++++++----- src/struphy/models/hybrid.py | 1457 +++-- src/struphy/models/kinetic.py | 1262 ++--- src/struphy/models/species.py | 211 + src/struphy/models/tests/test_fluid_models.py | 28 - .../models/tests/test_hybrid_models.py | 28 - .../models/tests/test_kinetic_models.py | 28 - src/struphy/models/tests/test_models.py | 176 + src/struphy/models/tests/test_toy_models.py | 28 - .../models/tests/test_verif_EulerSPH.py | 166 + .../models/tests/test_verif_LinearMHD.py | 154 + .../models/tests/test_verif_Maxwell.py | 269 + .../models/tests/test_verif_Poisson.py | 149 + .../test_verif_VlasovAmpereOneSpecies.py | 167 + src/struphy/models/tests/verification.py | 82 +- src/struphy/models/toy.py | 1366 +++-- src/struphy/models/variables.py | 415 ++ src/struphy/ode/solvers.py | 12 +- src/struphy/ode/tests/test_ode_feec.py | 28 +- src/struphy/ode/utils.py | 55 +- src/struphy/physics/__init__.py | 0 src/struphy/physics/physics.py | 11 + src/struphy/pic/accumulation/accum_kernels.py | 79 +- .../pic/accumulation/accum_kernels_gc.py | 744 ++- src/struphy/pic/accumulation/filter.py | 185 + .../pic/accumulation/filter_kernels.py | 22 +- .../pic/accumulation/particles_to_grid.py | 147 +- src/struphy/pic/base.py | 977 ++-- src/struphy/pic/particles.py | 216 +- src/struphy/pic/pushing/pusher.py | 25 +- src/struphy/pic/pushing/pusher_kernels.py | 529 +- src/struphy/pic/pushing/pusher_kernels_gc.py | 493 +- src/struphy/pic/sampling_kernels.py | 6 +- src/struphy/pic/sobol_seq.py | 55 +- src/struphy/pic/tests/test_accum_vec_H1.py | 38 +- src/struphy/pic/tests/test_accumulation.py | 45 +- src/struphy/pic/tests/test_binning.py | 489 +- src/struphy/pic/tests/test_draw_parallel.py | 26 +- src/struphy/pic/tests/test_mat_vec_filler.py | 71 +- .../test_pic_legacy_files/accumulation.py | 36 +- .../pic/tests/test_pic_legacy_files/pusher.py | 3 +- .../spline_evaluation_2d.py | 2 +- .../spline_evaluation_3d.py | 2 +- src/struphy/pic/tests/test_pushers.py | 118 +- src/struphy/pic/tests/test_sorting.py | 53 +- src/struphy/pic/tests/test_sph.py | 421 +- src/struphy/pic/tests/test_tesselation.py | 77 +- src/struphy/pic/utilities.py | 244 +- src/struphy/pic/utilities_kernels.py | 285 +- src/struphy/polar/basic.py | 15 +- src/struphy/polar/extraction_operators.py | 238 +- src/struphy/polar/linear_operators.py | 10 +- .../polar/tests/test_legacy_polar_splines.py | 20 +- src/struphy/polar/tests/test_polar.py | 44 +- .../likwid/plot_likwidproject.py | 8 +- .../likwid/plot_time_traces.py | 14 +- .../likwid/roofline_plotter.py | 13 +- .../post_processing/orbits/orbits_tools.py | 49 +- .../post_processing/post_processing_tools.py | 652 ++- src/struphy/post_processing/pproc_struphy.py | 103 +- .../post_processing/profile_struphy.py | 8 +- src/struphy/profiling/profiling.py | 23 +- src/struphy/propagators/__init__.py | 189 +- src/struphy/propagators/base.py | 215 +- .../propagators/propagators_coupling.py | 2227 ++++---- src/struphy/propagators/propagators_fields.py | 4847 +++++++++-------- .../propagators/propagators_markers.py | 1195 ++-- .../tests/test_gyrokinetic_poisson.py | 330 +- src/struphy/propagators/tests/test_poisson.py | 443 +- src/struphy/topology/__init__.py | 0 src/struphy/topology/grids.py | 21 + src/struphy/tutorials/tests/test_tutorials.py | 172 - src/struphy/utils/arrays.py | 59 - src/struphy/utils/clone_config.py | 15 +- src/struphy/utils/cupy_vs_numpy.py | 2 +- src/struphy/utils/mpi.py | 102 - src/struphy/utils/utils.py | 21 +- ...ipynb => tutorial_07_data_structures.ipynb | 321 +- tutorials/tutorial_01_parameter_files.ipynb | 415 ++ tutorials/tutorial_02_test_particles.ipynb | 1308 +++++ ...l_03_smoothed_particle_hydrodynamics.ipynb | 983 ++++ tutorials/tutorial_04_vlasov_maxwell.ipynb | 370 ++ .../tutorial_05_mapped_domains.ipynb | 9 +- .../tutorial_06_mhd_equilibria.ipynb | 2 +- .../tutorial_01_kinetic_particles.ipynb | 0 .../tutorial_01_parameter_files.ipynb | 378 ++ tutorials_old/tutorial_01_particles.ipynb | 273 + .../tutorial_02_fluid_particles.ipynb | 0 .../tutorial_03_discrete_derham.ipynb | 0 .../tutorial_06_poisson.ipynb | 0 .../tutorial_07_heat_equation.ipynb | 0 .../tutorial_08_maxwell.ipynb | 0 .../tutorial_09_vlasov_maxwell.ipynb | 0 .../tutorial_10_linear_mhd.ipynb | 0 .../tutorial_12_struphy_data_pproc.ipynb | 0 222 files changed, 26583 insertions(+), 19640 deletions(-) create mode 100644 .github/actions/tests/tutorials/action.yml create mode 100644 src/struphy/initial/base.py create mode 100644 src/struphy/io/inp/params_Maxwell.py create mode 100644 src/struphy/io/inp/params_Maxwell_lw.py create mode 100644 src/struphy/io/inp/verification/Maxwell_coaxial.py create mode 100644 src/struphy/io/options.py create mode 100644 src/struphy/linear_algebra/solver.py create mode 100644 src/struphy/models/species.py delete mode 100644 src/struphy/models/tests/test_fluid_models.py delete mode 100644 src/struphy/models/tests/test_hybrid_models.py delete mode 100644 src/struphy/models/tests/test_kinetic_models.py create mode 100644 src/struphy/models/tests/test_models.py delete mode 100644 src/struphy/models/tests/test_toy_models.py create mode 100644 src/struphy/models/tests/test_verif_EulerSPH.py create mode 100644 src/struphy/models/tests/test_verif_LinearMHD.py create mode 100644 src/struphy/models/tests/test_verif_Maxwell.py create mode 100644 src/struphy/models/tests/test_verif_Poisson.py create mode 100644 src/struphy/models/tests/test_verif_VlasovAmpereOneSpecies.py create mode 100644 src/struphy/models/variables.py create mode 100644 src/struphy/physics/__init__.py create mode 100644 src/struphy/physics/physics.py create mode 100644 src/struphy/pic/accumulation/filter.py create mode 100644 src/struphy/topology/__init__.py create mode 100644 src/struphy/topology/grids.py delete mode 100644 src/struphy/tutorials/tests/test_tutorials.py delete mode 100644 src/struphy/utils/arrays.py delete mode 100644 src/struphy/utils/mpi.py rename doc/tutorials/tutorial_11_data_structures.ipynb => tutorial_07_data_structures.ipynb (76%) create mode 100644 tutorials/tutorial_01_parameter_files.ipynb create mode 100644 tutorials/tutorial_02_test_particles.ipynb create mode 100644 tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb create mode 100644 tutorials/tutorial_04_vlasov_maxwell.ipynb rename doc/tutorials/tutorial_04_mapped_domains.ipynb => tutorials/tutorial_05_mapped_domains.ipynb (98%) rename doc/tutorials/tutorial_05_mhd_equilibria.ipynb => tutorials/tutorial_06_mhd_equilibria.ipynb (99%) rename {doc/tutorials => tutorials_old}/tutorial_01_kinetic_particles.ipynb (100%) create mode 100644 tutorials_old/tutorial_01_parameter_files.ipynb create mode 100644 tutorials_old/tutorial_01_particles.ipynb rename {doc/tutorials => tutorials_old}/tutorial_02_fluid_particles.ipynb (100%) rename {doc/tutorials => tutorials_old}/tutorial_03_discrete_derham.ipynb (100%) rename {doc/tutorials => tutorials_old}/tutorial_06_poisson.ipynb (100%) rename {doc/tutorials => tutorials_old}/tutorial_07_heat_equation.ipynb (100%) rename {doc/tutorials => tutorials_old}/tutorial_08_maxwell.ipynb (100%) rename {doc/tutorials => tutorials_old}/tutorial_09_vlasov_maxwell.ipynb (100%) rename {doc/tutorials => tutorials_old}/tutorial_10_linear_mhd.ipynb (100%) rename {doc/tutorials => tutorials_old}/tutorial_12_struphy_data_pproc.ipynb (100%) diff --git a/.github/actions/install/install-struphy-editable/action.yml b/.github/actions/install/install-struphy-editable/action.yml index 49d871e15..f395d5caa 100644 --- a/.github/actions/install/install-struphy-editable/action.yml +++ b/.github/actions/install/install-struphy-editable/action.yml @@ -9,6 +9,6 @@ runs: run: | pip install --upgrade pip pip uninstall -y gvec - pip install -e ".[dev,mpi]" + pip install -e ".[dev,mpi,doc]" pip list struphy -h diff --git a/.github/actions/install/install-struphy/action.yml b/.github/actions/install/install-struphy/action.yml index bce677589..a9589c4c4 100644 --- a/.github/actions/install/install-struphy/action.yml +++ b/.github/actions/install/install-struphy/action.yml @@ -7,15 +7,8 @@ runs: - name: Install struphy shell: bash run: | - echo $FC - echo $CC - echo $CXX - echo "----------------" - which gfortran - which gcc - which g++ pip install --upgrade pip - pip uninstall -y gvec - pip install ".[phys,mpi]" + pip install ".[phys,mpi,doc]" pip list struphy -h + struphy --refresh-models diff --git a/.github/actions/tests/models/action.yml b/.github/actions/tests/models/action.yml index 0a9fafc16..435276caa 100644 --- a/.github/actions/tests/models/action.yml +++ b/.github/actions/tests/models/action.yml @@ -3,14 +3,21 @@ name: "Run model tests" runs: using: composite steps: - - name: Install dependencies + - name: Model tests shell: bash run: | struphy compile --status + struphy test LinearMHD + struphy test toy + struphy test models + struphy test verification + - name: Model tests with MPI + shell: bash + run: | struphy compile --status - struphy test models --fast - struphy test models --fast --mpi 2 - struphy test models --fast --verification --mpi 1 - struphy test models --fast --verification --mpi 4 - struphy test models --fast --verification --mpi 4 --nclones 2 - struphy test DriftKineticElectrostaticAdiabatic --mpi 2 --nclones 2 \ No newline at end of file + struphy test models + struphy test models --mpi 2 + struphy test verification --mpi 1 + struphy test verification --mpi 4 + struphy test verification --mpi 4 --nclones 2 + struphy test VlasovAmpereOneSpecies --mpi 2 --nclones 2 diff --git a/.github/actions/tests/quickstart/action.yml b/.github/actions/tests/quickstart/action.yml index b78ade5e7..3845360d3 100644 --- a/.github/actions/tests/quickstart/action.yml +++ b/.github/actions/tests/quickstart/action.yml @@ -8,7 +8,7 @@ runs: run: | struphy -p struphy -h - struphy params VlasovAmpereOneSpecies -y + struphy params VlasovAmpereOneSpecies ls -1a mv params_VlasovAmpereOneSpecies.py test.py python3 test.py diff --git a/.github/actions/tests/tutorials/action.yml b/.github/actions/tests/tutorials/action.yml new file mode 100644 index 000000000..17d41603f --- /dev/null +++ b/.github/actions/tests/tutorials/action.yml @@ -0,0 +1,12 @@ +name: "Run tutorial tests" + +runs: + using: composite + steps: + - name: Run turorial tests + shell: bash + run: | + pwd + ls -1a + which python + jupyter nbconvert --to notebook --execute tutorials/*.ipynb diff --git a/.github/actions/tests/unit/action.yml b/.github/actions/tests/unit/action.yml index 2d0e70a2d..385d6f419 100644 --- a/.github/actions/tests/unit/action.yml +++ b/.github/actions/tests/unit/action.yml @@ -3,7 +3,19 @@ name: "Run unit tests" runs: using: composite steps: + - name: Run unit tests with MPI + shell: bash + run: | + struphy compile --status + struphy --refresh-models + struphy test unit --mpi 2 + - name: Run unit tests shell: bash run: | + struphy compile --status + struphy --refresh-models + pip show mpi4py + pip uninstall -y mpi4py + pip list struphy test unit diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 5dc140173..7fff5f1c3 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -86,7 +86,7 @@ jobs: run: | pip install isort isort --check src/ - isort --check doc/tutorials/ + isort --check tutorials/ # mypy: # runs-on: ubuntu-latest diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 3404f6348..654fb0fc2 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -22,7 +22,7 @@ jobs: python-version: ["3.12"] os: ["ubuntu-latest"] #, "macos-latest"] compile-language: ["fortran", "c"] - test-type: ["unit", "model"] #, "quickstart"] + test-type: ["unit", "model", "quickstart", "tutorials"] steps: # Checkout the repository @@ -90,6 +90,10 @@ jobs: if: matrix.test-type == 'model' uses: ./.github/actions/tests/models - #- name: Run quickstart tests - # if: matrix.test-type == 'quickstart' - # uses: ./.github/actions/tests/quickstart + - name: Run quickstart tests + if: matrix.test-type == 'quickstart' + uses: ./.github/actions/tests/quickstart + + - name: Run tutorials + if: matrix.test-type == 'tutorials' + uses: ./.github/actions/tests/tutorials diff --git a/.gitignore b/.gitignore index f529a8f8a..e01ccf71d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ share/python-wheels/ *.egg MANIFEST doc/_build/ +doc/source/* +tutorials/sim* *STUBDIR* .VSCodeCounter/ .pymon diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 96c2a4f8f..9df6d696b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -97,6 +97,7 @@ stages: rules: - !reference [.rules_common, skip_pages_and_scheduled] - if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "devel" && $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "318-parameter-file-as-py" && $CI_PIPELINE_SOURCE == "merge_request_event" .rules_mr_to_master: rules: @@ -398,6 +399,7 @@ stages: inspect_struphy: - struphy -p - struphy -h + - struphy --refresh-models - struphy run -h unit_tests: - struphy compile --status @@ -409,36 +411,41 @@ stages: - struphy test unit --mpi 2 model_tests: - struphy compile --status - - struphy test models --fast - - struphy test models --fast --verification + - struphy test LinearMHD + - struphy test toy + - struphy test models + - struphy test verification model_tests_mpi: - struphy compile --status - - struphy test models --fast - - struphy test models --fast --mpi 2 - - struphy test models --fast --verification --mpi 1 - - struphy test models --fast --verification --mpi 4 - - struphy test models --fast --verification --mpi 4 --nclones 2 - - struphy test DriftKineticElectrostaticAdiabatic --mpi 2 --nclones 2 + - struphy test models + - struphy test models --mpi 2 + - struphy test verification --mpi 1 + - struphy test verification --mpi 4 + - struphy test verification --mpi 4 --nclones 2 + - struphy test VlasovAmpereOneSpecies --mpi 2 --nclones 2 quickstart_tests: - - struphy --set-i . - - struphy --set-o . - struphy -p - struphy -h - - struphy run -h - - struphy params VlasovMaxwellOneSpecies -y + - struphy params VlasovAmpereOneSpecies + - ls -1a + - mv params_VlasovAmpereOneSpecies.py test.py + - python3 test.py + - ls sim_1/ + - mpirun -n 2 python3 test.py + - LINE_PROFILE=1 mpirun -n 2 python3 test.py + # - struphy pproc my_first_sim + # - ls my_first_sim/post_processing/fields_data/ + # - ls my_first_sim/post_processing/kinetic_data/ + # - struphy params VlasovMaxwellOneSpecies --options + # - struphy run -i test.yml -o my_first_sim_1clone --mpi 4 --nclones 1 + # - struphy run -i test.yml -o my_first_sim_2clone --mpi 4 --nclones 2 + # - struphy run -i test.yml -o my_first_sim_4clone --mpi 4 --nclones 4 + # - struphy pproc my_first_sim_1clone my_first_sim_2clone my_first_sim_4clone + tutorial_tests: + - pwd - ls -1a - - mv params_VlasovMaxwellOneSpecies.yml test.yml - - struphy params VlasovMaxwellOneSpecies --check-file test.yml - - struphy run -i test.yml -o my_first_sim - - ls my_first_sim/ - - struphy pproc my_first_sim - - ls my_first_sim/post_processing/fields_data/ - - ls my_first_sim/post_processing/kinetic_data/ - - struphy params VlasovMaxwellOneSpecies --options - - struphy run -i test.yml -o my_first_sim_1clone --mpi 4 --nclones 1 - - struphy run -i test.yml -o my_first_sim_2clone --mpi 4 --nclones 2 - - struphy run -i test.yml -o my_first_sim_4clone --mpi 4 --nclones 4 - - struphy pproc my_first_sim_1clone my_first_sim_2clone my_first_sim_4clone + - which python + - jupyter nbconvert --to notebook --execute tutorials/*.ipynb build_images: - buildah images - buildah build -t gitlab-registry.mpcdf.mpg.de/struphy/struphy/almalinux-latest -f docker/almalinux-latest.dockerfile . @@ -543,12 +550,11 @@ test_cupy: # Install cupy - python3 -m pip install --user cupy-cuda12x - python3 -c "import cupy as cp" + - python3 -m pip install cunumpy # Test numpy backend - - python3 src/struphy/utils/arrays.py - python3 src/struphy/utils/cupy_vs_numpy.py # Test cupy backend - export ARRAY_BACKEND=cupy - - python3 src/struphy/utils/arrays.py - python3 src/struphy/utils/cupy_vs_numpy.py install_tests: @@ -634,10 +640,21 @@ quickstart_tests: - !reference [.scripts, install_on_push] - !reference [.scripts, quickstart_tests] +# tutorial_tests: +# stage: test +# extends: +# - .rules_mr_to_devel +# - .image_gitlab_mpcdf_struphy +# - .before_script_load_modules +# - .variables_push +# script: +# - !reference [.scripts, install_on_push] +# - !reference [.scripts, tutorial_tests] + pages_tests: stage: test extends: - - .rules_mr_to_devel + - .rules_scheduled - .image_gitlab_mpcdf_struphy - .before_script_load_modules - .variables_push @@ -1027,6 +1044,20 @@ lint_repo: # Lint all files in current branch, FAIL the CI pipeline if any files are incorrectly formatted - struphy lint all --output-format plain --verbose +lint_repo_temporary: + stage: lint + needs: [] + extends: + - .rules_startup + - .image_ubuntu_latest + script: + - !reference [.scripts, inspect_directory] + - !reference [.scripts, create_venv] + - pip install -e .[dev] # We have to install struphy in editable mode for struphy lint to work + - !reference [.scripts, inspect_struphy] + # Lint all files in current branch, FAIL the CI pipeline if any files are incorrectly formatted + - struphy lint all --output-format plain --verbose + # Lint struphy with `struphy lint` lint_branch_report: stage: lint diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af670854e..3f3107d09 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,11 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 # Use the latest stable version + rev: v6.0.0 # Use the latest stable version hooks: - id: check-added-large-files # Prevent giant files from being committed. args: ["--maxkb=1000"] - id: check-merge-conflict # Check for files that contain merge conflict strings. + args: ["--assume-in-merge"] - id: check-toml # Attempts to load all TOML files to verify syntax. - id: check-yaml # Attempts to load all yaml files to verify syntax. args: ["--unsafe"] diff --git a/doc/conf.py b/doc/conf.py index 9f98eba4e..d5e8a3265 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -10,10 +10,28 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os +import os +import shutil + # import sys # sys.path.insert(0, os.path.abspath('.')) + +def copy_tutorials(app): + src = os.path.abspath("../tutorials") + dst = os.path.abspath("source/tutorials") + + # Remove existing target directory if it exists + if os.path.exists(dst): + shutil.rmtree(dst) + + shutil.copytree(src, dst) + + +def setup(app): + app.connect("builder-inited", copy_tutorials) + + with open("../src/struphy/console/main.py") as f: exec(f.read()) @@ -46,7 +64,7 @@ "sphinx_design", ] -nbsphinx_execute = 'auto' +nbsphinx_execute = "auto" napoleon_use_admonition_for_examples = True napoleon_use_admonition_for_notes = True diff --git a/doc/sections/tutorials.rst b/doc/sections/tutorials.rst index 9c1ebc8ae..d0052ad2a 100644 --- a/doc/sections/tutorials.rst +++ b/doc/sections/tutorials.rst @@ -3,7 +3,7 @@ Tutorials ========= -All notebooks are available at https://gitlab.mpcdf.mpg.de/struphy/struphy/-/blob/devel/doc/tutorials/. +All notebooks are available at https://gitlab.mpcdf.mpg.de/struphy/struphy/-/blob/devel/tutorials/. The objects used in these notebooks are the same as in the available :ref:`models`. They can thus be used for MPI parallel runs in HPC applications. @@ -11,16 +11,6 @@ They can thus be used for MPI parallel runs in HPC applications. .. toctree:: :maxdepth: 1 :caption: Notebook tutorials: + :glob: - ../tutorials/tutorial_01_kinetic_particles - ../tutorials/tutorial_02_fluid_particles - ../tutorials/tutorial_03_discrete_derham - ../tutorials/tutorial_04_mapped_domains - ../tutorials/tutorial_05_mhd_equilibria - ../tutorials/tutorial_06_poisson - ../tutorials/tutorial_07_heat_equation - ../tutorials/tutorial_08_maxwell - ../tutorials/tutorial_09_vlasov_maxwell - ../tutorials/tutorial_10_linear_mhd - ../tutorials/tutorial_11_data_structures - ../tutorials/tutorial_12_struphy_data_pproc + ../source/tutorials/* diff --git a/pyproject.toml b/pyproject.toml index 85dbbebf1..c4a2c9d27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ classifiers = [ ] dependencies = [ 'numpy', + 'cunumpy', 'pyccel>=2.0', 'psydac @ git+https://github.com/max-models/psydac-for-struphy.git@devel-tiny', 'scipy', @@ -34,6 +35,7 @@ dependencies = [ 'argcomplete', 'pytest', 'pytest-mpi', + 'line_profiler', ] [project.license] @@ -45,6 +47,7 @@ phys = [ 'desc-opt', ] dev = [ + "struphy[mpi]", "notebook", "autopep8", "isort", @@ -66,6 +69,9 @@ mpi = [ ] doc = [ "struphy[phys]", + "jupyter", + "nbconvert", + "ipykernel", "sphinx", "sphinx-design", "lxml_html_clean", @@ -166,3 +172,13 @@ ignore = [ "D211", "D213", ] + +[tool.pytest.ini_options] +markers = [ + "models", + "toy", + "fluid", + "kinetic", + "hybrid", + "single", +] diff --git a/src/struphy/bsplines/bsplines.py b/src/struphy/bsplines/bsplines.py index a659e4c61..a04ee4851 100644 --- a/src/struphy/bsplines/bsplines.py +++ b/src/struphy/bsplines/bsplines.py @@ -16,7 +16,7 @@ """ -from struphy.utils.arrays import xp as np +import cunumpy as xp __all__ = [ "find_span", @@ -105,7 +105,7 @@ def scaling_vector(knots, degree, span): Scaling vector with elements (p + 1)/(t[i + p + 1] - t[i]) """ - x = np.zeros(degree + 1, dtype=float) + x = xp.zeros(degree + 1, dtype=float) for il in range(degree + 1): i = span - il @@ -148,9 +148,9 @@ def basis_funs(knots, degree, x, span, normalize=False): by using 'left' and 'right' temporary arrays that are one element shorter. """ - left = np.empty(degree, dtype=float) - right = np.empty(degree, dtype=float) - values = np.empty(degree + 1, dtype=float) + left = xp.empty(degree, dtype=float) + right = xp.empty(degree, dtype=float) + values = xp.empty(degree + 1, dtype=float) values[0] = 1.0 @@ -205,7 +205,7 @@ def basis_funs_1st_der(knots, degree, x, span): # Compute derivatives at x using formula based on difference of splines of degree deg - 1 # ------- # j = 0 - ders = np.empty(degree + 1, dtype=float) + ders = xp.empty(degree + 1, dtype=float) saved = degree * values[0] / (knots[span + 1] - knots[span + 1 - degree]) ders[0] = -saved @@ -261,11 +261,11 @@ def basis_funs_all_ders(knots, degree, x, span, n): - innermost loops are replaced with vector operations on slices. """ - left = np.empty(degree) - right = np.empty(degree) - ndu = np.empty((degree + 1, degree + 1)) - a = np.empty((2, degree + 1)) - ders = np.zeros((n + 1, degree + 1)) # output array + left = xp.empty(degree) + right = xp.empty(degree) + ndu = xp.empty((degree + 1, degree + 1)) + a = xp.empty((2, degree + 1)) + ders = xp.zeros((n + 1, degree + 1)) # output array # Number of derivatives that need to be effectively computed # Derivatives higher than degree are = 0. @@ -304,7 +304,7 @@ def basis_funs_all_ders(knots, degree, x, span, n): j1 = 1 if (rk > -1) else -rk j2 = k - 1 if (r - 1 <= pk) else degree - r a[s2, j1 : j2 + 1] = (a[s1, j1 : j2 + 1] - a[s1, j1 - 1 : j2]) * ndu[pk + 1, rk + j1 : rk + j2 + 1] - d += np.dot(a[s2, j1 : j2 + 1], ndu[rk + j1 : rk + j2 + 1, pk]) + d += xp.dot(a[s2, j1 : j2 + 1], ndu[rk + j1 : rk + j2 + 1, pk]) if r <= pk: a[s2, k] = -a[s1, k - 1] * ndu[pk + 1, r] d += a[s2, k] * ndu[r, pk] @@ -362,7 +362,7 @@ def collocation_matrix(knots, degree, xgrid, periodic, normalize=False): nx = len(xgrid) # Collocation matrix as 2D Numpy array (dense storage) - mat = np.zeros((nx, nb), dtype=float) + mat = xp.zeros((nx, nb), dtype=float) # Indexing of basis functions (periodic or not) for a given span if periodic: @@ -418,12 +418,12 @@ def histopolation_matrix(knots, degree, xgrid, periodic): # Number of integrals if periodic: el_b = breakpoints(knots, degree) - xgrid = np.array([el_b[0]] + list(xgrid) + [el_b[-1]]) + xgrid = xp.array([el_b[0]] + list(xgrid) + [el_b[-1]]) ni = len(xgrid) - 1 # Histopolation matrix of M-splines as 2D Numpy array (dense storage) - his = np.zeros((ni, nbD), dtype=float) + his = xp.zeros((ni, nbD), dtype=float) # Collocation matrix of B-splines col = collocation_matrix(knots, degree, xgrid, False, normalize=False) @@ -434,7 +434,7 @@ def histopolation_matrix(knots, degree, xgrid, periodic): for k in range(j + 1): his[i, j % nbD] += col[i, k] - col[i + 1, k] - if np.abs(his[i, j % nbD]) < 1e-14: + if xp.abs(his[i, j % nbD]) < 1e-14: his[i, j % nbD] = 0.0 # add first to last integration interval in case of periodic splines @@ -470,7 +470,7 @@ def breakpoints(knots, degree): else: endsl = -degree - return np.unique(knots[slice(degree, endsl)]) + return xp.unique(knots[slice(degree, endsl)]) # ============================================================================== @@ -501,13 +501,13 @@ def greville(knots, degree, periodic): n = len(T) - 2 * p - 1 if periodic else len(T) - p - 1 # Compute greville abscissas as average of p consecutive knot values - xg = np.around([sum(T[i : i + p]) / p for i in range(s, s + n)], decimals=15) + xg = xp.around([sum(T[i : i + p]) / p for i in range(s, s + n)], decimals=15) # If needed apply periodic boundary conditions if periodic: a = T[p] b = T[-p] - xg = np.around((xg - a) % (b - a) + a, decimals=15) + xg = xp.around((xg - a) % (b - a) + a, decimals=15) return xg @@ -537,7 +537,7 @@ def elements_spans(knots, degree): >>> from psydac.core.bsplines import make_knots, elements_spans >>> p = 3 ; n = 8 - >>> grid = np.arange( n-p+1 ) + >>> grid = xp.arange( n-p+1 ) >>> knots = make_knots( breaks=grid, degree=p, periodic=False ) >>> spans = elements_spans( knots=knots, degree=p ) >>> spans @@ -549,13 +549,13 @@ def elements_spans(knots, degree): 2) This function could be written in two lines: breaks = breakpoints( knots, degree ) - spans = np.searchsorted( knots, breaks[:-1], side='right' ) - 1 + spans = xp.searchsorted( knots, breaks[:-1], side='right' ) - 1 """ breaks = breakpoints(knots, degree) nk = len(knots) ne = len(breaks) - 1 - spans = np.zeros(ne, dtype=int) + spans = xp.zeros(ne, dtype=int) ie = 0 for ik in range(degree, nk - degree): @@ -600,13 +600,13 @@ def make_knots(breaks, degree, periodic): # Consistency checks assert len(breaks) > 1 - assert all(np.diff(breaks) > 0) + assert all(xp.diff(breaks) > 0) assert degree > 0 if periodic: assert len(breaks) > degree p = degree - T = np.zeros(len(breaks) + 2 * p, dtype=float) + T = xp.zeros(len(breaks) + 2 * p, dtype=float) T[p:-p] = breaks if periodic: @@ -671,13 +671,13 @@ def quadrature_grid(breaks, quad_rule_x, quad_rule_w): assert min(quad_rule_x) >= -1 assert max(quad_rule_x) <= +1 - quad_rule_x = np.asarray(quad_rule_x) - quad_rule_w = np.asarray(quad_rule_w) + quad_rule_x = xp.asarray(quad_rule_x) + quad_rule_w = xp.asarray(quad_rule_w) ne = len(breaks) - 1 nq = len(quad_rule_x) - quad_x = np.zeros((ne, nq), dtype=float) - quad_w = np.zeros((ne, nq), dtype=float) + quad_x = xp.zeros((ne, nq), dtype=float) + quad_w = xp.zeros((ne, nq), dtype=float) # Compute location and weight of quadrature points from basic rule for ie, (a, b) in enumerate(zip(breaks[:-1], breaks[1:])): @@ -724,7 +724,7 @@ def basis_ders_on_quad_grid(knots, degree, quad_grid, nders, normalize=False): # TODO: check if it is safe to compute span only once for each element ne, nq = quad_grid.shape - basis = np.zeros((ne, degree + 1, nders + 1, nq), dtype=float) + basis = xp.zeros((ne, degree + 1, nders + 1, nq), dtype=float) # Loop over elements for ie in range(ne): diff --git a/src/struphy/bsplines/tests/test_bsplines_kernels.py b/src/struphy/bsplines/tests/test_bsplines_kernels.py index 1a16712c1..c1010dd08 100644 --- a/src/struphy/bsplines/tests/test_bsplines_kernels.py +++ b/src/struphy/bsplines/tests/test_bsplines_kernels.py @@ -1,10 +1,9 @@ import time +import cunumpy as xp import pytest from psydac.ddm.mpi import mpi as MPI -from struphy.utils.arrays import xp as np - @pytest.mark.parametrize("Nel", [[8, 9, 10]]) @pytest.mark.parametrize("p", [[1, 2, 1], [2, 1, 2], [3, 4, 3]]) @@ -34,9 +33,9 @@ def test_bsplines_span_and_basis(Nel, p, spl_kind): # Random points in domain of process n_pts = 100 dom = derham.domain_array[rank] - eta1s = np.random.rand(n_pts) * (dom[1] - dom[0]) + dom[0] - eta2s = np.random.rand(n_pts) * (dom[4] - dom[3]) + dom[3] - eta3s = np.random.rand(n_pts) * (dom[7] - dom[6]) + dom[6] + eta1s = xp.random.rand(n_pts) * (dom[1] - dom[0]) + dom[0] + eta2s = xp.random.rand(n_pts) * (dom[4] - dom[3]) + dom[3] + eta3s = xp.random.rand(n_pts) * (dom[7] - dom[6]) + dom[6] # struphy find_span t0 = time.time() @@ -60,18 +59,18 @@ def test_bsplines_span_and_basis(Nel, p, spl_kind): if rank == 0: print(f"psydac find_span_p : {t1 - t0}") - assert np.allclose(span1s, span1s_psy) - assert np.allclose(span2s, span2s_psy) - assert np.allclose(span3s, span3s_psy) + assert xp.allclose(span1s, span1s_psy) + assert xp.allclose(span2s, span2s_psy) + assert xp.allclose(span3s, span3s_psy) # allocate tmps - bn1 = np.empty(derham.p[0] + 1, dtype=float) - bn2 = np.empty(derham.p[1] + 1, dtype=float) - bn3 = np.empty(derham.p[2] + 1, dtype=float) + bn1 = xp.empty(derham.p[0] + 1, dtype=float) + bn2 = xp.empty(derham.p[1] + 1, dtype=float) + bn3 = xp.empty(derham.p[2] + 1, dtype=float) - bd1 = np.empty(derham.p[0], dtype=float) - bd2 = np.empty(derham.p[1], dtype=float) - bd3 = np.empty(derham.p[2], dtype=float) + bd1 = xp.empty(derham.p[0], dtype=float) + bd2 = xp.empty(derham.p[1], dtype=float) + bd3 = xp.empty(derham.p[2], dtype=float) # struphy b_splines_slim val1s, val2s, val3s = [], [], [] @@ -103,13 +102,13 @@ def test_bsplines_span_and_basis(Nel, p, spl_kind): # compare for val1, val1_psy in zip(val1s, val1s_psy): - assert np.allclose(val1, val1_psy) + assert xp.allclose(val1, val1_psy) for val2, val2_psy in zip(val2s, val2s_psy): - assert np.allclose(val2, val2_psy) + assert xp.allclose(val2, val2_psy) for val3, val3_psy in zip(val3s, val3s_psy): - assert np.allclose(val3, val3_psy) + assert xp.allclose(val3, val3_psy) # struphy b_d_splines_slim val1s_n, val2s_n, val3s_n = [], [], [] @@ -131,13 +130,13 @@ def test_bsplines_span_and_basis(Nel, p, spl_kind): # compare for val1, val1_psy in zip(val1s_n, val1s_psy): - assert np.allclose(val1, val1_psy) + assert xp.allclose(val1, val1_psy) for val2, val2_psy in zip(val2s_n, val2s_psy): - assert np.allclose(val2, val2_psy) + assert xp.allclose(val2, val2_psy) for val3, val3_psy in zip(val3s_n, val3s_psy): - assert np.allclose(val3, val3_psy) + assert xp.allclose(val3, val3_psy) # struphy d_splines_slim span1s, span2s, span3s = [], [], [] @@ -175,22 +174,22 @@ def test_bsplines_span_and_basis(Nel, p, spl_kind): # compare for val1, val1_psy in zip(val1s, val1s_psy): - assert np.allclose(val1, val1_psy) + assert xp.allclose(val1, val1_psy) for val2, val2_psy in zip(val2s, val2s_psy): - assert np.allclose(val2, val2_psy) + assert xp.allclose(val2, val2_psy) for val3, val3_psy in zip(val3s, val3s_psy): - assert np.allclose(val3, val3_psy) + assert xp.allclose(val3, val3_psy) for val1, val1_psy in zip(val1s_d, val1s_psy): - assert np.allclose(val1, val1_psy) + assert xp.allclose(val1, val1_psy) for val2, val2_psy in zip(val2s_d, val2s_psy): - assert np.allclose(val2, val2_psy) + assert xp.allclose(val2, val2_psy) for val3, val3_psy in zip(val3s_d, val3s_psy): - assert np.allclose(val3, val3_psy) + assert xp.allclose(val3, val3_psy) if __name__ == "__main__": diff --git a/src/struphy/bsplines/tests/test_eval_spline_mpi.py b/src/struphy/bsplines/tests/test_eval_spline_mpi.py index e40af4124..0703fb418 100644 --- a/src/struphy/bsplines/tests/test_eval_spline_mpi.py +++ b/src/struphy/bsplines/tests/test_eval_spline_mpi.py @@ -1,11 +1,10 @@ from sys import int_info from time import sleep +import cunumpy as xp import pytest from psydac.ddm.mpi import mpi as MPI -from struphy.utils.arrays import xp as np - @pytest.mark.parametrize("Nel", [[8, 9, 10]]) @pytest.mark.parametrize("p", [[1, 2, 3], [3, 1, 2]]) @@ -38,9 +37,9 @@ def test_eval_kernels(Nel, p, spl_kind, n_markers=10): # Random points in domain of process dom = derham.domain_array[rank] - eta1s = np.random.rand(n_markers) * (dom[1] - dom[0]) + dom[0] - eta2s = np.random.rand(n_markers) * (dom[4] - dom[3]) + dom[3] - eta3s = np.random.rand(n_markers) * (dom[7] - dom[6]) + dom[6] + eta1s = xp.random.rand(n_markers) * (dom[1] - dom[0]) + dom[0] + eta2s = xp.random.rand(n_markers) * (dom[4] - dom[3]) + dom[3] + eta3s = xp.random.rand(n_markers) * (dom[7] - dom[6]) + dom[6] for eta1, eta2, eta3 in zip(eta1s, eta2s, eta3s): comm.Barrier() @@ -56,13 +55,13 @@ def test_eval_kernels(Nel, p, spl_kind, n_markers=10): span3 = bsp.find_span(tn3, derham.p[2], eta3) # non-zero spline values at eta - bn1 = np.empty(derham.p[0] + 1, dtype=float) - bn2 = np.empty(derham.p[1] + 1, dtype=float) - bn3 = np.empty(derham.p[2] + 1, dtype=float) + bn1 = xp.empty(derham.p[0] + 1, dtype=float) + bn2 = xp.empty(derham.p[1] + 1, dtype=float) + bn3 = xp.empty(derham.p[2] + 1, dtype=float) - bd1 = np.empty(derham.p[0], dtype=float) - bd2 = np.empty(derham.p[1], dtype=float) - bd3 = np.empty(derham.p[2], dtype=float) + bd1 = xp.empty(derham.p[0], dtype=float) + bd2 = xp.empty(derham.p[1], dtype=float) + bd3 = xp.empty(derham.p[2], dtype=float) bsp.b_d_splines_slim(tn1, derham.p[0], eta1, span1, bn1, bd1) bsp.b_d_splines_slim(tn2, derham.p[1], eta2, span2, bn2, bd2) @@ -83,8 +82,8 @@ def test_eval_kernels(Nel, p, spl_kind, n_markers=10): # compare spline evaluation routines in V0 val = eval3d(*derham.p, bn1, bn2, bn3, ind_n1, ind_n2, ind_n3, x0[0]) - val_mpi = eval3d_mpi(*derham.p, bn1, bn2, bn3, span1, span2, span3, x0_psy._data, np.array(x0_psy.starts)) - assert np.allclose(val, val_mpi) + val_mpi = eval3d_mpi(*derham.p, bn1, bn2, bn3, span1, span2, span3, x0_psy._data, xp.array(x0_psy.starts)) + assert xp.allclose(val, val_mpi) # compare spline evaluation routines in V1 val = eval3d(derham.p[0] - 1, derham.p[1], derham.p[2], bd1, bn2, bn3, ind_d1, ind_n2, ind_n3, x1[0]) @@ -99,9 +98,9 @@ def test_eval_kernels(Nel, p, spl_kind, n_markers=10): span2, span3, x1_psy[0]._data, - np.array(x1_psy[0].starts), + xp.array(x1_psy[0].starts), ) - assert np.allclose(val, val_mpi) + assert xp.allclose(val, val_mpi) val = eval3d(derham.p[0], derham.p[1] - 1, derham.p[2], bn1, bd2, bn3, ind_n1, ind_d2, ind_n3, x1[1]) val_mpi = eval3d_mpi( @@ -115,9 +114,9 @@ def test_eval_kernels(Nel, p, spl_kind, n_markers=10): span2, span3, x1_psy[1]._data, - np.array(x1_psy[1].starts), + xp.array(x1_psy[1].starts), ) - assert np.allclose(val, val_mpi) + assert xp.allclose(val, val_mpi) val = eval3d(derham.p[0], derham.p[1], derham.p[2] - 1, bn1, bn2, bd3, ind_n1, ind_n2, ind_d3, x1[2]) val_mpi = eval3d_mpi( @@ -131,9 +130,9 @@ def test_eval_kernels(Nel, p, spl_kind, n_markers=10): span2, span3, x1_psy[2]._data, - np.array(x1_psy[2].starts), + xp.array(x1_psy[2].starts), ) - assert np.allclose(val, val_mpi) + assert xp.allclose(val, val_mpi) # compare spline evaluation routines in V2 val = eval3d(derham.p[0], derham.p[1] - 1, derham.p[2] - 1, bn1, bd2, bd3, ind_n1, ind_d2, ind_d3, x2[0]) @@ -148,9 +147,9 @@ def test_eval_kernels(Nel, p, spl_kind, n_markers=10): span2, span3, x2_psy[0]._data, - np.array(x2_psy[0].starts), + xp.array(x2_psy[0].starts), ) - assert np.allclose(val, val_mpi) + assert xp.allclose(val, val_mpi) val = eval3d(derham.p[0] - 1, derham.p[1], derham.p[2] - 1, bd1, bn2, bd3, ind_d1, ind_n2, ind_d3, x2[1]) val_mpi = eval3d_mpi( @@ -164,9 +163,9 @@ def test_eval_kernels(Nel, p, spl_kind, n_markers=10): span2, span3, x2_psy[1]._data, - np.array(x2_psy[1].starts), + xp.array(x2_psy[1].starts), ) - assert np.allclose(val, val_mpi) + assert xp.allclose(val, val_mpi) val = eval3d(derham.p[0] - 1, derham.p[1] - 1, derham.p[2], bd1, bd2, bn3, ind_d1, ind_d2, ind_n3, x2[2]) val_mpi = eval3d_mpi( @@ -180,9 +179,9 @@ def test_eval_kernels(Nel, p, spl_kind, n_markers=10): span2, span3, x2_psy[2]._data, - np.array(x2_psy[2].starts), + xp.array(x2_psy[2].starts), ) - assert np.allclose(val, val_mpi) + assert xp.allclose(val, val_mpi) # compare spline evaluation routines in V3 val = eval3d(derham.p[0] - 1, derham.p[1] - 1, derham.p[2] - 1, bd1, bd2, bd3, ind_d1, ind_d2, ind_d3, x3[0]) @@ -197,9 +196,9 @@ def test_eval_kernels(Nel, p, spl_kind, n_markers=10): span2, span3, x3_psy._data, - np.array(x3_psy.starts), + xp.array(x3_psy.starts), ) - assert np.allclose(val, val_mpi) + assert xp.allclose(val, val_mpi) @pytest.mark.parametrize("Nel", [[8, 9, 10]]) @@ -230,9 +229,9 @@ def test_eval_pointwise(Nel, p, spl_kind, n_markers=10): # Random points in domain of process dom = derham.domain_array[rank] - eta1s = np.random.rand(n_markers) * (dom[1] - dom[0]) + dom[0] - eta2s = np.random.rand(n_markers) * (dom[4] - dom[3]) + dom[3] - eta3s = np.random.rand(n_markers) * (dom[7] - dom[6]) + dom[6] + eta1s = xp.random.rand(n_markers) * (dom[1] - dom[0]) + dom[0] + eta2s = xp.random.rand(n_markers) * (dom[4] - dom[3]) + dom[3] + eta3s = xp.random.rand(n_markers) * (dom[7] - dom[6]) + dom[6] for eta1, eta2, eta3 in zip(eta1s, eta2s, eta3s): comm.Barrier() @@ -251,14 +250,14 @@ def test_eval_pointwise(Nel, p, spl_kind, n_markers=10): eta3, x0_psy._data, derham.spline_types_pyccel["0"], - np.array(derham.p), + xp.array(derham.p), tn1, tn2, tn3, - np.array(x0_psy.starts), + xp.array(x0_psy.starts), ) - assert np.allclose(val, val_mpi) + assert xp.allclose(val, val_mpi) # compare spline evaluation routines in V1 # 1st component @@ -287,14 +286,14 @@ def test_eval_pointwise(Nel, p, spl_kind, n_markers=10): eta3, x1_psy[0]._data, derham.spline_types_pyccel["1"][0], - np.array(derham.p), + xp.array(derham.p), tn1, tn2, tn3, - np.array(x0_psy.starts), + xp.array(x0_psy.starts), ) - assert np.allclose(val, val_mpi) + assert xp.allclose(val, val_mpi) # 2nd component val = evaluate_3d( @@ -322,14 +321,14 @@ def test_eval_pointwise(Nel, p, spl_kind, n_markers=10): eta3, x1_psy[1]._data, derham.spline_types_pyccel["1"][1], - np.array(derham.p), + xp.array(derham.p), tn1, tn2, tn3, - np.array(x0_psy.starts), + xp.array(x0_psy.starts), ) - assert np.allclose(val, val_mpi) + assert xp.allclose(val, val_mpi) # 3rd component val = evaluate_3d( @@ -357,14 +356,14 @@ def test_eval_pointwise(Nel, p, spl_kind, n_markers=10): eta3, x1_psy[2]._data, derham.spline_types_pyccel["1"][2], - np.array(derham.p), + xp.array(derham.p), tn1, tn2, tn3, - np.array(x0_psy.starts), + xp.array(x0_psy.starts), ) - assert np.allclose(val, val_mpi) + assert xp.allclose(val, val_mpi) # compare spline evaluation routines in V2 # 1st component @@ -393,14 +392,14 @@ def test_eval_pointwise(Nel, p, spl_kind, n_markers=10): eta3, x2_psy[0]._data, derham.spline_types_pyccel["2"][0], - np.array(derham.p), + xp.array(derham.p), tn1, tn2, tn3, - np.array(x0_psy.starts), + xp.array(x0_psy.starts), ) - assert np.allclose(val, val_mpi) + assert xp.allclose(val, val_mpi) # 2nd component val = evaluate_3d( @@ -428,14 +427,14 @@ def test_eval_pointwise(Nel, p, spl_kind, n_markers=10): eta3, x2_psy[1]._data, derham.spline_types_pyccel["2"][1], - np.array(derham.p), + xp.array(derham.p), tn1, tn2, tn3, - np.array(x0_psy.starts), + xp.array(x0_psy.starts), ) - assert np.allclose(val, val_mpi) + assert xp.allclose(val, val_mpi) # 3rd component val = evaluate_3d( @@ -463,14 +462,14 @@ def test_eval_pointwise(Nel, p, spl_kind, n_markers=10): eta3, x2_psy[2]._data, derham.spline_types_pyccel["2"][2], - np.array(derham.p), + xp.array(derham.p), tn1, tn2, tn3, - np.array(x0_psy.starts), + xp.array(x0_psy.starts), ) - assert np.allclose(val, val_mpi) + assert xp.allclose(val, val_mpi) # compare spline evaluation routines in V3 val = evaluate_3d( @@ -496,14 +495,14 @@ def test_eval_pointwise(Nel, p, spl_kind, n_markers=10): eta3, x3_psy._data, derham.spline_types_pyccel["3"], - np.array(derham.p), + xp.array(derham.p), tn1, tn2, tn3, - np.array(x0_psy.starts), + xp.array(x0_psy.starts), ) - assert np.allclose(val, val_mpi) + assert xp.allclose(val, val_mpi) @pytest.mark.parametrize("Nel", [[8, 9, 10]]) @@ -544,13 +543,13 @@ def test_eval_tensor_product(Nel, p, spl_kind, n_markers=10): # Random points in domain of process dom = derham.domain_array[rank] - eta1s = np.random.rand(n_markers) * (dom[1] - dom[0]) + dom[0] - eta2s = np.random.rand(n_markers + 1) * (dom[4] - dom[3]) + dom[3] - eta3s = np.random.rand(n_markers + 2) * (dom[7] - dom[6]) + dom[6] + eta1s = xp.random.rand(n_markers) * (dom[1] - dom[0]) + dom[0] + eta2s = xp.random.rand(n_markers + 1) * (dom[4] - dom[3]) + dom[3] + eta3s = xp.random.rand(n_markers + 2) * (dom[7] - dom[6]) + dom[6] - vals = np.zeros((n_markers, n_markers + 1, n_markers + 2), dtype=float) - vals_mpi = np.zeros((n_markers, n_markers + 1, n_markers + 2), dtype=float) - vals_mpi_fast = np.zeros((n_markers, n_markers + 1, n_markers + 2), dtype=float) + vals = xp.zeros((n_markers, n_markers + 1, n_markers + 2), dtype=float) + vals_mpi = xp.zeros((n_markers, n_markers + 1, n_markers + 2), dtype=float) + vals_mpi_fast = xp.zeros((n_markers, n_markers + 1, n_markers + 2), dtype=float) comm.Barrier() sleep(0.02 * (rank + 1)) @@ -573,11 +572,11 @@ def test_eval_tensor_product(Nel, p, spl_kind, n_markers=10): eta3s, x0_psy._data, derham.spline_types_pyccel["0"], - np.array(derham.p), + xp.array(derham.p), tn1, tn2, tn3, - np.array(x0_psy.starts), + xp.array(x0_psy.starts), vals_mpi, ) t1 = time.time() @@ -591,19 +590,19 @@ def test_eval_tensor_product(Nel, p, spl_kind, n_markers=10): eta3s, x0_psy._data, derham.spline_types_pyccel["0"], - np.array(derham.p), + xp.array(derham.p), tn1, tn2, tn3, - np.array(x0_psy.starts), + xp.array(x0_psy.starts), vals_mpi_fast, ) t1 = time.time() if rank == 0: print("v0 eval_spline_mpi_tensor_product_fast:".ljust(40), t1 - t0) - assert np.allclose(vals, vals_mpi) - assert np.allclose(vals, vals_mpi_fast) + assert xp.allclose(vals, vals_mpi) + assert xp.allclose(vals, vals_mpi_fast) # compare spline evaluation routines in V3 t0 = time.time() @@ -633,11 +632,11 @@ def test_eval_tensor_product(Nel, p, spl_kind, n_markers=10): eta3s, x3_psy._data, derham.spline_types_pyccel["3"], - np.array(derham.p), + xp.array(derham.p), tn1, tn2, tn3, - np.array(x0_psy.starts), + xp.array(x0_psy.starts), vals_mpi, ) t1 = time.time() @@ -651,19 +650,19 @@ def test_eval_tensor_product(Nel, p, spl_kind, n_markers=10): eta3s, x3_psy._data, derham.spline_types_pyccel["3"], - np.array(derham.p), + xp.array(derham.p), tn1, tn2, tn3, - np.array(x0_psy.starts), + xp.array(x0_psy.starts), vals_mpi_fast, ) t1 = time.time() if rank == 0: print("v3 eval_spline_mpi_tensor_product_fast:".ljust(40), t1 - t0) - assert np.allclose(vals, vals_mpi) - assert np.allclose(vals, vals_mpi_fast) + assert xp.allclose(vals, vals_mpi) + assert xp.allclose(vals, vals_mpi_fast) @pytest.mark.parametrize("Nel", [[8, 9, 10]]) @@ -710,9 +709,9 @@ def test_eval_tensor_product_grid(Nel, p, spl_kind, n_markers=10): spans_f, bns_f, bds_f = derham.prepare_eval_tp_fixed([eta1s, eta2s, eta3s]) # output arrays - vals = np.zeros((eta1s.size, eta2s.size, eta3s.size), dtype=float) - vals_mpi_fixed = np.zeros((eta1s.size, eta2s.size, eta3s.size), dtype=float) - vals_mpi_grid = np.zeros((eta1s.size, eta2s.size, eta3s.size), dtype=float) + vals = xp.zeros((eta1s.size, eta2s.size, eta3s.size), dtype=float) + vals_mpi_fixed = xp.zeros((eta1s.size, eta2s.size, eta3s.size), dtype=float) + vals_mpi_grid = xp.zeros((eta1s.size, eta2s.size, eta3s.size), dtype=float) comm.Barrier() sleep(0.02 * (rank + 1)) @@ -748,20 +747,20 @@ def test_eval_tensor_product_grid(Nel, p, spl_kind, n_markers=10): *bds_f, x3_psy._data, derham.spline_types_pyccel["3"], - np.array(derham.p), - np.array(x0_psy.starts), + xp.array(derham.p), + xp.array(x0_psy.starts), vals_mpi_fixed, ) t1 = time.time() if rank == 0: print("v3 eval_spline_mpi_tensor_product_fixed:".ljust(40), t1 - t0) - assert np.allclose(vals, vals_mpi_fixed) + assert xp.allclose(vals, vals_mpi_fixed) field = derham.create_spline_function("test", "L2") field.vector = x3_psy - assert np.allclose(field.vector._data, x3_psy._data) + assert xp.allclose(field.vector._data, x3_psy._data) t0 = time.time() field.eval_tp_fixed_loc(spans_f, bds_f, out=vals_mpi_fixed) @@ -769,7 +768,7 @@ def test_eval_tensor_product_grid(Nel, p, spl_kind, n_markers=10): if rank == 0: print("v3 field.eval_tp_fixed:".ljust(40), t1 - t0) - assert np.allclose(vals, vals_mpi_fixed) + assert xp.allclose(vals, vals_mpi_fixed) if __name__ == "__main__": diff --git a/src/struphy/compile_struphy.mk b/src/struphy/compile_struphy.mk index 62b310eab..2aaec6cc6 100644 --- a/src/struphy/compile_struphy.mk +++ b/src/struphy/compile_struphy.mk @@ -5,7 +5,7 @@ PYTHON := python3 SO_EXT := $(shell $(PYTHON) -c "import sysconfig; print(sysconfig.get_config_var('EXT_SUFFIX'))") LIBDIR := $(shell $(PYTHON) -c "import sysconfig; print(sysconfig.get_config_var('LIBDIR'))") -struphy_path := $(shell $(PYTHON) -c "import struphy as _; print(_.__path__[0])") +struphy_path := $(shell $(PYTHON) -c "import struphy; print(struphy.__path__[0])") # Arguments to this script are: STRUPHY_SOURCES := $(sources) diff --git a/src/struphy/conftest.py b/src/struphy/conftest.py index 5a936455c..05e10b55e 100644 --- a/src/struphy/conftest.py +++ b/src/struphy/conftest.py @@ -1,18 +1,14 @@ def pytest_addoption(parser): - parser.addoption("--fast", action="store_true") parser.addoption("--with-desc", action="store_true") parser.addoption("--vrbose", action="store_true") - parser.addoption("--verification", action="store_true") parser.addoption("--show-plots", action="store_true") parser.addoption("--nclones", type=int, default=1) + parser.addoption("--model-name", type=str, default="Maxwell") def pytest_generate_tests(metafunc): # This is called for every test. Only get/set command line arguments - # if the argument is specified in the list of test "fixturenames". - option_value = metafunc.config.option.fast - if "fast" in metafunc.fixturenames and option_value is not None: - metafunc.parametrize("fast", [option_value]) + # if the argument is specified in the list of test "fixturenames".]) option_value = metafunc.config.option.with_desc if "with_desc" in metafunc.fixturenames and option_value is not None: @@ -22,10 +18,6 @@ def pytest_generate_tests(metafunc): if "vrbose" in metafunc.fixturenames and option_value is not None: metafunc.parametrize("vrbose", [option_value]) - option_value = metafunc.config.option.verification - if "verification" in metafunc.fixturenames and option_value is not None: - metafunc.parametrize("verification", [option_value]) - option_value = metafunc.config.option.nclones if "nclones" in metafunc.fixturenames and option_value is not None: metafunc.parametrize("nclones", [option_value]) @@ -33,3 +25,7 @@ def pytest_generate_tests(metafunc): option_value = metafunc.config.option.show_plots if "show_plots" in metafunc.fixturenames and option_value is not None: metafunc.parametrize("show_plots", [option_value]) + + option_value = metafunc.config.option.model_name + if "model_name" in metafunc.fixturenames and option_value is not None: + metafunc.parametrize("model_name", [option_value]) diff --git a/src/struphy/console/main.py b/src/struphy/console/main.py index 1e2301555..f6e03ff16 100644 --- a/src/struphy/console/main.py +++ b/src/struphy/console/main.py @@ -16,7 +16,7 @@ # struphy path import struphy -import struphy.utils.utils as utils +from struphy.utils import utils libpath = struphy.__path__[0] __version__ = importlib.metadata.version("struphy") @@ -61,17 +61,18 @@ def struphy(): batch_files = get_batch_files(b_path) # Load the models and messages + model_message = "All models are listed on https://struphy.pages.mpcdf.de/struphy/sections/models.html" list_models = [] - model_message = fluid_message = kinetic_message = hybrid_message = toy_message = "" - try: - with open(os.path.join(libpath, "models", "models_list"), "rb") as fp: - list_models = pickle.load(fp) - with open(os.path.join(libpath, "models", "models_message"), "rb") as fp: - model_message, fluid_message, kinetic_message, hybrid_message, toy_message = pickle.load( - fp, - ) - except: - print("run: struphy --refresh-models") + ml_path = os.path.join(libpath, "models", "models_list") + if not os.path.isfile(ml_path): + utils.refresh_models() + + with open(ml_path, "rb") as fp: + list_models = pickle.load(fp) + with open(os.path.join(libpath, "models", "models_message"), "rb") as fp: + model_message, fluid_message, kinetic_message, hybrid_message, toy_message = pickle.load( + fp, + ) # 0. basic options add_parser_basic_options(parser, i_path, o_path, b_path) @@ -227,7 +228,7 @@ def struphy(): def get_params_files(i_path): if os.path.exists(i_path) and os.path.isdir(i_path): - params_files = recursive_get_files(i_path) + params_files = recursive_get_files(i_path, contains=(".yml", ".yaml", ".py")) else: print("Path to input files missing! Set it with `struphy --set-i PATH`") params_files = [] @@ -683,14 +684,14 @@ def add_parser_params(subparsers, list_models, model_message): "params", formatter_class=lambda prog: argparse.RawTextHelpFormatter( prog, - max_help_position=30, + max_help_position=35, ), help="create default parameter file for a model, or show model's options", - description="Creates a default parameter file for a specific model, or shows a model's options.", + description="Create default parameter file (.py) for a specific model.", ) parser_params.add_argument( - "model", + "model_name", type=str, choices=list_models, metavar="MODEL", @@ -698,18 +699,11 @@ def add_parser_params(subparsers, list_models, model_message): ) parser_params.add_argument( - "-f", - "--file", + "-p", + "--params-path", type=str, - metavar="FILE", - help="name of the parameter file (.yml) to be created in the current I/O path (default=params_.yml)", - ) - - parser_params.add_argument( - "-o", - "--options", - help="show model options", - action="store_true", + metavar="PATH", + help="Absolute path to the parameter file (default is getcwd()/params_MODEL.py)", ) parser_params.add_argument( @@ -722,7 +716,7 @@ def add_parser_params(subparsers, list_models, model_message): parser_params.add_argument( "-y", "--yes", - help="Say yes on prompt to overwrite .yml FILE", + help="Say yes on prompt to overwrite PATH", action="store_true", ) @@ -940,11 +934,19 @@ def add_parser_test(subparsers, list_models): parser_test.add_argument( "group", type=str, - choices=list_models + ["models"] + ["unit"] + ["fluid"] + ["kinetic"] + ["hybrid"] + ["toy"], + choices=list_models + + ["models"] + + ["unit"] + + ["fluid"] + + ["kinetic"] + + ["hybrid"] + + ["toy"] + + ["verification"], metavar="GROUP", help='can be either:\na) a model name \ \nb) "models" for testing of all models (or "fluid", "kinetic", "hybrid", "toy" for testing just a sub-group) \ - \nc) "unit" for performing unit tests', + \nc) "verification" for running all verification tests \ + \nd) "unit" for performing unit tests', ) parser_test.add_argument( @@ -955,27 +957,12 @@ def add_parser_test(subparsers, list_models): default=1, ) - parser_test.add_argument( - "-f", - "--fast", - help="test model(s) just in slab geometry (Cuboid)", - action="store_true", - ) - parser_test.add_argument( "--with-desc", help="include DESC equilibrium in tests (mem consuming)", action="store_true", ) - parser_test.add_argument( - "-T", - "--Tend", - type=float, - help="if GROUP=a), simulation end time in units of the model (default=0.015 with dt=0.005), data is only saved at TEND if set", - default=None, - ) - parser_test.add_argument( "-v", "--vrbose", @@ -983,12 +970,6 @@ def add_parser_test(subparsers, list_models): action="store_true", ) - parser_test.add_argument( - "--verification", - help="perform verification runs specified in io/inp/verification/", - action="store_true", - ) - parser_test.add_argument( "--nclones", type=int, diff --git a/src/struphy/console/params.py b/src/struphy/console/params.py index 555e50c76..ade6c5ea5 100644 --- a/src/struphy/console/params.py +++ b/src/struphy/console/params.py @@ -3,35 +3,34 @@ import yaml from psydac.ddm.mpi import mpi as MPI +from struphy.models import fluid, hybrid, kinetic, toy +from struphy.models.base import StruphyModel -def struphy_params(model, file, yes=False, options=False, check_file=None): + +def struphy_params(model_name: str, params_path: str, yes: bool = False, check_file: bool = False): """Create a model's default parameter file and save in current input path. Parameters ---------- - model : str + model_name : str The name of the Struphy model. - yes : bool - If true, say yes on prompt to overwrite .yml FILE - - file : str + params_path : str An alternative file name to the default params_.yml. - show_options : bool - Whether to print to screen all possible options for the model. + yes : bool + If true, say yes on prompt to overwrite .yml FILE """ - - from struphy.models import fluid, hybrid, kinetic, toy - - # load model class objs = [fluid, kinetic, hybrid, toy] for obj in objs: try: - model_class = getattr(obj, model) + model_class = getattr(obj, model_name) + model: StruphyModel = model_class() except AttributeError: pass + print(f"{model_name = }") + # print units if check_file: print(f"Checking {check_file} with model {model_class}") @@ -46,8 +45,8 @@ def struphy_params(model, file, yes=False, options=False, check_file=None): print(f"Failed to initialize model: {e}") sys.exit(1) - elif options: - model_class.show_options() else: prompt = not yes - params = model_class.generate_default_parameter_file(file=file, prompt=prompt) + model.generate_default_parameter_file(path=params_path, prompt=prompt) + # print(f"Generating default parameter file for {model_class}.") + # model_class().generate_default_parameter_file(path=params_path, prompt=prompt) diff --git a/src/struphy/console/profile.py b/src/struphy/console/profile.py index aa6a36d56..9577a00d9 100644 --- a/src/struphy/console/profile.py +++ b/src/struphy/console/profile.py @@ -6,12 +6,12 @@ def struphy_profile(dirs, replace, all, n_lines, print_callers, savefig): import os import pickle + import cunumpy as xp import yaml from matplotlib import pyplot as plt import struphy.utils.utils as utils from struphy.post_processing.cprofile_analyser import get_cprofile_data, replace_keys - from struphy.utils.arrays import xp as np # Read struphy state file state = utils.read_state() @@ -167,10 +167,10 @@ def struphy_profile(dirs, replace, all, n_lines, print_callers, savefig): ratio.append(str(int(float(t) / runtime * 100)) + "%") # strong scaling plot - if np.all([Nel == val["Nel"][0] for Nel in val["Nel"]]): + if xp.all([Nel == val["Nel"][0] for Nel in val["Nel"]]): # ideal scaling if n == 0: - ax.loglog(val["mpi_size"], 1 / 2 ** np.arange(len(val["time"])), "k--", alpha=0.3, label="ideal") + ax.loglog(val["mpi_size"], 1 / 2 ** xp.arange(len(val["time"])), "k--", alpha=0.3, label="ideal") # print average time per one time step if "integrate" in key: @@ -206,11 +206,11 @@ def struphy_profile(dirs, replace, all, n_lines, print_callers, savefig): ax.set_ylabel("time [s]") ax.set( title="Weak scaling for cells/mpi_size=" - + str(np.prod(val["Nel"][0]) / val["mpi_size"][0]) + + str(xp.prod(val["Nel"][0]) / val["mpi_size"][0]) + "=const." ) ax.legend(loc="upper left") - # ax.loglog(val['mpi_size'], val['time'][0]*np.ones_like(val['time']), 'k--', alpha=0.3) + # ax.loglog(val['mpi_size'], val['time'][0]*xp.ones_like(val['time']), 'k--', alpha=0.3) ax.set_xscale("log") if savefig is None: diff --git a/src/struphy/console/test.py b/src/struphy/console/test.py index 804279dc5..ebcff34d4 100644 --- a/src/struphy/console/test.py +++ b/src/struphy/console/test.py @@ -5,11 +5,8 @@ def struphy_test( group: str, *, mpi: int = 1, - fast: bool = False, with_desc: bool = False, - Tend: float = None, vrbose: bool = False, - verification: bool = False, show_plots: bool = False, nclones: int = 1, ): @@ -19,13 +16,10 @@ def struphy_test( Parameters ---------- group : str - Test identifier: "unit", "models", "fluid", "kinetic", "hybrid", "toy" or a model name. + Test identifier: "unit", "models", "fluid", "kinetic", "hybrid", "toy", "verification" or a model name. mpi : int - Number of MPI processes used in tests (must be >1, default=2). - - fast : bool - Whether to test models just in slab geometry. + Number of MPI processes used in tests (default=1). with_desc : bool Whether to include DESC equilibrium in unit tests (mem consuming). @@ -36,9 +30,6 @@ def struphy_test( vrbose : bool Show full screen output. - verification : bool - Whether to run verification tests specified in io/inp/tests. - show_plots : bool Show plots of tests. """ @@ -70,15 +61,18 @@ def struphy_test( subp_run(cmd) - elif "models" in group: + elif group in {"models", "fluid", "kinetic", "hybrid", "toy"}: if mpi > 1: cmd = [ "mpirun", + "--oversubscribe", "-n", str(mpi), "pytest", "-k", "_models", + "-m", + group, "-s", "--with-mpi", ] @@ -87,103 +81,68 @@ def struphy_test( "pytest", "-k", "_models", + "-m", + group, "-s", ] - if fast: - cmd += ["--fast"] if vrbose: cmd += ["--vrbose"] - if verification: - cmd += ["--verification"] if nclones > 1: cmd += ["--nclones", f"{nclones}"] if show_plots: cmd += ["--show-plots"] subp_run(cmd) - # test post processing of models - if not verification: + elif "verification" in group: + if mpi > 1: + cmd = [ + "mpirun", + "--oversubscribe", + "-n", + str(mpi), + "pytest", + "-k", + "_verif_", + "-s", + "--with-mpi", + ] + else: cmd = [ "pytest", "-k", - "pproc", + "_verif_", "-s", ] - subp_run(cmd) - elif group in {"fluid", "kinetic", "hybrid", "toy"}: - cmd = [ - "mpirun", - "-n", - str(mpi), - "pytest", - "-k", - group + "_models", - "-s", - "--with-mpi", - ] - if fast: - cmd += ["--fast"] if vrbose: cmd += ["--vrbose"] - if verification: - cmd += ["--verification"] if nclones > 1: cmd += ["--nclones", f"{nclones}"] if show_plots: cmd += ["--show-plots"] subp_run(cmd) - if not verification: - from struphy.models.tests.test_xxpproc import test_pproc_codes - - test_pproc_codes(group=group) - else: - import os - import pickle - - import struphy - - libpath = struphy.__path__[0] - - with open(os.path.join(libpath, "models", "models_message"), "rb") as fp: - model_message, fluid_message, kinetic_message, hybrid_message, toy_message = pickle.load( - fp, - ) - - if group in toy_message: - mtype = "toy" - elif group in fluid_message: - mtype = "fluid" - elif group in kinetic_message: - mtype = "kinetic" - elif group in hybrid_message: - mtype = "hybrid" - else: - raise ValueError(f"{group} is not a valid model name.") - - py_file = os.path.join(libpath, "models", "tests", "util.py") - cmd = [ "mpirun", + "--oversubscribe", "-n", str(mpi), - "python3", - py_file, - mtype, + "pytest", + "-k", + "_models", + "-m", + "single", + "-s", + "--with-mpi", + "--model-name", group, - str(Tend), - str(fast), - str(vrbose), - str(verification), - str(nclones), - str(show_plots), ] + if vrbose: + cmd += ["--vrbose"] + if nclones > 1: + cmd += ["--nclones", f"{nclones}"] + if show_plots: + cmd += ["--show-plots"] subp_run(cmd) - - if not verification: - from struphy.models.tests.test_xxpproc import test_pproc_codes - - test_pproc_codes(group=mtype) diff --git a/src/struphy/console/tests/test_console.py b/src/struphy/console/tests/test_console.py index ae31eb6d2..abdd51384 100644 --- a/src/struphy/console/tests/test_console.py +++ b/src/struphy/console/tests/test_console.py @@ -78,7 +78,7 @@ def split_command(command): # ["units", "Maxwell", "--input-abs", "/params.yml"], # Test cases for 'params' sub-command ["params", "Maxwell"], - ["params", "Vlasov", "--options"], + ["params", "Vlasov"], # ["params", "Maxwell", "-f", "params_Maxwell.yml"], # Test cases for 'profile' sub-command ["profile", "sim_1"], @@ -93,7 +93,7 @@ def split_command(command): # Test cases for 'test' sub-command ["test", "models"], ["test", "unit"], - ["test", "Maxwell", "--Tend", "1.0"], + ["test", "Maxwell"], ["test", "hybrid", "--mpi", "8"], ], ) @@ -360,10 +360,9 @@ def mock_remove(path): @pytest.mark.parametrize("model", ["Maxwell"]) @pytest.mark.parametrize("file", ["params_Maxwell.yml", "params_Maxwel2.yml"]) @pytest.mark.parametrize("yes", [True]) -@pytest.mark.parametrize("options", [True, False]) -def test_struphy_params(tmp_path, model, file, yes, options): +def test_struphy_params(tmp_path, model, file, yes): file_path = os.path.join(tmp_path, file) - struphy_params(model, str(file_path), yes=yes, options=options) + struphy_params(model, str(file_path), yes=yes) @pytest.mark.mpi_skip diff --git a/src/struphy/diagnostics/console_diagn.py b/src/struphy/diagnostics/console_diagn.py index 35294b37a..e110d1497 100644 --- a/src/struphy/diagnostics/console_diagn.py +++ b/src/struphy/diagnostics/console_diagn.py @@ -5,13 +5,13 @@ import os import subprocess +import cunumpy as xp import h5py import yaml import struphy import struphy.utils.utils as utils from struphy.diagnostics.diagn_tools import plot_distr_fun, plot_scalars, plots_videos_2d -from struphy.utils.arrays import xp as np def main(): @@ -301,7 +301,7 @@ def main(): bckgr_fun = getattr(maxwellians, default_bckgr_type)() # Get values of background shifts in velocity space - positions = [np.array([grid_slices["e" + str(k)]]) for k in range(1, 4)] + positions = [xp.array([grid_slices["e" + str(k)]]) for k in range(1, 4)] u = bckgr_fun.u(*positions) eval_params = {"u" + str(k + 1): u[k][0] for k in range(3)} @@ -315,7 +315,7 @@ def main(): # Plot the distribution function if "plot_distr" in actions: # Get index of where to plot in time - time_idx = np.argmin(np.abs(time - saved_time)) + time_idx = xp.argmin(xp.abs(time - saved_time)) plot_distr_fun( path=os.path.join( diff --git a/src/struphy/diagnostics/continuous_spectra.py b/src/struphy/diagnostics/continuous_spectra.py index cbe42dc94..5ed069179 100644 --- a/src/struphy/diagnostics/continuous_spectra.py +++ b/src/struphy/diagnostics/continuous_spectra.py @@ -37,8 +37,9 @@ def get_mhd_continua_2d(space, domain, omega2, U_eig, m_range, omega_A, div_tol, the radial location s_spec[m][0], squared eigenfrequencis s_spec[m][1] and global mode index s_spec[m][2] corresponding to slow sound modes for each poloidal mode number m in m_range. """ + import cunumpy as xp + import struphy.bsplines.bsplines as bsp - from struphy.utils.arrays import xp as np # greville points in radial direction (s) gN_1 = bsp.greville(space.T[0], space.p[0], space.spl_kind[0]) @@ -49,7 +50,7 @@ def get_mhd_continua_2d(space, domain, omega2, U_eig, m_range, omega_A, div_tol, gD_2 = bsp.greville(space.t[1], space.p[1] - 1, space.spl_kind[1]) # poloidal mode numbers - ms = np.arange(m_range[1] - m_range[0] + 1) + m_range[0] + ms = xp.arange(m_range[1] - m_range[0] + 1) + m_range[0] # grid for normalized Jacobian determinant det_df = domain.jacobian_det(gD_1, gD_2, 0.0) @@ -65,7 +66,7 @@ def get_mhd_continua_2d(space, domain, omega2, U_eig, m_range, omega_A, div_tol, s_spec = [[[], [], []] for m in ms] # only consider eigenmodes in range omega^2/omega_A^2 = [0, 1] - modes_ind = np.where((np.real(omega2) / omega_A**2 < 1.0) & (np.real(omega2) / omega_A**2 > 0.0))[0] + modes_ind = xp.where((xp.real(omega2) / omega_A**2 < 1.0) & (xp.real(omega2) / omega_A**2 > 0.0))[0] for i in range(modes_ind.size): # determine whether it's an Alfvén branch or sound branch by checking DIV(U) @@ -85,14 +86,14 @@ def get_mhd_continua_2d(space, domain, omega2, U_eig, m_range, omega_A, div_tol, U2_1_coeff = (U2_1_coeff[:, :, 0] - 1j * U2_1_coeff[:, :, 1]) / 2 # determine radial location of singularity by looking for a peak in eigenfunction U2_1 - s_ind = np.unravel_index(np.argmax(abs(U2_1_coeff)), U2_1_coeff.shape)[0] + s_ind = xp.unravel_index(xp.argmax(abs(U2_1_coeff)), U2_1_coeff.shape)[0] s = gN_1[s_ind] # perform fft to determine m - U2_1_fft = np.fft.fft(U2_1_coeff) + U2_1_fft = xp.fft.fft(U2_1_coeff) # determine m by looking for peak in Fourier spectrum at singularity - m = int((np.fft.fftfreq(U2_1_fft[s_ind].size) * U2_1_fft[s_ind].size)[np.argmax(abs(U2_1_fft[s_ind]))]) + m = int((xp.fft.fftfreq(U2_1_fft[s_ind].size) * U2_1_fft[s_ind].size)[xp.argmax(abs(U2_1_fft[s_ind]))]) ## perform shift for negative m # if m >= (space.Nel[1] + 1)//2: @@ -102,7 +103,7 @@ def get_mhd_continua_2d(space, domain, omega2, U_eig, m_range, omega_A, div_tol, for j in range(ms.size): if ms[j] == m: a_spec[j][0].append(s) - a_spec[j][1].append(np.real(omega2[modes_ind[i]])) + a_spec[j][1].append(xp.real(omega2[modes_ind[i]])) a_spec[j][2].append(modes_ind[i]) # Sound branch @@ -116,14 +117,14 @@ def get_mhd_continua_2d(space, domain, omega2, U_eig, m_range, omega_A, div_tol, U2_coeff = (U2_coeff[:, :, 0] - 1j * U2_coeff[:, :, 1]) / 2 # determine radial location of singularity by looking for a peak in eigenfunction (U2_2 or U2_3) - s_ind = np.unravel_index(np.argmax(abs(U2_coeff)), U2_coeff.shape)[0] + s_ind = xp.unravel_index(xp.argmax(abs(U2_coeff)), U2_coeff.shape)[0] s = gD_1[s_ind] # perform fft to determine m - U2_fft = np.fft.fft(U2_coeff) + U2_fft = xp.fft.fft(U2_coeff) # determine m by looking for peak in Fourier spectrum at singularity - m = int((np.fft.fftfreq(U2_fft[s_ind].size) * U2_fft[s_ind].size)[np.argmax(abs(U2_fft[s_ind]))]) + m = int((xp.fft.fftfreq(U2_fft[s_ind].size) * U2_fft[s_ind].size)[xp.argmax(abs(U2_fft[s_ind]))]) ## perform shift for negative m # if m >= (space.Nel[1] + 1)//2: @@ -133,13 +134,13 @@ def get_mhd_continua_2d(space, domain, omega2, U_eig, m_range, omega_A, div_tol, for j in range(ms.size): if ms[j] == m: s_spec[j][0].append(s) - s_spec[j][1].append(np.real(omega2[modes_ind[i]])) + s_spec[j][1].append(xp.real(omega2[modes_ind[i]])) s_spec[j][2].append(modes_ind[i]) # convert to array for j in range(ms.size): - a_spec[j] = np.array(a_spec[j]) - s_spec[j] = np.array(s_spec[j]) + a_spec[j] = xp.array(a_spec[j]) + s_spec[j] = xp.array(s_spec[j]) return a_spec, s_spec @@ -151,10 +152,9 @@ def get_mhd_continua_2d(space, domain, omega2, U_eig, m_range, omega_A, div_tol, import os import shutil + import cunumpy as xp import yaml - from struphy.utils.arrays import xp as np - # parse arguments parser = argparse.ArgumentParser( description="Looks for eigenmodes in a given MHD eigenspectrum in a certain poloidal mode number range and plots the continuous shear Alfvén and slow sound spectra (frequency versus radial-like coordinate)." @@ -256,7 +256,7 @@ def get_mhd_continua_2d(space, domain, omega2, U_eig, m_range, omega_A, div_tol, ) # load and analyze spectrum - omega2, U2_eig = np.split(np.load(spec_path), [1], axis=0) + omega2, U2_eig = xp.split(xp.load(spec_path), [1], axis=0) omega2 = omega2.flatten() m_range_alfven = [args.m_l_alfvén, args.m_u_alfvén] @@ -282,7 +282,7 @@ def get_mhd_continua_2d(space, domain, omega2, U_eig, m_range, omega_A, div_tol, fig.set_figheight(12) fig.set_figwidth(14) - etaplot = [np.linspace(0.0, 1.0, 201), np.linspace(0.0, 1.0, 101)] + etaplot = [xp.linspace(0.0, 1.0, 201), xp.linspace(0.0, 1.0, 101)] etaplot[0][0] += 1e-5 diff --git a/src/struphy/diagnostics/diagn_tools.py b/src/struphy/diagnostics/diagn_tools.py index 8ab8ec36a..d714306f4 100644 --- a/src/struphy/diagnostics/diagn_tools.py +++ b/src/struphy/diagnostics/diagn_tools.py @@ -3,6 +3,7 @@ import shutil import subprocess +import cunumpy as xp import matplotlib.colors as colors import matplotlib.pyplot as plt from scipy.fft import fftfreq, fftn @@ -10,23 +11,25 @@ from tqdm import tqdm from struphy.dispersion_relations import analytic -from struphy.utils.arrays import xp as np def power_spectrum_2d( - values, - name, - code, - grids, - grids_mapped=None, - component=0, - slice_at=(None, 0, 0), - do_plot=False, - disp_name=None, - disp_params={}, - save_plot=False, - save_name=None, - file_format="png", + values: dict, + name: str, + grids: tuple, + grids_mapped: tuple = None, + component: int = 0, + slice_at: tuple = (None, 0, 0), + do_plot: bool = False, + disp_name: str = None, + disp_params: dict = {}, + fit_branches: int = 0, + noise_level: float = 0.1, + extr_order: int = 10, + fit_degree: tuple = (1,), + save_plot: bool = False, + save_name: str = None, + file_format: str = "png", ): """Perform fft in space-time, (t, x) -> (omega, k), where x can be a logical or physical coordinate. Returns values if plot=False. @@ -34,25 +37,22 @@ def power_spectrum_2d( Parameters ---------- values : dict - Dictionary holding values of a B-spline FemField on the grid as 3d np.arrays: + Dictionary holding values of a B-spline FemField on the grid as 3d xp.arrays: values[n] contains the values at time step n, where n = 0:Nt-1:step with 0 0: + assert len(fit_degree) == fit_branches + # determine maxima for each k + k_start = kvec.size // 8 # take only first half of k-vector + k_end = kvec.size // 2 # take only first half of k-vector + k_fit = [] + omega_fit = {} + for n in range(fit_branches): + omega_fit[n] = [] + for k, f_of_omega in zip(kvec[k_start:k_end], dispersion[:, k_start:k_end].T): + threshold = xp.max(f_of_omega) * noise_level + extrms = argrelextrema(f_of_omega, xp.greater, order=extr_order)[0] + above_noise = xp.nonzero(f_of_omega > threshold)[0] + intersec = list(set(extrms) & set(above_noise)) + # intersec = list(set(extrms)) + if not intersec: + continue + intersec.sort() + # print(f"{intersec = }") + # print(f"{[omega[intersec[n]] for n in range(fit_branches)]}") + assert len(intersec) == fit_branches, ( + f"Number of found branches {len(intersec)} is not {fit_branches = }! \ + Try to lower 'noise_level' or increase 'extr_order'." + ) + k_fit += [k] + for n in range(fit_branches): + omega_fit[n] += [omega[intersec[n]]] - dispersion = (2.0 / Nt) * (2.0 / Nx) * np.abs(fftn(data))[: Nt // 2, : Nx // 2] - kvec = 2 * np.pi * fftfreq(Nx, dx)[: Nx // 2] - omega = 2 * np.pi * fftfreq(Nt, dt)[: Nt // 2] + # fit + coeffs = [] + for m, om in omega_fit.items(): + coeffs += [xp.polyfit(k_fit, om, deg=fit_degree[n])] + print(f"\nFitted {coeffs = }") if do_plot: _, ax = plt.subplots(1, 1, figsize=(10, 10)) colormap = "plasma" - K, W = np.meshgrid(kvec, omega) - lvls = np.logspace(-15, -1, 27) + K, W = xp.meshgrid(kvec, omega) + lvls = xp.logspace(-15, -1, 27) disp_plot = ax.contourf( K, W, @@ -156,11 +204,22 @@ def power_spectrum_2d( mappable=disp_plot, format="%.0e", ) - title = name + " component " + str(component + 1) + " from code: " + code + title = name + ", component " + str(component + 1) ax.set_title(title) ax.set_xlabel("$k$ [a.u.]") ax.set_ylabel(r"$\omega$ [a.u.]") + if fit_branches > 0: + for n, cs in enumerate(coeffs): + + def fun(k): + out = k * 0.0 + for i, c in enumerate(xp.flip(cs)): + out += c * k**i + return out + + ax.plot(kvec, fun(kvec), "r:", label=f"fit_{n + 1}") + # analytic solution: disp_class = getattr(analytic, disp_name) disp = disp_class(**disp_params) @@ -171,12 +230,12 @@ def power_spectrum_2d( set_min = 0.0 set_max = 0.0 for key, branch in branches.items(): - vals = np.real(branch) + vals = xp.real(branch) ax.plot(kvec, vals, "--", label=key) - tmp = np.min(vals) + tmp = xp.min(vals) if tmp < set_min: set_min = tmp - tmp = np.max(vals) + tmp = xp.max(vals) if tmp > set_max: set_max = tmp @@ -190,8 +249,7 @@ def power_spectrum_2d( else: plt.show() - else: - return kvec, omega, dispersion + return omega, kvec, dispersion, coeffs def plot_scalars( @@ -273,8 +331,8 @@ def plot_scalars( plt.figure("en_tot_rel_err") plt.plot( time[1:], - np.divide( - np.abs(en_tot[1:] - en_tot[0]), + xp.divide( + xp.abs(en_tot[1:] - en_tot[0]), en_tot[0], ), ) @@ -305,9 +363,9 @@ def plot_scalars( for key, plot_quantity in plot_quantities.items(): # Get the indices of the extrema if do_fit: - inds_exs = argrelextrema(plot_quantity, np.greater, order=order) + inds_exs = argrelextrema(plot_quantity, xp.greater, order=order) elif fit_minima: - inds_exs = argrelextrema(plot_quantity, np.less, order=order) + inds_exs = argrelextrema(plot_quantity, xp.less, order=order) else: inds_exs = None @@ -318,10 +376,10 @@ def plot_scalars( # for plotting take a bit more time at start and end if len(inds_exs[0]) >= 2: - time_start_idx = np.max( + time_start_idx = xp.max( [0, 2 * inds_exs[0][start_extremum] - inds_exs[0][start_extremum + 1]], ) - time_end_idx = np.min( + time_end_idx = xp.min( [ len(time) - 1, 2 * inds_exs[0][start_extremum + no_extrema - 1] - inds_exs[0][start_extremum + no_extrema - 2], @@ -337,9 +395,9 @@ def plot_scalars( if inds_exs is not None: # do the fitting - coeffs = np.polyfit( + coeffs = xp.polyfit( times_extrema, - np.log( + xp.log( quantity_extrema, ), deg=degree, @@ -352,15 +410,15 @@ def plot_scalars( ) plt.plot( time_cut, - np.exp(coeffs[0] * time_cut + coeffs[1]), - label=r"$a * \exp(m x)$ with" + f"\na={np.round(np.exp(coeffs[1]), 3)} m={np.round(coeffs[0], 3)}", + xp.exp(coeffs[0] * time_cut + coeffs[1]), + label=r"$a * \exp(m x)$ with" + f"\na={xp.round(xp.exp(coeffs[1]), 3)} m={xp.round(coeffs[0], 3)}", ) else: plt.plot(time, plot_quantity[:], ".", label=key, markersize=2) if inds_exs is not None: # do the fitting - coeffs = np.polyfit( + coeffs = xp.polyfit( times_extrema, quantity_extrema, deg=degree, @@ -375,8 +433,8 @@ def plot_scalars( ) plt.plot( time_cut, - np.exp(coeffs[0] * time_cut + coeffs[1]), - label=r"$a x + b$ with" + f"\na={np.round(coeffs[1], 3)} b={np.round(coeffs[0], 3)}", + xp.exp(coeffs[0] * time_cut + coeffs[1]), + label=r"$a x + b$ with" + f"\na={xp.round(coeffs[1], 3)} b={xp.round(coeffs[0], 3)}", ) plt.legend() @@ -438,11 +496,11 @@ def plot_distr_fun( # load full distribution functions if filename == "f_binned.npy": - f = np.load(filepath) + f = xp.load(filepath) # load delta f elif filename == "delta_f_binned.npy": - delta_f = np.load(filepath) + delta_f = xp.load(filepath) assert f is not None, "No distribution function file found!" @@ -450,7 +508,7 @@ def plot_distr_fun( directions = folder.split("_") for direction in directions: grids += [ - np.load( + xp.load( os.path.join( subpath, "grid_" + direction + ".npy", @@ -461,8 +519,8 @@ def plot_distr_fun( # Get indices of where to plot in other directions grid_idxs = {} for k in range(f.ndim - 1): - grid_idxs[directions[k]] = np.argmin( - np.abs(grids[k] - grid_slices[directions[k]]), + grid_idxs[directions[k]] = xp.argmin( + xp.abs(grids[k] - grid_slices[directions[k]]), ) for k in range(f.ndim - 1): @@ -597,17 +655,17 @@ def plots_videos_2d( grid_idxs = {} for k in range(df_data.ndim - 1): direc = directions[k] - grid_idxs[direc] = np.argmin( - np.abs(grids[direc] - grid_slices[direc]), + grid_idxs[direc] = xp.argmin( + xp.abs(grids[direc] - grid_slices[direc]), ) - grid_1 = np.load( + grid_1 = xp.load( os.path.join( data_path, "grid_" + label_1 + ".npy", ), ) - grid_2 = np.load( + grid_2 = xp.load( os.path.join( data_path, "grid_" + label_2 + ".npy", @@ -638,9 +696,9 @@ def plots_videos_2d( var *= polar_params["r_max"] - polar_params["r_min"] var += polar_params["r_min"] elif polar_params["angular_coord"] == sl: - var *= 2 * np.pi + var *= 2 * xp.pi - grid_1_mesh, grid_2_mesh = np.meshgrid(grid_1, grid_2, indexing="ij") + grid_1_mesh, grid_2_mesh = xp.meshgrid(grid_1, grid_2, indexing="ij") if output == "video": plots_2d_video( @@ -687,7 +745,7 @@ def video_2d(slc, diagn_path, images_path): Parameters ---------- - t_grid : np.ndarray + t_grid : xp.ndarray 1D-array containing all the times grid_slices : dict @@ -775,15 +833,15 @@ def plots_2d_video( # Get parameters for time and labelling for it nt = len(t_grid) - log_nt = int(np.log10(nt)) + 1 + log_nt = int(xp.log10(nt)) + 1 len_dt = len(str(t_grid[1]).split(".")[1]) # Get the correct scale for the plots - vmin += [np.min(df_binned[:]) / 3] - vmax += [np.max(df_binned[:]) / 3] - vmin = np.min(vmin) - vmax = np.max(vmax) - vscale = np.max(np.abs([vmin, vmax])) + vmin += [xp.min(df_binned[:]) / 3] + vmax += [xp.max(df_binned[:]) / 3] + vmin = xp.min(vmin) + vmax = xp.max(vmax) + vscale = xp.max(xp.abs([vmin, vmax])) # Set up the figure and axis once if do_polar: @@ -881,18 +939,18 @@ def plots_2d_overview( fig_height = 8.5 else: n_cols = 3 - n_rows = int(np.ceil(n_times / n_cols)) + n_rows = int(xp.ceil(n_times / n_cols)) fig_height = 4 * n_rows fig_size = (4 * n_cols, fig_height) # Get the correct scale for the plots for time in times: - vmin += [np.min(df_binned[time]) / 3] - vmax += [np.max(df_binned[time]) / 3] - vmin = np.min(vmin) - vmax = np.max(vmax) - vscale = np.max(np.abs([vmin, vmax])) + vmin += [xp.min(df_binned[time]) / 3] + vmax += [xp.max(df_binned[time]) / 3] + vmin = xp.min(vmin) + vmax = xp.max(vmax) + vscale = xp.max(xp.abs([vmin, vmax])) # Plot options for polar plots subplot_kw = dict(projection="polar") if do_polar else None @@ -901,8 +959,8 @@ def plots_2d_overview( fig, axes = plt.subplots(n_rows, n_cols, figsize=fig_size, subplot_kw=subplot_kw) # So we an use .flatten() even for just 1 plot - if not isinstance(axes, np.ndarray): - axes = np.array([axes]) + if not isinstance(axes, xp.ndarray): + axes = xp.array([axes]) # fig.tight_layout(h_pad=5.0, w_pad=5.0) # fig.tight_layout(pad=5.0) @@ -918,7 +976,7 @@ def plots_2d_overview( # Set the suptitle fig.suptitle(f"Struphy model '{model_name}'") - for k in np.arange(n_times): + for k in xp.arange(n_times): obj = axes.flatten()[k] n = times[k] t = f"%.{len_dt}f" % t_grid[n] @@ -990,13 +1048,13 @@ def get_slices_grids_directions_and_df_data(plot_full_f, grid_slices, data_path, slices_2d : list[string] A list of all the slicings - grids : list[np.ndarray] + grids : list[xp.ndarray] A list of all grids according to the slices directions : list[string] A list of the directions that appear in all slices - df_data : np.ndarray + df_data : xp.ndarray The data of delta-f (in case of full-f: distribution function minus background) """ @@ -1005,7 +1063,7 @@ def get_slices_grids_directions_and_df_data(plot_full_f, grid_slices, data_path, # Load all the grids grids = {} for direction in directions: - grids[direction] = np.load( + grids[direction] = xp.load( os.path.join(data_path, "grid_" + direction + ".npy"), ) @@ -1014,7 +1072,7 @@ def get_slices_grids_directions_and_df_data(plot_full_f, grid_slices, data_path, _name = "f_binned.npy" else: _name = "delta_f_binned.npy" - _data = np.load(os.path.join(data_path, _name)) + _data = xp.load(os.path.join(data_path, _name)) # Check how many slicings have been given and make slices_2d for all # combinations of spatial and velocity dimensions diff --git a/src/struphy/diagnostics/paraview/mesh_creator.py b/src/struphy/diagnostics/paraview/mesh_creator.py index 0a8a35903..c6c8d89a0 100644 --- a/src/struphy/diagnostics/paraview/mesh_creator.py +++ b/src/struphy/diagnostics/paraview/mesh_creator.py @@ -1,11 +1,10 @@ # from tqdm import tqdm +import cunumpy as xp import vtkmodules.all as vtk from vtkmodules.util.numpy_support import numpy_to_vtk as np2vtk from vtkmodules.util.numpy_support import vtk_to_numpy as vtk2np from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid -from struphy.utils.arrays import xp as np - def make_ugrid_and_write_vtu(filename: str, writer, vtk_dir, gvec, s_range, u_range, v_range, periodic): """A helper function to orchestrate operations to run many test cases. @@ -81,43 +80,43 @@ def gen_vtk_points(gvec, s_range, u_range, v_range, point_data, cell_data): pt_idx = 0 vtk_points = vtk.vtkPoints() - suv_points = np.zeros((s_range.shape[0], u_range.shape[0], v_range.shape[0], 3)) - xyz_points = np.zeros((s_range.shape[0], u_range.shape[0], v_range.shape[0], 3)) - point_indices = np.zeros((s_range.shape[0], u_range.shape[0], v_range.shape[0]), dtype=np.int_) + suv_points = xp.zeros((s_range.shape[0], u_range.shape[0], v_range.shape[0], 3)) + xyz_points = xp.zeros((s_range.shape[0], u_range.shape[0], v_range.shape[0], 3)) + point_indices = xp.zeros((s_range.shape[0], u_range.shape[0], v_range.shape[0]), dtype=xp.int_) # Add metadata to grid. num_pts = s_range.shape[0] * u_range.shape[0] * v_range.shape[0] - point_data["s"] = np.zeros(num_pts, dtype=np.float_) - point_data["u"] = np.zeros(num_pts, dtype=np.float_) - point_data["v"] = np.zeros(num_pts, dtype=np.float_) - point_data["x"] = np.zeros(num_pts, dtype=np.float_) - point_data["y"] = np.zeros(num_pts, dtype=np.float_) - point_data["z"] = np.zeros(num_pts, dtype=np.float_) - point_data["theta"] = np.zeros(num_pts, dtype=np.float_) - point_data["zeta"] = np.zeros(num_pts, dtype=np.float_) - point_data["Point ID"] = np.zeros(num_pts, dtype=np.int_) - point_data["pressure"] = np.zeros(num_pts, dtype=np.float_) - point_data["phi"] = np.zeros(num_pts, dtype=np.float_) - point_data["chi"] = np.zeros(num_pts, dtype=np.float_) - point_data["iota"] = np.zeros(num_pts, dtype=np.float_) - point_data["q"] = np.zeros(num_pts, dtype=np.float_) - point_data["det"] = np.zeros(num_pts, dtype=np.float_) - point_data["det/(2pi)^2"] = np.zeros(num_pts, dtype=np.float_) - point_data["A"] = np.zeros((num_pts, 3), dtype=np.float_) - point_data["A_vec"] = np.zeros((num_pts, 3), dtype=np.float_) - point_data["A_1"] = np.zeros((num_pts, 3), dtype=np.float_) - point_data["A_2"] = np.zeros((num_pts, 3), dtype=np.float_) - point_data["B"] = np.zeros((num_pts, 3), dtype=np.float_) - point_data["B_vec"] = np.zeros((num_pts, 3), dtype=np.float_) - point_data["B_1"] = np.zeros((num_pts, 3), dtype=np.float_) - point_data["B_2"] = np.zeros((num_pts, 3), dtype=np.float_) + point_data["s"] = xp.zeros(num_pts, dtype=xp.float_) + point_data["u"] = xp.zeros(num_pts, dtype=xp.float_) + point_data["v"] = xp.zeros(num_pts, dtype=xp.float_) + point_data["x"] = xp.zeros(num_pts, dtype=xp.float_) + point_data["y"] = xp.zeros(num_pts, dtype=xp.float_) + point_data["z"] = xp.zeros(num_pts, dtype=xp.float_) + point_data["theta"] = xp.zeros(num_pts, dtype=xp.float_) + point_data["zeta"] = xp.zeros(num_pts, dtype=xp.float_) + point_data["Point ID"] = xp.zeros(num_pts, dtype=xp.int_) + point_data["pressure"] = xp.zeros(num_pts, dtype=xp.float_) + point_data["phi"] = xp.zeros(num_pts, dtype=xp.float_) + point_data["chi"] = xp.zeros(num_pts, dtype=xp.float_) + point_data["iota"] = xp.zeros(num_pts, dtype=xp.float_) + point_data["q"] = xp.zeros(num_pts, dtype=xp.float_) + point_data["det"] = xp.zeros(num_pts, dtype=xp.float_) + point_data["det/(2pi)^2"] = xp.zeros(num_pts, dtype=xp.float_) + point_data["A"] = xp.zeros((num_pts, 3), dtype=xp.float_) + point_data["A_vec"] = xp.zeros((num_pts, 3), dtype=xp.float_) + point_data["A_1"] = xp.zeros((num_pts, 3), dtype=xp.float_) + point_data["A_2"] = xp.zeros((num_pts, 3), dtype=xp.float_) + point_data["B"] = xp.zeros((num_pts, 3), dtype=xp.float_) + point_data["B_vec"] = xp.zeros((num_pts, 3), dtype=xp.float_) + point_data["B_1"] = xp.zeros((num_pts, 3), dtype=xp.float_) + point_data["B_2"] = xp.zeros((num_pts, 3), dtype=xp.float_) # pbar = tqdm(total=num_pts) for s_idx, s in enumerate(s_range): for u_idx, u in enumerate(u_range): for v_idx, v in enumerate(v_range): point = gvec.f(s, u, v) - suv_points[s_idx, u_idx, v_idx, :] = np.array([s, u, v]) + suv_points[s_idx, u_idx, v_idx, :] = xp.array([s, u, v]) xyz_points[s_idx, u_idx, v_idx, :] = point point_indices[s_idx, u_idx, v_idx] = pt_idx vtk_points.InsertPoint(pt_idx, point) @@ -149,10 +148,10 @@ def gen_vtk_points(gvec, s_range, u_range, v_range, point_data, cell_data): pt_idx += 1 # pbar.close() - point_data["theta"] = 2 * np.pi * point_data["u"] - point_data["zeta"] = 2 * np.pi * point_data["v"] + point_data["theta"] = 2 * xp.pi * point_data["u"] + point_data["zeta"] = 2 * xp.pi * point_data["v"] point_data["q"] = 1 / point_data["iota"] - point_data["det/(2pi)^2"] = point_data["det"] / (2 * np.pi) ** 2 + point_data["det/(2pi)^2"] = point_data["det"] / (2 * xp.pi) ** 2 return vtk_points, suv_points, xyz_points, point_indices @@ -312,4 +311,4 @@ def connect_cell(s_range, u_range, v_range, point_indices, ugrid, point_data, ce cell_data["Cell ID"].append(cell_idx) cell_idx += 1 - cell_data["Cell ID"] = np.array(cell_data["Cell ID"], dtype=np.int_) + cell_data["Cell ID"] = xp.array(cell_data["Cell ID"], dtype=xp.int_) diff --git a/src/struphy/dispersion_relations/analytic.py b/src/struphy/dispersion_relations/analytic.py index 8a87a65bb..fb7a40ccd 100644 --- a/src/struphy/dispersion_relations/analytic.py +++ b/src/struphy/dispersion_relations/analytic.py @@ -1,12 +1,12 @@ "Analytic dispersion relations." +import cunumpy as xp from numpy.polynomial import Polynomial from scipy.optimize import fsolve from struphy.dispersion_relations.base import ContinuousSpectra1D, DispersionRelations1D from struphy.dispersion_relations.utilities import Zplasma from struphy.fields_background.equils import set_defaults -from struphy.utils.arrays import xp as np class Maxwell1D(DispersionRelations1D): @@ -108,18 +108,18 @@ def __call__(self, k): Bsquare = self.params["B0x"] ** 2 + self.params["B0y"] ** 2 + self.params["B0z"] ** 2 # Alfvén velocity and speed of sound - vA = np.sqrt(Bsquare / self.params["n0"]) + vA = xp.sqrt(Bsquare / self.params["n0"]) - cS = np.sqrt(self.params["gamma"] * self.params["p0"] / self.params["n0"]) + cS = xp.sqrt(self.params["gamma"] * self.params["p0"] / self.params["n0"]) # shear Alfvén branch - self._branches["shear Alfvén"] = vA * k * self.params["B0z"] / np.sqrt(Bsquare) + self._branches["shear Alfvén"] = vA * k * self.params["B0z"] / xp.sqrt(Bsquare) # slow/fast magnetosonic branch delta = (4 * self.params["B0z"] ** 2 * cS**2 * vA**2) / ((cS**2 + vA**2) ** 2 * Bsquare) - self._branches["slow magnetosonic"] = np.sqrt(1 / 2 * k**2 * (cS**2 + vA**2) * (1 - np.sqrt(1 - delta))) - self._branches["fast magnetosonic"] = np.sqrt(1 / 2 * k**2 * (cS**2 + vA**2) * (1 + np.sqrt(1 - delta))) + self._branches["slow magnetosonic"] = xp.sqrt(1 / 2 * k**2 * (cS**2 + vA**2) * (1 - xp.sqrt(1 - delta))) + self._branches["fast magnetosonic"] = xp.sqrt(1 / 2 * k**2 * (cS**2 + vA**2) * (1 + xp.sqrt(1 - delta))) return self.branches @@ -186,14 +186,14 @@ def __call__(self, k): Bsquare = self.params["B0x"] ** 2 + self.params["B0y"] ** 2 + self.params["B0z"] ** 2 - cos_theta = self.params["B0z"] / np.sqrt(Bsquare) + cos_theta = self.params["B0z"] / xp.sqrt(Bsquare) # Alfvén velocity, speed of sound and cyclotron frequency - vA = np.sqrt(Bsquare / self.params["n0"]) + vA = xp.sqrt(Bsquare / self.params["n0"]) - cS = np.sqrt(self.params["gamma"] * self.params["p0"] / self.params["n0"]) + cS = xp.sqrt(self.params["gamma"] * self.params["p0"] / self.params["n0"]) - Omega_i = np.sqrt(Bsquare) / self.params["eps"] + Omega_i = xp.sqrt(Bsquare) / self.params["eps"] # auxiliary functions def omega_0(k): @@ -218,7 +218,7 @@ def discriminant(k): ) # solve - out = np.zeros((k.size, 4), dtype=complex) + out = xp.zeros((k.size, 4), dtype=complex) for i, ki in enumerate(k): p0 = Polynomial([-(omega_0(ki) ** 2), 1.0]) p1 = Polynomial([d(ki), c(ki), b(ki), 1.0]) @@ -302,7 +302,7 @@ def discriminant(k): return -4.0 * p**3 - 27.0 * q(k) ** 2 # solve - out = np.zeros((k.size, 3), dtype=complex) + out = xp.zeros((k.size, 3), dtype=complex) for i, ki in enumerate(k): poly = Polynomial([q(ki), p, 0.0, 1.0]) out[i] = poly.roots() @@ -342,17 +342,17 @@ def __call__(self, kvec): # One complex array for each branch tmps = [] for n in range(self.nbranches): - tmps += [np.zeros_like(kvec, dtype=complex)] + tmps += [xp.zeros_like(kvec, dtype=complex)] ########### Model specific part ############################## # angle between k and magnetic field if self.params["B0z"] == 0: - theta = np.pi / 2 + theta = xp.pi / 2 else: - theta = np.arctan(np.sqrt(self.params["B0x"] ** 2 + self.params["B0y"] ** 2) / self.params["B0z"]) + theta = xp.arctan(xp.sqrt(self.params["B0x"] ** 2 + self.params["B0y"] ** 2) / self.params["B0z"]) print(theta) - cos2 = np.cos(theta) ** 2 + cos2 = xp.cos(theta) ** 2 neq = self.params["n0"] @@ -393,10 +393,10 @@ def __call__(self, kvec): e = eps6 # determinant in polynomial form - det = np.polynomial.Polynomial([a, b, c, d, e]) + det = xp.polynomial.Polynomial([a, b, c, d, e]) # solutions - sol = np.sqrt(np.abs(det.roots())) + sol = xp.sqrt(xp.abs(det.roots())) # Ion-cyclotron branch tmps[0][n] = sol[0] # Electron-cyclotron branch @@ -489,7 +489,7 @@ def __init__(self, **params): ee = 1.602176634e-19 # calculate coupling parameter alpha_c from bulk number density and mass number - self._kappa = ee * np.sqrt(mu * self.params["Ab"] * self.params["nb"] * 1e20 / mp) + self._kappa = ee * xp.sqrt(mu * self.params["Ab"] * self.params["nb"] * 1e20 / mp) def __call__(self, k, method="newton", tol=1e-10, max_it=100): """ @@ -518,7 +518,7 @@ def __call__(self, k, method="newton", tol=1e-10, max_it=100): # One complex array for each branch tmps = [] for _ in range(self.nbranches): - tmps += [np.zeros_like(k, dtype=complex)] + tmps += [xp.zeros_like(k, dtype=complex)] ########### Model specific part ############################## @@ -532,8 +532,8 @@ def __call__(self, k, method="newton", tol=1e-10, max_it=100): wR = [self.params["B0"] * ki, 0.0] wL = [self.params["B0"] * ki, 0.0] else: - wR = [np.real(tmps[0][i - 1]), np.imag(tmps[0][i - 1])] - wL = [np.real(tmps[1][i - 1]), np.imag(tmps[1][i - 1])] + wR = [xp.real(tmps[0][i - 1]), xp.imag(tmps[0][i - 1])] + wL = [xp.real(tmps[1][i - 1]), xp.imag(tmps[1][i - 1])] # apply solver if method == "newton": @@ -542,13 +542,13 @@ def __call__(self, k, method="newton", tol=1e-10, max_it=100): Dr, Di = self.D_RL(wR, ki, +1) - while np.abs(Dr + Di * 1j) > tol or counter == max_it: + while xp.abs(Dr + Di * 1j) > tol or counter == max_it: # derivative Drp, Dip = self.D_RL(wR, ki, +1, 1) # update - wR[0] = wR[0] - np.real((Dr + Di * 1j) / (Drp + Dip * 1j)) - wR[1] = wR[1] - np.imag((Dr + Di * 1j) / (Drp + Dip * 1j)) + wR[0] = wR[0] - xp.real((Dr + Di * 1j) / (Drp + Dip * 1j)) + wR[1] = wR[1] - xp.imag((Dr + Di * 1j) / (Drp + Dip * 1j)) Dr, Di = self.D_RL(wR, ki, +1) counter += 1 @@ -558,13 +558,13 @@ def __call__(self, k, method="newton", tol=1e-10, max_it=100): Dr, Di = self.D_RL(wL, ki, -1) - while np.abs(Dr + Di * 1j) > tol or counter == max_it: + while xp.abs(Dr + Di * 1j) > tol or counter == max_it: # derivative Drp, Dip = self.D_RL(wL, ki, -1, 1) # update - wL[0] = wL[0] - np.real((Dr + Di * 1j) / (Drp + Dip * 1j)) - wL[1] = wL[1] - np.imag((Dr + Di * 1j) / (Drp + Dip * 1j)) + wL[0] = wL[0] - xp.real((Dr + Di * 1j) / (Drp + Dip * 1j)) + wL[1] = wL[1] - xp.imag((Dr + Di * 1j) / (Drp + Dip * 1j)) Dr, Di = self.D_RL(wL, ki, -1) counter += 1 @@ -651,7 +651,7 @@ def D_RL(self, w, k, pol, der=0): * (Zplasma(xi, 0) + (w - k * v0) * Zplasma(xi, 1) * xip) ) - return np.real(out), np.imag(out) + return xp.real(out), xp.imag(out) class PressureCouplingFull6DParallel(DispersionRelations1D): @@ -723,7 +723,7 @@ def __call__(self, k, tol=1e-10): # One complex array for each branch tmps = [] for n in range(self.nbranches): - tmps += [np.zeros_like(k, dtype=complex)] + tmps += [xp.zeros_like(k, dtype=complex)] ########### Model specific part ############################## @@ -735,9 +735,9 @@ def __call__(self, k, tol=1e-10): wL = [1 * ki, 0.0] # TODO: use vA wS = [1 * ki, 0.0] # TODO: use cS else: - wR = [np.real(tmps[0][i - 1]), np.imag(tmps[0][i - 1])] - wL = [np.real(tmps[1][i - 1]), np.imag(tmps[1][i - 1])] - wS = [np.real(tmps[2][i - 1]), np.imag(tmps[2][i - 1])] + wR = [xp.real(tmps[0][i - 1]), xp.imag(tmps[0][i - 1])] + wL = [xp.real(tmps[1][i - 1]), xp.imag(tmps[1][i - 1])] + wS = [xp.real(tmps[2][i - 1]), xp.imag(tmps[2][i - 1])] # R/L shear Alfvén wave sol_R = fsolve(self.D_RL, x0=wR, args=(ki, +1), xtol=tol) @@ -796,8 +796,8 @@ def D_RL(self, w, k, pol): vperp = 1.0 # TODO vth = 1.0 - vA = np.sqrt((self.params["B0x"] ** 2 + self.params["B0y"] ** 2 + self.params["B0z"] ** 2) / self.params["n0"]) - # cS = np.sqrt(self.params['beta']*vA) + vA = xp.sqrt((self.params["B0x"] ** 2 + self.params["B0y"] ** 2 + self.params["B0z"] ** 2) / self.params["n0"]) + # cS = xp.sqrt(self.params['beta']*vA) cS = 1.0 a0 = u0 / vpara # TODO @@ -840,7 +840,7 @@ def D_RL(self, w, k, pol): ) ) - return np.real(c1), np.imag(c1) + return xp.real(c1), xp.imag(c1) def D_sonic(self, w, k): r""" @@ -873,8 +873,8 @@ def D_sonic(self, w, k): vperp = 1.0 # TODO vth = 1.0 - vA = np.sqrt((self.params["B0x"] ** 2 + self.params["B0y"] ** 2 + self.params["B0z"] ** 2) / self.params["n0"]) - # cS = np.sqrt(self.params['beta']*vA) + vA = xp.sqrt((self.params["B0x"] ** 2 + self.params["B0y"] ** 2 + self.params["B0z"] ** 2) / self.params["n0"]) + # cS = xp.sqrt(self.params['beta']*vA) cS = 1.0 a0 = u0 / vpara # TODO @@ -885,7 +885,7 @@ def D_sonic(self, w, k): c1 = w**2 - k**2 * cS**2 + 2 * w * k * nu * vpara * x4 - return np.real(c1), np.imag(c1) + return xp.real(c1), xp.imag(c1) # private methods: # ---------------- @@ -1014,10 +1014,10 @@ def __call__(self, x, m, n): specs = {} # shear Alfvén continuum - specs["shear_Alfvén"] = np.sqrt(F(x, m, n) ** 2 / rho(x)) + specs["shear_Alfvén"] = xp.sqrt(F(x, m, n) ** 2 / rho(x)) # slow sound continuum - specs["slow_sound"] = np.sqrt( + specs["slow_sound"] = xp.sqrt( gamma * p(x) * F(x, m, n) ** 2 / (rho(x) * (gamma * p(x) + By(x) ** 2 + Bz(x) ** 2)) ) @@ -1121,10 +1121,10 @@ def __call__(self, r, m, n): specs = {} # shear Alfvén continuum - specs["shear_Alfvén"] = np.sqrt(F(r, m, n) ** 2 / rho(r)) + specs["shear_Alfvén"] = xp.sqrt(F(r, m, n) ** 2 / rho(r)) # slow sound continuum - specs["slow_sound"] = np.sqrt( + specs["slow_sound"] = xp.sqrt( gamma * p(r) * F(r, m, n) ** 2 / (rho(r) * (gamma * p(r) + Bt(r) ** 2 + Bz(r) ** 2)) ) diff --git a/src/struphy/dispersion_relations/base.py b/src/struphy/dispersion_relations/base.py index 31a237a90..6994ae2fb 100644 --- a/src/struphy/dispersion_relations/base.py +++ b/src/struphy/dispersion_relations/base.py @@ -2,10 +2,9 @@ from abc import ABCMeta, abstractmethod +import cunumpy as xp from matplotlib import pyplot as plt -from struphy.utils.arrays import xp as np - class DispersionRelations1D(metaclass=ABCMeta): r""" @@ -100,18 +99,18 @@ def plot(self, k): plt.ylabel(rf"Im($\omega$) [{unit_om}]") for name, omega in self.branches.items(): plt.subplot(2, 1, 1) - plt.plot(k, np.real(omega), label=name) + plt.plot(k, xp.real(omega), label=name) plt.subplot(2, 1, 2) - plt.plot(k, np.imag(omega), label=name) + plt.plot(k, xp.imag(omega), label=name) plt.subplot(2, 1, 1) for lab, kc in self.k_crit.items(): - if kc > np.min(k) and kc < np.max(k): + if kc > xp.min(k) and kc < xp.max(k): plt.axvline(kc, color="k", linestyle="--", linewidth=0.5, label=lab) plt.legend() plt.subplot(2, 1, 2) for lab, kc in self.k_crit.items(): - if kc > np.min(k) and kc < np.max(k): + if kc > xp.min(k) and kc < xp.max(k): plt.axvline(kc, color="k", linestyle="--", linewidth=0.5, label=lab) diff --git a/src/struphy/dispersion_relations/utilities.py b/src/struphy/dispersion_relations/utilities.py index b0f74fbb4..b796eb321 100644 --- a/src/struphy/dispersion_relations/utilities.py +++ b/src/struphy/dispersion_relations/utilities.py @@ -1,7 +1,6 @@ +import cunumpy as xp from scipy.special import erfi -from struphy.utils.arrays import xp as np - def Zplasma(xi, der=0): """ @@ -24,7 +23,7 @@ def Zplasma(xi, der=0): assert der == 0 or der == 1, 'Parameter "der" must be either 0 or 1' if der == 0: - z = np.sqrt(np.pi) * np.exp(-(xi**2)) * (1j - erfi(xi)) + z = xp.sqrt(xp.pi) * xp.exp(-(xi**2)) * (1j - erfi(xi)) else: z = -2 * (1 + xi * Zplasma(xi, 0)) diff --git a/src/struphy/eigenvalue_solvers/derivatives.py b/src/struphy/eigenvalue_solvers/derivatives.py index 45d8e2d94..0e34cceb1 100644 --- a/src/struphy/eigenvalue_solvers/derivatives.py +++ b/src/struphy/eigenvalue_solvers/derivatives.py @@ -6,10 +6,9 @@ Modules to assemble discrete derivatives. """ +import cunumpy as xp import scipy.sparse as spa -from struphy.utils.arrays import xp as np - # ================== 1d incident matrix ======================= def grad_1d_matrix(spl_kind, NbaseN): @@ -32,7 +31,7 @@ def grad_1d_matrix(spl_kind, NbaseN): NbaseD = NbaseN - 1 + spl_kind - grad = np.zeros((NbaseD, NbaseN), dtype=float) + grad = xp.zeros((NbaseD, NbaseN), dtype=float) for i in range(NbaseD): grad[i, i] = -1.0 @@ -80,9 +79,9 @@ def discrete_derivatives_3d(space): grad_1d_3 = 0 * spa.identity(1, format="csr") else: if space.basis_tor == "r": - grad_1d_3 = 2 * np.pi * space.n_tor * spa.csr_matrix(np.array([[0.0, 1.0], [-1.0, 0.0]])) + grad_1d_3 = 2 * xp.pi * space.n_tor * spa.csr_matrix(xp.array([[0.0, 1.0], [-1.0, 0.0]])) else: - grad_1d_3 = 1j * 2 * np.pi * space.n_tor * spa.identity(1, format="csr") + grad_1d_3 = 1j * 2 * xp.pi * space.n_tor * spa.identity(1, format="csr") # standard tensor-product derivatives if space.ck == -1: diff --git a/src/struphy/eigenvalue_solvers/legacy/MHD_eigenvalues_cylinder_1D.py b/src/struphy/eigenvalue_solvers/legacy/MHD_eigenvalues_cylinder_1D.py index e60d565fb..9fbc38ce8 100644 --- a/src/struphy/eigenvalue_solvers/legacy/MHD_eigenvalues_cylinder_1D.py +++ b/src/struphy/eigenvalue_solvers/legacy/MHD_eigenvalues_cylinder_1D.py @@ -1,3 +1,4 @@ +import cunumpy as xp import scipy as sc import scipy.sparse as spa import scipy.special as sp @@ -9,7 +10,6 @@ import struphy.eigenvalue_solvers.mass_matrices_1d as mass import struphy.eigenvalue_solvers.projectors_global as pro import struphy.eigenvalue_solvers.spline_space as spl -from struphy.utils.arrays import xp as np # numerical solution of the general ideal MHD eigenvalue problem in a cylinder using 1d B-splines in radial direction @@ -21,7 +21,7 @@ def solve_ev_problem(rho, B_phi, dB_phi, B_z, p, gamma, a, k, m, num_params, bcZ r = lambda eta: a * eta # jacobian for integration - jac = lambda eta1: a * np.ones(eta1.shape, dtype=float) + jac = lambda eta1: a * xp.ones(eta1.shape, dtype=float) # ========================== kinetic energy functional ============================== # integrands (multiplied by -2/omega**2) @@ -46,11 +46,11 @@ def solve_ev_problem(rho, B_phi, dB_phi, B_z, p, gamma, a, k, m, num_params, bcZ # Bspline_A = Bsp.Bspline(splines.T, splines.p ) # Bspline_B = Bsp.Bspline(splines.t, splines.p - 1) # - # K_11_scipy = np.zeros((splines.NbaseN, splines.NbaseN), dtype=float) - # K_22_scipy = np.zeros((splines.NbaseD, splines.NbaseD), dtype=float) - # K_33_scipy = np.zeros((splines.NbaseD, splines.NbaseD), dtype=float) - # K_23_scipy = np.zeros((splines.NbaseD, splines.NbaseD), dtype=float) - # K_32_scipy = np.zeros((splines.NbaseD, splines.NbaseD), dtype=float) + # K_11_scipy = xp.zeros((splines.NbaseN, splines.NbaseN), dtype=float) + # K_22_scipy = xp.zeros((splines.NbaseD, splines.NbaseD), dtype=float) + # K_33_scipy = xp.zeros((splines.NbaseD, splines.NbaseD), dtype=float) + # K_23_scipy = xp.zeros((splines.NbaseD, splines.NbaseD), dtype=float) + # K_32_scipy = xp.zeros((splines.NbaseD, splines.NbaseD), dtype=float) # # for i in range(1, Bspline_A.N - 1): # for j in range(1, Bspline_A.N - 1): @@ -76,11 +76,11 @@ def solve_ev_problem(rho, B_phi, dB_phi, B_z, p, gamma, a, k, m, num_params, bcZ # integrand = lambda eta : a*K_ZV(eta)*Bspline_B(eta, i)*Bspline_B(eta, j) # K_32_scipy[i, j] = integrate.quad(integrand, 0., 1.)[0] - # assert np.allclose(K_11.toarray(), K_11_scipy[1:-1, 1:-1]) - # assert np.allclose(K_22.toarray(), K_22_scipy ) - # assert np.allclose(K_33.toarray(), K_33_scipy[bcZ:, bcZ:]) - # assert np.allclose(K_23.toarray(), K_23_scipy[ : , bcZ:]) - # assert np.allclose(K_32.toarray(), K_32_scipy[bcZ:, :]) + # assert xp.allclose(K_11.toarray(), K_11_scipy[1:-1, 1:-1]) + # assert xp.allclose(K_22.toarray(), K_22_scipy ) + # assert xp.allclose(K_33.toarray(), K_33_scipy[bcZ:, bcZ:]) + # assert xp.allclose(K_23.toarray(), K_23_scipy[ : , bcZ:]) + # assert xp.allclose(K_32.toarray(), K_32_scipy[bcZ:, :]) # ========================== potential energy functional =========================== # integrands (multiplied by 2) @@ -163,15 +163,15 @@ def solve_ev_problem(rho, B_phi, dB_phi, B_z, p, gamma, a, k, m, num_params, bcZ # return W_22 ## test correct computation - # W_11_scipy = np.zeros((splines.NbaseN, splines.NbaseN), dtype=float) - # W_22_scipy = np.zeros((splines.NbaseD, splines.NbaseD), dtype=float) - # W_33_scipy = np.zeros((splines.NbaseD, splines.NbaseD), dtype=float) - # W_12_scipy = np.zeros((splines.NbaseN, splines.NbaseD), dtype=float) - # W_21_scipy = np.zeros((splines.NbaseD, splines.NbaseN), dtype=float) - # W_13_scipy = np.zeros((splines.NbaseN, splines.NbaseD), dtype=float) - # W_31_scipy = np.zeros((splines.NbaseD, splines.NbaseN), dtype=float) - # W_23_scipy = np.zeros((splines.NbaseD, splines.NbaseD), dtype=float) - # W_32_scipy = np.zeros((splines.NbaseD, splines.NbaseD), dtype=float) + # W_11_scipy = xp.zeros((splines.NbaseN, splines.NbaseN), dtype=float) + # W_22_scipy = xp.zeros((splines.NbaseD, splines.NbaseD), dtype=float) + # W_33_scipy = xp.zeros((splines.NbaseD, splines.NbaseD), dtype=float) + # W_12_scipy = xp.zeros((splines.NbaseN, splines.NbaseD), dtype=float) + # W_21_scipy = xp.zeros((splines.NbaseD, splines.NbaseN), dtype=float) + # W_13_scipy = xp.zeros((splines.NbaseN, splines.NbaseD), dtype=float) + # W_31_scipy = xp.zeros((splines.NbaseD, splines.NbaseN), dtype=float) + # W_23_scipy = xp.zeros((splines.NbaseD, splines.NbaseD), dtype=float) + # W_32_scipy = xp.zeros((splines.NbaseD, splines.NbaseD), dtype=float) # # for i in range(1, Bspline_A.N - 1): # for j in range(1, Bspline_A.N - 1): @@ -187,15 +187,15 @@ def solve_ev_problem(rho, B_phi, dB_phi, B_z, p, gamma, a, k, m, num_params, bcZ # integrand = lambda eta : W_XdX(eta) * Bspline_A(eta, i, 0) * Bspline_A(eta, j, 1) # W_11_scipy[i, j] += integrate.quad(integrand, 0., 1.)[0] # - # assert np.allclose(W_11.toarray(), W_11_scipy[1:-1, 1:-1]) + # assert xp.allclose(W_11.toarray(), W_11_scipy[1:-1, 1:-1]) - # print(np.allclose(K, K.T)) - # print(np.allclose(W, W.T)) + # print(xp.allclose(K, K.T)) + # print(xp.allclose(W, W.T)) # solve eigenvalue problem omega**2*K*xi = W*xi - A = np.linalg.inv(K).dot(W) + A = xp.linalg.inv(K).dot(W) - omega2, XVZ_eig = np.linalg.eig(A) + omega2, XVZ_eig = xp.linalg.eig(A) # extract components X_eig = XVZ_eig[: (splines.NbaseN - 2), :] @@ -203,11 +203,11 @@ def solve_ev_problem(rho, B_phi, dB_phi, B_z, p, gamma, a, k, m, num_params, bcZ Z_eig = XVZ_eig[(splines.NbaseN - 2 + splines.NbaseD) :, :] # add boundary conditions X(0) = X(1) = 0 - X_eig = np.vstack((np.zeros(X_eig.shape[1], dtype=float), X_eig, np.zeros(X_eig.shape[1], dtype=float))) + X_eig = xp.vstack((xp.zeros(X_eig.shape[1], dtype=float), X_eig, xp.zeros(X_eig.shape[1], dtype=float))) # add boundary condition Z(0) = 0 if bcZ == 1: - Z_eig = np.vstack((np.zeros(Z_eig.shape[1], dtype=float), Z_eig)) + Z_eig = xp.vstack((xp.zeros(Z_eig.shape[1], dtype=float), Z_eig)) return omega2, X_eig, V_eig, Z_eig @@ -225,43 +225,43 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, # components of metric tensor and Jacobian determinant G_r = a**2 - G_phi = lambda eta: 4 * np.pi**2 * r(eta) ** 2 - G_z = 4 * np.pi**2 * R0**2 - J = lambda eta: 4 * np.pi**2 * R0 * a * r(eta) + G_phi = lambda eta: 4 * xp.pi**2 * r(eta) ** 2 + G_z = 4 * xp.pi**2 * R0**2 + J = lambda eta: 4 * xp.pi**2 * R0 * a * r(eta) # 2-from components of equilibrium magnetic field and its projection - B2_phi = lambda eta: 2 * np.pi * R0 * a * B_phi(r(eta)) - B2_z = lambda eta: 2 * np.pi * a * r(eta) * B_z(r(eta)) + B2_phi = lambda eta: 2 * xp.pi * R0 * a * B_phi(r(eta)) + B2_z = lambda eta: 2 * xp.pi * a * r(eta) * B_z(r(eta)) - b2_eq_phi = np.linalg.solve(proj.D.toarray(), proj.rhs_1(B2_phi)) - b2_eq_z = np.append(np.array([0.0]), np.linalg.solve(proj.D.toarray()[1:, 1:], proj.rhs_1(B2_z)[1:])) + b2_eq_phi = xp.linalg.solve(proj.D.toarray(), proj.rhs_1(B2_phi)) + b2_eq_z = xp.append(xp.array([0.0]), xp.linalg.solve(proj.D.toarray()[1:, 1:], proj.rhs_1(B2_z)[1:])) # 3-form components of equilibrium density and pessure and its projection Rho3 = lambda eta: J(eta) * Rho(r(eta)) P3 = lambda eta: J(eta) * P(r(eta)) - rho3_eq = np.append(np.array([0.0]), np.linalg.solve(proj.D.toarray()[1:, 1:], proj.rhs_1(Rho3)[1:])) - p3_eq = np.append(np.array([0.0]), np.linalg.solve(proj.D.toarray()[1:, 1:], proj.rhs_1(P3)[1:])) + rho3_eq = xp.append(xp.array([0.0]), xp.linalg.solve(proj.D.toarray()[1:, 1:], proj.rhs_1(Rho3)[1:])) + p3_eq = xp.append(xp.array([0.0]), xp.linalg.solve(proj.D.toarray()[1:, 1:], proj.rhs_1(P3)[1:])) # 2-form components of initial velocity and its projection U2_r = lambda eta: J(eta) * eta * (1 - eta) u2_r = proj.pi_0(U2_r) - u2_phi = -1 / (2 * np.pi * m) * GRAD_all.dot(u2_r) - u2_z = np.zeros(len(u2_phi), dtype=float) + u2_phi = -1 / (2 * xp.pi * m) * GRAD_all.dot(u2_r) + u2_z = xp.zeros(len(u2_phi), dtype=float) - b2_r = np.zeros(len(u2_r), dtype=float) - b2_phi = np.zeros(len(u2_phi), dtype=float) - b2_z = np.zeros(len(u2_z), dtype=float) + b2_r = xp.zeros(len(u2_r), dtype=float) + b2_phi = xp.zeros(len(u2_phi), dtype=float) + b2_z = xp.zeros(len(u2_z), dtype=float) - p3 = np.zeros(len(u2_z), dtype=float) + p3 = xp.zeros(len(u2_z), dtype=float) # projection matrices pi0_N_i, pi0_D_i, pi1_N_i, pi1_D_i = proj.projection_matrices_1d_reduced() pi0_NN_i, pi0_DN_i, pi0_ND_i, pi0_DD_i, pi1_NN_i, pi1_DN_i, pi1_ND_i, pi1_DD_i = proj.projection_matrices_1d() # 1D collocation matrices for interpolation in format (point, global basis function) - x_int = np.copy(proj.x_int) + x_int = xp.copy(proj.x_int) kind_splines = [False, True] @@ -270,22 +270,22 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, # 1D integration sub-intervals, quadrature points and weights if splines.p % 2 == 0: - x_his = np.union1d(x_int, splines.el_b) + x_his = xp.union1d(x_int, splines.el_b) else: - x_his = np.copy(x_int) + x_his = xp.copy(x_int) pts, wts = bsp.quadrature_grid(x_his, proj.pts_loc, proj.wts_loc) # compute number of sub-intervals for integrations (even degree) if splines.p % 2 == 0: - subs = 2 * np.ones(proj.pts.shape[0], dtype=int) + subs = 2 * xp.ones(proj.pts.shape[0], dtype=int) subs[: splines.p // 2] = 1 subs[-splines.p // 2 :] = 1 # compute number of sub-intervals for integrations (odd degree) else: - subs = np.ones(proj.pts.shape[0], dtype=int) + subs = xp.ones(proj.pts.shape[0], dtype=int) # evaluate basis functions on quadrature points in format (interval, local quad. point, global basis function) basis_his_N = bsp.collocation_matrix(splines.T, splines.p, pts.flatten(), False, normalize=kind_splines[0]).reshape( @@ -308,26 +308,26 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, M2_z = mass.get_M1(splines, mapping=lambda eta: J(eta) / G_z).toarray() # === matrices for curl of equilibrium field (with integration by parts) ========== - MB_12_eq = np.empty((splines.NbaseN, splines.NbaseD), dtype=float) - MB_13_eq = np.empty((splines.NbaseN, splines.NbaseD), dtype=float) + MB_12_eq = xp.empty((splines.NbaseN, splines.NbaseD), dtype=float) + MB_13_eq = xp.empty((splines.NbaseN, splines.NbaseD), dtype=float) - MB_21_eq = np.empty((splines.NbaseD, splines.NbaseN), dtype=float) - MB_31_eq = np.empty((splines.NbaseD, splines.NbaseN), dtype=float) + MB_21_eq = xp.empty((splines.NbaseD, splines.NbaseN), dtype=float) + MB_31_eq = xp.empty((splines.NbaseD, splines.NbaseN), dtype=float) - f_phi = np.linalg.inv(proj.N.toarray()).T.dot(GRAD_all.T.dot(M2_phi.dot(b2_eq_phi))) - f_z = np.linalg.inv(proj.N.toarray()).T.dot(GRAD_all.T.dot(M2_z.dot(b2_eq_z))) + f_phi = xp.linalg.inv(proj.N.toarray()).T.dot(GRAD_all.T.dot(M2_phi.dot(b2_eq_phi))) + f_z = xp.linalg.inv(proj.N.toarray()).T.dot(GRAD_all.T.dot(M2_z.dot(b2_eq_z))) - pi0_ND_phi = np.empty(pi0_ND_i[3].max() + 1, dtype=float) - pi0_ND_z = np.empty(pi0_ND_i[3].max() + 1, dtype=float) + pi0_ND_phi = xp.empty(pi0_ND_i[3].max() + 1, dtype=float) + pi0_ND_z = xp.empty(pi0_ND_i[3].max() + 1, dtype=float) - row_ND = np.empty(pi0_ND_i[3].max() + 1, dtype=int) - col_ND = np.empty(pi0_ND_i[3].max() + 1, dtype=int) + row_ND = xp.empty(pi0_ND_i[3].max() + 1, dtype=int) + col_ND = xp.empty(pi0_ND_i[3].max() + 1, dtype=int) - pi0_DN_phi = np.empty(pi0_DN_i[3].max() + 1, dtype=float) - pi0_DN_z = np.empty(pi0_DN_i[3].max() + 1, dtype=float) + pi0_DN_phi = xp.empty(pi0_DN_i[3].max() + 1, dtype=float) + pi0_DN_z = xp.empty(pi0_DN_i[3].max() + 1, dtype=float) - row_DN = np.empty(pi0_DN_i[3].max() + 1, dtype=int) - col_DN = np.empty(pi0_DN_i[3].max() + 1, dtype=int) + row_DN = xp.empty(pi0_DN_i[3].max() + 1, dtype=int) + col_DN = xp.empty(pi0_DN_i[3].max() + 1, dtype=int) ker.rhs0_f_1d(pi0_ND_i, basis_int_N, basis_int_D, 1 / J(x_int), f_phi, pi0_ND_phi, row_ND, col_ND) ker.rhs0_f_1d(pi0_ND_i, basis_int_N, basis_int_D, 1 / J(x_int), f_z, pi0_ND_z, row_ND, col_ND) @@ -348,23 +348,23 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, MB_31_eq[:, :] = -pi0_DN_z # === matrices for curl of equilibrium field (without integration by parts) ====== - MB_12_eq = np.empty((splines.NbaseN, splines.NbaseD), dtype=float) - MB_13_eq = np.empty((splines.NbaseN, splines.NbaseD), dtype=float) + MB_12_eq = xp.empty((splines.NbaseN, splines.NbaseD), dtype=float) + MB_13_eq = xp.empty((splines.NbaseN, splines.NbaseD), dtype=float) - MB_21_eq = np.empty((splines.NbaseD, splines.NbaseN), dtype=float) - MB_31_eq = np.empty((splines.NbaseD, splines.NbaseN), dtype=float) + MB_21_eq = xp.empty((splines.NbaseD, splines.NbaseN), dtype=float) + MB_31_eq = xp.empty((splines.NbaseD, splines.NbaseN), dtype=float) - cN = np.empty(splines.NbaseN, dtype=float) - cD = np.empty(splines.NbaseD, dtype=float) + cN = xp.empty(splines.NbaseN, dtype=float) + cD = xp.empty(splines.NbaseD, dtype=float) for j in range(splines.NbaseD): cD[:] = 0.0 cD[j] = 1.0 integrand2 = ( - lambda eta: splines.evaluate_D(eta, cD) / J(eta) * 2 * np.pi * a * (B_phi(r(eta)) + r(eta) * dB_phi(r(eta))) + lambda eta: splines.evaluate_D(eta, cD) / J(eta) * 2 * xp.pi * a * (B_phi(r(eta)) + r(eta) * dB_phi(r(eta))) ) - integrand3 = lambda eta: splines.evaluate_D(eta, cD) / J(eta) * 2 * np.pi * a * R0 * dB_z(r(eta)) + integrand3 = lambda eta: splines.evaluate_D(eta, cD) / J(eta) * 2 * xp.pi * a * R0 * dB_z(r(eta)) MB_12_eq[:, j] = inner.inner_prod_V0(splines, integrand2) MB_13_eq[:, j] = inner.inner_prod_V0(splines, integrand3) @@ -374,39 +374,39 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, cN[j] = 1.0 integrand2 = ( - lambda eta: splines.evaluate_N(eta, cN) / J(eta) * 2 * np.pi * a * (B_phi(r(eta)) + r(eta) * dB_phi(r(eta))) + lambda eta: splines.evaluate_N(eta, cN) / J(eta) * 2 * xp.pi * a * (B_phi(r(eta)) + r(eta) * dB_phi(r(eta))) ) - integrand3 = lambda eta: splines.evaluate_N(eta, cN) / J(eta) * 2 * np.pi * a * R0 * dB_z(r(eta)) + integrand3 = lambda eta: splines.evaluate_N(eta, cN) / J(eta) * 2 * xp.pi * a * R0 * dB_z(r(eta)) MB_21_eq[:, j] = inner.inner_prod_V1(splines, integrand2) MB_31_eq[:, j] = inner.inner_prod_V1(splines, integrand3) # ===== right-hand sides of projection matrices =============== - rhs0_N_phi = np.empty(pi0_N_i[0].size, dtype=float) - rhs0_N_z = np.empty(pi0_N_i[0].size, dtype=float) + rhs0_N_phi = xp.empty(pi0_N_i[0].size, dtype=float) + rhs0_N_z = xp.empty(pi0_N_i[0].size, dtype=float) - rhs1_D_phi = np.empty(pi1_D_i[0].size, dtype=float) - rhs1_D_z = np.empty(pi1_D_i[0].size, dtype=float) + rhs1_D_phi = xp.empty(pi1_D_i[0].size, dtype=float) + rhs1_D_z = xp.empty(pi1_D_i[0].size, dtype=float) - rhs0_N_pr = np.empty(pi0_N_i[0].size, dtype=float) - rhs1_D_pr = np.empty(pi1_D_i[0].size, dtype=float) + rhs0_N_pr = xp.empty(pi0_N_i[0].size, dtype=float) + rhs1_D_pr = xp.empty(pi1_D_i[0].size, dtype=float) - rhs0_N_rho = np.empty(pi0_N_i[0].size, dtype=float) - rhs1_D_rho = np.empty(pi1_D_i[0].size, dtype=float) + rhs0_N_rho = xp.empty(pi0_N_i[0].size, dtype=float) + rhs1_D_rho = xp.empty(pi1_D_i[0].size, dtype=float) # ker.rhs0_1d(pi0_N_i[0], pi0_N_i[1], basis_int_N, splines.evaluate_D(x_int, b2_eq_phi)/J(x_int), rhs0_N_phi) # ker.rhs0_1d(pi0_N_i[0], pi0_N_i[1], basis_int_N, splines.evaluate_D(x_int, b2_eq_z )/J(x_int), rhs0_N_z ) # - # ker.rhs1_1d(pi1_D_i[0], pi1_D_i[1], subs, np.append(0, np.cumsum(subs - 1)[:-1]), wts, basis_his_D, (splines.evaluate_D(pts.flatten(), b2_eq_z )/J(pts.flatten())).reshape(pts.shape[0], pts.shape[1]), rhs1_D_z) - # ker.rhs1_1d(pi1_D_i[0], pi1_D_i[1], subs, np.append(0, np.cumsum(subs - 1)[:-1]), wts, basis_his_D, (splines.evaluate_D(pts.flatten(), b2_eq_phi)/J(pts.flatten())).reshape(pts.shape[0], pts.shape[1]), rhs1_D_phi) + # ker.rhs1_1d(pi1_D_i[0], pi1_D_i[1], subs, xp.append(0, xp.cumsum(subs - 1)[:-1]), wts, basis_his_D, (splines.evaluate_D(pts.flatten(), b2_eq_z )/J(pts.flatten())).reshape(pts.shape[0], pts.shape[1]), rhs1_D_z) + # ker.rhs1_1d(pi1_D_i[0], pi1_D_i[1], subs, xp.append(0, xp.cumsum(subs - 1)[:-1]), wts, basis_his_D, (splines.evaluate_D(pts.flatten(), b2_eq_phi)/J(pts.flatten())).reshape(pts.shape[0], pts.shape[1]), rhs1_D_phi) # # ker.rhs0_1d(pi0_N_i[0], pi0_N_i[1], basis_int_N, splines.evaluate_D(x_int, p3_eq)/J(x_int), rhs0_N_pr) - # temp = np.empty(pi0_N_i[0].size, dtype=float) + # temp = xp.empty(pi0_N_i[0].size, dtype=float) # temp[:] = rhs0_N_pr - # ker.rhs1_1d(pi1_D_i[0], pi1_D_i[1], subs, np.append(0, np.cumsum(subs - 1)[:-1]), wts, basis_his_D, (splines.evaluate_D(pts.flatten(), p3_eq)/J(pts.flatten())).reshape(pts.shape[0], pts.shape[1]), rhs1_D_pr) + # ker.rhs1_1d(pi1_D_i[0], pi1_D_i[1], subs, xp.append(0, xp.cumsum(subs - 1)[:-1]), wts, basis_his_D, (splines.evaluate_D(pts.flatten(), p3_eq)/J(pts.flatten())).reshape(pts.shape[0], pts.shape[1]), rhs1_D_pr) # # ker.rhs0_1d(pi0_N_i[0], pi0_N_i[1], basis_int_N, splines.evaluate_D(x_int, rho3)/J(x_int), rhs0_N_rho) - # ker.rhs1_1d(pi1_D_i[0], pi1_D_i[1], subs, np.append(0, np.cumsum(subs - 1)[:-1]), wts, basis_his_D, (splines.evaluate_D(pts.flatten(), rho3)/J(pts.flatten())).reshape(pts.shape[0], pts.shape[1]), rhs1_D_rho) + # ker.rhs1_1d(pi1_D_i[0], pi1_D_i[1], subs, xp.append(0, xp.cumsum(subs - 1)[:-1]), wts, basis_his_D, (splines.evaluate_D(pts.flatten(), rho3)/J(pts.flatten())).reshape(pts.shape[0], pts.shape[1]), rhs1_D_rho) ker.rhs0_1d(pi0_N_i[0], pi0_N_i[1], basis_int_N, B2_phi(x_int) / J(x_int), rhs0_N_phi) ker.rhs0_1d(pi0_N_i[0], pi0_N_i[1], basis_int_N, B2_z(x_int) / J(x_int), rhs0_N_z) @@ -415,7 +415,7 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, pi1_D_i[0], pi1_D_i[1], subs, - np.append(0, np.cumsum(subs - 1)[:-1]), + xp.append(0, xp.cumsum(subs - 1)[:-1]), wts, basis_his_D, (B2_phi(pts.flatten()) / J(pts.flatten())).reshape(pts.shape[0], pts.shape[1]), @@ -426,20 +426,20 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, pi1_D_i[0], pi1_D_i[1], subs, - np.append(0, np.cumsum(subs - 1)[:-1]), + xp.append(0, xp.cumsum(subs - 1)[:-1]), wts, basis_his_D, (B2_z(pts.flatten()) / J(pts.flatten())).reshape(pts.shape[0], pts.shape[1]), rhs1_D_z, ) - # ker.rhs1_1d(pi1_D_i[0], pi1_D_i[1], subs, np.append(0, np.cumsum(subs - 1)[:-1]), wts, basis_his_D, np.ones(pts.shape, dtype=float), rhs1_D_z) + # ker.rhs1_1d(pi1_D_i[0], pi1_D_i[1], subs, xp.append(0, xp.cumsum(subs - 1)[:-1]), wts, basis_his_D, xp.ones(pts.shape, dtype=float), rhs1_D_z) ker.rhs0_1d(pi0_N_i[0], pi0_N_i[1], basis_int_N, P3(x_int) / J(x_int), rhs0_N_pr) ker.rhs1_1d( pi1_D_i[0], pi1_D_i[1], subs, - np.append(0, np.cumsum(subs - 1)[:-1]), + xp.append(0, xp.cumsum(subs - 1)[:-1]), wts, basis_his_D, (P3(pts.flatten()) / J(pts.flatten())).reshape(pts.shape[0], pts.shape[1]), @@ -451,7 +451,7 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, pi1_D_i[0], pi1_D_i[1], subs, - np.append(0, np.cumsum(subs - 1)[:-1]), + xp.append(0, xp.cumsum(subs - 1)[:-1]), wts, basis_his_D, (Rho3(pts.flatten()) / J(pts.flatten())).reshape(pts.shape[0], pts.shape[1]), @@ -480,38 +480,38 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, (rhs1_D_rho, (pi1_D_i[0], pi1_D_i[1])), shape=(splines.NbaseD, splines.NbaseD) ).toarray() - pi0_N_phi = np.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).dot(rhs0_N_phi[1:-1, 1:-1]) - pi0_N_z = np.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).dot(rhs0_N_z[1:-1, 1:-1]) + pi0_N_phi = xp.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).dot(rhs0_N_phi[1:-1, 1:-1]) + pi0_N_z = xp.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).dot(rhs0_N_z[1:-1, 1:-1]) - pi1_D_phi = np.linalg.inv(proj.D.toarray()).dot(rhs1_D_phi) - pi1_D_z = np.linalg.inv(proj.D.toarray()).dot(rhs1_D_z) + pi1_D_phi = xp.linalg.inv(proj.D.toarray()).dot(rhs1_D_phi) + pi1_D_z = xp.linalg.inv(proj.D.toarray()).dot(rhs1_D_z) - pi0_N_pr = np.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).dot(rhs0_N_pr[1:-1, 1:-1]) - pi1_D_pr = np.linalg.inv(proj.D.toarray()).dot(rhs1_D_pr) + pi0_N_pr = xp.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).dot(rhs0_N_pr[1:-1, 1:-1]) + pi1_D_pr = xp.linalg.inv(proj.D.toarray()).dot(rhs1_D_pr) - pi0_N_rho = np.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).dot(rhs0_N_rho[1:-1, 1:-1]) - pi1_D_rho = np.linalg.inv(proj.D.toarray()).dot(rhs1_D_rho) + pi0_N_rho = xp.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).dot(rhs0_N_rho[1:-1, 1:-1]) + pi1_D_rho = xp.linalg.inv(proj.D.toarray()).dot(rhs1_D_rho) # ======= matrices in strong induction equation ================ # 11 block - I_11 = -2 * np.pi * m * pi0_N_phi - 2 * np.pi * n * pi0_N_z + I_11 = -2 * xp.pi * m * pi0_N_phi - 2 * xp.pi * n * pi0_N_z # 21 block and 31 block I_21 = -GRAD.dot(pi0_N_phi) I_31 = -GRAD.dot(pi0_N_z) # 22 block and 32 block - I_22 = 2 * np.pi * n * pi1_D_z - I_32 = -2 * np.pi * m * pi1_D_z + I_22 = 2 * xp.pi * n * pi1_D_z + I_32 = -2 * xp.pi * m * pi1_D_z # 23 block and 33 block - I_23 = -2 * np.pi * n * pi1_D_phi - I_33 = 2 * np.pi * m * pi1_D_phi + I_23 = -2 * xp.pi * n * pi1_D_phi + I_33 = 2 * xp.pi * m * pi1_D_phi # total - I_all = np.block( + I_all = xp.block( [ - [I_11, np.zeros((len(u2_r) - 2, len(u2_phi))), np.zeros((len(u2_r) - 2, len(u2_z) - 1))], + [I_11, xp.zeros((len(u2_r) - 2, len(u2_phi))), xp.zeros((len(u2_r) - 2, len(u2_z) - 1))], [I_21, I_22, I_23[:, 1:]], [I_31[1:, :], I_32[1:, :], I_33[1:, 1:]], ] @@ -519,97 +519,97 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, # ======= matrices in strong pressure equation ================ P_1 = -GRAD.dot(pi0_N_pr) - (gamma - 1) * pi1_D_pr.dot(GRAD) - P_2 = -2 * np.pi * m * gamma * pi1_D_pr - P_3 = -2 * np.pi * n * gamma * pi1_D_pr + P_2 = -2 * xp.pi * m * gamma * pi1_D_pr + P_3 = -2 * xp.pi * n * gamma * pi1_D_pr - P_all = np.block([[P_1[1:, :], P_2[1:, :], P_3[1:, 1:]]]) + P_all = xp.block([[P_1[1:, :], P_2[1:, :], P_3[1:, 1:]]]) # ========== matrices in weak momentum balance equation ====== A_1 = 1 / 2 * (pi0_N_rho.T.dot(M2_r) + M2_r.dot(pi0_N_rho)) A_2 = 1 / 2 * (pi1_D_rho.T.dot(M2_phi) + M2_phi.dot(pi1_D_rho)) A_3 = 1 / 2 * (pi1_D_rho.T.dot(M2_z) + M2_z.dot(pi1_D_rho))[:, :] - A_all = np.block( + A_all = xp.block( [ - [A_1, np.zeros((A_1.shape[0], A_2.shape[1])), np.zeros((A_1.shape[0], A_3.shape[1]))], - [np.zeros((A_2.shape[0], A_1.shape[1])), A_2, np.zeros((A_2.shape[0], A_3.shape[1]))], - [np.zeros((A_3.shape[0], A_1.shape[1])), np.zeros((A_3.shape[0], A_2.shape[1])), A_3], + [A_1, xp.zeros((A_1.shape[0], A_2.shape[1])), xp.zeros((A_1.shape[0], A_3.shape[1]))], + [xp.zeros((A_2.shape[0], A_1.shape[1])), A_2, xp.zeros((A_2.shape[0], A_3.shape[1]))], + [xp.zeros((A_3.shape[0], A_1.shape[1])), xp.zeros((A_3.shape[0], A_2.shape[1])), A_3], ] ) - MB_11 = 2 * np.pi * n * pi0_N_z.T.dot(M2_r) + 2 * np.pi * m * pi0_N_phi.T.dot(M2_r) + MB_11 = 2 * xp.pi * n * pi0_N_z.T.dot(M2_r) + 2 * xp.pi * m * pi0_N_phi.T.dot(M2_r) MB_12 = pi0_N_phi.T.dot(GRAD.T.dot(M2_phi)) - MB_12_eq[1:-1, :] MB_13 = pi0_N_z.T.dot(GRAD.T.dot(M2_z)) - MB_13_eq[1:-1, :] MB_14 = GRAD.T.dot(M3) MB_21 = MB_21_eq[:, 1:-1] - MB_22 = -2 * np.pi * n * pi1_D_z.T.dot(M2_phi) - MB_23 = 2 * np.pi * m * pi1_D_z.T.dot(M2_z) - MB_24 = 2 * np.pi * m * M3 + MB_22 = -2 * xp.pi * n * pi1_D_z.T.dot(M2_phi) + MB_23 = 2 * xp.pi * m * pi1_D_z.T.dot(M2_z) + MB_24 = 2 * xp.pi * m * M3 MB_31 = MB_31_eq[:, 1:-1] - MB_32 = 2 * np.pi * n * pi1_D_phi.T.dot(M2_phi) - MB_33 = -2 * np.pi * m * pi1_D_phi.T.dot(M2_z) - MB_34 = 2 * np.pi * n * M3 + MB_32 = 2 * xp.pi * n * pi1_D_phi.T.dot(M2_phi) + MB_33 = -2 * xp.pi * m * pi1_D_phi.T.dot(M2_z) + MB_34 = 2 * xp.pi * n * M3 - MB_b_all = np.block( + MB_b_all = xp.block( [[MB_11, MB_12, MB_13[:, 1:]], [MB_21, MB_22, MB_23[:, 1:]], [MB_31[1:, :], MB_32[1:, :], MB_33[1:, 1:]]] ) - MB_p_all = np.block([[MB_14[:, 1:]], [MB_24[:, 1:]], [MB_34[1:, 1:]]]) + MB_p_all = xp.block([[MB_14[:, 1:]], [MB_24[:, 1:]], [MB_34[1:, 1:]]]) ## ======= matrices in strong induction equation ================ ## 11 block - # I_11 = np.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).dot(-2*np.pi*m*rhs0_N_phi[1:-1, 1:-1] - 2*np.pi*n*rhs0_N_z[1:-1, 1:-1]) + # I_11 = xp.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).dot(-2*xp.pi*m*rhs0_N_phi[1:-1, 1:-1] - 2*xp.pi*n*rhs0_N_z[1:-1, 1:-1]) # ## 21 block and 31 block - # I_21 = -GRAD[: , 1:-1].dot(np.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).dot(rhs0_N_phi[1:-1, 1:-1])) - # I_31 = -GRAD[1:, 1:-1].dot(np.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).dot(rhs0_N_z[1:-1, 1:-1])) + # I_21 = -GRAD[: , 1:-1].dot(xp.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).dot(rhs0_N_phi[1:-1, 1:-1])) + # I_31 = -GRAD[1:, 1:-1].dot(xp.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).dot(rhs0_N_z[1:-1, 1:-1])) # ## 22 block and 32 block - # I_22 = 2*np.pi*n*np.linalg.inv(proj.D.toarray()[ :, :]).dot(rhs1_D_z[ :, :]) - # I_32 = -2*np.pi*m*np.linalg.inv(proj.D.toarray()[1:, 1:]).dot(rhs1_D_z[1:, :]) + # I_22 = 2*xp.pi*n*xp.linalg.inv(proj.D.toarray()[ :, :]).dot(rhs1_D_z[ :, :]) + # I_32 = -2*xp.pi*m*xp.linalg.inv(proj.D.toarray()[1:, 1:]).dot(rhs1_D_z[1:, :]) # ## 23 block and 33 block - # I_23 = -2*np.pi*n*np.linalg.inv(proj.D.toarray()[ :, :]).dot(rhs1_D_phi[ :, 1:]) - # I_33 = 2*np.pi*m*np.linalg.inv(proj.D.toarray()[1:, 1:]).dot(rhs1_D_phi[1:, 1:]) + # I_23 = -2*xp.pi*n*xp.linalg.inv(proj.D.toarray()[ :, :]).dot(rhs1_D_phi[ :, 1:]) + # I_33 = 2*xp.pi*m*xp.linalg.inv(proj.D.toarray()[1:, 1:]).dot(rhs1_D_phi[1:, 1:]) # # ## ======= matrices in strong pressure equation ================ - # P_1 = -GRAD[1:, 1:-1].dot(np.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).dot(rhs0_N_pr[1:-1, 1:-1])) - (gamma - 1)*np.linalg.inv(proj.D.toarray()[1:, 1:]).dot(rhs1_D_pr[1:, :].dot(GRAD[:, 1:-1])) - # P_2 = -2*np.pi*m*gamma*np.linalg.inv(proj.D.toarray()[1:, 1:]).dot(rhs1_D_pr[1:, :]) - # P_3 = -2*np.pi*n*gamma*np.linalg.inv(proj.D.toarray()[1:, 1:]).dot(rhs1_D_pr[1:, 1:]) + # P_1 = -GRAD[1:, 1:-1].dot(xp.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).dot(rhs0_N_pr[1:-1, 1:-1])) - (gamma - 1)*xp.linalg.inv(proj.D.toarray()[1:, 1:]).dot(rhs1_D_pr[1:, :].dot(GRAD[:, 1:-1])) + # P_2 = -2*xp.pi*m*gamma*xp.linalg.inv(proj.D.toarray()[1:, 1:]).dot(rhs1_D_pr[1:, :]) + # P_3 = -2*xp.pi*n*gamma*xp.linalg.inv(proj.D.toarray()[1:, 1:]).dot(rhs1_D_pr[1:, 1:]) # # ## ========== matrices in weak momentum balance equation ====== - # rhs0_N_rho = np.empty(pi0_N_i[0].size, dtype=float) + # rhs0_N_rho = xp.empty(pi0_N_i[0].size, dtype=float) # ker.rhs0_1d(pi0_N_i[0], pi0_N_i[1], basis_int_N, splines.evaluate_D(x_int, rho3)/J(x_int), rhs0_N_rho) # # - # rhs1_D_rho = np.empty(pi1_D_i[0].size, dtype=float) - # ker.rhs1_1d(pi1_D_i[0], pi1_D_i[1], subs, np.append(0, np.cumsum(subs - 1)[:-1]), wts, basis_his_D, (splines.evaluate_D(pts.flatten(), rho3)/J(pts.flatten())).reshape(pts.shape[0], pts.shape[1]), rhs1_D_rho) + # rhs1_D_rho = xp.empty(pi1_D_i[0].size, dtype=float) + # ker.rhs1_1d(pi1_D_i[0], pi1_D_i[1], subs, xp.append(0, xp.cumsum(subs - 1)[:-1]), wts, basis_his_D, (splines.evaluate_D(pts.flatten(), rho3)/J(pts.flatten())).reshape(pts.shape[0], pts.shape[1]), rhs1_D_rho) # # # - # A_1 = 1/2*(rhs0_N_rho[1:-1, 1:-1].T.dot(np.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).T.dot(M2_r[1:-1, 1:-1])) + M2_r[1:-1, 1:-1].dot(np.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).dot(rhs0_N_rho[1:-1, 1:-1]))) - # A_2 = 1/2*(rhs1_D_rho.T.dot(np.linalg.inv(proj.D.toarray()[:, :]).T.dot(M2_phi)) + M2_phi.dot(np.linalg.inv(proj.D.toarray()[:, :]).dot(rhs1_D_rho))) - # A_3 = 1/2*(rhs1_D_rho[1:, 1:].T.dot(np.linalg.inv(proj.D.toarray()[1:, 1:]).T.dot(M2_z[1:, 1:])) + M2_z[1:, 1:].dot(np.linalg.inv(proj.D.toarray()[1:, 1:]).dot(rhs1_D_rho[1:, 1:]))) + # A_1 = 1/2*(rhs0_N_rho[1:-1, 1:-1].T.dot(xp.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).T.dot(M2_r[1:-1, 1:-1])) + M2_r[1:-1, 1:-1].dot(xp.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).dot(rhs0_N_rho[1:-1, 1:-1]))) + # A_2 = 1/2*(rhs1_D_rho.T.dot(xp.linalg.inv(proj.D.toarray()[:, :]).T.dot(M2_phi)) + M2_phi.dot(xp.linalg.inv(proj.D.toarray()[:, :]).dot(rhs1_D_rho))) + # A_3 = 1/2*(rhs1_D_rho[1:, 1:].T.dot(xp.linalg.inv(proj.D.toarray()[1:, 1:]).T.dot(M2_z[1:, 1:])) + M2_z[1:, 1:].dot(xp.linalg.inv(proj.D.toarray()[1:, 1:]).dot(rhs1_D_rho[1:, 1:]))) # # - # MB_11 = 2*np.pi*n*rhs0_N_z[1:-1, 1:-1].T.dot(np.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).T.dot(M2_r[1:-1, 1:-1])) + 2*np.pi*m*rhs0_N_phi[1:-1, 1:-1].T.dot(np.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).T.dot(M2_r[1:-1, 1:-1])) + # MB_11 = 2*xp.pi*n*rhs0_N_z[1:-1, 1:-1].T.dot(xp.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).T.dot(M2_r[1:-1, 1:-1])) + 2*xp.pi*m*rhs0_N_phi[1:-1, 1:-1].T.dot(xp.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).T.dot(M2_r[1:-1, 1:-1])) # - # MB_12 = rhs0_N_phi[1:-1, 1:-1].T.dot(np.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).T.dot(GRAD[:, 1:-1].T.dot(M2_phi))) - # MB_13 = rhs0_N_z[1:-1, 1:-1].T.dot(np.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).T.dot(GRAD[1:, 1:-1].T.dot(M2_z[1:, 1:]))) + # MB_12 = rhs0_N_phi[1:-1, 1:-1].T.dot(xp.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).T.dot(GRAD[:, 1:-1].T.dot(M2_phi))) + # MB_13 = rhs0_N_z[1:-1, 1:-1].T.dot(xp.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).T.dot(GRAD[1:, 1:-1].T.dot(M2_z[1:, 1:]))) # # MB_14 = GRAD[1:, 1:-1].T.dot(M3[1:, 1:]) # # - # MB_22 = -2*np.pi*n*rhs1_D_z.T.dot(np.linalg.inv(proj.D.toarray()).T.dot(M2_phi)) - # MB_23 = 2*np.pi*m*rhs1_D_z[1:, :].T.dot(np.linalg.inv(proj.D.toarray()[1:, 1:]).T.dot(M2_z[1:, 1:])) - # MB_24 = 2*np.pi*m*M3[ :, 1:] + # MB_22 = -2*xp.pi*n*rhs1_D_z.T.dot(xp.linalg.inv(proj.D.toarray()).T.dot(M2_phi)) + # MB_23 = 2*xp.pi*m*rhs1_D_z[1:, :].T.dot(xp.linalg.inv(proj.D.toarray()[1:, 1:]).T.dot(M2_z[1:, 1:])) + # MB_24 = 2*xp.pi*m*M3[ :, 1:] # - # MB_32 = 2*np.pi*n*rhs1_D_phi[:, 1:].T.dot(np.linalg.inv(proj.D.toarray()).T.dot(M2_phi)) - # MB_33 = -2*np.pi*m*rhs1_D_phi[1:, 1:].T.dot(np.linalg.inv(proj.D.toarray()[1:, 1:]).T.dot(M2_z[1:, 1:])) - # MB_34 = 2*np.pi*n*M3[1:, 1:] + # MB_32 = 2*xp.pi*n*rhs1_D_phi[:, 1:].T.dot(xp.linalg.inv(proj.D.toarray()).T.dot(M2_phi)) + # MB_33 = -2*xp.pi*m*rhs1_D_phi[1:, 1:].T.dot(xp.linalg.inv(proj.D.toarray()[1:, 1:]).T.dot(M2_z[1:, 1:])) + # MB_34 = 2*xp.pi*n*M3[1:, 1:] # # # ==== matrices in eigenvalue problem ======== @@ -625,17 +625,17 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, W_32 = MB_32.dot(I_22) + MB_33.dot(I_32) + MB_34.dot(P_2) W_33 = MB_32.dot(I_23) + MB_33.dot(I_33) + MB_34.dot(P_3) - # W = np.block([[W_11, W_12, W_13[:, 1:]], [W_21, W_22, W_23[:, 1:]], [W_31[1:, :], W_32[1:, :], W_33[1:, 1:]]]) - W = np.block([[W_11, W_12, W_13[:, :]], [W_21, W_22, W_23[:, :]], [W_31[:, :], W_32[:, :], W_33[:, :]]]) + # W = xp.block([[W_11, W_12, W_13[:, 1:]], [W_21, W_22, W_23[:, 1:]], [W_31[1:, :], W_32[1:, :], W_33[1:, 1:]]]) + W = xp.block([[W_11, W_12, W_13[:, :]], [W_21, W_22, W_23[:, :]], [W_31[:, :], W_32[:, :], W_33[:, :]]]) - # print(np.allclose(K, K.T)) - # print(np.allclose(W, W.T)) + # print(xp.allclose(K, K.T)) + # print(xp.allclose(W, W.T)) # solve eigenvalue problem omega**2*K*xi = W*xi - MAT = np.linalg.inv(-A_all).dot(W) + MAT = xp.linalg.inv(-A_all).dot(W) - omega2, XYZ_eig = np.linalg.eig(MAT) - # omega2, XYZ_eig = np.linalg.eig(np.linalg.inv(-A_all).dot(MB_b_all.dot(I_all) + MB_p_all.dot(P_all))) + omega2, XYZ_eig = xp.linalg.eig(MAT) + # omega2, XYZ_eig = xp.linalg.eig(xp.linalg.inv(-A_all).dot(MB_b_all.dot(I_all) + MB_p_all.dot(P_all))) # extract components X_eig = XYZ_eig[: (splines.NbaseN - 2), :] @@ -643,35 +643,35 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, Z_eig = XYZ_eig[(splines.NbaseN - 2 + splines.NbaseD) :, :] # add boundary conditions X(0) = X(1) = 0 - X_eig = np.vstack((np.zeros(X_eig.shape[1], dtype=float), X_eig, np.zeros(X_eig.shape[1], dtype=float))) + X_eig = xp.vstack((xp.zeros(X_eig.shape[1], dtype=float), X_eig, xp.zeros(X_eig.shape[1], dtype=float))) # add boundary condition Z(0) = 0 - Z_eig = np.vstack((np.zeros(Z_eig.shape[1], dtype=float), Z_eig)) + Z_eig = xp.vstack((xp.zeros(Z_eig.shape[1], dtype=float), Z_eig)) return omega2, X_eig, Y_eig, Z_eig ## ========== matrices in initial value problem === - LHS = np.block( + LHS = xp.block( [ - [A_all, np.zeros((A_all.shape[0], A_all.shape[1])), np.zeros((A_all.shape[0], len(p3) - 1))], + [A_all, xp.zeros((A_all.shape[0], A_all.shape[1])), xp.zeros((A_all.shape[0], len(p3) - 1))], [ - np.zeros((A_all.shape[0], A_all.shape[1])), - np.identity(A_all.shape[0]), - np.zeros((A_all.shape[0], len(p3) - 1)), + xp.zeros((A_all.shape[0], A_all.shape[1])), + xp.identity(A_all.shape[0]), + xp.zeros((A_all.shape[0], len(p3) - 1)), ], [ - np.zeros((len(p3) - 1, A_all.shape[1])), - np.zeros((len(p3) - 1, A_all.shape[1])), - np.identity(len(p3) - 1), + xp.zeros((len(p3) - 1, A_all.shape[1])), + xp.zeros((len(p3) - 1, A_all.shape[1])), + xp.identity(len(p3) - 1), ], ] ) - RHS = np.block( + RHS = xp.block( [ - [np.zeros((MB_b_all.shape[0], I_all.shape[1])), MB_b_all, MB_p_all], - [I_all, np.zeros((I_all.shape[0], MB_b_all.shape[1])), np.zeros((I_all.shape[0], MB_p_all.shape[1]))], - [P_all, np.zeros((P_all.shape[0], MB_b_all.shape[1])), np.zeros((P_all.shape[0], MB_p_all.shape[1]))], + [xp.zeros((MB_b_all.shape[0], I_all.shape[1])), MB_b_all, MB_p_all], + [I_all, xp.zeros((I_all.shape[0], MB_b_all.shape[1])), xp.zeros((I_all.shape[0], MB_p_all.shape[1]))], + [P_all, xp.zeros((P_all.shape[0], MB_b_all.shape[1])), xp.zeros((P_all.shape[0], MB_p_all.shape[1]))], ] ) @@ -679,35 +679,35 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, T = 200.0 Nt = int(T / dt) - UPDATE = np.linalg.inv(LHS - dt / 2 * RHS).dot(LHS + dt / 2 * RHS) - ##UPDATE = np.linalg.inv(LHS).dot(LHS + dt*RHS) + UPDATE = xp.linalg.inv(LHS - dt / 2 * RHS).dot(LHS + dt / 2 * RHS) + ##UPDATE = xp.linalg.inv(LHS).dot(LHS + dt*RHS) # - # lambdas, eig_vecs = np.linalg.eig(UPDATE) + # lambdas, eig_vecs = xp.linalg.eig(UPDATE) # return lambdas # # return lambdas # - u2_r_all = np.zeros((Nt + 1, len(u2_r)), dtype=float) - u2_phi_all = np.zeros((Nt + 1, len(u2_phi)), dtype=float) - u2_z_all = np.zeros((Nt + 1, len(u2_z)), dtype=float) + u2_r_all = xp.zeros((Nt + 1, len(u2_r)), dtype=float) + u2_phi_all = xp.zeros((Nt + 1, len(u2_phi)), dtype=float) + u2_z_all = xp.zeros((Nt + 1, len(u2_z)), dtype=float) - b2_r_all = np.zeros((Nt + 1, len(b2_r)), dtype=float) - b2_phi_all = np.zeros((Nt + 1, len(b2_phi)), dtype=float) - b2_z_all = np.zeros((Nt + 1, len(b2_z)), dtype=float) + b2_r_all = xp.zeros((Nt + 1, len(b2_r)), dtype=float) + b2_phi_all = xp.zeros((Nt + 1, len(b2_phi)), dtype=float) + b2_z_all = xp.zeros((Nt + 1, len(b2_z)), dtype=float) - p3_all = np.zeros((Nt + 1, len(p3)), dtype=float) + p3_all = xp.zeros((Nt + 1, len(p3)), dtype=float) # initialization # u2_r_all[0, :] = u2_r # u2_phi_all[0, :] = u2_phi - u2_r_all[0, 1:-1] = np.random.rand(len(u2_r) - 2) - p3_all[0, 1:] = np.random.rand(len(p3) - 1) + u2_r_all[0, 1:-1] = xp.random.rand(len(u2_r) - 2) + p3_all[0, 1:] = xp.random.rand(len(p3) - 1) # time integration for n in range(Nt): - old = np.concatenate( + old = xp.concatenate( ( u2_r_all[n, 1:-1], u2_phi_all[n, :], @@ -721,18 +721,18 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, new = UPDATE.dot(old) # extract components - unew, bnew, pnew = np.split( + unew, bnew, pnew = xp.split( new, [len(u2_r) - 2 + len(u2_phi) + len(u2_z) - 1, 2 * (len(u2_r) - 2 + len(u2_phi) + len(u2_z) - 1)] ) - u2_r_all[n + 1, :] = np.array([0.0] + list(unew[: (splines.NbaseN - 2)]) + [0.0]) + u2_r_all[n + 1, :] = xp.array([0.0] + list(unew[: (splines.NbaseN - 2)]) + [0.0]) u2_phi_all[n + 1, :] = unew[(splines.NbaseN - 2) : (splines.NbaseN - 2 + splines.NbaseD)] - u2_z_all[n + 1, :] = np.array([0.0] + list(unew[(splines.NbaseN - 2 + splines.NbaseD) :])) + u2_z_all[n + 1, :] = xp.array([0.0] + list(unew[(splines.NbaseN - 2 + splines.NbaseD) :])) - b2_r_all[n + 1, :] = np.array([0.0] + list(bnew[: (splines.NbaseN - 2)]) + [0.0]) + b2_r_all[n + 1, :] = xp.array([0.0] + list(bnew[: (splines.NbaseN - 2)]) + [0.0]) b2_phi_all[n + 1, :] = bnew[(splines.NbaseN - 2) : (splines.NbaseN - 2 + splines.NbaseD)] - b2_z_all[n + 1, :] = np.array([0.0] + list(bnew[(splines.NbaseN - 2 + splines.NbaseD) :])) + b2_z_all[n + 1, :] = xp.array([0.0] + list(bnew[(splines.NbaseN - 2 + splines.NbaseD) :])) - p3_all[n + 1, :] = np.array([0.0] + list(pnew)) + p3_all[n + 1, :] = xp.array([0.0] + list(pnew)) return u2_r_all, u2_phi_all, u2_z_all, b2_r_all, b2_phi_all, b2_z_all, p3_all, omega2 diff --git a/src/struphy/eigenvalue_solvers/legacy/control_variates/control_variate.py b/src/struphy/eigenvalue_solvers/legacy/control_variates/control_variate.py index 37940a8d6..a4b95eb06 100644 --- a/src/struphy/eigenvalue_solvers/legacy/control_variates/control_variate.py +++ b/src/struphy/eigenvalue_solvers/legacy/control_variates/control_variate.py @@ -6,11 +6,11 @@ Class for control variates in delta-f method for current coupling scheme. """ +import cunumpy as xp import scipy.sparse as spa import struphy.feec.basics.kernels_3d as ker import struphy.feec.control_variates.kernels_control_variate as ker_cv -from struphy.utils.arrays import xp as np class terms_control_variate: @@ -40,7 +40,7 @@ def __init__(self, tensor_space_FEM, domain, basis_u): kind_fun_eq = [11, 12, 13, 14] # ========= evaluation of DF^(-1) * jh_eq_phys * |det(DF)| at quadrature points ========= - self.mat_jh1 = np.empty( + self.mat_jh1 = xp.empty( ( self.space.Nel[0], self.space.n_quad[0], @@ -51,7 +51,7 @@ def __init__(self, tensor_space_FEM, domain, basis_u): ), dtype=float, ) - self.mat_jh2 = np.empty( + self.mat_jh2 = xp.empty( ( self.space.Nel[0], self.space.n_quad[0], @@ -62,7 +62,7 @@ def __init__(self, tensor_space_FEM, domain, basis_u): ), dtype=float, ) - self.mat_jh3 = np.empty( + self.mat_jh3 = xp.empty( ( self.space.Nel[0], self.space.n_quad[0], @@ -133,7 +133,7 @@ def __init__(self, tensor_space_FEM, domain, basis_u): ) # ========= evaluation of nh_eq_phys * |det(DF)| at quadrature points =================== - self.mat_nh = np.empty( + self.mat_nh = xp.empty( ( self.space.Nel[0], self.space.n_quad[0], @@ -166,7 +166,7 @@ def __init__(self, tensor_space_FEM, domain, basis_u): ) # =========== 2-form magnetic field at quadrature points ================================= - self.B2_1 = np.empty( + self.B2_1 = xp.empty( ( self.space.Nel[0], self.space.n_quad[0], @@ -177,7 +177,7 @@ def __init__(self, tensor_space_FEM, domain, basis_u): ), dtype=float, ) - self.B2_2 = np.empty( + self.B2_2 = xp.empty( ( self.space.Nel[0], self.space.n_quad[0], @@ -188,7 +188,7 @@ def __init__(self, tensor_space_FEM, domain, basis_u): ), dtype=float, ) - self.B2_3 = np.empty( + self.B2_3 = xp.empty( ( self.space.Nel[0], self.space.n_quad[0], @@ -202,7 +202,7 @@ def __init__(self, tensor_space_FEM, domain, basis_u): # ================== correction matrices in step 1 ======================== if self.basis_u == 0: - self.M12 = np.empty( + self.M12 = xp.empty( ( self.space.NbaseN[0], self.space.NbaseN[1], @@ -213,7 +213,7 @@ def __init__(self, tensor_space_FEM, domain, basis_u): ), dtype=float, ) - self.M13 = np.empty( + self.M13 = xp.empty( ( self.space.NbaseN[0], self.space.NbaseN[1], @@ -224,7 +224,7 @@ def __init__(self, tensor_space_FEM, domain, basis_u): ), dtype=float, ) - self.M23 = np.empty( + self.M23 = xp.empty( ( self.space.NbaseN[0], self.space.NbaseN[1], @@ -237,7 +237,7 @@ def __init__(self, tensor_space_FEM, domain, basis_u): ) elif self.basis_u == 2: - self.M12 = np.empty( + self.M12 = xp.empty( ( self.space.NbaseN[0], self.space.NbaseD[1], @@ -248,7 +248,7 @@ def __init__(self, tensor_space_FEM, domain, basis_u): ), dtype=float, ) - self.M13 = np.empty( + self.M13 = xp.empty( ( self.space.NbaseN[0], self.space.NbaseD[1], @@ -259,7 +259,7 @@ def __init__(self, tensor_space_FEM, domain, basis_u): ), dtype=float, ) - self.M23 = np.empty( + self.M23 = xp.empty( ( self.space.NbaseD[0], self.space.NbaseN[1], @@ -273,14 +273,14 @@ def __init__(self, tensor_space_FEM, domain, basis_u): # ==================== correction vectors in step 3 ======================= if self.basis_u == 0: - self.F1 = np.empty((self.space.NbaseN[0], self.space.NbaseN[1], self.space.NbaseN[2]), dtype=float) - self.F2 = np.empty((self.space.NbaseN[0], self.space.NbaseN[1], self.space.NbaseN[2]), dtype=float) - self.F3 = np.empty((self.space.NbaseN[0], self.space.NbaseN[1], self.space.NbaseN[2]), dtype=float) + self.F1 = xp.empty((self.space.NbaseN[0], self.space.NbaseN[1], self.space.NbaseN[2]), dtype=float) + self.F2 = xp.empty((self.space.NbaseN[0], self.space.NbaseN[1], self.space.NbaseN[2]), dtype=float) + self.F3 = xp.empty((self.space.NbaseN[0], self.space.NbaseN[1], self.space.NbaseN[2]), dtype=float) elif self.basis_u == 2: - self.F1 = np.empty((self.space.NbaseN[0], self.space.NbaseD[1], self.space.NbaseD[2]), dtype=float) - self.F2 = np.empty((self.space.NbaseD[0], self.space.NbaseN[1], self.space.NbaseD[2]), dtype=float) - self.F3 = np.empty((self.space.NbaseD[0], self.space.NbaseD[1], self.space.NbaseN[2]), dtype=float) + self.F1 = xp.empty((self.space.NbaseN[0], self.space.NbaseD[1], self.space.NbaseD[2]), dtype=float) + self.F2 = xp.empty((self.space.NbaseD[0], self.space.NbaseN[1], self.space.NbaseD[2]), dtype=float) + self.F3 = xp.empty((self.space.NbaseD[0], self.space.NbaseD[1], self.space.NbaseN[2]), dtype=float) # ===== inner product in V0^3 resp. V2 of (B x jh_eq) - term ========== def inner_prod_jh_eq(self, b1, b2, b3): @@ -511,7 +511,7 @@ def inner_prod_jh_eq(self, b1, b2, b3): self.B2_1 * self.mat_jh2 - self.B2_2 * self.mat_jh1, ) - return np.concatenate((self.F1.flatten(), self.F2.flatten(), self.F3.flatten())) + return xp.concatenate((self.F1.flatten(), self.F2.flatten(), self.F3.flatten())) # ===== mass matrix in V0^3 resp. V2 of -(rhoh_eq * (B x U)) - term ======= def mass_nh_eq(self, b1, b2, b3): diff --git a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fB_massless_control_variate.py b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fB_massless_control_variate.py index e39a463ab..49be8c3ef 100644 --- a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fB_massless_control_variate.py +++ b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fB_massless_control_variate.py @@ -1,9 +1,9 @@ +import cunumpy as xp import scipy.sparse as spa import struphy.feec.basics.kernels_3d as ker import struphy.feec.control_variates.kinetic_extended.fB_massless_kernels_control_variate as ker_cv import struphy.feec.control_variates.kinetic_extended.fnB_massless_cv_kernel_2 as ker_cv2 -from struphy.utils.arrays import xp as np def bv_right( @@ -204,7 +204,7 @@ def bv_right( ) # ========================= C.T =========================== return tensor_space_FEM.C.T.dot( - np.concatenate((temp_twoform1.flatten(), temp_twoform2.flatten(), temp_twoform3.flatten())) + xp.concatenate((temp_twoform1.flatten(), temp_twoform2.flatten(), temp_twoform3.flatten())) ) diff --git a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fnB_massless_control_variate.py b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fnB_massless_control_variate.py index cb8877c6f..d52a3e90e 100644 --- a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fnB_massless_control_variate.py +++ b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fnB_massless_control_variate.py @@ -1,9 +1,8 @@ +import cunumpy as xp import hylife.utilitis_FEEC.basics.kernels_3d as ker import hylife.utilitis_FEEC.control_variates.fnB_massless_kernels_control_variate as ker_cv import scipy.sparse as spa -from struphy.utils.arrays import xp as np - def bv_pre(tol, n, LO_inv, tensor_space_FEM, p, Nel, idnx, idny, idnz): r""" @@ -249,7 +248,7 @@ def bv_right( ) # ========================= C.T =========================== return tensor_space_FEM.C.T.dot( - np.concatenate((temp_twoform1.flatten(), temp_twoform2.flatten(), temp_twoform3.flatten())) + xp.concatenate((temp_twoform1.flatten(), temp_twoform2.flatten(), temp_twoform3.flatten())) ) @@ -430,7 +429,7 @@ def uv_right( ) # ========================= C.T =========================== temp_final = temp_final_0.flatten() + tensor_space_FEM.G.T.dot( - np.concatenate((temp_final_1.flatten(), temp_final_2.flatten(), temp_final_3.flatten())) + xp.concatenate((temp_final_1.flatten(), temp_final_2.flatten(), temp_final_3.flatten())) ) return temp_final diff --git a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/massless_control_variate.py b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/massless_control_variate.py index 1ea567314..4e97422f0 100644 --- a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/massless_control_variate.py +++ b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/massless_control_variate.py @@ -1,9 +1,8 @@ +import cunumpy as xp import hylife.utilitis_FEEC.basics.kernels_3d as ker import hylife.utilitis_FEEC.control_variates.massless_kernels_control_variate as ker_cv import scipy.sparse as spa -from struphy.utils.arrays import xp as np - def bv_pre(u, uvalue, tensor_space_FEM, p, Nel, idnx, idny, idnz): r""" @@ -248,7 +247,7 @@ def bv_right( ) # ========================= C.T =========================== return tensor_space_FEM.C.T.dot( - np.concatenate((temp_twoform1.flatten(), temp_twoform2.flatten(), temp_twoform3.flatten())) + xp.concatenate((temp_twoform1.flatten(), temp_twoform2.flatten(), temp_twoform3.flatten())) ) @@ -431,7 +430,7 @@ def uv_right( ) # ========================= C.T =========================== temp_final = temp_final_0.flatten() + tensor_space_FEM.G.T.dot( - np.concatenate((temp_final_1.flatten(), temp_final_2.flatten(), temp_final_3.flatten())) + xp.concatenate((temp_final_1.flatten(), temp_final_2.flatten(), temp_final_3.flatten())) ) return temp_final diff --git a/src/struphy/eigenvalue_solvers/legacy/emw_operators.py b/src/struphy/eigenvalue_solvers/legacy/emw_operators.py index 3187ac649..e2fd4ed22 100755 --- a/src/struphy/eigenvalue_solvers/legacy/emw_operators.py +++ b/src/struphy/eigenvalue_solvers/legacy/emw_operators.py @@ -6,11 +6,11 @@ Class for 2D/3D linear MHD projection operators. """ +import cunumpy as xp import scipy.sparse as spa import struphy.eigenvalue_solvers.kernels_3d as ker import struphy.eigenvalue_solvers.legacy.mass_matrices_3d_pre as mass_3d_pre -from struphy.utils.arrays import xp as np class EMW_operators: @@ -134,7 +134,7 @@ def __assemble_M1_cross(self, weight): Ni = self.SPACES.Nbase_1form[a] Nj = self.SPACES.Nbase_1form[b] - M[a][b] = np.zeros((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1), dtype=float) + M[a][b] = xp.zeros((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1), dtype=float) # evaluate metric tensor at quadrature points if a == 1 and b == 2: @@ -185,9 +185,9 @@ def __assemble_M1_cross(self, weight): mat_w, ) # convert to sparse matrix - indices = np.indices((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1)) + indices = xp.indices((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1)) - shift = [np.arange(Ni) - p for Ni, p in zip(Ni, p)] + shift = [xp.arange(Ni) - p for Ni, p in zip(Ni, p)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() diff --git a/src/struphy/eigenvalue_solvers/legacy/inner_products_1d.py b/src/struphy/eigenvalue_solvers/legacy/inner_products_1d.py index 5cae935fb..b4f019995 100644 --- a/src/struphy/eigenvalue_solvers/legacy/inner_products_1d.py +++ b/src/struphy/eigenvalue_solvers/legacy/inner_products_1d.py @@ -6,10 +6,9 @@ Modules to compute inner products in 1d. """ +import cunumpy as xp import scipy.sparse as spa -from struphy.utils.arrays import xp as np - # ======= inner product in V0 ==================== def inner_prod_V0(spline_space, fun, mapping=None): @@ -40,7 +39,7 @@ def inner_prod_V0(spline_space, fun, mapping=None): # evaluation of mapping at quadrature points if mapping == None: - mat_map = np.ones(pts.shape, dtype=float) + mat_map = xp.ones(pts.shape, dtype=float) else: mat_map = mapping(pts.flatten()).reshape(pts.shape) @@ -48,7 +47,7 @@ def inner_prod_V0(spline_space, fun, mapping=None): mat_f = fun(pts.flatten()).reshape(pts.shape) # assembly - F = np.zeros(NbaseN, dtype=float) + F = xp.zeros(NbaseN, dtype=float) for ie in range(Nel): for il in range(p + 1): @@ -91,7 +90,7 @@ def inner_prod_V1(spline_space, fun, mapping=None): # evaluation of mapping at quadrature points if mapping == None: - mat_map = np.ones(pts.shape, dtype=float) + mat_map = xp.ones(pts.shape, dtype=float) else: mat_map = 1 / mapping(pts.flatten()).reshape(pts.shape) @@ -99,7 +98,7 @@ def inner_prod_V1(spline_space, fun, mapping=None): mat_f = fun(pts.flatten()).reshape(pts.shape) # assembly - F = np.zeros(NbaseD, dtype=float) + F = xp.zeros(NbaseD, dtype=float) for ie in range(Nel): for il in range(p): diff --git a/src/struphy/eigenvalue_solvers/legacy/inner_products_2d.py b/src/struphy/eigenvalue_solvers/legacy/inner_products_2d.py index fd7ecabd4..05df4725f 100644 --- a/src/struphy/eigenvalue_solvers/legacy/inner_products_2d.py +++ b/src/struphy/eigenvalue_solvers/legacy/inner_products_2d.py @@ -6,10 +6,10 @@ Modules to compute inner products with given functions in 2D. """ +import cunumpy as xp import scipy.sparse as spa import struphy.eigenvalue_solvers.kernels_2d as ker -from struphy.utils.arrays import xp as np # ================ inner product in V0 =========================== @@ -25,7 +25,7 @@ def inner_prod_V0(tensor_space_FEM, domain, fun): domain : domain domain object defining the geometry - fun : callable or np.ndarray + fun : callable or xp.ndarray the 0-form with which the inner products shall be computed (either callable or 2D array with values at quadrature points) """ @@ -46,10 +46,10 @@ def inner_prod_V0(tensor_space_FEM, domain, fun): det_df = det_df.reshape(Nel[0], n_quad[0], Nel[1], n_quad[1]) # evaluation of given 0-form at quadrature points - mat_f = np.empty((pts[0].size, pts[1].size), dtype=float) + mat_f = xp.empty((pts[0].size, pts[1].size), dtype=float) if callable(fun): - quad_mesh = np.meshgrid(pts[0].flatten(), pts[1].flatten(), indexing="ij") + quad_mesh = xp.meshgrid(pts[0].flatten(), pts[1].flatten(), indexing="ij") mat_f[:, :] = fun(quad_mesh[0], quad_mesh[1], 0.0) else: mat_f[:, :] = fun @@ -57,7 +57,7 @@ def inner_prod_V0(tensor_space_FEM, domain, fun): # assembly Ni = tensor_space_FEM.Nbase_0form - F = np.zeros((Ni[0], Ni[1]), dtype=float) + F = xp.zeros((Ni[0], Ni[1]), dtype=float) mat_f = mat_f.reshape(Nel[0], n_quad[0], Nel[1], n_quad[1]) @@ -94,7 +94,7 @@ def inner_prod_V1(tensor_space_FEM, domain, fun): domain : domain domain object defining the geometry - fun : list of callables or np.ndarrays + fun : list of callables or xp.ndarrays the 1-form components with which the inner products shall be computed (either list of 3 callables or 2D arrays with values at quadrature points) """ @@ -127,10 +127,10 @@ def inner_prod_V1(tensor_space_FEM, domain, fun): g_inv = domain.metric_inv(pts[0].flatten(), pts[1].flatten(), 0.0) # 1-form components at quadrature points - mat_f = np.empty((pts[0].size, pts[1].size), dtype=float) + mat_f = xp.empty((pts[0].size, pts[1].size), dtype=float) if callable(fun[0]): - quad_mesh = np.meshgrid(pts[0].flatten(), pts[1].flatten(), indexing="ij") + quad_mesh = xp.meshgrid(pts[0].flatten(), pts[1].flatten(), indexing="ij") # components of global inner product F = [0, 0, 0] @@ -138,7 +138,7 @@ def inner_prod_V1(tensor_space_FEM, domain, fun): # assembly for a in range(3): Ni = tensor_space_FEM.Nbase_1form[a] - F[a] = np.zeros((Ni[0], Ni[1]), dtype=float) + F[a] = xp.zeros((Ni[0], Ni[1]), dtype=float) mat_f[:, :] = 0.0 @@ -170,7 +170,7 @@ def inner_prod_V1(tensor_space_FEM, domain, fun): mat_f * det_df, ) - F1 = tensor_space_FEM.E1_pol_0.dot(np.concatenate((F[0].flatten(), F[1].flatten()))) + F1 = tensor_space_FEM.E1_pol_0.dot(xp.concatenate((F[0].flatten(), F[1].flatten()))) F2 = tensor_space_FEM.E0_pol_0.dot(F[2].flatten()) return F1, F2 @@ -187,7 +187,7 @@ def inner_prod_V2(tensor_space_FEM, domain, fun): domain : domain domain object defining the geometry - fun : list of callables or np.ndarrays + fun : list of callables or xp.ndarrays the 2-form components with which the inner products shall be computed (either list of 3 callables or 2D arrays with values at quadrature points) """ @@ -220,10 +220,10 @@ def inner_prod_V2(tensor_space_FEM, domain, fun): g = domain.metric(pts[0].flatten(), pts[1].flatten(), 0.0) # 2-form components at quadrature points - mat_f = np.empty((pts[0].size, pts[1].size), dtype=float) + mat_f = xp.empty((pts[0].size, pts[1].size), dtype=float) if callable(fun[0]): - quad_mesh = np.meshgrid(pts[0].flatten(), pts[1].flatten(), indexing="ij") + quad_mesh = xp.meshgrid(pts[0].flatten(), pts[1].flatten(), indexing="ij") # components of global inner product F = [0, 0, 0] @@ -231,7 +231,7 @@ def inner_prod_V2(tensor_space_FEM, domain, fun): # assembly for a in range(3): Ni = tensor_space_FEM.Nbase_2form[a] - F[a] = np.zeros((Ni[0], Ni[1]), dtype=float) + F[a] = xp.zeros((Ni[0], Ni[1]), dtype=float) mat_f[:, :] = 0.0 @@ -263,7 +263,7 @@ def inner_prod_V2(tensor_space_FEM, domain, fun): mat_f / det_df, ) - F1 = tensor_space_FEM.E2_pol_0.dot(np.concatenate((F[0].flatten(), F[1].flatten()))) + F1 = tensor_space_FEM.E2_pol_0.dot(xp.concatenate((F[0].flatten(), F[1].flatten()))) F2 = tensor_space_FEM.E3_pol_0.dot(F[2].flatten()) return F1, F2 @@ -280,7 +280,7 @@ def inner_prod_V3(tensor_space_FEM, domain, fun): domain : domain domain object defining the geometry - fun : callable or np.ndarray + fun : callable or xp.ndarray the 3-form component with which the inner products shall be computed (either callable or 2D array with values at quadrature points) """ @@ -301,10 +301,10 @@ def inner_prod_V3(tensor_space_FEM, domain, fun): det_df = det_df.reshape(Nel[0], n_quad[0], Nel[1], n_quad[1]) # evaluation of given 3-form at quadrature points - mat_f = np.empty((pts[0].size, pts[1].size), dtype=float) + mat_f = xp.empty((pts[0].size, pts[1].size), dtype=float) if callable(fun): - quad_mesh = np.meshgrid(pts[0].flatten(), pts[1].flatten(), indexing="ij") + quad_mesh = xp.meshgrid(pts[0].flatten(), pts[1].flatten(), indexing="ij") mat_f[:, :] = fun(quad_mesh[0], quad_mesh[1], 0.0) else: mat_f[:, :] = fun @@ -312,7 +312,7 @@ def inner_prod_V3(tensor_space_FEM, domain, fun): # assembly Ni = tensor_space_FEM.Nbase_3form - F = np.zeros((Ni[0], Ni[1]), dtype=float) + F = xp.zeros((Ni[0], Ni[1]), dtype=float) mat_f = mat_f.reshape(Nel[0], n_quad[0], Nel[1], n_quad[1]) diff --git a/src/struphy/eigenvalue_solvers/legacy/inner_products_3d.py b/src/struphy/eigenvalue_solvers/legacy/inner_products_3d.py index 5aa9f710a..20d95c05c 100644 --- a/src/struphy/eigenvalue_solvers/legacy/inner_products_3d.py +++ b/src/struphy/eigenvalue_solvers/legacy/inner_products_3d.py @@ -6,10 +6,10 @@ Modules to compute inner products with given functions in 3D. """ +import cunumpy as xp import scipy.sparse as spa import struphy.eigenvalue_solvers.kernels_3d as ker -from struphy.utils.arrays import xp as np # ================ inner product in V0 =========================== @@ -25,7 +25,7 @@ def inner_prod_V0(tensor_space_FEM, domain, fun): domain : domain domain object defining the geometry - fun : callable or np.ndarray + fun : callable or xp.ndarray the 0-form with which the inner products shall be computed (either callable or 3D array with values at quadrature points) """ @@ -46,10 +46,10 @@ def inner_prod_V0(tensor_space_FEM, domain, fun): det_df = det_df.reshape(Nel[0], n_quad[0], Nel[1], n_quad[1], Nel[2], n_quad[2]) # evaluation of given 0-form at quadrature points - mat_f = np.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) + mat_f = xp.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) if callable(fun): - quad_mesh = np.meshgrid(pts[0].flatten(), pts[1].flatten(), pts[2].flatten(), indexing="ij") + quad_mesh = xp.meshgrid(pts[0].flatten(), pts[1].flatten(), pts[2].flatten(), indexing="ij") mat_f[:, :, :] = fun(quad_mesh[0], quad_mesh[1], quad_mesh[2]) else: mat_f[:, :, :] = fun @@ -57,7 +57,7 @@ def inner_prod_V0(tensor_space_FEM, domain, fun): # assembly Ni = tensor_space.Nbase_0form - F = np.zeros((Ni[0], Ni[1], Ni[2]), dtype=float) + F = xp.zeros((Ni[0], Ni[1], Ni[2]), dtype=float) mat_f = mat_f.reshape(Nel[0], n_quad[0], Nel[1], n_quad[1], Nel[2], n_quad[2]) @@ -101,7 +101,7 @@ def inner_prod_V1(tensor_space_FEM, domain, fun): domain : domain domain object defining the geometry - fun : list of callables or np.ndarrays + fun : list of callables or xp.ndarrays the 1-form components with which the inner products shall be computed (either list of 3 callables or 3D arrays with values at quadrature points) """ @@ -134,10 +134,10 @@ def inner_prod_V1(tensor_space_FEM, domain, fun): g_inv = domain.metric_inv(pts[0].flatten(), pts[1].flatten(), pts[2].flatten()) # 1-form components at quadrature points - mat_f = np.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) + mat_f = xp.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) if callable(fun[0]): - quad_mesh = np.meshgrid(pts[0].flatten(), pts[1].flatten(), pts[2].flatten(), indexing="ij") + quad_mesh = xp.meshgrid(pts[0].flatten(), pts[1].flatten(), pts[2].flatten(), indexing="ij") # components of global inner product F = [0, 0, 0] @@ -146,7 +146,7 @@ def inner_prod_V1(tensor_space_FEM, domain, fun): for a in range(3): Ni = tensor_space_FEM.Nbase_1form[a] - F[a] = np.zeros((Ni[0], Ni[1], Ni[2]), dtype=float) + F[a] = xp.zeros((Ni[0], Ni[1], Ni[2]), dtype=float) mat_f[:, :, :] = 0.0 @@ -185,7 +185,7 @@ def inner_prod_V1(tensor_space_FEM, domain, fun): mat_f * det_df, ) - return tensor_space_FEM.E1_0.dot(np.concatenate((F[0].flatten(), F[1].flatten(), F[2].flatten()))) + return tensor_space_FEM.E1_0.dot(xp.concatenate((F[0].flatten(), F[1].flatten(), F[2].flatten()))) # ================ inner product in V2 =========================== @@ -199,7 +199,7 @@ def inner_prod_V2(tensor_space_FEM, domain, fun): domain : domain domain object defining the geometry - fun : list of callables or np.ndarrays + fun : list of callables or xp.ndarrays the 2-form components with which the inner products shall be computed (either list of 3 callables or 3D arrays with values at quadrature points) """ @@ -232,10 +232,10 @@ def inner_prod_V2(tensor_space_FEM, domain, fun): g = domain.metric(pts[0].flatten(), pts[1].flatten(), pts[2].flatten()) # 2-form components at quadrature points - mat_f = np.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) + mat_f = xp.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) if callable(fun[0]): - quad_mesh = np.meshgrid(pts[0].flatten(), pts[1].flatten(), pts[2].flatten(), indexing="ij") + quad_mesh = xp.meshgrid(pts[0].flatten(), pts[1].flatten(), pts[2].flatten(), indexing="ij") # components of global inner product F = [0, 0, 0] @@ -244,7 +244,7 @@ def inner_prod_V2(tensor_space_FEM, domain, fun): for a in range(3): Ni = tensor_space_FEM.Nbase_2form[a] - F[a] = np.zeros((Ni[0], Ni[1], Ni[2]), dtype=float) + F[a] = xp.zeros((Ni[0], Ni[1], Ni[2]), dtype=float) mat_f[:, :, :] = 0.0 @@ -283,7 +283,7 @@ def inner_prod_V2(tensor_space_FEM, domain, fun): mat_f / det_df, ) - return tensor_space_FEM.E2_0.dot(np.concatenate((F[0].flatten(), F[1].flatten(), F[2].flatten()))) + return tensor_space_FEM.E2_0.dot(xp.concatenate((F[0].flatten(), F[1].flatten(), F[2].flatten()))) # ================ inner product in V3 =========================== @@ -297,7 +297,7 @@ def inner_prod_V3(tensor_space_FEM, domain, fun): domain : domain domain object defining the geometry - fun : callable or np.ndarray + fun : callable or xp.ndarray the 3-form component with which the inner products shall be computed (either callable or 3D array with values at quadrature points) """ @@ -318,10 +318,10 @@ def inner_prod_V3(tensor_space_FEM, domain, fun): det_df = det_df.reshape(Nel[0], n_quad[0], Nel[1], n_quad[1], Nel[2], n_quad[2]) # evaluation of given 3-form at quadrature points - mat_f = np.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) + mat_f = xp.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) if callable(fun): - quad_mesh = np.meshgrid(pts[0].flatten(), pts[1].flatten(), pts[2].flatten(), indexing="ij") + quad_mesh = xp.meshgrid(pts[0].flatten(), pts[1].flatten(), pts[2].flatten(), indexing="ij") mat_f[:, :, :] = fun(quad_mesh[0], quad_mesh[1], quad_mesh[2]) else: mat_f[:, :, :] = fun @@ -329,7 +329,7 @@ def inner_prod_V3(tensor_space_FEM, domain, fun): # assembly Ni = tensor_space.Nbase_3form - F = np.zeros((Ni[0], Ni[1], Ni[2]), dtype=float) + F = xp.zeros((Ni[0], Ni[1], Ni[2]), dtype=float) ker.kernel_inner( Nel[0], diff --git a/src/struphy/eigenvalue_solvers/legacy/l2_error_1d.py b/src/struphy/eigenvalue_solvers/legacy/l2_error_1d.py index f8544c1cf..d568d3207 100644 --- a/src/struphy/eigenvalue_solvers/legacy/l2_error_1d.py +++ b/src/struphy/eigenvalue_solvers/legacy/l2_error_1d.py @@ -6,10 +6,9 @@ Modules to compute L2-errors in 1d. """ +import cunumpy as xp import scipy.sparse as spa -from struphy.utils.arrays import xp as np - # ======= error in V0 ==================== def l2_error_V0(spline_space, mapping, coeff, fun): @@ -48,7 +47,7 @@ def l2_error_V0(spline_space, mapping, coeff, fun): mat_f = fun(pts) # assembly - error = np.zeros(Nel, dtype=float) + error = xp.zeros(Nel, dtype=float) for ie in range(Nel): for q in range(n_quad): @@ -59,7 +58,7 @@ def l2_error_V0(spline_space, mapping, coeff, fun): error[ie] += wts[ie, q] * (bi - mat_f[ie, q]) ** 2 - return np.sqrt(error.sum()) + return xp.sqrt(error.sum()) # ======= error in V1 ==================== @@ -99,7 +98,7 @@ def l2_error_V1(spline_space, mapping, coeff, fun): mat_f = fun(pts) # assembly - error = np.zeros(Nel, dtype=float) + error = xp.zeros(Nel, dtype=float) for ie in range(Nel): for q in range(n_quad): @@ -110,4 +109,4 @@ def l2_error_V1(spline_space, mapping, coeff, fun): error[ie] += wts[ie, q] * (bi - mat_f[ie, q]) ** 2 - return np.sqrt(error.sum()) + return xp.sqrt(error.sum()) diff --git a/src/struphy/eigenvalue_solvers/legacy/l2_error_2d.py b/src/struphy/eigenvalue_solvers/legacy/l2_error_2d.py index 818d0d7c2..452dd570b 100644 --- a/src/struphy/eigenvalue_solvers/legacy/l2_error_2d.py +++ b/src/struphy/eigenvalue_solvers/legacy/l2_error_2d.py @@ -6,10 +6,10 @@ Modules to compute L2-errors of discrete p-forms with analytical forms in 2D. """ +import cunumpy as xp import scipy.sparse as spa import struphy.eigenvalue_solvers.kernels_2d as ker -from struphy.utils.arrays import xp as np # ======= error in V0 ==================== @@ -25,7 +25,7 @@ def l2_error_V0(tensor_space_FEM, domain, f0, c0, method="standard"): domain : domain domain object defining the geometry - f0 : callable or np.ndarray + f0 : callable or xp.ndarray the 0-form with which the error shall be computed c0 : array_like @@ -63,12 +63,12 @@ def l2_error_V0(tensor_space_FEM, domain, f0, c0, method="standard"): # evaluation of exact 0-form at quadrature points if callable(f0): - quad_mesh = np.meshgrid(pts[0].flatten(), pts[1].flatten(), indexing="ij") + quad_mesh = xp.meshgrid(pts[0].flatten(), pts[1].flatten(), indexing="ij") f0 = f0(quad_mesh[0], quad_mesh[1], 0.0) if method == "standard": # evaluation of discrete 0-form at quadrature points - f0_h = tensor_space_FEM.evaluate_NN(pts[0].flatten(), pts[1].flatten(), np.array([0.0]), c0, "V0")[:, :, 0] + f0_h = tensor_space_FEM.evaluate_NN(pts[0].flatten(), pts[1].flatten(), xp.array([0.0]), c0, "V0")[:, :, 0] # compute error error = 0.0 @@ -78,7 +78,7 @@ def l2_error_V0(tensor_space_FEM, domain, f0, c0, method="standard"): else: # compute error in each element - error = np.zeros(Nel[:2], dtype=float) + error = xp.zeros(Nel[:2], dtype=float) ker.kernel_l2error( Nel, @@ -106,7 +106,7 @@ def l2_error_V0(tensor_space_FEM, domain, f0, c0, method="standard"): error = error.sum() - return np.sqrt(error) + return xp.sqrt(error) # ======= error in V1 ==================== @@ -122,7 +122,7 @@ def l2_error_V1(tensor_space_FEM, domain, f1, c1, method="standard"): domain : domain domain object defining the geometry - f1 : list of callables or np.ndarrays + f1 : list of callables or xp.ndarrays the three 1-form components with which the error shall be computed c1 : list of array_like @@ -162,16 +162,16 @@ def l2_error_V1(tensor_space_FEM, domain, f1, c1, method="standard"): # evaluation of exact 1-form components at quadrature points if callable(f1[0]): - quad_mesh = np.meshgrid(pts[0].flatten(), pts[1].flatten(), indexing="ij") + quad_mesh = xp.meshgrid(pts[0].flatten(), pts[1].flatten(), indexing="ij") f1_1 = f1[0](quad_mesh[0], quad_mesh[1], 0.0) f1_2 = f1[1](quad_mesh[0], quad_mesh[1], 0.0) f1_3 = f1[2](quad_mesh[0], quad_mesh[1], 0.0) if method == "standard": # evaluation of discrete 1-form components at quadrature points - f1_h_1 = tensor_space_FEM.evaluate_DN(pts[0].flatten(), pts[1].flatten(), np.array([0.0]), c1_1, "V1")[:, :, 0] - f1_h_2 = tensor_space_FEM.evaluate_ND(pts[0].flatten(), pts[1].flatten(), np.array([0.0]), c1_2, "V1")[:, :, 0] - f1_h_3 = tensor_space_FEM.evaluate_NN(pts[0].flatten(), pts[1].flatten(), np.array([0.0]), c1_3, "V1")[:, :, 0] + f1_h_1 = tensor_space_FEM.evaluate_DN(pts[0].flatten(), pts[1].flatten(), xp.array([0.0]), c1_1, "V1")[:, :, 0] + f1_h_2 = tensor_space_FEM.evaluate_ND(pts[0].flatten(), pts[1].flatten(), xp.array([0.0]), c1_2, "V1")[:, :, 0] + f1_h_3 = tensor_space_FEM.evaluate_NN(pts[0].flatten(), pts[1].flatten(), xp.array([0.0]), c1_3, "V1")[:, :, 0] # compute error error = 0.0 @@ -194,7 +194,7 @@ def l2_error_V1(tensor_space_FEM, domain, f1, c1, method="standard"): else: # compute error in each element - error = np.zeros(Nel[:2], dtype=float) + error = xp.zeros(Nel[:2], dtype=float) # 1 * d_f1 * G^11 * |det(DF)| * d_f1 ker.kernel_l2error( @@ -298,7 +298,7 @@ def l2_error_V1(tensor_space_FEM, domain, f1, c1, method="standard"): error = error.sum() - return np.sqrt(error) + return xp.sqrt(error) # ======= error in V2 ==================== @@ -314,7 +314,7 @@ def l2_error_V2(tensor_space_FEM, domain, f2, c2, method="standard"): domain : domain domain object defining the geometry - f2 : list of callables or np.ndarrays + f2 : list of callables or xp.ndarrays the three 2-form components with which the error shall be computed c2 : list of array_like @@ -354,16 +354,16 @@ def l2_error_V2(tensor_space_FEM, domain, f2, c2, method="standard"): # evaluation of exact 2-form components at quadrature points if callable(f2[0]): - quad_mesh = np.meshgrid(pts[0].flatten(), pts[1].flatten(), indexing="ij") + quad_mesh = xp.meshgrid(pts[0].flatten(), pts[1].flatten(), indexing="ij") f2_1 = f2[0](quad_mesh[0], quad_mesh[1], 0.0) f2_2 = f2[1](quad_mesh[0], quad_mesh[1], 0.0) f2_3 = f2[2](quad_mesh[0], quad_mesh[1], 0.0) if method == "standard": # evaluation of discrete 2-form components at quadrature points - f2_h_1 = tensor_space_FEM.evaluate_ND(pts[0].flatten(), pts[1].flatten(), np.array([0.0]), c2_1, "V2")[:, :, 0] - f2_h_2 = tensor_space_FEM.evaluate_DN(pts[0].flatten(), pts[1].flatten(), np.array([0.0]), c2_2, "V2")[:, :, 0] - f2_h_3 = tensor_space_FEM.evaluate_DD(pts[0].flatten(), pts[1].flatten(), np.array([0.0]), c2_3, "V2")[:, :, 0] + f2_h_1 = tensor_space_FEM.evaluate_ND(pts[0].flatten(), pts[1].flatten(), xp.array([0.0]), c2_1, "V2")[:, :, 0] + f2_h_2 = tensor_space_FEM.evaluate_DN(pts[0].flatten(), pts[1].flatten(), xp.array([0.0]), c2_2, "V2")[:, :, 0] + f2_h_3 = tensor_space_FEM.evaluate_DD(pts[0].flatten(), pts[1].flatten(), xp.array([0.0]), c2_3, "V2")[:, :, 0] # compute error error = 0.0 @@ -386,7 +386,7 @@ def l2_error_V2(tensor_space_FEM, domain, f2, c2, method="standard"): else: # compute error in each element - error = np.zeros(Nel[:2], dtype=float) + error = xp.zeros(Nel[:2], dtype=float) # 1 * d_f1 * G_11 / |det(DF)| * d_f1 ker.kernel_l2error( @@ -490,7 +490,7 @@ def l2_error_V2(tensor_space_FEM, domain, f2, c2, method="standard"): error = error.sum() - return np.sqrt(error) + return xp.sqrt(error) # ======= error in V3 ==================== @@ -506,7 +506,7 @@ def l2_error_V3(tensor_space_FEM, domain, f3, c3, method="standard"): domain : domain domain object defining the geometry - f3 : callable or np.ndarray + f3 : callable or xp.ndarray the 3-form component with which the error shall be computed c3 : array_like @@ -544,12 +544,12 @@ def l2_error_V3(tensor_space_FEM, domain, f3, c3, method="standard"): # evaluation of exact 3-form at quadrature points if callable(f3): - quad_mesh = np.meshgrid(pts[0].flatten(), pts[1].flatten(), indexing="ij") + quad_mesh = xp.meshgrid(pts[0].flatten(), pts[1].flatten(), indexing="ij") f3 = f3(quad_mesh[0], quad_mesh[1], 0.0) if method == "standard": # evaluation of discrete 3-form at quadrature points - f3_h = tensor_space_FEM.evaluate_DD(pts[0].flatten(), pts[1].flatten(), np.array([0.0]), c3, "V3")[:, :, 0] + f3_h = tensor_space_FEM.evaluate_DD(pts[0].flatten(), pts[1].flatten(), xp.array([0.0]), c3, "V3")[:, :, 0] # compute error error = 0.0 @@ -559,7 +559,7 @@ def l2_error_V3(tensor_space_FEM, domain, f3, c3, method="standard"): else: # compute error in each element - error = np.zeros(Nel[:2], dtype=float) + error = xp.zeros(Nel[:2], dtype=float) ker.kernel_l2error( Nel, @@ -587,4 +587,4 @@ def l2_error_V3(tensor_space_FEM, domain, f3, c3, method="standard"): error = error.sum() - return np.sqrt(error) + return xp.sqrt(error) diff --git a/src/struphy/eigenvalue_solvers/legacy/l2_error_3d.py b/src/struphy/eigenvalue_solvers/legacy/l2_error_3d.py index 39eac0b66..7553e3a83 100644 --- a/src/struphy/eigenvalue_solvers/legacy/l2_error_3d.py +++ b/src/struphy/eigenvalue_solvers/legacy/l2_error_3d.py @@ -6,10 +6,10 @@ Modules to compute L2-errors of discrete p-forms with analytical forms in 3D. """ +import cunumpy as xp import scipy.sparse as spa import struphy.eigenvalue_solvers.kernels_3d as ker -from struphy.utils.arrays import xp as np # ======= error in V0 ==================== @@ -25,7 +25,7 @@ def l2_error_V0(tensor_space_FEM, domain, fun, coeff): domain : domain domain object defining the geometry - fun : callable or np.ndarray + fun : callable or xp.ndarray the 0-form with which the error shall be computed coeff : array_like @@ -54,16 +54,16 @@ def l2_error_V0(tensor_space_FEM, domain, fun, coeff): det_df = abs(domain.jacobian_det(pts[0].flatten(), pts[1].flatten(), pts[2].flatten())) # evaluation of given 0-form at quadrature points - mat_f = np.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) + mat_f = xp.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) if callable(fun): - quad_mesh = np.meshgrid(pts[0].flatten(), pts[1].flatten(), pts[2].flatten(), indexing="ij") + quad_mesh = xp.meshgrid(pts[0].flatten(), pts[1].flatten(), pts[2].flatten(), indexing="ij") mat_f[:, :, :] = fun(quad_mesh[0], quad_mesh[1], quad_mesh[2]) else: mat_f[:, :, :] = fun # compute error - error = np.zeros(Nel, dtype=float) + error = xp.zeros(Nel, dtype=float) ker.kernel_l2error( Nel, @@ -94,7 +94,7 @@ def l2_error_V0(tensor_space_FEM, domain, fun, coeff): det_df.reshape(Nel[0], n_quad[0], Nel[1], n_quad[1], Nel[2], n_quad[2]), ) - return np.sqrt(error.sum()) + return xp.sqrt(error.sum()) # ======= error in V1 ==================== @@ -110,7 +110,7 @@ def l2_error_V1(tensor_space_FEM, domain, fun, coeff): domain : domain domain object defining the geometry - fun : list of callables or np.ndarrays + fun : list of callables or xp.ndarrays the three 1-form components with which the error shall be computed coeff : list of array_like @@ -141,12 +141,12 @@ def l2_error_V1(tensor_space_FEM, domain, fun, coeff): metric_coeffs *= abs(domain.jacobian_det(pts[0].flatten(), pts[1].flatten(), pts[2].flatten())) # evaluation of given 1-form components at quadrature points - mat_f1 = np.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) - mat_f2 = np.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) - mat_f3 = np.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) + mat_f1 = xp.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) + mat_f2 = xp.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) + mat_f3 = xp.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) if callable(fun[0]): - quad_mesh = np.meshgrid(pts[0].flatten(), pts[1].flatten(), pts[2].flatten(), indexing="ij") + quad_mesh = xp.meshgrid(pts[0].flatten(), pts[1].flatten(), pts[2].flatten(), indexing="ij") mat_f1[:, :, :] = fun[0](quad_mesh[0], quad_mesh[1], quad_mesh[2]) mat_f2[:, :, :] = fun[1](quad_mesh[0], quad_mesh[1], quad_mesh[2]) mat_f3[:, :, :] = fun[2](quad_mesh[0], quad_mesh[1], quad_mesh[2]) @@ -156,7 +156,7 @@ def l2_error_V1(tensor_space_FEM, domain, fun, coeff): mat_f3[:, :, :] = fun[2] # compute error - error = np.zeros(Nel, dtype=float) + error = xp.zeros(Nel, dtype=float) # 1 * f1 * G^11 * |det(DF)| * f1 ker.kernel_l2error( @@ -314,7 +314,7 @@ def l2_error_V1(tensor_space_FEM, domain, fun, coeff): 1 * metric_coeffs[2, 2].reshape(Nel[0], n_quad[0], Nel[1], n_quad[1], Nel[2], n_quad[2]), ) - return np.sqrt(error.sum()) + return xp.sqrt(error.sum()) # ======= error in V2 ==================== @@ -330,7 +330,7 @@ def l2_error_V2(tensor_space_FEM, domain, fun, coeff): domain : domain domain object defining the geometry - fun : list of callables or np.ndarrays + fun : list of callables or xp.ndarrays the three 2-form components with which the error shall be computed coeff : list of array_like @@ -361,12 +361,12 @@ def l2_error_V2(tensor_space_FEM, domain, fun, coeff): metric_coeffs /= abs(domain.jacobian_det(pts[0].flatten(), pts[1].flatten(), pts[2].flatten())) # evaluation of given 2-form components at quadrature points - mat_f1 = np.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) - mat_f2 = np.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) - mat_f3 = np.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) + mat_f1 = xp.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) + mat_f2 = xp.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) + mat_f3 = xp.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) if callable(fun[0]): - quad_mesh = np.meshgrid(pts[0].flatten(), pts[1].flatten(), pts[2].flatten(), indexing="ij") + quad_mesh = xp.meshgrid(pts[0].flatten(), pts[1].flatten(), pts[2].flatten(), indexing="ij") mat_f1[:, :, :] = fun[0](quad_mesh[0], quad_mesh[1], quad_mesh[2]) mat_f2[:, :, :] = fun[1](quad_mesh[0], quad_mesh[1], quad_mesh[2]) mat_f3[:, :, :] = fun[2](quad_mesh[0], quad_mesh[1], quad_mesh[2]) @@ -376,7 +376,7 @@ def l2_error_V2(tensor_space_FEM, domain, fun, coeff): mat_f3[:, :, :] = fun[2] # compute error - error = np.zeros(Nel, dtype=float) + error = xp.zeros(Nel, dtype=float) # 1 * f1 * G_11 / |det(DF)| * f1 ker.kernel_l2error( @@ -534,7 +534,7 @@ def l2_error_V2(tensor_space_FEM, domain, fun, coeff): 1 * metric_coeffs[2, 2].reshape(Nel[0], n_quad[0], Nel[1], n_quad[1], Nel[2], n_quad[2]), ) - return np.sqrt(error.sum()) + return xp.sqrt(error.sum()) # ======= error in V3 ==================== @@ -550,7 +550,7 @@ def l2_error_V3(tensor_space_FEM, domain, fun, coeff): domain : domain domain object defining the geometry - fun : callable or np.ndarray + fun : callable or xp.ndarray the 3-form component with which the error shall be computed coeff : array_like @@ -579,16 +579,16 @@ def l2_error_V3(tensor_space_FEM, domain, fun, coeff): det_df = abs(domain.jacobian_det(pts[0].flatten(), pts[1].flatten(), pts[2].flatten())) # evaluation of given 3-form component at quadrature points - mat_f = np.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) + mat_f = xp.empty((pts[0].size, pts[1].size, pts[2].size), dtype=float) if callable(fun): - quad_mesh = np.meshgrid(pts[0].flatten(), pts[1].flatten(), pts[2].flatten(), indexing="ij") + quad_mesh = xp.meshgrid(pts[0].flatten(), pts[1].flatten(), pts[2].flatten(), indexing="ij") mat_f[:, :, :] = fun(quad_mesh[0], quad_mesh[1], quad_mesh[2]) else: mat_f[:, :, :] = fun # compute error - error = np.zeros(Nel, dtype=float) + error = xp.zeros(Nel, dtype=float) ker.kernel_l2error( Nel, @@ -619,4 +619,4 @@ def l2_error_V3(tensor_space_FEM, domain, fun, coeff): 1 / det_df.reshape(Nel[0], n_quad[0], Nel[1], n_quad[1], Nel[2], n_quad[2]), ) - return np.sqrt(error.sum()) + return xp.sqrt(error.sum()) diff --git a/src/struphy/eigenvalue_solvers/legacy/mass_matrices_3d_pre.py b/src/struphy/eigenvalue_solvers/legacy/mass_matrices_3d_pre.py index 673069c0e..eecb68293 100644 --- a/src/struphy/eigenvalue_solvers/legacy/mass_matrices_3d_pre.py +++ b/src/struphy/eigenvalue_solvers/legacy/mass_matrices_3d_pre.py @@ -6,11 +6,11 @@ Modules to obtain preconditioners for mass matrices in 3D. """ +import cunumpy as xp import scipy.sparse as spa import struphy.eigenvalue_solvers.spline_space as spl import struphy.linear_algebra.linalg_kron as linkron -from struphy.utils.arrays import xp as np # ================ inverse mass matrix in V0 =========================== @@ -32,9 +32,9 @@ def get_M0_PRE(tensor_space_FEM, domain): # spaces_pre[1].set_extraction_operators() # spaces_pre[2].set_extraction_operators() - spaces_pre[0].assemble_M0(lambda eta: (domain.params[1] - domain.params[0]) * np.ones(eta.shape, dtype=float)) - spaces_pre[1].assemble_M0(lambda eta: (domain.params[3] - domain.params[2]) * np.ones(eta.shape, dtype=float)) - spaces_pre[2].assemble_M0(lambda eta: (domain.params[5] - domain.params[4]) * np.ones(eta.shape, dtype=float)) + spaces_pre[0].assemble_M0(lambda eta: (domain.params[1] - domain.params[0]) * xp.ones(eta.shape, dtype=float)) + spaces_pre[1].assemble_M0(lambda eta: (domain.params[3] - domain.params[2]) * xp.ones(eta.shape, dtype=float)) + spaces_pre[2].assemble_M0(lambda eta: (domain.params[5] - domain.params[4]) * xp.ones(eta.shape, dtype=float)) c_pre = [spaces_pre[0].M0.toarray()[:, 0], spaces_pre[1].M0.toarray()[:, 0], spaces_pre[2].M0.toarray()[:, 0]] @@ -63,20 +63,20 @@ def get_M1_PRE(tensor_space_FEM, domain): # spaces_pre[1].set_extraction_operators() # spaces_pre[2].set_extraction_operators() - spaces_pre[0].assemble_M0(lambda eta: (domain.params[1] - domain.params[0]) * np.ones(eta.shape, dtype=float)) - spaces_pre[1].assemble_M0(lambda eta: (domain.params[3] - domain.params[2]) * np.ones(eta.shape, dtype=float)) - spaces_pre[2].assemble_M0(lambda eta: (domain.params[5] - domain.params[4]) * np.ones(eta.shape, dtype=float)) + spaces_pre[0].assemble_M0(lambda eta: (domain.params[1] - domain.params[0]) * xp.ones(eta.shape, dtype=float)) + spaces_pre[1].assemble_M0(lambda eta: (domain.params[3] - domain.params[2]) * xp.ones(eta.shape, dtype=float)) + spaces_pre[2].assemble_M0(lambda eta: (domain.params[5] - domain.params[4]) * xp.ones(eta.shape, dtype=float)) - spaces_pre[0].assemble_M1(lambda eta: 1 / (domain.params[1] - domain.params[0]) * np.ones(eta.shape, dtype=float)) - spaces_pre[1].assemble_M1(lambda eta: 1 / (domain.params[3] - domain.params[2]) * np.ones(eta.shape, dtype=float)) - spaces_pre[2].assemble_M1(lambda eta: 1 / (domain.params[5] - domain.params[4]) * np.ones(eta.shape, dtype=float)) + spaces_pre[0].assemble_M1(lambda eta: 1 / (domain.params[1] - domain.params[0]) * xp.ones(eta.shape, dtype=float)) + spaces_pre[1].assemble_M1(lambda eta: 1 / (domain.params[3] - domain.params[2]) * xp.ones(eta.shape, dtype=float)) + spaces_pre[2].assemble_M1(lambda eta: 1 / (domain.params[5] - domain.params[4]) * xp.ones(eta.shape, dtype=float)) c11_pre = [spaces_pre[0].M1.toarray()[:, 0], spaces_pre[1].M0.toarray()[:, 0], spaces_pre[2].M0.toarray()[:, 0]] c22_pre = [spaces_pre[0].M0.toarray()[:, 0], spaces_pre[1].M1.toarray()[:, 0], spaces_pre[2].M0.toarray()[:, 0]] c33_pre = [spaces_pre[0].M0.toarray()[:, 0], spaces_pre[1].M0.toarray()[:, 0], spaces_pre[2].M1.toarray()[:, 0]] def solve(x): - x1, x2, x3 = np.split(x, 3) + x1, x2, x3 = xp.split(x, 3) x1 = x1.reshape(Nel_pre[0], Nel_pre[1], Nel_pre[2]) x2 = x2.reshape(Nel_pre[0], Nel_pre[1], Nel_pre[2]) @@ -86,7 +86,7 @@ def solve(x): r2 = linkron.kron_fftsolve_3d(c22_pre, x2).flatten() r3 = linkron.kron_fftsolve_3d(c33_pre, x3).flatten() - return np.concatenate((r1, r2, r3)) + return xp.concatenate((r1, r2, r3)) return spa.linalg.LinearOperator(shape=tensor_space_FEM.M1.shape, matvec=solve) @@ -110,20 +110,20 @@ def get_M2_PRE(tensor_space_FEM, domain): # spaces_pre[1].set_extraction_operators() # spaces_pre[2].set_extraction_operators() - spaces_pre[0].assemble_M0(lambda eta: (domain.params[1] - domain.params[0]) * np.ones(eta.shape, dtype=float)) - spaces_pre[1].assemble_M0(lambda eta: (domain.params[3] - domain.params[2]) * np.ones(eta.shape, dtype=float)) - spaces_pre[2].assemble_M0(lambda eta: (domain.params[5] - domain.params[4]) * np.ones(eta.shape, dtype=float)) + spaces_pre[0].assemble_M0(lambda eta: (domain.params[1] - domain.params[0]) * xp.ones(eta.shape, dtype=float)) + spaces_pre[1].assemble_M0(lambda eta: (domain.params[3] - domain.params[2]) * xp.ones(eta.shape, dtype=float)) + spaces_pre[2].assemble_M0(lambda eta: (domain.params[5] - domain.params[4]) * xp.ones(eta.shape, dtype=float)) - spaces_pre[0].assemble_M1(lambda eta: 1 / (domain.params[1] - domain.params[0]) * np.ones(eta.shape, dtype=float)) - spaces_pre[1].assemble_M1(lambda eta: 1 / (domain.params[3] - domain.params[2]) * np.ones(eta.shape, dtype=float)) - spaces_pre[2].assemble_M1(lambda eta: 1 / (domain.params[5] - domain.params[4]) * np.ones(eta.shape, dtype=float)) + spaces_pre[0].assemble_M1(lambda eta: 1 / (domain.params[1] - domain.params[0]) * xp.ones(eta.shape, dtype=float)) + spaces_pre[1].assemble_M1(lambda eta: 1 / (domain.params[3] - domain.params[2]) * xp.ones(eta.shape, dtype=float)) + spaces_pre[2].assemble_M1(lambda eta: 1 / (domain.params[5] - domain.params[4]) * xp.ones(eta.shape, dtype=float)) c11_pre = [spaces_pre[0].M0.toarray()[:, 0], spaces_pre[1].M1.toarray()[:, 0], spaces_pre[2].M1.toarray()[:, 0]] c22_pre = [spaces_pre[0].M1.toarray()[:, 0], spaces_pre[1].M0.toarray()[:, 0], spaces_pre[2].M1.toarray()[:, 0]] c33_pre = [spaces_pre[0].M1.toarray()[:, 0], spaces_pre[1].M1.toarray()[:, 0], spaces_pre[2].M0.toarray()[:, 0]] def solve(x): - x1, x2, x3 = np.split(x, 3) + x1, x2, x3 = xp.split(x, 3) x1 = x1.reshape(Nel_pre[0], Nel_pre[1], Nel_pre[2]) x2 = x2.reshape(Nel_pre[0], Nel_pre[1], Nel_pre[2]) @@ -133,7 +133,7 @@ def solve(x): r2 = linkron.kron_fftsolve_3d(c22_pre, x2).flatten() r3 = linkron.kron_fftsolve_3d(c33_pre, x3).flatten() - return np.concatenate((r1, r2, r3)) + return xp.concatenate((r1, r2, r3)) return spa.linalg.LinearOperator(shape=tensor_space_FEM.M2.shape, matvec=solve) @@ -157,9 +157,9 @@ def get_M3_PRE(tensor_space_FEM, domain): # spaces_pre[1].set_extraction_operators() # spaces_pre[2].set_extraction_operators() - spaces_pre[0].assemble_M1(lambda eta: 1 / (domain.params[1] - domain.params[0]) * np.ones(eta.shape, dtype=float)) - spaces_pre[1].assemble_M1(lambda eta: 1 / (domain.params[3] - domain.params[2]) * np.ones(eta.shape, dtype=float)) - spaces_pre[2].assemble_M1(lambda eta: 1 / (domain.params[5] - domain.params[4]) * np.ones(eta.shape, dtype=float)) + spaces_pre[0].assemble_M1(lambda eta: 1 / (domain.params[1] - domain.params[0]) * xp.ones(eta.shape, dtype=float)) + spaces_pre[1].assemble_M1(lambda eta: 1 / (domain.params[3] - domain.params[2]) * xp.ones(eta.shape, dtype=float)) + spaces_pre[2].assemble_M1(lambda eta: 1 / (domain.params[5] - domain.params[4]) * xp.ones(eta.shape, dtype=float)) c_pre = [spaces_pre[0].M1.toarray()[:, 0], spaces_pre[1].M1.toarray()[:, 0], spaces_pre[2].M1.toarray()[:, 0]] @@ -188,26 +188,26 @@ def get_Mv_PRE(tensor_space_FEM, domain): # spaces_pre[1].set_extraction_operators() # spaces_pre[2].set_extraction_operators() - spaces_pre[0].assemble_M0(lambda eta: domain.params[0] ** 3 * np.ones(eta.shape, dtype=float)) - spaces_pre[1].assemble_M0(lambda eta: domain.params[1] * np.ones(eta.shape, dtype=float)) - spaces_pre[2].assemble_M0(lambda eta: domain.params[2] * np.ones(eta.shape, dtype=float)) + spaces_pre[0].assemble_M0(lambda eta: domain.params[0] ** 3 * xp.ones(eta.shape, dtype=float)) + spaces_pre[1].assemble_M0(lambda eta: domain.params[1] * xp.ones(eta.shape, dtype=float)) + spaces_pre[2].assemble_M0(lambda eta: domain.params[2] * xp.ones(eta.shape, dtype=float)) c11_pre = [spaces_pre[0].M0.toarray()[:, 0], spaces_pre[1].M0.toarray()[:, 0], spaces_pre[2].M0.toarray()[:, 0]] - spaces_pre[0].assemble_M0(lambda eta: domain.params[0] * np.ones(eta.shape, dtype=float)) - spaces_pre[1].assemble_M0(lambda eta: domain.params[1] ** 3 * np.ones(eta.shape, dtype=float)) - spaces_pre[2].assemble_M0(lambda eta: domain.params[2] * np.ones(eta.shape, dtype=float)) + spaces_pre[0].assemble_M0(lambda eta: domain.params[0] * xp.ones(eta.shape, dtype=float)) + spaces_pre[1].assemble_M0(lambda eta: domain.params[1] ** 3 * xp.ones(eta.shape, dtype=float)) + spaces_pre[2].assemble_M0(lambda eta: domain.params[2] * xp.ones(eta.shape, dtype=float)) c22_pre = [spaces_pre[0].M0.toarray()[:, 0], spaces_pre[1].M0.toarray()[:, 0], spaces_pre[2].M0.toarray()[:, 0]] - spaces_pre[0].assemble_M0(lambda eta: domain.params[0] * np.ones(eta.shape, dtype=float)) - spaces_pre[1].assemble_M0(lambda eta: domain.params[1] * np.ones(eta.shape, dtype=float)) - spaces_pre[2].assemble_M0(lambda eta: domain.params[2] ** 3 * np.ones(eta.shape, dtype=float)) + spaces_pre[0].assemble_M0(lambda eta: domain.params[0] * xp.ones(eta.shape, dtype=float)) + spaces_pre[1].assemble_M0(lambda eta: domain.params[1] * xp.ones(eta.shape, dtype=float)) + spaces_pre[2].assemble_M0(lambda eta: domain.params[2] ** 3 * xp.ones(eta.shape, dtype=float)) c33_pre = [spaces_pre[0].M0.toarray()[:, 0], spaces_pre[1].M0.toarray()[:, 0], spaces_pre[2].M0.toarray()[:, 0]] def solve(x): - x1, x2, x3 = np.split(x, 3) + x1, x2, x3 = xp.split(x, 3) x1 = x1.reshape(Nel_pre[0], Nel_pre[1], Nel_pre[2]) x2 = x2.reshape(Nel_pre[0], Nel_pre[1], Nel_pre[2]) @@ -217,7 +217,7 @@ def solve(x): r2 = linkron.kron_fftsolve_3d(c22_pre, x2).flatten() r3 = linkron.kron_fftsolve_3d(c33_pre, x3).flatten() - return np.concatenate((r1, r2, r3)) + return xp.concatenate((r1, r2, r3)) return spa.linalg.LinearOperator(shape=tensor_space_FEM.Mv.shape, matvec=solve) @@ -282,7 +282,7 @@ def solve(x): r1 = linkron.kron_fftsolve_2d(M1_pol_0_11_LU, tor_vec0, x1).flatten() r2 = linkron.kron_fftsolve_2d(M1_pol_0_22_LU, tor_vec1, x2).flatten() - return np.concatenate((r1, r2)) + return xp.concatenate((r1, r2)) return spa.linalg.LinearOperator(shape=tensor_space_FEM.M1_0.shape, matvec=solve) @@ -320,7 +320,7 @@ def solve(x): r1 = linkron.kron_fftsolve_2d(M2_pol_0_11_LU, tor_vec1, x1).flatten() r2 = linkron.kron_fftsolve_2d(M2_pol_0_22_LU, tor_vec0, x2).flatten() - return np.concatenate((r1, r2)) + return xp.concatenate((r1, r2)) return spa.linalg.LinearOperator(shape=tensor_space_FEM.M2_0.shape, matvec=solve) @@ -382,6 +382,6 @@ def solve(x): r1 = linkron.kron_fftsolve_2d(Mv_pol_0_11_LU, tor_vec0, x1).flatten() r2 = linkron.kron_fftsolve_2d(Mv_pol_0_22_LU, tor_vec0, x2).flatten() - return np.concatenate((r1, r2)) + return xp.concatenate((r1, r2)) return spa.linalg.LinearOperator(shape=tensor_space_FEM.Mv_0.shape, matvec=solve) diff --git a/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_arrays.py b/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_arrays.py index 8514d25fc..e74302878 100644 --- a/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_arrays.py +++ b/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_arrays.py @@ -1,13 +1,13 @@ import time import timeit +import cunumpy as xp import scipy.sparse as spa from psydac.ddm.mpi import mpi as MPI import struphy.geometry.mappings_3d as mapping3d import struphy.geometry.mappings_3d_fast as mapping_fast import struphy.linear_algebra.linalg_kernels as linalg -from struphy.utils.arrays import xp as np class Temp_arrays: @@ -39,67 +39,67 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): self.Ntot_1form = TENSOR_SPACE_FEM.Ntot_1form self.Ntot_2form = TENSOR_SPACE_FEM.Ntot_2form - self.b1_old = np.empty(TENSOR_SPACE_FEM.Nbase_1form[0], dtype=float) - self.b2_old = np.empty(TENSOR_SPACE_FEM.Nbase_1form[1], dtype=float) - self.b3_old = np.empty(TENSOR_SPACE_FEM.Nbase_1form[2], dtype=float) + self.b1_old = xp.empty(TENSOR_SPACE_FEM.Nbase_1form[0], dtype=float) + self.b2_old = xp.empty(TENSOR_SPACE_FEM.Nbase_1form[1], dtype=float) + self.b3_old = xp.empty(TENSOR_SPACE_FEM.Nbase_1form[2], dtype=float) - self.b1_iter = np.empty(TENSOR_SPACE_FEM.Nbase_1form[0], dtype=float) - self.b2_iter = np.empty(TENSOR_SPACE_FEM.Nbase_1form[1], dtype=float) - self.b3_iter = np.empty(TENSOR_SPACE_FEM.Nbase_1form[2], dtype=float) + self.b1_iter = xp.empty(TENSOR_SPACE_FEM.Nbase_1form[0], dtype=float) + self.b2_iter = xp.empty(TENSOR_SPACE_FEM.Nbase_1form[1], dtype=float) + self.b3_iter = xp.empty(TENSOR_SPACE_FEM.Nbase_1form[2], dtype=float) - self.temp_dft = np.empty((3, 3), dtype=float) - self.temp_generate_weight1 = np.empty(3, dtype=float) - self.temp_generate_weight2 = np.empty(3, dtype=float) - self.temp_generate_weight3 = np.empty(3, dtype=float) + self.temp_dft = xp.empty((3, 3), dtype=float) + self.temp_generate_weight1 = xp.empty(3, dtype=float) + self.temp_generate_weight2 = xp.empty(3, dtype=float) + self.temp_generate_weight3 = xp.empty(3, dtype=float) - self.zerosform_temp_long = np.empty(TENSOR_SPACE_FEM.Ntot_0form, dtype=float) - self.oneform_temp1_long = np.empty(TENSOR_SPACE_FEM.Ntot_1form[0], dtype=float) - self.oneform_temp2_long = np.empty(TENSOR_SPACE_FEM.Ntot_1form[1], dtype=float) - self.oneform_temp3_long = np.empty(TENSOR_SPACE_FEM.Ntot_1form[2], dtype=float) + self.zerosform_temp_long = xp.empty(TENSOR_SPACE_FEM.Ntot_0form, dtype=float) + self.oneform_temp1_long = xp.empty(TENSOR_SPACE_FEM.Ntot_1form[0], dtype=float) + self.oneform_temp2_long = xp.empty(TENSOR_SPACE_FEM.Ntot_1form[1], dtype=float) + self.oneform_temp3_long = xp.empty(TENSOR_SPACE_FEM.Ntot_1form[2], dtype=float) - self.oneform_temp_long = np.empty( + self.oneform_temp_long = xp.empty( TENSOR_SPACE_FEM.Ntot_1form[0] + TENSOR_SPACE_FEM.Ntot_1form[1] + TENSOR_SPACE_FEM.Ntot_1form[2], dtype=float, ) - self.twoform_temp1_long = np.empty(TENSOR_SPACE_FEM.Ntot_2form[0], dtype=float) - self.twoform_temp2_long = np.empty(TENSOR_SPACE_FEM.Ntot_2form[1], dtype=float) - self.twoform_temp3_long = np.empty(TENSOR_SPACE_FEM.Ntot_2form[2], dtype=float) + self.twoform_temp1_long = xp.empty(TENSOR_SPACE_FEM.Ntot_2form[0], dtype=float) + self.twoform_temp2_long = xp.empty(TENSOR_SPACE_FEM.Ntot_2form[1], dtype=float) + self.twoform_temp3_long = xp.empty(TENSOR_SPACE_FEM.Ntot_2form[2], dtype=float) - self.twoform_temp_long = np.empty( + self.twoform_temp_long = xp.empty( TENSOR_SPACE_FEM.Ntot_2form[0] + TENSOR_SPACE_FEM.Ntot_2form[1] + TENSOR_SPACE_FEM.Ntot_2form[2], dtype=float, ) - self.temp_twoform1 = np.empty(TENSOR_SPACE_FEM.Nbase_2form[0], dtype=float) - self.temp_twoform2 = np.empty(TENSOR_SPACE_FEM.Nbase_2form[1], dtype=float) - self.temp_twoform3 = np.empty(TENSOR_SPACE_FEM.Nbase_2form[2], dtype=float) + self.temp_twoform1 = xp.empty(TENSOR_SPACE_FEM.Nbase_2form[0], dtype=float) + self.temp_twoform2 = xp.empty(TENSOR_SPACE_FEM.Nbase_2form[1], dtype=float) + self.temp_twoform3 = xp.empty(TENSOR_SPACE_FEM.Nbase_2form[2], dtype=float) # arrays used to store intermidaite values - self.form_0_flatten = np.empty(self.Ntot_0form, dtype=float) + self.form_0_flatten = xp.empty(self.Ntot_0form, dtype=float) - self.form_1_1_flatten = np.empty(self.Ntot_1form[0], dtype=float) - self.form_1_2_flatten = np.empty(self.Ntot_1form[1], dtype=float) - self.form_1_3_flatten = np.empty(self.Ntot_1form[2], dtype=float) + self.form_1_1_flatten = xp.empty(self.Ntot_1form[0], dtype=float) + self.form_1_2_flatten = xp.empty(self.Ntot_1form[1], dtype=float) + self.form_1_3_flatten = xp.empty(self.Ntot_1form[2], dtype=float) - self.form_1_tot_flatten = np.empty(self.Ntot_1form[0] + self.Ntot_1form[1] + self.Ntot_1form[2], dtype=float) + self.form_1_tot_flatten = xp.empty(self.Ntot_1form[0] + self.Ntot_1form[1] + self.Ntot_1form[2], dtype=float) - self.form_2_1_flatten = np.empty(self.Ntot_2form[0], dtype=float) - self.form_2_2_flatten = np.empty(self.Ntot_2form[1], dtype=float) - self.form_2_3_flatten = np.empty(self.Ntot_2form[2], dtype=float) + self.form_2_1_flatten = xp.empty(self.Ntot_2form[0], dtype=float) + self.form_2_2_flatten = xp.empty(self.Ntot_2form[1], dtype=float) + self.form_2_3_flatten = xp.empty(self.Ntot_2form[2], dtype=float) - self.form_2_tot_flatten = np.empty(self.Ntot_2form[0] + self.Ntot_2form[1] + self.Ntot_2form[2], dtype=float) + self.form_2_tot_flatten = xp.empty(self.Ntot_2form[0] + self.Ntot_2form[1] + self.Ntot_2form[2], dtype=float) - self.bulkspeed_loc = np.zeros((3, self.Nel[0], self.Nel[1], self.Nel[2]), dtype=float) - self.temperature_loc = np.zeros((3, self.Nel[0], self.Nel[1], self.Nel[2]), dtype=float) - self.bulkspeed = np.zeros((3, self.Nel[0], self.Nel[1], self.Nel[2]), dtype=float) + self.bulkspeed_loc = xp.zeros((3, self.Nel[0], self.Nel[1], self.Nel[2]), dtype=float) + self.temperature_loc = xp.zeros((3, self.Nel[0], self.Nel[1], self.Nel[2]), dtype=float) + self.bulkspeed = xp.zeros((3, self.Nel[0], self.Nel[1], self.Nel[2]), dtype=float) if self.mpi_rank == 0: - temperature = np.zeros((3, self.Nel[0], self.Nel[1], self.Nel[2]), dtype=float) + temperature = xp.zeros((3, self.Nel[0], self.Nel[1], self.Nel[2]), dtype=float) else: temperature = None # values of magnetic fields at all quadrature points - self.LO_inv = np.empty( + self.LO_inv = xp.empty( ( self.Nel[0], self.Nel[1], @@ -111,7 +111,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): dtype=float, ) - self.LO_b1 = np.empty( + self.LO_b1 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -122,7 +122,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.LO_b2 = np.empty( + self.LO_b2 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -133,7 +133,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.LO_b3 = np.empty( + self.LO_b3 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -145,7 +145,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): dtype=float, ) # values of weights (used in the linear operators) - self.LO_w1 = np.empty( + self.LO_w1 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -156,7 +156,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.LO_w2 = np.empty( + self.LO_w2 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -167,7 +167,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.LO_w3 = np.empty( + self.LO_w3 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -179,7 +179,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): dtype=float, ) # values of a function (given its finite element coefficients) at all quadrature points - self.LO_r1 = np.empty( + self.LO_r1 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -190,7 +190,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.LO_r2 = np.empty( + self.LO_r2 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -201,7 +201,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.LO_r3 = np.empty( + self.LO_r3 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -213,7 +213,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): dtype=float, ) # values of determinant of Jacobi matrix of the map at all quadrature points - self.df_det = np.empty( + self.df_det = xp.empty( ( self.Nel[0], self.Nel[1], @@ -226,7 +226,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ) # when using delta f method, the values of current equilibrium at all quadrature points if control == True: - self.Jeqx = np.empty( + self.Jeqx = xp.empty( ( self.Nel[0], self.Nel[1], @@ -237,7 +237,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.Jeqy = np.empty( + self.Jeqy = xp.empty( ( self.Nel[0], self.Nel[1], @@ -248,7 +248,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.Jeqz = np.empty( + self.Jeqz = xp.empty( ( self.Nel[0], self.Nel[1], @@ -260,7 +260,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): dtype=float, ) # values of DF and inverse of DF at all quadrature points - self.DF_11 = np.empty( + self.DF_11 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -271,7 +271,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DF_12 = np.empty( + self.DF_12 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -282,7 +282,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DF_13 = np.empty( + self.DF_13 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -293,7 +293,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DF_21 = np.empty( + self.DF_21 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -304,7 +304,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DF_22 = np.empty( + self.DF_22 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -315,7 +315,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DF_23 = np.empty( + self.DF_23 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -326,7 +326,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DF_31 = np.empty( + self.DF_31 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -337,7 +337,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DF_32 = np.empty( + self.DF_32 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -348,7 +348,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DF_33 = np.empty( + self.DF_33 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -360,7 +360,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): dtype=float, ) - self.DFI_11 = np.empty( + self.DFI_11 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -371,7 +371,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DFI_12 = np.empty( + self.DFI_12 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -382,7 +382,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DFI_13 = np.empty( + self.DFI_13 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -393,7 +393,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DFI_21 = np.empty( + self.DFI_21 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -404,7 +404,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DFI_22 = np.empty( + self.DFI_22 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -415,7 +415,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DFI_23 = np.empty( + self.DFI_23 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -426,7 +426,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DFI_31 = np.empty( + self.DFI_31 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -437,7 +437,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DFI_32 = np.empty( + self.DFI_32 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -448,7 +448,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DFI_33 = np.empty( + self.DFI_33 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -460,7 +460,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): dtype=float, ) - self.DFIT_11 = np.empty( + self.DFIT_11 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -471,7 +471,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DFIT_12 = np.empty( + self.DFIT_12 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -482,7 +482,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DFIT_13 = np.empty( + self.DFIT_13 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -493,7 +493,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DFIT_21 = np.empty( + self.DFIT_21 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -504,7 +504,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DFIT_22 = np.empty( + self.DFIT_22 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -515,7 +515,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DFIT_23 = np.empty( + self.DFIT_23 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -526,7 +526,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DFIT_31 = np.empty( + self.DFIT_31 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -537,7 +537,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DFIT_32 = np.empty( + self.DFIT_32 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -548,7 +548,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.DFIT_33 = np.empty( + self.DFIT_33 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -560,7 +560,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): dtype=float, ) - self.G_inv_11 = np.empty( + self.G_inv_11 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -571,7 +571,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.G_inv_12 = np.empty( + self.G_inv_12 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -582,7 +582,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.G_inv_13 = np.empty( + self.G_inv_13 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -594,7 +594,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): dtype=float, ) - self.G_inv_22 = np.empty( + self.G_inv_22 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -605,7 +605,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): ), dtype=float, ) - self.G_inv_23 = np.empty( + self.G_inv_23 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -617,7 +617,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): dtype=float, ) - self.G_inv_33 = np.empty( + self.G_inv_33 = xp.empty( ( self.Nel[0], self.Nel[1], @@ -629,7 +629,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): dtype=float, ) - self.temp_particle = np.empty(3, dtype=float) + self.temp_particle = xp.empty(3, dtype=float) # initialization of DF and its inverse # ================ for mapping evaluation ================== # spline degrees @@ -638,34 +638,34 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): pf3 = DOMAIN.p[2] # pf + 1 non-vanishing basis functions up tp degree pf - b1f = np.empty((pf1 + 1, pf1 + 1), dtype=float) - b2f = np.empty((pf2 + 1, pf2 + 1), dtype=float) - b3f = np.empty((pf3 + 1, pf3 + 1), dtype=float) + b1f = xp.empty((pf1 + 1, pf1 + 1), dtype=float) + b2f = xp.empty((pf2 + 1, pf2 + 1), dtype=float) + b3f = xp.empty((pf3 + 1, pf3 + 1), dtype=float) # left and right values for spline evaluation - l1f = np.empty(pf1, dtype=float) - l2f = np.empty(pf2, dtype=float) - l3f = np.empty(pf3, dtype=float) + l1f = xp.empty(pf1, dtype=float) + l2f = xp.empty(pf2, dtype=float) + l3f = xp.empty(pf3, dtype=float) - r1f = np.empty(pf1, dtype=float) - r2f = np.empty(pf2, dtype=float) - r3f = np.empty(pf3, dtype=float) + r1f = xp.empty(pf1, dtype=float) + r2f = xp.empty(pf2, dtype=float) + r3f = xp.empty(pf3, dtype=float) # scaling arrays for M-splines - d1f = np.empty(pf1, dtype=float) - d2f = np.empty(pf2, dtype=float) - d3f = np.empty(pf3, dtype=float) + d1f = xp.empty(pf1, dtype=float) + d2f = xp.empty(pf2, dtype=float) + d3f = xp.empty(pf3, dtype=float) # pf + 1 derivatives - der1f = np.empty(pf1 + 1, dtype=float) - der2f = np.empty(pf2 + 1, dtype=float) - der3f = np.empty(pf3 + 1, dtype=float) + der1f = xp.empty(pf1 + 1, dtype=float) + der2f = xp.empty(pf2 + 1, dtype=float) + der3f = xp.empty(pf3 + 1, dtype=float) # needed mapping quantities - df = np.empty((3, 3), dtype=float) - fx = np.empty(3, dtype=float) - ginv = np.empty((3, 3), dtype=float) - dfinv = np.empty((3, 3), dtype=float) + df = xp.empty((3, 3), dtype=float) + fx = xp.empty(3, dtype=float) + ginv = xp.empty((3, 3), dtype=float) + dfinv = xp.empty((3, 3), dtype=float) for ie1 in range(self.Nel[0]): for ie2 in range(self.Nel[1]): diff --git a/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_massless_linear_operators.py b/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_massless_linear_operators.py index 843952a7b..2ddddc64a 100644 --- a/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_massless_linear_operators.py +++ b/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_massless_linear_operators.py @@ -1,11 +1,11 @@ import time +import cunumpy as xp import scipy.sparse as spa import struphy.feec.massless_operators.fB_bb_kernel as bb_kernel import struphy.feec.massless_operators.fB_bv_kernel as bv_kernel import struphy.feec.massless_operators.fB_vv_kernel as vv_kernel -from struphy.utils.arrays import xp as np class Massless_linear_operators: @@ -49,15 +49,15 @@ def linearoperator_step_vv(self, M2_PRE, M2, M1_PRE, M1, TEMP, ACC_VV): This function is used in substep vv with L2 projector. """ - dft = np.empty((3, 3), dtype=float) - generate_weight1 = np.zeros(3, dtype=float) - generate_weight2 = np.zeros(3, dtype=float) - generate_weight3 = np.zeros(3, dtype=float) + dft = xp.empty((3, 3), dtype=float) + generate_weight1 = xp.zeros(3, dtype=float) + generate_weight2 = xp.zeros(3, dtype=float) + generate_weight3 = xp.zeros(3, dtype=float) # =========================inverse of M1 =========================== - ACC_VV.temp1[:], ACC_VV.temp2[:], ACC_VV.temp3[:] = np.split( + ACC_VV.temp1[:], ACC_VV.temp2[:], ACC_VV.temp3[:] = xp.split( spa.linalg.cg( M1, - 1.0 / self.Np * np.concatenate((ACC_VV.vec1.flatten(), ACC_VV.vec2.flatten(), ACC_VV.vec3.flatten())), + 1.0 / self.Np * xp.concatenate((ACC_VV.vec1.flatten(), ACC_VV.vec2.flatten(), ACC_VV.vec3.flatten())), tol=10 ** (-14), M=M1_PRE, )[0], @@ -153,10 +153,10 @@ def linearoperator_step_vv(self, M2_PRE, M2, M1_PRE, M1, TEMP, ACC_VV): ) # =========================inverse of M1 =========================== - ACC_VV.temp1[:], ACC_VV.temp2[:], ACC_VV.temp3[:] = np.split( + ACC_VV.temp1[:], ACC_VV.temp2[:], ACC_VV.temp3[:] = xp.split( spa.linalg.cg( M1, - np.concatenate((ACC_VV.one_form1.flatten(), ACC_VV.one_form2.flatten(), ACC_VV.one_form3.flatten())), + xp.concatenate((ACC_VV.one_form1.flatten(), ACC_VV.one_form2.flatten(), ACC_VV.one_form3.flatten())), tol=10 ** (-14), M=M1_PRE, )[0], @@ -310,10 +310,10 @@ def linearoperator_pre_step_vv( indN = tensor_space_FEM.indN indD = tensor_space_FEM.indD - dft = np.empty((3, 3), dtype=float) - generate_weight1 = np.zeros(3, dtype=float) - generate_weight2 = np.zeros(3, dtype=float) - generate_weight3 = np.zeros(3, dtype=float) + dft = xp.empty((3, 3), dtype=float) + generate_weight1 = xp.zeros(3, dtype=float) + generate_weight2 = xp.zeros(3, dtype=float) + generate_weight3 = xp.zeros(3, dtype=float) vv_kernel.prepre( indN[0], @@ -716,15 +716,15 @@ def linearoperator_step3( Ntot_2form = tensor_space_FEM.Ntot_2form Nbase_2form = tensor_space_FEM.Nbase_2form - dft = np.empty((3, 3), dtype=float) - generate_weight1 = np.empty(3, dtype=float) - generate_weight2 = np.empty(3, dtype=float) - generate_weight3 = np.empty(3, dtype=float) + dft = xp.empty((3, 3), dtype=float) + generate_weight1 = xp.empty(3, dtype=float) + generate_weight2 = xp.empty(3, dtype=float) + generate_weight3 = xp.empty(3, dtype=float) # ================================================================== # ========================= C =========================== # time1 = time.time() - twoform_temp1_long[:], twoform_temp2_long[:], twoform_temp3_long[:] = np.split( + twoform_temp1_long[:], twoform_temp2_long[:], twoform_temp3_long[:] = xp.split( tensor_space_FEM.C.dot(input_vector), [Ntot_2form[0], Ntot_2form[0] + Ntot_2form[1]] ) temp_vector_1[:, :, :] = twoform_temp1_long.reshape(Nbase_2form[0]) @@ -826,7 +826,7 @@ def linearoperator_step3( # ========================= C.T =========================== # time1 = time.time() temp_final = tensor_space_FEM.M1.dot(input_vector) - dt / 2.0 * tensor_space_FEM.C.T.dot( - np.concatenate((temp_vector_1.flatten(), temp_vector_2.flatten(), temp_vector_3.flatten())) + xp.concatenate((temp_vector_1.flatten(), temp_vector_2.flatten(), temp_vector_3.flatten())) ) # time2 = time.time() # print('second_curl_time', time2 - time1) @@ -921,14 +921,14 @@ def linearoperator_right_step3( Ntot_2form = tensor_space_FEM.Ntot_2form Nbase_2form = tensor_space_FEM.Nbase_2form - dft = np.empty((3, 3), dtype=float) - generate_weight1 = np.empty(3, dtype=float) - generate_weight2 = np.empty(3, dtype=float) - generate_weight3 = np.empty(3, dtype=float) + dft = xp.empty((3, 3), dtype=float) + generate_weight1 = xp.empty(3, dtype=float) + generate_weight2 = xp.empty(3, dtype=float) + generate_weight3 = xp.empty(3, dtype=float) # ================================================================== # ========================= C =========================== - twoform_temp1_long[:], twoform_temp2_long[:], twoform_temp3_long[:] = np.split( + twoform_temp1_long[:], twoform_temp2_long[:], twoform_temp3_long[:] = xp.split( tensor_space_FEM.C.dot(input_vector), [Ntot_2form[0], Ntot_2form[0] + Ntot_2form[1]] ) temp_vector_1[:, :, :] = twoform_temp1_long.reshape(Nbase_2form[0]) @@ -1082,7 +1082,7 @@ def linearoperator_right_step3( # print('final_bb', time2 - time1) # ========================= C.T =========================== temp_final = tensor_space_FEM.M1.dot(input_vector) + dt / 2.0 * tensor_space_FEM.C.T.dot( - np.concatenate((temp_vector_1.flatten(), temp_vector_2.flatten(), temp_vector_3.flatten())) + xp.concatenate((temp_vector_1.flatten(), temp_vector_2.flatten(), temp_vector_3.flatten())) ) return temp_final @@ -1145,7 +1145,7 @@ def substep4_linear_operator( wts = tensor_space_FEM.wts # global quadrature weights # ========================================== - acc.twoform_temp1_long[:], acc.twoform_temp2_long[:], acc.twoform_temp3_long[:] = np.split( + acc.twoform_temp1_long[:], acc.twoform_temp2_long[:], acc.twoform_temp3_long[:] = xp.split( tensor_space_FEM.C.dot(input), [Ntot_2form[0], Ntot_2form[0] + Ntot_2form[1]] ) acc.twoform_temp1[:, :, :] = acc.twoform_temp1_long.reshape(Nbase_2form[0]) @@ -1249,12 +1249,12 @@ def substep4_linear_operator( ) acc.oneform_temp_long[:] = spa.linalg.gmres( M1, - np.concatenate((acc.oneform_temp1.flatten(), acc.oneform_temp2.flatten(), acc.oneform_temp3.flatten())), + xp.concatenate((acc.oneform_temp1.flatten(), acc.oneform_temp2.flatten(), acc.oneform_temp3.flatten())), tol=10 ** (-10), M=M1_PRE, )[0] - acc.oneform_temp1_long[:], acc.oneform_temp2_long[:], acc.oneform_temp3_long[:] = np.split( + acc.oneform_temp1_long[:], acc.oneform_temp2_long[:], acc.oneform_temp3_long[:] = xp.split( spa.linalg.gmres(M1, mat.dot(acc.oneform_temp_long), tol=10 ** (-10), M=M1_PRE)[0], [Ntot_1form[0], Ntot_1form[0] + Ntot_1form[1]], ) @@ -1359,7 +1359,7 @@ def substep4_linear_operator( ) return M1.dot(input) + dt**2 / 4.0 * tensor_space_FEM.C.T.dot( - np.concatenate((acc.twoform_temp1.flatten(), acc.twoform_temp2.flatten(), acc.twoform_temp3.flatten())) + xp.concatenate((acc.twoform_temp1.flatten(), acc.twoform_temp2.flatten(), acc.twoform_temp3.flatten())) ) # ========================================================================================================== @@ -1422,8 +1422,8 @@ def substep4_linear_operator_right( wts = tensor_space_FEM.wts # global quadrature weights # ========================================== - acc.twoform_temp1_long[:], acc.twoform_temp2_long[:], acc.twoform_temp3_long[:] = np.split( - CURL.dot(np.concatenate((bb1.flatten(), bb2.flatten(), bb3.flatten()))), + acc.twoform_temp1_long[:], acc.twoform_temp2_long[:], acc.twoform_temp3_long[:] = xp.split( + CURL.dot(xp.concatenate((bb1.flatten(), bb2.flatten(), bb3.flatten()))), [Ntot_2form[0], Ntot_2form[0] + Ntot_2form[1]], ) acc.twoform_temp1[:, :, :] = acc.twoform_temp1_long.reshape(Nbase_2form[0]) @@ -1529,13 +1529,13 @@ def substep4_linear_operator_right( acc.oneform_temp_long[:] = mat.dot( spa.linalg.gmres( M1, - np.concatenate((acc.oneform_temp1.flatten(), acc.oneform_temp2.flatten(), acc.oneform_temp3.flatten())), + xp.concatenate((acc.oneform_temp1.flatten(), acc.oneform_temp2.flatten(), acc.oneform_temp3.flatten())), tol=10 ** (-10), M=M1_PRE, )[0] ) - acc.oneform_temp1_long[:], acc.oneform_temp2_long[:], acc.oneform_temp3_long[:] = np.split( + acc.oneform_temp1_long[:], acc.oneform_temp2_long[:], acc.oneform_temp3_long[:] = xp.split( spa.linalg.gmres(M1, dt**2.0 / 4.0 * acc.oneform_temp_long + dt * vec, tol=10 ** (-10), M=M1_PRE)[0], [Ntot_1form[0], Ntot_1form[0] + Ntot_1form[1]], ) @@ -1639,8 +1639,8 @@ def substep4_linear_operator_right( tensor_space_FEM.basisD[2], ) - return M1.dot(np.concatenate((bb1.flatten(), bb2.flatten(), bb3.flatten()))) - CURL.T.dot( - np.concatenate((acc.twoform_temp1.flatten(), acc.twoform_temp2.flatten(), acc.twoform_temp3.flatten())) + return M1.dot(xp.concatenate((bb1.flatten(), bb2.flatten(), bb3.flatten()))) - CURL.T.dot( + xp.concatenate((acc.twoform_temp1.flatten(), acc.twoform_temp2.flatten(), acc.twoform_temp3.flatten())) ) # ========================================================================================================== @@ -1792,8 +1792,8 @@ def substep4_pusher_field( wts = tensor_space_FEM.wts # global quadrature weights # ========================================== - acc.twoform_temp1_long[:], acc.twoform_temp2_long[:], acc.twoform_temp3_long[:] = np.split( - CURL.dot(np.concatenate((bb1.flatten(), bb2.flatten(), bb3.flatten()))), + acc.twoform_temp1_long[:], acc.twoform_temp2_long[:], acc.twoform_temp3_long[:] = xp.split( + CURL.dot(xp.concatenate((bb1.flatten(), bb2.flatten(), bb3.flatten()))), [Ntot_2form[0], Ntot_2form[0] + Ntot_2form[1]], ) acc.twoform_temp1[:, :, :] = acc.twoform_temp1_long.reshape(Nbase_2form[0]) @@ -1898,7 +1898,7 @@ def substep4_pusher_field( return spa.linalg.cg( M1, - np.concatenate((acc.oneform_temp1.flatten(), acc.oneform_temp2.flatten(), acc.oneform_temp3.flatten())), + xp.concatenate((acc.oneform_temp1.flatten(), acc.oneform_temp2.flatten(), acc.oneform_temp3.flatten())), tol=10 ** (-13), M=M1_PRE, )[0] @@ -1960,7 +1960,7 @@ def substep4_localproj_linear_operator( wts = tensor_space_FEM.wts # global quadrature weights # ========================================== - acc.twoform_temp1_long[:], acc.twoform_temp2_long[:], acc.twoform_temp3_long[:] = np.split( + acc.twoform_temp1_long[:], acc.twoform_temp2_long[:], acc.twoform_temp3_long[:] = xp.split( tensor_space_FEM.C.dot(input), [Ntot_2form[0], Ntot_2form[0] + Ntot_2form[1]] ) acc.twoform_temp1[:, :, :] = acc.twoform_temp1_long.reshape(Nbase_2form[0]) @@ -2063,9 +2063,9 @@ def substep4_localproj_linear_operator( tensor_space_FEM.basisD[2], ) - acc.oneform_temp1_long[:], acc.oneform_temp2_long[:], acc.oneform_temp3_long[:] = np.split( + acc.oneform_temp1_long[:], acc.oneform_temp2_long[:], acc.oneform_temp3_long[:] = xp.split( mat.dot( - np.concatenate((acc.oneform_temp1.flatten(), acc.oneform_temp2.flatten(), acc.oneform_temp3.flatten())) + xp.concatenate((acc.oneform_temp1.flatten(), acc.oneform_temp2.flatten(), acc.oneform_temp3.flatten())) ), [Ntot_1form[0], Ntot_1form[0] + Ntot_1form[1]], ) @@ -2171,7 +2171,7 @@ def substep4_localproj_linear_operator( ) return M1.dot(input) + dt**2 / 4.0 * tensor_space_FEM.C.T.dot( - np.concatenate((acc.twoform_temp1.flatten(), acc.twoform_temp2.flatten(), acc.twoform_temp3.flatten())) + xp.concatenate((acc.twoform_temp1.flatten(), acc.twoform_temp2.flatten(), acc.twoform_temp3.flatten())) ) # ========================================================================================================== @@ -2234,8 +2234,8 @@ def substep4_localproj_linear_operator_right( wts = tensor_space_FEM.wts # global quadrature weights # ========================================== - acc.twoform_temp1_long[:], acc.twoform_temp2_long[:], acc.twoform_temp3_long[:] = np.split( - CURL.dot(np.concatenate((bb1.flatten(), bb2.flatten(), bb3.flatten()))), + acc.twoform_temp1_long[:], acc.twoform_temp2_long[:], acc.twoform_temp3_long[:] = xp.split( + CURL.dot(xp.concatenate((bb1.flatten(), bb2.flatten(), bb3.flatten()))), [Ntot_2form[0], Ntot_2form[0] + Ntot_2form[1]], ) acc.twoform_temp1[:, :, :] = acc.twoform_temp1_long.reshape(Nbase_2form[0]) @@ -2339,10 +2339,10 @@ def substep4_localproj_linear_operator_right( tensor_space_FEM.basisD[2], ) acc.oneform_temp_long[:] = mat.dot( - np.concatenate((acc.oneform_temp1.flatten(), acc.oneform_temp2.flatten(), acc.oneform_temp3.flatten())) + xp.concatenate((acc.oneform_temp1.flatten(), acc.oneform_temp2.flatten(), acc.oneform_temp3.flatten())) ) - acc.oneform_temp1_long[:], acc.oneform_temp2_long[:], acc.oneform_temp3_long[:] = np.split( + acc.oneform_temp1_long[:], acc.oneform_temp2_long[:], acc.oneform_temp3_long[:] = xp.split( (dt**2.0 / 4.0 * acc.oneform_temp_long + dt * vec), [Ntot_1form[0], Ntot_1form[0] + Ntot_1form[1]] ) @@ -2446,8 +2446,8 @@ def substep4_localproj_linear_operator_right( tensor_space_FEM.basisD[2], ) - return M1.dot(np.concatenate((bb1.flatten(), bb2.flatten(), bb3.flatten()))) - CURL.T.dot( - np.concatenate((acc.twoform_temp1.flatten(), acc.twoform_temp2.flatten(), acc.twoform_temp3.flatten())) + return M1.dot(xp.concatenate((bb1.flatten(), bb2.flatten(), bb3.flatten()))) - CURL.T.dot( + xp.concatenate((acc.twoform_temp1.flatten(), acc.twoform_temp2.flatten(), acc.twoform_temp3.flatten())) ) # ========================================================================================================== @@ -2509,8 +2509,8 @@ def substep4_localproj_pusher_field( wts = tensor_space_FEM.wts # global quadrature weights # ========================================== - acc.twoform_temp1_long[:], acc.twoform_temp2_long[:], acc.twoform_temp3_long[:] = np.split( - CURL.dot(np.concatenate((bb1.flatten(), bb2.flatten(), bb3.flatten()))), + acc.twoform_temp1_long[:], acc.twoform_temp2_long[:], acc.twoform_temp3_long[:] = xp.split( + CURL.dot(xp.concatenate((bb1.flatten(), bb2.flatten(), bb3.flatten()))), [Ntot_2form[0], Ntot_2form[0] + Ntot_2form[1]], ) acc.twoform_temp1[:, :, :] = acc.twoform_temp1_long.reshape(Nbase_2form[0]) @@ -2613,4 +2613,4 @@ def substep4_localproj_pusher_field( tensor_space_FEM.basisD[2], ) - return np.concatenate((acc.oneform_temp1.flatten(), acc.oneform_temp2.flatten(), acc.oneform_temp3.flatten())) + return xp.concatenate((acc.oneform_temp1.flatten(), acc.oneform_temp2.flatten(), acc.oneform_temp3.flatten())) diff --git a/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_vv_kernel.py b/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_vv_kernel.py index 216103640..4a5a2dbe0 100644 --- a/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_vv_kernel.py +++ b/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_vv_kernel.py @@ -1,9 +1,9 @@ +import cunumpy as xp from numpy import empty, exp, floor, zeros import struphy.bsplines.bsplines_kernels as bsp import struphy.geometry.mappings_kernels as mapping_fast import struphy.linear_algebra.linalg_kernels as linalg -from struphy.utils.arrays import xp as np # ========================================================================================== diff --git a/src/struphy/eigenvalue_solvers/legacy/mhd_operators_MF.py b/src/struphy/eigenvalue_solvers/legacy/mhd_operators_MF.py index 9591fe06b..560314458 100644 --- a/src/struphy/eigenvalue_solvers/legacy/mhd_operators_MF.py +++ b/src/struphy/eigenvalue_solvers/legacy/mhd_operators_MF.py @@ -1,9 +1,9 @@ +import cunumpy as xp import scipy.sparse as spa from struphy.eigenvalue_solvers.projectors_global import Projectors_tensor_3d from struphy.eigenvalue_solvers.spline_space import Tensor_spline_space from struphy.linear_algebra.linalg_kron import kron_matvec_3d, kron_solve_3d -from struphy.utils.arrays import xp as np # ================================================================================================= @@ -107,9 +107,9 @@ def __init__(self, space, eq_MHD): self.pts1_D_2 = self.space.spaces[1].projectors.D_pts self.pts1_D_3 = self.space.spaces[2].projectors.D_pts - # assert np.allclose(self.N_1.toarray(), self.pts0_N_1.toarray(), atol=1e-14) - # assert np.allclose(self.N_2.toarray(), self.pts0_N_2.toarray(), atol=1e-14) - # assert np.allclose(self.N_3.toarray(), self.pts0_N_3.toarray(), atol=1e-14) + # assert xp.allclose(self.N_1.toarray(), self.pts0_N_1.toarray(), atol=1e-14) + # assert xp.allclose(self.N_2.toarray(), self.pts0_N_2.toarray(), atol=1e-14) + # assert xp.allclose(self.N_3.toarray(), self.pts0_N_3.toarray(), atol=1e-14) # ===== call equilibrium_mhd values at the projection points ===== # projection points @@ -210,11 +210,11 @@ def __init__(self, space, eq_MHD): # # Operator A # if self.basis_u == 1: # self.A = spa.linalg.LinearOperator((self.dim_1, self.dim_1), matvec = lambda x : (self.M1.dot(self.W1_dot(x)) + self.transpose_W1_dot(self.M1.dot(x))) / 2 ) - # self.A_mat = spa.csc_matrix(self.A.dot(np.identity(self.dim_1))) + # self.A_mat = spa.csc_matrix(self.A.dot(xp.identity(self.dim_1))) # elif self.basis_u == 2: # self.A = spa.linalg.LinearOperator((self.dim_2, self.dim_2), matvec = lambda x : (self.M2.dot(self.Q2_dot(x)) + self.transpose_Q2_dot(self.M2.dot(x))) / 2 ) - # self.A_mat = spa.csc_matrix(self.A.dot(np.identity(self.dim_2))) + # self.A_mat = spa.csc_matrix(self.A.dot(xp.identity(self.dim_2))) # self.A_inv = spa.linalg.inv(self.A_mat) @@ -228,12 +228,12 @@ def Q1_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^1} Returns ---------- - res : np.array + res : xp.array dim R^{N^2} Notes @@ -320,7 +320,7 @@ def Q1_dot(self, x): # xi3 : histo(xi1)-histo(xi2)-inter(xi3)-polation. res_3 = self.space.projectors.PI_mat("23", DOF_3) - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ================================================================== def transpose_Q1_dot(self, x): @@ -329,12 +329,12 @@ def transpose_Q1_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^2} Returns ---------- - res : np.array + res : xp.array dim R^{N^1} Notes @@ -403,7 +403,7 @@ def transpose_Q1_dot(self, x): res_2 = res_12 + res_22 + res_32 res_3 = res_13 + res_23 + res_33 - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # =================================================================== def W1_dot(self, x): @@ -412,12 +412,12 @@ def W1_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^1} Returns ---------- - res : np.array + res : xp.array dim R^{N^1} Notes @@ -482,7 +482,7 @@ def W1_dot(self, x): # xi3 : inter(xi1)-inter(xi2)-histo(xi3)-polation. res_3 = self.space.projectors.PI_mat("13", DOF_3) - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # =================================================================== def transpose_W1_dot(self, x): @@ -491,12 +491,12 @@ def transpose_W1_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R{N^1} Returns ---------- - res : np.array + res : xp.array dim R{N^1} Notes @@ -545,7 +545,7 @@ def transpose_W1_dot(self, x): res_2 = kron_matvec_3d([self.pts0_N_1.T, self.pts1_D_2.T, self.pts0_N_3.T], mat_f_2_c) res_3 = kron_matvec_3d([self.pts0_N_1.T, self.pts0_N_2.T, self.pts1_D_3.T], mat_f_3_c) - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ==================================================================== def U1_dot(self, x): @@ -554,12 +554,12 @@ def U1_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^1} Returns ---------- - res : np.array + res : xp.array dim R^{N^2} Notes @@ -645,7 +645,7 @@ def U1_dot(self, x): # xi3 : histo(xi1)-histo(xi2)-inter(xi3)-polation. res_3 = self.space.projectors.PI_mat("23", DOF_3) - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ==================================================================== def transpose_U1_dot(self, x): @@ -654,12 +654,12 @@ def transpose_U1_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R{N^2} Returns ---------- - res : np.array + res : xp.array dim R{N^1} Notes @@ -728,7 +728,7 @@ def transpose_U1_dot(self, x): res_2 = res_12 + res_22 + res_32 res_3 = res_13 + res_23 + res_33 - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ==================================================================== def P1_dot(self, x): @@ -737,12 +737,12 @@ def P1_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^2} Returns ---------- - res : np.array + res : xp.array dim R^{N^1} Notes @@ -831,7 +831,7 @@ def P1_dot(self, x): # xi3 : inter(xi1)-inter(xi2)-histo(xi3)-polation. res_3 = self.space.projectors.PI_mat("13", DOF_3) - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ==================================================================== def transpose_P1_dot(self, x): @@ -840,12 +840,12 @@ def transpose_P1_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R{N^1} Returns ---------- - res : np.array + res : xp.array dim R{N^2} Notes @@ -914,7 +914,7 @@ def transpose_P1_dot(self, x): res_2 = res_12 + res_22 + res_32 res_3 = res_13 + res_23 + res_33 - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ==================================================================== def S1_dot(self, x): @@ -923,12 +923,12 @@ def S1_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^1} Returns ---------- - res : np.array + res : xp.array dim R^{N^2} Notes @@ -1015,7 +1015,7 @@ def S1_dot(self, x): # xi3 : histo(xi1)-histo(xi2)-inter(xi3)-polation. res_3 = self.space.projectors.PI_mat("23", DOF_3) - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ==================================================================== def transpose_S1_dot(self, x): @@ -1024,12 +1024,12 @@ def transpose_S1_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R{N^2} Returns ---------- - res : np.array + res : xp.array dim R{N^1} Notes @@ -1098,7 +1098,7 @@ def transpose_S1_dot(self, x): res_2 = res_12 + res_22 + res_32 res_3 = res_13 + res_23 + res_33 - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # =================================================================== def S10_dot(self, x): @@ -1107,12 +1107,12 @@ def S10_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^1} Returns ---------- - res : np.array + res : xp.array dim R^{N^1} Notes @@ -1178,7 +1178,7 @@ def S10_dot(self, x): # xi3 : inter(xi1)-inter(xi2)-histo(xi3)-polation. res_3 = self.space.projectors.PI_mat("13", DOF_3) - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # =================================================================== def transpose_S10_dot(self, x): @@ -1187,12 +1187,12 @@ def transpose_S10_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R{N^1} Returns ---------- - res : np.array + res : xp.array dim R{N^1} Notes @@ -1241,7 +1241,7 @@ def transpose_S10_dot(self, x): res_2 = kron_matvec_3d([self.pts0_N_1.T, self.pts1_D_2.T, self.pts0_N_3.T], mat_f_2_c) res_3 = kron_matvec_3d([self.pts0_N_1.T, self.pts0_N_2.T, self.pts1_D_3.T], mat_f_3_c) - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ================================================================= def K1_dot(self, x): @@ -1250,12 +1250,12 @@ def K1_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^3} Returns ---------- - res : np.array + res : xp.array dim R^{N^3} Notes @@ -1307,7 +1307,7 @@ def transpose_K1_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R{N^3} Returns @@ -1350,12 +1350,12 @@ def K10_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^0} Returns ---------- - res : np.array + res : xp.array dim R^{N^0} Notes @@ -1406,7 +1406,7 @@ def transpose_K10_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R{N^0} Returns @@ -1449,12 +1449,12 @@ def T1_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^1} Returns ---------- - res : np.array + res : xp.array dim R^{N^1} Notes @@ -1543,7 +1543,7 @@ def T1_dot(self, x): # xi3 : inter(xi1)-inter(xi2)-histo(xi3)-polation. res_3 = self.space.projectors.PI_mat("13", DOF_3) - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ================================================================= def transpose_T1_dot(self, x): @@ -1552,12 +1552,12 @@ def transpose_T1_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R{N^1} Returns ---------- - res : np.array + res : xp.array dim R{N^1} Notes @@ -1626,7 +1626,7 @@ def transpose_T1_dot(self, x): res_2 = res_12 + res_22 + res_32 res_3 = res_13 + res_23 + res_33 - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ================================================================= def X1_dot(self, x): @@ -1635,13 +1635,13 @@ def X1_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^1} Returns ---------- res : list - 3 np.arrays of dim R^{N^0} + 3 xp.arrays of dim R^{N^0} Notes ----- @@ -1718,12 +1718,12 @@ def transpose_X1_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R{N^0 x 3} Returns ---------- - res : np.array + res : xp.array dim R{N^1} Notes @@ -1738,9 +1738,9 @@ def transpose_X1_dot(self, x): # x dim check # x should be R{N^0 * 3} # assert len(x) == self.space.Ntot_0form * 3 - # x_loc_1 = self.space.extract_0(np.split(x,3)[0]) - # x_loc_2 = self.space.extract_0(np.split(x,3)[1]) - # x_loc_3 = self.space.extract_0(np.split(x,3)[2]) + # x_loc_1 = self.space.extract_0(xp.split(x,3)[0]) + # x_loc_2 = self.space.extract_0(xp.split(x,3)[1]) + # x_loc_3 = self.space.extract_0(xp.split(x,3)[2]) # x_loc = list((x_loc_1, x_loc_2, x_loc_3)) x_loc_1 = self.space.extract_0(x[0]) @@ -1794,7 +1794,7 @@ def transpose_X1_dot(self, x): res_2 = res_12 + res_22 + res_32 res_3 = res_13 + res_23 + res_33 - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) ######################################## ########## 2-form formulation ########## @@ -1806,12 +1806,12 @@ def Q2_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^2} Returns ---------- - res : np.array + res : xp.array dim R^{N^2} Notes @@ -1882,7 +1882,7 @@ def Q2_dot(self, x): # xi3 : histo(xi1)-histo(xi2)-inter(xi3)-polation. res_3 = self.space.projectors.PI_mat("23", DOF_3) - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ==================================================================== def transpose_Q2_dot(self, x): @@ -1891,12 +1891,12 @@ def transpose_Q2_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R{N^2} Returns ---------- - res : np.array + res : xp.array dim R{N^2} Notes @@ -1946,7 +1946,7 @@ def transpose_Q2_dot(self, x): res_2 = kron_matvec_3d([self.pts1_D_1.T, self.pts0_N_2.T, self.pts1_D_3.T], mat_f_2_c) res_3 = kron_matvec_3d([self.pts1_D_1.T, self.pts1_D_2.T, self.pts0_N_3.T], mat_f_3_c) - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ==================================================================== def T2_dot(self, x): @@ -1955,12 +1955,12 @@ def T2_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^2} Returns ---------- - res : np.array + res : xp.array dim R^{N^1} Notes @@ -2049,7 +2049,7 @@ def T2_dot(self, x): # xi3 : inter(xi1)-inter(xi2)-histo(xi3)-polation. res_3 = self.space.projectors.PI_mat("13", DOF_3) - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ==================================================================== def transpose_T2_dot(self, x): @@ -2058,12 +2058,12 @@ def transpose_T2_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R{N^1} Returns ---------- - res : np.array + res : xp.array dim R{N^2} Notes @@ -2132,7 +2132,7 @@ def transpose_T2_dot(self, x): res_2 = res_12 + res_22 + res_32 res_3 = res_13 + res_23 + res_33 - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ==================================================================== def P2_dot(self, x): @@ -2141,12 +2141,12 @@ def P2_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^2} Returns ---------- - res : np.array + res : xp.array dim R^{N^2} Notes @@ -2235,7 +2235,7 @@ def P2_dot(self, x): # xi3 : histo(xi1)-histo(xi2)-inter(xi3)-polation. res_3 = self.space.projectors.PI_mat("23", DOF_3) - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ==================================================================== def transpose_P2_dot(self, x): @@ -2244,12 +2244,12 @@ def transpose_P2_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R{N^2} Returns ---------- - res : np.array + res : xp.array dim R{N^2} Notes @@ -2317,7 +2317,7 @@ def transpose_P2_dot(self, x): res_2 = res_12 + res_22 + res_32 res_3 = res_13 + res_23 + res_33 - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ==================================================================== def S2_dot(self, x): @@ -2326,12 +2326,12 @@ def S2_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^2} Returns ---------- - res : np.array + res : xp.array dim R^{N^2} Notes @@ -2402,7 +2402,7 @@ def S2_dot(self, x): # xi3 : histo(xi1)-histo(xi2)-inter(xi3)-polation. res_3 = self.space.projectors.PI_mat("23", DOF_3) - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ==================================================================== def transpose_S2_dot(self, x): @@ -2411,12 +2411,12 @@ def transpose_S2_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R{N^2} Returns ---------- - res : np.array + res : xp.array dim R{N^2} Notes @@ -2466,7 +2466,7 @@ def transpose_S2_dot(self, x): res_2 = kron_matvec_3d([self.pts1_D_1.T, self.pts0_N_2.T, self.pts1_D_3.T], mat_f_2_c) res_3 = kron_matvec_3d([self.pts1_D_1.T, self.pts1_D_2.T, self.pts0_N_3.T], mat_f_3_c) - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ==================================================================== def K2_dot(self, x): @@ -2475,12 +2475,12 @@ def K2_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^3} Returns ---------- - res : np.array + res : xp.array dim R^{N^3} Notes @@ -2532,7 +2532,7 @@ def transpose_K2_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R{N^3} Returns @@ -2575,13 +2575,13 @@ def X2_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^2} Returns ---------- res : list - 3 np.arrays of dim R^{N^0} + 3 xp.arrays of dim R^{N^0} Notes ----- @@ -2658,12 +2658,12 @@ def transpose_X2_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R{N^0 x 3} Returns ---------- - res : np.array + res : xp.array dim R{N^2} Notes @@ -2678,9 +2678,9 @@ def transpose_X2_dot(self, x): # x dim check # x should be R{N^0 * 3} # assert len(x) == self.space.Ntot_0form * 3 - # x_loc_1 = self.space.extract_0(np.split(x,3)[0]) - # x_loc_2 = self.space.extract_0(np.split(x,3)[1]) - # x_loc_3 = self.space.extract_0(np.split(x,3)[2]) + # x_loc_1 = self.space.extract_0(xp.split(x,3)[0]) + # x_loc_2 = self.space.extract_0(xp.split(x,3)[1]) + # x_loc_3 = self.space.extract_0(xp.split(x,3)[2]) # x_loc = list((x_loc_1, x_loc_2, x_loc_3)) x_loc_1 = self.space.extract_0(x[0]) @@ -2734,7 +2734,7 @@ def transpose_X2_dot(self, x): res_2 = res_12 + res_22 + res_32 res_3 = res_13 + res_23 + res_33 - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ==================================================================== def Z20_dot(self, x): @@ -2743,12 +2743,12 @@ def Z20_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^2} Returns ---------- - res : np.array + res : xp.array dim R^{N^1} Notes @@ -2835,7 +2835,7 @@ def Z20_dot(self, x): # xi3 : inter(xi1)-inter(xi2)-histo(xi3)-polation. res_3 = self.space.projectors.PI_mat("13", DOF_3) - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ==================================================================== def transpose_Z20_dot(self, x): @@ -2844,12 +2844,12 @@ def transpose_Z20_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R{N^2} Returns ---------- - res : np.array + res : xp.array dim R{N^1} Notes @@ -2918,7 +2918,7 @@ def transpose_Z20_dot(self, x): res_2 = res_12 + res_22 + res_32 res_3 = res_13 + res_23 + res_33 - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ==================================================================== def Y20_dot(self, x): @@ -2927,12 +2927,12 @@ def Y20_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^0} Returns ---------- - res : np.array + res : xp.array dim R^{N^3} Notes @@ -2984,12 +2984,12 @@ def transpose_Y20_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R{N^3} Returns ---------- - res : np.array + res : xp.array dim R{N^0} Notes @@ -3027,12 +3027,12 @@ def S20_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R^{N^2} Returns ---------- - res : np.array + res : xp.array dim R^{N^1} Notes @@ -3119,7 +3119,7 @@ def S20_dot(self, x): # xi3 : inter(xi1)-inter(xi2)-histo(xi3)-polation. res_3 = self.space.projectors.PI_mat("13", DOF_3) - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) # ==================================================================== def transpose_S20_dot(self, x): @@ -3128,12 +3128,12 @@ def transpose_S20_dot(self, x): Parameters ---------- - x : np.array + x : xp.array dim R{N^1} Returns ---------- - res : np.array + res : xp.array dim R{N^2} Notes @@ -3202,4 +3202,4 @@ def transpose_S20_dot(self, x): res_2 = res_12 + res_22 + res_32 res_3 = res_13 + res_23 + res_33 - return np.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) + return xp.concatenate((res_1.flatten(), res_2.flatten(), res_3.flatten())) diff --git a/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/mhd_operators_3d_local.py b/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/mhd_operators_3d_local.py index bd840b3f0..0c96616de 100644 --- a/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/mhd_operators_3d_local.py +++ b/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/mhd_operators_3d_local.py @@ -8,13 +8,13 @@ import sys +import cunumpy as xp import scipy.sparse as spa import source_run.kernels_projectors_evaluation as ker_eva import struphy.feec.basics.kernels_3d as ker_loc_3d import struphy.feec.bsplines as bsp import struphy.feec.projectors.pro_local.kernels_projectors_local_mhd as ker_loc -from struphy.utils.arrays import xp as np class projectors_local_mhd: @@ -44,8 +44,8 @@ def __init__(self, tensor_space, n_quad): self.n_quad = n_quad # number of quadrature point per integration interval # Gauss - Legendre quadrature points and weights in (-1, 1) - self.pts_loc = [np.polynomial.legendre.leggauss(n_quad)[0] for n_quad in self.n_quad] - self.wts_loc = [np.polynomial.legendre.leggauss(n_quad)[1] for n_quad in self.n_quad] + self.pts_loc = [xp.polynomial.legendre.leggauss(n_quad)[0] for n_quad in self.n_quad] + self.wts_loc = [xp.polynomial.legendre.leggauss(n_quad)[1] for n_quad in self.n_quad] # set interpolation and histopolation coefficients self.coeff_i = [0, 0, 0] @@ -53,78 +53,78 @@ def __init__(self, tensor_space, n_quad): for a in range(3): if self.bc[a] == True: - self.coeff_i[a] = np.zeros((1, 2 * self.p[a] - 1), dtype=float) - self.coeff_h[a] = np.zeros((1, 2 * self.p[a]), dtype=float) + self.coeff_i[a] = xp.zeros((1, 2 * self.p[a] - 1), dtype=float) + self.coeff_h[a] = xp.zeros((1, 2 * self.p[a]), dtype=float) if self.p[a] == 1: - self.coeff_i[a][0, :] = np.array([1.0]) - self.coeff_h[a][0, :] = np.array([1.0, 1.0]) + self.coeff_i[a][0, :] = xp.array([1.0]) + self.coeff_h[a][0, :] = xp.array([1.0, 1.0]) elif self.p[a] == 2: - self.coeff_i[a][0, :] = 1 / 2 * np.array([-1.0, 4.0, -1.0]) - self.coeff_h[a][0, :] = 1 / 2 * np.array([-1.0, 3.0, 3.0, -1.0]) + self.coeff_i[a][0, :] = 1 / 2 * xp.array([-1.0, 4.0, -1.0]) + self.coeff_h[a][0, :] = 1 / 2 * xp.array([-1.0, 3.0, 3.0, -1.0]) elif self.p[a] == 3: - self.coeff_i[a][0, :] = 1 / 6 * np.array([1.0, -8.0, 20.0, -8.0, 1.0]) - self.coeff_h[a][0, :] = 1 / 6 * np.array([1.0, -7.0, 12.0, 12.0, -7.0, 1.0]) + self.coeff_i[a][0, :] = 1 / 6 * xp.array([1.0, -8.0, 20.0, -8.0, 1.0]) + self.coeff_h[a][0, :] = 1 / 6 * xp.array([1.0, -7.0, 12.0, 12.0, -7.0, 1.0]) elif self.p[a] == 4: - self.coeff_i[a][0, :] = 2 / 45 * np.array([-1.0, 16.0, -295 / 4, 140.0, -295 / 4, 16.0, -1.0]) + self.coeff_i[a][0, :] = 2 / 45 * xp.array([-1.0, 16.0, -295 / 4, 140.0, -295 / 4, 16.0, -1.0]) self.coeff_h[a][0, :] = ( - 2 / 45 * np.array([-1.0, 15.0, -231 / 4, 265 / 4, 265 / 4, -231 / 4, 15.0, -1.0]) + 2 / 45 * xp.array([-1.0, 15.0, -231 / 4, 265 / 4, 265 / 4, -231 / 4, 15.0, -1.0]) ) else: print("degree > 4 not implemented!") else: - self.coeff_i[a] = np.zeros((2 * self.p[a] - 1, 2 * self.p[a] - 1), dtype=float) - self.coeff_h[a] = np.zeros((2 * self.p[a] - 1, 2 * self.p[a]), dtype=float) + self.coeff_i[a] = xp.zeros((2 * self.p[a] - 1, 2 * self.p[a] - 1), dtype=float) + self.coeff_h[a] = xp.zeros((2 * self.p[a] - 1, 2 * self.p[a]), dtype=float) if self.p[a] == 1: - self.coeff_i[a][0, :] = np.array([1.0]) - self.coeff_h[a][0, :] = np.array([1.0, 1.0]) + self.coeff_i[a][0, :] = xp.array([1.0]) + self.coeff_h[a][0, :] = xp.array([1.0, 1.0]) elif self.p[a] == 2: - self.coeff_i[a][0, :] = 1 / 2 * np.array([2.0, 0.0, 0.0]) - self.coeff_i[a][1, :] = 1 / 2 * np.array([-1.0, 4.0, -1.0]) - self.coeff_i[a][2, :] = 1 / 2 * np.array([0.0, 0.0, 2.0]) + self.coeff_i[a][0, :] = 1 / 2 * xp.array([2.0, 0.0, 0.0]) + self.coeff_i[a][1, :] = 1 / 2 * xp.array([-1.0, 4.0, -1.0]) + self.coeff_i[a][2, :] = 1 / 2 * xp.array([0.0, 0.0, 2.0]) - self.coeff_h[a][0, :] = 1 / 2 * np.array([3.0, -1.0, 0.0, 0.0]) - self.coeff_h[a][1, :] = 1 / 2 * np.array([-1.0, 3.0, 3.0, -1.0]) - self.coeff_h[a][2, :] = 1 / 2 * np.array([0.0, 0.0, -1.0, 3.0]) + self.coeff_h[a][0, :] = 1 / 2 * xp.array([3.0, -1.0, 0.0, 0.0]) + self.coeff_h[a][1, :] = 1 / 2 * xp.array([-1.0, 3.0, 3.0, -1.0]) + self.coeff_h[a][2, :] = 1 / 2 * xp.array([0.0, 0.0, -1.0, 3.0]) elif self.p[a] == 3: - self.coeff_i[a][0, :] = 1 / 18 * np.array([18.0, 0.0, 0.0, 0.0, 0.0]) - self.coeff_i[a][1, :] = 1 / 18 * np.array([-5.0, 40.0, -24.0, 8.0, -1.0]) - self.coeff_i[a][2, :] = 1 / 18 * np.array([3.0, -24.0, 60.0, -24.0, 3.0]) - self.coeff_i[a][3, :] = 1 / 18 * np.array([-1.0, 8.0, -24.0, 40.0, -5.0]) - self.coeff_i[a][4, :] = 1 / 18 * np.array([0.0, 0.0, 0.0, 0.0, 18.0]) - - self.coeff_h[a][0, :] = 1 / 18 * np.array([23.0, -17.0, 7.0, -1.0, 0.0, 0.0]) - self.coeff_h[a][1, :] = 1 / 18 * np.array([-8.0, 56.0, -28.0, 4.0, 0.0, 0.0]) - self.coeff_h[a][2, :] = 1 / 18 * np.array([3.0, -21.0, 36.0, 36.0, -21.0, 3.0]) - self.coeff_h[a][3, :] = 1 / 18 * np.array([0.0, 0.0, 4.0, -28.0, 56.0, -8.0]) - self.coeff_h[a][4, :] = 1 / 18 * np.array([0.0, 0.0, -1.0, 7.0, -17.0, 23.0]) + self.coeff_i[a][0, :] = 1 / 18 * xp.array([18.0, 0.0, 0.0, 0.0, 0.0]) + self.coeff_i[a][1, :] = 1 / 18 * xp.array([-5.0, 40.0, -24.0, 8.0, -1.0]) + self.coeff_i[a][2, :] = 1 / 18 * xp.array([3.0, -24.0, 60.0, -24.0, 3.0]) + self.coeff_i[a][3, :] = 1 / 18 * xp.array([-1.0, 8.0, -24.0, 40.0, -5.0]) + self.coeff_i[a][4, :] = 1 / 18 * xp.array([0.0, 0.0, 0.0, 0.0, 18.0]) + + self.coeff_h[a][0, :] = 1 / 18 * xp.array([23.0, -17.0, 7.0, -1.0, 0.0, 0.0]) + self.coeff_h[a][1, :] = 1 / 18 * xp.array([-8.0, 56.0, -28.0, 4.0, 0.0, 0.0]) + self.coeff_h[a][2, :] = 1 / 18 * xp.array([3.0, -21.0, 36.0, 36.0, -21.0, 3.0]) + self.coeff_h[a][3, :] = 1 / 18 * xp.array([0.0, 0.0, 4.0, -28.0, 56.0, -8.0]) + self.coeff_h[a][4, :] = 1 / 18 * xp.array([0.0, 0.0, -1.0, 7.0, -17.0, 23.0]) elif self.p[a] == 4: - self.coeff_i[a][0, :] = 1 / 360 * np.array([360.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) - self.coeff_i[a][1, :] = 1 / 360 * np.array([-59.0, 944.0, -1000.0, 720.0, -305.0, 64.0, -4.0]) - self.coeff_i[a][2, :] = 1 / 360 * np.array([23.0, -368.0, 1580.0, -1360.0, 605.0, -128.0, 8.0]) - self.coeff_i[a][3, :] = 1 / 360 * np.array([-16.0, 256.0, -1180.0, 2240.0, -1180.0, 256.0, -16.0]) - self.coeff_i[a][4, :] = 1 / 360 * np.array([8.0, -128.0, 605.0, -1360.0, 1580.0, -368.0, 23.0]) - self.coeff_i[a][5, :] = 1 / 360 * np.array([-4.0, 64.0, -305.0, 720.0, -1000.0, 944.0, -59.0]) - self.coeff_i[a][6, :] = 1 / 360 * np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 360.0]) - - self.coeff_h[a][0, :] = 1 / 360 * np.array([419.0, -525.0, 475.0, -245.0, 60.0, -4.0, 0.0, 0.0]) - self.coeff_h[a][1, :] = 1 / 360 * np.array([-82.0, 1230.0, -1350.0, 730.0, -180.0, 12.0, 0.0, 0.0]) - self.coeff_h[a][2, :] = 1 / 360 * np.array([39.0, -585.0, 2175.0, -1425.0, 360.0, -24.0, 0.0, 0.0]) + self.coeff_i[a][0, :] = 1 / 360 * xp.array([360.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + self.coeff_i[a][1, :] = 1 / 360 * xp.array([-59.0, 944.0, -1000.0, 720.0, -305.0, 64.0, -4.0]) + self.coeff_i[a][2, :] = 1 / 360 * xp.array([23.0, -368.0, 1580.0, -1360.0, 605.0, -128.0, 8.0]) + self.coeff_i[a][3, :] = 1 / 360 * xp.array([-16.0, 256.0, -1180.0, 2240.0, -1180.0, 256.0, -16.0]) + self.coeff_i[a][4, :] = 1 / 360 * xp.array([8.0, -128.0, 605.0, -1360.0, 1580.0, -368.0, 23.0]) + self.coeff_i[a][5, :] = 1 / 360 * xp.array([-4.0, 64.0, -305.0, 720.0, -1000.0, 944.0, -59.0]) + self.coeff_i[a][6, :] = 1 / 360 * xp.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 360.0]) + + self.coeff_h[a][0, :] = 1 / 360 * xp.array([419.0, -525.0, 475.0, -245.0, 60.0, -4.0, 0.0, 0.0]) + self.coeff_h[a][1, :] = 1 / 360 * xp.array([-82.0, 1230.0, -1350.0, 730.0, -180.0, 12.0, 0.0, 0.0]) + self.coeff_h[a][2, :] = 1 / 360 * xp.array([39.0, -585.0, 2175.0, -1425.0, 360.0, -24.0, 0.0, 0.0]) self.coeff_h[a][3, :] = ( - 1 / 360 * np.array([-16.0, 240.0, -924.0, 1060.0, 1060.0, -924.0, 240.0, -16.0]) + 1 / 360 * xp.array([-16.0, 240.0, -924.0, 1060.0, 1060.0, -924.0, 240.0, -16.0]) ) - self.coeff_h[a][4, :] = 1 / 360 * np.array([0.0, 0.0, -24.0, 360.0, -1425.0, 2175.0, -585.0, 39.0]) - self.coeff_h[a][5, :] = 1 / 360 * np.array([0.0, 0.0, 12.0, -180.0, 730.0, -1350.0, 1230.0, -82.0]) - self.coeff_h[a][6, :] = 1 / 360 * np.array([0.0, 0.0, -4.0, 60.0, -245.0, 475.0, -525.0, 419.0]) + self.coeff_h[a][4, :] = 1 / 360 * xp.array([0.0, 0.0, -24.0, 360.0, -1425.0, 2175.0, -585.0, 39.0]) + self.coeff_h[a][5, :] = 1 / 360 * xp.array([0.0, 0.0, 12.0, -180.0, 730.0, -1350.0, 1230.0, -82.0]) + self.coeff_h[a][6, :] = 1 / 360 * xp.array([0.0, 0.0, -4.0, 60.0, -245.0, 475.0, -525.0, 419.0]) else: print("degree > 4 not implemented!") @@ -150,31 +150,31 @@ def __init__(self, tensor_space, n_quad): ) # number of non-vanishing D bf in interpolation interval (1, 2, 4, 6) self.x_int = [ - np.zeros((n_lambda_int, n_int), dtype=float) for n_lambda_int, n_int in zip(n_lambda_int, self.n_int) + xp.zeros((n_lambda_int, n_int), dtype=float) for n_lambda_int, n_int in zip(n_lambda_int, self.n_int) ] self.int_global_N = [ - np.zeros((n_lambda_int, n_int_locbf_N), dtype=int) + xp.zeros((n_lambda_int, n_int_locbf_N), dtype=int) for n_lambda_int, n_int_locbf_N in zip(n_lambda_int, self.n_int_locbf_N) ] self.int_global_D = [ - np.zeros((n_lambda_int, n_int_locbf_D), dtype=int) + xp.zeros((n_lambda_int, n_int_locbf_D), dtype=int) for n_lambda_int, n_int_locbf_D in zip(n_lambda_int, self.n_int_locbf_D) ] self.int_loccof_N = [ - np.zeros((n_lambda_int, n_int_locbf_N), dtype=int) + xp.zeros((n_lambda_int, n_int_locbf_N), dtype=int) for n_lambda_int, n_int_locbf_N in zip(n_lambda_int, self.n_int_locbf_N) ] self.int_loccof_D = [ - np.zeros((n_lambda_int, n_int_locbf_D), dtype=int) + xp.zeros((n_lambda_int, n_int_locbf_D), dtype=int) for n_lambda_int, n_int_locbf_D in zip(n_lambda_int, self.n_int_locbf_D) ] self.x_int_indices = [ - np.zeros((n_lambda_int, n_int), dtype=int) for n_lambda_int, n_int in zip(n_lambda_int, self.n_int) + xp.zeros((n_lambda_int, n_int), dtype=int) for n_lambda_int, n_int in zip(n_lambda_int, self.n_int) ] - self.coeffi_indices = [np.zeros(n_lambda_int, dtype=int) for n_lambda_int in n_lambda_int] + self.coeffi_indices = [xp.zeros(n_lambda_int, dtype=int) for n_lambda_int in n_lambda_int] self.n_int_nvcof_D = [None, None, None] self.n_int_nvcof_N = [None, None, None] @@ -197,39 +197,39 @@ def __init__(self, tensor_space, n_quad): self.n_int_nvcof_N[a] = 3 * self.p[a] - 2 # shift in local coefficient indices at right boundary (only for non-periodic boundary conditions) - self.int_add_D[a] = np.arange(self.n_int[a] - 2) + 1 - self.int_add_N[a] = np.arange(self.n_int[a] - 1) + 1 + self.int_add_D[a] = xp.arange(self.n_int[a] - 2) + 1 + self.int_add_N[a] = xp.arange(self.n_int[a] - 1) + 1 counter_D = 0 counter_N = 0 # shift local coefficients --> global coefficients (D) if self.p[a] == 1: - self.int_shift_D[a] = np.arange(self.NbaseD[a]) + self.int_shift_D[a] = xp.arange(self.NbaseD[a]) else: - self.int_shift_D[a] = np.arange(self.NbaseD[a]) - (self.p[a] - 2) + self.int_shift_D[a] = xp.arange(self.NbaseD[a]) - (self.p[a] - 2) self.int_shift_D[a][: 2 * self.p[a] - 2] = 0 self.int_shift_D[a][-(2 * self.p[a] - 2) :] = self.int_shift_D[a][-(2 * self.p[a] - 2)] # shift local coefficients --> global coefficients (N) if self.p[a] == 1: - self.int_shift_N[a] = np.arange(self.NbaseN[a]) + self.int_shift_N[a] = xp.arange(self.NbaseN[a]) self.int_shift_N[a][-1] = self.int_shift_N[a][-2] else: - self.int_shift_N[a] = np.arange(self.NbaseN[a]) - (self.p[a] - 1) + self.int_shift_N[a] = xp.arange(self.NbaseN[a]) - (self.p[a] - 1) self.int_shift_N[a][: 2 * self.p[a] - 1] = 0 self.int_shift_N[a][-(2 * self.p[a] - 1) :] = self.int_shift_N[a][-(2 * self.p[a] - 1)] - counter_coeffi = np.copy(self.p[a]) + counter_coeffi = xp.copy(self.p[a]) for i in range(n_lambda_int[a]): # left boundary region if i < self.p[a] - 1: - self.int_global_N[a][i] = np.arange(self.n_int_locbf_N[a]) - self.int_global_D[a][i] = np.arange(self.n_int_locbf_D[a]) + self.int_global_N[a][i] = xp.arange(self.n_int_locbf_N[a]) + self.int_global_D[a][i] = xp.arange(self.n_int_locbf_D[a]) - self.x_int_indices[a][i] = np.arange(self.n_int[a]) + self.x_int_indices[a][i] = xp.arange(self.n_int[a]) self.coeffi_indices[a][i] = i for j in range(2 * (self.p[a] - 1) + 1): xi = self.p[a] - 1 @@ -240,13 +240,13 @@ def __init__(self, tensor_space, n_quad): # right boundary region elif i > n_lambda_int[a] - self.p[a]: self.int_global_N[a][i] = ( - np.arange(self.n_int_locbf_N[a]) + n_lambda_int[a] - self.p[a] - (self.p[a] - 1) + xp.arange(self.n_int_locbf_N[a]) + n_lambda_int[a] - self.p[a] - (self.p[a] - 1) ) self.int_global_D[a][i] = ( - np.arange(self.n_int_locbf_D[a]) + n_lambda_int[a] - self.p[a] - (self.p[a] - 1) + xp.arange(self.n_int_locbf_D[a]) + n_lambda_int[a] - self.p[a] - (self.p[a] - 1) ) - self.x_int_indices[a][i] = np.arange(self.n_int[a]) + 2 * ( + self.x_int_indices[a][i] = xp.arange(self.n_int[a]) + 2 * ( n_lambda_int[a] - self.p[a] - (self.p[a] - 1) ) self.coeffi_indices[a][i] = counter_coeffi @@ -260,20 +260,20 @@ def __init__(self, tensor_space, n_quad): # interior else: if self.p[a] == 1: - self.int_global_N[a][i] = np.arange(self.n_int_locbf_N[a]) + i - self.int_global_D[a][i] = np.arange(self.n_int_locbf_D[a]) + i + self.int_global_N[a][i] = xp.arange(self.n_int_locbf_N[a]) + i + self.int_global_D[a][i] = xp.arange(self.n_int_locbf_D[a]) + i self.int_global_N[a][-1] = self.int_global_N[a][-2] self.int_global_D[a][-1] = self.int_global_D[a][-2] else: - self.int_global_N[a][i] = np.arange(self.n_int_locbf_N[a]) + i - (self.p[a] - 1) - self.int_global_D[a][i] = np.arange(self.n_int_locbf_D[a]) + i - (self.p[a] - 1) + self.int_global_N[a][i] = xp.arange(self.n_int_locbf_N[a]) + i - (self.p[a] - 1) + self.int_global_D[a][i] = xp.arange(self.n_int_locbf_D[a]) + i - (self.p[a] - 1) if self.p[a] == 1: self.x_int_indices[a][i] = i else: - self.x_int_indices[a][i] = np.arange(self.n_int[a]) + 2 * (i - (self.p[a] - 1)) + self.x_int_indices[a][i] = xp.arange(self.n_int[a]) + 2 * (i - (self.p[a] - 1)) self.coeffi_indices[a][i] = self.p[a] - 1 @@ -284,8 +284,8 @@ def __init__(self, tensor_space, n_quad): # local coefficient index if self.p[a] == 1: - self.int_loccof_N[a][i] = np.array([0, 1]) - self.int_loccof_D[a][-1] = np.array([1]) + self.int_loccof_N[a][i] = xp.array([0, 1]) + self.int_loccof_D[a][-1] = xp.array([1]) else: if i > 0: @@ -293,8 +293,8 @@ def __init__(self, tensor_space, n_quad): k_glob_new = self.int_global_D[a][i, il] bol = k_glob_new == self.int_global_D[a][i - 1] - if np.any(bol): - self.int_loccof_D[a][i, il] = self.int_loccof_D[a][i - 1, np.where(bol)[0][0]] + 1 + if xp.any(bol): + self.int_loccof_D[a][i, il] = self.int_loccof_D[a][i - 1, xp.where(bol)[0][0]] + 1 if (k_glob_new >= n_lambda_int[a] - self.p[a] - (self.p[a] - 2)) and ( self.int_loccof_D[a][i, il] == 0 @@ -306,8 +306,8 @@ def __init__(self, tensor_space, n_quad): k_glob_new = self.int_global_N[a][i, il] bol = k_glob_new == self.int_global_N[a][i - 1] - if np.any(bol): - self.int_loccof_N[a][i, il] = self.int_loccof_N[a][i - 1, np.where(bol)[0][0]] + 1 + if xp.any(bol): + self.int_loccof_N[a][i, il] = self.int_loccof_N[a][i - 1, xp.where(bol)[0][0]] + 1 if (k_glob_new >= n_lambda_int[a] - self.p[a] - (self.p[a] - 2)) and ( self.int_loccof_N[a][i, il] == 0 @@ -327,24 +327,24 @@ def __init__(self, tensor_space, n_quad): # shift local coefficients --> global coefficients if self.p[a] == 1: - self.int_shift_D[a] = np.arange(self.NbaseN[a]) - (self.p[a] - 1) - self.int_shift_N[a] = np.arange(self.NbaseN[a]) - (self.p[a]) + self.int_shift_D[a] = xp.arange(self.NbaseN[a]) - (self.p[a] - 1) + self.int_shift_N[a] = xp.arange(self.NbaseN[a]) - (self.p[a]) else: - self.int_shift_D[a] = np.arange(self.NbaseN[a]) - (self.p[a] - 2) - self.int_shift_N[a] = np.arange(self.NbaseN[a]) - (self.p[a] - 1) + self.int_shift_D[a] = xp.arange(self.NbaseN[a]) - (self.p[a] - 2) + self.int_shift_N[a] = xp.arange(self.NbaseN[a]) - (self.p[a] - 1) for i in range(n_lambda_int[a]): # global indices of non-vanishing basis functions and position of coefficients in final matrix - self.int_global_N[a][i] = (np.arange(self.n_int_locbf_N[a]) + i - (self.p[a] - 1)) % self.NbaseN[a] - self.int_global_D[a][i] = (np.arange(self.n_int_locbf_D[a]) + i - (self.p[a] - 1)) % self.NbaseD[a] + self.int_global_N[a][i] = (xp.arange(self.n_int_locbf_N[a]) + i - (self.p[a] - 1)) % self.NbaseN[a] + self.int_global_D[a][i] = (xp.arange(self.n_int_locbf_D[a]) + i - (self.p[a] - 1)) % self.NbaseD[a] - self.int_loccof_N[a][i] = np.arange(self.n_int_locbf_N[a] - 1, -1, -1) - self.int_loccof_D[a][i] = np.arange(self.n_int_locbf_D[a] - 1, -1, -1) + self.int_loccof_N[a][i] = xp.arange(self.n_int_locbf_N[a] - 1, -1, -1) + self.int_loccof_D[a][i] = xp.arange(self.n_int_locbf_D[a] - 1, -1, -1) if self.p[a] == 1: self.x_int_indices[a][i] = i else: - self.x_int_indices[a][i] = (np.arange(self.n_int[a]) + 2 * (i - (self.p[a] - 1))) % ( + self.x_int_indices[a][i] = (xp.arange(self.n_int[a]) + 2 * (i - (self.p[a] - 1))) % ( 2 * self.Nel[a] ) @@ -356,41 +356,41 @@ def __init__(self, tensor_space, n_quad): ) % 1.0 # identify unique interpolation points to save memory - self.x_int[a] = np.unique(self.x_int[a].flatten()) + self.x_int[a] = xp.unique(self.x_int[a].flatten()) # set histopolation points, quadrature points and weights - n_lambda_his = [np.copy(NbaseD) for NbaseD in self.NbaseD] # number of coefficients in space V1 + n_lambda_his = [xp.copy(NbaseD) for NbaseD in self.NbaseD] # number of coefficients in space V1 self.n_his = [2 * p for p in self.p] # number of histopolation intervals self.n_his_locbf_N = [2 * p for p in self.p] # number of non-vanishing N bf in histopolation interval self.n_his_locbf_D = [2 * p - 1 for p in self.p] # number of non-vanishing D bf in histopolation interval self.x_his = [ - np.zeros((n_lambda_his, n_his + 1), dtype=float) for n_lambda_his, n_his in zip(n_lambda_his, self.n_his) + xp.zeros((n_lambda_his, n_his + 1), dtype=float) for n_lambda_his, n_his in zip(n_lambda_his, self.n_his) ] self.his_global_N = [ - np.zeros((n_lambda_his, n_his_locbf_N), dtype=int) + xp.zeros((n_lambda_his, n_his_locbf_N), dtype=int) for n_lambda_his, n_his_locbf_N in zip(n_lambda_his, self.n_his_locbf_N) ] self.his_global_D = [ - np.zeros((n_lambda_his, n_his_locbf_D), dtype=int) + xp.zeros((n_lambda_his, n_his_locbf_D), dtype=int) for n_lambda_his, n_his_locbf_D in zip(n_lambda_his, self.n_his_locbf_D) ] self.his_loccof_N = [ - np.zeros((n_lambda_his, n_his_locbf_N), dtype=int) + xp.zeros((n_lambda_his, n_his_locbf_N), dtype=int) for n_lambda_his, n_his_locbf_N in zip(n_lambda_his, self.n_his_locbf_N) ] self.his_loccof_D = [ - np.zeros((n_lambda_his, n_his_locbf_D), dtype=int) + xp.zeros((n_lambda_his, n_his_locbf_D), dtype=int) for n_lambda_his, n_his_locbf_D in zip(n_lambda_his, self.n_his_locbf_D) ] self.x_his_indices = [ - np.zeros((n_lambda_his, n_his), dtype=int) for n_lambda_his, n_his in zip(n_lambda_his, self.n_his) + xp.zeros((n_lambda_his, n_his), dtype=int) for n_lambda_his, n_his in zip(n_lambda_his, self.n_his) ] - self.coeffh_indices = [np.zeros(n_lambda_his, dtype=int) for n_lambda_his in n_lambda_his] + self.coeffh_indices = [xp.zeros(n_lambda_his, dtype=int) for n_lambda_his in n_lambda_his] self.pts = [0, 0, 0] self.wts = [0, 0, 0] @@ -411,31 +411,31 @@ def __init__(self, tensor_space, n_quad): self.n_his_nvcof_N[a] = 3 * self.p[a] - 1 # shift in local coefficient indices at right boundary (only for non-periodic boundary conditions) - self.his_add_D[a] = np.arange(self.n_his[a] - 2) + 1 - self.his_add_N[a] = np.arange(self.n_his[a] - 1) + 1 + self.his_add_D[a] = xp.arange(self.n_his[a] - 2) + 1 + self.his_add_N[a] = xp.arange(self.n_his[a] - 1) + 1 counter_D = 0 counter_N = 0 # shift local coefficients --> global coefficients (D) - self.his_shift_D[a] = np.arange(self.NbaseD[a]) - (self.p[a] - 1) + self.his_shift_D[a] = xp.arange(self.NbaseD[a]) - (self.p[a] - 1) self.his_shift_D[a][: 2 * self.p[a] - 1] = 0 self.his_shift_D[a][-(2 * self.p[a] - 1) :] = self.his_shift_D[a][-(2 * self.p[a] - 1)] # shift local coefficients --> global coefficients (N) - self.his_shift_N[a] = np.arange(self.NbaseN[a]) - self.p[a] + self.his_shift_N[a] = xp.arange(self.NbaseN[a]) - self.p[a] self.his_shift_N[a][: 2 * self.p[a]] = 0 self.his_shift_N[a][-2 * self.p[a] :] = self.his_shift_N[a][-2 * self.p[a]] - counter_coeffh = np.copy(self.p[a]) + counter_coeffh = xp.copy(self.p[a]) for i in range(n_lambda_his[a]): # left boundary region if i < self.p[a] - 1: - self.his_global_N[a][i] = np.arange(self.n_his_locbf_N[a]) - self.his_global_D[a][i] = np.arange(self.n_his_locbf_D[a]) + self.his_global_N[a][i] = xp.arange(self.n_his_locbf_N[a]) + self.his_global_D[a][i] = xp.arange(self.n_his_locbf_D[a]) - self.x_his_indices[a][i] = np.arange(self.n_his[a]) + self.x_his_indices[a][i] = xp.arange(self.n_his[a]) self.coeffh_indices[a][i] = i for j in range(2 * self.p[a] + 1): xi = self.p[a] - 1 @@ -446,13 +446,13 @@ def __init__(self, tensor_space, n_quad): # right boundary region elif i > n_lambda_his[a] - self.p[a]: self.his_global_N[a][i] = ( - np.arange(self.n_his_locbf_N[a]) + n_lambda_his[a] - self.p[a] - (self.p[a] - 1) + xp.arange(self.n_his_locbf_N[a]) + n_lambda_his[a] - self.p[a] - (self.p[a] - 1) ) self.his_global_D[a][i] = ( - np.arange(self.n_his_locbf_D[a]) + n_lambda_his[a] - self.p[a] - (self.p[a] - 1) + xp.arange(self.n_his_locbf_D[a]) + n_lambda_his[a] - self.p[a] - (self.p[a] - 1) ) - self.x_his_indices[a][i] = np.arange(self.n_his[a]) + 2 * ( + self.x_his_indices[a][i] = xp.arange(self.n_his[a]) + 2 * ( n_lambda_his[a] - self.p[a] - (self.p[a] - 1) ) self.coeffh_indices[a][i] = counter_coeffh @@ -465,10 +465,10 @@ def __init__(self, tensor_space, n_quad): # interior else: - self.his_global_N[a][i] = np.arange(self.n_his_locbf_N[a]) + i - (self.p[a] - 1) - self.his_global_D[a][i] = np.arange(self.n_his_locbf_D[a]) + i - (self.p[a] - 1) + self.his_global_N[a][i] = xp.arange(self.n_his_locbf_N[a]) + i - (self.p[a] - 1) + self.his_global_D[a][i] = xp.arange(self.n_his_locbf_D[a]) + i - (self.p[a] - 1) - self.x_his_indices[a][i] = np.arange(self.n_his[a]) + 2 * (i - (self.p[a] - 1)) + self.x_his_indices[a][i] = xp.arange(self.n_his[a]) + 2 * (i - (self.p[a] - 1)) self.coeffh_indices[a][i] = self.p[a] - 1 for j in range(2 * self.p[a] + 1): self.x_his[a][i, j] = ( @@ -481,8 +481,8 @@ def __init__(self, tensor_space, n_quad): k_glob_new = self.his_global_D[a][i, il] bol = k_glob_new == self.his_global_D[a][i - 1] - if np.any(bol): - self.his_loccof_D[a][i, il] = self.his_loccof_D[a][i - 1, np.where(bol)[0][0]] + 1 + if xp.any(bol): + self.his_loccof_D[a][i, il] = self.his_loccof_D[a][i - 1, xp.where(bol)[0][0]] + 1 if (k_glob_new >= n_lambda_his[a] - self.p[a] - (self.p[a] - 2)) and ( self.his_loccof_D[a][i, il] == 0 @@ -494,8 +494,8 @@ def __init__(self, tensor_space, n_quad): k_glob_new = self.his_global_N[a][i, il] bol = k_glob_new == self.his_global_N[a][i - 1] - if np.any(bol): - self.his_loccof_N[a][i, il] = self.his_loccof_N[a][i - 1, np.where(bol)[0][0]] + 1 + if xp.any(bol): + self.his_loccof_N[a][i, il] = self.his_loccof_N[a][i - 1, xp.where(bol)[0][0]] + 1 if (k_glob_new >= n_lambda_his[a] - self.p[a] - (self.p[a] - 2)) and ( self.his_loccof_N[a][i, il] == 0 @@ -505,7 +505,7 @@ def __init__(self, tensor_space, n_quad): # quadrature points and weights self.pts[a], self.wts[a] = bsp.quadrature_grid( - np.unique(self.x_his[a].flatten()), self.pts_loc[a], self.wts_loc[a] + xp.unique(self.x_his[a].flatten()), self.pts_loc[a], self.wts_loc[a] ) else: @@ -514,18 +514,18 @@ def __init__(self, tensor_space, n_quad): self.n_his_nvcof_N[a] = 2 * self.p[a] # shift local coefficients --> global coefficients (D) - self.his_shift_D[a] = np.arange(self.NbaseD[a]) - (self.p[a] - 1) + self.his_shift_D[a] = xp.arange(self.NbaseD[a]) - (self.p[a] - 1) # shift local coefficients --> global coefficients (N) - self.his_shift_N[a] = np.arange(self.NbaseD[a]) - self.p[a] + self.his_shift_N[a] = xp.arange(self.NbaseD[a]) - self.p[a] for i in range(n_lambda_his[a]): - self.his_global_N[a][i] = (np.arange(self.n_his_locbf_N[a]) + i - (self.p[a] - 1)) % self.NbaseN[a] - self.his_global_D[a][i] = (np.arange(self.n_his_locbf_D[a]) + i - (self.p[a] - 1)) % self.NbaseD[a] - self.his_loccof_N[a][i] = np.arange(self.n_his_locbf_N[a] - 1, -1, -1) - self.his_loccof_D[a][i] = np.arange(self.n_his_locbf_D[a] - 1, -1, -1) + self.his_global_N[a][i] = (xp.arange(self.n_his_locbf_N[a]) + i - (self.p[a] - 1)) % self.NbaseN[a] + self.his_global_D[a][i] = (xp.arange(self.n_his_locbf_D[a]) + i - (self.p[a] - 1)) % self.NbaseD[a] + self.his_loccof_N[a][i] = xp.arange(self.n_his_locbf_N[a] - 1, -1, -1) + self.his_loccof_D[a][i] = xp.arange(self.n_his_locbf_D[a] - 1, -1, -1) - self.x_his_indices[a][i] = (np.arange(self.n_his[a]) + 2 * (i - (self.p[a] - 1))) % ( + self.x_his_indices[a][i] = (xp.arange(self.n_his[a]) + 2 * (i - (self.p[a] - 1))) % ( 2 * self.Nel[a] ) self.coeffh_indices[a][i] = 0 @@ -535,7 +535,7 @@ def __init__(self, tensor_space, n_quad): # quadrature points and weights self.pts[a], self.wts[a] = bsp.quadrature_grid( - np.append(np.unique(self.x_his[a].flatten() % 1.0), 1.0), self.pts_loc[a], self.wts_loc[a] + xp.append(xp.unique(self.x_his[a].flatten() % 1.0), 1.0), self.pts_loc[a], self.wts_loc[a] ) # evaluate N basis functions at interpolation and quadrature points @@ -586,7 +586,7 @@ def projection_Q_0form(self, domain): """ # non-vanishing coefficients - Q11 = np.empty( + Q11 = xp.empty( ( self.NbaseN[0], self.NbaseN[1], @@ -597,7 +597,7 @@ def projection_Q_0form(self, domain): ), dtype=float, ) - Q22 = np.empty( + Q22 = xp.empty( ( self.NbaseN[0], self.NbaseN[1], @@ -608,7 +608,7 @@ def projection_Q_0form(self, domain): ), dtype=float, ) - Q33 = np.empty( + Q33 = xp.empty( ( self.NbaseN[0], self.NbaseN[1], @@ -626,7 +626,7 @@ def projection_Q_0form(self, domain): n_unique3 = [self.pts[0].flatten().size, self.pts[1].flatten().size, self.x_int[2].size] # ========= assembly of 1 - component (pi2_1 : int, his, his) ============ - mat_eq = np.empty((n_unique1[0], n_unique1[1], n_unique1[2]), dtype=float) + mat_eq = xp.empty((n_unique1[0], n_unique1[1], n_unique1[2]), dtype=float) ker_eva.kernel_eva( self.x_int[0], @@ -682,7 +682,7 @@ def projection_Q_0form(self, domain): ) # ========= assembly of 2 - component (pi2_2 : his, int, his) ============ - mat_eq = np.empty((n_unique2[0], n_unique2[1], n_unique2[2]), dtype=float) + mat_eq = xp.empty((n_unique2[0], n_unique2[1], n_unique2[2]), dtype=float) ker_eva.kernel_eva( self.pts[0].flatten(), @@ -738,7 +738,7 @@ def projection_Q_0form(self, domain): ) # ========= assembly of 3 - component (pi2_3 : his, his, int) ============ - mat_eq = np.empty((n_unique3[0], n_unique3[1], n_unique3[2]), dtype=float) + mat_eq = xp.empty((n_unique3[0], n_unique3[1], n_unique3[2]), dtype=float) ker_eva.kernel_eva( self.pts[0].flatten(), @@ -794,7 +794,7 @@ def projection_Q_0form(self, domain): ) # ========= conversion to sparse matrices (1 - component) ================= - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseN[1], @@ -819,7 +819,7 @@ def projection_Q_0form(self, domain): Q11.eliminate_zeros() # ========= conversion to sparse matrices (2 - component) ================= - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseN[1], @@ -844,7 +844,7 @@ def projection_Q_0form(self, domain): Q22.eliminate_zeros() # ========= conversion to sparse matrices (3 - component) ================= - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseN[1], @@ -895,7 +895,7 @@ def projection_Q_2form(self, domain): """ # non-vanishing coefficients - Q11 = np.empty( + Q11 = xp.empty( ( self.NbaseN[0], self.NbaseD[1], @@ -906,7 +906,7 @@ def projection_Q_2form(self, domain): ), dtype=float, ) - Q22 = np.empty( + Q22 = xp.empty( ( self.NbaseD[0], self.NbaseN[1], @@ -917,7 +917,7 @@ def projection_Q_2form(self, domain): ), dtype=float, ) - Q33 = np.empty( + Q33 = xp.empty( ( self.NbaseD[0], self.NbaseD[1], @@ -935,7 +935,7 @@ def projection_Q_2form(self, domain): n_unique3 = [self.pts[0].flatten().size, self.pts[1].flatten().size, self.x_int[2].size] # ========= assembly of 1 - component (pi2_1 : int, his, his) ============ - mat_eq = np.empty((n_unique1[0], n_unique1[1], n_unique1[2]), dtype=float) + mat_eq = xp.empty((n_unique1[0], n_unique1[1], n_unique1[2]), dtype=float) ker_eva.kernel_eva( self.x_int[0], @@ -991,7 +991,7 @@ def projection_Q_2form(self, domain): ) # ========= assembly of 2 - component (pi2_2 : his, int, his) ============ - mat_eq = np.empty((n_unique2[0], n_unique2[1], n_unique2[2]), dtype=float) + mat_eq = xp.empty((n_unique2[0], n_unique2[1], n_unique2[2]), dtype=float) ker_eva.kernel_eva( self.pts[0].flatten(), @@ -1047,7 +1047,7 @@ def projection_Q_2form(self, domain): ) # ========= assembly of 3 - component (pi2_3 : his, his, int) ============ - mat_eq = np.empty((n_unique3[0], n_unique3[1], n_unique3[2]), dtype=float) + mat_eq = xp.empty((n_unique3[0], n_unique3[1], n_unique3[2]), dtype=float) ker_eva.kernel_eva( self.pts[0].flatten(), @@ -1103,7 +1103,7 @@ def projection_Q_2form(self, domain): ) # ========= conversion to sparse matrices (1 - component) ================= - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseD[1], @@ -1128,7 +1128,7 @@ def projection_Q_2form(self, domain): Q11.eliminate_zeros() # ========= conversion to sparse matrices (2 - component) ================= - indices = np.indices( + indices = xp.indices( ( self.NbaseD[0], self.NbaseN[1], @@ -1153,7 +1153,7 @@ def projection_Q_2form(self, domain): Q22.eliminate_zeros() # ========= conversion to sparse matrices (3 - component) ================= - indices = np.indices( + indices = xp.indices( ( self.NbaseD[0], self.NbaseD[1], @@ -1204,7 +1204,7 @@ def projection_W_0form(self, domain): """ # non-vanishing coefficients - W1 = np.empty( + W1 = xp.empty( ( self.NbaseN[0], self.NbaseN[1], @@ -1215,14 +1215,14 @@ def projection_W_0form(self, domain): ), dtype=float, ) - # W2 = np.empty((self.NbaseN[0], self.NbaseN[1], self.NbaseN[2], self.n_int_nvcof_N[0], self.n_int_nvcof_N[1], self.n_int_nvcof_N[2]), dtype=float) - # W3 = np.empty((self.NbaseN[0], self.NbaseN[1], self.NbaseN[2], self.n_int_nvcof_N[0], self.n_int_nvcof_N[1], self.n_int_nvcof_N[2]), dtype=float) + # W2 = xp.empty((self.NbaseN[0], self.NbaseN[1], self.NbaseN[2], self.n_int_nvcof_N[0], self.n_int_nvcof_N[1], self.n_int_nvcof_N[2]), dtype=float) + # W3 = xp.empty((self.NbaseN[0], self.NbaseN[1], self.NbaseN[2], self.n_int_nvcof_N[0], self.n_int_nvcof_N[1], self.n_int_nvcof_N[2]), dtype=float) # size of interpolation/quadrature points of the 3 components n_unique = [self.x_int[0].size, self.x_int[1].size, self.x_int[2].size] # assembly - mat_eq = np.empty((n_unique[0], n_unique[1], n_unique[2]), dtype=float) + mat_eq = xp.empty((n_unique[0], n_unique[1], n_unique[2]), dtype=float) ker_eva.kernel_eva( self.x_int[0], @@ -1284,7 +1284,7 @@ def projection_W_0form(self, domain): """ # conversion to sparse matrix - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseN[1], @@ -1345,7 +1345,7 @@ def projection_T_0form(self, domain): """ # non-vanishing coefficients - T12 = np.empty( + T12 = xp.empty( ( self.NbaseN[0], self.NbaseN[1], @@ -1356,7 +1356,7 @@ def projection_T_0form(self, domain): ), dtype=float, ) - T13 = np.empty( + T13 = xp.empty( ( self.NbaseN[0], self.NbaseN[1], @@ -1368,7 +1368,7 @@ def projection_T_0form(self, domain): dtype=float, ) - T21 = np.empty( + T21 = xp.empty( ( self.NbaseN[0], self.NbaseN[1], @@ -1379,7 +1379,7 @@ def projection_T_0form(self, domain): ), dtype=float, ) - T23 = np.empty( + T23 = xp.empty( ( self.NbaseN[0], self.NbaseN[1], @@ -1391,7 +1391,7 @@ def projection_T_0form(self, domain): dtype=float, ) - T31 = np.empty( + T31 = xp.empty( ( self.NbaseN[0], self.NbaseN[1], @@ -1402,7 +1402,7 @@ def projection_T_0form(self, domain): ), dtype=float, ) - T32 = np.empty( + T32 = xp.empty( ( self.NbaseN[0], self.NbaseN[1], @@ -1420,7 +1420,7 @@ def projection_T_0form(self, domain): n_unique3 = [self.x_int[0].size, self.x_int[1].size, self.pts[2].flatten().size] # ================= assembly of 1 - component (pi1_1 : his, int, int) ============ - mat_eq = np.empty((n_unique1[0], n_unique1[1], n_unique1[2]), dtype=float) + mat_eq = xp.empty((n_unique1[0], n_unique1[1], n_unique1[2]), dtype=float) ker_eva.kernel_eva( self.pts[0].flatten(), @@ -1515,7 +1515,7 @@ def projection_T_0form(self, domain): ) # ================= assembly of 2 - component (PI_1_2 : int, his, int) ============ - mat_eq = np.empty((n_unique2[0], n_unique2[1], n_unique2[2]), dtype=float) + mat_eq = xp.empty((n_unique2[0], n_unique2[1], n_unique2[2]), dtype=float) ker_eva.kernel_eva( self.x_int[0], @@ -1621,7 +1621,7 @@ def projection_T_0form(self, domain): ) # ================= assembly of 3 - component (PI_1_3 : int, int, his) ============ - mat_eq = np.empty((n_unique3[0], n_unique3[1], n_unique3[2]), dtype=float) + mat_eq = xp.empty((n_unique3[0], n_unique3[1], n_unique3[2]), dtype=float) ker_eva.kernel_eva( self.x_int[0], @@ -1727,7 +1727,7 @@ def projection_T_0form(self, domain): ) # conversion to sparse matrices (1 - component) - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseN[1], @@ -1751,7 +1751,7 @@ def projection_T_0form(self, domain): ) T12.eliminate_zeros() - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseN[1], @@ -1776,7 +1776,7 @@ def projection_T_0form(self, domain): T13.eliminate_zeros() # conversion to sparse matrices (2 - component) - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseN[1], @@ -1800,7 +1800,7 @@ def projection_T_0form(self, domain): ) T21.eliminate_zeros() - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseN[1], @@ -1825,7 +1825,7 @@ def projection_T_0form(self, domain): T23.eliminate_zeros() # conversion to sparse matrices (3 - component) - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseN[1], @@ -1849,7 +1849,7 @@ def projection_T_0form(self, domain): ) T31.eliminate_zeros() - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseN[1], @@ -1900,7 +1900,7 @@ def projection_T_1form(self, domain): """ # non-vanishing coefficients - T12 = np.empty( + T12 = xp.empty( ( self.NbaseN[0], self.NbaseD[1], @@ -1911,7 +1911,7 @@ def projection_T_1form(self, domain): ), dtype=float, ) - T13 = np.empty( + T13 = xp.empty( ( self.NbaseN[0], self.NbaseN[1], @@ -1923,7 +1923,7 @@ def projection_T_1form(self, domain): dtype=float, ) - T21 = np.empty( + T21 = xp.empty( ( self.NbaseD[0], self.NbaseN[1], @@ -1934,7 +1934,7 @@ def projection_T_1form(self, domain): ), dtype=float, ) - T23 = np.empty( + T23 = xp.empty( ( self.NbaseN[0], self.NbaseN[1], @@ -1946,7 +1946,7 @@ def projection_T_1form(self, domain): dtype=float, ) - T31 = np.empty( + T31 = xp.empty( ( self.NbaseD[0], self.NbaseN[1], @@ -1957,7 +1957,7 @@ def projection_T_1form(self, domain): ), dtype=float, ) - T32 = np.empty( + T32 = xp.empty( ( self.NbaseN[0], self.NbaseD[1], @@ -1975,7 +1975,7 @@ def projection_T_1form(self, domain): n_unique3 = [self.x_int[0].size, self.x_int[1].size, self.pts[2].flatten().size] # ================= assembly of 1 - component (pi1_1 : his, int, int) ============ - mat_eq = np.empty((n_unique1[0], n_unique1[1], n_unique1[2]), dtype=float) + mat_eq = xp.empty((n_unique1[0], n_unique1[1], n_unique1[2]), dtype=float) ker_eva.kernel_eva( self.pts[0].flatten(), @@ -2070,7 +2070,7 @@ def projection_T_1form(self, domain): ) # ================= assembly of 2 - component (PI_1_2 : int, his, int) ============ - mat_eq = np.empty((n_unique2[0], n_unique2[1], n_unique2[2]), dtype=float) + mat_eq = xp.empty((n_unique2[0], n_unique2[1], n_unique2[2]), dtype=float) ker_eva.kernel_eva( self.x_int[0], @@ -2165,7 +2165,7 @@ def projection_T_1form(self, domain): ) # ================= assembly of 3 - component (PI_1_3 : int, int, his) ============ - mat_eq = np.empty((n_unique3[0], n_unique3[1], n_unique3[2]), dtype=float) + mat_eq = xp.empty((n_unique3[0], n_unique3[1], n_unique3[2]), dtype=float) ker_eva.kernel_eva( self.x_int[0], @@ -2260,7 +2260,7 @@ def projection_T_1form(self, domain): ) # conversion to sparse matrices (1 - component) - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseD[1], @@ -2284,7 +2284,7 @@ def projection_T_1form(self, domain): ) T12.eliminate_zeros() - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseN[1], @@ -2309,7 +2309,7 @@ def projection_T_1form(self, domain): T13.eliminate_zeros() # conversion to sparse matrices (2 - component) - indices = np.indices( + indices = xp.indices( ( self.NbaseD[0], self.NbaseN[1], @@ -2333,7 +2333,7 @@ def projection_T_1form(self, domain): ) T21.eliminate_zeros() - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseN[1], @@ -2358,7 +2358,7 @@ def projection_T_1form(self, domain): T23.eliminate_zeros() # conversion to sparse matrices (3 - component) - indices = np.indices( + indices = xp.indices( ( self.NbaseD[0], self.NbaseN[1], @@ -2382,7 +2382,7 @@ def projection_T_1form(self, domain): ) T31.eliminate_zeros() - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseD[1], @@ -2433,7 +2433,7 @@ def projection_T_2form(self, domain): """ # non-vanishing coefficients - T12 = np.empty( + T12 = xp.empty( ( self.NbaseD[0], self.NbaseN[1], @@ -2444,7 +2444,7 @@ def projection_T_2form(self, domain): ), dtype=float, ) - T13 = np.empty( + T13 = xp.empty( ( self.NbaseD[0], self.NbaseD[1], @@ -2456,7 +2456,7 @@ def projection_T_2form(self, domain): dtype=float, ) - T21 = np.empty( + T21 = xp.empty( ( self.NbaseN[0], self.NbaseD[1], @@ -2467,7 +2467,7 @@ def projection_T_2form(self, domain): ), dtype=float, ) - T23 = np.empty( + T23 = xp.empty( ( self.NbaseD[0], self.NbaseD[1], @@ -2479,7 +2479,7 @@ def projection_T_2form(self, domain): dtype=float, ) - T31 = np.empty( + T31 = xp.empty( ( self.NbaseN[0], self.NbaseD[1], @@ -2490,7 +2490,7 @@ def projection_T_2form(self, domain): ), dtype=float, ) - T32 = np.empty( + T32 = xp.empty( ( self.NbaseD[0], self.NbaseN[1], @@ -2508,7 +2508,7 @@ def projection_T_2form(self, domain): n_unique3 = [self.x_int[0].size, self.x_int[1].size, self.pts[2].flatten().size] # ================= assembly of 1 - component (pi1_1 : his, int, int) ============ - mat_eq = np.empty((n_unique1[0], n_unique1[1], n_unique1[2]), dtype=float) + mat_eq = xp.empty((n_unique1[0], n_unique1[1], n_unique1[2]), dtype=float) ker_eva.kernel_eva( self.pts[0].flatten(), @@ -2603,7 +2603,7 @@ def projection_T_2form(self, domain): ) # ================= assembly of 2 - component (PI_1_2 : int, his, int) ============ - mat_eq = np.empty((n_unique2[0], n_unique2[1], n_unique2[2]), dtype=float) + mat_eq = xp.empty((n_unique2[0], n_unique2[1], n_unique2[2]), dtype=float) ker_eva.kernel_eva( self.x_int[0], @@ -2698,7 +2698,7 @@ def projection_T_2form(self, domain): ) # ================= assembly of 3 - component (PI_1_3 : int, int, his) ============ - mat_eq = np.empty((n_unique3[0], n_unique3[1], n_unique3[2]), dtype=float) + mat_eq = xp.empty((n_unique3[0], n_unique3[1], n_unique3[2]), dtype=float) ker_eva.kernel_eva( self.x_int[0], @@ -2793,7 +2793,7 @@ def projection_T_2form(self, domain): ) # ============== conversion to sparse matrices (1 - component) ============== - indices = np.indices( + indices = xp.indices( ( self.NbaseD[0], self.NbaseN[1], @@ -2817,7 +2817,7 @@ def projection_T_2form(self, domain): ) T12.eliminate_zeros() - indices = np.indices( + indices = xp.indices( ( self.NbaseD[0], self.NbaseD[1], @@ -2842,7 +2842,7 @@ def projection_T_2form(self, domain): T13.eliminate_zeros() # ============== conversion to sparse matrices (2 - component) ============== - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseD[1], @@ -2866,7 +2866,7 @@ def projection_T_2form(self, domain): ) T21.eliminate_zeros() - indices = np.indices( + indices = xp.indices( ( self.NbaseD[0], self.NbaseD[1], @@ -2891,7 +2891,7 @@ def projection_T_2form(self, domain): T23.eliminate_zeros() # ============== conversion to sparse matrices (3 - component) ============== - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseD[1], @@ -2915,7 +2915,7 @@ def projection_T_2form(self, domain): ) T31.eliminate_zeros() - indices = np.indices( + indices = xp.indices( ( self.NbaseD[0], self.NbaseN[1], @@ -2966,7 +2966,7 @@ def projection_S_0form(self, domain): """ # non-vanishing coefficients - S11 = np.empty( + S11 = xp.empty( ( self.NbaseN[0], self.NbaseN[1], @@ -2977,7 +2977,7 @@ def projection_S_0form(self, domain): ), dtype=float, ) - S22 = np.empty( + S22 = xp.empty( ( self.NbaseN[0], self.NbaseN[1], @@ -2988,7 +2988,7 @@ def projection_S_0form(self, domain): ), dtype=float, ) - S33 = np.empty( + S33 = xp.empty( ( self.NbaseN[0], self.NbaseN[1], @@ -3006,7 +3006,7 @@ def projection_S_0form(self, domain): n_unique3 = [self.pts[0].flatten().size, self.pts[1].flatten().size, self.x_int[2].size] # ========= assembly of 1 - component (pi2_1 : int, his, his) ============ - mat_eq = np.empty((n_unique1[0], n_unique1[1], n_unique1[2]), dtype=float) + mat_eq = xp.empty((n_unique1[0], n_unique1[1], n_unique1[2]), dtype=float) ker_eva.kernel_eva( self.x_int[0], @@ -3062,7 +3062,7 @@ def projection_S_0form(self, domain): ) # ========= assembly of 2 - component (pi2_2 : his, int, his) ============ - mat_eq = np.empty((n_unique2[0], n_unique2[1], n_unique2[2]), dtype=float) + mat_eq = xp.empty((n_unique2[0], n_unique2[1], n_unique2[2]), dtype=float) ker_eva.kernel_eva( self.pts[0].flatten(), @@ -3118,7 +3118,7 @@ def projection_S_0form(self, domain): ) # ========= assembly of 3 - component (pi2_3 : his, his, int) ============ - mat_eq = np.empty((n_unique3[0], n_unique3[1], n_unique3[2]), dtype=float) + mat_eq = xp.empty((n_unique3[0], n_unique3[1], n_unique3[2]), dtype=float) ker_eva.kernel_eva( self.pts[0].flatten(), @@ -3174,7 +3174,7 @@ def projection_S_0form(self, domain): ) # ========= conversion to sparse matrices (1 - component) ================= - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseN[1], @@ -3199,7 +3199,7 @@ def projection_S_0form(self, domain): S11.eliminate_zeros() # ========= conversion to sparse matrices (2 - component) ================= - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseN[1], @@ -3224,7 +3224,7 @@ def projection_S_0form(self, domain): S22.eliminate_zeros() # ========= conversion to sparse matrices (3 - component) ================= - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseN[1], @@ -3275,7 +3275,7 @@ def projection_S_2form(self, domain): """ # non-vanishing coefficients - S11 = np.empty( + S11 = xp.empty( ( self.NbaseN[0], self.NbaseD[1], @@ -3286,7 +3286,7 @@ def projection_S_2form(self, domain): ), dtype=float, ) - S22 = np.empty( + S22 = xp.empty( ( self.NbaseD[0], self.NbaseN[1], @@ -3297,7 +3297,7 @@ def projection_S_2form(self, domain): ), dtype=float, ) - S33 = np.empty( + S33 = xp.empty( ( self.NbaseD[0], self.NbaseD[1], @@ -3315,7 +3315,7 @@ def projection_S_2form(self, domain): n_unique3 = [self.pts[0].flatten().size, self.pts[1].flatten().size, self.x_int[2].size] # ========= assembly of 1 - component (pi2_1 : int, his, his) ============ - mat_eq = np.empty((n_unique1[0], n_unique1[1], n_unique1[2]), dtype=float) + mat_eq = xp.empty((n_unique1[0], n_unique1[1], n_unique1[2]), dtype=float) ker_eva.kernel_eva( self.x_int[0], @@ -3371,7 +3371,7 @@ def projection_S_2form(self, domain): ) # ========= assembly of 2 - component (pi2_2 : his, int, his) ============ - mat_eq = np.empty((n_unique2[0], n_unique2[1], n_unique2[2]), dtype=float) + mat_eq = xp.empty((n_unique2[0], n_unique2[1], n_unique2[2]), dtype=float) ker_eva.kernel_eva( self.pts[0].flatten(), @@ -3427,7 +3427,7 @@ def projection_S_2form(self, domain): ) # ========= assembly of 3 - component (pi2_3 : his, his, int) ============ - mat_eq = np.empty((n_unique3[0], n_unique3[1], n_unique3[2]), dtype=float) + mat_eq = xp.empty((n_unique3[0], n_unique3[1], n_unique3[2]), dtype=float) ker_eva.kernel_eva( self.pts[0].flatten(), @@ -3483,7 +3483,7 @@ def projection_S_2form(self, domain): ) # ========= conversion to sparse matrices (1 - component) ================= - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseD[1], @@ -3508,7 +3508,7 @@ def projection_S_2form(self, domain): S11.eliminate_zeros() # ========= conversion to sparse matrices (2 - component) ================= - indices = np.indices( + indices = xp.indices( ( self.NbaseD[0], self.NbaseN[1], @@ -3533,7 +3533,7 @@ def projection_S_2form(self, domain): S22.eliminate_zeros() # ========= conversion to sparse matrices (3 - component) ================= - indices = np.indices( + indices = xp.indices( ( self.NbaseD[0], self.NbaseD[1], @@ -3582,7 +3582,7 @@ def projection_K_3form(self, domain): """ # non-vanishing coefficients - K = np.zeros( + K = xp.zeros( ( self.NbaseD[0], self.NbaseD[1], @@ -3597,7 +3597,7 @@ def projection_K_3form(self, domain): # evaluation of equilibrium pressure at interpolation points n_unique = [self.pts[0].flatten().size, self.pts[1].flatten().size, self.pts[2].flatten().size] - mat_eq = np.zeros((n_unique[0], n_unique[1], n_unique[2]), dtype=float) + mat_eq = xp.zeros((n_unique[0], n_unique[1], n_unique[2]), dtype=float) ker_eva.kernel_eva( self.pts[0].flatten(), @@ -3656,7 +3656,7 @@ def projection_K_3form(self, domain): ) # conversion to sparse matrix - indices = np.indices( + indices = xp.indices( ( self.NbaseD[0], self.NbaseD[1], @@ -3711,7 +3711,7 @@ def projection_N_0form(self, domain): """ # non-vanishing coefficients - N11 = np.empty( + N11 = xp.empty( ( self.NbaseN[0], self.NbaseN[1], @@ -3722,7 +3722,7 @@ def projection_N_0form(self, domain): ), dtype=float, ) - N22 = np.empty( + N22 = xp.empty( ( self.NbaseN[0], self.NbaseN[1], @@ -3733,7 +3733,7 @@ def projection_N_0form(self, domain): ), dtype=float, ) - N33 = np.empty( + N33 = xp.empty( ( self.NbaseN[0], self.NbaseN[1], @@ -3751,7 +3751,7 @@ def projection_N_0form(self, domain): n_unique3 = [self.pts[0].flatten().size, self.pts[1].flatten().size, self.x_int[2].size] # ========= assembly of 1 - component (pi2_1 : int, his, his) ============ - mat_eq = np.empty((n_unique1[0], n_unique1[1], n_unique1[2]), dtype=float) + mat_eq = xp.empty((n_unique1[0], n_unique1[1], n_unique1[2]), dtype=float) ker_eva.kernel_eva( self.x_int[0], @@ -3807,7 +3807,7 @@ def projection_N_0form(self, domain): ) # ========= assembly of 2 - component (pi2_2 : his, int, his) ============ - mat_eq = np.empty((n_unique2[0], n_unique2[1], n_unique2[2]), dtype=float) + mat_eq = xp.empty((n_unique2[0], n_unique2[1], n_unique2[2]), dtype=float) ker_eva.kernel_eva( self.pts[0].flatten(), @@ -3863,7 +3863,7 @@ def projection_N_0form(self, domain): ) # ========= assembly of 3 - component (pi2_3 : his, his, int) ============ - mat_eq = np.empty((n_unique3[0], n_unique3[1], n_unique3[2]), dtype=float) + mat_eq = xp.empty((n_unique3[0], n_unique3[1], n_unique3[2]), dtype=float) ker_eva.kernel_eva( self.pts[0].flatten(), @@ -3919,7 +3919,7 @@ def projection_N_0form(self, domain): ) # ========= conversion to sparse matrices (1 - component) ================= - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseN[1], @@ -3944,7 +3944,7 @@ def projection_N_0form(self, domain): N11.eliminate_zeros() # ========= conversion to sparse matrices (2 - component) ================= - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseN[1], @@ -3969,7 +3969,7 @@ def projection_N_0form(self, domain): N22.eliminate_zeros() # ========= conversion to sparse matrices (3 - component) ================= - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseN[1], @@ -4020,7 +4020,7 @@ def projection_N_2form(self, domain): """ # non-vanishing coefficients - N11 = np.empty( + N11 = xp.empty( ( self.NbaseN[0], self.NbaseD[1], @@ -4031,7 +4031,7 @@ def projection_N_2form(self, domain): ), dtype=float, ) - N22 = np.empty( + N22 = xp.empty( ( self.NbaseD[0], self.NbaseN[1], @@ -4042,7 +4042,7 @@ def projection_N_2form(self, domain): ), dtype=float, ) - N33 = np.empty( + N33 = xp.empty( ( self.NbaseD[0], self.NbaseD[1], @@ -4060,7 +4060,7 @@ def projection_N_2form(self, domain): n_unique3 = [self.pts[0].flatten().size, self.pts[1].flatten().size, self.x_int[2].size] # ========= assembly of 1 - component (pi2_1 : int, his, his) ============ - mat_eq = np.empty((n_unique1[0], n_unique1[1], n_unique1[2]), dtype=float) + mat_eq = xp.empty((n_unique1[0], n_unique1[1], n_unique1[2]), dtype=float) ker_eva.kernel_eva( self.x_int[0], @@ -4108,7 +4108,7 @@ def projection_N_2form(self, domain): ) # ========= assembly of 2 - component (pi2_2 : his, int, his) ============ - mat_eq = np.empty((n_unique2[0], n_unique2[1], n_unique2[2]), dtype=float) + mat_eq = xp.empty((n_unique2[0], n_unique2[1], n_unique2[2]), dtype=float) ker_eva.kernel_eva( self.pts[0].flatten(), @@ -4156,7 +4156,7 @@ def projection_N_2form(self, domain): ) # ========= assembly of 3 - component (pi2_3 : his, his, int) ============ - mat_eq = np.empty((n_unique3[0], n_unique3[1], n_unique3[2]), dtype=float) + mat_eq = xp.empty((n_unique3[0], n_unique3[1], n_unique3[2]), dtype=float) ker_eva.kernel_eva( self.pts[0].flatten(), @@ -4204,7 +4204,7 @@ def projection_N_2form(self, domain): ) # ========= conversion to sparse matrices (1 - component) ================= - indices = np.indices( + indices = xp.indices( ( self.NbaseN[0], self.NbaseD[1], @@ -4229,7 +4229,7 @@ def projection_N_2form(self, domain): N11.eliminate_zeros() # ========= conversion to sparse matrices (2 - component) ================= - indices = np.indices( + indices = xp.indices( ( self.NbaseD[0], self.NbaseN[1], @@ -4254,7 +4254,7 @@ def projection_N_2form(self, domain): N22.eliminate_zeros() # ========= conversion to sparse matrices (3 - component) ================= - indices = np.indices( + indices = xp.indices( ( self.NbaseD[0], self.NbaseD[1], @@ -4356,13 +4356,13 @@ def __init__( self.cz = cz # ============= evaluation of background magnetic field at quadrature points ========= - self.mat_curl_beq_1 = np.empty( + self.mat_curl_beq_1 = xp.empty( (self.Nel[0], self.Nel[1], self.Nel[2], self.n_quad[0], self.n_quad[1], self.n_quad[2]), dtype=float ) - self.mat_curl_beq_2 = np.empty( + self.mat_curl_beq_2 = xp.empty( (self.Nel[0], self.Nel[1], self.Nel[2], self.n_quad[0], self.n_quad[1], self.n_quad[2]), dtype=float ) - self.mat_curl_beq_3 = np.empty( + self.mat_curl_beq_3 = xp.empty( (self.Nel[0], self.Nel[1], self.Nel[2], self.n_quad[0], self.n_quad[1], self.n_quad[2]), dtype=float ) @@ -4454,20 +4454,20 @@ def __init__( ) # ====================== perturbed magnetic field at quadrature points ========== - self.B1 = np.empty( + self.B1 = xp.empty( (self.Nel[0], self.Nel[1], self.Nel[2], self.n_quad[0], self.n_quad[1], self.n_quad[2]), dtype=float ) - self.B2 = np.empty( + self.B2 = xp.empty( (self.Nel[0], self.Nel[1], self.Nel[2], self.n_quad[0], self.n_quad[1], self.n_quad[2]), dtype=float ) - self.B3 = np.empty( + self.B3 = xp.empty( (self.Nel[0], self.Nel[1], self.Nel[2], self.n_quad[0], self.n_quad[1], self.n_quad[2]), dtype=float ) # ========================== inner products ===================================== - self.F1 = np.empty((self.NbaseN[0], self.NbaseN[1], self.NbaseN[2]), dtype=float) - self.F2 = np.empty((self.NbaseN[0], self.NbaseN[1], self.NbaseN[2]), dtype=float) - self.F3 = np.empty((self.NbaseN[0], self.NbaseN[1], self.NbaseN[2]), dtype=float) + self.F1 = xp.empty((self.NbaseN[0], self.NbaseN[1], self.NbaseN[2]), dtype=float) + self.F2 = xp.empty((self.NbaseN[0], self.NbaseN[1], self.NbaseN[2]), dtype=float) + self.F3 = xp.empty((self.NbaseN[0], self.NbaseN[1], self.NbaseN[2]), dtype=float) # ============================================================ def inner_curl_beq(self, b1, b2, b3): @@ -4598,7 +4598,7 @@ def inner_curl_beq(self, b1, b2, b3): # ker_loc_3d.kernel_inner_2(self.Nel[0], self.Nel[1], self.Nel[2], self.p[0], self.p[1], self.p[2], self.n_quad[0], self.n_quad[1], self.n_quad[2], 0, 0, 0, self.wts[0], self.wts[1], self.wts[2], self.basisN[0], self.basisN[1], self.basisN[2], self.NbaseN[0], self.NbaseN[1], self.NbaseN[2], self.F3, self.mat_curl_beq_3) # convert to 1d array and return - return np.concatenate((self.F1.flatten(), self.F2.flatten(), self.F3.flatten())) + return xp.concatenate((self.F1.flatten(), self.F2.flatten(), self.F3.flatten())) # ================ mass matrix in V1 =========================== @@ -4641,9 +4641,9 @@ def mass_curl(tensor_space, kind_map, params_map): Nbj3 = [NbaseD[2], NbaseN[2], NbaseD[2], NbaseN[2], NbaseD[2], NbaseD[2]] # ============= evaluation of background magnetic field at quadrature points ========= - mat_curl_beq_1 = np.empty((Nel[0], Nel[1], Nel[2], n_quad[0], n_quad[1], n_quad[2]), dtype=float) - mat_curl_beq_2 = np.empty((Nel[0], Nel[1], Nel[2], n_quad[0], n_quad[1], n_quad[2]), dtype=float) - mat_curl_beq_3 = np.empty((Nel[0], Nel[1], Nel[2], n_quad[0], n_quad[1], n_quad[2]), dtype=float) + mat_curl_beq_1 = xp.empty((Nel[0], Nel[1], Nel[2], n_quad[0], n_quad[1], n_quad[2]), dtype=float) + mat_curl_beq_2 = xp.empty((Nel[0], Nel[1], Nel[2], n_quad[0], n_quad[1], n_quad[2]), dtype=float) + mat_curl_beq_3 = xp.empty((Nel[0], Nel[1], Nel[2], n_quad[0], n_quad[1], n_quad[2]), dtype=float) ker_eva.kernel_eva_quad(Nel, n_quad, pts[0], pts[1], pts[2], mat_curl_beq_1, 61, kind_map, params_map) ker_eva.kernel_eva_quad(Nel, n_quad, pts[0], pts[1], pts[2], mat_curl_beq_2, 62, kind_map, params_map) @@ -4652,7 +4652,7 @@ def mass_curl(tensor_space, kind_map, params_map): # blocks of global mass matrix M = [ - np.zeros((Nbi1, Nbi2, Nbi3, 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1), dtype=float) + xp.zeros((Nbi1, Nbi2, Nbi3, 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1), dtype=float) for Nbi1, Nbi2, Nbi3 in zip(Nbi1, Nbi2, Nbi3) ] @@ -4858,11 +4858,11 @@ def mass_curl(tensor_space, kind_map, params_map): counter = 0 for i in range(6): - indices = np.indices((Nbi1[counter], Nbi2[counter], Nbi3[counter], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1)) + indices = xp.indices((Nbi1[counter], Nbi2[counter], Nbi3[counter], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1)) - shift1 = np.arange(Nbi1[counter]) - p[0] - shift2 = np.arange(Nbi2[counter]) - p[1] - shift3 = np.arange(Nbi3[counter]) - p[2] + shift1 = xp.arange(Nbi1[counter]) - p[0] + shift2 = xp.arange(Nbi2[counter]) - p[1] + shift3 = xp.arange(Nbi3[counter]) - p[2] row = (Nbi2[counter] * Nbi3[counter] * indices[0] + Nbi3[counter] * indices[1] + indices[2]).flatten() diff --git a/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/projectors_local.py b/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/projectors_local.py index 72c5babfd..dacc4f243 100644 --- a/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/projectors_local.py +++ b/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/projectors_local.py @@ -6,11 +6,11 @@ Classes for local projectors in 1D and 3D based on quasi-spline interpolation and histopolation. """ +import cunumpy as xp import scipy.sparse as spa import struphy.feec.bsplines as bsp import struphy.feec.projectors.pro_local.kernels_projectors_local as ker_loc -from struphy.utils.arrays import xp as np # ======================= 1d ==================================== @@ -41,85 +41,85 @@ def __init__(self, spline_space, n_quad): self.n_quad = n_quad # number of quadrature point per integration interval # Gauss - Legendre quadrature points and weights in (-1, 1) - self.pts_loc = np.polynomial.legendre.leggauss(self.n_quad)[0] - self.wts_loc = np.polynomial.legendre.leggauss(self.n_quad)[1] + self.pts_loc = xp.polynomial.legendre.leggauss(self.n_quad)[0] + self.wts_loc = xp.polynomial.legendre.leggauss(self.n_quad)[1] # set interpolation and histopolation coefficients if self.bc == True: - self.coeff_i = np.zeros((1, 2 * self.p - 1), dtype=float) - self.coeff_h = np.zeros((1, 2 * self.p), dtype=float) + self.coeff_i = xp.zeros((1, 2 * self.p - 1), dtype=float) + self.coeff_h = xp.zeros((1, 2 * self.p), dtype=float) if self.p == 1: - self.coeff_i[0, :] = np.array([1.0]) - self.coeff_h[0, :] = np.array([1.0, 1.0]) + self.coeff_i[0, :] = xp.array([1.0]) + self.coeff_h[0, :] = xp.array([1.0, 1.0]) elif self.p == 2: - self.coeff_i[0, :] = 1 / 2 * np.array([-1.0, 4.0, -1.0]) - self.coeff_h[0, :] = 1 / 2 * np.array([-1.0, 3.0, 3.0, -1.0]) + self.coeff_i[0, :] = 1 / 2 * xp.array([-1.0, 4.0, -1.0]) + self.coeff_h[0, :] = 1 / 2 * xp.array([-1.0, 3.0, 3.0, -1.0]) elif self.p == 3: - self.coeff_i[0, :] = 1 / 6 * np.array([1.0, -8.0, 20.0, -8.0, 1.0]) - self.coeff_h[0, :] = 1 / 6 * np.array([1.0, -7.0, 12.0, 12.0, -7.0, 1.0]) + self.coeff_i[0, :] = 1 / 6 * xp.array([1.0, -8.0, 20.0, -8.0, 1.0]) + self.coeff_h[0, :] = 1 / 6 * xp.array([1.0, -7.0, 12.0, 12.0, -7.0, 1.0]) elif self.p == 4: - self.coeff_i[0, :] = 2 / 45 * np.array([-1.0, 16.0, -295 / 4, 140.0, -295 / 4, 16.0, -1.0]) - self.coeff_h[0, :] = 2 / 45 * np.array([-1.0, 15.0, -231 / 4, 265 / 4, 265 / 4, -231 / 4, 15.0, -1.0]) + self.coeff_i[0, :] = 2 / 45 * xp.array([-1.0, 16.0, -295 / 4, 140.0, -295 / 4, 16.0, -1.0]) + self.coeff_h[0, :] = 2 / 45 * xp.array([-1.0, 15.0, -231 / 4, 265 / 4, 265 / 4, -231 / 4, 15.0, -1.0]) else: print("degree > 4 not implemented!") else: - self.coeff_i = np.zeros((2 * self.p - 1, 2 * self.p - 1), dtype=float) - self.coeff_h = np.zeros((2 * self.p - 1, 2 * self.p), dtype=float) + self.coeff_i = xp.zeros((2 * self.p - 1, 2 * self.p - 1), dtype=float) + self.coeff_h = xp.zeros((2 * self.p - 1, 2 * self.p), dtype=float) if self.p == 1: - self.coeff_i[0, :] = np.array([1.0]) - self.coeff_h[0, :] = np.array([1.0, 1.0]) + self.coeff_i[0, :] = xp.array([1.0]) + self.coeff_h[0, :] = xp.array([1.0, 1.0]) elif self.p == 2: - self.coeff_i[0, :] = 1 / 2 * np.array([2.0, 0.0, 0.0]) - self.coeff_i[1, :] = 1 / 2 * np.array([-1.0, 4.0, -1.0]) - self.coeff_i[2, :] = 1 / 2 * np.array([0.0, 0.0, 2.0]) + self.coeff_i[0, :] = 1 / 2 * xp.array([2.0, 0.0, 0.0]) + self.coeff_i[1, :] = 1 / 2 * xp.array([-1.0, 4.0, -1.0]) + self.coeff_i[2, :] = 1 / 2 * xp.array([0.0, 0.0, 2.0]) - self.coeff_h[0, :] = 1 / 2 * np.array([3.0, -1.0, 0.0, 0.0]) - self.coeff_h[1, :] = 1 / 2 * np.array([-1.0, 3.0, 3.0, -1.0]) - self.coeff_h[2, :] = 1 / 2 * np.array([0.0, 0.0, -1.0, 3.0]) + self.coeff_h[0, :] = 1 / 2 * xp.array([3.0, -1.0, 0.0, 0.0]) + self.coeff_h[1, :] = 1 / 2 * xp.array([-1.0, 3.0, 3.0, -1.0]) + self.coeff_h[2, :] = 1 / 2 * xp.array([0.0, 0.0, -1.0, 3.0]) elif self.p == 3: - self.coeff_i[0, :] = 1 / 18 * np.array([18.0, 0.0, 0.0, 0.0, 0.0]) - self.coeff_i[1, :] = 1 / 18 * np.array([-5.0, 40.0, -24.0, 8.0, -1.0]) - self.coeff_i[2, :] = 1 / 18 * np.array([3.0, -24.0, 60.0, -24.0, 3.0]) - self.coeff_i[3, :] = 1 / 18 * np.array([-1.0, 8.0, -24.0, 40.0, -5.0]) - self.coeff_i[4, :] = 1 / 18 * np.array([0.0, 0.0, 0.0, 0.0, 18.0]) - - self.coeff_h[0, :] = 1 / 18 * np.array([23.0, -17.0, 7.0, -1.0, 0.0, 0.0]) - self.coeff_h[1, :] = 1 / 18 * np.array([-8.0, 56.0, -28.0, 4.0, 0.0, 0.0]) - self.coeff_h[2, :] = 1 / 18 * np.array([3.0, -21.0, 36.0, 36.0, -21.0, 3.0]) - self.coeff_h[3, :] = 1 / 18 * np.array([0.0, 0.0, 4.0, -28.0, 56.0, -8.0]) - self.coeff_h[4, :] = 1 / 18 * np.array([0.0, 0.0, -1.0, 7.0, -17.0, 23.0]) + self.coeff_i[0, :] = 1 / 18 * xp.array([18.0, 0.0, 0.0, 0.0, 0.0]) + self.coeff_i[1, :] = 1 / 18 * xp.array([-5.0, 40.0, -24.0, 8.0, -1.0]) + self.coeff_i[2, :] = 1 / 18 * xp.array([3.0, -24.0, 60.0, -24.0, 3.0]) + self.coeff_i[3, :] = 1 / 18 * xp.array([-1.0, 8.0, -24.0, 40.0, -5.0]) + self.coeff_i[4, :] = 1 / 18 * xp.array([0.0, 0.0, 0.0, 0.0, 18.0]) + + self.coeff_h[0, :] = 1 / 18 * xp.array([23.0, -17.0, 7.0, -1.0, 0.0, 0.0]) + self.coeff_h[1, :] = 1 / 18 * xp.array([-8.0, 56.0, -28.0, 4.0, 0.0, 0.0]) + self.coeff_h[2, :] = 1 / 18 * xp.array([3.0, -21.0, 36.0, 36.0, -21.0, 3.0]) + self.coeff_h[3, :] = 1 / 18 * xp.array([0.0, 0.0, 4.0, -28.0, 56.0, -8.0]) + self.coeff_h[4, :] = 1 / 18 * xp.array([0.0, 0.0, -1.0, 7.0, -17.0, 23.0]) elif self.p == 4: - self.coeff_i[0, :] = 1 / 360 * np.array([360.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) - self.coeff_i[1, :] = 1 / 360 * np.array([-59.0, 944.0, -1000.0, 720.0, -305.0, 64.0, -4.0]) - self.coeff_i[2, :] = 1 / 360 * np.array([23.0, -368.0, 1580.0, -1360.0, 605.0, -128.0, 8.0]) - self.coeff_i[3, :] = 1 / 360 * np.array([-16.0, 256.0, -1180.0, 2240.0, -1180.0, 256.0, -16.0]) - self.coeff_i[4, :] = 1 / 360 * np.array([8.0, -128.0, 605.0, -1360.0, 1580.0, -368.0, 23.0]) - self.coeff_i[5, :] = 1 / 360 * np.array([-4.0, 64.0, -305.0, 720.0, -1000.0, 944.0, -59.0]) - self.coeff_i[6, :] = 1 / 360 * np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 360.0]) - - self.coeff_h[0, :] = 1 / 360 * np.array([419.0, -525.0, 475.0, -245.0, 60.0, -4.0, 0.0, 0.0]) - self.coeff_h[1, :] = 1 / 360 * np.array([-82.0, 1230.0, -1350.0, 730.0, -180.0, 12.0, 0.0, 0.0]) - self.coeff_h[2, :] = 1 / 360 * np.array([39.0, -585.0, 2175.0, -1425.0, 360.0, -24.0, 0.0, 0.0]) - self.coeff_h[3, :] = 1 / 360 * np.array([-16.0, 240.0, -924.0, 1060.0, 1060.0, -924.0, 240.0, -16.0]) - self.coeff_h[4, :] = 1 / 360 * np.array([0.0, 0.0, -24.0, 360.0, -1425.0, 2175.0, -585.0, 39.0]) - self.coeff_h[5, :] = 1 / 360 * np.array([0.0, 0.0, 12.0, -180.0, 730.0, -1350.0, 1230.0, -82.0]) - self.coeff_h[6, :] = 1 / 360 * np.array([0.0, 0.0, -4.0, 60.0, -245.0, 475.0, -525.0, 419.0]) + self.coeff_i[0, :] = 1 / 360 * xp.array([360.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + self.coeff_i[1, :] = 1 / 360 * xp.array([-59.0, 944.0, -1000.0, 720.0, -305.0, 64.0, -4.0]) + self.coeff_i[2, :] = 1 / 360 * xp.array([23.0, -368.0, 1580.0, -1360.0, 605.0, -128.0, 8.0]) + self.coeff_i[3, :] = 1 / 360 * xp.array([-16.0, 256.0, -1180.0, 2240.0, -1180.0, 256.0, -16.0]) + self.coeff_i[4, :] = 1 / 360 * xp.array([8.0, -128.0, 605.0, -1360.0, 1580.0, -368.0, 23.0]) + self.coeff_i[5, :] = 1 / 360 * xp.array([-4.0, 64.0, -305.0, 720.0, -1000.0, 944.0, -59.0]) + self.coeff_i[6, :] = 1 / 360 * xp.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 360.0]) + + self.coeff_h[0, :] = 1 / 360 * xp.array([419.0, -525.0, 475.0, -245.0, 60.0, -4.0, 0.0, 0.0]) + self.coeff_h[1, :] = 1 / 360 * xp.array([-82.0, 1230.0, -1350.0, 730.0, -180.0, 12.0, 0.0, 0.0]) + self.coeff_h[2, :] = 1 / 360 * xp.array([39.0, -585.0, 2175.0, -1425.0, 360.0, -24.0, 0.0, 0.0]) + self.coeff_h[3, :] = 1 / 360 * xp.array([-16.0, 240.0, -924.0, 1060.0, 1060.0, -924.0, 240.0, -16.0]) + self.coeff_h[4, :] = 1 / 360 * xp.array([0.0, 0.0, -24.0, 360.0, -1425.0, 2175.0, -585.0, 39.0]) + self.coeff_h[5, :] = 1 / 360 * xp.array([0.0, 0.0, 12.0, -180.0, 730.0, -1350.0, 1230.0, -82.0]) + self.coeff_h[6, :] = 1 / 360 * xp.array([0.0, 0.0, -4.0, 60.0, -245.0, 475.0, -525.0, 419.0]) else: print("degree > 4 not implemented!") # set interpolation points - n_lambda_int = np.copy(self.NbaseN) # number of coefficients in space V0 + n_lambda_int = xp.copy(self.NbaseN) # number of coefficients in space V0 self.n_int = 2 * self.p - 1 # number of local interpolation points (1, 3, 5, 7, ...) if self.p == 1: @@ -134,21 +134,21 @@ def __init__(self, spline_space, n_quad): 2 * self.p - 2 ) # number of non-vanishing D bf in interpolation interval (1, 2, 4, 6, ...) - self.x_int = np.zeros((n_lambda_int, self.n_int), dtype=float) # interpolation points for each coeff. + self.x_int = xp.zeros((n_lambda_int, self.n_int), dtype=float) # interpolation points for each coeff. - self.int_global_N = np.zeros( + self.int_global_N = xp.zeros( (n_lambda_int, self.n_int_locbf_N), dtype=int ) # global indices of non-vanishing N bf - self.int_global_D = np.zeros( + self.int_global_D = xp.zeros( (n_lambda_int, self.n_int_locbf_D), dtype=int ) # global indices of non-vanishing D bf - self.int_loccof_N = np.zeros((n_lambda_int, self.n_int_locbf_N), dtype=int) # index of non-vanishing coeff. (N) - self.int_loccof_D = np.zeros((n_lambda_int, self.n_int_locbf_D), dtype=int) # index of non-vanishing coeff. (D) + self.int_loccof_N = xp.zeros((n_lambda_int, self.n_int_locbf_N), dtype=int) # index of non-vanishing coeff. (N) + self.int_loccof_D = xp.zeros((n_lambda_int, self.n_int_locbf_D), dtype=int) # index of non-vanishing coeff. (D) - self.x_int_indices = np.zeros((n_lambda_int, self.n_int), dtype=int) + self.x_int_indices = xp.zeros((n_lambda_int, self.n_int), dtype=int) - self.coeffi_indices = np.zeros(n_lambda_int, dtype=int) + self.coeffi_indices = xp.zeros(n_lambda_int, dtype=int) if self.bc == False: # maximum number of non-vanishing coefficients @@ -160,39 +160,39 @@ def __init__(self, spline_space, n_quad): self.n_int_nvcof_N = 3 * self.p - 2 # shift in local coefficient indices at right boundary (only for non-periodic boundary conditions) - self.int_add_D = np.arange(self.n_int - 2) + 1 - self.int_add_N = np.arange(self.n_int - 1) + 1 + self.int_add_D = xp.arange(self.n_int - 2) + 1 + self.int_add_N = xp.arange(self.n_int - 1) + 1 counter_D = 0 counter_N = 0 # shift local coefficients --> global coefficients (D) if self.p == 1: - self.int_shift_D = np.arange(self.NbaseD) + self.int_shift_D = xp.arange(self.NbaseD) else: - self.int_shift_D = np.arange(self.NbaseD) - (self.p - 2) + self.int_shift_D = xp.arange(self.NbaseD) - (self.p - 2) self.int_shift_D[: 2 * self.p - 2] = 0 self.int_shift_D[-(2 * self.p - 2) :] = self.int_shift_D[-(2 * self.p - 2)] # shift local coefficients --> global coefficients (N) if self.p == 1: - self.int_shift_N = np.arange(self.NbaseN) + self.int_shift_N = xp.arange(self.NbaseN) self.int_shift_N[-1] = self.int_shift_N[-2] else: - self.int_shift_N = np.arange(self.NbaseN) - (self.p - 1) + self.int_shift_N = xp.arange(self.NbaseN) - (self.p - 1) self.int_shift_N[: 2 * self.p - 1] = 0 self.int_shift_N[-(2 * self.p - 1) :] = self.int_shift_N[-(2 * self.p - 1)] - counter_coeffi = np.copy(self.p) + counter_coeffi = xp.copy(self.p) for i in range(n_lambda_int): # left boundary region if i < self.p - 1: - self.int_global_N[i] = np.arange(self.n_int_locbf_N) - self.int_global_D[i] = np.arange(self.n_int_locbf_D) + self.int_global_N[i] = xp.arange(self.n_int_locbf_N) + self.int_global_D[i] = xp.arange(self.n_int_locbf_D) - self.x_int_indices[i] = np.arange(self.n_int) + self.x_int_indices[i] = xp.arange(self.n_int) self.coeffi_indices[i] = i for j in range(2 * (self.p - 1) + 1): xi = self.p - 1 @@ -200,10 +200,10 @@ def __init__(self, spline_space, n_quad): # right boundary region elif i > n_lambda_int - self.p: - self.int_global_N[i] = np.arange(self.n_int_locbf_N) + n_lambda_int - self.p - (self.p - 1) - self.int_global_D[i] = np.arange(self.n_int_locbf_D) + n_lambda_int - self.p - (self.p - 1) + self.int_global_N[i] = xp.arange(self.n_int_locbf_N) + n_lambda_int - self.p - (self.p - 1) + self.int_global_D[i] = xp.arange(self.n_int_locbf_D) + n_lambda_int - self.p - (self.p - 1) - self.x_int_indices[i] = np.arange(self.n_int) + 2 * (n_lambda_int - self.p - (self.p - 1)) + self.x_int_indices[i] = xp.arange(self.n_int) + 2 * (n_lambda_int - self.p - (self.p - 1)) self.coeffi_indices[i] = counter_coeffi counter_coeffi += 1 for j in range(2 * (self.p - 1) + 1): @@ -213,20 +213,20 @@ def __init__(self, spline_space, n_quad): # interior else: if self.p == 1: - self.int_global_N[i] = np.arange(self.n_int_locbf_N) + i - self.int_global_D[i] = np.arange(self.n_int_locbf_D) + i + self.int_global_N[i] = xp.arange(self.n_int_locbf_N) + i + self.int_global_D[i] = xp.arange(self.n_int_locbf_D) + i self.int_global_N[-1] = self.int_global_N[-2] self.int_global_D[-1] = self.int_global_D[-2] else: - self.int_global_N[i] = np.arange(self.n_int_locbf_N) + i - (self.p - 1) - self.int_global_D[i] = np.arange(self.n_int_locbf_D) + i - (self.p - 1) + self.int_global_N[i] = xp.arange(self.n_int_locbf_N) + i - (self.p - 1) + self.int_global_D[i] = xp.arange(self.n_int_locbf_D) + i - (self.p - 1) if self.p == 1: self.x_int_indices[i] = i else: - self.x_int_indices[i] = np.arange(self.n_int) + 2 * (i - (self.p - 1)) + self.x_int_indices[i] = xp.arange(self.n_int) + 2 * (i - (self.p - 1)) self.coeffi_indices[i] = self.p - 1 for j in range(2 * (self.p - 1) + 1): @@ -234,8 +234,8 @@ def __init__(self, spline_space, n_quad): # local coefficient index if self.p == 1: - self.int_loccof_N[i] = np.array([0, 1]) - self.int_loccof_D[-1] = np.array([1]) + self.int_loccof_N[i] = xp.array([0, 1]) + self.int_loccof_D[-1] = xp.array([1]) else: if i > 0: @@ -243,8 +243,8 @@ def __init__(self, spline_space, n_quad): k_glob_new = self.int_global_D[i, il] bol = k_glob_new == self.int_global_D[i - 1] - if np.any(bol): - self.int_loccof_D[i, il] = self.int_loccof_D[i - 1, np.where(bol)[0][0]] + 1 + if xp.any(bol): + self.int_loccof_D[i, il] = self.int_loccof_D[i - 1, xp.where(bol)[0][0]] + 1 if (k_glob_new >= n_lambda_int - self.p - (self.p - 2)) and (self.int_loccof_D[i, il] == 0): self.int_loccof_D[i, il] = self.int_add_D[counter_D] @@ -254,8 +254,8 @@ def __init__(self, spline_space, n_quad): k_glob_new = self.int_global_N[i, il] bol = k_glob_new == self.int_global_N[i - 1] - if np.any(bol): - self.int_loccof_N[i, il] = self.int_loccof_N[i - 1, np.where(bol)[0][0]] + 1 + if xp.any(bol): + self.int_loccof_N[i, il] = self.int_loccof_N[i - 1, xp.where(bol)[0][0]] + 1 if (k_glob_new >= n_lambda_int - self.p - (self.p - 2)) and (self.int_loccof_N[i, il] == 0): self.int_loccof_N[i, il] = self.int_add_N[counter_N] @@ -273,24 +273,24 @@ def __init__(self, spline_space, n_quad): # shift local coefficients --> global coefficients if self.p == 1: - self.int_shift_D = np.arange(self.NbaseN) - (self.p - 1) - self.int_shift_N = np.arange(self.NbaseN) - (self.p) + self.int_shift_D = xp.arange(self.NbaseN) - (self.p - 1) + self.int_shift_N = xp.arange(self.NbaseN) - (self.p) else: - self.int_shift_D = np.arange(self.NbaseN) - (self.p - 2) - self.int_shift_N = np.arange(self.NbaseN) - (self.p - 1) + self.int_shift_D = xp.arange(self.NbaseN) - (self.p - 2) + self.int_shift_N = xp.arange(self.NbaseN) - (self.p - 1) for i in range(n_lambda_int): # global indices of non-vanishing basis functions and position of coefficients in final matrix - self.int_global_D[i] = (np.arange(self.n_int_locbf_D) + i - (self.p - 1)) % self.NbaseD - self.int_loccof_D[i] = np.arange(self.n_int_locbf_D - 1, -1, -1) + self.int_global_D[i] = (xp.arange(self.n_int_locbf_D) + i - (self.p - 1)) % self.NbaseD + self.int_loccof_D[i] = xp.arange(self.n_int_locbf_D - 1, -1, -1) - self.int_global_N[i] = (np.arange(self.n_int_locbf_N) + i - (self.p - 1)) % self.NbaseN - self.int_loccof_N[i] = np.arange(self.n_int_locbf_N - 1, -1, -1) + self.int_global_N[i] = (xp.arange(self.n_int_locbf_N) + i - (self.p - 1)) % self.NbaseN + self.int_loccof_N[i] = xp.arange(self.n_int_locbf_N - 1, -1, -1) if self.p == 1: self.x_int_indices[i] = i else: - self.x_int_indices[i] = np.arange(self.n_int) + 2 * (i - (self.p - 1)) + self.x_int_indices[i] = xp.arange(self.n_int) + 2 * (i - (self.p - 1)) self.coeffi_indices[i] = 0 @@ -298,23 +298,23 @@ def __init__(self, spline_space, n_quad): self.x_int[i, j] = ((self.T[i + 1 + int(j / 2)] + self.T[i + 1 + int((j + 1) / 2)]) / 2) % 1.0 # set histopolation points, quadrature points and weights - n_lambda_his = np.copy(self.NbaseD) # number of coefficients in space V1 + n_lambda_his = xp.copy(self.NbaseD) # number of coefficients in space V1 self.n_his = 2 * self.p # number of histopolation intervals (2, 4, 6, 8, ...) self.n_his_locbf_N = 2 * self.p # number of non-vanishing N bf in histopolation interval (2, 4, 6, 8, ...) self.n_his_locbf_D = 2 * self.p - 1 # number of non-vanishing D bf in histopolation interval (2, 4, 6, 8, ...) - self.x_his = np.zeros((n_lambda_his, self.n_his + 1), dtype=float) # histopolation boundaries + self.x_his = xp.zeros((n_lambda_his, self.n_his + 1), dtype=float) # histopolation boundaries - self.his_global_N = np.zeros((n_lambda_his, self.n_his_locbf_N), dtype=int) - self.his_global_D = np.zeros((n_lambda_his, self.n_his_locbf_D), dtype=int) + self.his_global_N = xp.zeros((n_lambda_his, self.n_his_locbf_N), dtype=int) + self.his_global_D = xp.zeros((n_lambda_his, self.n_his_locbf_D), dtype=int) - self.his_loccof_N = np.zeros((n_lambda_his, self.n_his_locbf_N), dtype=int) - self.his_loccof_D = np.zeros((n_lambda_his, self.n_his_locbf_D), dtype=int) + self.his_loccof_N = xp.zeros((n_lambda_his, self.n_his_locbf_N), dtype=int) + self.his_loccof_D = xp.zeros((n_lambda_his, self.n_his_locbf_D), dtype=int) - self.x_his_indices = np.zeros((n_lambda_his, self.n_his), dtype=int) + self.x_his_indices = xp.zeros((n_lambda_his, self.n_his), dtype=int) - self.coeffh_indices = np.zeros(n_lambda_his, dtype=int) + self.coeffh_indices = xp.zeros(n_lambda_his, dtype=int) if self.bc == False: # maximum number of non-vanishing coefficients @@ -322,31 +322,31 @@ def __init__(self, spline_space, n_quad): self.n_his_nvcof_N = 3 * self.p - 1 # shift in local coefficient indices at right boundary (only for non-periodic boundary conditions) - self.his_add_D = np.arange(self.n_his - 2) + 1 - self.his_add_N = np.arange(self.n_his - 1) + 1 + self.his_add_D = xp.arange(self.n_his - 2) + 1 + self.his_add_N = xp.arange(self.n_his - 1) + 1 counter_D = 0 counter_N = 0 # shift local coefficients --> global coefficients (D) - self.his_shift_D = np.arange(self.NbaseD) - (self.p - 1) + self.his_shift_D = xp.arange(self.NbaseD) - (self.p - 1) self.his_shift_D[: 2 * self.p - 1] = 0 self.his_shift_D[-(2 * self.p - 1) :] = self.his_shift_D[-(2 * self.p - 1)] # shift local coefficients --> global coefficients (N) - self.his_shift_N = np.arange(self.NbaseN) - self.p + self.his_shift_N = xp.arange(self.NbaseN) - self.p self.his_shift_N[: 2 * self.p] = 0 self.his_shift_N[-2 * self.p :] = self.his_shift_N[-2 * self.p] - counter_coeffh = np.copy(self.p) + counter_coeffh = xp.copy(self.p) for i in range(n_lambda_his): # left boundary region if i < self.p - 1: - self.his_global_N[i] = np.arange(self.n_his_locbf_N) - self.his_global_D[i] = np.arange(self.n_his_locbf_D) + self.his_global_N[i] = xp.arange(self.n_his_locbf_N) + self.his_global_D[i] = xp.arange(self.n_his_locbf_D) - self.x_his_indices[i] = np.arange(self.n_his) + self.x_his_indices[i] = xp.arange(self.n_his) self.coeffh_indices[i] = i for j in range(2 * self.p + 1): xi = self.p - 1 @@ -354,10 +354,10 @@ def __init__(self, spline_space, n_quad): # right boundary region elif i > n_lambda_his - self.p: - self.his_global_N[i] = np.arange(self.n_his_locbf_N) + n_lambda_his - self.p - (self.p - 1) - self.his_global_D[i] = np.arange(self.n_his_locbf_D) + n_lambda_his - self.p - (self.p - 1) + self.his_global_N[i] = xp.arange(self.n_his_locbf_N) + n_lambda_his - self.p - (self.p - 1) + self.his_global_D[i] = xp.arange(self.n_his_locbf_D) + n_lambda_his - self.p - (self.p - 1) - self.x_his_indices[i] = np.arange(self.n_his) + 2 * (n_lambda_his - self.p - (self.p - 1)) + self.x_his_indices[i] = xp.arange(self.n_his) + 2 * (n_lambda_his - self.p - (self.p - 1)) self.coeffh_indices[i] = counter_coeffh counter_coeffh += 1 for j in range(2 * self.p + 1): @@ -366,10 +366,10 @@ def __init__(self, spline_space, n_quad): # interior else: - self.his_global_N[i] = np.arange(self.n_his_locbf_N) + i - (self.p - 1) - self.his_global_D[i] = np.arange(self.n_his_locbf_D) + i - (self.p - 1) + self.his_global_N[i] = xp.arange(self.n_his_locbf_N) + i - (self.p - 1) + self.his_global_D[i] = xp.arange(self.n_his_locbf_D) + i - (self.p - 1) - self.x_his_indices[i] = np.arange(self.n_his) + 2 * (i - (self.p - 1)) + self.x_his_indices[i] = xp.arange(self.n_his) + 2 * (i - (self.p - 1)) self.coeffh_indices[i] = self.p - 1 for j in range(2 * self.p + 1): self.x_his[i, j] = (self.T[i + 1 + int(j / 2)] + self.T[i + 1 + int((j + 1) / 2)]) / 2 @@ -380,8 +380,8 @@ def __init__(self, spline_space, n_quad): k_glob_new = self.his_global_D[i, il] bol = k_glob_new == self.his_global_D[i - 1] - if np.any(bol): - self.his_loccof_D[i, il] = self.his_loccof_D[i - 1, np.where(bol)[0][0]] + 1 + if xp.any(bol): + self.his_loccof_D[i, il] = self.his_loccof_D[i - 1, xp.where(bol)[0][0]] + 1 if (k_glob_new >= n_lambda_his - self.p - (self.p - 2)) and (self.his_loccof_D[i, il] == 0): self.his_loccof_D[i, il] = self.his_add_D[counter_D] @@ -391,15 +391,15 @@ def __init__(self, spline_space, n_quad): k_glob_new = self.his_global_N[i, il] bol = k_glob_new == self.his_global_N[i - 1] - if np.any(bol): - self.his_loccof_N[i, il] = self.his_loccof_N[i - 1, np.where(bol)[0][0]] + 1 + if xp.any(bol): + self.his_loccof_N[i, il] = self.his_loccof_N[i - 1, xp.where(bol)[0][0]] + 1 if (k_glob_new >= n_lambda_his - self.p - (self.p - 2)) and (self.his_loccof_N[i, il] == 0): self.his_loccof_N[i, il] = self.his_add_N[counter_N] counter_N += 1 # quadrature points and weights - self.pts, self.wts = bsp.quadrature_grid(np.unique(self.x_his.flatten()), self.pts_loc, self.wts_loc) + self.pts, self.wts = bsp.quadrature_grid(xp.unique(self.x_his.flatten()), self.pts_loc, self.wts_loc) else: # maximum number of non-vanishing coefficients @@ -407,31 +407,31 @@ def __init__(self, spline_space, n_quad): self.n_his_nvcof_N = 2 * self.p # shift local coefficients --> global coefficients - self.his_shift_D = np.arange(self.NbaseD) - (self.p - 1) - self.his_shift_N = np.arange(self.NbaseD) - self.p + self.his_shift_D = xp.arange(self.NbaseD) - (self.p - 1) + self.his_shift_N = xp.arange(self.NbaseD) - self.p for i in range(n_lambda_his): - self.his_global_N[i] = (np.arange(self.n_his_locbf_N) + i - (self.p - 1)) % self.NbaseN - self.his_global_D[i] = (np.arange(self.n_his_locbf_D) + i - (self.p - 1)) % self.NbaseD - self.his_loccof_N[i] = np.arange(self.n_his_locbf_N - 1, -1, -1) - self.his_loccof_D[i] = np.arange(self.n_his_locbf_D - 1, -1, -1) + self.his_global_N[i] = (xp.arange(self.n_his_locbf_N) + i - (self.p - 1)) % self.NbaseN + self.his_global_D[i] = (xp.arange(self.n_his_locbf_D) + i - (self.p - 1)) % self.NbaseD + self.his_loccof_N[i] = xp.arange(self.n_his_locbf_N - 1, -1, -1) + self.his_loccof_D[i] = xp.arange(self.n_his_locbf_D - 1, -1, -1) - self.x_his_indices[i] = np.arange(self.n_his) + 2 * (i - (self.p - 1)) + self.x_his_indices[i] = xp.arange(self.n_his) + 2 * (i - (self.p - 1)) self.coeffh_indices[i] = 0 for j in range(2 * self.p + 1): self.x_his[i, j] = (self.T[i + 1 + int(j / 2)] + self.T[i + 1 + int((j + 1) / 2)]) / 2 # quadrature points and weights self.pts, self.wts = bsp.quadrature_grid( - np.append(np.unique(self.x_his.flatten() % 1.0), 1.0), self.pts_loc, self.wts_loc + xp.append(xp.unique(self.x_his.flatten() % 1.0), 1.0), self.pts_loc, self.wts_loc ) # quasi interpolation def pi_0(self, fun): - lambdas = np.zeros(self.NbaseN, dtype=float) + lambdas = xp.zeros(self.NbaseN, dtype=float) # evaluate function at interpolation points - mat_f = fun(np.unique(self.x_int.flatten())) + mat_f = fun(xp.unique(self.x_int.flatten())) for i in range(self.NbaseN): for j in range(self.n_int): @@ -441,7 +441,7 @@ def pi_0(self, fun): # quasi histopolation def pi_1(self, fun): - lambdas = np.zeros(self.NbaseD, dtype=float) + lambdas = xp.zeros(self.NbaseD, dtype=float) # evaluate function at quadrature points mat_f = fun(self.pts) @@ -459,17 +459,17 @@ def pi_1(self, fun): # projection matrices of products of basis functions: pi0_i(A_j*B_k) and pi1_i(A_j*B_k) def projection_matrices_1d(self, bc_kind=["free", "free"]): - PI0_NN = np.empty((self.NbaseN, self.NbaseN, self.NbaseN), dtype=float) - PI0_DN = np.empty((self.NbaseN, self.NbaseD, self.NbaseN), dtype=float) - PI0_DD = np.empty((self.NbaseN, self.NbaseD, self.NbaseD), dtype=float) + PI0_NN = xp.empty((self.NbaseN, self.NbaseN, self.NbaseN), dtype=float) + PI0_DN = xp.empty((self.NbaseN, self.NbaseD, self.NbaseN), dtype=float) + PI0_DD = xp.empty((self.NbaseN, self.NbaseD, self.NbaseD), dtype=float) - PI1_NN = np.empty((self.NbaseD, self.NbaseN, self.NbaseN), dtype=float) - PI1_DN = np.empty((self.NbaseD, self.NbaseD, self.NbaseN), dtype=float) - PI1_DD = np.empty((self.NbaseD, self.NbaseD, self.NbaseD), dtype=float) + PI1_NN = xp.empty((self.NbaseD, self.NbaseN, self.NbaseN), dtype=float) + PI1_DN = xp.empty((self.NbaseD, self.NbaseD, self.NbaseN), dtype=float) + PI1_DD = xp.empty((self.NbaseD, self.NbaseD, self.NbaseD), dtype=float) # ========= PI0__NN and PI1_NN ============= - ci = np.zeros(self.NbaseN, dtype=float) - cj = np.zeros(self.NbaseN, dtype=float) + ci = xp.zeros(self.NbaseN, dtype=float) + cj = xp.zeros(self.NbaseN, dtype=float) for i in range(self.NbaseN): for j in range(self.NbaseN): @@ -485,8 +485,8 @@ def projection_matrices_1d(self, bc_kind=["free", "free"]): PI1_NN[:, i, j] = self.pi_1(fun) # ========= PI0__DN and PI1_DN ============= - ci = np.zeros(self.NbaseD, dtype=float) - cj = np.zeros(self.NbaseN, dtype=float) + ci = xp.zeros(self.NbaseD, dtype=float) + cj = xp.zeros(self.NbaseN, dtype=float) for i in range(self.NbaseD): for j in range(self.NbaseN): @@ -502,8 +502,8 @@ def projection_matrices_1d(self, bc_kind=["free", "free"]): PI1_DN[:, i, j] = self.pi_1(fun) # ========= PI0__DD and PI1_DD ============= - ci = np.zeros(self.NbaseD, dtype=float) - cj = np.zeros(self.NbaseD, dtype=float) + ci = xp.zeros(self.NbaseD, dtype=float) + cj = xp.zeros(self.NbaseD, dtype=float) for i in range(self.NbaseD): for j in range(self.NbaseD): @@ -518,8 +518,8 @@ def projection_matrices_1d(self, bc_kind=["free", "free"]): PI0_DD[:, i, j] = self.pi_0(fun) PI1_DD[:, i, j] = self.pi_1(fun) - PI0_ND = np.transpose(PI0_DN, (0, 2, 1)) - PI1_ND = np.transpose(PI1_DN, (0, 2, 1)) + PI0_ND = xp.transpose(PI0_DN, (0, 2, 1)) + PI1_ND = xp.transpose(PI1_DN, (0, 2, 1)) # remove contributions from first and last N-splines if bc_kind[0] == "dirichlet": @@ -544,25 +544,25 @@ def projection_matrices_1d(self, bc_kind=["free", "free"]): PI1_DN[:, :, -1] = 0.0 PI1_ND[:, -1, :] = 0.0 - PI0_NN_indices = np.nonzero(PI0_NN) - PI0_DN_indices = np.nonzero(PI0_DN) - PI0_ND_indices = np.nonzero(PI0_ND) - PI0_DD_indices = np.nonzero(PI0_DD) + PI0_NN_indices = xp.nonzero(PI0_NN) + PI0_DN_indices = xp.nonzero(PI0_DN) + PI0_ND_indices = xp.nonzero(PI0_ND) + PI0_DD_indices = xp.nonzero(PI0_DD) - PI1_NN_indices = np.nonzero(PI1_NN) - PI1_DN_indices = np.nonzero(PI1_DN) - PI1_ND_indices = np.nonzero(PI1_ND) - PI1_DD_indices = np.nonzero(PI1_DD) + PI1_NN_indices = xp.nonzero(PI1_NN) + PI1_DN_indices = xp.nonzero(PI1_DN) + PI1_ND_indices = xp.nonzero(PI1_ND) + PI1_DD_indices = xp.nonzero(PI1_DD) - PI0_NN_indices = np.vstack((PI0_NN_indices[0], PI0_NN_indices[1], PI0_NN_indices[2])) - PI0_DN_indices = np.vstack((PI0_DN_indices[0], PI0_DN_indices[1], PI0_DN_indices[2])) - PI0_ND_indices = np.vstack((PI0_ND_indices[0], PI0_ND_indices[1], PI0_ND_indices[2])) - PI0_DD_indices = np.vstack((PI0_DD_indices[0], PI0_DD_indices[1], PI0_DD_indices[2])) + PI0_NN_indices = xp.vstack((PI0_NN_indices[0], PI0_NN_indices[1], PI0_NN_indices[2])) + PI0_DN_indices = xp.vstack((PI0_DN_indices[0], PI0_DN_indices[1], PI0_DN_indices[2])) + PI0_ND_indices = xp.vstack((PI0_ND_indices[0], PI0_ND_indices[1], PI0_ND_indices[2])) + PI0_DD_indices = xp.vstack((PI0_DD_indices[0], PI0_DD_indices[1], PI0_DD_indices[2])) - PI1_NN_indices = np.vstack((PI1_NN_indices[0], PI1_NN_indices[1], PI1_NN_indices[2])) - PI1_DN_indices = np.vstack((PI1_DN_indices[0], PI1_DN_indices[1], PI1_DN_indices[2])) - PI1_ND_indices = np.vstack((PI1_ND_indices[0], PI1_ND_indices[1], PI1_ND_indices[2])) - PI1_DD_indices = np.vstack((PI1_DD_indices[0], PI1_DD_indices[1], PI1_DD_indices[2])) + PI1_NN_indices = xp.vstack((PI1_NN_indices[0], PI1_NN_indices[1], PI1_NN_indices[2])) + PI1_DN_indices = xp.vstack((PI1_DN_indices[0], PI1_DN_indices[1], PI1_DN_indices[2])) + PI1_ND_indices = xp.vstack((PI1_ND_indices[0], PI1_ND_indices[1], PI1_ND_indices[2])) + PI1_DD_indices = xp.vstack((PI1_DD_indices[0], PI1_DD_indices[1], PI1_DD_indices[2])) return ( PI0_NN, @@ -617,8 +617,8 @@ def __init__(self, tensor_space, n_quad): self.polar = False # local projectors for polar splines are not implemented yet # Gauss - Legendre quadrature points and weights in (-1, 1) - self.pts_loc = [np.polynomial.legendre.leggauss(n_quad)[0] for n_quad in self.n_quad] - self.wts_loc = [np.polynomial.legendre.leggauss(n_quad)[1] for n_quad in self.n_quad] + self.pts_loc = [xp.polynomial.legendre.leggauss(n_quad)[0] for n_quad in self.n_quad] + self.wts_loc = [xp.polynomial.legendre.leggauss(n_quad)[1] for n_quad in self.n_quad] # set interpolation and histopolation coefficients self.coeff_i = [0, 0, 0] @@ -626,78 +626,78 @@ def __init__(self, tensor_space, n_quad): for a in range(3): if self.bc[a] == True: - self.coeff_i[a] = np.zeros((1, 2 * self.p[a] - 1), dtype=float) - self.coeff_h[a] = np.zeros((1, 2 * self.p[a]), dtype=float) + self.coeff_i[a] = xp.zeros((1, 2 * self.p[a] - 1), dtype=float) + self.coeff_h[a] = xp.zeros((1, 2 * self.p[a]), dtype=float) if self.p[a] == 1: - self.coeff_i[a][0, :] = np.array([1.0]) - self.coeff_h[a][0, :] = np.array([1.0, 1.0]) + self.coeff_i[a][0, :] = xp.array([1.0]) + self.coeff_h[a][0, :] = xp.array([1.0, 1.0]) elif self.p[a] == 2: - self.coeff_i[a][0, :] = 1 / 2 * np.array([-1.0, 4.0, -1.0]) - self.coeff_h[a][0, :] = 1 / 2 * np.array([-1.0, 3.0, 3.0, -1.0]) + self.coeff_i[a][0, :] = 1 / 2 * xp.array([-1.0, 4.0, -1.0]) + self.coeff_h[a][0, :] = 1 / 2 * xp.array([-1.0, 3.0, 3.0, -1.0]) elif self.p[a] == 3: - self.coeff_i[a][0, :] = 1 / 6 * np.array([1.0, -8.0, 20.0, -8.0, 1.0]) - self.coeff_h[a][0, :] = 1 / 6 * np.array([1.0, -7.0, 12.0, 12.0, -7.0, 1.0]) + self.coeff_i[a][0, :] = 1 / 6 * xp.array([1.0, -8.0, 20.0, -8.0, 1.0]) + self.coeff_h[a][0, :] = 1 / 6 * xp.array([1.0, -7.0, 12.0, 12.0, -7.0, 1.0]) elif self.p[a] == 4: - self.coeff_i[a][0, :] = 2 / 45 * np.array([-1.0, 16.0, -295 / 4, 140.0, -295 / 4, 16.0, -1.0]) + self.coeff_i[a][0, :] = 2 / 45 * xp.array([-1.0, 16.0, -295 / 4, 140.0, -295 / 4, 16.0, -1.0]) self.coeff_h[a][0, :] = ( - 2 / 45 * np.array([-1.0, 15.0, -231 / 4, 265 / 4, 265 / 4, -231 / 4, 15.0, -1.0]) + 2 / 45 * xp.array([-1.0, 15.0, -231 / 4, 265 / 4, 265 / 4, -231 / 4, 15.0, -1.0]) ) else: print("degree > 4 not implemented!") else: - self.coeff_i[a] = np.zeros((2 * self.p[a] - 1, 2 * self.p[a] - 1), dtype=float) - self.coeff_h[a] = np.zeros((2 * self.p[a] - 1, 2 * self.p[a]), dtype=float) + self.coeff_i[a] = xp.zeros((2 * self.p[a] - 1, 2 * self.p[a] - 1), dtype=float) + self.coeff_h[a] = xp.zeros((2 * self.p[a] - 1, 2 * self.p[a]), dtype=float) if self.p[a] == 1: - self.coeff_i[a][0, :] = np.array([1.0]) - self.coeff_h[a][0, :] = np.array([1.0, 1.0]) + self.coeff_i[a][0, :] = xp.array([1.0]) + self.coeff_h[a][0, :] = xp.array([1.0, 1.0]) elif self.p[a] == 2: - self.coeff_i[a][0, :] = 1 / 2 * np.array([2.0, 0.0, 0.0]) - self.coeff_i[a][1, :] = 1 / 2 * np.array([-1.0, 4.0, -1.0]) - self.coeff_i[a][2, :] = 1 / 2 * np.array([0.0, 0.0, 2.0]) + self.coeff_i[a][0, :] = 1 / 2 * xp.array([2.0, 0.0, 0.0]) + self.coeff_i[a][1, :] = 1 / 2 * xp.array([-1.0, 4.0, -1.0]) + self.coeff_i[a][2, :] = 1 / 2 * xp.array([0.0, 0.0, 2.0]) - self.coeff_h[a][0, :] = 1 / 2 * np.array([3.0, -1.0, 0.0, 0.0]) - self.coeff_h[a][1, :] = 1 / 2 * np.array([-1.0, 3.0, 3.0, -1.0]) - self.coeff_h[a][2, :] = 1 / 2 * np.array([0.0, 0.0, -1.0, 3.0]) + self.coeff_h[a][0, :] = 1 / 2 * xp.array([3.0, -1.0, 0.0, 0.0]) + self.coeff_h[a][1, :] = 1 / 2 * xp.array([-1.0, 3.0, 3.0, -1.0]) + self.coeff_h[a][2, :] = 1 / 2 * xp.array([0.0, 0.0, -1.0, 3.0]) elif self.p[a] == 3: - self.coeff_i[a][0, :] = 1 / 18 * np.array([18.0, 0.0, 0.0, 0.0, 0.0]) - self.coeff_i[a][1, :] = 1 / 18 * np.array([-5.0, 40.0, -24.0, 8.0, -1.0]) - self.coeff_i[a][2, :] = 1 / 18 * np.array([3.0, -24.0, 60.0, -24.0, 3.0]) - self.coeff_i[a][3, :] = 1 / 18 * np.array([-1.0, 8.0, -24.0, 40.0, -5.0]) - self.coeff_i[a][4, :] = 1 / 18 * np.array([0.0, 0.0, 0.0, 0.0, 18.0]) - - self.coeff_h[a][0, :] = 1 / 18 * np.array([23.0, -17.0, 7.0, -1.0, 0.0, 0.0]) - self.coeff_h[a][1, :] = 1 / 18 * np.array([-8.0, 56.0, -28.0, 4.0, 0.0, 0.0]) - self.coeff_h[a][2, :] = 1 / 18 * np.array([3.0, -21.0, 36.0, 36.0, -21.0, 3.0]) - self.coeff_h[a][3, :] = 1 / 18 * np.array([0.0, 0.0, 4.0, -28.0, 56.0, -8.0]) - self.coeff_h[a][4, :] = 1 / 18 * np.array([0.0, 0.0, -1.0, 7.0, -17.0, 23.0]) + self.coeff_i[a][0, :] = 1 / 18 * xp.array([18.0, 0.0, 0.0, 0.0, 0.0]) + self.coeff_i[a][1, :] = 1 / 18 * xp.array([-5.0, 40.0, -24.0, 8.0, -1.0]) + self.coeff_i[a][2, :] = 1 / 18 * xp.array([3.0, -24.0, 60.0, -24.0, 3.0]) + self.coeff_i[a][3, :] = 1 / 18 * xp.array([-1.0, 8.0, -24.0, 40.0, -5.0]) + self.coeff_i[a][4, :] = 1 / 18 * xp.array([0.0, 0.0, 0.0, 0.0, 18.0]) + + self.coeff_h[a][0, :] = 1 / 18 * xp.array([23.0, -17.0, 7.0, -1.0, 0.0, 0.0]) + self.coeff_h[a][1, :] = 1 / 18 * xp.array([-8.0, 56.0, -28.0, 4.0, 0.0, 0.0]) + self.coeff_h[a][2, :] = 1 / 18 * xp.array([3.0, -21.0, 36.0, 36.0, -21.0, 3.0]) + self.coeff_h[a][3, :] = 1 / 18 * xp.array([0.0, 0.0, 4.0, -28.0, 56.0, -8.0]) + self.coeff_h[a][4, :] = 1 / 18 * xp.array([0.0, 0.0, -1.0, 7.0, -17.0, 23.0]) elif self.p[a] == 4: - self.coeff_i[a][0, :] = 1 / 360 * np.array([360.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) - self.coeff_i[a][1, :] = 1 / 360 * np.array([-59.0, 944.0, -1000.0, 720.0, -305.0, 64.0, -4.0]) - self.coeff_i[a][2, :] = 1 / 360 * np.array([23.0, -368.0, 1580.0, -1360.0, 605.0, -128.0, 8.0]) - self.coeff_i[a][3, :] = 1 / 360 * np.array([-16.0, 256.0, -1180.0, 2240.0, -1180.0, 256.0, -16.0]) - self.coeff_i[a][4, :] = 1 / 360 * np.array([8.0, -128.0, 605.0, -1360.0, 1580.0, -368.0, 23.0]) - self.coeff_i[a][5, :] = 1 / 360 * np.array([-4.0, 64.0, -305.0, 720.0, -1000.0, 944.0, -59.0]) - self.coeff_i[a][6, :] = 1 / 360 * np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 360.0]) - - self.coeff_h[a][0, :] = 1 / 360 * np.array([419.0, -525.0, 475.0, -245.0, 60.0, -4.0, 0.0, 0.0]) - self.coeff_h[a][1, :] = 1 / 360 * np.array([-82.0, 1230.0, -1350.0, 730.0, -180.0, 12.0, 0.0, 0.0]) - self.coeff_h[a][2, :] = 1 / 360 * np.array([39.0, -585.0, 2175.0, -1425.0, 360.0, -24.0, 0.0, 0.0]) + self.coeff_i[a][0, :] = 1 / 360 * xp.array([360.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + self.coeff_i[a][1, :] = 1 / 360 * xp.array([-59.0, 944.0, -1000.0, 720.0, -305.0, 64.0, -4.0]) + self.coeff_i[a][2, :] = 1 / 360 * xp.array([23.0, -368.0, 1580.0, -1360.0, 605.0, -128.0, 8.0]) + self.coeff_i[a][3, :] = 1 / 360 * xp.array([-16.0, 256.0, -1180.0, 2240.0, -1180.0, 256.0, -16.0]) + self.coeff_i[a][4, :] = 1 / 360 * xp.array([8.0, -128.0, 605.0, -1360.0, 1580.0, -368.0, 23.0]) + self.coeff_i[a][5, :] = 1 / 360 * xp.array([-4.0, 64.0, -305.0, 720.0, -1000.0, 944.0, -59.0]) + self.coeff_i[a][6, :] = 1 / 360 * xp.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 360.0]) + + self.coeff_h[a][0, :] = 1 / 360 * xp.array([419.0, -525.0, 475.0, -245.0, 60.0, -4.0, 0.0, 0.0]) + self.coeff_h[a][1, :] = 1 / 360 * xp.array([-82.0, 1230.0, -1350.0, 730.0, -180.0, 12.0, 0.0, 0.0]) + self.coeff_h[a][2, :] = 1 / 360 * xp.array([39.0, -585.0, 2175.0, -1425.0, 360.0, -24.0, 0.0, 0.0]) self.coeff_h[a][3, :] = ( - 1 / 360 * np.array([-16.0, 240.0, -924.0, 1060.0, 1060.0, -924.0, 240.0, -16.0]) + 1 / 360 * xp.array([-16.0, 240.0, -924.0, 1060.0, 1060.0, -924.0, 240.0, -16.0]) ) - self.coeff_h[a][4, :] = 1 / 360 * np.array([0.0, 0.0, -24.0, 360.0, -1425.0, 2175.0, -585.0, 39.0]) - self.coeff_h[a][5, :] = 1 / 360 * np.array([0.0, 0.0, 12.0, -180.0, 730.0, -1350.0, 1230.0, -82.0]) - self.coeff_h[a][6, :] = 1 / 360 * np.array([0.0, 0.0, -4.0, 60.0, -245.0, 475.0, -525.0, 419.0]) + self.coeff_h[a][4, :] = 1 / 360 * xp.array([0.0, 0.0, -24.0, 360.0, -1425.0, 2175.0, -585.0, 39.0]) + self.coeff_h[a][5, :] = 1 / 360 * xp.array([0.0, 0.0, 12.0, -180.0, 730.0, -1350.0, 1230.0, -82.0]) + self.coeff_h[a][6, :] = 1 / 360 * xp.array([0.0, 0.0, -4.0, 60.0, -245.0, 475.0, -525.0, 419.0]) else: print("degree > 4 not implemented!") @@ -723,31 +723,31 @@ def __init__(self, tensor_space, n_quad): ) # number of non-vanishing D bf in interpolation interval (1, 2, 4, 6) self.x_int = [ - np.zeros((n_lambda_int, n_int), dtype=float) for n_lambda_int, n_int in zip(n_lambda_int, self.n_int) + xp.zeros((n_lambda_int, n_int), dtype=float) for n_lambda_int, n_int in zip(n_lambda_int, self.n_int) ] self.int_global_N = [ - np.zeros((n_lambda_int, n_int_locbf_N), dtype=int) + xp.zeros((n_lambda_int, n_int_locbf_N), dtype=int) for n_lambda_int, n_int_locbf_N in zip(n_lambda_int, self.n_int_locbf_N) ] self.int_global_D = [ - np.zeros((n_lambda_int, n_int_locbf_D), dtype=int) + xp.zeros((n_lambda_int, n_int_locbf_D), dtype=int) for n_lambda_int, n_int_locbf_D in zip(n_lambda_int, self.n_int_locbf_D) ] self.int_loccof_N = [ - np.zeros((n_lambda_int, n_int_locbf_N), dtype=int) + xp.zeros((n_lambda_int, n_int_locbf_N), dtype=int) for n_lambda_int, n_int_locbf_N in zip(n_lambda_int, self.n_int_locbf_N) ] self.int_loccof_D = [ - np.zeros((n_lambda_int, n_int_locbf_D), dtype=int) + xp.zeros((n_lambda_int, n_int_locbf_D), dtype=int) for n_lambda_int, n_int_locbf_D in zip(n_lambda_int, self.n_int_locbf_D) ] self.x_int_indices = [ - np.zeros((n_lambda_int, n_int), dtype=int) for n_lambda_int, n_int in zip(n_lambda_int, self.n_int) + xp.zeros((n_lambda_int, n_int), dtype=int) for n_lambda_int, n_int in zip(n_lambda_int, self.n_int) ] - self.coeffi_indices = [np.zeros(n_lambda_int, dtype=int) for n_lambda_int in n_lambda_int] + self.coeffi_indices = [xp.zeros(n_lambda_int, dtype=int) for n_lambda_int in n_lambda_int] self.n_int_nvcof_D = [None, None, None] self.n_int_nvcof_N = [None, None, None] @@ -770,39 +770,39 @@ def __init__(self, tensor_space, n_quad): self.n_int_nvcof_N[a] = 3 * self.p[a] - 2 # shift in local coefficient indices at right boundary (only for non-periodic boundary conditions) - self.int_add_D[a] = np.arange(self.n_int[a] - 2) + 1 - self.int_add_N[a] = np.arange(self.n_int[a] - 1) + 1 + self.int_add_D[a] = xp.arange(self.n_int[a] - 2) + 1 + self.int_add_N[a] = xp.arange(self.n_int[a] - 1) + 1 counter_D = 0 counter_N = 0 # shift local coefficients --> global coefficients (D) if self.p[a] == 1: - self.int_shift_D[a] = np.arange(self.NbaseD[a]) + self.int_shift_D[a] = xp.arange(self.NbaseD[a]) else: - self.int_shift_D[a] = np.arange(self.NbaseD[a]) - (self.p[a] - 2) + self.int_shift_D[a] = xp.arange(self.NbaseD[a]) - (self.p[a] - 2) self.int_shift_D[a][: 2 * self.p[a] - 2] = 0 self.int_shift_D[a][-(2 * self.p[a] - 2) :] = self.int_shift_D[a][-(2 * self.p[a] - 2)] # shift local coefficients --> global coefficients (N) if self.p[a] == 1: - self.int_shift_N[a] = np.arange(self.NbaseN[a]) + self.int_shift_N[a] = xp.arange(self.NbaseN[a]) self.int_shift_N[a][-1] = self.int_shift_N[a][-2] else: - self.int_shift_N[a] = np.arange(self.NbaseN[a]) - (self.p[a] - 1) + self.int_shift_N[a] = xp.arange(self.NbaseN[a]) - (self.p[a] - 1) self.int_shift_N[a][: 2 * self.p[a] - 1] = 0 self.int_shift_N[a][-(2 * self.p[a] - 1) :] = self.int_shift_N[a][-(2 * self.p[a] - 1)] - counter_coeffi = np.copy(self.p[a]) + counter_coeffi = xp.copy(self.p[a]) for i in range(n_lambda_int[a]): # left boundary region if i < self.p[a] - 1: - self.int_global_N[a][i] = np.arange(self.n_int_locbf_N[a]) - self.int_global_D[a][i] = np.arange(self.n_int_locbf_D[a]) + self.int_global_N[a][i] = xp.arange(self.n_int_locbf_N[a]) + self.int_global_D[a][i] = xp.arange(self.n_int_locbf_D[a]) - self.x_int_indices[a][i] = np.arange(self.n_int[a]) + self.x_int_indices[a][i] = xp.arange(self.n_int[a]) self.coeffi_indices[a][i] = i for j in range(2 * (self.p[a] - 1) + 1): xi = self.p[a] - 1 @@ -813,13 +813,13 @@ def __init__(self, tensor_space, n_quad): # right boundary region elif i > n_lambda_int[a] - self.p[a]: self.int_global_N[a][i] = ( - np.arange(self.n_int_locbf_N[a]) + n_lambda_int[a] - self.p[a] - (self.p[a] - 1) + xp.arange(self.n_int_locbf_N[a]) + n_lambda_int[a] - self.p[a] - (self.p[a] - 1) ) self.int_global_D[a][i] = ( - np.arange(self.n_int_locbf_D[a]) + n_lambda_int[a] - self.p[a] - (self.p[a] - 1) + xp.arange(self.n_int_locbf_D[a]) + n_lambda_int[a] - self.p[a] - (self.p[a] - 1) ) - self.x_int_indices[a][i] = np.arange(self.n_int[a]) + 2 * ( + self.x_int_indices[a][i] = xp.arange(self.n_int[a]) + 2 * ( n_lambda_int[a] - self.p[a] - (self.p[a] - 1) ) self.coeffi_indices[a][i] = counter_coeffi @@ -833,20 +833,20 @@ def __init__(self, tensor_space, n_quad): # interior else: if self.p[a] == 1: - self.int_global_N[a][i] = np.arange(self.n_int_locbf_N[a]) + i - self.int_global_D[a][i] = np.arange(self.n_int_locbf_D[a]) + i + self.int_global_N[a][i] = xp.arange(self.n_int_locbf_N[a]) + i + self.int_global_D[a][i] = xp.arange(self.n_int_locbf_D[a]) + i self.int_global_N[a][-1] = self.int_global_N[a][-2] self.int_global_D[a][-1] = self.int_global_D[a][-2] else: - self.int_global_N[a][i] = np.arange(self.n_int_locbf_N[a]) + i - (self.p[a] - 1) - self.int_global_D[a][i] = np.arange(self.n_int_locbf_D[a]) + i - (self.p[a] - 1) + self.int_global_N[a][i] = xp.arange(self.n_int_locbf_N[a]) + i - (self.p[a] - 1) + self.int_global_D[a][i] = xp.arange(self.n_int_locbf_D[a]) + i - (self.p[a] - 1) if self.p[a] == 1: self.x_int_indices[a][i] = i else: - self.x_int_indices[a][i] = np.arange(self.n_int[a]) + 2 * (i - (self.p[a] - 1)) + self.x_int_indices[a][i] = xp.arange(self.n_int[a]) + 2 * (i - (self.p[a] - 1)) self.coeffi_indices[a][i] = self.p[a] - 1 @@ -857,8 +857,8 @@ def __init__(self, tensor_space, n_quad): # local coefficient index if self.p[a] == 1: - self.int_loccof_N[a][i] = np.array([0, 1]) - self.int_loccof_D[a][-1] = np.array([1]) + self.int_loccof_N[a][i] = xp.array([0, 1]) + self.int_loccof_D[a][-1] = xp.array([1]) else: if i > 0: @@ -866,8 +866,8 @@ def __init__(self, tensor_space, n_quad): k_glob_new = self.int_global_D[a][i, il] bol = k_glob_new == self.int_global_D[a][i - 1] - if np.any(bol): - self.int_loccof_D[a][i, il] = self.int_loccof_D[a][i - 1, np.where(bol)[0][0]] + 1 + if xp.any(bol): + self.int_loccof_D[a][i, il] = self.int_loccof_D[a][i - 1, xp.where(bol)[0][0]] + 1 if (k_glob_new >= n_lambda_int[a] - self.p[a] - (self.p[a] - 2)) and ( self.int_loccof_D[a][i, il] == 0 @@ -879,8 +879,8 @@ def __init__(self, tensor_space, n_quad): k_glob_new = self.int_global_N[a][i, il] bol = k_glob_new == self.int_global_N[a][i - 1] - if np.any(bol): - self.int_loccof_N[a][i, il] = self.int_loccof_N[a][i - 1, np.where(bol)[0][0]] + 1 + if xp.any(bol): + self.int_loccof_N[a][i, il] = self.int_loccof_N[a][i - 1, xp.where(bol)[0][0]] + 1 if (k_glob_new >= n_lambda_int[a] - self.p[a] - (self.p[a] - 2)) and ( self.int_loccof_N[a][i, il] == 0 @@ -900,24 +900,24 @@ def __init__(self, tensor_space, n_quad): # shift local coefficients --> global coefficients if self.p[a] == 1: - self.int_shift_D[a] = np.arange(self.NbaseN[a]) - (self.p[a] - 1) - self.int_shift_N[a] = np.arange(self.NbaseN[a]) - (self.p[a]) + self.int_shift_D[a] = xp.arange(self.NbaseN[a]) - (self.p[a] - 1) + self.int_shift_N[a] = xp.arange(self.NbaseN[a]) - (self.p[a]) else: - self.int_shift_D[a] = np.arange(self.NbaseN[a]) - (self.p[a] - 2) - self.int_shift_N[a] = np.arange(self.NbaseN[a]) - (self.p[a] - 1) + self.int_shift_D[a] = xp.arange(self.NbaseN[a]) - (self.p[a] - 2) + self.int_shift_N[a] = xp.arange(self.NbaseN[a]) - (self.p[a] - 1) for i in range(n_lambda_int[a]): # global indices of non-vanishing basis functions and position of coefficients in final matrix - self.int_global_N[a][i] = (np.arange(self.n_int_locbf_N[a]) + i - (self.p[a] - 1)) % self.NbaseN[a] - self.int_global_D[a][i] = (np.arange(self.n_int_locbf_D[a]) + i - (self.p[a] - 1)) % self.NbaseD[a] + self.int_global_N[a][i] = (xp.arange(self.n_int_locbf_N[a]) + i - (self.p[a] - 1)) % self.NbaseN[a] + self.int_global_D[a][i] = (xp.arange(self.n_int_locbf_D[a]) + i - (self.p[a] - 1)) % self.NbaseD[a] - self.int_loccof_N[a][i] = np.arange(self.n_int_locbf_N[a] - 1, -1, -1) - self.int_loccof_D[a][i] = np.arange(self.n_int_locbf_D[a] - 1, -1, -1) + self.int_loccof_N[a][i] = xp.arange(self.n_int_locbf_N[a] - 1, -1, -1) + self.int_loccof_D[a][i] = xp.arange(self.n_int_locbf_D[a] - 1, -1, -1) if self.p[a] == 1: self.x_int_indices[a][i] = i else: - self.x_int_indices[a][i] = (np.arange(self.n_int[a]) + 2 * (i - (self.p[a] - 1))) % ( + self.x_int_indices[a][i] = (xp.arange(self.n_int[a]) + 2 * (i - (self.p[a] - 1))) % ( 2 * self.Nel[a] ) @@ -929,38 +929,38 @@ def __init__(self, tensor_space, n_quad): ) % 1.0 # set histopolation points, quadrature points and weights - n_lambda_his = [np.copy(NbaseD) for NbaseD in self.NbaseD] # number of coefficients in space V1 + n_lambda_his = [xp.copy(NbaseD) for NbaseD in self.NbaseD] # number of coefficients in space V1 self.n_his = [2 * p for p in self.p] # number of histopolation intervals self.n_his_locbf_N = [2 * p for p in self.p] # number of non-vanishing N bf in histopolation interval self.n_his_locbf_D = [2 * p - 1 for p in self.p] # number of non-vanishing D bf in histopolation interval self.x_his = [ - np.zeros((n_lambda_his, n_his + 1), dtype=float) for n_lambda_his, n_his in zip(n_lambda_his, self.n_his) + xp.zeros((n_lambda_his, n_his + 1), dtype=float) for n_lambda_his, n_his in zip(n_lambda_his, self.n_his) ] self.his_global_N = [ - np.zeros((n_lambda_his, n_his_locbf_N), dtype=int) + xp.zeros((n_lambda_his, n_his_locbf_N), dtype=int) for n_lambda_his, n_his_locbf_N in zip(n_lambda_his, self.n_his_locbf_N) ] self.his_global_D = [ - np.zeros((n_lambda_his, n_his_locbf_D), dtype=int) + xp.zeros((n_lambda_his, n_his_locbf_D), dtype=int) for n_lambda_his, n_his_locbf_D in zip(n_lambda_his, self.n_his_locbf_D) ] self.his_loccof_N = [ - np.zeros((n_lambda_his, n_his_locbf_N), dtype=int) + xp.zeros((n_lambda_his, n_his_locbf_N), dtype=int) for n_lambda_his, n_his_locbf_N in zip(n_lambda_his, self.n_his_locbf_N) ] self.his_loccof_D = [ - np.zeros((n_lambda_his, n_his_locbf_D), dtype=int) + xp.zeros((n_lambda_his, n_his_locbf_D), dtype=int) for n_lambda_his, n_his_locbf_D in zip(n_lambda_his, self.n_his_locbf_D) ] self.x_his_indices = [ - np.zeros((n_lambda_his, n_his), dtype=int) for n_lambda_his, n_his in zip(n_lambda_his, self.n_his) + xp.zeros((n_lambda_his, n_his), dtype=int) for n_lambda_his, n_his in zip(n_lambda_his, self.n_his) ] - self.coeffh_indices = [np.zeros(n_lambda_his, dtype=int) for n_lambda_his in n_lambda_his] + self.coeffh_indices = [xp.zeros(n_lambda_his, dtype=int) for n_lambda_his in n_lambda_his] self.pts = [0, 0, 0] self.wts = [0, 0, 0] @@ -981,31 +981,31 @@ def __init__(self, tensor_space, n_quad): self.n_his_nvcof_N[a] = 3 * self.p[a] - 1 # shift in local coefficient indices at right boundary (only for non-periodic boundary conditions) - self.his_add_D[a] = np.arange(self.n_his[a] - 2) + 1 - self.his_add_N[a] = np.arange(self.n_his[a] - 1) + 1 + self.his_add_D[a] = xp.arange(self.n_his[a] - 2) + 1 + self.his_add_N[a] = xp.arange(self.n_his[a] - 1) + 1 counter_D = 0 counter_N = 0 # shift local coefficients --> global coefficients (D) - self.his_shift_D[a] = np.arange(self.NbaseD[a]) - (self.p[a] - 1) + self.his_shift_D[a] = xp.arange(self.NbaseD[a]) - (self.p[a] - 1) self.his_shift_D[a][: 2 * self.p[a] - 1] = 0 self.his_shift_D[a][-(2 * self.p[a] - 1) :] = self.his_shift_D[a][-(2 * self.p[a] - 1)] # shift local coefficients --> global coefficients (N) - self.his_shift_N[a] = np.arange(self.NbaseN[a]) - self.p[a] + self.his_shift_N[a] = xp.arange(self.NbaseN[a]) - self.p[a] self.his_shift_N[a][: 2 * self.p[a]] = 0 self.his_shift_N[a][-2 * self.p[a] :] = self.his_shift_N[a][-2 * self.p[a]] - counter_coeffh = np.copy(self.p[a]) + counter_coeffh = xp.copy(self.p[a]) for i in range(n_lambda_his[a]): # left boundary region if i < self.p[a] - 1: - self.his_global_N[a][i] = np.arange(self.n_his_locbf_N[a]) - self.his_global_D[a][i] = np.arange(self.n_his_locbf_D[a]) + self.his_global_N[a][i] = xp.arange(self.n_his_locbf_N[a]) + self.his_global_D[a][i] = xp.arange(self.n_his_locbf_D[a]) - self.x_his_indices[a][i] = np.arange(self.n_his[a]) + self.x_his_indices[a][i] = xp.arange(self.n_his[a]) self.coeffh_indices[a][i] = i for j in range(2 * self.p[a] + 1): xi = self.p[a] - 1 @@ -1016,13 +1016,13 @@ def __init__(self, tensor_space, n_quad): # right boundary region elif i > n_lambda_his[a] - self.p[a]: self.his_global_N[a][i] = ( - np.arange(self.n_his_locbf_N[a]) + n_lambda_his[a] - self.p[a] - (self.p[a] - 1) + xp.arange(self.n_his_locbf_N[a]) + n_lambda_his[a] - self.p[a] - (self.p[a] - 1) ) self.his_global_D[a][i] = ( - np.arange(self.n_his_locbf_D[a]) + n_lambda_his[a] - self.p[a] - (self.p[a] - 1) + xp.arange(self.n_his_locbf_D[a]) + n_lambda_his[a] - self.p[a] - (self.p[a] - 1) ) - self.x_his_indices[a][i] = np.arange(self.n_his[a]) + 2 * ( + self.x_his_indices[a][i] = xp.arange(self.n_his[a]) + 2 * ( n_lambda_his[a] - self.p[a] - (self.p[a] - 1) ) self.coeffh_indices[a][i] = counter_coeffh @@ -1035,10 +1035,10 @@ def __init__(self, tensor_space, n_quad): # interior else: - self.his_global_N[a][i] = np.arange(self.n_his_locbf_N[a]) + i - (self.p[a] - 1) - self.his_global_D[a][i] = np.arange(self.n_his_locbf_D[a]) + i - (self.p[a] - 1) + self.his_global_N[a][i] = xp.arange(self.n_his_locbf_N[a]) + i - (self.p[a] - 1) + self.his_global_D[a][i] = xp.arange(self.n_his_locbf_D[a]) + i - (self.p[a] - 1) - self.x_his_indices[a][i] = np.arange(self.n_his[a]) + 2 * (i - (self.p[a] - 1)) + self.x_his_indices[a][i] = xp.arange(self.n_his[a]) + 2 * (i - (self.p[a] - 1)) self.coeffh_indices[a][i] = self.p[a] - 1 for j in range(2 * self.p[a] + 1): self.x_his[a][i, j] = ( @@ -1051,8 +1051,8 @@ def __init__(self, tensor_space, n_quad): k_glob_new = self.his_global_D[a][i, il] bol = k_glob_new == self.his_global_D[a][i - 1] - if np.any(bol): - self.his_loccof_D[a][i, il] = self.his_loccof_D[a][i - 1, np.where(bol)[0][0]] + 1 + if xp.any(bol): + self.his_loccof_D[a][i, il] = self.his_loccof_D[a][i - 1, xp.where(bol)[0][0]] + 1 if (k_glob_new >= n_lambda_his[a] - self.p[a] - (self.p[a] - 2)) and ( self.his_loccof_D[a][i, il] == 0 @@ -1064,8 +1064,8 @@ def __init__(self, tensor_space, n_quad): k_glob_new = self.his_global_N[a][i, il] bol = k_glob_new == self.his_global_N[a][i - 1] - if np.any(bol): - self.his_loccof_N[a][i, il] = self.his_loccof_N[a][i - 1, np.where(bol)[0][0]] + 1 + if xp.any(bol): + self.his_loccof_N[a][i, il] = self.his_loccof_N[a][i - 1, xp.where(bol)[0][0]] + 1 if (k_glob_new >= n_lambda_his[a] - self.p[a] - (self.p[a] - 2)) and ( self.his_loccof_N[a][i, il] == 0 @@ -1075,7 +1075,7 @@ def __init__(self, tensor_space, n_quad): # quadrature points and weights self.pts[a], self.wts[a] = bsp.quadrature_grid( - np.unique(self.x_his[a].flatten()), self.pts_loc[a], self.wts_loc[a] + xp.unique(self.x_his[a].flatten()), self.pts_loc[a], self.wts_loc[a] ) else: @@ -1084,18 +1084,18 @@ def __init__(self, tensor_space, n_quad): self.n_his_nvcof_N[a] = 2 * self.p[a] # shift local coefficients --> global coefficients (D) - self.his_shift_D[a] = np.arange(self.NbaseD[a]) - (self.p[a] - 1) + self.his_shift_D[a] = xp.arange(self.NbaseD[a]) - (self.p[a] - 1) # shift local coefficients --> global coefficients (N) - self.his_shift_N[a] = np.arange(self.NbaseD[a]) - self.p[a] + self.his_shift_N[a] = xp.arange(self.NbaseD[a]) - self.p[a] for i in range(n_lambda_his[a]): - self.his_global_N[a][i] = (np.arange(self.n_his_locbf_N[a]) + i - (self.p[a] - 1)) % self.NbaseN[a] - self.his_global_D[a][i] = (np.arange(self.n_his_locbf_D[a]) + i - (self.p[a] - 1)) % self.NbaseD[a] - self.his_loccof_N[a][i] = np.arange(self.n_his_locbf_N[a] - 1, -1, -1) - self.his_loccof_D[a][i] = np.arange(self.n_his_locbf_D[a] - 1, -1, -1) + self.his_global_N[a][i] = (xp.arange(self.n_his_locbf_N[a]) + i - (self.p[a] - 1)) % self.NbaseN[a] + self.his_global_D[a][i] = (xp.arange(self.n_his_locbf_D[a]) + i - (self.p[a] - 1)) % self.NbaseD[a] + self.his_loccof_N[a][i] = xp.arange(self.n_his_locbf_N[a] - 1, -1, -1) + self.his_loccof_D[a][i] = xp.arange(self.n_his_locbf_D[a] - 1, -1, -1) - self.x_his_indices[a][i] = (np.arange(self.n_his[a]) + 2 * (i - (self.p[a] - 1))) % ( + self.x_his_indices[a][i] = (xp.arange(self.n_his[a]) + 2 * (i - (self.p[a] - 1))) % ( 2 * self.Nel[a] ) self.coeffh_indices[a][i] = 0 @@ -1105,7 +1105,7 @@ def __init__(self, tensor_space, n_quad): # quadrature points and weights self.pts[a], self.wts[a] = bsp.quadrature_grid( - np.append(np.unique(self.x_his[a].flatten() % 1.0), 1.0), self.pts_loc[a], self.wts_loc[a] + xp.append(xp.unique(self.x_his[a].flatten() % 1.0), 1.0), self.pts_loc[a], self.wts_loc[a] ) # projector on space V0 (interpolation) @@ -1131,18 +1131,18 @@ def pi_0(self, fun, include_bc=True, eval_kind="meshgrid"): """ # interpolation points - x_int1 = np.unique(self.x_int[0].flatten()) - x_int2 = np.unique(self.x_int[1].flatten()) - x_int3 = np.unique(self.x_int[2].flatten()) + x_int1 = xp.unique(self.x_int[0].flatten()) + x_int2 = xp.unique(self.x_int[1].flatten()) + x_int3 = xp.unique(self.x_int[2].flatten()) # evaluation of function at interpolation points - mat_f = np.empty((x_int1.size, x_int2.size, x_int3.size), dtype=float) + mat_f = xp.empty((x_int1.size, x_int2.size, x_int3.size), dtype=float) # external function call if a callable is passed if callable(fun): # create a meshgrid and evaluate function on point set if eval_kind == "meshgrid": - pts1, pts2, pts3 = np.meshgrid(x_int1, x_int2, x_int3, indexing="ij") + pts1, pts2, pts3 = xp.meshgrid(x_int1, x_int2, x_int3, indexing="ij") mat_f[:, :, :] = fun(pts1, pts2, pts3) # tensor-product evaluation is done by input function @@ -1161,7 +1161,7 @@ def pi_0(self, fun, include_bc=True, eval_kind="meshgrid"): print("no internal 3D function implemented!") # coefficients - lambdas = np.zeros((self.NbaseN[0], self.NbaseN[1], self.NbaseN[2]), dtype=float) + lambdas = xp.zeros((self.NbaseN[0], self.NbaseN[1], self.NbaseN[2]), dtype=float) ker_loc.kernel_pi0_3d( self.NbaseN, @@ -1204,20 +1204,20 @@ def pi_1(self, fun, include_bc=True, eval_kind="meshgrid"): """ # interpolation points - x_int1 = np.unique(self.x_int[0].flatten()) - x_int2 = np.unique(self.x_int[1].flatten()) - x_int3 = np.unique(self.x_int[2].flatten()) + x_int1 = xp.unique(self.x_int[0].flatten()) + x_int2 = xp.unique(self.x_int[1].flatten()) + x_int3 = xp.unique(self.x_int[2].flatten()) # ======== 1-component ======== # evaluation of function at interpolation/quadrature points - mat_f = np.empty((self.pts[0].flatten().size, x_int2.size, x_int3.size), dtype=float) + mat_f = xp.empty((self.pts[0].flatten().size, x_int2.size, x_int3.size), dtype=float) # external function call if a callable is passed if callable(fun[0]): # create a meshgrid and evaluate function on point set if eval_kind == "meshgrid": - pts1, pts2, pts3 = np.meshgrid(self.pts[0].flatten(), x_int2, x_int3, indexing="ij") + pts1, pts2, pts3 = xp.meshgrid(self.pts[0].flatten(), x_int2, x_int3, indexing="ij") mat_f[:, :, :] = fun[0](pts1, pts2, pts3) # tensor-product evaluation is done by input function @@ -1236,7 +1236,7 @@ def pi_1(self, fun, include_bc=True, eval_kind="meshgrid"): print("no internal 3D function implemented!") # compute coefficients - lambdas1 = np.zeros((self.NbaseD[0], self.NbaseN[1], self.NbaseN[2]), dtype=float) + lambdas1 = xp.zeros((self.NbaseD[0], self.NbaseN[1], self.NbaseN[2]), dtype=float) ker_loc.kernel_pi11_3d( [self.NbaseD[0], self.NbaseN[1], self.NbaseN[2]], @@ -1259,13 +1259,13 @@ def pi_1(self, fun, include_bc=True, eval_kind="meshgrid"): # ======== 2-component ======== # evaluation of function at interpolation/quadrature points - mat_f = np.empty((x_int1.size, self.pts[1].flatten().size, x_int3.size), dtype=float) + mat_f = xp.empty((x_int1.size, self.pts[1].flatten().size, x_int3.size), dtype=float) # external function call if a callable is passed if callable(fun[1]): # create a meshgrid and evaluate function on point set if eval_kind == "meshgrid": - pts1, pts2, pts3 = np.meshgrid(x_int1, self.pts[1].flatten(), x_int3, indexing="ij") + pts1, pts2, pts3 = xp.meshgrid(x_int1, self.pts[1].flatten(), x_int3, indexing="ij") mat_f[:, :, :] = fun[1](pts1, pts2, pts3) # tensor-product evaluation is done by input function @@ -1284,7 +1284,7 @@ def pi_1(self, fun, include_bc=True, eval_kind="meshgrid"): print("no internal 3D function implemented!") # compute coefficients - lambdas2 = np.zeros((self.NbaseN[0], self.NbaseD[1], self.NbaseN[2]), dtype=float) + lambdas2 = xp.zeros((self.NbaseN[0], self.NbaseD[1], self.NbaseN[2]), dtype=float) ker_loc.kernel_pi12_3d( [self.NbaseN[0], self.NbaseD[1], self.NbaseN[2]], @@ -1307,13 +1307,13 @@ def pi_1(self, fun, include_bc=True, eval_kind="meshgrid"): # ======== 3-component ======== # evaluation of function at interpolation/quadrature points - mat_f = np.empty((x_int1.size, x_int1.size, self.pts[2].flatten().size), dtype=float) + mat_f = xp.empty((x_int1.size, x_int1.size, self.pts[2].flatten().size), dtype=float) # external function call if a callable is passed if callable(fun[2]): # create a meshgrid and evaluate function on point set if eval_kind == "meshgrid": - pts1, pts2, pts3 = np.meshgrid(x_int1, x_int2, self.pts[2].flatten(), indexing="ij") + pts1, pts2, pts3 = xp.meshgrid(x_int1, x_int2, self.pts[2].flatten(), indexing="ij") mat_f[:, :, :] = fun[2](pts1, pts2, pts3) # tensor-product evaluation is done by input function @@ -1332,7 +1332,7 @@ def pi_1(self, fun, include_bc=True, eval_kind="meshgrid"): print("no internal 3D function implemented!") # compute coefficients - lambdas3 = np.zeros((self.NbaseN[0], self.NbaseN[1], self.NbaseD[2]), dtype=float) + lambdas3 = xp.zeros((self.NbaseN[0], self.NbaseN[1], self.NbaseD[2]), dtype=float) ker_loc.kernel_pi13_3d( [self.NbaseN[0], self.NbaseN[1], self.NbaseD[2]], @@ -1352,7 +1352,7 @@ def pi_1(self, fun, include_bc=True, eval_kind="meshgrid"): lambdas3, ) - return np.concatenate((lambdas1.flatten(), lambdas2.flatten(), lambdas3.flatten())) + return xp.concatenate((lambdas1.flatten(), lambdas2.flatten(), lambdas3.flatten())) # projector on space V1 ([inter, histo, histo], [histo, inter, histo], [histo, histo, inter]) def pi_2(self, fun, include_bc=True, eval_kind="meshgrid"): @@ -1377,20 +1377,20 @@ def pi_2(self, fun, include_bc=True, eval_kind="meshgrid"): """ # interpolation points - x_int1 = np.unique(self.x_int[0].flatten()) - x_int2 = np.unique(self.x_int[1].flatten()) - x_int3 = np.unique(self.x_int[2].flatten()) + x_int1 = xp.unique(self.x_int[0].flatten()) + x_int2 = xp.unique(self.x_int[1].flatten()) + x_int3 = xp.unique(self.x_int[2].flatten()) # ======== 1-component ======== # evaluation of function at interpolation/quadrature points - mat_f = np.empty((x_int1.size, self.pts[1].flatten().size, self.pts[2].flatten().size), dtype=float) + mat_f = xp.empty((x_int1.size, self.pts[1].flatten().size, self.pts[2].flatten().size), dtype=float) # external function call if a callable is passed if callable(fun[0]): # create a meshgrid and evaluate function on point set if eval_kind == "meshgrid": - pts1, pts2, pts3 = np.meshgrid(x_int1, self.pts[1].flatten(), self.pts[2].flatten(), indexing="ij") + pts1, pts2, pts3 = xp.meshgrid(x_int1, self.pts[1].flatten(), self.pts[2].flatten(), indexing="ij") mat_f[:, :, :] = fun[0](pts1, pts2, pts3) # tensor-product evaluation is done by input function @@ -1409,7 +1409,7 @@ def pi_2(self, fun, include_bc=True, eval_kind="meshgrid"): print("no internal 3D function implemented!") # compute coefficients - lambdas1 = np.zeros((self.NbaseN[0], self.NbaseD[1], self.NbaseD[2]), dtype=float) + lambdas1 = xp.zeros((self.NbaseN[0], self.NbaseD[1], self.NbaseD[2]), dtype=float) ker_loc.kernel_pi21_3d( [self.NbaseN[0], self.NbaseD[1], self.NbaseD[2]], @@ -1435,13 +1435,13 @@ def pi_2(self, fun, include_bc=True, eval_kind="meshgrid"): # ======== 2-component ======== # evaluation of function at interpolation/quadrature points - mat_f = np.empty((self.pts[0].flatten().size, x_int2.size, self.pts[2].flatten().size), dtype=float) + mat_f = xp.empty((self.pts[0].flatten().size, x_int2.size, self.pts[2].flatten().size), dtype=float) # external function call if a callable is passed if callable(fun[1]): # create a meshgrid and evaluate function on point set if eval_kind == "meshgrid": - pts1, pts2, pts3 = np.meshgrid(self.pts[0].flatten(), x_int2, self.pts[2].flatten(), indexing="ij") + pts1, pts2, pts3 = xp.meshgrid(self.pts[0].flatten(), x_int2, self.pts[2].flatten(), indexing="ij") mat_f[:, :, :] = fun[1](pts1, pts2, pts3) # tensor-product evaluation is done by input function @@ -1460,7 +1460,7 @@ def pi_2(self, fun, include_bc=True, eval_kind="meshgrid"): print("no internal 3D function implemented!") # compute coefficients - lambdas2 = np.zeros((self.NbaseD[0], self.NbaseN[1], self.NbaseD[2]), dtype=float) + lambdas2 = xp.zeros((self.NbaseD[0], self.NbaseN[1], self.NbaseD[2]), dtype=float) ker_loc.kernel_pi22_3d( [self.NbaseD[0], self.NbaseN[1], self.NbaseD[2]], @@ -1486,13 +1486,13 @@ def pi_2(self, fun, include_bc=True, eval_kind="meshgrid"): # ======== 3-component ======== # evaluation of function at interpolation/quadrature points - mat_f = np.empty((self.pts[0].flatten().size, self.pts[1].flatten().size, x_int3.size), dtype=float) + mat_f = xp.empty((self.pts[0].flatten().size, self.pts[1].flatten().size, x_int3.size), dtype=float) # external function call if a callable is passed if callable(fun[2]): # create a meshgrid and evaluate function on point set if eval_kind == "meshgrid": - pts1, pts2, pts3 = np.meshgrid(self.pts[0].flatten(), self.pts[1].flatten(), x_int3, indexing="ij") + pts1, pts2, pts3 = xp.meshgrid(self.pts[0].flatten(), self.pts[1].flatten(), x_int3, indexing="ij") mat_f[:, :, :] = fun[2](pts1, pts2, pts3) # tensor-product evaluation is done by input function @@ -1511,7 +1511,7 @@ def pi_2(self, fun, include_bc=True, eval_kind="meshgrid"): print("no internal 3D function implemented!") # compute coefficients - lambdas3 = np.zeros((self.NbaseD[0], self.NbaseD[1], self.NbaseN[2]), dtype=float) + lambdas3 = xp.zeros((self.NbaseD[0], self.NbaseD[1], self.NbaseN[2]), dtype=float) ker_loc.kernel_pi23_3d( [self.NbaseD[0], self.NbaseD[1], self.NbaseN[2]], @@ -1534,7 +1534,7 @@ def pi_2(self, fun, include_bc=True, eval_kind="meshgrid"): lambdas3, ) - return np.concatenate((lambdas1.flatten(), lambdas2.flatten(), lambdas3.flatten())) + return xp.concatenate((lambdas1.flatten(), lambdas2.flatten(), lambdas3.flatten())) # projector on space V3 (histopolation) def pi_3(self, fun, include_bc=True, eval_kind="meshgrid"): @@ -1559,7 +1559,7 @@ def pi_3(self, fun, include_bc=True, eval_kind="meshgrid"): """ # evaluation of function at quadrature points - mat_f = np.empty( + mat_f = xp.empty( (self.pts[0].flatten().size, self.pts[1].flatten().size, self.pts[2].flatten().size), dtype=float ) @@ -1567,7 +1567,7 @@ def pi_3(self, fun, include_bc=True, eval_kind="meshgrid"): if callable(fun): # create a meshgrid and evaluate function on point set if eval_kind == "meshgrid": - pts1, pts2, pts3 = np.meshgrid( + pts1, pts2, pts3 = xp.meshgrid( self.pts[0].flatten(), self.pts[1].flatten(), self.pts[2].flatten(), indexing="ij" ) mat_f[:, :, :] = fun(pts1, pts2, pts3) @@ -1590,7 +1590,7 @@ def pi_3(self, fun, include_bc=True, eval_kind="meshgrid"): print("no internal 3D function implemented!") # compute coefficients - lambdas = np.zeros((self.NbaseD[0], self.NbaseD[1], self.NbaseD[2]), dtype=float) + lambdas = xp.zeros((self.NbaseD[0], self.NbaseD[1], self.NbaseD[2]), dtype=float) ker_loc.kernel_pi3_3d( self.NbaseD, diff --git a/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_L2.py b/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_L2.py index e85dfaeb5..20814c8ac 100644 --- a/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_L2.py +++ b/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_L2.py @@ -5,12 +5,12 @@ Classes for local projectors in 1D and 3D based on quasi-spline interpolation and histopolation. """ +import cunumpy as xp import scipy.sparse as spa from psydac.ddm.mpi import mpi as MPI import struphy.feec.bsplines as bsp import struphy.feec.projectors.shape_pro_local.shape_L2_projector_kernel as ker_loc -from struphy.utils.arrays import xp as np # ======================= 3d ==================================== @@ -50,48 +50,48 @@ def __init__(self, tensor_space, p_shape, p_size, NbaseN, NbaseD, mpi_comm): self.indD = tensor_space.indD self.polar = False # local projectors for polar splines are not implemented yet - self.lambdas_0 = np.zeros((NbaseN[0], NbaseN[1], NbaseN[2]), dtype=float) - self.potential_lambdas_0 = np.zeros((NbaseN[0], NbaseN[1], NbaseN[2]), dtype=float) + self.lambdas_0 = xp.zeros((NbaseN[0], NbaseN[1], NbaseN[2]), dtype=float) + self.potential_lambdas_0 = xp.zeros((NbaseN[0], NbaseN[1], NbaseN[2]), dtype=float) - self.lambdas_1_11 = np.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) - self.lambdas_1_12 = np.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) - self.lambdas_1_13 = np.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) + self.lambdas_1_11 = xp.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) + self.lambdas_1_12 = xp.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) + self.lambdas_1_13 = xp.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) - self.lambdas_1_21 = np.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) - self.lambdas_1_22 = np.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) - self.lambdas_1_23 = np.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) + self.lambdas_1_21 = xp.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) + self.lambdas_1_22 = xp.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) + self.lambdas_1_23 = xp.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) - self.lambdas_1_31 = np.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) - self.lambdas_1_32 = np.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) - self.lambdas_1_33 = np.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) + self.lambdas_1_31 = xp.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) + self.lambdas_1_32 = xp.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) + self.lambdas_1_33 = xp.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) - self.lambdas_2_11 = np.zeros((NbaseN[0], NbaseD[1], NbaseD[2]), dtype=float) - self.lambdas_2_12 = np.zeros((NbaseD[0], NbaseN[1], NbaseD[2]), dtype=float) - self.lambdas_2_13 = np.zeros((NbaseD[0], NbaseD[1], NbaseN[2]), dtype=float) + self.lambdas_2_11 = xp.zeros((NbaseN[0], NbaseD[1], NbaseD[2]), dtype=float) + self.lambdas_2_12 = xp.zeros((NbaseD[0], NbaseN[1], NbaseD[2]), dtype=float) + self.lambdas_2_13 = xp.zeros((NbaseD[0], NbaseD[1], NbaseN[2]), dtype=float) - self.lambdas_2_21 = np.zeros((NbaseN[0], NbaseD[1], NbaseD[2]), dtype=float) - self.lambdas_2_22 = np.zeros((NbaseD[0], NbaseN[1], NbaseD[2]), dtype=float) - self.lambdas_2_23 = np.zeros((NbaseD[0], NbaseD[1], NbaseN[2]), dtype=float) + self.lambdas_2_21 = xp.zeros((NbaseN[0], NbaseD[1], NbaseD[2]), dtype=float) + self.lambdas_2_22 = xp.zeros((NbaseD[0], NbaseN[1], NbaseD[2]), dtype=float) + self.lambdas_2_23 = xp.zeros((NbaseD[0], NbaseD[1], NbaseN[2]), dtype=float) - self.lambdas_2_31 = np.zeros((NbaseN[0], NbaseD[1], NbaseD[2]), dtype=float) - self.lambdas_2_32 = np.zeros((NbaseD[0], NbaseN[1], NbaseD[2]), dtype=float) - self.lambdas_2_33 = np.zeros((NbaseD[0], NbaseD[1], NbaseN[2]), dtype=float) + self.lambdas_2_31 = xp.zeros((NbaseN[0], NbaseD[1], NbaseD[2]), dtype=float) + self.lambdas_2_32 = xp.zeros((NbaseD[0], NbaseN[1], NbaseD[2]), dtype=float) + self.lambdas_2_33 = xp.zeros((NbaseD[0], NbaseD[1], NbaseN[2]), dtype=float) - self.lambdas_3 = np.zeros((NbaseD[0], NbaseD[1], NbaseD[2]), dtype=float) + self.lambdas_3 = xp.zeros((NbaseD[0], NbaseD[1], NbaseD[2]), dtype=float) self.p_size = p_size self.p_shape = p_shape - self.related = np.zeros(3, dtype=int) + self.related = xp.zeros(3, dtype=int) for a in range(3): - # self.related[a] = int(np.floor(NbaseN[a]/2.0)) + # self.related[a] = int(xp.floor(NbaseN[a]/2.0)) self.related[a] = int( - np.floor((3 * int((self.p_size[a] * (self.p_shape[a] + 1)) * self.Nel[a] + 1) + 3 * self.p[a]) / 2.0) + xp.floor((3 * int((self.p_size[a] * (self.p_shape[a] + 1)) * self.Nel[a] + 1) + 3 * self.p[a]) / 2.0) ) if (2 * self.related[a] + 1) > NbaseN[a]: - self.related[a] = int(np.floor(NbaseN[a] / 2.0)) + self.related[a] = int(xp.floor(NbaseN[a] / 2.0)) - self.kernel_0_loc = np.zeros( + self.kernel_0_loc = xp.zeros( ( NbaseN[0], NbaseN[1], @@ -103,7 +103,7 @@ def __init__(self, tensor_space, p_shape, p_size, NbaseN, NbaseD, mpi_comm): dtype=float, ) - self.kernel_1_11_loc = np.zeros( + self.kernel_1_11_loc = xp.zeros( ( NbaseD[0], NbaseN[1], @@ -114,7 +114,7 @@ def __init__(self, tensor_space, p_shape, p_size, NbaseN, NbaseD, mpi_comm): ), dtype=float, ) - self.kernel_1_12_loc = np.zeros( + self.kernel_1_12_loc = xp.zeros( ( NbaseD[0], NbaseN[1], @@ -125,7 +125,7 @@ def __init__(self, tensor_space, p_shape, p_size, NbaseN, NbaseD, mpi_comm): ), dtype=float, ) - self.kernel_1_13_loc = np.zeros( + self.kernel_1_13_loc = xp.zeros( ( NbaseD[0], NbaseN[1], @@ -137,7 +137,7 @@ def __init__(self, tensor_space, p_shape, p_size, NbaseN, NbaseD, mpi_comm): dtype=float, ) - self.kernel_1_22_loc = np.zeros( + self.kernel_1_22_loc = xp.zeros( ( NbaseN[0], NbaseD[1], @@ -148,7 +148,7 @@ def __init__(self, tensor_space, p_shape, p_size, NbaseN, NbaseD, mpi_comm): ), dtype=float, ) - self.kernel_1_23_loc = np.zeros( + self.kernel_1_23_loc = xp.zeros( ( NbaseN[0], NbaseD[1], @@ -160,7 +160,7 @@ def __init__(self, tensor_space, p_shape, p_size, NbaseN, NbaseD, mpi_comm): dtype=float, ) - self.kernel_1_33_loc = np.zeros( + self.kernel_1_33_loc = xp.zeros( ( NbaseN[0], NbaseN[1], @@ -172,12 +172,12 @@ def __init__(self, tensor_space, p_shape, p_size, NbaseN, NbaseD, mpi_comm): dtype=float, ) - self.right_loc_1 = np.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) - self.right_loc_2 = np.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) - self.right_loc_3 = np.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) + self.right_loc_1 = xp.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) + self.right_loc_2 = xp.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) + self.right_loc_3 = xp.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) if self.mpi_rank == 0: - self.kernel_0 = np.zeros( + self.kernel_0 = xp.zeros( ( NbaseN[0], NbaseN[1], @@ -189,7 +189,7 @@ def __init__(self, tensor_space, p_shape, p_size, NbaseN, NbaseD, mpi_comm): dtype=float, ) - self.kernel_1_11 = np.zeros( + self.kernel_1_11 = xp.zeros( ( NbaseD[0], NbaseN[1], @@ -200,7 +200,7 @@ def __init__(self, tensor_space, p_shape, p_size, NbaseN, NbaseD, mpi_comm): ), dtype=float, ) - self.kernel_1_12 = np.zeros( + self.kernel_1_12 = xp.zeros( ( NbaseN[0], NbaseD[1], @@ -211,7 +211,7 @@ def __init__(self, tensor_space, p_shape, p_size, NbaseN, NbaseD, mpi_comm): ), dtype=float, ) - self.kernel_1_13 = np.zeros( + self.kernel_1_13 = xp.zeros( ( NbaseN[0], NbaseN[1], @@ -223,7 +223,7 @@ def __init__(self, tensor_space, p_shape, p_size, NbaseN, NbaseD, mpi_comm): dtype=float, ) - self.kernel_1_22 = np.zeros( + self.kernel_1_22 = xp.zeros( ( NbaseN[0], NbaseD[1], @@ -234,7 +234,7 @@ def __init__(self, tensor_space, p_shape, p_size, NbaseN, NbaseD, mpi_comm): ), dtype=float, ) - self.kernel_1_23 = np.zeros( + self.kernel_1_23 = xp.zeros( ( NbaseN[0], NbaseN[1], @@ -246,7 +246,7 @@ def __init__(self, tensor_space, p_shape, p_size, NbaseN, NbaseD, mpi_comm): dtype=float, ) - self.kernel_1_33 = np.zeros( + self.kernel_1_33 = xp.zeros( ( NbaseN[0], NbaseN[1], @@ -258,9 +258,9 @@ def __init__(self, tensor_space, p_shape, p_size, NbaseN, NbaseD, mpi_comm): dtype=float, ) - self.right_1 = np.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) - self.right_2 = np.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) - self.right_3 = np.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) + self.right_1 = xp.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) + self.right_2 = xp.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) + self.right_3 = xp.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) else: self.kernel_0 = None @@ -301,11 +301,11 @@ def assemble_0_form(self, tensor_space_FEM, mpi_comm): Nj = tensor_space_FEM.Nbase_0form # conversion to sparse matrix - indices = np.indices( + indices = xp.indices( (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) ) - shift = [np.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] + shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() @@ -358,11 +358,11 @@ def assemble_1_form(self, tensor_space_FEM): Nj = tensor_space_FEM.Nbase_1form[b] # convert to sparse matrix - indices = np.indices( + indices = xp.indices( (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) ) - shift = [np.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] + shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() @@ -384,11 +384,11 @@ def assemble_1_form(self, tensor_space_FEM): Nj = tensor_space_FEM.Nbase_1form[b] # convert to sparse matrix - indices = np.indices( + indices = xp.indices( (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) ) - shift = [np.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] + shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() @@ -410,11 +410,11 @@ def assemble_1_form(self, tensor_space_FEM): Nj = tensor_space_FEM.Nbase_1form[b] # convert to sparse matrix - indices = np.indices( + indices = xp.indices( (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) ) - shift = [np.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] + shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() @@ -436,11 +436,11 @@ def assemble_1_form(self, tensor_space_FEM): Nj = tensor_space_FEM.Nbase_1form[b] # convert to sparse matrix - indices = np.indices( + indices = xp.indices( (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) ) - shift = [np.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] + shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() @@ -462,11 +462,11 @@ def assemble_1_form(self, tensor_space_FEM): Nj = tensor_space_FEM.Nbase_1form[b] # convert to sparse matrix - indices = np.indices( + indices = xp.indices( (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) ) - shift = [np.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] + shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() @@ -488,11 +488,11 @@ def assemble_1_form(self, tensor_space_FEM): Nj = tensor_space_FEM.Nbase_1form[b] # convert to sparse matrix - indices = np.indices( + indices = xp.indices( (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) ) - shift = [np.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] + shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() @@ -510,7 +510,7 @@ def assemble_1_form(self, tensor_space_FEM): # final block matrix M = spa.bmat([[M11, M12, M13], [M12.T, M22, M23], [M13.T, M23.T, M33]], format="csr") # print('insider_check', self.kernel_1_33) - return (M, np.concatenate((self.right_1.flatten(), self.right_2.flatten(), self.right_3.flatten()))) + return (M, xp.concatenate((self.right_1.flatten(), self.right_2.flatten(), self.right_3.flatten()))) def heavy_test(self, test1, test2, test3, acc, particles_loc, Np, domain): ker_loc.kernel_1_heavy( diff --git a/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_local.py b/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_local.py index 5951e835a..7c9425e47 100644 --- a/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_local.py +++ b/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_local.py @@ -5,12 +5,12 @@ Classes for local projectors in 1D and 3D based on quasi-spline interpolation and histopolation. """ +import cunumpy as xp import scipy.sparse as spa from psydac.ddm.mpi import mpi as MPI import struphy.feec.bsplines as bsp import struphy.feec.projectors.shape_pro_local.shape_local_projector_kernel as ker_loc -from struphy.utils.arrays import xp as np # ======================= 3d ==================================== @@ -51,48 +51,48 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co self.polar = False # local projectors for polar splines are not implemented yet - self.lambdas_0 = np.zeros((NbaseN[0], NbaseN[1], NbaseN[2]), dtype=float) - self.potential_lambdas_0 = np.zeros((NbaseN[0], NbaseN[1], NbaseN[2]), dtype=float) + self.lambdas_0 = xp.zeros((NbaseN[0], NbaseN[1], NbaseN[2]), dtype=float) + self.potential_lambdas_0 = xp.zeros((NbaseN[0], NbaseN[1], NbaseN[2]), dtype=float) - self.lambdas_1_11 = np.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) - self.lambdas_1_12 = np.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) - self.lambdas_1_13 = np.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) + self.lambdas_1_11 = xp.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) + self.lambdas_1_12 = xp.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) + self.lambdas_1_13 = xp.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) - self.lambdas_1_21 = np.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) - self.lambdas_1_22 = np.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) - self.lambdas_1_23 = np.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) + self.lambdas_1_21 = xp.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) + self.lambdas_1_22 = xp.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) + self.lambdas_1_23 = xp.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) - self.lambdas_1_31 = np.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) - self.lambdas_1_32 = np.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) - self.lambdas_1_33 = np.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) + self.lambdas_1_31 = xp.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) + self.lambdas_1_32 = xp.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) + self.lambdas_1_33 = xp.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) - self.lambdas_2_11 = np.zeros((NbaseN[0], NbaseD[1], NbaseD[2]), dtype=float) - self.lambdas_2_12 = np.zeros((NbaseD[0], NbaseN[1], NbaseD[2]), dtype=float) - self.lambdas_2_13 = np.zeros((NbaseD[0], NbaseD[1], NbaseN[2]), dtype=float) + self.lambdas_2_11 = xp.zeros((NbaseN[0], NbaseD[1], NbaseD[2]), dtype=float) + self.lambdas_2_12 = xp.zeros((NbaseD[0], NbaseN[1], NbaseD[2]), dtype=float) + self.lambdas_2_13 = xp.zeros((NbaseD[0], NbaseD[1], NbaseN[2]), dtype=float) - self.lambdas_2_21 = np.zeros((NbaseN[0], NbaseD[1], NbaseD[2]), dtype=float) - self.lambdas_2_22 = np.zeros((NbaseD[0], NbaseN[1], NbaseD[2]), dtype=float) - self.lambdas_2_23 = np.zeros((NbaseD[0], NbaseD[1], NbaseN[2]), dtype=float) + self.lambdas_2_21 = xp.zeros((NbaseN[0], NbaseD[1], NbaseD[2]), dtype=float) + self.lambdas_2_22 = xp.zeros((NbaseD[0], NbaseN[1], NbaseD[2]), dtype=float) + self.lambdas_2_23 = xp.zeros((NbaseD[0], NbaseD[1], NbaseN[2]), dtype=float) - self.lambdas_2_31 = np.zeros((NbaseN[0], NbaseD[1], NbaseD[2]), dtype=float) - self.lambdas_2_32 = np.zeros((NbaseD[0], NbaseN[1], NbaseD[2]), dtype=float) - self.lambdas_2_33 = np.zeros((NbaseD[0], NbaseD[1], NbaseN[2]), dtype=float) + self.lambdas_2_31 = xp.zeros((NbaseN[0], NbaseD[1], NbaseD[2]), dtype=float) + self.lambdas_2_32 = xp.zeros((NbaseD[0], NbaseN[1], NbaseD[2]), dtype=float) + self.lambdas_2_33 = xp.zeros((NbaseD[0], NbaseD[1], NbaseN[2]), dtype=float) - self.lambdas_3 = np.zeros((NbaseD[0], NbaseD[1], NbaseD[2]), dtype=float) + self.lambdas_3 = xp.zeros((NbaseD[0], NbaseD[1], NbaseD[2]), dtype=float) self.p_size = p_size self.p_shape = p_shape - self.related = np.zeros(3, dtype=int) + self.related = xp.zeros(3, dtype=int) for a in range(3): - # self.related[a] = int(np.floor(NbaseN[a]/2.0)) + # self.related[a] = int(xp.floor(NbaseN[a]/2.0)) self.related[a] = int( - np.floor((3 * int((self.p_size[a] * (self.p_shape[a] + 1)) * self.Nel[a] + 1) + 3 * self.p[a]) / 2.0) + xp.floor((3 * int((self.p_size[a] * (self.p_shape[a] + 1)) * self.Nel[a] + 1) + 3 * self.p[a]) / 2.0) ) if (2 * self.related[a] + 1) > NbaseN[a]: - self.related[a] = int(np.floor(NbaseN[a] / 2.0)) + self.related[a] = int(xp.floor(NbaseN[a] / 2.0)) - self.kernel_0_loc = np.zeros( + self.kernel_0_loc = xp.zeros( ( NbaseN[0], NbaseN[1], @@ -104,7 +104,7 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co dtype=float, ) - self.kernel_1_11_loc = np.zeros( + self.kernel_1_11_loc = xp.zeros( ( NbaseD[0], NbaseN[1], @@ -115,7 +115,7 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co ), dtype=float, ) - self.kernel_1_12_loc = np.zeros( + self.kernel_1_12_loc = xp.zeros( ( NbaseD[0], NbaseN[1], @@ -126,7 +126,7 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co ), dtype=float, ) - self.kernel_1_13_loc = np.zeros( + self.kernel_1_13_loc = xp.zeros( ( NbaseD[0], NbaseN[1], @@ -138,7 +138,7 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co dtype=float, ) - self.kernel_1_22_loc = np.zeros( + self.kernel_1_22_loc = xp.zeros( ( NbaseN[0], NbaseD[1], @@ -149,7 +149,7 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co ), dtype=float, ) - self.kernel_1_23_loc = np.zeros( + self.kernel_1_23_loc = xp.zeros( ( NbaseN[0], NbaseD[1], @@ -161,7 +161,7 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co dtype=float, ) - self.kernel_1_33_loc = np.zeros( + self.kernel_1_33_loc = xp.zeros( ( NbaseN[0], NbaseN[1], @@ -173,12 +173,12 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co dtype=float, ) - self.right_loc_1 = np.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) - self.right_loc_2 = np.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) - self.right_loc_3 = np.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) + self.right_loc_1 = xp.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) + self.right_loc_2 = xp.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) + self.right_loc_3 = xp.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) if self.mpi_rank == 0: - self.kernel_0 = np.zeros( + self.kernel_0 = xp.zeros( ( NbaseN[0], NbaseN[1], @@ -190,7 +190,7 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co dtype=float, ) - self.kernel_1_11 = np.zeros( + self.kernel_1_11 = xp.zeros( ( NbaseD[0], NbaseN[1], @@ -201,7 +201,7 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co ), dtype=float, ) - self.kernel_1_12 = np.zeros( + self.kernel_1_12 = xp.zeros( ( NbaseN[0], NbaseD[1], @@ -212,7 +212,7 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co ), dtype=float, ) - self.kernel_1_13 = np.zeros( + self.kernel_1_13 = xp.zeros( ( NbaseN[0], NbaseN[1], @@ -224,7 +224,7 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co dtype=float, ) - self.kernel_1_22 = np.zeros( + self.kernel_1_22 = xp.zeros( ( NbaseN[0], NbaseD[1], @@ -235,7 +235,7 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co ), dtype=float, ) - self.kernel_1_23 = np.zeros( + self.kernel_1_23 = xp.zeros( ( NbaseN[0], NbaseN[1], @@ -247,7 +247,7 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co dtype=float, ) - self.kernel_1_33 = np.zeros( + self.kernel_1_33 = xp.zeros( ( NbaseN[0], NbaseN[1], @@ -259,9 +259,9 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co dtype=float, ) - self.right_1 = np.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) - self.right_2 = np.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) - self.right_3 = np.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) + self.right_1 = xp.zeros((NbaseD[0], NbaseN[1], NbaseN[2]), dtype=float) + self.right_2 = xp.zeros((NbaseN[0], NbaseD[1], NbaseN[2]), dtype=float) + self.right_3 = xp.zeros((NbaseN[0], NbaseN[1], NbaseD[2]), dtype=float) else: self.kernel_0 = None @@ -279,7 +279,7 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co self.right_2 = None self.right_3 = None - self.num_cell = np.empty(3, dtype=int) + self.num_cell = xp.empty(3, dtype=int) for i in range(3): if self.p[i] == 1: self.num_cell[i] = 1 @@ -287,8 +287,8 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co self.num_cell[i] = 2 # Gauss - Legendre quadrature points and weights in (-1, 1) - self.pts_loc = [np.polynomial.legendre.leggauss(n_quad)[0] for n_quad in self.n_quad] - self.wts_loc = [np.polynomial.legendre.leggauss(n_quad)[1] for n_quad in self.n_quad] + self.pts_loc = [xp.polynomial.legendre.leggauss(n_quad)[0] for n_quad in self.n_quad] + self.wts_loc = [xp.polynomial.legendre.leggauss(n_quad)[1] for n_quad in self.n_quad] self.pts = [0, 0, 0] self.wts = [0, 0, 0] @@ -303,78 +303,78 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co self.coeff_h = [0, 0, 0] for a in range(3): if self.bc[a] == True: - self.coeff_i[a] = np.zeros(2 * self.p[a], dtype=float) - self.coeff_h[a] = np.zeros(2 * self.p[a], dtype=float) + self.coeff_i[a] = xp.zeros(2 * self.p[a], dtype=float) + self.coeff_h[a] = xp.zeros(2 * self.p[a], dtype=float) if self.p[a] == 1: - self.coeff_i[a][:] = np.array([1.0, 0.0]) - self.coeff_h[a][:] = np.array([1.0, 1.0]) + self.coeff_i[a][:] = xp.array([1.0, 0.0]) + self.coeff_h[a][:] = xp.array([1.0, 1.0]) elif self.p[a] == 2: - self.coeff_i[a][:] = 1 / 2 * np.array([-1.0, 4.0, -1.0, 0.0]) - self.coeff_h[a][:] = 1 / 2 * np.array([-1.0, 3.0, 3.0, -1.0]) + self.coeff_i[a][:] = 1 / 2 * xp.array([-1.0, 4.0, -1.0, 0.0]) + self.coeff_h[a][:] = 1 / 2 * xp.array([-1.0, 3.0, 3.0, -1.0]) elif self.p[a] == 3: - self.coeff_i[a][:] = 1 / 6 * np.array([1.0, -8.0, 20.0, -8.0, 1.0, 0.0]) - self.coeff_h[a][:] = 1 / 6 * np.array([1.0, -7.0, 12.0, 12.0, -7.0, 1.0]) + self.coeff_i[a][:] = 1 / 6 * xp.array([1.0, -8.0, 20.0, -8.0, 1.0, 0.0]) + self.coeff_h[a][:] = 1 / 6 * xp.array([1.0, -7.0, 12.0, 12.0, -7.0, 1.0]) elif self.p[a] == 4: - self.coeff_i[a][:] = 2 / 45 * np.array([-1.0, 16.0, -295 / 4, 140.0, -295 / 4, 16.0, -1.0, 0.0]) + self.coeff_i[a][:] = 2 / 45 * xp.array([-1.0, 16.0, -295 / 4, 140.0, -295 / 4, 16.0, -1.0, 0.0]) self.coeff_h[a][:] = ( - 2 / 45 * np.array([-1.0, 15.0, -231 / 4, 265 / 4, 265 / 4, -231 / 4, 15.0, -1.0]) + 2 / 45 * xp.array([-1.0, 15.0, -231 / 4, 265 / 4, 265 / 4, -231 / 4, 15.0, -1.0]) ) else: print("degree > 4 not implemented!") else: - self.coeff_i[a] = np.zeros((2 * self.p[a] - 1, 2 * self.p[a] - 1), dtype=float) - self.coeff_h[a] = np.zeros((2 * self.p[a] - 1, 2 * self.p[a]), dtype=float) + self.coeff_i[a] = xp.zeros((2 * self.p[a] - 1, 2 * self.p[a] - 1), dtype=float) + self.coeff_h[a] = xp.zeros((2 * self.p[a] - 1, 2 * self.p[a]), dtype=float) if self.p[a] == 1: - self.coeff_i[a][0, :] = np.array([1.0]) - self.coeff_h[a][0, :] = np.array([1.0, 1.0]) + self.coeff_i[a][0, :] = xp.array([1.0]) + self.coeff_h[a][0, :] = xp.array([1.0, 1.0]) elif self.p[a] == 2: - self.coeff_i[a][0, :] = 1 / 2 * np.array([2.0, 0.0, 0.0]) - self.coeff_i[a][1, :] = 1 / 2 * np.array([-1.0, 4.0, -1.0]) - self.coeff_i[a][2, :] = 1 / 2 * np.array([0.0, 0.0, 2.0]) + self.coeff_i[a][0, :] = 1 / 2 * xp.array([2.0, 0.0, 0.0]) + self.coeff_i[a][1, :] = 1 / 2 * xp.array([-1.0, 4.0, -1.0]) + self.coeff_i[a][2, :] = 1 / 2 * xp.array([0.0, 0.0, 2.0]) - self.coeff_h[a][0, :] = 1 / 2 * np.array([3.0, -1.0, 0.0, 0.0]) - self.coeff_h[a][1, :] = 1 / 2 * np.array([-1.0, 3.0, 3.0, -1.0]) - self.coeff_h[a][2, :] = 1 / 2 * np.array([0.0, 0.0, -1.0, 3.0]) + self.coeff_h[a][0, :] = 1 / 2 * xp.array([3.0, -1.0, 0.0, 0.0]) + self.coeff_h[a][1, :] = 1 / 2 * xp.array([-1.0, 3.0, 3.0, -1.0]) + self.coeff_h[a][2, :] = 1 / 2 * xp.array([0.0, 0.0, -1.0, 3.0]) elif self.p[a] == 3: - self.coeff_i[a][0, :] = 1 / 18 * np.array([18.0, 0.0, 0.0, 0.0, 0.0]) - self.coeff_i[a][1, :] = 1 / 18 * np.array([-5.0, 40.0, -24.0, 8.0, -1.0]) - self.coeff_i[a][2, :] = 1 / 18 * np.array([3.0, -24.0, 60.0, -24.0, 3.0]) - self.coeff_i[a][3, :] = 1 / 18 * np.array([-1.0, 8.0, -24.0, 40.0, -5.0]) - self.coeff_i[a][4, :] = 1 / 18 * np.array([0.0, 0.0, 0.0, 0.0, 18.0]) - - self.coeff_h[a][0, :] = 1 / 18 * np.array([23.0, -17.0, 7.0, -1.0, 0.0, 0.0]) - self.coeff_h[a][1, :] = 1 / 18 * np.array([-8.0, 56.0, -28.0, 4.0, 0.0, 0.0]) - self.coeff_h[a][2, :] = 1 / 18 * np.array([3.0, -21.0, 36.0, 36.0, -21.0, 3.0]) - self.coeff_h[a][3, :] = 1 / 18 * np.array([0.0, 0.0, 4.0, -28.0, 56.0, -8.0]) - self.coeff_h[a][4, :] = 1 / 18 * np.array([0.0, 0.0, -1.0, 7.0, -17.0, 23.0]) + self.coeff_i[a][0, :] = 1 / 18 * xp.array([18.0, 0.0, 0.0, 0.0, 0.0]) + self.coeff_i[a][1, :] = 1 / 18 * xp.array([-5.0, 40.0, -24.0, 8.0, -1.0]) + self.coeff_i[a][2, :] = 1 / 18 * xp.array([3.0, -24.0, 60.0, -24.0, 3.0]) + self.coeff_i[a][3, :] = 1 / 18 * xp.array([-1.0, 8.0, -24.0, 40.0, -5.0]) + self.coeff_i[a][4, :] = 1 / 18 * xp.array([0.0, 0.0, 0.0, 0.0, 18.0]) + + self.coeff_h[a][0, :] = 1 / 18 * xp.array([23.0, -17.0, 7.0, -1.0, 0.0, 0.0]) + self.coeff_h[a][1, :] = 1 / 18 * xp.array([-8.0, 56.0, -28.0, 4.0, 0.0, 0.0]) + self.coeff_h[a][2, :] = 1 / 18 * xp.array([3.0, -21.0, 36.0, 36.0, -21.0, 3.0]) + self.coeff_h[a][3, :] = 1 / 18 * xp.array([0.0, 0.0, 4.0, -28.0, 56.0, -8.0]) + self.coeff_h[a][4, :] = 1 / 18 * xp.array([0.0, 0.0, -1.0, 7.0, -17.0, 23.0]) elif self.p[a] == 4: - self.coeff_i[a][0, :] = 1 / 360 * np.array([360.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) - self.coeff_i[a][1, :] = 1 / 360 * np.array([-59.0, 944.0, -1000.0, 720.0, -305.0, 64.0, -4.0]) - self.coeff_i[a][2, :] = 1 / 360 * np.array([23.0, -368.0, 1580.0, -1360.0, 605.0, -128.0, 8.0]) - self.coeff_i[a][3, :] = 1 / 360 * np.array([-16.0, 256.0, -1180.0, 2240.0, -1180.0, 256.0, -16.0]) - self.coeff_i[a][4, :] = 1 / 360 * np.array([8.0, -128.0, 605.0, -1360.0, 1580.0, -368.0, 23.0]) - self.coeff_i[a][5, :] = 1 / 360 * np.array([-4.0, 64.0, -305.0, 720.0, -1000.0, 944.0, -59.0]) - self.coeff_i[a][6, :] = 1 / 360 * np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 360.0]) - - self.coeff_h[a][0, :] = 1 / 360 * np.array([419.0, -525.0, 475.0, -245.0, 60.0, -4.0, 0.0, 0.0]) - self.coeff_h[a][1, :] = 1 / 360 * np.array([-82.0, 1230.0, -1350.0, 730.0, -180.0, 12.0, 0.0, 0.0]) - self.coeff_h[a][2, :] = 1 / 360 * np.array([39.0, -585.0, 2175.0, -1425.0, 360.0, -24.0, 0.0, 0.0]) + self.coeff_i[a][0, :] = 1 / 360 * xp.array([360.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + self.coeff_i[a][1, :] = 1 / 360 * xp.array([-59.0, 944.0, -1000.0, 720.0, -305.0, 64.0, -4.0]) + self.coeff_i[a][2, :] = 1 / 360 * xp.array([23.0, -368.0, 1580.0, -1360.0, 605.0, -128.0, 8.0]) + self.coeff_i[a][3, :] = 1 / 360 * xp.array([-16.0, 256.0, -1180.0, 2240.0, -1180.0, 256.0, -16.0]) + self.coeff_i[a][4, :] = 1 / 360 * xp.array([8.0, -128.0, 605.0, -1360.0, 1580.0, -368.0, 23.0]) + self.coeff_i[a][5, :] = 1 / 360 * xp.array([-4.0, 64.0, -305.0, 720.0, -1000.0, 944.0, -59.0]) + self.coeff_i[a][6, :] = 1 / 360 * xp.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 360.0]) + + self.coeff_h[a][0, :] = 1 / 360 * xp.array([419.0, -525.0, 475.0, -245.0, 60.0, -4.0, 0.0, 0.0]) + self.coeff_h[a][1, :] = 1 / 360 * xp.array([-82.0, 1230.0, -1350.0, 730.0, -180.0, 12.0, 0.0, 0.0]) + self.coeff_h[a][2, :] = 1 / 360 * xp.array([39.0, -585.0, 2175.0, -1425.0, 360.0, -24.0, 0.0, 0.0]) self.coeff_h[a][3, :] = ( - 1 / 360 * np.array([-16.0, 240.0, -924.0, 1060.0, 1060.0, -924.0, 240.0, -16.0]) + 1 / 360 * xp.array([-16.0, 240.0, -924.0, 1060.0, 1060.0, -924.0, 240.0, -16.0]) ) - self.coeff_h[a][4, :] = 1 / 360 * np.array([0.0, 0.0, -24.0, 360.0, -1425.0, 2175.0, -585.0, 39.0]) - self.coeff_h[a][5, :] = 1 / 360 * np.array([0.0, 0.0, 12.0, -180.0, 730.0, -1350.0, 1230.0, -82.0]) - self.coeff_h[a][6, :] = 1 / 360 * np.array([0.0, 0.0, -4.0, 60.0, -245.0, 475.0, -525.0, 419.0]) + self.coeff_h[a][4, :] = 1 / 360 * xp.array([0.0, 0.0, -24.0, 360.0, -1425.0, 2175.0, -585.0, 39.0]) + self.coeff_h[a][5, :] = 1 / 360 * xp.array([0.0, 0.0, 12.0, -180.0, 730.0, -1350.0, 1230.0, -82.0]) + self.coeff_h[a][6, :] = 1 / 360 * xp.array([0.0, 0.0, -4.0, 60.0, -245.0, 475.0, -525.0, 419.0]) else: print("degree > 4 not implemented!") @@ -402,11 +402,11 @@ def assemble_0_form(self, tensor_space_FEM, mpi_comm): Nj = tensor_space_FEM.Nbase_0form # conversion to sparse matrix - indices = np.indices( + indices = xp.indices( (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) ) - shift = [np.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] + shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() @@ -459,11 +459,11 @@ def assemble_1_form(self, tensor_space_FEM): Nj = tensor_space_FEM.Nbase_1form[b] # convert to sparse matrix - indices = np.indices( + indices = xp.indices( (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) ) - shift = [np.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] + shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() @@ -485,11 +485,11 @@ def assemble_1_form(self, tensor_space_FEM): Nj = tensor_space_FEM.Nbase_1form[b] # convert to sparse matrix - indices = np.indices( + indices = xp.indices( (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) ) - shift = [np.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] + shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() @@ -511,11 +511,11 @@ def assemble_1_form(self, tensor_space_FEM): Nj = tensor_space_FEM.Nbase_1form[b] # convert to sparse matrix - indices = np.indices( + indices = xp.indices( (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) ) - shift = [np.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] + shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() @@ -537,11 +537,11 @@ def assemble_1_form(self, tensor_space_FEM): Nj = tensor_space_FEM.Nbase_1form[b] # convert to sparse matrix - indices = np.indices( + indices = xp.indices( (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) ) - shift = [np.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] + shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() @@ -563,11 +563,11 @@ def assemble_1_form(self, tensor_space_FEM): Nj = tensor_space_FEM.Nbase_1form[b] # convert to sparse matrix - indices = np.indices( + indices = xp.indices( (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) ) - shift = [np.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] + shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() @@ -589,11 +589,11 @@ def assemble_1_form(self, tensor_space_FEM): Nj = tensor_space_FEM.Nbase_1form[b] # convert to sparse matrix - indices = np.indices( + indices = xp.indices( (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) ) - shift = [np.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] + shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() @@ -611,7 +611,7 @@ def assemble_1_form(self, tensor_space_FEM): # final block matrix M = spa.bmat([[M11, M12, M13], [M12.T, M22, M23], [M13.T, M23.T, M33]], format="csr") # print('insider_check', self.kernel_1_33) - return (M, np.concatenate((self.right_1.flatten(), self.right_2.flatten(), self.right_3.flatten()))) + return (M, xp.concatenate((self.right_1.flatten(), self.right_2.flatten(), self.right_3.flatten()))) def heavy_test(self, test1, test2, test3, acc, particles_loc, Np, domain): ker_loc.kernel_1_heavy( diff --git a/src/struphy/eigenvalue_solvers/mass_matrices_1d.py b/src/struphy/eigenvalue_solvers/mass_matrices_1d.py index 99316215d..b5013c088 100644 --- a/src/struphy/eigenvalue_solvers/mass_matrices_1d.py +++ b/src/struphy/eigenvalue_solvers/mass_matrices_1d.py @@ -2,10 +2,10 @@ # # Copyright 2020 Florian Holderied +import cunumpy as xp import scipy.sparse as spa import struphy.bsplines.bsplines as bsp -from struphy.utils.arrays import xp as np # ======= mass matrices in 1D ==================== @@ -47,7 +47,7 @@ def get_M(spline_space, phi_i=0, phi_j=0, fun=None): # evaluation of weight function at quadrature points (optional) if fun == None: - mat_fun = np.ones(pts.shape, dtype=float) + mat_fun = xp.ones(pts.shape, dtype=float) else: mat_fun = fun(pts.flatten()).reshape(Nel, n_quad) @@ -74,7 +74,7 @@ def get_M(spline_space, phi_i=0, phi_j=0, fun=None): bj = basisD[:, :, 0, :] # matrix assembly - M = np.zeros((Ni, 2 * p + 1), dtype=float) + M = xp.zeros((Ni, 2 * p + 1), dtype=float) for ie in range(Nel): for il in range(p + 1 - ni): @@ -86,8 +86,8 @@ def get_M(spline_space, phi_i=0, phi_j=0, fun=None): M[(ie + il) % Ni, p + jl - il] += value - indices = np.indices((Ni, 2 * p + 1)) - shift = np.arange(Ni) - p + indices = xp.indices((Ni, 2 * p + 1)) + shift = xp.arange(Ni) - p row = indices[0].flatten() col = (indices[1] + shift[:, None]) % Nj @@ -137,13 +137,13 @@ def get_M_gen(spline_space, phi_i=0, phi_j=0, fun=None, jac=None): # evaluation of weight function at quadrature points (optional) if fun == None: - mat_fun = np.ones(pts.shape, dtype=float) + mat_fun = xp.ones(pts.shape, dtype=float) else: mat_fun = fun(pts.flatten()).reshape(Nel, n_quad) # evaluation of jacobian at quadrature points if jac == None: - mat_jac = np.ones(pts.shape, dtype=float) + mat_jac = xp.ones(pts.shape, dtype=float) else: mat_jac = jac(pts.flatten()).reshape(Nel, n_quad) @@ -180,7 +180,7 @@ def get_M_gen(spline_space, phi_i=0, phi_j=0, fun=None, jac=None): bj = basis_t[:, :, 0, :] # matrix assembly - M = np.zeros((Ni, 2 * p + 1), dtype=float) + M = xp.zeros((Ni, 2 * p + 1), dtype=float) for ie in range(Nel): for il in range(p + 1 - ni): @@ -192,8 +192,8 @@ def get_M_gen(spline_space, phi_i=0, phi_j=0, fun=None, jac=None): M[(ie + il) % Ni, p + jl - il] += value - indices = np.indices((Ni, 2 * p + 1)) - shift = np.arange(Ni) - p + indices = xp.indices((Ni, 2 * p + 1)) + shift = xp.arange(Ni) - p row = indices[0].flatten() col = (indices[1] + shift[:, None]) % Nj @@ -235,11 +235,11 @@ def test_M(spline_space, phi_i=0, phi_j=0, fun=lambda eta: 1.0, jac=lambda eta: bj = lambda eta: spline_space.evaluate_D(eta, cj) / spline_space.Nel # coefficients - ci = np.zeros(Ni, dtype=float) - cj = np.zeros(Nj, dtype=float) + ci = xp.zeros(Ni, dtype=float) + cj = xp.zeros(Nj, dtype=float) # integration - M = np.zeros((Ni, Nj), dtype=float) + M = xp.zeros((Ni, Nj), dtype=float) for i in range(Ni): for j in range(Nj): diff --git a/src/struphy/eigenvalue_solvers/mass_matrices_2d.py b/src/struphy/eigenvalue_solvers/mass_matrices_2d.py index e19b31ac6..c2c6c3ae9 100644 --- a/src/struphy/eigenvalue_solvers/mass_matrices_2d.py +++ b/src/struphy/eigenvalue_solvers/mass_matrices_2d.py @@ -2,10 +2,10 @@ # # Copyright 2020 Florian Holderied +import cunumpy as xp import scipy.sparse as spa import struphy.eigenvalue_solvers.kernels_2d as ker -from struphy.utils.arrays import xp as np # ================ mass matrix in V0 =========================== @@ -46,7 +46,7 @@ def get_M0(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): # evaluation of weight function at quadrature points if weight == None: - mat_w = np.ones(det_df.shape, dtype=float) + mat_w = xp.ones(det_df.shape, dtype=float) else: mat_w = weight(pts[0].flatten(), pts[1].flatten(), 0.0) mat_w = mat_w.reshape(Nel[0], n_quad[0], Nel[1], n_quad[1]) @@ -55,14 +55,14 @@ def get_M0(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): Ni = tensor_space_FEM.Nbase_0form Nj = tensor_space_FEM.Nbase_0form - M = np.zeros((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1), dtype=float) + M = xp.zeros((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1), dtype=float) ker.kernel_mass( - np.array(Nel), - np.array(p), - np.array(n_quad), - np.array([0, 0]), - np.array([0, 0]), + xp.array(Nel), + xp.array(p), + xp.array(n_quad), + xp.array([0, 0]), + xp.array([0, 0]), wts[0], wts[1], basisN[0], @@ -76,9 +76,9 @@ def get_M0(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): ) # conversion to sparse matrix - indices = np.indices((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1)) + indices = xp.indices((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1)) - shift = [np.arange(Ni) - p for Ni, p in zip(Ni, p)] + shift = [xp.arange(Ni) - p for Ni, p in zip(Ni, p)] row = (Ni[1] * indices[0] + indices[1]).flatten() @@ -156,7 +156,7 @@ def get_M1(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): Ni = tensor_space_FEM.Nbase_1form[a] Nj = tensor_space_FEM.Nbase_1form[b] - M[a][b] = np.zeros((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1), dtype=float) + M[a][b] = xp.zeros((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1), dtype=float) # evaluate inverse metric tensor at quadrature points if weight == None: @@ -167,13 +167,13 @@ def get_M1(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): mat_w = mat_w.reshape(Nel[0], n_quad[0], Nel[1], n_quad[1]) # assemble block if weight is not zero - if np.any(mat_w): + if xp.any(mat_w): ker.kernel_mass( - np.array(Nel), - np.array(p), - np.array(n_quad), - np.array(ns[a]), - np.array(ns[b]), + xp.array(Nel), + xp.array(p), + xp.array(n_quad), + xp.array(ns[a]), + xp.array(ns[b]), wts[0], wts[1], basis[a][0], @@ -187,9 +187,9 @@ def get_M1(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): ) # convert to sparse matrix - indices = np.indices((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1)) + indices = xp.indices((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1)) - shift = [np.arange(Ni) - p for Ni, p in zip(Ni, p)] + shift = [xp.arange(Ni) - p for Ni, p in zip(Ni, p)] row = (Ni[1] * indices[0] + indices[1]).flatten() @@ -272,7 +272,7 @@ def get_M2(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): Ni = tensor_space_FEM.Nbase_2form[a] Nj = tensor_space_FEM.Nbase_2form[b] - M[a][b] = np.zeros((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1), dtype=float) + M[a][b] = xp.zeros((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1), dtype=float) # evaluate metric tensor at quadrature points if weight == None: @@ -283,13 +283,13 @@ def get_M2(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): mat_w = mat_w.reshape(Nel[0], n_quad[0], Nel[1], n_quad[1]) # assemble block if weight is not zero - if np.any(mat_w): + if xp.any(mat_w): ker.kernel_mass( - np.array(Nel), - np.array(p), - np.array(n_quad), - np.array(ns[a]), - np.array(ns[b]), + xp.array(Nel), + xp.array(p), + xp.array(n_quad), + xp.array(ns[a]), + xp.array(ns[b]), wts[0], wts[1], basis[a][0], @@ -303,9 +303,9 @@ def get_M2(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): ) # convert to sparse matrix - indices = np.indices((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1)) + indices = xp.indices((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1)) - shift = [np.arange(Ni) - p for Ni, p in zip(Ni, p)] + shift = [xp.arange(Ni) - p for Ni, p in zip(Ni, p)] row = (Ni[1] * indices[0] + indices[1]).flatten() @@ -369,7 +369,7 @@ def get_M3(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): # evaluation of weight function at quadrature points if weight == None: - mat_w = np.ones(det_df.shape, dtype=float) + mat_w = xp.ones(det_df.shape, dtype=float) else: mat_w = weight(pts[0].flatten(), pts[1].flatten(), 0.0) mat_w = mat_w.reshape(Nel[0], n_quad[0], Nel[1], n_quad[1]) @@ -378,14 +378,14 @@ def get_M3(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): Ni = tensor_space_FEM.Nbase_3form Nj = tensor_space_FEM.Nbase_3form - M = np.zeros((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1), dtype=float) + M = xp.zeros((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1), dtype=float) ker.kernel_mass( - np.array(Nel), - np.array(p), - np.array(n_quad), - np.array([1, 1]), - np.array([1, 1]), + xp.array(Nel), + xp.array(p), + xp.array(n_quad), + xp.array([1, 1]), + xp.array([1, 1]), wts[0], wts[1], basisD[0], @@ -399,9 +399,9 @@ def get_M3(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): ) # conversion to sparse matrix - indices = np.indices((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1)) + indices = xp.indices((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1)) - shift = [np.arange(Ni) - p for Ni, p in zip(Ni, p)] + shift = [xp.arange(Ni) - p for Ni, p in zip(Ni, p)] row = (Ni[1] * indices[0] + indices[1]).flatten() @@ -475,7 +475,7 @@ def get_Mv(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): Ni = tensor_space_FEM.Nbase_0form Nj = tensor_space_FEM.Nbase_0form - M[a][b] = np.zeros((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1), dtype=float) + M[a][b] = xp.zeros((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1), dtype=float) # evaluate metric tensor at quadrature points if weight == None: @@ -486,13 +486,13 @@ def get_Mv(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): mat_w = mat_w.reshape(Nel[0], n_quad[0], Nel[1], n_quad[1]) # assemble block if weight is not zero - if np.any(mat_w): + if xp.any(mat_w): ker.kernel_mass( - np.array(Nel), - np.array(p), - np.array(n_quad), - np.array(ns[a]), - np.array(ns[b]), + xp.array(Nel), + xp.array(p), + xp.array(n_quad), + xp.array(ns[a]), + xp.array(ns[b]), wts[0], wts[1], basis[a][0], @@ -506,9 +506,9 @@ def get_Mv(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): ) # convert to sparse matrix - indices = np.indices((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1)) + indices = xp.indices((Ni[0], Ni[1], 2 * p[0] + 1, 2 * p[1] + 1)) - shift = [np.arange(Ni) - p for Ni, p in zip(Ni, p)] + shift = [xp.arange(Ni) - p for Ni, p in zip(Ni, p)] row = (Ni[1] * indices[0] + indices[1]).flatten() diff --git a/src/struphy/eigenvalue_solvers/mass_matrices_3d.py b/src/struphy/eigenvalue_solvers/mass_matrices_3d.py index ef6ee1e0c..05f019e13 100644 --- a/src/struphy/eigenvalue_solvers/mass_matrices_3d.py +++ b/src/struphy/eigenvalue_solvers/mass_matrices_3d.py @@ -2,10 +2,10 @@ # # Copyright 2020 Florian Holderied (florian.holderied@ipp.mpg.de) +import cunumpy as xp import scipy.sparse as spa import struphy.eigenvalue_solvers.kernels_3d as ker -from struphy.utils.arrays import xp as np # ================ mass matrix in V0 =========================== @@ -46,7 +46,7 @@ def get_M0(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): # evaluation of weight function at quadrature points if weight == None: - mat_w = np.ones(det_df.shape, dtype=float) + mat_w = xp.ones(det_df.shape, dtype=float) else: mat_w = weight(pts[0].flatten(), pts[1].flatten(), pts[2].flatten()) mat_w = mat_w.reshape(Nel[0], n_quad[0], Nel[1], n_quad[1], Nel[2], n_quad[2]) @@ -55,14 +55,14 @@ def get_M0(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): Ni = tensor_space_FEM.Nbase_0form Nj = tensor_space_FEM.Nbase_0form - M = np.zeros((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1), dtype=float) + M = xp.zeros((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1), dtype=float) ker.kernel_mass( - np.array(Nel), - np.array(p), - np.array(n_quad), - np.array([0, 0, 0]), - np.array([0, 0, 0]), + xp.array(Nel), + xp.array(p), + xp.array(n_quad), + xp.array([0, 0, 0]), + xp.array([0, 0, 0]), wts[0], wts[1], wts[2], @@ -80,9 +80,9 @@ def get_M0(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): ) # conversion to sparse matrix - indices = np.indices((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1)) + indices = xp.indices((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1)) - shift = [np.arange(Ni) - p for Ni, p in zip(Ni, p)] + shift = [xp.arange(Ni) - p for Ni, p in zip(Ni, p)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() @@ -161,7 +161,7 @@ def get_M1(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): Ni = tensor_space_FEM.Nbase_1form[a] Nj = tensor_space_FEM.Nbase_1form[b] - M[a][b] = np.zeros((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1), dtype=float) + M[a][b] = xp.zeros((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1), dtype=float) # evaluate metric tensor at quadrature points if weight == None: @@ -172,13 +172,13 @@ def get_M1(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): mat_w = mat_w.reshape(Nel[0], n_quad[0], Nel[1], n_quad[1], Nel[2], n_quad[2]) # assemble block if weight is not zero - if np.any(mat_w): + if xp.any(mat_w): ker.kernel_mass( - np.array(Nel), - np.array(p), - np.array(n_quad), - np.array(ns[a]), - np.array(ns[b]), + xp.array(Nel), + xp.array(p), + xp.array(n_quad), + xp.array(ns[a]), + xp.array(ns[b]), wts[0], wts[1], wts[2], @@ -196,9 +196,9 @@ def get_M1(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): ) # convert to sparse matrix - indices = np.indices((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1)) + indices = xp.indices((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1)) - shift = [np.arange(Ni) - p for Ni, p in zip(Ni, p)] + shift = [xp.arange(Ni) - p for Ni, p in zip(Ni, p)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() @@ -280,7 +280,7 @@ def get_M2(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): Ni = tensor_space_FEM.Nbase_2form[a] Nj = tensor_space_FEM.Nbase_2form[b] - M[a][b] = np.zeros((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1), dtype=float) + M[a][b] = xp.zeros((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1), dtype=float) # evaluate metric tensor at quadrature points if weight == None: @@ -291,13 +291,13 @@ def get_M2(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): mat_w = mat_w.reshape(Nel[0], n_quad[0], Nel[1], n_quad[1], Nel[2], n_quad[2]) # assemble block if weight is not zero - if np.any(mat_w): + if xp.any(mat_w): ker.kernel_mass( - np.array(Nel), - np.array(p), - np.array(n_quad), - np.array(ns[a]), - np.array(ns[b]), + xp.array(Nel), + xp.array(p), + xp.array(n_quad), + xp.array(ns[a]), + xp.array(ns[b]), wts[0], wts[1], wts[2], @@ -315,9 +315,9 @@ def get_M2(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): ) # convert to sparse matrix - indices = np.indices((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1)) + indices = xp.indices((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1)) - shift = [np.arange(Ni) - p for Ni, p in zip(Ni, p)] + shift = [xp.arange(Ni) - p for Ni, p in zip(Ni, p)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() @@ -381,7 +381,7 @@ def get_M3(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): # evaluation of weight function at quadrature points if weight == None: - mat_w = np.ones(det_df.shape, dtype=float) + mat_w = xp.ones(det_df.shape, dtype=float) else: mat_w = weight(pts[0].flatten(), pts[1].flatten(), pts[2].flatten()) mat_w = mat_w.reshape(Nel[0], n_quad[0], Nel[1], n_quad[1], Nel[2], n_quad[2]) @@ -390,14 +390,14 @@ def get_M3(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): Ni = tensor_space_FEM.Nbase_3form Nj = tensor_space_FEM.Nbase_3form - M = np.zeros((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1), dtype=float) + M = xp.zeros((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1), dtype=float) ker.kernel_mass( - np.array(Nel), - np.array(p), - np.array(n_quad), - np.array([1, 1, 1]), - np.array([1, 1, 1]), + xp.array(Nel), + xp.array(p), + xp.array(n_quad), + xp.array([1, 1, 1]), + xp.array([1, 1, 1]), wts[0], wts[1], wts[2], @@ -415,9 +415,9 @@ def get_M3(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): ) # conversion to sparse matrix - indices = np.indices((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1)) + indices = xp.indices((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1)) - shift = [np.arange(Ni) - p for Ni, p in zip(Ni, p)] + shift = [xp.arange(Ni) - p for Ni, p in zip(Ni, p)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() @@ -515,7 +515,7 @@ def get_Mv(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): Ni = tensor_space_FEM.Nbase_2form[a] Nj = tensor_space_FEM.Nbase_2form[b] - M[a][b] = np.zeros((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1), dtype=float) + M[a][b] = xp.zeros((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1), dtype=float) # evaluate metric tensor at quadrature points if weight == None: @@ -526,13 +526,13 @@ def get_Mv(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): mat_w = mat_w.reshape(Nel[0], n_quad[0], Nel[1], n_quad[1], Nel[2], n_quad[2]) # assemble block if weight is not zero - if np.any(mat_w): + if xp.any(mat_w): ker.kernel_mass( - np.array(Nel), - np.array(p), - np.array(n_quad), - np.array(ns[a]), - np.array(ns[b]), + xp.array(Nel), + xp.array(p), + xp.array(n_quad), + xp.array(ns[a]), + xp.array(ns[b]), wts[0], wts[1], wts[2], @@ -550,9 +550,9 @@ def get_Mv(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): ) # convert to sparse matrix - indices = np.indices((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1)) + indices = xp.indices((Ni[0], Ni[1], Ni[2], 2 * p[0] + 1, 2 * p[1] + 1, 2 * p[2] + 1)) - shift = [np.arange(Ni) - p for Ni, p in zip(Ni, p)] + shift = [xp.arange(Ni) - p for Ni, p in zip(Ni, p)] row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() diff --git a/src/struphy/eigenvalue_solvers/mhd_axisymmetric_main.py b/src/struphy/eigenvalue_solvers/mhd_axisymmetric_main.py index 009190582..c44c97335 100644 --- a/src/struphy/eigenvalue_solvers/mhd_axisymmetric_main.py +++ b/src/struphy/eigenvalue_solvers/mhd_axisymmetric_main.py @@ -32,11 +32,11 @@ def solve_mhd_ev_problem_2d(num_params, eq_mhd, n_tor, basis_tor="i", path_out=N import os import time + import cunumpy as xp import scipy.sparse as spa from struphy.eigenvalue_solvers.mhd_operators import MHDOperators from struphy.eigenvalue_solvers.spline_space import Spline_space_1d, Tensor_spline_space - from struphy.utils.arrays import xp as np print("\nStart of eigenspectrum calculation for toroidal mode number", n_tor) print("") @@ -148,7 +148,7 @@ def solve_mhd_ev_problem_2d(num_params, eq_mhd, n_tor, basis_tor="i", path_out=N print("Assembly of final system matrix done --> start of eigenvalue calculation") - omega2, U2_eig = np.linalg.eig(MAT) + omega2, U2_eig = xp.linalg.eig(MAT) print("Eigenstates calculated") @@ -161,8 +161,8 @@ def solve_mhd_ev_problem_2d(num_params, eq_mhd, n_tor, basis_tor="i", path_out=N else: n_tor_str = "+" + str(n_tor) - np.save( - os.path.join(path_out, "spec_n_" + n_tor_str + ".npy"), np.vstack((omega2.reshape(1, omega2.size), U2_eig)) + xp.save( + os.path.join(path_out, "spec_n_" + n_tor_str + ".npy"), xp.vstack((omega2.reshape(1, omega2.size), U2_eig)) ) # or return eigenfrequencies, eigenvectors and system matrix diff --git a/src/struphy/eigenvalue_solvers/mhd_axisymmetric_pproc.py b/src/struphy/eigenvalue_solvers/mhd_axisymmetric_pproc.py index dc6e53ddd..10e9e274e 100644 --- a/src/struphy/eigenvalue_solvers/mhd_axisymmetric_pproc.py +++ b/src/struphy/eigenvalue_solvers/mhd_axisymmetric_pproc.py @@ -3,10 +3,9 @@ def main(): import argparse import os + import cunumpy as xp import yaml - from struphy.utils.arrays import xp as np - # parse arguments parser = argparse.ArgumentParser(description="Restrict a full .npy eigenspectrum to a range of eigenfrequencies.") @@ -52,18 +51,18 @@ def main(): spec_path = os.path.join(input_path, "spec_n_" + n_tor_str + ".npy") - omega2, U2_eig = np.split(np.load(spec_path), [1], axis=0) + omega2, U2_eig = xp.split(xp.load(spec_path), [1], axis=0) omega2 = omega2.flatten() - modes_ind = np.where((np.real(omega2) < args.upper) & (np.real(omega2) > args.lower))[0] + modes_ind = xp.where((xp.real(omega2) < args.upper) & (xp.real(omega2) > args.lower))[0] omega2 = omega2[modes_ind] U2_eig = U2_eig[:, modes_ind] # save restricted spectrum - np.save( + xp.save( os.path.join(input_path, "spec_" + str(args.lower) + "_" + str(args.upper) + "_n_" + n_tor_str + ".npy"), - np.vstack((omega2.reshape(1, omega2.size), U2_eig)), + xp.vstack((omega2.reshape(1, omega2.size), U2_eig)), ) diff --git a/src/struphy/eigenvalue_solvers/mhd_operators.py b/src/struphy/eigenvalue_solvers/mhd_operators.py index b2cb669ae..5f1462c24 100644 --- a/src/struphy/eigenvalue_solvers/mhd_operators.py +++ b/src/struphy/eigenvalue_solvers/mhd_operators.py @@ -3,11 +3,11 @@ # Copyright 2021 Florian Holderied (florian.holderied@ipp.mpg.de) +import cunumpy as xp import scipy.sparse as spa import struphy.eigenvalue_solvers.legacy.mass_matrices_3d_pre as mass_3d_pre from struphy.eigenvalue_solvers.mhd_operators_core import MHDOperatorsCore -from struphy.utils.arrays import xp as np class MHDOperators: @@ -402,7 +402,7 @@ def __EF(self, u): out1 = self.int_N3.dot(self.dofs_EF[0].dot(u1).T).T + self.int_N3.dot(self.dofs_EF[1].dot(u3).T).T out3 = self.his_N3.dot(self.dofs_EF[2].dot(u1).T).T - out = np.concatenate((out1.flatten(), out3.flatten())) + out = xp.concatenate((out1.flatten(), out3.flatten())) elif self.core.basis_u == 2: u1, u3 = self.core.space.reshape_pol_2(u) @@ -410,7 +410,7 @@ def __EF(self, u): out1 = self.int_D3.dot(self.dofs_EF[0].dot(u1).T).T + self.int_N3.dot(self.dofs_EF[1].dot(u3).T).T out3 = self.his_D3.dot(self.dofs_EF[2].dot(u1).T).T - out = np.concatenate((out1.flatten(), out3.flatten())) + out = xp.concatenate((out1.flatten(), out3.flatten())) else: out = self.dofs_EF.dot(u) @@ -434,7 +434,7 @@ def __EF_transposed(self, e): ) out3 = self.int_N3.T.dot(self.dofs_EF[1].T.dot(e1).T).T - out = np.concatenate((out1.flatten(), out3.flatten())) + out = xp.concatenate((out1.flatten(), out3.flatten())) elif self.core.basis_u == 2: out1 = ( @@ -442,7 +442,7 @@ def __EF_transposed(self, e): ) out3 = self.int_N3.T.dot(self.dofs_EF[1].T.dot(e1).T).T - out = np.concatenate((out1.flatten(), out3.flatten())) + out = xp.concatenate((out1.flatten(), out3.flatten())) else: out = self.dofs_EF.T.dot(e) @@ -462,7 +462,7 @@ def __MF(self, u): out1 = self.his_N3.dot(self.dofs_MF[0].dot(u1).T).T out3 = self.int_N3.dot(self.dofs_MF[1].dot(u3).T).T - out = np.concatenate((out1.flatten(), out3.flatten())) + out = xp.concatenate((out1.flatten(), out3.flatten())) elif self.core.basis_u == 2: u1, u3 = self.core.space.reshape_pol_2(u) @@ -470,7 +470,7 @@ def __MF(self, u): out1 = self.his_D3.dot(self.dofs_MF[0].dot(u1).T).T out3 = self.int_N3.dot(self.dofs_MF[1].dot(u3).T).T - out = np.concatenate((out1.flatten(), out3.flatten())) + out = xp.concatenate((out1.flatten(), out3.flatten())) else: out = self.dofs_MF.dot(u) @@ -492,13 +492,13 @@ def __MF_transposed(self, f): out1 = self.his_N3.T.dot(self.dofs_MF[0].T.dot(f1).T).T out3 = self.int_N3.T.dot(self.dofs_MF[1].T.dot(f3).T).T - out = np.concatenate((out1.flatten(), out3.flatten())) + out = xp.concatenate((out1.flatten(), out3.flatten())) elif self.core.basis_u == 2: out1 = self.his_D3.T.dot(self.dofs_MF[0].T.dot(f1).T).T out3 = self.int_N3.T.dot(self.dofs_MF[1].T.dot(f3).T).T - out = np.concatenate((out1.flatten(), out3.flatten())) + out = xp.concatenate((out1.flatten(), out3.flatten())) else: out = self.dofs_MF.T.dot(f) @@ -518,7 +518,7 @@ def __PF(self, u): out1 = self.his_N3.dot(self.dofs_PF[0].dot(u1).T).T out3 = self.int_N3.dot(self.dofs_PF[1].dot(u3).T).T - out = np.concatenate((out1.flatten(), out3.flatten())) + out = xp.concatenate((out1.flatten(), out3.flatten())) elif self.core.basis_u == 2: u1, u3 = self.core.space.reshape_pol_2(u) @@ -526,7 +526,7 @@ def __PF(self, u): out1 = self.his_D3.dot(self.dofs_PF[0].dot(u1).T).T out3 = self.int_N3.dot(self.dofs_PF[1].dot(u3).T).T - out = np.concatenate((out1.flatten(), out3.flatten())) + out = xp.concatenate((out1.flatten(), out3.flatten())) else: out = self.dofs_PF.dot(u) @@ -548,13 +548,13 @@ def __PF_transposed(self, f): out1 = self.his_N3.T.dot(self.dofs_PF[0].T.dot(f1).T).T out3 = self.int_N3.T.dot(self.dofs_PF[1].T.dot(f3).T).T - out = np.concatenate((out1.flatten(), out3.flatten())) + out = xp.concatenate((out1.flatten(), out3.flatten())) elif self.core.basis_u == 2: out1 = self.his_D3.T.dot(self.dofs_PF[0].T.dot(f1).T).T out3 = self.int_N3.T.dot(self.dofs_PF[1].T.dot(f3).T).T - out = np.concatenate((out1.flatten(), out3.flatten())) + out = xp.concatenate((out1.flatten(), out3.flatten())) else: out = self.dofs_PF.T.dot(f) @@ -574,7 +574,7 @@ def __JF(self, u): out1 = self.his_N3.dot(self.dofs_JF[0].dot(u1).T).T out3 = self.int_N3.dot(self.dofs_JF[1].dot(u3).T).T - out = np.concatenate((out1.flatten(), out3.flatten())) + out = xp.concatenate((out1.flatten(), out3.flatten())) elif self.core.basis_u == 2: u1, u3 = self.core.space.reshape_pol_2(u) @@ -582,7 +582,7 @@ def __JF(self, u): out1 = self.his_D3.dot(self.dofs_JF[0].dot(u1).T).T out3 = self.int_N3.dot(self.dofs_JF[1].dot(u3).T).T - out = np.concatenate((out1.flatten(), out3.flatten())) + out = xp.concatenate((out1.flatten(), out3.flatten())) else: out = self.dofs_JF.dot(u) @@ -604,13 +604,13 @@ def __JF_transposed(self, f): out1 = self.his_N3.T.dot(self.dofs_JF[0].T.dot(f1).T).T out3 = self.int_N3.T.dot(self.dofs_JF[1].T.dot(f3).T).T - out = np.concatenate((out1.flatten(), out3.flatten())) + out = xp.concatenate((out1.flatten(), out3.flatten())) elif self.core.basis_u == 2: out1 = self.his_D3.T.dot(self.dofs_JF[0].T.dot(f1).T).T out3 = self.int_N3.T.dot(self.dofs_JF[1].T.dot(f3).T).T - out = np.concatenate((out1.flatten(), out3.flatten())) + out = xp.concatenate((out1.flatten(), out3.flatten())) else: out = self.dofs_JF.T.dot(f) @@ -678,7 +678,7 @@ def __MJ(self, b): if self.MJ_as_tensor: if self.core.basis_u == 0: - out = np.zeros(self.core.space.Ev_0.shape[0], dtype=float) + out = xp.zeros(self.core.space.Ev_0.shape[0], dtype=float) elif self.core.basis_u == 2: out = self.core.space.apply_M2_ten( b, [[self.MJ_mat[0], self.core.space.M1_tor], [self.MJ_mat[1], self.core.space.M0_tor]] @@ -686,7 +686,7 @@ def __MJ(self, b): else: if self.core.basis_u == 0: - out = np.zeros(self.core.space.Ev_0.shape[0], dtype=float) + out = xp.zeros(self.core.space.Ev_0.shape[0], dtype=float) elif self.core.basis_u == 2: out = self.MJ_mat.dot(b) @@ -929,7 +929,7 @@ def guess_S2(self, u, b, kind): u_guess = u + self.dt_2 / 6 * (k1_u + 2 * k2_u + 2 * k3_u + k4_u) else: - u_guess = np.copy(u) + u_guess = xp.copy(u) return u_guess diff --git a/src/struphy/eigenvalue_solvers/mhd_operators_core.py b/src/struphy/eigenvalue_solvers/mhd_operators_core.py index 528ec2b78..76752ccc3 100644 --- a/src/struphy/eigenvalue_solvers/mhd_operators_core.py +++ b/src/struphy/eigenvalue_solvers/mhd_operators_core.py @@ -3,12 +3,12 @@ # Copyright 2021 Florian Holderied (florian.holderied@ipp.mpg.de) +import cunumpy as xp import scipy.sparse as spa import struphy.eigenvalue_solvers.kernels_projectors_global_mhd as ker import struphy.eigenvalue_solvers.mass_matrices_2d as mass_2d import struphy.eigenvalue_solvers.mass_matrices_3d as mass_3d -from struphy.utils.arrays import xp as np class MHDOperatorsCore: @@ -58,11 +58,11 @@ def __init__(self, space, equilibrium, basis_u): self.subs_cum = [space.projectors.subs_cum for space in self.space.spaces] # get 1D indices of non-vanishing values of expressions dofs_0(N), dofs_0(D), dofs_1(N) and dofs_1(D) - self.dofs_0_N_i = [list(np.nonzero(space.projectors.I.toarray())) for space in self.space.spaces] - self.dofs_1_D_i = [list(np.nonzero(space.projectors.H.toarray())) for space in self.space.spaces] + self.dofs_0_N_i = [list(xp.nonzero(space.projectors.I.toarray())) for space in self.space.spaces] + self.dofs_1_D_i = [list(xp.nonzero(space.projectors.H.toarray())) for space in self.space.spaces] - self.dofs_0_D_i = [list(np.nonzero(space.projectors.ID.toarray())) for space in self.space.spaces] - self.dofs_1_N_i = [list(np.nonzero(space.projectors.HN.toarray())) for space in self.space.spaces] + self.dofs_0_D_i = [list(xp.nonzero(space.projectors.ID.toarray())) for space in self.space.spaces] + self.dofs_1_N_i = [list(xp.nonzero(space.projectors.HN.toarray())) for space in self.space.spaces] for i in range(self.space.dim): for j in range(2): @@ -116,9 +116,9 @@ def get_blocks_EF(self, pol=True): B2_3_pts = B2_3_pts.reshape(self.nhis[0], self.nq[0], self.nint[1]) # assemble sparse matrix - val = np.empty(self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=float) - row = np.empty(self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) - col = np.empty(self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) + val = xp.empty(self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=float) + row = xp.empty(self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) + col = xp.empty(self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) ker.rhs11_2d( self.dofs_1_N_i[0][0], @@ -130,8 +130,8 @@ def get_blocks_EF(self, pol=True): self.wts[0], self.basis_his_N[0], self.basis_int_N[1], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), -B2_3_pts, val, row, @@ -150,9 +150,9 @@ def get_blocks_EF(self, pol=True): B2_2_pts = B2_2_pts.reshape(self.nhis[0], self.nq[0], self.nint[1]) # assemble sparse matrix - val = np.empty(self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=float) - row = np.empty(self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) - col = np.empty(self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) + val = xp.empty(self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=float) + row = xp.empty(self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) + col = xp.empty(self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) ker.rhs11_2d( self.dofs_1_N_i[0][0], @@ -164,8 +164,8 @@ def get_blocks_EF(self, pol=True): self.wts[0], self.basis_his_N[0], self.basis_int_N[1], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), B2_2_pts, val, row, @@ -184,9 +184,9 @@ def get_blocks_EF(self, pol=True): B2_3_pts = B2_3_pts.reshape(self.nint[0], self.nhis[1], self.nq[1]) # assemble sparse matrix - val = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=float) - row = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=int) - col = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=int) + val = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=float) + row = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=int) + col = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=int) ker.rhs12_2d( self.dofs_0_N_i[0][0], @@ -198,8 +198,8 @@ def get_blocks_EF(self, pol=True): self.wts[1], self.basis_int_N[0], self.basis_his_N[1], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), B2_3_pts, val, row, @@ -218,9 +218,9 @@ def get_blocks_EF(self, pol=True): B2_1_pts = B2_1_pts.reshape(self.nint[0], self.nhis[1], self.nq[1]) # assemble sparse matrix - val = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=float) - row = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=int) - col = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=int) + val = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=float) + row = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=int) + col = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=int) ker.rhs12_2d( self.dofs_0_N_i[0][0], @@ -232,8 +232,8 @@ def get_blocks_EF(self, pol=True): self.wts[1], self.basis_int_N[0], self.basis_his_N[1], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), -B2_1_pts, val, row, @@ -251,9 +251,9 @@ def get_blocks_EF(self, pol=True): B2_2_pts = self.equilibrium.b2_2(self.eta_int[0], self.eta_int[1], 0.0) # assemble sparse matrix - val = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=float) - row = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) - col = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) + val = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=float) + row = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) + col = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) ker.rhs0_2d( self.dofs_0_N_i[0][0], @@ -279,9 +279,9 @@ def get_blocks_EF(self, pol=True): B2_1_pts = self.equilibrium.b2_1(self.eta_int[0], self.eta_int[1], 0.0) # assemble sparse matrix - val = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=float) - row = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) - col = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) + val = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=float) + row = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) + col = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) ker.rhs0_2d( self.dofs_0_N_i[0][0], @@ -309,13 +309,13 @@ def get_blocks_EF(self, pol=True): B2_3_pts = B2_3_pts.reshape(self.nhis[0], self.nq[0], self.nint[1], self.nint[2]) # assemble sparse matrix - val = np.empty( + val = xp.empty( self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=float ) - row = np.empty( + row = xp.empty( self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int ) - col = np.empty( + col = xp.empty( self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int ) @@ -332,8 +332,8 @@ def get_blocks_EF(self, pol=True): self.basis_his_N[0], self.basis_int_N[1], self.basis_int_N[2], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), -B2_3_pts, val, row, @@ -350,13 +350,13 @@ def get_blocks_EF(self, pol=True): B2_2_pts = B2_2_pts.reshape(self.nhis[0], self.nq[0], self.nint[1], self.nint[2]) # assemble sparse matrix - val = np.empty( + val = xp.empty( self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=float ) - row = np.empty( + row = xp.empty( self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int ) - col = np.empty( + col = xp.empty( self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int ) @@ -373,8 +373,8 @@ def get_blocks_EF(self, pol=True): self.basis_his_N[0], self.basis_int_N[1], self.basis_int_N[2], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), B2_2_pts, val, row, @@ -391,13 +391,13 @@ def get_blocks_EF(self, pol=True): B2_3_pts = B2_3_pts.reshape(self.nint[0], self.nhis[1], self.nq[1], self.nint[2]) # assemble sparse matrix - val = np.empty( + val = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=float ) - row = np.empty( + row = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int ) - col = np.empty( + col = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int ) @@ -414,8 +414,8 @@ def get_blocks_EF(self, pol=True): self.basis_int_N[0], self.basis_his_N[1], self.basis_int_N[2], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), B2_3_pts, val, row, @@ -432,13 +432,13 @@ def get_blocks_EF(self, pol=True): B2_1_pts = B2_1_pts.reshape(self.nint[0], self.nhis[1], self.nq[1], self.nint[2]) # assemble sparse matrix - val = np.empty( + val = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=float ) - row = np.empty( + row = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int ) - col = np.empty( + col = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int ) @@ -455,8 +455,8 @@ def get_blocks_EF(self, pol=True): self.basis_int_N[0], self.basis_his_N[1], self.basis_int_N[2], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), -B2_1_pts, val, row, @@ -473,13 +473,13 @@ def get_blocks_EF(self, pol=True): B2_2_pts = B2_2_pts.reshape(self.nint[0], self.nint[1], self.nhis[2], self.nq[2]) # assemble sparse matrix - val = np.empty( + val = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=float ) - row = np.empty( + row = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=int ) - col = np.empty( + col = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=int ) @@ -496,8 +496,8 @@ def get_blocks_EF(self, pol=True): self.basis_int_N[0], self.basis_int_N[1], self.basis_his_N[2], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), -B2_2_pts, val, row, @@ -514,13 +514,13 @@ def get_blocks_EF(self, pol=True): B2_1_pts = B2_1_pts.reshape(self.nint[0], self.nint[1], self.nhis[2], self.nq[2]) # assemble sparse matrix - val = np.empty( + val = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=float ) - row = np.empty( + row = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=int ) - col = np.empty( + col = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=int ) @@ -537,8 +537,8 @@ def get_blocks_EF(self, pol=True): self.basis_int_N[0], self.basis_int_N[1], self.basis_his_N[2], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), B2_1_pts, val, row, @@ -561,9 +561,9 @@ def get_blocks_EF(self, pol=True): det_dF = det_dF.reshape(self.nhis[0], self.nq[0], self.nint[1]) # assemble sparse matrix - val = np.empty(self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=float) - row = np.empty(self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) - col = np.empty(self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) + val = xp.empty(self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=float) + row = xp.empty(self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) + col = xp.empty(self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) ker.rhs11_2d( self.dofs_1_D_i[0][0], @@ -575,8 +575,8 @@ def get_blocks_EF(self, pol=True): self.wts[0], self.basis_his_D[0], self.basis_int_N[1], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), -B2_3_pts / det_dF, val, row, @@ -599,9 +599,9 @@ def get_blocks_EF(self, pol=True): det_dF = det_dF.reshape(self.nhis[0], self.nq[0], self.nint[1]) # assemble sparse matrix - val = np.empty(self.dofs_1_D_i[0][0].size * self.dofs_0_D_i[1][0].size, dtype=float) - row = np.empty(self.dofs_1_D_i[0][0].size * self.dofs_0_D_i[1][0].size, dtype=int) - col = np.empty(self.dofs_1_D_i[0][0].size * self.dofs_0_D_i[1][0].size, dtype=int) + val = xp.empty(self.dofs_1_D_i[0][0].size * self.dofs_0_D_i[1][0].size, dtype=float) + row = xp.empty(self.dofs_1_D_i[0][0].size * self.dofs_0_D_i[1][0].size, dtype=int) + col = xp.empty(self.dofs_1_D_i[0][0].size * self.dofs_0_D_i[1][0].size, dtype=int) ker.rhs11_2d( self.dofs_1_D_i[0][0], @@ -613,8 +613,8 @@ def get_blocks_EF(self, pol=True): self.wts[0], self.basis_his_D[0], self.basis_int_D[1], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), B2_2_pts / det_dF, val, row, @@ -637,9 +637,9 @@ def get_blocks_EF(self, pol=True): det_dF = det_dF.reshape(self.nint[0], self.nhis[1], self.nq[1]) # assemble sparse matrix - val = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=float) - row = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) - col = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) + val = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=float) + row = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) + col = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) ker.rhs12_2d( self.dofs_0_N_i[0][0], @@ -651,8 +651,8 @@ def get_blocks_EF(self, pol=True): self.wts[1], self.basis_int_N[0], self.basis_his_D[1], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), B2_3_pts / det_dF, val, row, @@ -675,9 +675,9 @@ def get_blocks_EF(self, pol=True): det_dF = det_dF.reshape(self.nint[0], self.nhis[1], self.nq[1]) # assemble sparse matrix - val = np.empty(self.dofs_0_D_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=float) - row = np.empty(self.dofs_0_D_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) - col = np.empty(self.dofs_0_D_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) + val = xp.empty(self.dofs_0_D_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=float) + row = xp.empty(self.dofs_0_D_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) + col = xp.empty(self.dofs_0_D_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) ker.rhs12_2d( self.dofs_0_D_i[0][0], @@ -689,8 +689,8 @@ def get_blocks_EF(self, pol=True): self.wts[1], self.basis_int_D[0], self.basis_his_D[1], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), -B2_1_pts / det_dF, val, row, @@ -711,9 +711,9 @@ def get_blocks_EF(self, pol=True): det_dF = abs(self.equilibrium.domain.jacobian_det(self.eta_int[0], self.eta_int[1], 0.0)) # assemble sparse matrix - val = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_0_D_i[1][0].size, dtype=float) - row = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_0_D_i[1][0].size, dtype=int) - col = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_0_D_i[1][0].size, dtype=int) + val = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_0_D_i[1][0].size, dtype=float) + row = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_0_D_i[1][0].size, dtype=int) + col = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_0_D_i[1][0].size, dtype=int) ker.rhs0_2d( self.dofs_0_N_i[0][0], @@ -742,9 +742,9 @@ def get_blocks_EF(self, pol=True): det_dF = abs(self.equilibrium.domain.jacobian_det(self.eta_int[0], self.eta_int[1], 0.0)) # assemble sparse matrix - val = np.empty(self.dofs_0_D_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=float) - row = np.empty(self.dofs_0_D_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) - col = np.empty(self.dofs_0_D_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) + val = xp.empty(self.dofs_0_D_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=float) + row = xp.empty(self.dofs_0_D_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) + col = xp.empty(self.dofs_0_D_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) ker.rhs0_2d( self.dofs_0_D_i[0][0], @@ -778,13 +778,13 @@ def get_blocks_EF(self, pol=True): det_dF = det_dF.reshape(self.nhis[0], self.nq[0], self.nint[1], self.nint[2]) # assemble sparse matrix - val = np.empty( + val = xp.empty( self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_D_i[2][0].size, dtype=float ) - row = np.empty( + row = xp.empty( self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_D_i[2][0].size, dtype=int ) - col = np.empty( + col = xp.empty( self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_D_i[2][0].size, dtype=int ) @@ -801,8 +801,8 @@ def get_blocks_EF(self, pol=True): self.basis_his_D[0], self.basis_int_N[1], self.basis_int_D[2], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), -B2_3_pts / det_dF, val, row, @@ -825,13 +825,13 @@ def get_blocks_EF(self, pol=True): det_dF = det_dF.reshape(self.nhis[0], self.nq[0], self.nint[1], self.nint[2]) # assemble sparse matrix - val = np.empty( + val = xp.empty( self.dofs_1_D_i[0][0].size * self.dofs_0_D_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=float ) - row = np.empty( + row = xp.empty( self.dofs_1_D_i[0][0].size * self.dofs_0_D_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int ) - col = np.empty( + col = xp.empty( self.dofs_1_D_i[0][0].size * self.dofs_0_D_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int ) @@ -848,8 +848,8 @@ def get_blocks_EF(self, pol=True): self.basis_his_D[0], self.basis_int_D[1], self.basis_int_N[2], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), B2_2_pts / det_dF, val, row, @@ -872,13 +872,13 @@ def get_blocks_EF(self, pol=True): det_dF = det_dF.reshape(self.nint[0], self.nhis[1], self.nq[1], self.nint[2]) # assemble sparse matrix - val = np.empty( + val = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_D_i[2][0].size, dtype=float ) - row = np.empty( + row = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_D_i[2][0].size, dtype=int ) - col = np.empty( + col = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_D_i[2][0].size, dtype=int ) @@ -895,8 +895,8 @@ def get_blocks_EF(self, pol=True): self.basis_int_N[0], self.basis_his_D[1], self.basis_int_D[2], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), B2_3_pts / det_dF, val, row, @@ -919,13 +919,13 @@ def get_blocks_EF(self, pol=True): det_dF = det_dF.reshape(self.nint[0], self.nhis[1], self.nq[1], self.nint[2]) # assemble sparse matrix - val = np.empty( + val = xp.empty( self.dofs_0_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=float ) - row = np.empty( + row = xp.empty( self.dofs_0_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int ) - col = np.empty( + col = xp.empty( self.dofs_0_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int ) @@ -942,8 +942,8 @@ def get_blocks_EF(self, pol=True): self.basis_int_D[0], self.basis_his_D[1], self.basis_int_N[2], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), -B2_1_pts / det_dF, val, row, @@ -966,13 +966,13 @@ def get_blocks_EF(self, pol=True): det_dF = det_dF.reshape(self.nint[0], self.nint[1], self.nhis[2], self.nq[2]) # assemble sparse matrix - val = np.empty( + val = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_0_D_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=float ) - row = np.empty( + row = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_0_D_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int ) - col = np.empty( + col = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_0_D_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int ) @@ -989,8 +989,8 @@ def get_blocks_EF(self, pol=True): self.basis_int_N[0], self.basis_int_D[1], self.basis_his_D[2], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), -B2_2_pts / det_dF, val, row, @@ -1013,13 +1013,13 @@ def get_blocks_EF(self, pol=True): det_dF = det_dF.reshape(self.nint[0], self.nint[1], self.nhis[2], self.nq[2]) # assemble sparse matrix - val = np.empty( + val = xp.empty( self.dofs_0_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=float ) - row = np.empty( + row = xp.empty( self.dofs_0_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int ) - col = np.empty( + col = xp.empty( self.dofs_0_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int ) @@ -1036,8 +1036,8 @@ def get_blocks_EF(self, pol=True): self.basis_int_D[0], self.basis_int_N[1], self.basis_his_D[2], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), B2_1_pts / det_dF, val, row, @@ -1093,9 +1093,9 @@ def get_blocks_FL(self, which, pol=True): EQ = EQ.reshape(self.nint[0], self.nhis[1], self.nq[1]) # assemble sparse matrix - val = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=float) - row = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=int) - col = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=int) + val = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=float) + row = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=int) + col = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=int) ker.rhs12_2d( self.dofs_0_N_i[0][0], @@ -1107,8 +1107,8 @@ def get_blocks_FL(self, which, pol=True): self.wts[1], self.basis_int_N[0], self.basis_his_N[1], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), EQ, val, row, @@ -1133,9 +1133,9 @@ def get_blocks_FL(self, which, pol=True): EQ = EQ.reshape(self.nhis[0], self.nq[0], self.nint[1]) # assemble sparse matrix - val = np.empty(self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=float) - row = np.empty(self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) - col = np.empty(self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) + val = xp.empty(self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=float) + row = xp.empty(self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) + col = xp.empty(self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) ker.rhs11_2d( self.dofs_1_N_i[0][0], @@ -1147,8 +1147,8 @@ def get_blocks_FL(self, which, pol=True): self.wts[0], self.basis_his_N[0], self.basis_int_N[1], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), EQ, val, row, @@ -1173,9 +1173,9 @@ def get_blocks_FL(self, which, pol=True): EQ = EQ.reshape(self.nhis[0], self.nq[0], self.nhis[1], self.nq[1]) # assemble sparse matrix - val = np.empty(self.dofs_1_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=float) - row = np.empty(self.dofs_1_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=int) - col = np.empty(self.dofs_1_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=int) + val = xp.empty(self.dofs_1_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=float) + row = xp.empty(self.dofs_1_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=int) + col = xp.empty(self.dofs_1_N_i[0][0].size * self.dofs_1_N_i[1][0].size, dtype=int) ker.rhs2_2d( self.dofs_1_N_i[0][0], @@ -1190,8 +1190,8 @@ def get_blocks_FL(self, which, pol=True): self.wts[1], self.basis_his_N[0], self.basis_his_N[1], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), EQ, val, row, @@ -1219,13 +1219,13 @@ def get_blocks_FL(self, which, pol=True): EQ = EQ.reshape(self.nint[0], self.nhis[1], self.nq[1], self.nhis[2], self.nq[2]) # assemble sparse matrix - val = np.empty( + val = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=float ) - row = np.empty( + row = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=int ) - col = np.empty( + col = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=int ) @@ -1245,8 +1245,8 @@ def get_blocks_FL(self, which, pol=True): self.basis_int_N[0], self.basis_his_N[1], self.basis_his_N[2], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), EQ, val, row, @@ -1271,13 +1271,13 @@ def get_blocks_FL(self, which, pol=True): EQ = EQ.reshape(self.nhis[0], self.nq[0], self.nint[1], self.nhis[2], self.nq[2]) # assemble sparse matrix - val = np.empty( + val = xp.empty( self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=float ) - row = np.empty( + row = xp.empty( self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=int ) - col = np.empty( + col = xp.empty( self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=int ) @@ -1297,8 +1297,8 @@ def get_blocks_FL(self, which, pol=True): self.basis_his_N[0], self.basis_int_N[1], self.basis_his_N[2], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), EQ, val, row, @@ -1323,13 +1323,13 @@ def get_blocks_FL(self, which, pol=True): EQ = EQ.reshape(self.nhis[0], self.nq[0], self.nhis[1], self.nq[1], self.nint[2]) # assemble sparse matrix - val = np.empty( + val = xp.empty( self.dofs_1_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=float ) - row = np.empty( + row = xp.empty( self.dofs_1_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int ) - col = np.empty( + col = xp.empty( self.dofs_1_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int ) @@ -1349,8 +1349,8 @@ def get_blocks_FL(self, which, pol=True): self.basis_his_N[0], self.basis_his_N[1], self.basis_int_N[2], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), EQ, val, row, @@ -1377,9 +1377,9 @@ def get_blocks_FL(self, which, pol=True): det_dF = det_dF.reshape(self.nint[0], self.nhis[1], self.nq[1]) # assemble sparse matrix - val = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=float) - row = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) - col = np.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) + val = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=float) + row = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) + col = xp.empty(self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) ker.rhs12_2d( self.dofs_0_N_i[0][0], @@ -1391,8 +1391,8 @@ def get_blocks_FL(self, which, pol=True): self.wts[1], self.basis_int_N[0], self.basis_his_D[1], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), EQ / det_dF, val, row, @@ -1419,9 +1419,9 @@ def get_blocks_FL(self, which, pol=True): det_dF = det_dF.reshape(self.nhis[0], self.nq[0], self.nint[1]) # assemble sparse matrix - val = np.empty(self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=float) - row = np.empty(self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) - col = np.empty(self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) + val = xp.empty(self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=float) + row = xp.empty(self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) + col = xp.empty(self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size, dtype=int) ker.rhs11_2d( self.dofs_1_D_i[0][0], @@ -1433,8 +1433,8 @@ def get_blocks_FL(self, which, pol=True): self.wts[0], self.basis_his_D[0], self.basis_int_N[1], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), EQ / det_dF, val, row, @@ -1463,9 +1463,9 @@ def get_blocks_FL(self, which, pol=True): det_dF = det_dF.reshape(self.nhis[0], self.nq[0], self.nhis[1], self.nq[1]) # assemble sparse matrix - val = np.empty(self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=float) - row = np.empty(self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) - col = np.empty(self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) + val = xp.empty(self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=float) + row = xp.empty(self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) + col = xp.empty(self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) ker.rhs2_2d( self.dofs_1_D_i[0][0], @@ -1480,8 +1480,8 @@ def get_blocks_FL(self, which, pol=True): self.wts[1], self.basis_his_D[0], self.basis_his_D[1], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), EQ / det_dF, val, row, @@ -1513,13 +1513,13 @@ def get_blocks_FL(self, which, pol=True): det_dF = det_dF.reshape(self.nint[0], self.nhis[1], self.nq[1], self.nhis[2], self.nq[2]) # assemble sparse matrix - val = np.empty( + val = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=float ) - row = np.empty( + row = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int ) - col = np.empty( + col = xp.empty( self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int ) @@ -1539,8 +1539,8 @@ def get_blocks_FL(self, which, pol=True): self.basis_int_N[0], self.basis_his_D[1], self.basis_his_D[2], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), EQ / det_dF, val, row, @@ -1569,13 +1569,13 @@ def get_blocks_FL(self, which, pol=True): det_dF = det_dF.reshape(self.nhis[0], self.nq[0], self.nint[1], self.nhis[2], self.nq[2]) # assemble sparse matrix - val = np.empty( + val = xp.empty( self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=float ) - row = np.empty( + row = xp.empty( self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int ) - col = np.empty( + col = xp.empty( self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int ) @@ -1595,8 +1595,8 @@ def get_blocks_FL(self, which, pol=True): self.basis_his_D[0], self.basis_int_N[1], self.basis_his_D[2], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), EQ / det_dF, val, row, @@ -1625,13 +1625,13 @@ def get_blocks_FL(self, which, pol=True): det_dF = det_dF.reshape(self.nhis[0], self.nq[0], self.nhis[1], self.nq[1], self.nint[2]) # assemble sparse matrix - val = np.empty( + val = xp.empty( self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=float ) - row = np.empty( + row = xp.empty( self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int ) - col = np.empty( + col = xp.empty( self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int ) @@ -1651,8 +1651,8 @@ def get_blocks_FL(self, which, pol=True): self.basis_his_D[0], self.basis_his_D[1], self.basis_int_N[2], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), EQ / det_dF, val, row, @@ -1696,9 +1696,9 @@ def get_blocks_PR(self, pol=True): det_dF = det_dF.reshape(self.nhis[0], self.nq[0], self.nhis[1], self.nq[1]) # assemble sparse matrix - val = np.empty(self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=float) - row = np.empty(self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) - col = np.empty(self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) + val = xp.empty(self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=float) + row = xp.empty(self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) + col = xp.empty(self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size, dtype=int) ker.rhs2_2d( self.dofs_1_D_i[0][0], @@ -1713,8 +1713,8 @@ def get_blocks_PR(self, pol=True): self.wts[1], self.basis_his_D[0], self.basis_his_D[1], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), P3_pts / det_dF, val, row, @@ -1745,13 +1745,13 @@ def get_blocks_PR(self, pol=True): det_dF = det_dF.reshape(self.nhis[0], self.nq[0], self.nhis[1], self.nq[1], self.nhis[2], self.nq[2]) # assemble sparse matrix - val = np.empty( + val = xp.empty( self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=float ) - row = np.empty( + row = xp.empty( self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int ) - col = np.empty( + col = xp.empty( self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int ) @@ -1774,8 +1774,8 @@ def get_blocks_PR(self, pol=True): self.basis_his_D[0], self.basis_his_D[1], self.basis_his_D[2], - np.array(self.space.NbaseN), - np.array(self.space.NbaseD), + xp.array(self.space.NbaseN), + xp.array(self.space.NbaseD), P3_pts / det_dF, val, row, diff --git a/src/struphy/eigenvalue_solvers/projectors_global.py b/src/struphy/eigenvalue_solvers/projectors_global.py index 208ff701a..fa7b69958 100644 --- a/src/struphy/eigenvalue_solvers/projectors_global.py +++ b/src/struphy/eigenvalue_solvers/projectors_global.py @@ -6,11 +6,11 @@ Classes for commuting projectors in 1D, 2D and 3D based on global spline interpolation and histopolation. """ +import cunumpy as xp import scipy.sparse as spa import struphy.bsplines.bsplines as bsp from struphy.linear_algebra.linalg_kron import kron_lusolve_2d, kron_lusolve_3d, kron_matvec_2d, kron_matvec_3d -from struphy.utils.arrays import xp as np # ======================= 1d ==================================== @@ -156,15 +156,15 @@ def __init__(self, spline_space, n_quad=6): self.n_quad = n_quad # Gauss - Legendre quadrature points and weights in (-1, 1) - self.pts_loc = np.polynomial.legendre.leggauss(self.n_quad)[0] - self.wts_loc = np.polynomial.legendre.leggauss(self.n_quad)[1] + self.pts_loc = xp.polynomial.legendre.leggauss(self.n_quad)[0] + self.wts_loc = xp.polynomial.legendre.leggauss(self.n_quad)[1] # set interpolation points (Greville points) self.x_int = spline_space.greville.copy() # set number of sub-intervals per integration interval between Greville points and integration boundaries - self.subs = np.ones(spline_space.NbaseD, dtype=int) - self.x_his = np.array([self.x_int[0]]) + self.subs = xp.ones(spline_space.NbaseD, dtype=int) + self.x_his = xp.array([self.x_int[0]]) for i in range(spline_space.NbaseD): for br in spline_space.el_b: @@ -181,16 +181,16 @@ def __init__(self, spline_space, n_quad=6): # compute subs and x_his if (br > xl + 1e-10) and (br < xr - 1e-10): self.subs[i] += 1 - self.x_his = np.append(self.x_his, br) + self.x_his = xp.append(self.x_his, br) elif br >= xr - 1e-10: - self.x_his = np.append(self.x_his, xr) + self.x_his = xp.append(self.x_his, xr) break if spline_space.spl_kind == True and spline_space.p % 2 == 0: - self.x_his = np.append(self.x_his, spline_space.el_b[-1] + self.x_his[0]) + self.x_his = xp.append(self.x_his, spline_space.el_b[-1] + self.x_his[0]) # cumulative number of sub-intervals for conversion local interval --> global interval - self.subs_cum = np.append(0, np.cumsum(self.subs - 1)[:-1]) + self.subs_cum = xp.append(0, xp.cumsum(self.subs - 1)[:-1]) # quadrature points and weights self.pts, self.wts = bsp.quadrature_grid(self.x_his, self.pts_loc, self.wts_loc) @@ -200,31 +200,31 @@ def __init__(self, spline_space, n_quad=6): self.x_hisG = self.x_int if spline_space.spl_kind == True: if spline_space.p % 2 == 0: - self.x_hisG = np.append(self.x_hisG, spline_space.el_b[-1] + self.x_hisG[0]) + self.x_hisG = xp.append(self.x_hisG, spline_space.el_b[-1] + self.x_hisG[0]) else: - self.x_hisG = np.append(self.x_hisG, spline_space.el_b[-1]) + self.x_hisG = xp.append(self.x_hisG, spline_space.el_b[-1]) self.ptsG, self.wtsG = bsp.quadrature_grid(self.x_hisG, self.pts_loc, self.wts_loc) self.ptsG = self.ptsG % spline_space.el_b[-1] # Knot span indices at interpolation points in format (greville, 0) - self.span_x_int_N = np.zeros(self.x_int[:, None].shape, dtype=int) - self.span_x_int_D = np.zeros(self.x_int[:, None].shape, dtype=int) + self.span_x_int_N = xp.zeros(self.x_int[:, None].shape, dtype=int) + self.span_x_int_D = xp.zeros(self.x_int[:, None].shape, dtype=int) for i in range(self.x_int.shape[0]): self.span_x_int_N[i, 0] = bsp.find_span(self.space.T, self.space.p, self.x_int[i]) self.span_x_int_D[i, 0] = bsp.find_span(self.space.t, self.space.p - 1, self.x_int[i]) # Knot span indices at quadrature points between x_int in format (i, iq) - self.span_ptsG_N = np.zeros(self.ptsG.shape, dtype=int) - self.span_ptsG_D = np.zeros(self.ptsG.shape, dtype=int) + self.span_ptsG_N = xp.zeros(self.ptsG.shape, dtype=int) + self.span_ptsG_D = xp.zeros(self.ptsG.shape, dtype=int) for i in range(self.ptsG.shape[0]): for iq in range(self.ptsG.shape[1]): self.span_ptsG_N[i, iq] = bsp.find_span(self.space.T, self.space.p, self.ptsG[i, iq]) self.span_ptsG_D[i, iq] = bsp.find_span(self.space.t, self.space.p - 1, self.ptsG[i, iq]) # Values of p + 1 non-zero basis functions at Greville points in format (greville, 0, basis function) - self.basis_x_int_N = np.zeros((*self.x_int[:, None].shape, self.space.p + 1), dtype=float) - self.basis_x_int_D = np.zeros((*self.x_int[:, None].shape, self.space.p), dtype=float) + self.basis_x_int_N = xp.zeros((*self.x_int[:, None].shape, self.space.p + 1), dtype=float) + self.basis_x_int_D = xp.zeros((*self.x_int[:, None].shape, self.space.p), dtype=float) N_temp = bsp.basis_ders_on_quad_grid(self.space.T, self.space.p, self.x_int[:, None], 0, normalize=False) D_temp = bsp.basis_ders_on_quad_grid(self.space.t, self.space.p - 1, self.x_int[:, None], 0, normalize=True) @@ -236,8 +236,8 @@ def __init__(self, spline_space, n_quad=6): self.basis_x_int_D[i, 0, b] = D_temp[i, b, 0, 0] # Values of p + 1 non-zero basis functions at quadrature points points between x_int in format (i, iq, basis function) - self.basis_ptsG_N = np.zeros((*self.ptsG.shape, self.space.p + 1), dtype=float) - self.basis_ptsG_D = np.zeros((*self.ptsG.shape, self.space.p), dtype=float) + self.basis_ptsG_N = xp.zeros((*self.ptsG.shape, self.space.p + 1), dtype=float) + self.basis_ptsG_D = xp.zeros((*self.ptsG.shape, self.space.p), dtype=float) N_temp = bsp.basis_ders_on_quad_grid(self.space.T, self.space.p, self.ptsG, 0, normalize=False) D_temp = bsp.basis_ders_on_quad_grid(self.space.t, self.space.p - 1, self.ptsG, 0, normalize=True) @@ -250,7 +250,7 @@ def __init__(self, spline_space, n_quad=6): self.basis_ptsG_D[i, iq, b] = D_temp[i, b, 0, iq] # quadrature matrix for performing integrations as matrix-vector products - self.Q = np.zeros((spline_space.NbaseD, self.wts.shape[0] * self.n_quad), dtype=float) + self.Q = xp.zeros((spline_space.NbaseD, self.wts.shape[0] * self.n_quad), dtype=float) for i in range(spline_space.NbaseD): for j in range(self.subs[i]): @@ -260,7 +260,7 @@ def __init__(self, spline_space, n_quad=6): self.Q = spa.csr_matrix(self.Q) # quadrature matrix for performing integrations as matrix-vector products, ignoring subs (less accurate integration for even degree) - self.QG = np.zeros((spline_space.NbaseD, self.wtsG.shape[0] * self.n_quad), dtype=float) + self.QG = xp.zeros((spline_space.NbaseD, self.wtsG.shape[0] * self.n_quad), dtype=float) for i in range(spline_space.NbaseD): self.QG[i, self.n_quad * i : self.n_quad * (i + 1)] = self.wtsG[i] @@ -399,17 +399,17 @@ def dofs_1d_bases_products(self, space): dofs_1_i(D_j*D_k). """ - dofs_0_NN = np.empty((space.NbaseN, space.NbaseN, space.NbaseN), dtype=float) - dofs_0_DN = np.empty((space.NbaseN, space.NbaseD, space.NbaseN), dtype=float) - dofs_0_DD = np.empty((space.NbaseN, space.NbaseD, space.NbaseD), dtype=float) + dofs_0_NN = xp.empty((space.NbaseN, space.NbaseN, space.NbaseN), dtype=float) + dofs_0_DN = xp.empty((space.NbaseN, space.NbaseD, space.NbaseN), dtype=float) + dofs_0_DD = xp.empty((space.NbaseN, space.NbaseD, space.NbaseD), dtype=float) - dofs_1_NN = np.empty((space.NbaseD, space.NbaseN, space.NbaseN), dtype=float) - dofs_1_DN = np.empty((space.NbaseD, space.NbaseD, space.NbaseN), dtype=float) - dofs_1_DD = np.empty((space.NbaseD, space.NbaseD, space.NbaseD), dtype=float) + dofs_1_NN = xp.empty((space.NbaseD, space.NbaseN, space.NbaseN), dtype=float) + dofs_1_DN = xp.empty((space.NbaseD, space.NbaseD, space.NbaseN), dtype=float) + dofs_1_DD = xp.empty((space.NbaseD, space.NbaseD, space.NbaseD), dtype=float) # ========= dofs_0_NN and dofs_1_NN ============== - cj = np.zeros(space.NbaseN, dtype=float) - ck = np.zeros(space.NbaseN, dtype=float) + cj = xp.zeros(space.NbaseN, dtype=float) + ck = xp.zeros(space.NbaseN, dtype=float) for j in range(space.NbaseN): for k in range(space.NbaseN): @@ -426,8 +426,8 @@ def N_jN_k(eta): dofs_1_NN[:, j, k] = self.dofs_1(N_jN_k) # ========= dofs_0_DN and dofs_1_DN ============== - cj = np.zeros(space.NbaseD, dtype=float) - ck = np.zeros(space.NbaseN, dtype=float) + cj = xp.zeros(space.NbaseD, dtype=float) + ck = xp.zeros(space.NbaseN, dtype=float) for j in range(space.NbaseD): for k in range(space.NbaseN): @@ -444,8 +444,8 @@ def D_jN_k(eta): dofs_1_DN[:, j, k] = self.dofs_1(D_jN_k) # ========= dofs_0_DD and dofs_1_DD ============= - cj = np.zeros(space.NbaseD, dtype=float) - ck = np.zeros(space.NbaseD, dtype=float) + cj = xp.zeros(space.NbaseD, dtype=float) + ck = xp.zeros(space.NbaseD, dtype=float) for j in range(space.NbaseD): for k in range(space.NbaseD): @@ -461,109 +461,109 @@ def D_jD_k(eta): dofs_0_DD[:, j, k] = self.dofs_0(D_jD_k) dofs_1_DD[:, j, k] = self.dofs_1(D_jD_k) - dofs_0_ND = np.transpose(dofs_0_DN, (0, 2, 1)) - dofs_1_ND = np.transpose(dofs_1_DN, (0, 2, 1)) + dofs_0_ND = xp.transpose(dofs_0_DN, (0, 2, 1)) + dofs_1_ND = xp.transpose(dofs_1_DN, (0, 2, 1)) # find non-zero entries - dofs_0_NN_indices = np.nonzero(dofs_0_NN) - dofs_0_DN_indices = np.nonzero(dofs_0_DN) - dofs_0_ND_indices = np.nonzero(dofs_0_ND) - dofs_0_DD_indices = np.nonzero(dofs_0_DD) - - dofs_1_NN_indices = np.nonzero(dofs_1_NN) - dofs_1_DN_indices = np.nonzero(dofs_1_DN) - dofs_1_ND_indices = np.nonzero(dofs_1_ND) - dofs_1_DD_indices = np.nonzero(dofs_1_DD) - - dofs_0_NN_i_red = np.empty(dofs_0_NN_indices[0].size, dtype=int) - dofs_0_DN_i_red = np.empty(dofs_0_DN_indices[0].size, dtype=int) - dofs_0_ND_i_red = np.empty(dofs_0_ND_indices[0].size, dtype=int) - dofs_0_DD_i_red = np.empty(dofs_0_DD_indices[0].size, dtype=int) - - dofs_1_NN_i_red = np.empty(dofs_1_NN_indices[0].size, dtype=int) - dofs_1_DN_i_red = np.empty(dofs_1_DN_indices[0].size, dtype=int) - dofs_1_ND_i_red = np.empty(dofs_1_ND_indices[0].size, dtype=int) - dofs_1_DD_i_red = np.empty(dofs_1_DD_indices[0].size, dtype=int) + dofs_0_NN_indices = xp.nonzero(dofs_0_NN) + dofs_0_DN_indices = xp.nonzero(dofs_0_DN) + dofs_0_ND_indices = xp.nonzero(dofs_0_ND) + dofs_0_DD_indices = xp.nonzero(dofs_0_DD) + + dofs_1_NN_indices = xp.nonzero(dofs_1_NN) + dofs_1_DN_indices = xp.nonzero(dofs_1_DN) + dofs_1_ND_indices = xp.nonzero(dofs_1_ND) + dofs_1_DD_indices = xp.nonzero(dofs_1_DD) + + dofs_0_NN_i_red = xp.empty(dofs_0_NN_indices[0].size, dtype=int) + dofs_0_DN_i_red = xp.empty(dofs_0_DN_indices[0].size, dtype=int) + dofs_0_ND_i_red = xp.empty(dofs_0_ND_indices[0].size, dtype=int) + dofs_0_DD_i_red = xp.empty(dofs_0_DD_indices[0].size, dtype=int) + + dofs_1_NN_i_red = xp.empty(dofs_1_NN_indices[0].size, dtype=int) + dofs_1_DN_i_red = xp.empty(dofs_1_DN_indices[0].size, dtype=int) + dofs_1_ND_i_red = xp.empty(dofs_1_ND_indices[0].size, dtype=int) + dofs_1_DD_i_red = xp.empty(dofs_1_DD_indices[0].size, dtype=int) # ================================ nv = space.NbaseN * dofs_0_NN_indices[1] + dofs_0_NN_indices[2] - un = np.unique(nv) + un = xp.unique(nv) for i in range(dofs_0_NN_indices[0].size): - dofs_0_NN_i_red[i] = np.nonzero(un == nv[i])[0] + dofs_0_NN_i_red[i] = xp.nonzero(un == nv[i])[0] # ================================ nv = space.NbaseN * dofs_0_DN_indices[1] + dofs_0_DN_indices[2] - un = np.unique(nv) + un = xp.unique(nv) for i in range(dofs_0_DN_indices[0].size): - dofs_0_DN_i_red[i] = np.nonzero(un == nv[i])[0] + dofs_0_DN_i_red[i] = xp.nonzero(un == nv[i])[0] # ================================ nv = space.NbaseD * dofs_0_ND_indices[1] + dofs_0_ND_indices[2] - un = np.unique(nv) + un = xp.unique(nv) for i in range(dofs_0_ND_indices[0].size): - dofs_0_ND_i_red[i] = np.nonzero(un == nv[i])[0] + dofs_0_ND_i_red[i] = xp.nonzero(un == nv[i])[0] # ================================ nv = space.NbaseD * dofs_0_DD_indices[1] + dofs_0_DD_indices[2] - un = np.unique(nv) + un = xp.unique(nv) for i in range(dofs_0_DD_indices[0].size): - dofs_0_DD_i_red[i] = np.nonzero(un == nv[i])[0] + dofs_0_DD_i_red[i] = xp.nonzero(un == nv[i])[0] # ================================ nv = space.NbaseN * dofs_1_NN_indices[1] + dofs_1_NN_indices[2] - un = np.unique(nv) + un = xp.unique(nv) for i in range(dofs_1_NN_indices[0].size): - dofs_1_NN_i_red[i] = np.nonzero(un == nv[i])[0] + dofs_1_NN_i_red[i] = xp.nonzero(un == nv[i])[0] # ================================ nv = space.NbaseN * dofs_1_DN_indices[1] + dofs_1_DN_indices[2] - un = np.unique(nv) + un = xp.unique(nv) for i in range(dofs_1_DN_indices[0].size): - dofs_1_DN_i_red[i] = np.nonzero(un == nv[i])[0] + dofs_1_DN_i_red[i] = xp.nonzero(un == nv[i])[0] # ================================ nv = space.NbaseD * dofs_1_ND_indices[1] + dofs_1_ND_indices[2] - un = np.unique(nv) + un = xp.unique(nv) for i in range(dofs_1_ND_indices[0].size): - dofs_1_ND_i_red[i] = np.nonzero(un == nv[i])[0] + dofs_1_ND_i_red[i] = xp.nonzero(un == nv[i])[0] # ================================ nv = space.NbaseD * dofs_1_DD_indices[1] + dofs_1_DD_indices[2] - un = np.unique(nv) + un = xp.unique(nv) for i in range(dofs_1_DD_indices[0].size): - dofs_1_DD_i_red[i] = np.nonzero(un == nv[i])[0] + dofs_1_DD_i_red[i] = xp.nonzero(un == nv[i])[0] - dofs_0_NN_indices = np.vstack( + dofs_0_NN_indices = xp.vstack( (dofs_0_NN_indices[0], dofs_0_NN_indices[1], dofs_0_NN_indices[2], dofs_0_NN_i_red) ) - dofs_0_DN_indices = np.vstack( + dofs_0_DN_indices = xp.vstack( (dofs_0_DN_indices[0], dofs_0_DN_indices[1], dofs_0_DN_indices[2], dofs_0_DN_i_red) ) - dofs_0_ND_indices = np.vstack( + dofs_0_ND_indices = xp.vstack( (dofs_0_ND_indices[0], dofs_0_ND_indices[1], dofs_0_ND_indices[2], dofs_0_ND_i_red) ) - dofs_0_DD_indices = np.vstack( + dofs_0_DD_indices = xp.vstack( (dofs_0_DD_indices[0], dofs_0_DD_indices[1], dofs_0_DD_indices[2], dofs_0_DD_i_red) ) - dofs_1_NN_indices = np.vstack( + dofs_1_NN_indices = xp.vstack( (dofs_1_NN_indices[0], dofs_1_NN_indices[1], dofs_1_NN_indices[2], dofs_1_NN_i_red) ) - dofs_1_DN_indices = np.vstack( + dofs_1_DN_indices = xp.vstack( (dofs_1_DN_indices[0], dofs_1_DN_indices[1], dofs_1_DN_indices[2], dofs_1_DN_i_red) ) - dofs_1_ND_indices = np.vstack( + dofs_1_ND_indices = xp.vstack( (dofs_1_ND_indices[0], dofs_1_ND_indices[1], dofs_1_ND_indices[2], dofs_1_ND_i_red) ) - dofs_1_DD_indices = np.vstack( + dofs_1_DD_indices = xp.vstack( (dofs_1_DD_indices[0], dofs_1_DD_indices[1], dofs_1_DD_indices[2], dofs_1_DD_i_red) ) @@ -642,8 +642,8 @@ def eval_for_PI(self, comp, fun): pts_PI = self.pts_PI[comp] - pts1, pts2 = np.meshgrid(pts_PI[0], pts_PI[1], indexing="ij") - # pts1, pts2 = np.meshgrid(pts_PI[0], pts_PI[1], indexing='ij', sparse=True) # numpy >1.7 + pts1, pts2 = xp.meshgrid(pts_PI[0], pts_PI[1], indexing="ij") + # pts1, pts2 = xp.meshgrid(pts_PI[0], pts_PI[1], indexing='ij', sparse=True) # numpy >1.7 return fun(pts1, pts2) @@ -906,8 +906,8 @@ def eval_for_PI(self, comp, fun): pts_PI = self.pts_PI[comp] - pts1, pts2, pts3 = np.meshgrid(pts_PI[0], pts_PI[1], pts_PI[2], indexing="ij") - # pts1, pts2, pts3 = np.meshgrid(pts_PI[0], pts_PI[1], pts_PI[2], indexing='ij', sparse=True) # numpy >1.7 + pts1, pts2, pts3 = xp.meshgrid(pts_PI[0], pts_PI[1], pts_PI[2], indexing="ij") + # pts1, pts2, pts3 = xp.meshgrid(pts_PI[0], pts_PI[1], pts_PI[2], indexing='ij', sparse=True) # numpy >1.7 return fun(pts1, pts2, pts3) @@ -939,25 +939,25 @@ def eval_for_PI(self, comp, fun): # rhs = mat_f # # elif comp=='11': - # rhs = np.empty( (self.d1, self.n2, self.n3) ) + # rhs = xp.empty( (self.d1, self.n2, self.n3) ) # # ker_glob.kernel_int_3d_eta1(self.subs1, self.subs_cum1, self.wts1, # mat_f.reshape(self.ne1, self.nq1, self.n2, self.n3), rhs # ) # elif comp=='12': - # rhs = np.empty( (self.n1, self.d2, self.n3) ) + # rhs = xp.empty( (self.n1, self.d2, self.n3) ) # # ker_glob.kernel_int_3d_eta2(self.subs2, self.subs_cum2, self.wts2, # mat_f.reshape(self.n1, self.ne2, self.nq2, self.n3), rhs # ) # elif comp=='13': - # rhs = np.empty( (self.n1, self.n2, self.d3) ) + # rhs = xp.empty( (self.n1, self.n2, self.d3) ) # # ker_glob.kernel_int_3d_eta3(self.subs3, self.subs_cum3, self.wts3, # mat_f.reshape(self.n1, self.n2, self.ne3, self.nq3), rhs # ) # elif comp=='21': - # rhs = np.empty( (self.n1, self.d2, self.d3) ) + # rhs = xp.empty( (self.n1, self.d2, self.d3) ) # # ker_glob.kernel_int_3d_eta2_eta3(self.subs2, self.subs3, # self.subs_cum2, self.subs_cum3, @@ -965,7 +965,7 @@ def eval_for_PI(self, comp, fun): # mat_f.reshape(self.n1, self.ne2, self.nq2, self.ne3, self.nq3), rhs # ) # elif comp=='22': - # rhs = np.empty( (self.d1, self.n2, self.d3) ) + # rhs = xp.empty( (self.d1, self.n2, self.d3) ) # # ker_glob.kernel_int_3d_eta1_eta3(self.subs1, self.subs3, # self.subs_cum1, self.subs_cum3, @@ -973,7 +973,7 @@ def eval_for_PI(self, comp, fun): # mat_f.reshape(self.ne1, self.nq1, self.n2, self.ne3, self.nq3), rhs # ) # elif comp=='23': - # rhs = np.empty( (self.d1, self.d2, self.n3) ) + # rhs = xp.empty( (self.d1, self.d2, self.n3) ) # # ker_glob.kernel_int_3d_eta1_eta2(self.subs1, self.subs2, # self.subs_cum1, self.subs_cum2, @@ -981,7 +981,7 @@ def eval_for_PI(self, comp, fun): # mat_f.reshape(self.ne1, self.nq1, self.ne2, self.nq2, self.n3), rhs # ) # elif comp=='3': - # rhs = np.empty( (self.d1, self.d2, self.d3) ) + # rhs = xp.empty( (self.d1, self.d2, self.d3) ) # # ker_glob.kernel_int_3d_eta1_eta2_eta3(self.subs1, self.subs2, self.subs3, # self.subs_cum1, self.subs_cum2, self.subs_cum3, @@ -1025,7 +1025,7 @@ def eval_for_PI(self, comp, fun): # # elif comp=='11': # assert mat_dofs.shape == (self.d1, self.n2, self.n3) - # rhs = np.empty( (self.ne1, self.nq1, self.n2, self.n3) ) + # rhs = xp.empty( (self.ne1, self.nq1, self.n2, self.n3) ) # # ker_glob.kernel_int_3d_eta1_transpose(self.subs1, self.subs_cum1, self.wts1, # mat_dofs, rhs) @@ -1034,7 +1034,7 @@ def eval_for_PI(self, comp, fun): # # elif comp=='12': # assert mat_dofs.shape == (self.n1, self.d2, self.n3) - # rhs = np.empty( (self.n1, self.ne2, self.nq2, self.n3) ) + # rhs = xp.empty( (self.n1, self.ne2, self.nq2, self.n3) ) # # ker_glob.kernel_int_3d_eta2_transpose(self.subs2, self.subs_cum2, self.wts2, # mat_dofs, rhs) @@ -1043,7 +1043,7 @@ def eval_for_PI(self, comp, fun): # # elif comp=='13': # assert mat_dofs.shape == (self.n1, self.n2, self.d3) - # rhs = np.empty( (self.n1, self.n2, self.ne3, self.nq3) ) + # rhs = xp.empty( (self.n1, self.n2, self.ne3, self.nq3) ) # # ker_glob.kernel_int_3d_eta3_transpose(self.subs3, self.subs_cum3, self.wts3, # mat_dofs, rhs) @@ -1052,7 +1052,7 @@ def eval_for_PI(self, comp, fun): # # elif comp=='21': # assert mat_dofs.shape == (self.n1, self.d2, self.d3) - # rhs = np.empty( (self.n1, self.ne2, self.nq2, self.ne3, self.nq3) ) + # rhs = xp.empty( (self.n1, self.ne2, self.nq2, self.ne3, self.nq3) ) # # ker_glob.kernel_int_3d_eta2_eta3_transpose(self.subs2, self.subs3, # self.subs_cum2, self.subs_cum3, @@ -1062,7 +1062,7 @@ def eval_for_PI(self, comp, fun): # # elif comp=='22': # assert mat_dofs.shape == (self.d1, self.n2, self.d3) - # rhs = np.empty( (self.ne1, self.nq1, self.n2, self.ne3, self.nq3) ) + # rhs = xp.empty( (self.ne1, self.nq1, self.n2, self.ne3, self.nq3) ) # # ker_glob.kernel_int_3d_eta1_eta3_transpose(self.subs1, self.subs3, # self.subs_cum1, self.subs_cum3, @@ -1072,7 +1072,7 @@ def eval_for_PI(self, comp, fun): # # elif comp=='23': # assert mat_dofs.shape == (self.d1, self.d2, self.n3) - # rhs = np.empty( (self.ne1, self.nq1, self.ne2, self.nq2, self.n3) ) + # rhs = xp.empty( (self.ne1, self.nq1, self.ne2, self.nq2, self.n3) ) # # ker_glob.kernel_int_3d_eta1_eta2_transpose(self.subs1, self.subs2, # self.subs_cum1, self.subs_cum2, @@ -1082,7 +1082,7 @@ def eval_for_PI(self, comp, fun): # # elif comp=='3': # assert mat_dofs.shape == (self.d1, self.d2, self.d3) - # rhs = np.empty( (self.ne1, self.nq1, self.ne2, self.nq2, self.ne3, self.nq3) ) + # rhs = xp.empty( (self.ne1, self.nq1, self.ne2, self.nq2, self.ne3, self.nq3) ) # # ker_glob.kernel_int_3d_eta1_eta2_eta3_transpose(self.subs1, self.subs2, self.subs3, # self.subs_cum1, self.subs_cum2, self.subs_cum3, @@ -1595,26 +1595,26 @@ def __init__(self, tensor_space): else: if tensor_space.n_tor == 0: - x_i3 = np.array([0.0]) - x_q3 = np.array([0.0]) - x_q3G = np.array([0.0]) + x_i3 = xp.array([0.0]) + x_q3 = xp.array([0.0]) + x_q3G = xp.array([0.0]) else: if tensor_space.basis_tor == "r": if tensor_space.n_tor > 0: - x_i3 = np.array([1.0, 0.25 / tensor_space.n_tor]) - x_q3 = np.array([1.0, 0.25 / tensor_space.n_tor]) - x_q3G = np.array([1.0, 0.25 / tensor_space.n_tor]) + x_i3 = xp.array([1.0, 0.25 / tensor_space.n_tor]) + x_q3 = xp.array([1.0, 0.25 / tensor_space.n_tor]) + x_q3G = xp.array([1.0, 0.25 / tensor_space.n_tor]) else: - x_i3 = np.array([1.0, 0.75 / (-tensor_space.n_tor)]) - x_q3 = np.array([1.0, 0.75 / (-tensor_space.n_tor)]) - x_q3G = np.array([1.0, 0.75 / (-tensor_space.n_tor)]) + x_i3 = xp.array([1.0, 0.75 / (-tensor_space.n_tor)]) + x_q3 = xp.array([1.0, 0.75 / (-tensor_space.n_tor)]) + x_q3G = xp.array([1.0, 0.75 / (-tensor_space.n_tor)]) else: - x_i3 = np.array([0.0]) - x_q3 = np.array([0.0]) - x_q3G = np.array([0.0]) + x_i3 = xp.array([0.0]) + x_q3 = xp.array([0.0]) + x_q3G = xp.array([0.0]) self.Q3 = spa.identity(tensor_space.NbaseN[2], format="csr") self.Q3G = spa.identity(tensor_space.NbaseN[2], format="csr") @@ -1756,11 +1756,11 @@ def eval_for_PI(self, comp, fun, eval_kind, with_subs=True): pts_PI = self.getpts_for_PI(comp, with_subs) # array of evaluated function - mat_f = np.empty((pts_PI[0].size, pts_PI[1].size, pts_PI[2].size), dtype=float) + mat_f = xp.empty((pts_PI[0].size, pts_PI[1].size, pts_PI[2].size), dtype=float) # create a meshgrid and evaluate function on point set if eval_kind == "meshgrid": - pts1, pts2, pts3 = np.meshgrid(pts_PI[0], pts_PI[1], pts_PI[2], indexing="ij") + pts1, pts2, pts3 = xp.meshgrid(pts_PI[0], pts_PI[1], pts_PI[2], indexing="ij") mat_f[:, :, :] = fun(pts1, pts2, pts3) # tensor-product evaluation is done by input function @@ -1783,13 +1783,13 @@ def eval_for_PI(self, comp, fun, eval_kind, with_subs=True): # n2 = self.pts_PI_0[1].size # # # apply (I0_22) to each column - # self.S0 = np.zeros(((n1 - 2)*n2, 3), dtype=float) + # self.S0 = xp.zeros(((n1 - 2)*n2, 3), dtype=float) # # for i in range(3): # self.S0[:, i] = kron_lusolve_2d(self.I0_22_LUs, self.I0_21[:, i].toarray().reshape(n1 - 2, n2)).flatten() # # # 3 x 3 matrix - # self.S0 = np.linalg.inv(self.I0_11.toarray() - self.I0_12.toarray().dot(self.S0)) + # self.S0 = xp.linalg.inv(self.I0_11.toarray() - self.I0_12.toarray().dot(self.S0)) # # # ====================================== @@ -1814,7 +1814,7 @@ def eval_for_PI(self, comp, fun, eval_kind, with_subs=True): # # solve for tensor-product coefficients # out2 = out2 - kron_lusolve_2d(self.I0_22_LUs, self.I0_21.dot(self.S0.dot(rhs1)).reshape(n1 - 2, n2)) + kron_lusolve_2d(self.I0_22_LUs, self.I0_21.dot(self.S0.dot(self.I0_12.dot(out2.flatten()))).reshape(n1 - 2, n2)) # - # return np.concatenate((out1, out2.flatten())) + # return xp.concatenate((out1, out2.flatten())) # ====================================== @@ -1857,7 +1857,7 @@ def solve_V1(self, dofs_1, include_bc): coeffs1 = self.I0_tor_LU.solve(self.I1_pol_0_LU.solve(dofs_11).T).T coeffs2 = self.H0_tor_LU.solve(self.I0_pol_0_LU.solve(dofs_12).T).T - return np.concatenate((coeffs1.flatten(), coeffs2.flatten())) + return xp.concatenate((coeffs1.flatten(), coeffs2.flatten())) # ====================================== def solve_V2(self, dofs_2, include_bc): @@ -1885,7 +1885,7 @@ def solve_V2(self, dofs_2, include_bc): coeffs1 = self.H0_tor_LU.solve(self.I2_pol_0_LU.solve(dofs_21).T).T coeffs2 = self.I0_tor_LU.solve(self.I3_pol_0_LU.solve(dofs_22).T).T - return np.concatenate((coeffs1.flatten(), coeffs2.flatten())) + return xp.concatenate((coeffs1.flatten(), coeffs2.flatten())) # ====================================== def solve_V3(self, dofs_3, include_bc): @@ -1947,7 +1947,7 @@ def apply_IinvT_V1(self, rhs, include_bc=False): rhs1 = self.I1_pol_0_T_LU.solve(self.I0_tor_T_LU.solve(rhs1.T).T) rhs2 = self.I0_pol_0_T_LU.solve(self.H0_tor_T_LU.solve(rhs2.T).T) - return np.concatenate((rhs1.flatten(), rhs2.flatten())) + return xp.concatenate((rhs1.flatten(), rhs2.flatten())) # ====================================== def apply_IinvT_V2(self, rhs, include_bc=False): @@ -1977,7 +1977,7 @@ def apply_IinvT_V2(self, rhs, include_bc=False): rhs1 = self.I2_pol_0_T_LU.solve(self.H0_tor_T_LU.solve(rhs1.T).T) rhs2 = self.I3_pol_0_T_LU.solve(self.I0_tor_T_LU.solve(rhs2.T).T) - return np.concatenate((rhs1.flatten(), rhs2.flatten())) + return xp.concatenate((rhs1.flatten(), rhs2.flatten())) # ====================================== def apply_IinvT_V3(self, rhs, include_bc=False): @@ -2042,9 +2042,9 @@ def dofs_1(self, fun, include_bc=True, eval_kind="meshgrid", with_subs=True): # apply extraction operator for dofs if include_bc: - dofs = self.P1.dot(np.concatenate((dofs_1.flatten(), dofs_2.flatten(), dofs_3.flatten()))) + dofs = self.P1.dot(xp.concatenate((dofs_1.flatten(), dofs_2.flatten(), dofs_3.flatten()))) else: - dofs = self.P1_0.dot(np.concatenate((dofs_1.flatten(), dofs_2.flatten(), dofs_3.flatten()))) + dofs = self.P1_0.dot(xp.concatenate((dofs_1.flatten(), dofs_2.flatten(), dofs_3.flatten()))) return dofs @@ -2075,9 +2075,9 @@ def dofs_2(self, fun, include_bc=True, eval_kind="meshgrid", with_subs=True): # apply extraction operator for dofs if include_bc: - dofs = self.P2.dot(np.concatenate((dofs_1.flatten(), dofs_2.flatten(), dofs_3.flatten()))) + dofs = self.P2.dot(xp.concatenate((dofs_1.flatten(), dofs_2.flatten(), dofs_3.flatten()))) else: - dofs = self.P2_0.dot(np.concatenate((dofs_1.flatten(), dofs_2.flatten(), dofs_3.flatten()))) + dofs = self.P2_0.dot(xp.concatenate((dofs_1.flatten(), dofs_2.flatten(), dofs_3.flatten()))) return dofs @@ -2122,18 +2122,18 @@ def pi_3(self, fun, include_bc=True, eval_kind="meshgrid", with_subs=True): def assemble_approx_inv(self, tol): if self.approx_Ik_0_inv == False or (self.approx_Ik_0_inv == True and self.approx_Ik_0_tol != tol): # poloidal plane - I0_pol_0_inv_approx = np.linalg.inv(self.I0_pol_0.toarray()) - I1_pol_0_inv_approx = np.linalg.inv(self.I1_pol_0.toarray()) - I2_pol_0_inv_approx = np.linalg.inv(self.I2_pol_0.toarray()) - I3_pol_0_inv_approx = np.linalg.inv(self.I3_pol_0.toarray()) - I0_pol_inv_approx = np.linalg.inv(self.I0_pol.toarray()) + I0_pol_0_inv_approx = xp.linalg.inv(self.I0_pol_0.toarray()) + I1_pol_0_inv_approx = xp.linalg.inv(self.I1_pol_0.toarray()) + I2_pol_0_inv_approx = xp.linalg.inv(self.I2_pol_0.toarray()) + I3_pol_0_inv_approx = xp.linalg.inv(self.I3_pol_0.toarray()) + I0_pol_inv_approx = xp.linalg.inv(self.I0_pol.toarray()) if tol > 1e-14: - I0_pol_0_inv_approx[np.abs(I0_pol_0_inv_approx) < tol] = 0.0 - I1_pol_0_inv_approx[np.abs(I1_pol_0_inv_approx) < tol] = 0.0 - I2_pol_0_inv_approx[np.abs(I2_pol_0_inv_approx) < tol] = 0.0 - I3_pol_0_inv_approx[np.abs(I3_pol_0_inv_approx) < tol] = 0.0 - I0_pol_inv_approx[np.abs(I0_pol_inv_approx) < tol] = 0.0 + I0_pol_0_inv_approx[xp.abs(I0_pol_0_inv_approx) < tol] = 0.0 + I1_pol_0_inv_approx[xp.abs(I1_pol_0_inv_approx) < tol] = 0.0 + I2_pol_0_inv_approx[xp.abs(I2_pol_0_inv_approx) < tol] = 0.0 + I3_pol_0_inv_approx[xp.abs(I3_pol_0_inv_approx) < tol] = 0.0 + I0_pol_inv_approx[xp.abs(I0_pol_inv_approx) < tol] = 0.0 I0_pol_0_inv_approx = spa.csr_matrix(I0_pol_0_inv_approx) I1_pol_0_inv_approx = spa.csr_matrix(I1_pol_0_inv_approx) @@ -2142,12 +2142,12 @@ def assemble_approx_inv(self, tol): I0_pol_inv_approx = spa.csr_matrix(I0_pol_inv_approx) # toroidal direction - I_inv_tor_approx = np.linalg.inv(self.I_tor.toarray()) - H_inv_tor_approx = np.linalg.inv(self.H_tor.toarray()) + I_inv_tor_approx = xp.linalg.inv(self.I_tor.toarray()) + H_inv_tor_approx = xp.linalg.inv(self.H_tor.toarray()) if tol > 1e-14: - I_inv_tor_approx[np.abs(I_inv_tor_approx) < tol] = 0.0 - H_inv_tor_approx[np.abs(H_inv_tor_approx) < tol] = 0.0 + I_inv_tor_approx[xp.abs(I_inv_tor_approx) < tol] = 0.0 + H_inv_tor_approx[xp.abs(H_inv_tor_approx) < tol] = 0.0 I_inv_tor_approx = spa.csr_matrix(I_inv_tor_approx) H_inv_tor_approx = spa.csr_matrix(H_inv_tor_approx) diff --git a/src/struphy/eigenvalue_solvers/spline_space.py b/src/struphy/eigenvalue_solvers/spline_space.py index cffc1902d..b36ad73db 100644 --- a/src/struphy/eigenvalue_solvers/spline_space.py +++ b/src/struphy/eigenvalue_solvers/spline_space.py @@ -6,11 +6,10 @@ Basic modules to create tensor-product finite element spaces of univariate B-splines. """ +import cunumpy as xp import matplotlib import scipy.sparse as spa -from struphy.utils.arrays import xp as np - matplotlib.rcParams.update({"font.size": 16}) import matplotlib.pyplot as plt @@ -50,19 +49,19 @@ class Spline_space_1d: Attributes ---------- - el_b : np.array + el_b : xp.array Element boundaries, equally spaced. delta : float Uniform grid spacing - T : np.array + T : xp.array Knot vector of 0-space. - t : np.arrray + t : xp.arrray Knot vector of 1-space. - greville : np.array + greville : xp.array Greville points. NbaseN : int @@ -71,22 +70,22 @@ class Spline_space_1d: NbaseD : int Dimension of 1-space. - indN : np.array + indN : xp.array Global indices of non-vanishing B-splines in each element in format (element, local basis function) - indD : np.array + indD : xp.array Global indices of non-vanishing M-splines in each element in format (element, local basis function) - pts : np.array + pts : xp.array Global GL quadrature points in format (element, local point). - wts : np.array + wts : xp.array Global GL quadrature weights in format (element, local point). - basisN : np.array + basisN : xp.array N-basis functions evaluated at quadrature points in format (element, local basis function, derivative, local point) - basisD : np.array + basisD : xp.array D-basis functions evaluated at quadrature points in format (element, local basis function, derivative, local point) E0 : csr_matrix @@ -140,7 +139,7 @@ def __init__(self, Nel, p, spl_kind, n_quad=6, bc=["f", "f"]): else: self.bc = bc - self.el_b = np.linspace(0.0, 1.0, Nel + 1) # element boundaries + self.el_b = xp.linspace(0.0, 1.0, Nel + 1) # element boundaries self.delta = 1 / self.Nel # element length self.T = bsp.make_knots(self.el_b, self.p, self.spl_kind) # spline knot vector for B-splines (N) @@ -152,13 +151,13 @@ def __init__(self, Nel, p, spl_kind, n_quad=6, bc=["f", "f"]): self.NbaseD = self.NbaseN - 1 + self.spl_kind # total number of M-splines (D) # global indices of non-vanishing splines in each element in format (Nel, p + 1) - self.indN = (np.indices((self.Nel, self.p + 1 - 0))[1] + np.arange(self.Nel)[:, None]) % self.NbaseN - self.indD = (np.indices((self.Nel, self.p + 1 - 1))[1] + np.arange(self.Nel)[:, None]) % self.NbaseD + self.indN = (xp.indices((self.Nel, self.p + 1 - 0))[1] + xp.arange(self.Nel)[:, None]) % self.NbaseN + self.indD = (xp.indices((self.Nel, self.p + 1 - 1))[1] + xp.arange(self.Nel)[:, None]) % self.NbaseD self.n_quad = n_quad # number of Gauss-Legendre points per grid cell (defined by break points) - self.pts_loc = np.polynomial.legendre.leggauss(self.n_quad)[0] # Gauss-Legendre points (GLQP) in (-1, 1) - self.wts_loc = np.polynomial.legendre.leggauss(self.n_quad)[1] # Gauss-Legendre weights (GLQW) in (-1, 1) + self.pts_loc = xp.polynomial.legendre.leggauss(self.n_quad)[0] # Gauss-Legendre points (GLQP) in (-1, 1) + self.wts_loc = xp.polynomial.legendre.leggauss(self.n_quad)[1] # Gauss-Legendre weights (GLQW) in (-1, 1) # global GLQP in format (element, local point) and total number of GLQP self.pts = bsp.quadrature_grid(self.el_b, self.pts_loc, self.wts_loc)[0] @@ -178,8 +177,8 @@ def __init__(self, Nel, p, spl_kind, n_quad=6, bc=["f", "f"]): d1 = self.NbaseD # boundary operators - self.B0 = np.identity(n1, dtype=float) - self.B1 = np.identity(d1, dtype=float) + self.B0 = xp.identity(n1, dtype=float) + self.B1 = xp.identity(d1, dtype=float) # extraction operators without boundary conditions self.E0 = spa.csr_matrix(self.B0.copy()) @@ -268,16 +267,16 @@ def evaluate_N(self, eta, coeff, kind=0): coeff = self.E0_0.T.dot(coeff) if isinstance(eta, float): - pts = np.array([eta]) - elif isinstance(eta, np.ndarray): + pts = xp.array([eta]) + elif isinstance(eta, xp.ndarray): pts = eta.flatten() - values = np.empty(pts.size, dtype=float) + values = xp.empty(pts.size, dtype=float) eva_1d.evaluate_vector(self.T, self.p, self.indN, coeff, pts, values, kind) if isinstance(eta, float): values = values[0] - elif isinstance(eta, np.ndarray): + elif isinstance(eta, xp.ndarray): values = values.reshape(eta.shape) return values @@ -304,16 +303,16 @@ def evaluate_D(self, eta, coeff): assert coeff.size == self.E1.shape[0] if isinstance(eta, float): - pts = np.array([eta]) - elif isinstance(eta, np.ndarray): + pts = xp.array([eta]) + elif isinstance(eta, xp.ndarray): pts = eta.flatten() - values = np.empty(pts.size, dtype=float) + values = xp.empty(pts.size, dtype=float) eva_1d.evaluate_vector(self.t, self.p - 1, self.indD, coeff, pts, values, 1) if isinstance(eta, float): values = values[0] - elif isinstance(eta, np.ndarray): + elif isinstance(eta, xp.ndarray): values = values.reshape(eta.shape) return values @@ -332,12 +331,12 @@ def plot_splines(self, n_pts=500, which="N"): which basis to plot. 'N', 'D' or 'dN' (optional, default='N') """ - etaplot = np.linspace(0.0, 1.0, n_pts) + etaplot = xp.linspace(0.0, 1.0, n_pts) degree = self.p if which == "N": - coeff = np.zeros(self.NbaseN, dtype=float) + coeff = xp.zeros(self.NbaseN, dtype=float) for i in range(self.NbaseN): coeff[:] = 0.0 @@ -345,7 +344,7 @@ def plot_splines(self, n_pts=500, which="N"): plt.plot(etaplot, self.evaluate_N(etaplot, coeff), label=str(i)) elif which == "D": - coeff = np.zeros(self.NbaseD, dtype=float) + coeff = xp.zeros(self.NbaseD, dtype=float) for i in range(self.NbaseD): coeff[:] = 0.0 @@ -355,7 +354,7 @@ def plot_splines(self, n_pts=500, which="N"): degree = self.p - 1 elif which == "dN": - coeff = np.zeros(self.NbaseN, dtype=float) + coeff = xp.zeros(self.NbaseN, dtype=float) for i in range(self.NbaseN): coeff[:] = 0.0 @@ -370,8 +369,8 @@ def plot_splines(self, n_pts=500, which="N"): else: bcs = "clamped" - (greville,) = plt.plot(self.greville, np.zeros(self.greville.shape), "ro", label="greville") - (breaks,) = plt.plot(self.el_b, np.zeros(self.el_b.shape), "k+", label="breaks") + (greville,) = plt.plot(self.greville, xp.zeros(self.greville.shape), "ro", label="greville") + (breaks,) = plt.plot(self.el_b, xp.zeros(self.el_b.shape), "k+", label="breaks") plt.title(which + f"$^{degree}$-splines, " + bcs + f", Nel={self.Nel}") plt.legend(handles=[greville, breaks]) @@ -555,8 +554,8 @@ def __init__(self, spline_spaces, ck=-1, cx=None, cy=None, n_tor=0, basis_tor="r self.M1_tor = spa.identity(1, format="csr") else: - self.M0_tor = spa.csr_matrix(np.identity(2) / 2) - self.M1_tor = spa.csr_matrix(np.identity(2) / 2) + self.M0_tor = spa.csr_matrix(xp.identity(2) / 2) + self.M1_tor = spa.csr_matrix(xp.identity(2) / 2) else: self.M0_tor = mass_1d.get_M(self.spaces[2], 0, 0) @@ -786,7 +785,7 @@ def apply_M1_ten(self, x, mats): out1 = mats[0][1].dot(mats[0][0].dot(x1).T).T out2 = mats[1][1].dot(mats[1][0].dot(x2).T).T - return np.concatenate((out1.flatten(), out2.flatten())) + return xp.concatenate((out1.flatten(), out2.flatten())) def apply_M2_ten(self, x, mats): """ @@ -798,7 +797,7 @@ def apply_M2_ten(self, x, mats): out1 = mats[0][1].dot(mats[0][0].dot(x1).T).T out2 = mats[1][1].dot(mats[1][0].dot(x2).T).T - return np.concatenate((out1.flatten(), out2.flatten())) + return xp.concatenate((out1.flatten(), out2.flatten())) def apply_M3_ten(self, x, mats): """ @@ -821,7 +820,7 @@ def apply_Mv_ten(self, x, mats): out1 = mats[0][1].dot(mats[0][0].dot(x1).T).T out2 = mats[1][1].dot(mats[1][0].dot(x2).T).T - return np.concatenate((out1.flatten(), out2.flatten())) + return xp.concatenate((out1.flatten(), out2.flatten())) def apply_M0_0_ten(self, x, mats): """ @@ -848,7 +847,7 @@ def apply_M1_0_ten(self, x, mats): mats[1][1].dot(self.B1_tor.T.dot(self.B0_pol.dot(mats[1][0].dot(self.B0_pol.T.dot(x2))).T)) ).T - return np.concatenate((out1.flatten(), out2.flatten())) + return xp.concatenate((out1.flatten(), out2.flatten())) def apply_M2_0_ten(self, x, mats): """ @@ -864,7 +863,7 @@ def apply_M2_0_ten(self, x, mats): mats[1][1].dot(self.B0_tor.T.dot(self.B3_pol.dot(mats[1][0].dot(self.B3_pol.T.dot(x2))).T)) ).T - return np.concatenate((out1.flatten(), out2.flatten())) + return xp.concatenate((out1.flatten(), out2.flatten())) def apply_M3_0_ten(self, x, mats): """ @@ -887,7 +886,7 @@ def apply_Mv_0_ten(self, x, mats): out1 = mats[0][1].dot(self.Bv_pol.dot(mats[0][0].dot(self.Bv_pol.T.dot(x1))).T).T out2 = self.B0_tor.dot(mats[1][1].dot(self.B0_tor.T.dot(mats[1][0].dot(x2).T))).T - return np.concatenate((out1.flatten(), out2.flatten())) + return xp.concatenate((out1.flatten(), out2.flatten())) def __assemble_M0(self, domain, as_tensor=False): """ @@ -1229,7 +1228,7 @@ def extract_1(self, coeff): else: coeff1 = self.E1_0.T.dot(coeff) - coeff1_1, coeff1_2, coeff1_3 = np.split(coeff1, [self.Ntot_1form_cum[0], self.Ntot_1form_cum[1]]) + coeff1_1, coeff1_2, coeff1_3 = xp.split(coeff1, [self.Ntot_1form_cum[0], self.Ntot_1form_cum[1]]) coeff1_1 = coeff1_1.reshape(self.Nbase_1form[0]) coeff1_2 = coeff1_2.reshape(self.Nbase_1form[1]) @@ -1260,7 +1259,7 @@ def extract_2(self, coeff): else: coeff2 = self.E2_0.T.dot(coeff) - coeff2_1, coeff2_2, coeff2_3 = np.split(coeff2, [self.Ntot_2form_cum[0], self.Ntot_2form_cum[1]]) + coeff2_1, coeff2_2, coeff2_3 = xp.split(coeff2, [self.Ntot_2form_cum[0], self.Ntot_2form_cum[1]]) coeff2_1 = coeff2_1.reshape(self.Nbase_2form[0]) coeff2_2 = coeff2_2.reshape(self.Nbase_2form[1]) @@ -1305,7 +1304,7 @@ def extract_v(self, coeff): else: coeffv = self.Ev_0.T.dot(coeff) - coeffv_1, coeffv_2, coeffv_3 = np.split(coeffv, [self.Ntot_0form, 2 * self.Ntot_0form]) + coeffv_1, coeffv_2, coeffv_3 = xp.split(coeffv, [self.Ntot_0form, 2 * self.Ntot_0form]) coeffv_1 = coeffv_1.reshape(self.Nbase_0form) coeffv_2 = coeffv_2.reshape(self.Nbase_0form) @@ -1358,15 +1357,15 @@ def evaluate_NN(self, eta1, eta2, eta3, coeff, which="V0", part="r"): assert coeff.shape[:2] == (self.NbaseN[0], self.NbaseN[1]) # get real and imaginary part - coeff_r = np.real(coeff) - coeff_i = np.imag(coeff) + coeff_r = xp.real(coeff) + coeff_i = xp.imag(coeff) # ------ evaluate FEM field at given points -------- - if isinstance(eta1, np.ndarray): + if isinstance(eta1, xp.ndarray): # tensor-product evaluation if eta1.ndim == 1: - values_r_1 = np.empty((eta1.shape[0], eta2.shape[0]), dtype=float) - values_i_1 = np.empty((eta1.shape[0], eta2.shape[0]), dtype=float) + values_r_1 = xp.empty((eta1.shape[0], eta2.shape[0]), dtype=float) + values_i_1 = xp.empty((eta1.shape[0], eta2.shape[0]), dtype=float) eva_2d.evaluate_tensor_product_2d( self.T[0], @@ -1396,8 +1395,8 @@ def evaluate_NN(self, eta1, eta2, eta3, coeff, which="V0", part="r"): ) if self.n_tor != 0 and self.basis_tor == "r": - values_r_2 = np.empty((eta1.shape[0], eta2.shape[0]), dtype=float) - values_i_2 = np.empty((eta1.shape[0], eta2.shape[0]), dtype=float) + values_r_2 = xp.empty((eta1.shape[0], eta2.shape[0]), dtype=float) + values_i_2 = xp.empty((eta1.shape[0], eta2.shape[0]), dtype=float) eva_2d.evaluate_tensor_product_2d( self.T[0], @@ -1428,8 +1427,8 @@ def evaluate_NN(self, eta1, eta2, eta3, coeff, which="V0", part="r"): # matrix evaluation else: - values_r_1 = np.empty((eta1.shape[0], eta2.shape[1]), dtype=float) - values_i_1 = np.empty((eta1.shape[0], eta2.shape[1]), dtype=float) + values_r_1 = xp.empty((eta1.shape[0], eta2.shape[1]), dtype=float) + values_i_1 = xp.empty((eta1.shape[0], eta2.shape[1]), dtype=float) eva_2d.evaluate_matrix_2d( self.T[0], @@ -1459,8 +1458,8 @@ def evaluate_NN(self, eta1, eta2, eta3, coeff, which="V0", part="r"): ) if self.n_tor != 0 and self.basis_tor == "r": - values_r_2 = np.empty((eta1.shape[0], eta2.shape[1]), dtype=float) - values_i_2 = np.empty((eta1.shape[0], eta2.shape[1]), dtype=float) + values_r_2 = xp.empty((eta1.shape[0], eta2.shape[1]), dtype=float) + values_i_2 = xp.empty((eta1.shape[0], eta2.shape[1]), dtype=float) eva_2d.evaluate_matrix_2d( self.T[0], @@ -1491,15 +1490,15 @@ def evaluate_NN(self, eta1, eta2, eta3, coeff, which="V0", part="r"): # multiply with Fourier basis in third direction if self.n_tor == 0: - out = (values_r_1 + 1j * values_i_1)[:, :, None] * np.ones(eta3.shape, dtype=float) + out = (values_r_1 + 1j * values_i_1)[:, :, None] * xp.ones(eta3.shape, dtype=float) else: if self.basis_tor == "r": - out = (values_r_1 + 1j * values_i_1)[:, :, None] * np.cos(2 * np.pi * self.n_tor * eta3) - out += (values_r_2 + 1j * values_i_2)[:, :, None] * np.sin(2 * np.pi * self.n_tor * eta3) + out = (values_r_1 + 1j * values_i_1)[:, :, None] * xp.cos(2 * xp.pi * self.n_tor * eta3) + out += (values_r_2 + 1j * values_i_2)[:, :, None] * xp.sin(2 * xp.pi * self.n_tor * eta3) else: - out = (values_r_1 + 1j * values_i_1)[:, :, None] * np.exp(1j * 2 * np.pi * self.n_tor * eta3) + out = (values_r_1 + 1j * values_i_1)[:, :, None] * xp.exp(1j * 2 * xp.pi * self.n_tor * eta3) # --------- evaluate FEM field at given point ------- else: @@ -1540,17 +1539,17 @@ def evaluate_NN(self, eta1, eta2, eta3, coeff, which="V0", part="r"): else: if self.basis_tor == "r": - out = (real_1 + 1j * imag_1) * np.cos(2 * np.pi * self.n_tor * eta3) - out += (real_2 + 1j * imag_2) * np.sin(2 * np.pi * self.n_tor * eta3) + out = (real_1 + 1j * imag_1) * xp.cos(2 * xp.pi * self.n_tor * eta3) + out += (real_2 + 1j * imag_2) * xp.sin(2 * xp.pi * self.n_tor * eta3) else: - out = (real_1 + 1j * imag_1) * np.exp(1j * 2 * np.pi * self.n_tor * eta3) + out = (real_1 + 1j * imag_1) * xp.exp(1j * 2 * xp.pi * self.n_tor * eta3) # return real or imaginary part if part == "r": - out = np.real(out) + out = xp.real(out) else: - out = np.imag(out) + out = xp.imag(out) return out @@ -1599,15 +1598,15 @@ def evaluate_DN(self, eta1, eta2, eta3, coeff, which="V1", part="r"): assert coeff.shape[:2] == (self.NbaseD[0], self.NbaseN[1]) # get real and imaginary part - coeff_r = np.real(coeff) - coeff_i = np.imag(coeff) + coeff_r = xp.real(coeff) + coeff_i = xp.imag(coeff) # ------ evaluate FEM field at given points -------- - if isinstance(eta1, np.ndarray): + if isinstance(eta1, xp.ndarray): # tensor-product evaluation if eta1.ndim == 1: - values_r_1 = np.empty((eta1.shape[0], eta2.shape[0]), dtype=float) - values_i_1 = np.empty((eta1.shape[0], eta2.shape[0]), dtype=float) + values_r_1 = xp.empty((eta1.shape[0], eta2.shape[0]), dtype=float) + values_i_1 = xp.empty((eta1.shape[0], eta2.shape[0]), dtype=float) eva_2d.evaluate_tensor_product_2d( self.t[0], @@ -1637,8 +1636,8 @@ def evaluate_DN(self, eta1, eta2, eta3, coeff, which="V1", part="r"): ) if self.n_tor != 0 and self.basis_tor == "r": - values_r_2 = np.empty((eta1.shape[0], eta2.shape[0]), dtype=float) - values_i_2 = np.empty((eta1.shape[0], eta2.shape[0]), dtype=float) + values_r_2 = xp.empty((eta1.shape[0], eta2.shape[0]), dtype=float) + values_i_2 = xp.empty((eta1.shape[0], eta2.shape[0]), dtype=float) eva_2d.evaluate_tensor_product_2d( self.t[0], @@ -1669,8 +1668,8 @@ def evaluate_DN(self, eta1, eta2, eta3, coeff, which="V1", part="r"): # matrix evaluation else: - values_r_1 = np.empty((eta1.shape[0], eta2.shape[1]), dtype=float) - values_i_1 = np.empty((eta1.shape[0], eta2.shape[1]), dtype=float) + values_r_1 = xp.empty((eta1.shape[0], eta2.shape[1]), dtype=float) + values_i_1 = xp.empty((eta1.shape[0], eta2.shape[1]), dtype=float) eva_2d.evaluate_matrix_2d( self.t[0], @@ -1700,8 +1699,8 @@ def evaluate_DN(self, eta1, eta2, eta3, coeff, which="V1", part="r"): ) if self.n_tor != 0 and self.basis_tor == "r": - values_r_2 = np.empty((eta1.shape[0], eta2.shape[1]), dtype=float) - values_i_2 = np.empty((eta1.shape[0], eta2.shape[1]), dtype=float) + values_r_2 = xp.empty((eta1.shape[0], eta2.shape[1]), dtype=float) + values_i_2 = xp.empty((eta1.shape[0], eta2.shape[1]), dtype=float) eva_2d.evaluate_matrix_2d( self.t[0], @@ -1732,15 +1731,15 @@ def evaluate_DN(self, eta1, eta2, eta3, coeff, which="V1", part="r"): # multiply with Fourier basis in third direction if self.n_tor == 0: - out = (values_r_1 + 1j * values_i_1)[:, :, None] * np.ones(eta3.shape, dtype=float) + out = (values_r_1 + 1j * values_i_1)[:, :, None] * xp.ones(eta3.shape, dtype=float) else: if self.basis_tor == "r": - out = (values_r_1 + 1j * values_i_1)[:, :, None] * np.cos(2 * np.pi * self.n_tor * eta3) - out += (values_r_2 + 1j * values_i_2)[:, :, None] * np.sin(2 * np.pi * self.n_tor * eta3) + out = (values_r_1 + 1j * values_i_1)[:, :, None] * xp.cos(2 * xp.pi * self.n_tor * eta3) + out += (values_r_2 + 1j * values_i_2)[:, :, None] * xp.sin(2 * xp.pi * self.n_tor * eta3) else: - out = (values_r_1 + 1j * values_i_1)[:, :, None] * np.exp(1j * 2 * np.pi * self.n_tor * eta3) + out = (values_r_1 + 1j * values_i_1)[:, :, None] * xp.exp(1j * 2 * xp.pi * self.n_tor * eta3) # --------- evaluate FEM field at given point ------- else: @@ -1797,17 +1796,17 @@ def evaluate_DN(self, eta1, eta2, eta3, coeff, which="V1", part="r"): else: if self.basis_tor == "r": - out = (real_1 + 1j * imag_1) * np.cos(2 * np.pi * self.n_tor * eta3) - out += (real_2 + 1j * imag_2) * np.sin(2 * np.pi * self.n_tor * eta3) + out = (real_1 + 1j * imag_1) * xp.cos(2 * xp.pi * self.n_tor * eta3) + out += (real_2 + 1j * imag_2) * xp.sin(2 * xp.pi * self.n_tor * eta3) else: - out = (real_1 + 1j * imag_1) * np.exp(1j * 2 * np.pi * self.n_tor * eta3) + out = (real_1 + 1j * imag_1) * xp.exp(1j * 2 * xp.pi * self.n_tor * eta3) # return real or imaginary part if part == "r": - out = np.real(out) + out = xp.real(out) else: - out = np.imag(out) + out = xp.imag(out) return out @@ -1856,15 +1855,15 @@ def evaluate_ND(self, eta1, eta2, eta3, coeff, which="V2", part="r"): assert coeff.shape[:2] == (self.NbaseN[0], self.NbaseD[1]) # get real and imaginary part - coeff_r = np.real(coeff) - coeff_i = np.imag(coeff) + coeff_r = xp.real(coeff) + coeff_i = xp.imag(coeff) # ------ evaluate FEM field at given points -------- - if isinstance(eta1, np.ndarray): + if isinstance(eta1, xp.ndarray): # tensor-product evaluation if eta1.ndim == 1: - values_r_1 = np.empty((eta1.shape[0], eta2.shape[0]), dtype=float) - values_i_1 = np.empty((eta1.shape[0], eta2.shape[0]), dtype=float) + values_r_1 = xp.empty((eta1.shape[0], eta2.shape[0]), dtype=float) + values_i_1 = xp.empty((eta1.shape[0], eta2.shape[0]), dtype=float) eva_2d.evaluate_tensor_product_2d( self.T[0], @@ -1894,8 +1893,8 @@ def evaluate_ND(self, eta1, eta2, eta3, coeff, which="V2", part="r"): ) if self.n_tor != 0 and self.basis_tor == "r": - values_r_2 = np.empty((eta1.shape[0], eta2.shape[0]), dtype=float) - values_i_2 = np.empty((eta1.shape[0], eta2.shape[0]), dtype=float) + values_r_2 = xp.empty((eta1.shape[0], eta2.shape[0]), dtype=float) + values_i_2 = xp.empty((eta1.shape[0], eta2.shape[0]), dtype=float) eva_2d.evaluate_tensor_product_2d( self.T[0], @@ -1926,8 +1925,8 @@ def evaluate_ND(self, eta1, eta2, eta3, coeff, which="V2", part="r"): # matrix evaluation else: - values_r_1 = np.empty((eta1.shape[0], eta2.shape[1]), dtype=float) - values_i_1 = np.empty((eta1.shape[0], eta2.shape[1]), dtype=float) + values_r_1 = xp.empty((eta1.shape[0], eta2.shape[1]), dtype=float) + values_i_1 = xp.empty((eta1.shape[0], eta2.shape[1]), dtype=float) eva_2d.evaluate_matrix_2d( self.T[0], @@ -1957,8 +1956,8 @@ def evaluate_ND(self, eta1, eta2, eta3, coeff, which="V2", part="r"): ) if self.n_tor != 0 and self.basis_tor == "r": - values_r_2 = np.empty((eta1.shape[0], eta2.shape[1]), dtype=float) - values_i_2 = np.empty((eta1.shape[0], eta2.shape[1]), dtype=float) + values_r_2 = xp.empty((eta1.shape[0], eta2.shape[1]), dtype=float) + values_i_2 = xp.empty((eta1.shape[0], eta2.shape[1]), dtype=float) eva_2d.evaluate_matrix_2d( self.T[0], @@ -1989,15 +1988,15 @@ def evaluate_ND(self, eta1, eta2, eta3, coeff, which="V2", part="r"): # multiply with Fourier basis in third direction if self.n_tor == 0: - out = (values_r_1 + 1j * values_i_1)[:, :, None] * np.ones(eta3.shape, dtype=float) + out = (values_r_1 + 1j * values_i_1)[:, :, None] * xp.ones(eta3.shape, dtype=float) else: if self.basis_tor == "r": - out = (values_r_1 + 1j * values_i_1)[:, :, None] * np.cos(2 * np.pi * self.n_tor * eta3) - out += (values_r_2 + 1j * values_i_2)[:, :, None] * np.sin(2 * np.pi * self.n_tor * eta3) + out = (values_r_1 + 1j * values_i_1)[:, :, None] * xp.cos(2 * xp.pi * self.n_tor * eta3) + out += (values_r_2 + 1j * values_i_2)[:, :, None] * xp.sin(2 * xp.pi * self.n_tor * eta3) else: - out = (values_r_1 + 1j * values_i_1)[:, :, None] * np.exp(1j * 2 * np.pi * self.n_tor * eta3) + out = (values_r_1 + 1j * values_i_1)[:, :, None] * xp.exp(1j * 2 * xp.pi * self.n_tor * eta3) # --------- evaluate FEM field at given point ------- else: @@ -2054,17 +2053,17 @@ def evaluate_ND(self, eta1, eta2, eta3, coeff, which="V2", part="r"): else: if self.basis_tor == "r": - out = (real_1 + 1j * imag_1) * np.cos(2 * np.pi * self.n_tor * eta3) - out += (real_2 + 1j * imag_2) * np.sin(2 * np.pi * self.n_tor * eta3) + out = (real_1 + 1j * imag_1) * xp.cos(2 * xp.pi * self.n_tor * eta3) + out += (real_2 + 1j * imag_2) * xp.sin(2 * xp.pi * self.n_tor * eta3) else: - out = (real_1 + 1j * imag_1) * np.exp(1j * 2 * np.pi * self.n_tor * eta3) + out = (real_1 + 1j * imag_1) * xp.exp(1j * 2 * xp.pi * self.n_tor * eta3) # return real or imaginary part if part == "r": - out = np.real(out) + out = xp.real(out) else: - out = np.imag(out) + out = xp.imag(out) return out @@ -2116,15 +2115,15 @@ def evaluate_DD(self, eta1, eta2, eta3, coeff, which="V3", part="r"): assert coeff.shape[:2] == (self.NbaseD[0], self.NbaseD[1]) # get real and imaginary part - coeff_r = np.real(coeff) - coeff_i = np.imag(coeff) + coeff_r = xp.real(coeff) + coeff_i = xp.imag(coeff) # ------ evaluate FEM field at given points -------- - if isinstance(eta1, np.ndarray): + if isinstance(eta1, xp.ndarray): # tensor-product evaluation if eta1.ndim == 1: - values_r_1 = np.empty((eta1.shape[0], eta2.shape[0]), dtype=float) - values_i_1 = np.empty((eta1.shape[0], eta2.shape[0]), dtype=float) + values_r_1 = xp.empty((eta1.shape[0], eta2.shape[0]), dtype=float) + values_i_1 = xp.empty((eta1.shape[0], eta2.shape[0]), dtype=float) eva_2d.evaluate_tensor_product_2d( self.t[0], @@ -2154,8 +2153,8 @@ def evaluate_DD(self, eta1, eta2, eta3, coeff, which="V3", part="r"): ) if self.n_tor != 0 and self.basis_tor == "r": - values_r_2 = np.empty((eta1.shape[0], eta2.shape[0]), dtype=float) - values_i_2 = np.empty((eta1.shape[0], eta2.shape[0]), dtype=float) + values_r_2 = xp.empty((eta1.shape[0], eta2.shape[0]), dtype=float) + values_i_2 = xp.empty((eta1.shape[0], eta2.shape[0]), dtype=float) eva_2d.evaluate_tensor_product_2d( self.t[0], @@ -2186,8 +2185,8 @@ def evaluate_DD(self, eta1, eta2, eta3, coeff, which="V3", part="r"): # matrix evaluation else: - values_r_1 = np.empty((eta1.shape[0], eta2.shape[1]), dtype=float) - values_i_1 = np.empty((eta1.shape[0], eta2.shape[1]), dtype=float) + values_r_1 = xp.empty((eta1.shape[0], eta2.shape[1]), dtype=float) + values_i_1 = xp.empty((eta1.shape[0], eta2.shape[1]), dtype=float) eva_2d.evaluate_matrix_2d( self.t[0], @@ -2217,8 +2216,8 @@ def evaluate_DD(self, eta1, eta2, eta3, coeff, which="V3", part="r"): ) if self.n_tor != 0 and self.basis_tor == "r": - values_r_2 = np.empty((eta1.shape[0], eta2.shape[1]), dtype=float) - values_i_2 = np.empty((eta1.shape[0], eta2.shape[1]), dtype=float) + values_r_2 = xp.empty((eta1.shape[0], eta2.shape[1]), dtype=float) + values_i_2 = xp.empty((eta1.shape[0], eta2.shape[1]), dtype=float) eva_2d.evaluate_matrix_2d( self.t[0], @@ -2249,15 +2248,15 @@ def evaluate_DD(self, eta1, eta2, eta3, coeff, which="V3", part="r"): # multiply with Fourier basis in third direction if self.n_tor == 0: - out = (values_r_1 + 1j * values_i_1)[:, :, None] * np.ones(eta3.shape, dtype=float) + out = (values_r_1 + 1j * values_i_1)[:, :, None] * xp.ones(eta3.shape, dtype=float) else: if self.basis_tor == "r": - out = (values_r_1 + 1j * values_i_1)[:, :, None] * np.cos(2 * np.pi * self.n_tor * eta3) - out += (values_r_2 + 1j * values_i_2)[:, :, None] * np.sin(2 * np.pi * self.n_tor * eta3) + out = (values_r_1 + 1j * values_i_1)[:, :, None] * xp.cos(2 * xp.pi * self.n_tor * eta3) + out += (values_r_2 + 1j * values_i_2)[:, :, None] * xp.sin(2 * xp.pi * self.n_tor * eta3) else: - out = (values_r_1 + 1j * values_i_1)[:, :, None] * np.exp(1j * 2 * np.pi * self.n_tor * eta3) + out = (values_r_1 + 1j * values_i_1)[:, :, None] * xp.exp(1j * 2 * xp.pi * self.n_tor * eta3) # --------- evaluate FEM field at given point ------- else: @@ -2314,17 +2313,17 @@ def evaluate_DD(self, eta1, eta2, eta3, coeff, which="V3", part="r"): else: if self.basis_tor == "r": - out = (real_1 + 1j * imag_1) * np.cos(2 * np.pi * self.n_tor * eta3) - out += (real_2 + 1j * imag_2) * np.sin(2 * np.pi * self.n_tor * eta3) + out = (real_1 + 1j * imag_1) * xp.cos(2 * xp.pi * self.n_tor * eta3) + out += (real_2 + 1j * imag_2) * xp.sin(2 * xp.pi * self.n_tor * eta3) else: - out = (real_1 + 1j * imag_1) * np.exp(1j * 2 * np.pi * self.n_tor * eta3) + out = (real_1 + 1j * imag_1) * xp.exp(1j * 2 * xp.pi * self.n_tor * eta3) # return real or imaginary part if part == "r": - out = np.real(out) + out = xp.real(out) else: - out = np.imag(out) + out = xp.imag(out) return out @@ -2335,13 +2334,13 @@ def evaluate_NNN(self, eta1, eta2, eta3, coeff): Parameters ---------- - eta1 : double or np.ndarray + eta1 : double or xp.ndarray 1st component of logical evaluation point - eta2 : double or np.ndarray + eta2 : double or xp.ndarray 2nd component of logical evaluation point - eta3 : double or np.ndarray + eta3 : double or xp.ndarray 3rd component of logical evaluation point coeff : array_like @@ -2356,10 +2355,10 @@ def evaluate_NNN(self, eta1, eta2, eta3, coeff): if coeff.ndim == 1: coeff = self.extract_0(coeff) - if isinstance(eta1, np.ndarray): + if isinstance(eta1, xp.ndarray): # tensor-product evaluation if eta1.ndim == 1: - values = np.empty((eta1.size, eta2.size, eta3.size), dtype=float) + values = xp.empty((eta1.size, eta2.size, eta3.size), dtype=float) eva_3d.evaluate_tensor_product( self.T[0], self.T[1], @@ -2380,7 +2379,7 @@ def evaluate_NNN(self, eta1, eta2, eta3, coeff): # matrix evaluation else: - values = np.empty((eta1.shape[0], eta2.shape[1], eta3.shape[2]), dtype=float) + values = xp.empty((eta1.shape[0], eta2.shape[1], eta3.shape[2]), dtype=float) # `eta1` is a sparse meshgrid. if max(eta1.shape) == eta1.size: eva_3d.evaluate_sparse( @@ -2450,13 +2449,13 @@ def evaluate_DNN(self, eta1, eta2, eta3, coeff): Parameters ---------- - eta1 : double or np.ndarray + eta1 : double or xp.ndarray 1st component of logical evaluation point - eta2 : double or np.ndarray + eta2 : double or xp.ndarray 2nd component of logical evaluation point - eta3 : double or np.ndarray + eta3 : double or xp.ndarray 3rd component of logical evaluation point coeff : array_like @@ -2471,10 +2470,10 @@ def evaluate_DNN(self, eta1, eta2, eta3, coeff): if coeff.ndim == 1: coeff = self.extract_1(coeff)[0] - if isinstance(eta1, np.ndarray): + if isinstance(eta1, xp.ndarray): # tensor product evaluation if eta1.ndim == 1: - values = np.empty((eta1.size, eta2.size, eta3.size), dtype=float) + values = xp.empty((eta1.size, eta2.size, eta3.size), dtype=float) eva_3d.evaluate_tensor_product( self.t[0], self.T[1], @@ -2495,7 +2494,7 @@ def evaluate_DNN(self, eta1, eta2, eta3, coeff): # matrix evaluation else: - values = np.empty((eta1.shape[0], eta2.shape[1], eta3.shape[2]), dtype=float) + values = xp.empty((eta1.shape[0], eta2.shape[1], eta3.shape[2]), dtype=float) # `eta1` is a sparse meshgrid. if max(eta1.shape) == eta1.size: eva_3d.evaluate_sparse( @@ -2564,13 +2563,13 @@ def evaluate_NDN(self, eta1, eta2, eta3, coeff): Parameters ---------- - eta1 : double or np.ndarray + eta1 : double or xp.ndarray 1st component of logical evaluation point - eta2 : double or np.ndarray + eta2 : double or xp.ndarray 2nd component of logical evaluation point - eta3 : double or np.ndarray + eta3 : double or xp.ndarray 3rd component of logical evaluation point coeff : array_like @@ -2585,10 +2584,10 @@ def evaluate_NDN(self, eta1, eta2, eta3, coeff): if coeff.ndim == 1: coeff = self.extract_1(coeff)[1] - if isinstance(eta1, np.ndarray): + if isinstance(eta1, xp.ndarray): # tensor product evaluation if eta1.ndim == 1: - values = np.empty((eta1.size, eta2.size, eta3.size), dtype=float) + values = xp.empty((eta1.size, eta2.size, eta3.size), dtype=float) eva_3d.evaluate_tensor_product( self.T[0], self.t[1], @@ -2609,7 +2608,7 @@ def evaluate_NDN(self, eta1, eta2, eta3, coeff): # matrix evaluation else: - values = np.empty((eta1.shape[0], eta2.shape[1], eta3.shape[2]), dtype=float) + values = xp.empty((eta1.shape[0], eta2.shape[1], eta3.shape[2]), dtype=float) # `eta1` is a sparse meshgrid. if max(eta1.shape) == eta1.size: eva_3d.evaluate_sparse( @@ -2678,13 +2677,13 @@ def evaluate_NND(self, eta1, eta2, eta3, coeff): Parameters ---------- - eta1 : double or np.ndarray + eta1 : double or xp.ndarray 1st component of logical evaluation point - eta2 : double or np.ndarray + eta2 : double or xp.ndarray 2nd component of logical evaluation point - eta3 : double or np.ndarray + eta3 : double or xp.ndarray 3rd component of logical evaluation point coeff : array_like @@ -2699,10 +2698,10 @@ def evaluate_NND(self, eta1, eta2, eta3, coeff): if coeff.ndim == 1: coeff = self.extract_1(coeff)[2] - if isinstance(eta1, np.ndarray): + if isinstance(eta1, xp.ndarray): # tensor product evaluation if eta1.ndim == 1: - values = np.empty((eta1.size, eta2.size, eta3.size), dtype=float) + values = xp.empty((eta1.size, eta2.size, eta3.size), dtype=float) eva_3d.evaluate_tensor_product( self.T[0], self.T[1], @@ -2723,7 +2722,7 @@ def evaluate_NND(self, eta1, eta2, eta3, coeff): # matrix evaluation else: - values = np.empty((eta1.shape[0], eta2.shape[1], eta3.shape[2]), dtype=float) + values = xp.empty((eta1.shape[0], eta2.shape[1], eta3.shape[2]), dtype=float) # `eta1` is a sparse meshgrid. if max(eta1.shape) == eta1.size: eva_3d.evaluate_sparse( @@ -2792,13 +2791,13 @@ def evaluate_NDD(self, eta1, eta2, eta3, coeff): Parameters ---------- - eta1 : double or np.ndarray + eta1 : double or xp.ndarray 1st component of logical evaluation point - eta2 : double or np.ndarray + eta2 : double or xp.ndarray 2nd component of logical evaluation point - eta3 : double or np.ndarray + eta3 : double or xp.ndarray 3rd component of logical evaluation point coeff : array_like @@ -2813,10 +2812,10 @@ def evaluate_NDD(self, eta1, eta2, eta3, coeff): if coeff.ndim == 1: coeff = self.extract_2(coeff)[0] - if isinstance(eta1, np.ndarray): + if isinstance(eta1, xp.ndarray): # tensor product evaluation if eta1.ndim == 1: - values = np.empty((eta1.size, eta2.size, eta3.size), dtype=float) + values = xp.empty((eta1.size, eta2.size, eta3.size), dtype=float) eva_3d.evaluate_tensor_product( self.T[0], self.t[1], @@ -2837,7 +2836,7 @@ def evaluate_NDD(self, eta1, eta2, eta3, coeff): # matrix evaluation else: - values = np.empty((eta1.shape[0], eta2.shape[1], eta3.shape[2]), dtype=float) + values = xp.empty((eta1.shape[0], eta2.shape[1], eta3.shape[2]), dtype=float) # `eta1` is a sparse meshgrid. if max(eta1.shape) == eta1.size: eva_3d.evaluate_sparse( @@ -2906,13 +2905,13 @@ def evaluate_DND(self, eta1, eta2, eta3, coeff): Parameters ---------- - eta1 : double or np.ndarray + eta1 : double or xp.ndarray 1st component of logical evaluation point - eta2 : double or np.ndarray + eta2 : double or xp.ndarray 2nd component of logical evaluation point - eta3 : double or np.ndarray + eta3 : double or xp.ndarray 3rd component of logical evaluation point coeff : array_like @@ -2927,10 +2926,10 @@ def evaluate_DND(self, eta1, eta2, eta3, coeff): if coeff.ndim == 1: coeff = self.extract_2(coeff)[1] - if isinstance(eta1, np.ndarray): + if isinstance(eta1, xp.ndarray): # tensor product evaluation if eta1.ndim == 1: - values = np.empty((eta1.size, eta2.size, eta3.size), dtype=float) + values = xp.empty((eta1.size, eta2.size, eta3.size), dtype=float) eva_3d.evaluate_tensor_product( self.t[0], self.T[1], @@ -2951,7 +2950,7 @@ def evaluate_DND(self, eta1, eta2, eta3, coeff): # matrix evaluation else: - values = np.empty((eta1.shape[0], eta2.shape[1], eta3.shape[2]), dtype=float) + values = xp.empty((eta1.shape[0], eta2.shape[1], eta3.shape[2]), dtype=float) # `eta1` is a sparse meshgrid. if max(eta1.shape) == eta1.size: eva_3d.evaluate_sparse( @@ -3020,13 +3019,13 @@ def evaluate_DDN(self, eta1, eta2, eta3, coeff): Parameters ---------- - eta1 : double or np.ndarray + eta1 : double or xp.ndarray 1st component of logical evaluation point - eta2 : double or np.ndarray + eta2 : double or xp.ndarray 2nd component of logical evaluation point - eta3 : double or np.ndarray + eta3 : double or xp.ndarray 3rd component of logical evaluation point coeff : array_like @@ -3041,10 +3040,10 @@ def evaluate_DDN(self, eta1, eta2, eta3, coeff): if coeff.ndim == 1: coeff = self.extract_2(coeff)[2] - if isinstance(eta1, np.ndarray): + if isinstance(eta1, xp.ndarray): # tensor product evaluation if eta1.ndim == 1: - values = np.empty((eta1.size, eta2.size, eta3.size), dtype=float) + values = xp.empty((eta1.size, eta2.size, eta3.size), dtype=float) eva_3d.evaluate_tensor_product( self.t[0], self.t[1], @@ -3065,7 +3064,7 @@ def evaluate_DDN(self, eta1, eta2, eta3, coeff): # matrix evaluation else: - values = np.empty((eta1.shape[0], eta2.shape[1], eta3.shape[2]), dtype=float) + values = xp.empty((eta1.shape[0], eta2.shape[1], eta3.shape[2]), dtype=float) # `eta1` is a sparse meshgrid. if max(eta1.shape) == eta1.size: eva_3d.evaluate_sparse( @@ -3134,13 +3133,13 @@ def evaluate_DDD(self, eta1, eta2, eta3, coeff): Parameters ---------- - eta1 : double or np.ndarray + eta1 : double or xp.ndarray 1st component of logical evaluation point - eta2 : double or np.ndarray + eta2 : double or xp.ndarray 2nd component of logical evaluation point - eta3 : double or np.ndarray + eta3 : double or xp.ndarray 3rd component of logical evaluation point coeff : array_like @@ -3155,10 +3154,10 @@ def evaluate_DDD(self, eta1, eta2, eta3, coeff): if coeff.ndim == 1: coeff = self.extract_3(coeff) - if isinstance(eta1, np.ndarray): + if isinstance(eta1, xp.ndarray): # tensor product evaluation if eta1.ndim == 1: - values = np.empty((eta1.size, eta2.size, eta3.size), dtype=float) + values = xp.empty((eta1.size, eta2.size, eta3.size), dtype=float) eva_3d.evaluate_tensor_product( self.t[0], self.t[1], @@ -3179,7 +3178,7 @@ def evaluate_DDD(self, eta1, eta2, eta3, coeff): # matrix evaluation else: - values = np.empty((eta1.shape[0], eta2.shape[1], eta3.shape[2]), dtype=float) + values = xp.empty((eta1.shape[0], eta2.shape[1], eta3.shape[2]), dtype=float) # `eta1` is a sparse meshgrid. if max(eta1.shape) == eta1.size: eva_3d.evaluate_sparse( diff --git a/src/struphy/examples/_draw_parallel.py b/src/struphy/examples/_draw_parallel.py index b2f3cef83..e95598b3c 100644 --- a/src/struphy/examples/_draw_parallel.py +++ b/src/struphy/examples/_draw_parallel.py @@ -1,9 +1,9 @@ +import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from struphy.feec.psydac_derham import Derham from struphy.geometry import domains from struphy.pic.particles import Particles6D -from struphy.utils.arrays import xp as np def main(): @@ -69,18 +69,18 @@ def main(): ) # are all markers in the correct domain? - conds = np.logical_and( + conds = xp.logical_and( particles.markers[:, :3] > derham.domain_array[rank, 0::3], particles.markers[:, :3] < derham.domain_array[rank, 1::3], ) holes = particles.markers[:, 0] == -1.0 - stay = np.all(conds, axis=1) + stay = xp.all(conds, axis=1) - error_mks = particles.markers[np.logical_and(~stay, ~holes)] + error_mks = particles.markers[xp.logical_and(~stay, ~holes)] print( - f"rank {rank} | markers not on correct process: {np.nonzero(np.logical_and(~stay, ~holes))} \ + f"rank {rank} | markers not on correct process: {xp.nonzero(xp.logical_and(~stay, ~holes))} \ \n corresponding positions:\n {error_mks[:, :3]}" ) diff --git a/src/struphy/examples/restelli2018/callables.py b/src/struphy/examples/restelli2018/callables.py index 4a4b3d5c5..505a60f90 100644 --- a/src/struphy/examples/restelli2018/callables.py +++ b/src/struphy/examples/restelli2018/callables.py @@ -1,6 +1,6 @@ "Analytical callables needed for the simulation of the Two-Fluid Quasi-Neutral Model by Restelli." -from struphy.utils.arrays import xp as np +import cunumpy as xp class RestelliForcingTerm: @@ -74,9 +74,9 @@ def __init__(self, nu=1.0, R0=2.0, a=1.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1.0, self._eps_norm = eps def __call__(self, x, y, z): - R = np.sqrt(x**2 + y**2) - R = np.where(R == 0.0, 1e-9, R) - phi = np.arctan2(-y, x) + R = xp.sqrt(x**2 + y**2) + R = xp.where(R == 0.0, 1e-9, R) + phi = xp.arctan2(-y, x) force_Z = self._nu * ( self._alpha * (self._R0 - 4 * R) / (self._a * self._R0 * R) - self._beta * self._Bp * self._R0**2 / (self._B0 * self._a * R**3) @@ -197,31 +197,31 @@ def __init__( def __call__(self, x, y, z): A = self._alpha / (self._a * self._R0) C = self._beta * self._Bp * self._R0 / (self._B0 * self._a) - R = np.sqrt(x**2 + y**2) - R = np.where(R == 0.0, 1e-9, R) - phi = np.arctan2(-y, x) + R = xp.sqrt(x**2 + y**2) + R = xp.where(R == 0.0, 1e-9, R) + phi = xp.arctan2(-y, x) if self._species == "Ions": """Forceterm for ions on the right hand side.""" if self._dimension == "2D": fx = ( - -2.0 * np.pi * np.sin(2 * np.pi * x) - + np.cos(2 * np.pi * x) * np.cos(2 * np.pi * y) * self._B0 / self._eps_norm - - self._nu * 8.0 * np.pi**2 * np.sin(2 * np.pi * x) * np.sin(2 * np.pi * y) + -2.0 * xp.pi * xp.sin(2 * xp.pi * x) + + xp.cos(2 * xp.pi * x) * xp.cos(2 * xp.pi * y) * self._B0 / self._eps_norm + - self._nu * 8.0 * xp.pi**2 * xp.sin(2 * xp.pi * x) * xp.sin(2 * xp.pi * y) ) fy = ( - 2.0 * np.pi * np.cos(2 * np.pi * y) - - np.sin(2 * np.pi * x) * np.sin(2 * np.pi * y) * self._B0 / self._eps_norm - - self._nu * 8.0 * np.pi**2 * np.cos(2 * np.pi * x) * np.cos(2 * np.pi * y) + 2.0 * xp.pi * xp.cos(2 * xp.pi * y) + - xp.sin(2 * xp.pi * x) * xp.sin(2 * xp.pi * y) * self._B0 / self._eps_norm + - self._nu * 8.0 * xp.pi**2 * xp.cos(2 * xp.pi * x) * xp.cos(2 * xp.pi * y) ) fz = 0.0 * x elif self._dimension == "1D": fx = ( - 2.0 * np.pi * np.cos(2 * np.pi * x) - + self._nu * 4.0 * np.pi**2 * np.sin(2 * np.pi * x) - + (np.sin(2 * np.pi * x) + 1.0) / self._dt + 2.0 * xp.pi * xp.cos(2 * xp.pi * x) + + self._nu * 4.0 * xp.pi**2 * xp.sin(2 * xp.pi * x) + + (xp.sin(2 * xp.pi * x) + 1.0) / self._dt ) - fy = (np.sin(2 * np.pi * x) + 1.0) * self._B0 / self._eps_norm + fy = (xp.sin(2 * xp.pi * x) + 1.0) * self._B0 / self._eps_norm fz = 0.0 * x elif self._dimension == "Tokamak": @@ -234,8 +234,8 @@ def __call__(self, x, y, z): fZ = self._alpha * self._B0 * z / self._a + A * self._R0 / R * ((R - self._R0) * self._B0) fphi = A * self._R0 * self._Bp / (self._a * R**2) * ((R - self._R0) ** 2 + z**2) - fx = np.cos(phi) * fR - R * np.sin(phi) * fphi - fy = -np.sin(phi) * fR - R * np.cos(phi) * fphi + fx = xp.cos(phi) * fR - R * xp.sin(phi) * fphi + fy = -xp.sin(phi) * fR - R * xp.cos(phi) * fphi fz = fZ if self._comp == "0": @@ -251,26 +251,26 @@ def __call__(self, x, y, z): """Forceterm for electrons on the right hand side.""" if self._dimension == "2D": fx = ( - 2.0 * np.pi * np.sin(2 * np.pi * x) - - np.cos(4 * np.pi * x) * np.cos(4 * np.pi * y) * self._B0 / self._eps_norm - - self._nu_e * 32.0 * np.pi**2 * np.sin(4 * np.pi * x) * np.sin(4 * np.pi * y) - - self._stab_sigma * (-np.sin(4 * np.pi * x) * np.sin(4 * np.pi * y)) + 2.0 * xp.pi * xp.sin(2 * xp.pi * x) + - xp.cos(4 * xp.pi * x) * xp.cos(4 * xp.pi * y) * self._B0 / self._eps_norm + - self._nu_e * 32.0 * xp.pi**2 * xp.sin(4 * xp.pi * x) * xp.sin(4 * xp.pi * y) + - self._stab_sigma * (-xp.sin(4 * xp.pi * x) * xp.sin(4 * xp.pi * y)) ) fy = ( - -2.0 * np.pi * np.cos(2 * np.pi * y) - + np.sin(4 * np.pi * x) * np.sin(4 * np.pi * y) * self._B0 / self._eps_norm - - self._nu_e * 32.0 * np.pi**2 * np.cos(4 * np.pi * x) * np.cos(4 * np.pi * y) - - self._stab_sigma * (-np.cos(4 * np.pi * x) * np.cos(4 * np.pi * y)) + -2.0 * xp.pi * xp.cos(2 * xp.pi * y) + + xp.sin(4 * xp.pi * x) * xp.sin(4 * xp.pi * y) * self._B0 / self._eps_norm + - self._nu_e * 32.0 * xp.pi**2 * xp.cos(4 * xp.pi * x) * xp.cos(4 * xp.pi * y) + - self._stab_sigma * (-xp.cos(4 * xp.pi * x) * xp.cos(4 * xp.pi * y)) ) fz = 0.0 * x elif self._dimension == "1D": fx = ( - -2.0 * np.pi * np.cos(2 * np.pi * x) - + self._nu_e * 4.0 * np.pi**2 * np.sin(2 * np.pi * x) - - self._stab_sigma * np.sin(2 * np.pi * x) + -2.0 * xp.pi * xp.cos(2 * xp.pi * x) + + self._nu_e * 4.0 * xp.pi**2 * xp.sin(2 * xp.pi * x) + - self._stab_sigma * xp.sin(2 * xp.pi * x) ) - fy = -np.sin(2 * np.pi * x) * self._B0 / self._eps_norm + fy = -xp.sin(2 * xp.pi * x) * self._B0 / self._eps_norm fz = 0.0 * x elif self._dimension == "Tokamak": @@ -283,8 +283,8 @@ def __call__(self, x, y, z): fZ = -self._alpha * self._B0 * z / self._a - A * self._R0 / R * ((R - self._R0) * self._B0) fphi = -A * self._R0 * self._Bp / (self._a * R**2) * ((R - self._R0) ** 2 + z**2) - fx = np.cos(phi) * fR - R * np.sin(phi) * fphi - fy = -np.sin(phi) * fR - R * np.cos(phi) * fphi + fx = xp.cos(phi) * fR - R * xp.sin(phi) * fphi + fy = -xp.sin(phi) * fR - R * xp.cos(phi) * fphi fz = fZ if self._comp == "0": diff --git a/src/struphy/feec/basis_projection_ops.py b/src/struphy/feec/basis_projection_ops.py index 8d4f24d54..3bce4701f 100644 --- a/src/struphy/feec/basis_projection_ops.py +++ b/src/struphy/feec/basis_projection_ops.py @@ -1,3 +1,4 @@ +import cunumpy as xp from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL from psydac.ddm.mpi import mpi as MPI from psydac.fem.basic import FemSpace @@ -14,7 +15,6 @@ from struphy.feec.utilities import RotationMatrix from struphy.polar.basic import PolarDerhamSpace, PolarVector from struphy.polar.linear_operators import PolarExtractionOperator -from struphy.utils.arrays import xp as np from struphy.utils.pyccel import Pyccelkernel @@ -51,7 +51,7 @@ def __init__(self, derham, domain, verbose=True, **weights): self._rank = derham.comm.Get_rank() if derham.comm is not None else 0 - if np.any([p == 1 and Nel > 1 for p, Nel in zip(derham.p, derham.Nel)]): + if xp.any([p == 1 and Nel > 1 for p, Nel in zip(derham.p, derham.Nel)]): if MPI.COMM_WORLD.Get_rank() == 0: print( f'\nWARNING: Class "BasisProjectionOperators" called with p={derham.p} (interpolation of piece-wise constants should be avoided).', @@ -1051,11 +1051,11 @@ def __init__( if isinstance(V, TensorFemSpace): self._Vspaces = [V.coeff_space] self._V1ds = [V.spaces] - self._VNbasis = np.array([self._V1ds[0][0].nbasis, self._V1ds[0][1].nbasis, self._V1ds[0][2].nbasis]) + self._VNbasis = xp.array([self._V1ds[0][0].nbasis, self._V1ds[0][1].nbasis, self._V1ds[0][2].nbasis]) else: self._Vspaces = V.coeff_space self._V1ds = [comp.spaces for comp in V.spaces] - self._VNbasis = np.array( + self._VNbasis = xp.array( [ [self._V1ds[0][0].nbasis, self._V1ds[0][1].nbasis, self._V1ds[0][2].nbasis], [ @@ -1283,7 +1283,7 @@ def assemble(self, verbose=False): self._pds, self._periodic, self._p, - np.array([col0, col1, col2]), + xp.array([col0, col1, col2]), self._VNbasis, self._mat._data, coeff, @@ -1358,7 +1358,7 @@ def assemble(self, verbose=False): self._pds, self._periodic, self._p, - np.array( + xp.array( [ col0, col1, @@ -1438,7 +1438,7 @@ def assemble(self, verbose=False): self._pds[h], self._periodic, self._p, - np.array( + xp.array( [ col0, col1, @@ -1539,7 +1539,7 @@ def assemble(self, verbose=False): self._pds[h], self._periodic, self._p, - np.array( + xp.array( [ col0, col1, @@ -1613,7 +1613,7 @@ class BasisProjectionOperator(LinOpWithTransp): Finite element spline space (domain, input space). weights : list - Weight function(s) (callables or np.ndarrays) in a 2d list of shape corresponding to number of components of domain/codomain. + Weight function(s) (callables or xp.ndarrays) in a 2d list of shape corresponding to number of components of domain/codomain. V_extraction_op : PolarExtractionOperator | IdentityOperator Extraction operator to polar sub-space of V. @@ -1889,7 +1889,7 @@ def update_weights(self, weights): Parameters ---------- weights : list - Weight function(s) (callables or np.ndarrays) in a 2d list of shape corresponding to number of components of domain/codomain. + Weight function(s) (callables or xp.ndarrays) in a 2d list of shape corresponding to number of components of domain/codomain. """ self._weights = weights @@ -1945,13 +1945,13 @@ def assemble(self, weights=None, verbose=False): # input vector space (domain), column of block for j, (Vspace, V1d, loc_weight) in enumerate(zip(_Vspaces, _V1ds, weight_line)): - _starts_in = np.array(Vspace.starts) - _ends_in = np.array(Vspace.ends) - _pads_in = np.array(Vspace.pads) + _starts_in = xp.array(Vspace.starts) + _ends_in = xp.array(Vspace.ends) + _pads_in = xp.array(Vspace.pads) - _starts_out = np.array(Wspace.starts) - _ends_out = np.array(Wspace.ends) - _pads_out = np.array(Wspace.pads) + _starts_out = xp.array(Wspace.starts) + _ends_out = xp.array(Wspace.ends) + _pads_out = xp.array(Wspace.pads) # use cached information if asked if self._use_cache: @@ -1998,21 +1998,21 @@ def assemble(self, weights=None, verbose=False): # Evaluate weight function at quadrature points # evaluate weight at quadrature points if callable(loc_weight): - PTS = np.meshgrid(*_ptsG, indexing="ij") + PTS = xp.meshgrid(*_ptsG, indexing="ij") mat_w = loc_weight(*PTS).copy() - elif isinstance(loc_weight, np.ndarray): + elif isinstance(loc_weight, xp.ndarray): assert loc_weight.shape == (len(_ptsG[0]), len(_ptsG[1]), len(_ptsG[2])) mat_w = loc_weight elif loc_weight is not None: raise TypeError( - "weights must be np.ndarray, callable or None", + "weights must be xp.ndarray, callable or None", ) # Call the kernel if weight function is not zero or in the scalar case # to avoid calling _block of a StencilMatrix in the else - not_weight_zero = np.array( - int(loc_weight is not None and np.any(np.abs(mat_w) > 1e-14)), + not_weight_zero = xp.array( + int(loc_weight is not None and xp.any(xp.abs(mat_w) > 1e-14)), ) if self._mpi_comm is not None: diff --git a/src/struphy/feec/linear_operators.py b/src/struphy/feec/linear_operators.py index d3c4770e4..3a29ea821 100644 --- a/src/struphy/feec/linear_operators.py +++ b/src/struphy/feec/linear_operators.py @@ -1,6 +1,7 @@ import itertools from abc import abstractmethod +import cunumpy as xp from psydac.ddm.mpi import MockComm from psydac.ddm.mpi import mpi as MPI from psydac.linalg.basic import LinearOperator, Vector, VectorSpace @@ -10,7 +11,6 @@ from struphy.feec.utilities import apply_essential_bc_to_array from struphy.polar.basic import PolarDerhamSpace -from struphy.utils.arrays import xp as np class LinOpWithTransp(LinearOperator): @@ -66,14 +66,14 @@ def toarray_struphy(self, out=None, is_sparse=False, format="csr"): if is_sparse == False: if out is None: # We declare the matrix form of our linear operator - out = np.zeros([self.codomain.dimension, self.domain.dimension], dtype=self.dtype) + out = xp.zeros([self.codomain.dimension, self.domain.dimension], dtype=self.dtype) else: - assert isinstance(out, np.ndarray) + assert isinstance(out, xp.ndarray) assert out.shape[0] == self.codomain.dimension assert out.shape[1] == self.domain.dimension # We use this matrix to store the partial results that we shall combine into the final matrix with a reduction at the end - result = np.zeros((self.codomain.dimension, self.domain.dimension), dtype=self.dtype) + result = xp.zeros((self.codomain.dimension, self.domain.dimension), dtype=self.dtype) else: if out is not None: raise Exception("If is_sparse is True then out must be set to None.") @@ -97,10 +97,10 @@ def toarray_struphy(self, out=None, is_sparse=False, format="csr"): ndim = [sp.ndim for sp in self.domain.spaces] # First each rank is going to need to know the starts and ends of all other ranks - startsarr = np.array([starts[i][j] for i in range(nsp) for j in range(ndim[i])], dtype=int) + startsarr = xp.array([starts[i][j] for i in range(nsp) for j in range(ndim[i])], dtype=int) # Create an array to store gathered data from all ranks - allstarts = np.empty(size * len(startsarr), dtype=int) + allstarts = xp.empty(size * len(startsarr), dtype=int) # Use Allgather to gather 'starts' from all ranks into 'allstarts' if comm is None or isinstance(comm, MockComm): @@ -111,9 +111,9 @@ def toarray_struphy(self, out=None, is_sparse=False, format="csr"): # Reshape 'allstarts' to have 9 columns and 'size' rows allstarts = allstarts.reshape((size, len(startsarr))) - endsarr = np.array([ends[i][j] for i in range(nsp) for j in range(ndim[i])], dtype=int) + endsarr = xp.array([ends[i][j] for i in range(nsp) for j in range(ndim[i])], dtype=int) # Create an array to store gathered data from all ranks - allends = np.empty(size * len(endsarr), dtype=int) + allends = xp.empty(size * len(endsarr), dtype=int) # Use Allgather to gather 'ends' from all ranks into 'allends' if comm is None or isinstance(comm, MockComm): @@ -148,13 +148,13 @@ def toarray_struphy(self, out=None, is_sparse=False, format="csr"): self.dot(v, out=tmp2) # Compute to which column this iteration belongs col = spoint - col += np.ravel_multi_index(i, npts[h]) + col += xp.ravel_multi_index(i, npts[h]) if is_sparse == False: result[:, col] = tmp2.toarray() else: aux = tmp2.toarray() # We now need to now which entries on tmp2 are non-zero and store then in our data list - for l in np.where(aux != 0)[0]: + for l in xp.where(aux != 0)[0]: data.append(aux[l]) colarr.append(col) row.append(l) @@ -179,9 +179,9 @@ def toarray_struphy(self, out=None, is_sparse=False, format="csr"): ndim = self.domain.ndim # First each rank is going to need to know the starts and ends of all other ranks - startsarr = np.array([starts[j] for j in range(ndim)], dtype=int) + startsarr = xp.array([starts[j] for j in range(ndim)], dtype=int) # Create an array to store gathered data from all ranks - allstarts = np.empty(size * len(startsarr), dtype=int) + allstarts = xp.empty(size * len(startsarr), dtype=int) # Use Allgather to gather 'starts' from all ranks into 'allstarts' if comm is None or isinstance(comm, MockComm): @@ -192,9 +192,9 @@ def toarray_struphy(self, out=None, is_sparse=False, format="csr"): # Reshape 'allstarts' to have 3 columns and 'size' rows allstarts = allstarts.reshape((size, len(startsarr))) - endsarr = np.array([ends[j] for j in range(ndim)], dtype=int) + endsarr = xp.array([ends[j] for j in range(ndim)], dtype=int) # Create an array to store gathered data from all ranks - allends = np.empty(size * len(endsarr), dtype=int) + allends = xp.empty(size * len(endsarr), dtype=int) # Use Allgather to gather 'ends' from all ranks into 'allends' if comm is None or isinstance(comm, MockComm): @@ -219,13 +219,13 @@ def toarray_struphy(self, out=None, is_sparse=False, format="csr"): # Compute dot product with the linear operator. self.dot(v, out=tmp2) # Compute to which column this iteration belongs - col = np.ravel_multi_index(i, npts) + col = xp.ravel_multi_index(i, npts) if is_sparse == False: result[:, col] = tmp2.toarray() else: aux = tmp2.toarray() # We now need to now which entries on tmp2 are non-zero and store then in our data list - for l in np.where(aux != 0)[0]: + for l in xp.where(aux != 0)[0]: data.append(aux[l]) colarr.append(col) row.append(l) @@ -309,7 +309,7 @@ class BoundaryOperator(LinOpWithTransp): space_id : str Symbolic space ID of vector_space (H1, Hcurl, Hdiv, L2 or H1vec). - dirichlet_bc : list[list[bool]] + dirichlet_bc : tuple[tuple[bool]] Whether to apply homogeneous Dirichlet boundary conditions (at left or right boundary in each direction). """ @@ -324,7 +324,7 @@ def __init__(self, vector_space, space_id, dirichlet_bc): self._space_id = space_id self._bc = dirichlet_bc - assert isinstance(dirichlet_bc, list) + assert isinstance(dirichlet_bc, tuple) assert len(dirichlet_bc) == 3 # number of non-zero elements in poloidal/toroidal direction diff --git a/src/struphy/feec/mass.py b/src/struphy/feec/mass.py index a86e02be8..76a26bd4d 100644 --- a/src/struphy/feec/mass.py +++ b/src/struphy/feec/mass.py @@ -1,11 +1,14 @@ import inspect +from copy import deepcopy +import cunumpy as xp from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL from psydac.ddm.mpi import mpi as MPI from psydac.fem.tensor import TensorFemSpace from psydac.fem.vector import VectorFemSpace from psydac.linalg.basic import IdentityOperator, LinearOperator, Vector from psydac.linalg.block import BlockLinearOperator, BlockVector +from psydac.linalg.solvers import inverse from psydac.linalg.stencil import StencilDiagonalMatrix, StencilMatrix, StencilVector from struphy.feec import mass_kernels @@ -14,7 +17,6 @@ from struphy.feec.utilities import RotationMatrix from struphy.geometry.base import Domain from struphy.polar.linear_operators import PolarExtractionOperator -from struphy.utils.arrays import xp as np from struphy.utils.pyccel import Pyccelkernel @@ -729,6 +731,12 @@ def M1gyro(self): return self._M1gyro + @property + def WMM(self): + if not hasattr(self, "_WMM"): + self._WMM = self.H1vecMassMatrix_density(self.derham, self, self.domain) + return self._WMM + ####################################### # Wrapper around WeightedMassOperator # ####################################### @@ -769,7 +777,7 @@ def create_weighted_mass( 1. ``str`` : for square block matrices (V=W), a symmetry can be set in order to accelerate the assembly process. Possible strings are ``symm`` (symmetric), ``asym`` (anti-symmetric) and ``diag`` (diagonal). 2. ``None`` : all blocks are allocated, disregarding zero-blocks or any symmetry. 3. ``1D list`` : 1d list consisting of either a) strings or b) matrices (3x3 callables or 3x3 list) and can be mixed. Predefined names are ``G``, ``Ginv``, ``DFinv``, ``sqrt_g``. Access them using strings in the 1d list: ``weights=['']``. Possible choices for key-value pairs in **weights** are, at the moment: ``eq_mhd``: :class:`~struphy.fields_background.base.MHDequilibrium`. To access them, use for ```` the string ``eq_``, where ```` can be found in the just mentioned base classes for MHD equilibria. By default, all scalars are multiplied. For division of scalars use ``1/``. - 4. ``2D list`` : 2d list with the same number of rows/columns as the number of components of the domain/codomain spaces. The entries can be either a) callables or b) np.ndarrays representing the weights at the quadrature points. If an entry is zero or ``None``, the corresponding block is set to ``None`` to accelerate the dot product. + 4. ``2D list`` : 2d list with the same number of rows/columns as the number of components of the domain/codomain spaces. The entries can be either a) callables or b) xp.ndarrays representing the weights at the quadrature points. If an entry is zero or ``None``, the corresponding block is set to ``None`` to accelerate the dot product. assemble: bool Whether to assemble the weighted mass matrix, i.e. computes the integrals with @@ -978,11 +986,11 @@ def _get_range_rank(self, func): else: dummy_eta = (0.0, 0.0, 0.0) val = func(*dummy_eta) - assert isinstance(val, np.ndarray) + assert isinstance(val, xp.ndarray) out = len(val.shape) - 3 else: if isinstance(func, list): - if isinstance(func[0], np.ndarray): + if isinstance(func[0], xp.ndarray): out = 2 else: out = len(func) - 1 @@ -1025,6 +1033,92 @@ def _operate(self, f1, f2, op, e1, e2, e3): return out + ####################################### + # Aux classes (to be removed in TODO) # + ####################################### + class H1vecMassMatrix_density: + """Wrapper around a Weighted mass operator from H1vec to H1vec whose weights are given by a 3 form""" + + def __init__(self, derham, mass_ops, domain): + self._massop = mass_ops.create_weighted_mass("H1vec", "H1vec") + self.field = derham.create_spline_function("field", "L2") + + integration_grid = [grid_1d.flatten() for grid_1d in derham.quad_grid_pts["0"]] + + self.integration_grid_spans, self.integration_grid_bn, self.integration_grid_bd = ( + derham.prepare_eval_tp_fixed( + integration_grid, + ) + ) + + grid_shape = tuple([len(loc_grid) for loc_grid in integration_grid]) + self._f_values = xp.zeros(grid_shape, dtype=float) + + metric = domain.metric(*integration_grid) + self._mass_metric_term = deepcopy(metric) + self._full_term_mass = deepcopy(metric) + + @property + def massop( + self, + ): + """The WeightedMassOperator""" + return self._massop + + @property + def inv( + self, + ): + """The inverse WeightedMassOperator""" + if not hasattr(self, "_inv"): + self._create_inv() + return self._inv + + def update_weight(self, coeffs): + """Update the weighted mass matrix operator""" + + self.field.vector = coeffs + f_values = self.field.eval_tp_fixed_loc( + self.integration_grid_spans, + self.integration_grid_bd, + out=self._f_values, + ) + for i in range(3): + for j in range(3): + self._full_term_mass[i, j] = f_values * self._mass_metric_term[i, j] + + self._massop.assemble( + [ + [self._full_term_mass[0, 0], self._full_term_mass[0, 1], self._full_term_mass[0, 2]], + [ + self._full_term_mass[1, 0], + self._full_term_mass[ + 1, + 1, + ], + self._full_term_mass[1, 2], + ], + [self._full_term_mass[2, 0], self._full_term_mass[2, 1], self._full_term_mass[2, 2]], + ], + verbose=False, + ) + + if hasattr(self, "_inv") and self.inv._options["pc"] is not None: + self.inv._options["pc"].update_mass_operator(self.massop) + + def _create_inv(self, type="pcg", tol=1e-16, maxiter=500, verbose=False): + """Inverse the weighted mass matrix, preconditioner must be set outside + via self._inv._options['pc'] = ...""" + self._inv = inverse( + self.massop, + type, + pc=None, + tol=tol, + maxiter=maxiter, + verbose=verbose, + recycle=True, + ) + class WeightedMassOperatorsOldForTesting: r""" @@ -1989,7 +2083,7 @@ class WeightedMassOperator(LinOpWithTransp): 1. ``None`` : all blocks are allocated, disregarding zero-blocks or any symmetry. 2. ``str`` : for square block matrices (V=W), a symmetry can be set in order to accelerate the assembly process. Possible strings are ``symm`` (symmetric), ``asym`` (anti-symmetric) and ``diag`` (diagonal). - 3. ``list`` : 2d list with the same number of rows/columns as the number of components of the domain/codomain spaces. The entries can be either a) callables or b) np.ndarrays representing the weights at the quadrature points. If an entry is zero or ``None``, the corresponding block is set to ``None`` to accelerate the dot product. + 3. ``list`` : 2d list with the same number of rows/columns as the number of components of the domain/codomain spaces. The entries can be either a) callables or b) xp.ndarrays representing the weights at the quadrature points. If an entry is zero or ``None``, the corresponding block is set to ``None`` to accelerate the dot product. transposed : bool Whether to assemble the transposed operator. @@ -2270,16 +2364,16 @@ def __init__( ] if callable(weights_info[a][b]): - PTS = np.meshgrid(*pts, indexing="ij") + PTS = xp.meshgrid(*pts, indexing="ij") mat_w = weights_info[a][b](*PTS).copy() - elif isinstance(weights_info[a][b], np.ndarray): + elif isinstance(weights_info[a][b], xp.ndarray): mat_w = weights_info[a][b] assert mat_w.shape == tuple( [pt.size for pt in pts], ) - if np.any(np.abs(mat_w) > 1e-14): + if xp.any(xp.abs(mat_w) > 1e-14): if self._matrix_free: blocks[-1] += [ StencilMatrixFreeMassOperator( @@ -2595,7 +2689,7 @@ def assemble(self, weights=None, clear=True, verbose=True): Parameters ---------- weights : list | NoneType - Weight function(s) (callables or np.ndarrays) in a 2d list of shape corresponding to + Weight function(s) (callables or xp.ndarrays) in a 2d list of shape corresponding to number of components of domain/codomain. If ``weights=None``, the weight is taken from the given weights in the instanziation of the object, else it will be overriden. @@ -2618,7 +2712,7 @@ def assemble(self, weights=None, clear=True, verbose=True): if weight is not None: assert callable(weight) or isinstance( weight, - np.ndarray, + xp.ndarray, ) self._mat[a, b].weights = weight @@ -2728,13 +2822,13 @@ def assemble(self, weights=None, clear=True, verbose=True): # evaluate weight at quadrature points if callable(loc_weight): - PTS = np.meshgrid(*pts, indexing="ij") + PTS = xp.meshgrid(*pts, indexing="ij") mat_w = loc_weight(*PTS).copy() - elif isinstance(loc_weight, np.ndarray): + elif isinstance(loc_weight, xp.ndarray): mat_w = loc_weight elif loc_weight is not None: raise TypeError( - "weights must be callable or np.ndarray or None but is {}".format( + "weights must be callable or xp.ndarray or None but is {}".format( type(self._weights[a][b]), ), ) @@ -2742,8 +2836,8 @@ def assemble(self, weights=None, clear=True, verbose=True): if loc_weight is not None: assert mat_w.shape == tuple([pt.size for pt in pts]) - not_weight_zero = np.array( - int(loc_weight is not None and np.any(np.abs(mat_w) > 1e-14)), + not_weight_zero = xp.array( + int(loc_weight is not None and xp.any(xp.abs(mat_w) > 1e-14)), ) if self._mpi_comm is not None: self._mpi_comm.Allreduce( @@ -2767,7 +2861,7 @@ def assemble(self, weights=None, clear=True, verbose=True): mat = self._mat if loc_weight is None: # in case it's none we still need to have zeros weights to call the kernel - mat_w = np.zeros( + mat_w = xp.zeros( tuple([pt.size for pt in pts]), ) else: @@ -2903,12 +2997,12 @@ def eval_quad(W, coeffs, out=None): coeffs : StencilVector | BlockVector The coefficient vector corresponding to the FEM field. Ghost regions must be up-to-date! - out : np.ndarray | list/tuple of np.ndarrays, optional + out : xp.ndarray | list/tuple of xp.ndarrays, optional If given, the result will be written into these arrays in-place. Number of outs must be compatible with number of components of FEM field. Returns ------- - out : np.ndarray | list/tuple of np.ndarrays + out : xp.ndarray | list/tuple of xp.ndarrays The values of the FEM field at the quadrature points. """ @@ -2927,7 +3021,7 @@ def eval_quad(W, coeffs, out=None): out = () if isinstance(W, TensorFemSpace): out += ( - np.zeros( + xp.zeros( [ q_grid[nquad].points.size for q_grid, nquad in zip(self.derham.get_quad_grids(W, nquads=self.nquads), self.nquads) @@ -2938,7 +3032,7 @@ def eval_quad(W, coeffs, out=None): else: for space in W.spaces: out += ( - np.zeros( + xp.zeros( [ q_grid[nquad].points.size for q_grid, nquad in zip( @@ -2951,7 +3045,7 @@ def eval_quad(W, coeffs, out=None): else: if isinstance(W, TensorFemSpace): - assert isinstance(out, np.ndarray) + assert isinstance(out, xp.ndarray) out = (out,) else: assert isinstance(out, (list, tuple)) @@ -3067,7 +3161,7 @@ def __init__(self, derham, V, W, weights=None, nquads=None): ) shape = tuple(e - s + 1 for s, e in zip(V.coeff_space.starts, V.coeff_space.ends)) - self._diag_tmp = np.zeros((shape)) + self._diag_tmp = xp.zeros((shape)) # knot span indices of elements of local domain self._codomain_spans = [ @@ -3194,16 +3288,16 @@ def dot(self, v, out=None): # evaluate weight at quadrature points if callable(self._weights): - PTS = np.meshgrid(*self._pts, indexing="ij") + PTS = xp.meshgrid(*self._pts, indexing="ij") mat_w = self._weights(*PTS).copy() - elif isinstance(self._weights, np.ndarray): + elif isinstance(self._weights, xp.ndarray): mat_w = self._weights if self._weights is not None: assert mat_w.shape == tuple([pt.size for pt in self._pts]) # call kernel (if mat_w is not zero) by calling the appropriate kernel (1d, 2d or 3d) - if np.any(np.abs(mat_w) > 1e-14): + if xp.any(xp.abs(mat_w) > 1e-14): self._dot_kernel( *self._codomain_spans, *self._domain_spans, @@ -3263,9 +3357,9 @@ def diagonal(self, inverse=False, sqrt=False, out=None): # evaluate weight at quadrature points if callable(self._weights): - PTS = np.meshgrid(*self._pts, indexing="ij") + PTS = xp.meshgrid(*self._pts, indexing="ij") mat_w = self._weights(*PTS).copy() - elif isinstance(self._weights, np.ndarray): + elif isinstance(self._weights, xp.ndarray): mat_w = self._weights diag = self._diag_tmp @@ -3285,12 +3379,12 @@ def diagonal(self, inverse=False, sqrt=False, out=None): # Calculate entries of StencilDiagonalMatrix if sqrt: - diag = np.sqrt(diag) + diag = xp.sqrt(diag) if inverse: - data = np.divide(1, diag, out=data) + data = xp.divide(1, diag, out=data) elif out: - np.copyto(data, diag) + xp.copyto(data, diag) else: data = diag.copy() diff --git a/src/struphy/feec/preconditioner.py b/src/struphy/feec/preconditioner.py index b3a8744eb..f3016b532 100644 --- a/src/struphy/feec/preconditioner.py +++ b/src/struphy/feec/preconditioner.py @@ -1,3 +1,4 @@ +import cunumpy as xp from psydac.api.essential_bc import apply_essential_bc_stencil from psydac.ddm.cart import CartDecomposition, DomainDecomposition from psydac.fem.tensor import TensorFemSpace @@ -11,7 +12,6 @@ from struphy.feec.linear_operators import BoundaryOperator from struphy.feec.mass import WeightedMassOperator -from struphy.utils.arrays import xp as np class MassMatrixPreconditioner(LinearOperator): @@ -94,12 +94,12 @@ def fun(e): s = e.shape[0] newshape = tuple([1 if i != d else s for i in range(n_dims)]) f = e.reshape(newshape) - return np.atleast_1d( + return xp.atleast_1d( loc_weights( - *[np.array(np.full_like(f, 0.5)) if i != d else np.array(f) for i in range(n_dims)], + *[xp.array(xp.full_like(f, 0.5)) if i != d else xp.array(f) for i in range(n_dims)], ).squeeze(), ) - elif isinstance(loc_weights, np.ndarray): + elif isinstance(loc_weights, xp.ndarray): s = loc_weights.shape if d == 0: fun = loc_weights[:, s[1] // 2, s[2] // 2] @@ -108,14 +108,14 @@ def fun(e): elif d == 2: fun = loc_weights[s[0] // 2, s[1] // 2, :] elif loc_weights is None: - fun = lambda e: np.ones(e.size, dtype=float) + fun = lambda e: xp.ones(e.size, dtype=float) else: raise TypeError( - "weights needs to be callable, np.ndarray or None but is{}".format(type(loc_weights)), + "weights needs to be callable, xp.ndarray or None but is{}".format(type(loc_weights)), ) fun = [[fun]] else: - fun = [[lambda e: np.ones(e.size, dtype=float)]] + fun = [[lambda e: xp.ones(e.size, dtype=float)]] # get 1D FEM space (serial, not distributed) and quadrature order femspace_1d = femspaces[c].spaces[d] @@ -207,7 +207,7 @@ def fun(e): M_local = StencilMatrix(V_local, V_local) - row_indices, col_indices = np.nonzero(M_arr) + row_indices, col_indices = xp.nonzero(M_arr) for row_i, col_i in zip(row_indices, col_indices): # only consider row indices on process @@ -220,7 +220,7 @@ def fun(e): ] = M_arr[row_i, col_i] # check if stencil matrix was built correctly - assert np.allclose(M_local.toarray()[s : e + 1], M_arr[s : e + 1]) + assert xp.allclose(M_local.toarray()[s : e + 1], M_arr[s : e + 1]) matrixcells += [M_local.copy()] # ======================================================================================================= @@ -487,7 +487,7 @@ def __init__(self, mass_operator, apply_bc=True): # loop over spatial directions for d in range(n_dims): - fun = [[lambda e: np.ones(e.size, dtype=float)]] + fun = [[lambda e: xp.ones(e.size, dtype=float)]] # get 1D FEM space (serial, not distributed) and quadrature order femspace_1d = femspaces[c].spaces[d] @@ -579,7 +579,7 @@ def __init__(self, mass_operator, apply_bc=True): M_local = StencilMatrix(V_local, V_local) - row_indices, col_indices = np.nonzero(M_arr) + row_indices, col_indices = xp.nonzero(M_arr) for row_i, col_i in zip(row_indices, col_indices): # only consider row indices on process @@ -592,7 +592,7 @@ def __init__(self, mass_operator, apply_bc=True): ] = M_arr[row_i, col_i] # check if stencil matrix was built correctly - assert np.allclose(M_local.toarray()[s : e + 1], M_arr[s : e + 1]) + assert xp.allclose(M_local.toarray()[s : e + 1], M_arr[s : e + 1]) matrixcells += [M_local.copy()] # ======================================================================================================= @@ -676,7 +676,7 @@ def __init__(self, mass_operator, apply_bc=True): # Need to assemble the logical mass matrix to extract the coefficients fun = [ - [lambda e1, e2, e3: np.ones_like(e1, dtype=float) if i == j else None for j in range(3)] for i in range(3) + [lambda e1, e2, e3: xp.ones_like(e1, dtype=float) if i == j else None for j in range(3)] for i in range(3) ] log_M = WeightedMassOperator( self._mass_operator.derham, @@ -864,15 +864,15 @@ class FFTSolver(BandedSolver): Parameters ---------- - circmat : np.ndarray + circmat : xp.ndarray Generic circulant matrix. """ def __init__(self, circmat): - assert isinstance(circmat, np.ndarray) + assert isinstance(circmat, xp.ndarray) assert is_circulant(circmat) - self._space = np.ndarray + self._space = xp.ndarray self._column = circmat[:, 0] # -------------------------------------- @@ -889,13 +889,13 @@ def solve(self, rhs, out=None, transposed=False): Parameters ---------- - rhs : np.ndarray + rhs : xp.ndarray The right-hand sides to solve for. The vectors are assumed to be given in C-contiguous order, i.e. if multiple right-hand sides are given, then rhs is a two-dimensional array with the 0-th index denoting the number of the right-hand side, and the 1-st index denoting the element inside a right-hand side. - out : np.ndarray, optional + out : xp.ndarray, optional Output vector. If given, it has to have the same shape and datatype as rhs. transposed : bool @@ -913,7 +913,7 @@ def solve(self, rhs, out=None, transposed=False): try: out[:] = solve_circulant(self._column, rhs.T).T - except np.linalg.LinAlgError: + except xp.linalg.LinAlgError: eps = 1e-4 print(f"Stabilizing singular preconditioning FFTSolver with {eps = }:") self._column[0] *= 1.0 + eps @@ -937,13 +937,13 @@ def is_circulant(mat): Whether the matrix is circulant (=True) or not (=False). """ - assert isinstance(mat, np.ndarray) + assert isinstance(mat, xp.ndarray) assert len(mat.shape) == 2 assert mat.shape[0] == mat.shape[1] if mat.shape[0] > 1: for i in range(mat.shape[0] - 1): - circulant = np.allclose(mat[i, :], np.roll(mat[i + 1, :], -1)) + circulant = xp.allclose(mat[i, :], xp.roll(mat[i + 1, :], -1)) if not circulant: return circulant else: diff --git a/src/struphy/feec/projectors.py b/src/struphy/feec/projectors.py index 1e9421c7e..a291d18bd 100644 --- a/src/struphy/feec/projectors.py +++ b/src/struphy/feec/projectors.py @@ -1,3 +1,4 @@ +import cunumpy as xp from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL from psydac.ddm.mpi import mpi as MPI from psydac.feec.global_projectors import GlobalProjector @@ -37,7 +38,6 @@ from struphy.kernel_arguments.local_projectors_args_kernels import LocalProjectorsArguments from struphy.polar.basic import PolarVector from struphy.polar.linear_operators import PolarExtractionOperator -from struphy.utils.arrays import xp as np class CommutingProjector: @@ -576,24 +576,24 @@ class CommutingProjectorLocal: fem_space : FemSpace FEEC space into which the functions shall be projected. - pts : list of np.array + pts : list of xp.array 3-list (or nested 3-list[3-list] for BlockVectors) of 2D arrays with the quasi-interpolation points (or Gauss-Legendre quadrature points for histopolation). In format [spatial direction](B-spline index, point) for StencilVector spaces or [vector component][spatial direction](B-spline index, point) for BlockVector spaces. - wts : list of np.array + wts : list of xp.array 3D (4D for BlockVectors) list of 2D array with the Gauss-Legendre quadrature weights (full of ones for interpolation). In format [spatial direction](B-spline index, point) for StencilVector spaces or [vector component][spatial direction](B-spline index, point) for BlockVector spaces. - wij : list of np.array + wij : list of xp.array List of 2D arrays for the coefficients :math:`\omega_j^i` obtained by inverting the local collocation matrix. Use for obtaining the FE coefficients of a function via interpolation. In format [spatial direction](B-spline index, point). - whij : list of np.array + whij : list of xp.array List of 2D arrays for the coefficients :math:`\hat{\omega}_j^i` obtained from the :math:`\omega_j^i`. Use for obtaining the FE coefficients of a function via histopolation. In format [spatial direction](D-spline index, point). @@ -639,22 +639,22 @@ def __init__( # FE space of zero forms. That means that we have B-splines in all three spatial directions. Bspaces_1d = [fem_space_B.spaces] - self._B_nbasis = np.array([space.nbasis for space in Bspaces_1d[0]]) + self._B_nbasis = xp.array([space.nbasis for space in Bspaces_1d[0]]) # Degree of the B-spline space, not to be confused with the degrees given by fem_space.spaces.degree since depending on the situation it will give the D-spline degree instead - self._p = np.zeros(3, dtype=int) + self._p = xp.zeros(3, dtype=int) for i, space in enumerate(fem_space_B.spaces): self._p[i] = space.degree # FE space of three forms. That means that we have D-splines in all three spatial directions. Dspaces_1d = [fem_space_D.spaces] - D_nbasis = np.array([space.nbasis for space in Dspaces_1d[0]]) + D_nbasis = xp.array([space.nbasis for space in Dspaces_1d[0]]) self._periodic = [] for space in fem_space.spaces: self._periodic.append(space.periodic) - self._periodic = np.array(self._periodic) + self._periodic = xp.array(self._periodic) if isinstance(fem_space, TensorFemSpace): # The comm, rank and size are only necessary for debugging. In particular, for printing stuff @@ -667,21 +667,21 @@ def __init__( self._size = self._comm.Get_size() # We get the start and endpoint for each sublist in out - self._starts = np.array(self.coeff_space.starts) - self._ends = np.array(self.coeff_space.ends) + self._starts = xp.array(self.coeff_space.starts) + self._ends = xp.array(self.coeff_space.ends) # We compute the number of FE coefficients the current MPI rank is responsible for - self._loc_num_coeff = np.array([self._ends[i] + 1 - self._starts[i] for i in range(3)], dtype=int) + self._loc_num_coeff = xp.array([self._ends[i] + 1 - self._starts[i] for i in range(3)], dtype=int) # We get the pads - self._pds = np.array(self.coeff_space.pads) + self._pds = xp.array(self.coeff_space.pads) # We get the number of spaces we have self._nsp = 1 self._localpts = [] self._index_translation = [] self._inv_index_translation = [] - self._original_pts_size = np.zeros((3), dtype=int) + self._original_pts_size = xp.zeros((3), dtype=int) elif isinstance(fem_space, VectorFemSpace): # The comm, rank and size are only necessary for debugging. In particular, for printing stuff @@ -694,17 +694,17 @@ def __init__( self._size = self._comm.Get_size() # we collect all starts and ends in two big lists - self._starts = np.array([vi.starts for vi in self.coeff_space.spaces]) - self._ends = np.array([vi.ends for vi in self.coeff_space.spaces]) + self._starts = xp.array([vi.starts for vi in self.coeff_space.spaces]) + self._ends = xp.array([vi.ends for vi in self.coeff_space.spaces]) # We compute the number of FE coefficients the current MPI rank is responsible for - self._loc_num_coeff = np.array( + self._loc_num_coeff = xp.array( [[self._ends[h][i] + 1 - self._starts[h][i] for i in range(3)] for h in range(3)], dtype=int, ) # We collect the pads - self._pds = np.array([vi.pads for vi in self.coeff_space.spaces]) + self._pds = xp.array([vi.pads for vi in self.coeff_space.spaces]) # We get the number of space we have self._nsp = len(self.coeff_space.spaces) @@ -720,7 +720,7 @@ def __init__( self._localpts = [[], [], []] # Here we will store the global number of points for each block entry and for each spatial direction. - self._original_pts_size = [np.zeros((3), dtype=int), np.zeros((3), dtype=int), np.zeros((3), dtype=int)] + self._original_pts_size = [xp.zeros((3), dtype=int), xp.zeros((3), dtype=int), xp.zeros((3), dtype=int)] # This will be a list of three elements (the first one for the first block element, the second one for the second block element, ...), each one being a list with three arrays, # each array will contain the B-spline indices of the corresponding spatial direction for which this MPI rank has to store at least one non-zero FE coefficient for the storage of the @@ -740,8 +740,8 @@ def __init__( self._are_zero_block_B_or_D_splines = [[], [], []] # self._Basis_function_indices_agreggated_B[i][j] = -1 if the jth B-spline is not necessary for any of the three block entries in the ith spatial direction, otherwise it is 0 - self._Basis_function_indices_agreggated_B = [-1 * np.ones(nbasis, dtype=int) for nbasis in self._B_nbasis] - self._Basis_function_indices_agreggated_D = [-1 * np.ones(nbasis, dtype=int) for nbasis in D_nbasis] + self._Basis_function_indices_agreggated_B = [-1 * xp.ones(nbasis, dtype=int) for nbasis in self._B_nbasis] + self._Basis_function_indices_agreggated_D = [-1 * xp.ones(nbasis, dtype=int) for nbasis in D_nbasis] # List that will contain the LocalProjectorsArguments for each value of h = 0,1,2. self._solve_args = [] @@ -753,20 +753,20 @@ def __init__( # List of list that tell us for each spatial direction whether we have Interpolation or Histopolation. IoH_for_indices = ["I", "I", "I"] # Same list as before but with bools instead of chars - self._IoH = np.array([False, False, False], dtype=bool) + self._IoH = xp.array([False, False, False], dtype=bool) # We make a list with the interpolation/histopolation weights we need for each block and each direction. self._geo_weights = [self._wij[0], self._wij[1], self._wij[2]] elif space_id == "L2": IoH_for_indices = ["H", "H", "H"] - self._IoH = np.array([True, True, True], dtype=bool) + self._IoH = xp.array([True, True, True], dtype=bool) self._geo_weights = [self._whij[0], self._whij[1], self._whij[2]] lenj1, lenj2, lenj3 = get_local_problem_size(self._periodic, self._p, self._IoH) lenj = [lenj1, lenj2, lenj3] - self._shift = np.array([0, 0, 0], dtype=int) + self._shift = xp.array([0, 0, 0], dtype=int) compute_shifts(self._IoH, self._p, self._B_nbasis, self._shift) split_points( @@ -788,7 +788,7 @@ def __init__( ) # We want to build the meshgrid for the evaluation of the degrees of freedom so it only contains the evaluation points that each specific MPI rank is actually going to use. - self._meshgrid = np.meshgrid( + self._meshgrid = xp.meshgrid( *[pt for pt in self._localpts], indexing="ij", ) @@ -927,18 +927,18 @@ def __init__( ) elif isinstance(fem_space, VectorFemSpace): - self._shift = [np.array([0, 0, 0], dtype=int) for _ in range(3)] + self._shift = [xp.array([0, 0, 0], dtype=int) for _ in range(3)] if space_id == "H1vec": # List of list that tell us for each block entry and for each spatial direction whether we have Interpolation or Histopolation. IoH_for_indices = [["I", "I", "I"], ["I", "I", "I"], ["I", "I", "I"]] # Same list as before but with bools instead of chars self._IoH = [ - np.array([False, False, False], dtype=bool), - np.array( + xp.array([False, False, False], dtype=bool), + xp.array( [False, False, False], dtype=bool, ), - np.array([False, False, False], dtype=bool), + xp.array([False, False, False], dtype=bool), ] # We make a list with the interpolation/histopolation weights we need for each block and each direction. self._geo_weights = [[self._wij[0], self._wij[1], self._wij[2]] for _ in range(3)] @@ -946,12 +946,12 @@ def __init__( elif space_id == "Hcurl": IoH_for_indices = [["H", "I", "I"], ["I", "H", "I"], ["I", "I", "H"]] self._IoH = [ - np.array([True, False, False], dtype=bool), - np.array( + xp.array([True, False, False], dtype=bool), + xp.array( [False, True, False], dtype=bool, ), - np.array([False, False, True], dtype=bool), + xp.array([False, False, True], dtype=bool), ] self._geo_weights = [ [self._whij[0], self._wij[1], self._wij[2]], @@ -966,12 +966,12 @@ def __init__( elif space_id == "Hdiv": IoH_for_indices = [["I", "H", "H"], ["H", "I", "H"], ["H", "H", "I"]] self._IoH = [ - np.array([False, True, True], dtype=bool), - np.array( + xp.array([False, True, True], dtype=bool), + xp.array( [True, False, True], dtype=bool, ), - np.array([True, True, False], dtype=bool), + xp.array([True, True, False], dtype=bool), ] self._geo_weights = [ [self._wij[0], self._whij[1], self._whij[2]], @@ -1010,7 +1010,7 @@ def __init__( # meshgrid for h component self._meshgrid.append( - np.meshgrid( + xp.meshgrid( *[pt for pt in self._localpts[h]], indexing="ij", ), @@ -1328,9 +1328,9 @@ def solve_weighted(self, rhs, out=None): if isinstance(self._fem_space, TensorFemSpace): if out is None: - out = np.zeros((self._loc_num_coeff[0], self._loc_num_coeff[1], self._loc_num_coeff[2]), dtype=float) + out = xp.zeros((self._loc_num_coeff[0], self._loc_num_coeff[1], self._loc_num_coeff[2]), dtype=float) else: - assert np.shape(out) == (self._loc_num_coeff[0], self._loc_num_coeff[1], self._loc_num_coeff[2]) + assert xp.shape(out) == (self._loc_num_coeff[0], self._loc_num_coeff[1], self._loc_num_coeff[2]) solve_local_main_loop_weighted( self._solve_args, @@ -1352,7 +1352,7 @@ def solve_weighted(self, rhs, out=None): out = [] for h in range(3): out.append( - np.zeros( + xp.zeros( ( self._loc_num_coeff[h][0], self._loc_num_coeff[h][1], @@ -1365,7 +1365,7 @@ def solve_weighted(self, rhs, out=None): else: assert len(out) == 3 for h in range(3): - assert np.shape(out[h]) == ( + assert xp.shape(out[h]) == ( self._loc_num_coeff[h][0], self._loc_num_coeff[h][1], self._loc_num_coeff[h][2], @@ -1375,7 +1375,7 @@ def solve_weighted(self, rhs, out=None): # the out block for which do_nothing tell us before hand they shall be zero. for h in range(3): if self._do_nothing[h] == 1: - out[h] = np.zeros( + out[h] = xp.zeros( ( self._loc_num_coeff[h][0], self._loc_num_coeff[h][1], @@ -1430,7 +1430,7 @@ def get_dofs(self, fun, dofs=None): fh = fun[h](*self._meshgrid[h]) # Array into which we will write the Dofs. - f_eval_aux = np.zeros(tuple(np.shape(dim)[0] for dim in self._localpts[h])) + f_eval_aux = xp.zeros(tuple(xp.shape(dim)[0] for dim in self._localpts[h])) # For 1-forms if self._space_key == "1": @@ -1442,7 +1442,7 @@ def get_dofs(self, fun, dofs=None): f_eval.append(f_eval_aux) elif self._space_key == "3": - f_eval = np.zeros(tuple(np.shape(dim)[0] for dim in self._localpts)) + f_eval = xp.zeros(tuple(xp.shape(dim)[0] for dim in self._localpts)) # Evaluation of the function at all Gauss-Legendre quadrature points faux = fun(*self._meshgrid) get_dofs_local_3_form(self._solve_args, faux, f_eval) @@ -1483,7 +1483,7 @@ def get_dofs_weighted(self, fun, dofs=None, first_go=True, pre_computed_dofs=Non elif self._space_key == "1" or self._space_key == "2": assert len(fun) == 3, f"List input only for vector-valued spaces of size 3, but {len(fun) = }." - self._do_nothing = np.zeros(3, dtype=int) + self._do_nothing = xp.zeros(3, dtype=int) f_eval = [] # If this is the first time this rank has to evaluate the weights degrees of freedom we declare the list where to store them. @@ -1496,7 +1496,7 @@ def get_dofs_weighted(self, fun, dofs=None, first_go=True, pre_computed_dofs=Non pre_computed_dofs.append(fun[h](*self._meshgrid[h])) # Array into which we will write the Dofs. - f_eval_aux = np.zeros(tuple(np.shape(dim)[0] for dim in self._localpts[h])) + f_eval_aux = xp.zeros(tuple(xp.shape(dim)[0] for dim in self._localpts[h])) # We check if the current set of basis functions is not one of those we have to compute in the current MPI rank. if ( @@ -1541,7 +1541,7 @@ def get_dofs_weighted(self, fun, dofs=None, first_go=True, pre_computed_dofs=Non f_eval.append(f_eval_aux) elif self._space_key == "3": - f_eval = np.zeros(tuple(np.shape(dim)[0] for dim in self._localpts)) + f_eval = xp.zeros(tuple(xp.shape(dim)[0] for dim in self._localpts)) # Evaluation of the function at all Gauss-Legendre quadrature points if first_go == True: pre_computed_dofs = [fun(*self._meshgrid)] @@ -1563,7 +1563,7 @@ def get_dofs_weighted(self, fun, dofs=None, first_go=True, pre_computed_dofs=Non elif self._space_key == "v": assert len(fun) == 3, f"List input only for vector-valued spaces of size 3, but {len(fun) = }." - self._do_nothing = np.zeros(3, dtype=int) + self._do_nothing = xp.zeros(3, dtype=int) for h in range(3): # We check if the current set of basis functions is not one of those we have to compute in the current MPI rank. if ( @@ -1641,13 +1641,13 @@ def __call__( set to false it means we computed it once already and we can reuse the dofs evaluation of the weights instead of recomputing them. - pre_computed_dofs : list of np.arrays + pre_computed_dofs : list of xp.arrays If we have already computed the evaluation of the weights at the dofs we can pass the arrays with their values here, so we do not have to compute them again. Returns ------- - coeffs : psydac.linalg.basic.vector | np.array 3D + coeffs : psydac.linalg.basic.vector | xp.array 3D The FEM spline coefficients after projection. """ if weighted == False: @@ -1858,7 +1858,7 @@ def __init__(self, space_id, mass_ops, **params): self._quad_grid_pts = self.mass_ops.derham.quad_grid_pts[self.space_key] if space_id in ("H1", "L2"): - self._quad_grid_mesh = np.meshgrid( + self._quad_grid_mesh = xp.meshgrid( *[pt.flatten() for pt in self.quad_grid_pts], indexing="ij", ) @@ -1868,12 +1868,12 @@ def __init__(self, space_id, mass_ops, **params): self._tmp = [] # tmp for matrix-vector product of geom_weights with fun for pts in self.quad_grid_pts: self._quad_grid_mesh += [ - np.meshgrid( + xp.meshgrid( *[pt.flatten() for pt in pts], indexing="ij", ), ] - self._tmp += [np.zeros_like(self.quad_grid_mesh[-1][0])] + self._tmp += [xp.zeros_like(self.quad_grid_mesh[-1][0])] # geometric weights evaluated at quadrature grid self._geom_weights = [] # loop over rows (different meshes) @@ -1884,7 +1884,7 @@ def __init__(self, space_id, mass_ops, **params): if weight is not None: self._geom_weights[-1] += [weight(*mesh)] else: - self._geom_weights[-1] += [np.zeros_like(mesh[0])] + self._geom_weights[-1] += [xp.zeros_like(mesh[0])] # other quad grid info if isinstance(self.space, TensorFemSpace): @@ -2010,7 +2010,7 @@ def get_dofs(self, fun, dofs=None, apply_bc=False, clear=True): Parameters ---------- fun : callable | list - Weight function(s) (callables or np.ndarrays) in a 1d list of shape corresponding to number of components. + Weight function(s) (callables or xp.ndarrays) in a 1d list of shape corresponding to number of components. dofs : StencilVector | BlockVector, optional The vector for the output. @@ -2025,7 +2025,7 @@ def get_dofs(self, fun, dofs=None, apply_bc=False, clear=True): # evaluate fun at quad_grid or check array size if callable(fun): fun_weights = fun(*self._quad_grid_mesh) - elif isinstance(fun, np.ndarray): + elif isinstance(fun, xp.ndarray): assert fun.shape == self._quad_grid_mesh[0].shape, ( f"Expected shape {self._quad_grid_mesh[0].shape}, got {fun.shape = } instead." ) @@ -2045,7 +2045,7 @@ def get_dofs(self, fun, dofs=None, apply_bc=False, clear=True): for f in fun: if callable(f): fun_weights[-1] += [f(*mesh)] - elif isinstance(f, np.ndarray): + elif isinstance(f, xp.ndarray): assert f.shape == mesh[0].shape, f"Expected shape {mesh[0].shape}, got {f.shape = } instead." fun_weights[-1] += [f] else: @@ -2062,7 +2062,7 @@ def get_dofs(self, fun, dofs=None, apply_bc=False, clear=True): # compute matrix data for kernel, i.e. fun * geom_weight tot_weights = [] - if isinstance(fun_weights, np.ndarray): + if isinstance(fun_weights, xp.ndarray): tot_weights += [fun_weights * self.geom_weights] else: # loop over rows (differnt meshes) diff --git a/src/struphy/feec/psydac_derham.py b/src/struphy/feec/psydac_derham.py index 3af8ec664..f296d95a9 100644 --- a/src/struphy/feec/psydac_derham.py +++ b/src/struphy/feec/psydac_derham.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import importlib.metadata +import cunumpy as xp import psydac.core.bsplines as bsp from psydac.ddm.cart import DomainDecomposition from psydac.ddm.mpi import MockComm, MockMPI @@ -21,16 +22,18 @@ from struphy.feec.linear_operators import BoundaryOperator from struphy.feec.local_projectors_kernels import get_local_problem_size, select_quasi_points from struphy.feec.projectors import CommutingProjector, CommutingProjectorLocal -from struphy.fields_background.base import MHDequilibrium +from struphy.fields_background.base import FluidEquilibrium, MHDequilibrium from struphy.fields_background.equils import set_defaults from struphy.geometry.base import Domain from struphy.geometry.utilities import TransformedPformComponent from struphy.initial import perturbations, utilities +from struphy.initial.base import Perturbation +from struphy.initial.perturbations import Noise +from struphy.io.options import FieldsBackground, GivenInBasis, NoiseDirections from struphy.kernel_arguments.pusher_args_kernels import DerhamArguments from struphy.polar.basic import PolarDerhamSpace, PolarVector from struphy.polar.extraction_operators import PolarExtractionBlocksC1 from struphy.polar.linear_operators import PolarExtractionOperator, PolarLinearOperator -from struphy.utils.arrays import xp as np class Derham: @@ -114,7 +117,7 @@ def __init__( if dirichlet_bc is not None: assert len(dirichlet_bc) == 3 # make sure that boundary conditions are compatible with spline space - assert np.all([bc == [False, False] for i, bc in enumerate(dirichlet_bc) if spl_kind[i]]) + assert xp.all([bc == (False, False) for i, bc in enumerate(dirichlet_bc) if spl_kind[i]]) self._dirichlet_bc = dirichlet_bc @@ -297,7 +300,7 @@ def __init__( fag.basis, ] - self._spline_types_pyccel[sp_form][-1] = np.array( + self._spline_types_pyccel[sp_form][-1] = xp.array( self._spline_types_pyccel[sp_form][-1], ) # In this case we are working with a scalar valued space @@ -349,7 +352,7 @@ def __init__( self._quad_grid_spans[sp_form] += [fag.spans] self._quad_grid_bases[sp_form] += [fag.basis] - self._spline_types_pyccel[sp_form] = np.array( + self._spline_types_pyccel[sp_form] = xp.array( self._spline_types_pyccel[sp_form], ) else: @@ -361,8 +364,8 @@ def __init__( # index arrays self._indN = [ ( - np.indices((space.ncells, space.degree + 1))[1] - + np.arange( + xp.indices((space.ncells, space.degree + 1))[1] + + xp.arange( space.ncells, )[:, None] ) @@ -371,8 +374,8 @@ def __init__( ] self._indD = [ ( - np.indices((space.ncells, space.degree + 1))[1] - + np.arange( + xp.indices((space.ncells, space.degree + 1))[1] + + xp.arange( space.ncells, )[:, None] ) @@ -522,11 +525,11 @@ def __init__( # collect arguments for kernels self._args_derham = DerhamArguments( - np.array(self.p), + xp.array(self.p), self.Vh_fem["0"].knots[0], self.Vh_fem["0"].knots[1], self.Vh_fem["0"].knots[2], - np.array(self.Vh["0"].starts), + xp.array(self.Vh["0"].starts), ) @property @@ -870,8 +873,11 @@ def create_spline_function( name: str, space_id: str, coeffs: StencilVector | BlockVector = None, - bckgr_params: dict = None, - pert_params: dict = None, + backgrounds: FieldsBackground | list = None, + perturbations: Perturbation | list = None, + domain: Domain = None, + equil: FluidEquilibrium = None, + verbose: bool = True, ): """Creat a callable spline function. @@ -886,19 +892,28 @@ def create_spline_function( coeffs : StencilVector | BlockVector The spline coefficients. - bckgr_params : dict - Field's background parameters. + backgrounds : FieldsBackground | list + For the initial condition. + + perturbations : Perturbation | list + For the initial condition. + + domain : Domain + Mapping for pullback/transform of initial condition. - pert_params : dict - Field's perturbation parameters for initial condition. + equil : FLuidEquilibrium + Fluid background used for inital condition. """ return SplineFunction( name, space_id, self, coeffs, - bckgr_params=bckgr_params, - pert_params=pert_params, + backgrounds=backgrounds, + perturbations=perturbations, + domain=domain, + equil=equil, + verbose=verbose, ) def prepare_eval_tp_fixed(self, grids_1d): @@ -1047,7 +1062,7 @@ def _discretize_space( ) # Create uniform grid - grids = [np.linspace(xmin, xmax, num=ne + 1) for xmin, xmax, ne in zip(min_coords, max_coords, ncells)] + grids = [xp.linspace(xmin, xmax, num=ne + 1) for xmin, xmax, ne in zip(min_coords, max_coords, ncells)] # Create 1D finite element spaces and precompute quadrature data spaces_1d = [ @@ -1092,7 +1107,7 @@ def _get_domain_array(self): Returns ------- - dom_arr : np.ndarray + dom_arr : xp.ndarray A 2d array of shape (#MPI processes, 9). The row index denotes the process rank. The columns are for n=0,1,2: - arr[i, 3*n + 0] holds the LEFT domain boundary of process i in direction eta_(n+1). - arr[i, 3*n + 1] holds the RIGHT domain boundary of process i in direction eta_(n+1). @@ -1106,10 +1121,10 @@ def _get_domain_array(self): nproc = 1 # send buffer - dom_arr_loc = np.zeros(9, dtype=float) + dom_arr_loc = xp.zeros(9, dtype=float) # main array (receive buffers) - dom_arr = np.zeros(nproc * 9, dtype=float) + dom_arr = xp.zeros(nproc * 9, dtype=float) # Get global starts and ends of domain decomposition gl_s = self.domain_decomposition.starts @@ -1140,7 +1155,7 @@ def _get_index_array(self, decomposition): Returns ------- - ind_arr : np.ndarray + ind_arr : xp.ndarray A 2d array of shape (#MPI processes, 6). The row index denotes the process rank. The columns are for n=0,1,2: - arr[i, 2*n + 0] holds the global start index process i in direction eta_(n+1). - arr[i, 2*n + 1] holds the global end index of process i in direction eta_(n+1). @@ -1153,10 +1168,10 @@ def _get_index_array(self, decomposition): nproc = 1 # send buffer - ind_arr_loc = np.zeros(6, dtype=int) + ind_arr_loc = xp.zeros(6, dtype=int) # main array (receive buffers) - ind_arr = np.zeros(nproc * 6, dtype=int) + ind_arr = xp.zeros(nproc * 6, dtype=int) # Get global starts and ends of cart OR domain decomposition gl_s = decomposition.starts @@ -1199,13 +1214,13 @@ def _get_neighbours(self): Returns ------- - neighbours : np.ndarray + neighbours : xp.ndarray A 3d array of shape (3,3,3). The i-th axis is the direction eta_(i+1). Neighbours along the faces have index with two 1s, neighbours along the edges only have one 1, neighbours along the edges have no 1 in the index. """ - neighs = np.empty((3, 3, 3), dtype=int) + neighs = xp.empty((3, 3, 3), dtype=int) for i in range(3): for j in range(3): @@ -1250,8 +1265,8 @@ def _get_neighbour_one_component(self, comp): if comp == [1, 1, 1]: return neigh_id - comp = np.array(comp) - kinds = np.array(kinds) + comp = xp.array(comp) + kinds = xp.array(kinds) # if only one process: check if comp is neighbour in non-peridic directions, if this is not the case then return the rank as neighbour id if size == 1: @@ -1286,15 +1301,15 @@ def _get_neighbour_one_component(self, comp): "Wrong value for component; must be 0 or 1 or 2 !", ) - neigh_inds = np.array(neigh_inds) + neigh_inds = xp.array(neigh_inds) # only use indices where information is present to find the neighbours rank - inds = np.where(neigh_inds != None) + inds = xp.where(neigh_inds != None) # find ranks (row index of domain_array) which agree in start/end indices - index_temp = np.squeeze(self.index_array[:, inds]) - unique_ranks = np.where( - np.equal(index_temp, neigh_inds[inds]).all(1), + index_temp = xp.squeeze(self.index_array[:, inds]) + unique_ranks = xp.where( + xp.equal(index_temp, neigh_inds[inds]).all(1), )[0] # if any row satisfies condition, return its index (=rank of neighbour) @@ -1314,7 +1329,7 @@ def _get_span_and_basis_for_eval_mpi(self, etas, Nspace, end): Parameters ---------- - etas : np.array + etas : xp.array 1d array of evaluation points (ascending). Nspace : SplineSpace @@ -1325,13 +1340,13 @@ def _get_span_and_basis_for_eval_mpi(self, etas, Nspace, end): Returns ------- - spans : np.array + spans : xp.array 1d array of knot span indices. - bn : np.array + bn : xp.array 2d array of pn + 1 values of N-splines indexed by (eta, spline value). - bd : np.array + bd : xp.array 2d array of pn values of D-splines indexed by (eta, spline value). """ @@ -1341,11 +1356,11 @@ def _get_span_and_basis_for_eval_mpi(self, etas, Nspace, end): Tn = Nspace.knots pn = Nspace.degree - spans = np.zeros(etas.size, dtype=int) - bns = np.zeros((etas.size, pn + 1), dtype=float) - bds = np.zeros((etas.size, pn), dtype=float) - bn = np.zeros(pn + 1, dtype=float) - bd = np.zeros(pn, dtype=float) + spans = xp.zeros(etas.size, dtype=int) + bns = xp.zeros((etas.size, pn + 1), dtype=float) + bds = xp.zeros((etas.size, pn), dtype=float) + bn = xp.zeros(pn + 1, dtype=float) + bd = xp.zeros(pn, dtype=float) for n in range(etas.size): # avoid 1. --> 0. for clamped interpolation @@ -1391,11 +1406,17 @@ class SplineFunction: coeffs : StencilVector | BlockVector The spline coefficients (optional). - bckgr_params : dict - Field's background parameters. + backgrounds : FieldsBackground | list + For the initial condition. + + perturbations : Perturbation | list + For the initial condition. + + domain : Domain + Mapping for pullback/transform of initial condition. - pert_params : dict - Field's perturbation parameters for initial condition. + equil : FluidEquilibrium + Fluid background used for inital condition. """ def __init__( @@ -1404,14 +1425,19 @@ def __init__( space_id: str, derham: Derham, coeffs: StencilVector | BlockVector = None, - bckgr_params: dict = None, - pert_params: dict = None, + backgrounds: FieldsBackground | list = None, + perturbations: Perturbation | list = None, + domain: Domain = None, + equil: FluidEquilibrium = None, + verbose: bool = True, ): self._name = name self._space_id = space_id self._derham = derham - self._bckgr_params = bckgr_params - self._pert_params = pert_params + self._backgrounds = backgrounds + self._perturbations = perturbations + self._domain = domain + self._equil = equil # initialize field in memory (FEM space, vector and tensor product (stencil) vector) self._space_key = derham.space_to_form[space_id] @@ -1451,6 +1477,12 @@ def __init__( else: self._nbasis = [tuple([space.nbasis for space in vec_space.spaces]) for vec_space in self.fem_space.spaces] + if verbose and MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nAllocated SplineFuntion '{self.name}' in space '{self.space_id}'.") + + if self.backgrounds is not None or self.perturbations is not None: + self.initialize_coeffs(domain=self.domain, equil=self.equil) + @property def name(self): """Name of the field in data container (string).""" @@ -1471,6 +1503,16 @@ def derham(self): """3d Derham complex struphy.feec.psydac_derham.Derham.""" return self._derham + @property + def domain(self): + """Mapping for pullback/transform of initial condition.""" + return self._domain + + @property + def equil(self): + """Fluid equilibirum used for initial condition.""" + return self._equil + @property def space(self): """Coefficient space (VectorSpace) of the field.""" @@ -1496,7 +1538,7 @@ def vector(self, value): """In-place setter for Stencil-/Block-/PolarVector.""" if isinstance(self._vector, StencilVector): - assert isinstance(value, (StencilVector, np.ndarray)) + assert isinstance(value, (StencilVector, xp.ndarray)) s1, s2, s3 = self.starts e1, e2, e3 = self.ends @@ -1519,10 +1561,10 @@ def vector(self, value): self._vector.set_vector(value) else: if isinstance(self._vector.tp, StencilVector): - assert isinstance(value[0], np.ndarray) + assert isinstance(value[0], xp.ndarray) assert isinstance( value[1], - (StencilVector, np.ndarray), + (StencilVector, xp.ndarray), ) self._vector.pol[0][:] = value[0][:] @@ -1535,10 +1577,10 @@ def vector(self, value): ] else: for n in range(3): - assert isinstance(value[n][0], np.ndarray) + assert isinstance(value[n][0], xp.ndarray) assert isinstance( value[n][1], - (StencilVector, np.ndarray), + (StencilVector, xp.ndarray), ) self._vector.pol[n][:] = value[n][0][:] @@ -1583,14 +1625,14 @@ def vector_stencil(self): return self._vector_stencil @property - def bckgr_params(self): - """Field's background parameters.""" - return self._bckgr_params + def backgrounds(self) -> FieldsBackground | list: + """For the initial condition.""" + return self._backgrounds @property - def pert_params(self): - """Field's perturbation parameters for initial condition.""" - return self._pert_params + def perturbations(self) -> Perturbation | list: + """For the initial condition.""" + return self._perturbations ############### ### Methods ### @@ -1612,173 +1654,180 @@ def extract_coeffs(self, update_ghost_regions=True): def initialize_coeffs( self, *, - bckgr_params=None, - pert_params=None, - domain=None, - bckgr_obj=None, - species=None, + backgrounds: FieldsBackground | list = None, + perturbations: Perturbation | list = None, + domain: Domain = None, + equil: FluidEquilibrium = None, ): """ - Sets the initial conditions for self.vector. - - Parameters - ---------- - bckgr_params : dict - Field's background parameters. - - pert_params : dict - Field's perturbation parameters for initial condition. - - domain : struphy.geometry.domains - Domain object for metric coefficients, only needed for transform of analytical perturbations. - - bckgr_obj: FluidEquilibrium - Fields background object. - - species : string - Species name (e.g. "mhd") the field belongs to. + Set the initial conditions for self.vector. """ # set background paramters - if bckgr_params is not None: - if self._bckgr_params is not None: - print(f"Attention: overwriting background parameters for {self.name}") - self._bckgr_params = bckgr_params + if backgrounds is not None: + # if self.backgrounds is not None: + # print(f"Attention: overwriting backgrounds for {self.name}") + self._backgrounds = backgrounds # set perturbation paramters - if pert_params is not None: - if self._pert_params is not None: - print(f"Attention: overwriting perturbation parameters for {self.name}") - self._pert_params = pert_params + if perturbations is not None: + # if self.perturbations is not None: + # print(f"Attention: overwriting perturbation parameters for {self.name}") + self._perturbations = perturbations + # set domain + if domain is not None: + # if self.domain is not None: + # print(f"Attention: overwriting domain for {self.name}") + self._domain = domain + + if isinstance(self.backgrounds, FieldsBackground): + self._backgrounds = [self.backgrounds] + + if isinstance(self.perturbations, Perturbation): + self._perturbations = [self.perturbations] + + # start from zero coeffs self._vector *= 0.0 if MPI.COMM_WORLD.Get_rank() == 0: print(f"Initializing {self.name} ...") - # add background to initial vector - if self.bckgr_params is not None: - for _type in self.bckgr_params: - _params = self.bckgr_params[_type].copy() + # add backgrounds to initial vector + if self.backgrounds is not None: + for fb in self.backgrounds: + assert isinstance(fb, FieldsBackground) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"Adding background {fb} ...") # special case of const - if "LogicalConst" in _type: - _val = _params["values"] + if fb.type == "LogicalConst": + vals = fb.values + assert isinstance(vals, (list, tuple)) if self.space_id in {"H1", "L2"}: - assert isinstance(_val, float) or isinstance(_val, int) def f_tmp(e1, e2, e3): - return _val + 0.0 * e1 + return vals[0] + 0.0 * e1 fun = f_tmp else: - assert isinstance(_val, list) - assert len(_val) == 3 + assert len(vals) == 3 fun = [] - for i, _v in enumerate(_val): - assert isinstance(_v, float) or isinstance(_v, int) or _v is None - if _val[0] is not None: - fun += [lambda e1, e2, e3: _val[0] + 0.0 * e1] + if vals[0] is not None: + fun += [lambda e1, e2, e3: vals[0] + 0.0 * e1] else: fun += [lambda e1, e2, e3: 0.0 * e1] - if _val[1] is not None: - fun += [lambda e1, e2, e3: _val[1] + 0.0 * e1] + if vals[1] is not None: + fun += [lambda e1, e2, e3: vals[1] + 0.0 * e1] else: fun += [lambda e1, e2, e3: 0.0 * e1] - if _val[2] is not None: - fun += [lambda e1, e2, e3: _val[2] + 0.0 * e1] + if vals[2] is not None: + fun += [lambda e1, e2, e3: vals[2] + 0.0 * e1] else: fun += [lambda e1, e2, e3: 0.0 * e1] else: - assert bckgr_obj is not None - _var = _params["variable"] - assert _var in dir(MHDequilibrium), f"{_var = } is not an attribute of any fields background." + assert equil is not None + var = fb.variable + assert var in dir(MHDequilibrium), f"{var = } is not an attribute of any fields background." if self.space_id in {"H1", "L2"}: - fun = getattr(bckgr_obj, _var) + fun = getattr(equil, var) else: - assert (_var + "_1") in dir(MHDequilibrium), ( - f"{(_var + '_1') = } is not an attribute of any fields background." + assert (var + "_1") in dir(MHDequilibrium), ( + f"{(var + '_1') = } is not an attribute of any fields background." ) fun = [ - getattr(bckgr_obj, _var + "_1"), - getattr(bckgr_obj, _var + "_2"), - getattr(bckgr_obj, _var + "_3"), + getattr(equil, var + "_1"), + getattr(equil, var + "_2"), + getattr(equil, var + "_3"), ] - # peform projection + # perform projection self.vector += self.derham.P[self.space_key](fun) # add perturbations to coefficient vector - if self.pert_params is not None: - for _type in self.pert_params: + if self.perturbations is not None: + for ptb in self.perturbations: if MPI.COMM_WORLD.Get_rank() == 0: - print(f"Adding perturbation {_type} ...") - - _params = self.pert_params[_type].copy() + print(f"Adding perturbation {ptb} ...") # special case of white noise in logical space for different components - if "noise" in _type: - # component(s) to perturb - if isinstance(_params["comps"], bool): - comps = [_params["comps"]] - else: - comps = _params["comps"] - _params.pop("comps") - + if isinstance(ptb, Noise): # set white noise FE coefficients + self._add_noise( + direction=ptb.direction, + amp=ptb.amp, + seed=ptb.seed, + n=ptb.comp, + ) + # perturbation class + elif isinstance(ptb, Perturbation): if self.space_id in {"H1", "L2"}: - if comps[0]: - self._add_noise(**_params) + fun = TransformedPformComponent( + ptb, + ptb.given_in_basis, + self.space_key, + domain=domain, + ) elif self.space_id in {"Hcurl", "Hdiv", "H1vec"}: - for n, comp in enumerate(comps): - if comp: - self._add_noise(**_params, n=n) + fun_vec = [None] * 3 + fun_vec[ptb.comp] = ptb - # given function class - elif _type in dir(perturbations): - fun = transform_perturbation(_type, _params, self.space_key, domain) + # pullback callable for each component + fun = [] + for comp in range(3): + fun += [ + TransformedPformComponent( + fun_vec, + ptb.given_in_basis, + self.space_key, + comp=comp, + domain=domain, + ), + ] # peform projection self.vector += self.derham.P[self.space_key](fun) - # loading of MHD eigenfunction (legacy code, might not be up to date) - elif "EigFun" in _type: - print("Warning: Eigfun is not regularly tested ...") - from struphy.initial import eigenfunctions - - # select class - funs = getattr(eigenfunctions, _type)( - self.derham, - **_params, - ) - - # select eigenvector and set coefficients - if hasattr(funs, self.name): - eig_vec = getattr(funs, self.name) - - self.vector += eig_vec - - # initialize from existing output file - elif "InitFromOutput" in _type: - # select class - o_data = getattr(utilities, _type)( - self.derham, - self.name, - species, - **_params, - ) + # TODO: re-add Eigfun and InitFromOutput in new framework - if isinstance(self.vector, StencilVector): - self.vector._data[:] += o_data.vector - - else: - for n in range(3): - self.vector[n]._data[:] += o_data.vector[n] + # loading of MHD eigenfunction (legacy code, might not be up to date) + # elif "EigFun" in _type: + # print("Warning: Eigfun is not regularly tested ...") + # from struphy.initial import eigenfunctions + + # # select class + # funs = getattr(eigenfunctions, _type)( + # self.derham, + # **_params, + # ) + + # # select eigenvector and set coefficients + # if hasattr(funs, self.name): + # eig_vec = getattr(funs, self.name) + + # self.vector += eig_vec + + # # initialize from existing output file + # elif "InitFromOutput" in _type: + # # select class + # o_data = getattr(utilities, _type)( + # self.derham, + # self.name, + # species, + # **_params, + # ) + + # if isinstance(self.vector, StencilVector): + # self.vector._data[:] += o_data.vector + + # else: + # for n in range(3): + # self.vector[n]._data[:] += o_data.vector[n] # apply boundary operator (in-place) self.derham.boundary_ops[self.space_key].dot( @@ -1835,7 +1884,7 @@ def eval_tp_fixed_loc(self, spans, bases, out=None): assert [span.size for span in spans] == [base.shape[0] for base in bases] if out is None: - out = np.empty([span.size for span in spans], dtype=float) + out = xp.empty([span.size for span in spans], dtype=float) else: assert out.shape == tuple([span.size for span in spans]) @@ -1844,8 +1893,8 @@ def eval_tp_fixed_loc(self, spans, bases, out=None): *bases, vec._data, self.derham.spline_types_pyccel[self.space_key], - np.array(self.derham.p), - np.array(self.starts), + xp.array(self.derham.p), + xp.array(self.starts), out, ) @@ -1859,7 +1908,7 @@ def eval_tp_fixed_loc(self, spans, bases, out=None): assert [span.size for span in spans] == [base.shape[0] for base in bases[i]] if out_is_none: - out += np.empty( + out += xp.empty( [span.size for span in spans], dtype=float, ) @@ -1873,10 +1922,10 @@ def eval_tp_fixed_loc(self, spans, bases, out=None): *bases[i], vec[i]._data, self.derham.spline_types_pyccel[self.space_key][i], - np.array( + xp.array( self.derham.p, ), - np.array( + xp.array( self.starts[i], ), out[i], @@ -1943,14 +1992,14 @@ def __call__(self, *etas, out=None, tmp=None, squeeze_out=False, local=False): # prepare arrays for AllReduce if tmp is None: - tmp = np.zeros( + tmp = xp.zeros( tmp_shape, dtype=float, ) else: - assert isinstance(tmp, np.ndarray) + assert isinstance(tmp, xp.ndarray) assert tmp.shape == tmp_shape - assert tmp.dtype.type is np.float64 + assert tmp.dtype.type is xp.float64 tmp[:] = 0.0 # scalar-valued field @@ -1965,11 +2014,11 @@ def __call__(self, *etas, out=None, tmp=None, squeeze_out=False, local=False): E3, self._vector_stencil._data, kind, - np.array(self.derham.p), + xp.array(self.derham.p), T1, T2, T3, - np.array(self.starts), + xp.array(self.starts), tmp, ) elif marker_evaluation: @@ -1978,11 +2027,11 @@ def __call__(self, *etas, out=None, tmp=None, squeeze_out=False, local=False): markers, self._vector_stencil._data, kind, - np.array(self.derham.p), + xp.array(self.derham.p), T1, T2, T3, - np.array(self.starts), + xp.array(self.starts), tmp, ) else: @@ -1993,11 +2042,11 @@ def __call__(self, *etas, out=None, tmp=None, squeeze_out=False, local=False): E3, self._vector_stencil._data, kind, - np.array(self.derham.p), + xp.array(self.derham.p), T1, T2, T3, - np.array(self.starts), + xp.array(self.starts), tmp, ) @@ -2017,7 +2066,7 @@ def __call__(self, *etas, out=None, tmp=None, squeeze_out=False, local=False): out += tmp if squeeze_out: - out = np.squeeze(out) + out = xp.squeeze(out) if out.ndim == 0: out = out.item() @@ -2036,11 +2085,11 @@ def __call__(self, *etas, out=None, tmp=None, squeeze_out=False, local=False): E3, self._vector_stencil[n]._data, kind, - np.array(self.derham.p), + xp.array(self.derham.p), T1, T2, T3, - np.array(self.starts[n]), + xp.array(self.starts[n]), tmp, ) elif marker_evaluation: @@ -2049,11 +2098,11 @@ def __call__(self, *etas, out=None, tmp=None, squeeze_out=False, local=False): markers, self._vector_stencil[n]._data, kind, - np.array(self.derham.p), + xp.array(self.derham.p), T1, T2, T3, - np.array(self.starts[n]), + xp.array(self.starts[n]), tmp, ) else: @@ -2064,11 +2113,11 @@ def __call__(self, *etas, out=None, tmp=None, squeeze_out=False, local=False): E3, self._vector_stencil[n]._data, kind, - np.array(self.derham.p), + xp.array(self.derham.p), T1, T2, T3, - np.array(self.starts[n]), + xp.array(self.starts[n]), tmp, ) @@ -2090,7 +2139,7 @@ def __call__(self, *etas, out=None, tmp=None, squeeze_out=False, local=False): tmp[:] = 0.0 if squeeze_out: - out[-1] = np.squeeze(out[-1]) + out[-1] = xp.squeeze(out[-1]) if out[-1].ndim == 0: out[-1] = out[-1].item() @@ -2123,11 +2172,11 @@ def _flag_pts_not_on_proc(self, *etas): markers = etas[0] # check which particles are on the current process domain - is_on_proc_domain = np.logical_and( + is_on_proc_domain = xp.logical_and( markers[:, :3] >= dom_arr[rank, 0::3], markers[:, :3] <= dom_arr[rank, 1::3], ) - on_proc = np.all(is_on_proc_domain, axis=1) + on_proc = xp.all(is_on_proc_domain, axis=1) markers[~on_proc, :] = -1.0 @@ -2153,15 +2202,15 @@ def _flag_pts_not_on_proc(self, *etas): E3[E3 == dom_arr[rank, 7]] += 1e-8 # True for eval points on current process - E1_on_proc = np.logical_and( + E1_on_proc = xp.logical_and( E1 >= dom_arr[rank, 0], E1 <= dom_arr[rank, 1], ) - E2_on_proc = np.logical_and( + E2_on_proc = xp.logical_and( E2 >= dom_arr[rank, 3], E2 <= dom_arr[rank, 4], ) - E3_on_proc = np.logical_and( + E3_on_proc = xp.logical_and( E3 >= dom_arr[rank, 6], E3 <= dom_arr[rank, 7], ) @@ -2171,7 +2220,13 @@ def _flag_pts_not_on_proc(self, *etas): E2[~E2_on_proc] = -1.0 E3[~E3_on_proc] = -1.0 - def _add_noise(self, direction="e3", amp=0.0001, seed=None, n=None): + def _add_noise( + self, + direction: NoiseDirections = "e3", + amp: float = 0.0001, + seed: int = None, + n: int = None, + ): """Add noise to a vector component where init_comps==True, otherwise leave at zero. Parameters @@ -2316,7 +2371,7 @@ def _tmp_noise_for_mpi(self, *shapes, direction="e3", amp=0.0001, seed=None): Returns ------- - _amps : np.array + _amps : xp.array The noisy FE coefficients in the desired direction (1d, 2d or 3d array).""" if self.derham.comm is not None: @@ -2331,40 +2386,40 @@ def _tmp_noise_for_mpi(self, *shapes, direction="e3", amp=0.0001, seed=None): domain_array = self.derham.domain_array if seed is not None: - np.random.seed(seed) + xp.random.seed(seed) # temporary - _amps = np.zeros(shapes) + _amps = xp.zeros(shapes) # no process has been drawn for yet - already_drawn = np.zeros(nprocs) == 1.0 + already_drawn = xp.zeros(nprocs) == 1.0 # 1d mid point arrays in each direction mid_points = [] for npr in nprocs: delta = 1.0 / npr - mid_points_i = np.zeros(npr) + mid_points_i = xp.zeros(npr) for n in range(npr): mid_points_i[n] = delta * (n + 1 / 2) mid_points += [mid_points_i] if direction == "e1": - tmp_arrays = np.zeros(nprocs[0]).tolist() + tmp_arrays = xp.zeros(nprocs[0]).tolist() elif direction == "e2": - tmp_arrays = np.zeros(nprocs[1]).tolist() + tmp_arrays = xp.zeros(nprocs[1]).tolist() elif direction == "e3": - tmp_arrays = np.zeros(nprocs[2]).tolist() + tmp_arrays = xp.zeros(nprocs[2]).tolist() elif direction == "e1e2": - tmp_arrays = np.zeros((nprocs[0], nprocs[1])).tolist() + tmp_arrays = xp.zeros((nprocs[0], nprocs[1])).tolist() Warning, f"2d noise in the directions {direction} is not correctly initilaized for MPI !!" elif direction == "e1e3": - tmp_arrays = np.zeros((nprocs[0], nprocs[2])).tolist() + tmp_arrays = xp.zeros((nprocs[0], nprocs[2])).tolist() Warning, f"2d noise in the directions {direction} is not correctly initilaized for MPI !!" elif direction == "e2e3": - tmp_arrays = np.zeros((nprocs[1], nprocs[2])).tolist() + tmp_arrays = xp.zeros((nprocs[1], nprocs[2])).tolist() Warning, f"2d noise in the directions {direction} is not correctly initilaized for MPI !!" elif direction == "e1e2e3": - tmp_arrays = np.zeros((nprocs[0], nprocs[1], nprocs[2])).tolist() + tmp_arrays = xp.zeros((nprocs[0], nprocs[1], nprocs[2])).tolist() Warning, f"3d noise in the directions {direction} is not correctly initilaized for MPI !!" else: raise ValueError("Invalid direction for tmp_arrays.") @@ -2373,7 +2428,7 @@ def _tmp_noise_for_mpi(self, *shapes, direction="e3", amp=0.0001, seed=None): inds_current = [] for n in range(3): mid_pt_current = (domain_array[rank, 3 * n] + domain_array[rank, 3 * n + 1]) / 2.0 - inds_current += [np.argmin(np.abs(mid_points[n] - mid_pt_current))] + inds_current += [xp.argmin(xp.abs(mid_points[n] - mid_pt_current))] # loop over processes for i in range(comm_size): @@ -2381,7 +2436,7 @@ def _tmp_noise_for_mpi(self, *shapes, direction="e3", amp=0.0001, seed=None): inds = [] for n in range(3): mid_pt = (domain_array[i, 3 * n] + domain_array[i, 3 * n + 1]) / 2.0 - inds += [np.argmin(np.abs(mid_points[n] - mid_pt))] + inds += [xp.argmin(xp.abs(mid_points[n] - mid_pt))] if already_drawn[inds[0], inds[1], inds[2]]: if direction == "e1": @@ -2403,7 +2458,7 @@ def _tmp_noise_for_mpi(self, *shapes, direction="e3", amp=0.0001, seed=None): if direction == "e1": tmp_arrays[inds[0]] = ( ( - np.random.rand( + xp.random.rand( *shapes, ) - 0.5 @@ -2416,7 +2471,7 @@ def _tmp_noise_for_mpi(self, *shapes, direction="e3", amp=0.0001, seed=None): elif direction == "e2": tmp_arrays[inds[1]] = ( ( - np.random.rand( + xp.random.rand( *shapes, ) - 0.5 @@ -2429,7 +2484,7 @@ def _tmp_noise_for_mpi(self, *shapes, direction="e3", amp=0.0001, seed=None): elif direction == "e3": tmp_arrays[inds[2]] = ( ( - np.random.rand( + xp.random.rand( *shapes, ) - 0.5 @@ -2440,23 +2495,23 @@ def _tmp_noise_for_mpi(self, *shapes, direction="e3", amp=0.0001, seed=None): already_drawn[:, :, inds[2]] = True _amps[:] = tmp_arrays[inds[2]] elif direction == "e1e2": - tmp_arrays[inds[0]][inds[1]] = (np.random.rand(*shapes) - 0.5) * 2.0 * amp + tmp_arrays[inds[0]][inds[1]] = (xp.random.rand(*shapes) - 0.5) * 2.0 * amp already_drawn[inds[0], inds[1], :] = True _amps[:] = tmp_arrays[inds[0]][inds[1]] elif direction == "e1e3": - tmp_arrays[inds[0]][inds[2]] = (np.random.rand(*shapes) - 0.5) * 2.0 * amp + tmp_arrays[inds[0]][inds[2]] = (xp.random.rand(*shapes) - 0.5) * 2.0 * amp already_drawn[inds[0], :, inds[2]] = True _amps[:] = tmp_arrays[inds[0]][inds[2]] elif direction == "e2e3": - tmp_arrays[inds[1]][inds[2]] = (np.random.rand(*shapes) - 0.5) * 2.0 * amp + tmp_arrays[inds[1]][inds[2]] = (xp.random.rand(*shapes) - 0.5) * 2.0 * amp already_drawn[:, inds[1], inds[2]] = True _amps[:] = tmp_arrays[inds[1]][inds[2]] elif direction == "e1e2e3": - tmp_arrays[inds[0]][inds[1]][inds[2]] = (np.random.rand(*shapes) - 0.5) * 2.0 * amp + tmp_arrays[inds[0]][inds[1]][inds[2]] = (xp.random.rand(*shapes) - 0.5) * 2.0 * amp already_drawn[inds[0], inds[1], inds[2]] = True _amps[:] = tmp_arrays[inds[0]][inds[1]][inds[2]] - if np.all(np.array([ind_c == ind for ind_c, ind in zip(inds_current, inds)])): + if xp.all(xp.array([ind_c == ind for ind_c, ind in zip(inds_current, inds)])): return _amps @@ -2707,16 +2762,16 @@ def get_pts_and_wts(space_1d, start, end, n_quad=None, polar_shift=False): histopol_loc = space_1d.histopolation_grid[start : end + 2].copy() # make sure that greville points used for interpolation are in [0, 1] - assert np.all(np.logical_and(greville_loc >= 0.0, greville_loc <= 1.0)) + assert xp.all(xp.logical_and(greville_loc >= 0.0, greville_loc <= 1.0)) # interpolation if space_1d.basis == "B": x_grid = greville_loc pts = greville_loc[:, None] - wts = np.ones(pts.shape, dtype=float) + wts = xp.ones(pts.shape, dtype=float) # sub-interval index is always 0 for interpolation. - subs = np.zeros(pts.shape[0], dtype=int) + subs = xp.zeros(pts.shape[0], dtype=int) # !! shift away first interpolation point in eta_1 direction for polar domains !! if pts[0] == 0.0 and polar_shift: @@ -2730,27 +2785,27 @@ def get_pts_and_wts(space_1d, start, end, n_quad=None, polar_shift=False): union_breaks = space_1d.breaks[:-1] # Make union of Greville and break points - tmp = set(np.round(space_1d.histopolation_grid, decimals=14)).union( - np.round(union_breaks, decimals=14), + tmp = set(xp.round(space_1d.histopolation_grid, decimals=14)).union( + xp.round(union_breaks, decimals=14), ) tmp = list(tmp) tmp.sort() - tmp_a = np.array(tmp) + tmp_a = xp.array(tmp) x_grid = tmp_a[ - np.logical_and( + xp.logical_and( tmp_a - >= np.min( + >= xp.min( histopol_loc, ) - 1e-14, - tmp_a <= np.max(histopol_loc) + 1e-14, + tmp_a <= xp.max(histopol_loc) + 1e-14, ) ] # determine subinterval index (= 0 or 1): - subs = np.zeros(x_grid[:-1].size, dtype=int) + subs = xp.zeros(x_grid[:-1].size, dtype=int) for n, x_h in enumerate(x_grid[:-1]): add = 1 for x_g in histopol_loc: @@ -2763,7 +2818,7 @@ def get_pts_and_wts(space_1d, start, end, n_quad=None, polar_shift=False): # products of basis functions are integrated exactly n_quad = space_1d.degree + 1 - pts_loc, wts_loc = np.polynomial.legendre.leggauss(n_quad) + pts_loc, wts_loc = xp.polynomial.legendre.leggauss(n_quad) x, wts = bsp.quadrature_grid(x_grid, pts_loc, wts_loc) @@ -2826,12 +2881,12 @@ def get_pts_and_wts_quasi( # interpolation if space_1d.basis == "B": if p == 1 and h != 1.0: - x_grid = np.linspace(-(p - 1) * h, 1.0 - h + (h / 2.0), (N + p - 1) * 2) + x_grid = xp.linspace(-(p - 1) * h, 1.0 - h + (h / 2.0), (N + p - 1) * 2) else: - x_grid = np.linspace(-(p - 1) * h, 1.0 - h, (N + p - 1) * 2 - 1) + x_grid = xp.linspace(-(p - 1) * h, 1.0 - h, (N + p - 1) * 2 - 1) pts = x_grid[:, None] % 1.0 - wts = np.ones(pts.shape, dtype=float) + wts = xp.ones(pts.shape, dtype=float) # !! shift away first interpolation point in eta_1 direction for polar domains !! if pts[0] == 0.0 and polar_shift: @@ -2842,16 +2897,16 @@ def get_pts_and_wts_quasi( # The computation of histopolation points breaks in case we have Nel=1 and periodic boundary conditions since we end up with only one x_grid point. # We need to build the histopolation points by hand in this scenario. if p == 0 and h == 1.0: - x_grid = np.array([0.0, 0.5, 1.0]) + x_grid = xp.array([0.0, 0.5, 1.0]) elif p == 0 and h != 1.0: - x_grid = np.linspace(-p * h, 1.0 - h + (h / 2.0), (N + p) * 2) + x_grid = xp.linspace(-p * h, 1.0 - h + (h / 2.0), (N + p) * 2) else: - x_grid = np.linspace(-p * h, 1.0 - h, (N + p) * 2 - 1) + x_grid = xp.linspace(-p * h, 1.0 - h, (N + p) * 2 - 1) n_quad = p + 1 # Gauss - Legendre quadrature points and weights # products of basis functions are integrated exactly - pts_loc, wts_loc = np.polynomial.legendre.leggauss(n_quad) + pts_loc, wts_loc = xp.polynomial.legendre.leggauss(n_quad) x, wts = bsp.quadrature_grid(x_grid, pts_loc, wts_loc) pts = x % 1.0 @@ -2865,26 +2920,26 @@ def get_pts_and_wts_quasi( N_b = N + p # Filling the quasi-interpolation points for i=0 and i=1 (since they are equal) - x_grid = np.linspace(0.0, knots[p + 1], p + 1) - x_aux = np.linspace(0.0, knots[p + 1], p + 1) - x_grid = np.append(x_grid, x_aux) + x_grid = xp.linspace(0.0, knots[p + 1], p + 1) + x_aux = xp.linspace(0.0, knots[p + 1], p + 1) + x_grid = xp.append(x_grid, x_aux) # Now we append those for 1 V2):") res_PSY = OPS_PSY.Q1.dot(x1_st) - res_STR = OPS_STR.Q1_dot(np.concatenate((x1[0].flatten(), x1[1].flatten(), x1[2].flatten()))) + res_STR = OPS_STR.Q1_dot(xp.concatenate((x1[0].flatten(), x1[1].flatten(), x1[2].flatten()))) res_STR_0, res_STR_1, res_STR_2 = SPACES.extract_2(res_STR) MPI_COMM.Barrier() @@ -284,7 +284,7 @@ def test_some_basis_ops(Nel, p, spl_kind, mapping): Q1T = OPS_PSY.Q1.transpose() res_PSY = Q1T.dot(x2_st) - res_STR = OPS_STR.transpose_Q1_dot(np.concatenate((x2[0].flatten(), x2[1].flatten(), x2[2].flatten()))) + res_STR = OPS_STR.transpose_Q1_dot(xp.concatenate((x2[0].flatten(), x2[1].flatten(), x2[2].flatten()))) res_STR_0, res_STR_1, res_STR_2 = SPACES.extract_1(res_STR) MPI_COMM.Barrier() @@ -310,7 +310,7 @@ def test_some_basis_ops(Nel, p, spl_kind, mapping): print("\nW1 (V1 --> V1, Identity operator in this case):") res_PSY = OPS_PSY.W1.dot(x1_st) - res_STR = OPS_STR.W1_dot(np.concatenate((x1[0].flatten(), x1[1].flatten(), x1[2].flatten()))) + res_STR = OPS_STR.W1_dot(xp.concatenate((x1[0].flatten(), x1[1].flatten(), x1[2].flatten()))) res_STR_0, res_STR_1, res_STR_2 = SPACES.extract_1(res_STR) MPI_COMM.barrier() @@ -333,7 +333,7 @@ def test_some_basis_ops(Nel, p, spl_kind, mapping): W1T = OPS_PSY.W1.transpose() res_PSY = W1T.dot(x1_st) - res_STR = OPS_STR.transpose_W1_dot(np.concatenate((x1[0].flatten(), x1[1].flatten(), x1[2].flatten()))) + res_STR = OPS_STR.transpose_W1_dot(xp.concatenate((x1[0].flatten(), x1[1].flatten(), x1[2].flatten()))) res_STR_0, res_STR_1, res_STR_2 = SPACES.extract_1(res_STR) MPI_COMM.barrier() @@ -359,7 +359,7 @@ def test_some_basis_ops(Nel, p, spl_kind, mapping): print("\nQ2 (V2 --> V2, Identity operator in this case):") res_PSY = OPS_PSY.Q2.dot(x2_st) - res_STR = OPS_STR.Q2_dot(np.concatenate((x2[0].flatten(), x2[1].flatten(), x2[2].flatten()))) + res_STR = OPS_STR.Q2_dot(xp.concatenate((x2[0].flatten(), x2[1].flatten(), x2[2].flatten()))) res_STR_0, res_STR_1, res_STR_2 = SPACES.extract_2(res_STR) MPI_COMM.Barrier() @@ -382,7 +382,7 @@ def test_some_basis_ops(Nel, p, spl_kind, mapping): Q2T = OPS_PSY.Q2.transpose() res_PSY = Q2T.dot(x2_st) - res_STR = OPS_STR.transpose_Q2_dot(np.concatenate((x2[0].flatten(), x2[1].flatten(), x2[2].flatten()))) + res_STR = OPS_STR.transpose_Q2_dot(xp.concatenate((x2[0].flatten(), x2[1].flatten(), x2[2].flatten()))) res_STR_0, res_STR_1, res_STR_2 = SPACES.extract_2(res_STR) MPI_COMM.Barrier() @@ -408,7 +408,7 @@ def test_some_basis_ops(Nel, p, spl_kind, mapping): print("\nX1 (V1 --> V0 x V0 x V0):") res_PSY = OPS_PSY.X1.dot(x1_st) - res_STR = OPS_STR.X1_dot(np.concatenate((x1[0].flatten(), x1[1].flatten(), x1[2].flatten()))) + res_STR = OPS_STR.X1_dot(xp.concatenate((x1[0].flatten(), x1[1].flatten(), x1[2].flatten()))) res_STR_0 = SPACES.extract_0(res_STR[0]) res_STR_1 = SPACES.extract_0(res_STR[1]) res_STR_2 = SPACES.extract_0(res_STR[2]) @@ -460,10 +460,11 @@ def test_some_basis_ops(Nel, p, spl_kind, mapping): @pytest.mark.parametrize("spl_kind", [[False, True, True], [False, True, False]]) @pytest.mark.parametrize( "dirichlet_bc", - [None, [[False, True], [False, False], [False, True]], [[False, False], [False, False], [True, False]]], + [None, [(False, True), (False, False), (False, True)], [(False, False), (False, False), (True, False)]], ) @pytest.mark.parametrize("mapping", [["IGAPolarCylinder", {"a": 1.0, "Lz": 3.0}]]) def test_basis_ops_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): + import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from struphy.eigenvalue_solvers.mhd_operators import MHDOperators @@ -474,7 +475,6 @@ def test_basis_ops_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=Fal from struphy.fields_background.equils import ScrewPinch from struphy.geometry import domains from struphy.polar.basic import PolarVector - from struphy.utils.arrays import xp as np mpi_comm = MPI.COMM_WORLD mpi_rank = mpi_comm.Get_rank() @@ -515,9 +515,11 @@ def test_basis_ops_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=Fal if dirichlet_bc is not None: for i, knd in enumerate(spl_kind): if knd: - dirichlet_bc[i] = [False, False] + dirichlet_bc[i] = (False, False) else: - dirichlet_bc = [[False, False]] * 3 + dirichlet_bc = [(False, False)] * 3 + + dirichlet_bc = tuple(dirichlet_bc) # derham object nq_el = [p[0] + 1, p[1] + 1, p[2] + 1] @@ -582,11 +584,11 @@ def test_basis_ops_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=Fal x2_pol_psy.tp = x2_psy x3_pol_psy.tp = x3_psy - np.random.seed(1607) - x0_pol_psy.pol = [np.random.rand(x0_pol_psy.pol[0].shape[0], x0_pol_psy.pol[0].shape[1])] - x1_pol_psy.pol = [np.random.rand(x1_pol_psy.pol[n].shape[0], x1_pol_psy.pol[n].shape[1]) for n in range(3)] - x2_pol_psy.pol = [np.random.rand(x2_pol_psy.pol[n].shape[0], x2_pol_psy.pol[n].shape[1]) for n in range(3)] - x3_pol_psy.pol = [np.random.rand(x3_pol_psy.pol[0].shape[0], x3_pol_psy.pol[0].shape[1])] + xp.random.seed(1607) + x0_pol_psy.pol = [xp.random.rand(x0_pol_psy.pol[0].shape[0], x0_pol_psy.pol[0].shape[1])] + x1_pol_psy.pol = [xp.random.rand(x1_pol_psy.pol[n].shape[0], x1_pol_psy.pol[n].shape[1]) for n in range(3)] + x2_pol_psy.pol = [xp.random.rand(x2_pol_psy.pol[n].shape[0], x2_pol_psy.pol[n].shape[1]) for n in range(3)] + x3_pol_psy.pol = [xp.random.rand(x3_pol_psy.pol[0].shape[0], x3_pol_psy.pol[0].shape[1])] # apply boundary conditions to legacy vectors for right shape x0_pol_str = space.B0.dot(x0_pol_psy.toarray(True)) @@ -612,7 +614,7 @@ def test_basis_ops_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=Fal r_str = mhd_ops_str.PR(x3_pol_str) print(f"Rank {mpi_rank} | Asserting MHD operator K3.") - np.allclose(space.B3.T.dot(r_str), r_psy.toarray(True)) + xp.allclose(space.B3.T.dot(r_str), r_psy.toarray(True)) print(f"Rank {mpi_rank} | Assertion passed.") mpi_comm.Barrier() @@ -625,7 +627,7 @@ def test_basis_ops_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=Fal r_str = mhd_ops_str.PR.T(x3_pol_str) print(f"Rank {mpi_rank} | Asserting transpose MHD operator K3.T.") - np.allclose(space.B3.T.dot(r_str), r_psy.toarray(True)) + xp.allclose(space.B3.T.dot(r_str), r_psy.toarray(True)) print(f"Rank {mpi_rank} | Assertion passed.") # ===== operator Q2 (V2 --> V2) ============ @@ -642,7 +644,7 @@ def test_basis_ops_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=Fal r_str = mhd_ops_str.MF(x2_pol_str) print(f"Rank {mpi_rank} | Asserting MHD operator Q2.") - np.allclose(space.B2.T.dot(r_str), r_psy.toarray(True)) + xp.allclose(space.B2.T.dot(r_str), r_psy.toarray(True)) print(f"Rank {mpi_rank} | Assertion passed.") mpi_comm.Barrier() @@ -655,7 +657,7 @@ def test_basis_ops_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=Fal r_str = mhd_ops_str.MF.T(x2_pol_str) print(f"Rank {mpi_rank} | Asserting transposed MHD operator Q2.T.") - np.allclose(space.B2.T.dot(r_str), r_psy.toarray(True)) + xp.allclose(space.B2.T.dot(r_str), r_psy.toarray(True)) print(f"Rank {mpi_rank} | Assertion passed.") # ===== operator T2 (V2 --> V1) ============ @@ -672,7 +674,7 @@ def test_basis_ops_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=Fal r_str = mhd_ops_str.EF(x2_pol_str) print(f"Rank {mpi_rank} | Asserting MHD operator T2.") - np.allclose(space.B1.T.dot(r_str), r_psy.toarray(True)) + xp.allclose(space.B1.T.dot(r_str), r_psy.toarray(True)) print(f"Rank {mpi_rank} | Assertion passed.") mpi_comm.Barrier() @@ -685,7 +687,7 @@ def test_basis_ops_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=Fal r_str = mhd_ops_str.EF.T(x1_pol_str) print(f"Rank {mpi_rank} | Asserting transposed MHD operator T2.T.") - np.allclose(space.B2.T.dot(r_str), r_psy.toarray(True)) + xp.allclose(space.B2.T.dot(r_str), r_psy.toarray(True)) print(f"Rank {mpi_rank} | Assertion passed.") # ===== operator S2 (V2 --> V2) ============ @@ -702,7 +704,7 @@ def test_basis_ops_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=Fal r_str = mhd_ops_str.PF(x2_pol_str) print(f"Rank {mpi_rank} | Asserting MHD operator S2.") - np.allclose(space.B2.T.dot(r_str), r_psy.toarray(True)) + xp.allclose(space.B2.T.dot(r_str), r_psy.toarray(True)) print(f"Rank {mpi_rank} | Assertion passed.") mpi_comm.Barrier() @@ -715,7 +717,7 @@ def test_basis_ops_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=Fal r_str = mhd_ops_str.PF.T(x2_pol_str) print(f"Rank {mpi_rank} | Asserting transposed MHD operator S2.T.") - np.allclose(space.B2.T.dot(r_str), r_psy.toarray(True)) + xp.allclose(space.B2.T.dot(r_str), r_psy.toarray(True)) print(f"Rank {mpi_rank} | Assertion passed.") @@ -724,7 +726,7 @@ def assert_ops(mpi_rank, res_PSY, res_STR, verbose=False, MPI_COMM=None): TODO """ - from struphy.utils.arrays import xp as np + import cunumpy as xp if verbose: if MPI_COMM is not None: @@ -787,8 +789,8 @@ def assert_ops(mpi_rank, res_PSY, res_STR, verbose=False, MPI_COMM=None): print( f"Rank {mpi_rank} | Maximum absolute diference (result):\n", - np.max( - np.abs( + xp.max( + xp.abs( res_PSY[ res_PSY.starts[0] : res_PSY.ends[0] + 1, res_PSY.starts[1] : res_PSY.ends[1] + 1, @@ -807,7 +809,7 @@ def assert_ops(mpi_rank, res_PSY, res_STR, verbose=False, MPI_COMM=None): MPI_COMM.Barrier() # Compare results. (Works only for Nel=[N, N, N] so far! TODO: Find this bug!) - assert np.allclose( + assert xp.allclose( res_PSY[ res_PSY.starts[0] : res_PSY.ends[0] + 1, res_PSY.starts[1] : res_PSY.ends[1] + 1, diff --git a/src/struphy/feec/tests/test_derham.py b/src/struphy/feec/tests/test_derham.py index e5cf181c9..c5c0e57ea 100644 --- a/src/struphy/feec/tests/test_derham.py +++ b/src/struphy/feec/tests/test_derham.py @@ -7,6 +7,7 @@ def test_psydac_derham(Nel, p, spl_kind): """Remark: p=even projectors yield slightly different results, pass with atol=1e-3.""" + import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from psydac.linalg.block import BlockVector from psydac.linalg.stencil import StencilVector @@ -14,7 +15,6 @@ def test_psydac_derham(Nel, p, spl_kind): from struphy.eigenvalue_solvers.spline_space import Spline_space_1d, Tensor_spline_space from struphy.feec.psydac_derham import Derham from struphy.feec.utilities import compare_arrays - from struphy.utils.arrays import xp as np comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -47,11 +47,11 @@ def test_psydac_derham(Nel, p, spl_kind): N3_tot = DR_STR.Ntot_3form # Random vectors for testing - np.random.seed(1981) - x0 = np.random.rand(N0_tot) - x1 = np.random.rand(np.sum(N1_tot)) - x2 = np.random.rand(np.sum(N2_tot)) - x3 = np.random.rand(N3_tot) + xp.random.seed(1981) + x0 = xp.random.rand(N0_tot) + x1 = xp.random.rand(xp.sum(N1_tot)) + x2 = xp.random.rand(xp.sum(N2_tot)) + x3 = xp.random.rand(N3_tot) ############################ ### TEST STENCIL VECTORS ### @@ -174,7 +174,7 @@ def test_psydac_derham(Nel, p, spl_kind): zero2_STR = curl_STR.dot(d1_STR) zero2_PSY = derham.curl.dot(d1_PSY) - assert np.allclose(zero2_STR, np.zeros_like(zero2_STR)) + assert xp.allclose(zero2_STR, xp.zeros_like(zero2_STR)) if rank == 0: print("\nCompare curl of grad:") compare_arrays(zero2_PSY, DR_STR.extract_2(zero2_STR), rank) @@ -183,7 +183,7 @@ def test_psydac_derham(Nel, p, spl_kind): zero3_STR = div_STR.dot(d2_STR) zero3_PSY = derham.div.dot(d2_PSY) - assert np.allclose(zero3_STR, np.zeros_like(zero3_STR)) + assert xp.allclose(zero3_STR, xp.zeros_like(zero3_STR)) if rank == 0: print("\nCompare div of curl:") compare_arrays(zero3_PSY, DR_STR.extract_3(zero3_STR), rank) @@ -201,7 +201,7 @@ def test_psydac_derham(Nel, p, spl_kind): # compare projectors def f(eta1, eta2, eta3): - return np.sin(4 * np.pi * eta1) * np.cos(2 * np.pi * eta2) + np.exp(np.cos(2 * np.pi * eta3)) + return xp.sin(4 * xp.pi * eta1) * xp.cos(2 * xp.pi * eta2) + xp.exp(xp.cos(2 * xp.pi * eta3)) fh0_STR = PI("0", f) fh0_PSY = derham.P["0"](f) diff --git a/src/struphy/feec/tests/test_eval_field.py b/src/struphy/feec/tests/test_eval_field.py index 077b0f158..6148fa41e 100644 --- a/src/struphy/feec/tests/test_eval_field.py +++ b/src/struphy/feec/tests/test_eval_field.py @@ -1,9 +1,8 @@ +import cunumpy as xp import pytest from psydac.ddm.mpi import MockComm from psydac.ddm.mpi import mpi as MPI -from struphy.utils.arrays import xp as np - @pytest.mark.parametrize("Nel", [[8, 9, 10]]) @pytest.mark.parametrize("p", [[3, 2, 4]]) @@ -15,6 +14,7 @@ def test_eval_field(Nel, p, spl_kind): from struphy.feec.psydac_derham import Derham from struphy.feec.utilities import compare_arrays from struphy.geometry.base import Domain + from struphy.initial import perturbations comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -29,84 +29,57 @@ def test_eval_field(Nel, p, spl_kind): n3 = derham.create_spline_function("density", "L2") uv = derham.create_spline_function("velocity", "H1vec") - # initialize fields as forms - comps = { - "pressure": "0", - "e_field": ["1", "1", "1"], - "b_field": ["2", "2", "2"], - "density": "3", - "velocity": ["v", "v", "v"], - } - # initialize with sin/cos perturbations - pert_params_p0 = {"ModesCos": {"given_in_basis": "0", "ls": [0], "ms": [0], "ns": [1], "amps": [5.0]}} - - pert_params_E1 = { - "ModesCos": { - "given_in_basis": ["1", "1", "1"], - "ls": [[0], [0], [0]], - "ms": [[0], [0], [0]], - "ns": [[1], [1], [1]], - "amps": [[5.0], [5.0], [5.0]], - } - } - - pert_params_B2 = { - "ModesCos": { - "given_in_basis": ["2", "2", "2"], - "ls": [[0], [0], [0]], - "ms": [[0], [0], [0]], - "ns": [[1], [1], [1]], - "amps": [[5.0], [5.0], [5.0]], - } - } - - pert_params_n3 = {"ModesCos": {"given_in_basis": "3", "ls": [0], "ms": [0], "ns": [1], "amps": [5.0]}} - - pert_params_uv = { - "ModesCos": { - "given_in_basis": ["v", "v", "v"], - "ls": [[0], [0], [0]], - "ms": [[0], [0], [0]], - "ns": [[1], [1], [1]], - "amps": [[5.0], [5.0], [5.0]], - } - } - - p0.initialize_coeffs(pert_params=pert_params_p0) - E1.initialize_coeffs(pert_params=pert_params_E1) - B2.initialize_coeffs(pert_params=pert_params_B2) - n3.initialize_coeffs(pert_params=pert_params_n3) - uv.initialize_coeffs(pert_params=pert_params_uv) + pert_p0 = perturbations.ModesCos(ls=(0,), ms=(0,), ns=(1,), amps=(5.0,)) + + pert_E1_1 = perturbations.ModesCos(ls=(0,), ms=(0,), ns=(1,), amps=(5.0,), given_in_basis="1", comp=0) + pert_E1_2 = perturbations.ModesCos(ls=(0,), ms=(0,), ns=(1,), amps=(5.0,), given_in_basis="1", comp=1) + pert_E1_3 = perturbations.ModesCos(ls=(0,), ms=(0,), ns=(1,), amps=(5.0,), given_in_basis="1", comp=2) + + pert_B2_1 = perturbations.ModesCos(ls=(0,), ms=(0,), ns=(1,), amps=(5.0,), given_in_basis="2", comp=0) + pert_B2_2 = perturbations.ModesCos(ls=(0,), ms=(0,), ns=(1,), amps=(5.0,), given_in_basis="2", comp=1) + pert_B2_3 = perturbations.ModesCos(ls=(0,), ms=(0,), ns=(1,), amps=(5.0,), given_in_basis="2", comp=2) + + pert_n3 = perturbations.ModesCos(ls=(0,), ms=(0,), ns=(1,), amps=(5.0,)) + + pert_uv_1 = perturbations.ModesCos(ls=(0,), ms=(0,), ns=(1,), amps=(5.0,), given_in_basis="v", comp=0) + pert_uv_2 = perturbations.ModesCos(ls=(0,), ms=(0,), ns=(1,), amps=(5.0,), given_in_basis="v", comp=1) + pert_uv_3 = perturbations.ModesCos(ls=(0,), ms=(0,), ns=(1,), amps=(5.0,), given_in_basis="v", comp=2) + + p0.initialize_coeffs(perturbations=pert_p0) + E1.initialize_coeffs(perturbations=[pert_E1_1, pert_E1_2, pert_E1_3]) + B2.initialize_coeffs(perturbations=[pert_B2_1, pert_B2_2, pert_B2_3]) + n3.initialize_coeffs(perturbations=pert_n3) + uv.initialize_coeffs(perturbations=[pert_uv_1, pert_uv_2, pert_uv_3]) # evaluation points for meshgrid - eta1 = np.linspace(0, 1, 11) - eta2 = np.linspace(0, 1, 14) - eta3 = np.linspace(0, 1, 18) + eta1 = xp.linspace(0, 1, 11) + eta2 = xp.linspace(0, 1, 14) + eta3 = xp.linspace(0, 1, 18) # evaluation points for markers Np = 33 - markers = np.random.rand(Np, 3) - markers_1 = np.zeros((eta1.size, 3)) + markers = xp.random.rand(Np, 3) + markers_1 = xp.zeros((eta1.size, 3)) markers_1[:, 0] = eta1 - markers_2 = np.zeros((eta2.size, 3)) + markers_2 = xp.zeros((eta2.size, 3)) markers_2[:, 1] = eta2 - markers_3 = np.zeros((eta3.size, 3)) + markers_3 = xp.zeros((eta3.size, 3)) markers_3[:, 2] = eta3 # arrays for legacy evaluation arr1, arr2, arr3, is_sparse_meshgrid = Domain.prepare_eval_pts(eta1, eta2, eta3) - tmp = np.zeros_like(arr1) + tmp = xp.zeros_like(arr1) ###### # V0 # ###### # create legacy arrays with same coeffs - coeffs_loc = np.reshape(p0.vector.toarray(), p0.nbasis) + coeffs_loc = xp.reshape(p0.vector.toarray(), p0.nbasis) if isinstance(comm, MockComm): coeffs = coeffs_loc else: - coeffs = np.zeros_like(coeffs_loc) + coeffs = xp.zeros_like(coeffs_loc) comm.Allreduce(coeffs_loc, coeffs, op=MPI.SUM) compare_arrays(p0.vector, coeffs, rank) @@ -128,12 +101,12 @@ def test_eval_field(Nel, p, spl_kind): tmp, 0, ) - val_legacy = np.squeeze(tmp.copy()) + val_legacy = xp.squeeze(tmp.copy()) tmp[:] = 0 # distributed evaluation and comparison val = p0(eta1, eta2, eta3, squeeze_out=True) - assert np.allclose(val, val_legacy) + assert xp.allclose(val, val_legacy) # marker evaluation m_vals = p0(markers) @@ -146,19 +119,19 @@ def test_eval_field(Nel, p, spl_kind): m_vals_ref_2 = p0(0.0, eta2, 0.0, squeeze_out=True) m_vals_ref_3 = p0(0.0, 0.0, eta3, squeeze_out=True) - assert np.allclose(m_vals_1, m_vals_ref_1) - assert np.allclose(m_vals_2, m_vals_ref_2) - assert np.allclose(m_vals_3, m_vals_ref_3) + assert xp.allclose(m_vals_1, m_vals_ref_1) + assert xp.allclose(m_vals_2, m_vals_ref_2) + assert xp.allclose(m_vals_3, m_vals_ref_3) ###### # V1 # ###### # create legacy arrays with same coeffs - coeffs_loc = np.reshape(E1.vector[0].toarray(), E1.nbasis[0]) + coeffs_loc = xp.reshape(E1.vector[0].toarray(), E1.nbasis[0]) if isinstance(comm, MockComm): coeffs = coeffs_loc else: - coeffs = np.zeros_like(coeffs_loc) + coeffs = xp.zeros_like(coeffs_loc) comm.Allreduce(coeffs_loc, coeffs, op=MPI.SUM) compare_arrays(E1.vector[0], coeffs, rank) @@ -180,15 +153,15 @@ def test_eval_field(Nel, p, spl_kind): tmp, 11, ) - val_legacy_1 = np.squeeze(tmp.copy()) + val_legacy_1 = xp.squeeze(tmp.copy()) tmp[:] = 0 # create legacy arrays with same coeffs - coeffs_loc = np.reshape(E1.vector[1].toarray(), E1.nbasis[1]) + coeffs_loc = xp.reshape(E1.vector[1].toarray(), E1.nbasis[1]) if isinstance(comm, MockComm): coeffs = coeffs_loc else: - coeffs = np.zeros_like(coeffs_loc) + coeffs = xp.zeros_like(coeffs_loc) comm.Allreduce(coeffs_loc, coeffs, op=MPI.SUM) compare_arrays(E1.vector[1], coeffs, rank) @@ -210,15 +183,15 @@ def test_eval_field(Nel, p, spl_kind): tmp, 12, ) - val_legacy_2 = np.squeeze(tmp.copy()) + val_legacy_2 = xp.squeeze(tmp.copy()) tmp[:] = 0 # create legacy arrays with same coeffs - coeffs_loc = np.reshape(E1.vector[2].toarray(), E1.nbasis[2]) + coeffs_loc = xp.reshape(E1.vector[2].toarray(), E1.nbasis[2]) if isinstance(comm, MockComm): coeffs = coeffs_loc else: - coeffs = np.zeros_like(coeffs_loc) + coeffs = xp.zeros_like(coeffs_loc) comm.Allreduce(coeffs_loc, coeffs, op=MPI.SUM) compare_arrays(E1.vector[2], coeffs, rank) @@ -240,14 +213,14 @@ def test_eval_field(Nel, p, spl_kind): tmp, 13, ) - val_legacy_3 = np.squeeze(tmp.copy()) + val_legacy_3 = xp.squeeze(tmp.copy()) tmp[:] = 0 # distributed evaluation and comparison val1, val2, val3 = E1(eta1, eta2, eta3, squeeze_out=True) - assert np.allclose(val1, val_legacy_1) - assert np.allclose(val2, val_legacy_2) - assert np.allclose(val3, val_legacy_3) + assert xp.allclose(val1, val_legacy_1) + assert xp.allclose(val2, val_legacy_2) + assert xp.allclose(val3, val_legacy_3) # marker evaluation m_vals = E1(markers) @@ -260,25 +233,25 @@ def test_eval_field(Nel, p, spl_kind): m_vals_ref_2 = E1(0.0, eta2, 0.0, squeeze_out=True) m_vals_ref_3 = E1(0.0, 0.0, eta3, squeeze_out=True) - assert np.all( - [np.allclose(m_vals_1_i, m_vals_ref_1_i) for m_vals_1_i, m_vals_ref_1_i in zip(m_vals_1, m_vals_ref_1)] + assert xp.all( + [xp.allclose(m_vals_1_i, m_vals_ref_1_i) for m_vals_1_i, m_vals_ref_1_i in zip(m_vals_1, m_vals_ref_1)] ) - assert np.all( - [np.allclose(m_vals_2_i, m_vals_ref_2_i) for m_vals_2_i, m_vals_ref_2_i in zip(m_vals_2, m_vals_ref_2)] + assert xp.all( + [xp.allclose(m_vals_2_i, m_vals_ref_2_i) for m_vals_2_i, m_vals_ref_2_i in zip(m_vals_2, m_vals_ref_2)] ) - assert np.all( - [np.allclose(m_vals_3_i, m_vals_ref_3_i) for m_vals_3_i, m_vals_ref_3_i in zip(m_vals_3, m_vals_ref_3)] + assert xp.all( + [xp.allclose(m_vals_3_i, m_vals_ref_3_i) for m_vals_3_i, m_vals_ref_3_i in zip(m_vals_3, m_vals_ref_3)] ) ###### # V2 # ###### # create legacy arrays with same coeffs - coeffs_loc = np.reshape(B2.vector[0].toarray(), B2.nbasis[0]) + coeffs_loc = xp.reshape(B2.vector[0].toarray(), B2.nbasis[0]) if isinstance(comm, MockComm): coeffs = coeffs_loc else: - coeffs = np.zeros_like(coeffs_loc) + coeffs = xp.zeros_like(coeffs_loc) comm.Allreduce(coeffs_loc, coeffs, op=MPI.SUM) compare_arrays(B2.vector[0], coeffs, rank) @@ -300,15 +273,15 @@ def test_eval_field(Nel, p, spl_kind): tmp, 21, ) - val_legacy_1 = np.squeeze(tmp.copy()) + val_legacy_1 = xp.squeeze(tmp.copy()) tmp[:] = 0 # create legacy arrays with same coeffs - coeffs_loc = np.reshape(B2.vector[1].toarray(), B2.nbasis[1]) + coeffs_loc = xp.reshape(B2.vector[1].toarray(), B2.nbasis[1]) if isinstance(comm, MockComm): coeffs = coeffs_loc else: - coeffs = np.zeros_like(coeffs_loc) + coeffs = xp.zeros_like(coeffs_loc) comm.Allreduce(coeffs_loc, coeffs, op=MPI.SUM) compare_arrays(B2.vector[1], coeffs, rank) @@ -330,15 +303,15 @@ def test_eval_field(Nel, p, spl_kind): tmp, 22, ) - val_legacy_2 = np.squeeze(tmp.copy()) + val_legacy_2 = xp.squeeze(tmp.copy()) tmp[:] = 0 # create legacy arrays with same coeffs - coeffs_loc = np.reshape(B2.vector[2].toarray(), B2.nbasis[2]) + coeffs_loc = xp.reshape(B2.vector[2].toarray(), B2.nbasis[2]) if isinstance(comm, MockComm): coeffs = coeffs_loc else: - coeffs = np.zeros_like(coeffs_loc) + coeffs = xp.zeros_like(coeffs_loc) comm.Allreduce(coeffs_loc, coeffs, op=MPI.SUM) compare_arrays(B2.vector[2], coeffs, rank) @@ -360,14 +333,14 @@ def test_eval_field(Nel, p, spl_kind): tmp, 23, ) - val_legacy_3 = np.squeeze(tmp.copy()) + val_legacy_3 = xp.squeeze(tmp.copy()) tmp[:] = 0 # distributed evaluation and comparison val1, val2, val3 = B2(eta1, eta2, eta3, squeeze_out=True) - assert np.allclose(val1, val_legacy_1) - assert np.allclose(val2, val_legacy_2) - assert np.allclose(val3, val_legacy_3) + assert xp.allclose(val1, val_legacy_1) + assert xp.allclose(val2, val_legacy_2) + assert xp.allclose(val3, val_legacy_3) # marker evaluation m_vals = B2(markers) @@ -380,25 +353,25 @@ def test_eval_field(Nel, p, spl_kind): m_vals_ref_2 = B2(0.0, eta2, 0.0, squeeze_out=True) m_vals_ref_3 = B2(0.0, 0.0, eta3, squeeze_out=True) - assert np.all( - [np.allclose(m_vals_1_i, m_vals_ref_1_i) for m_vals_1_i, m_vals_ref_1_i in zip(m_vals_1, m_vals_ref_1)] + assert xp.all( + [xp.allclose(m_vals_1_i, m_vals_ref_1_i) for m_vals_1_i, m_vals_ref_1_i in zip(m_vals_1, m_vals_ref_1)] ) - assert np.all( - [np.allclose(m_vals_2_i, m_vals_ref_2_i) for m_vals_2_i, m_vals_ref_2_i in zip(m_vals_2, m_vals_ref_2)] + assert xp.all( + [xp.allclose(m_vals_2_i, m_vals_ref_2_i) for m_vals_2_i, m_vals_ref_2_i in zip(m_vals_2, m_vals_ref_2)] ) - assert np.all( - [np.allclose(m_vals_3_i, m_vals_ref_3_i) for m_vals_3_i, m_vals_ref_3_i in zip(m_vals_3, m_vals_ref_3)] + assert xp.all( + [xp.allclose(m_vals_3_i, m_vals_ref_3_i) for m_vals_3_i, m_vals_ref_3_i in zip(m_vals_3, m_vals_ref_3)] ) ###### # V3 # ###### # create legacy arrays with same coeffs - coeffs_loc = np.reshape(n3.vector.toarray(), n3.nbasis) + coeffs_loc = xp.reshape(n3.vector.toarray(), n3.nbasis) if isinstance(comm, MockComm): coeffs = coeffs_loc else: - coeffs = np.zeros_like(coeffs_loc) + coeffs = xp.zeros_like(coeffs_loc) comm.Allreduce(coeffs_loc, coeffs, op=MPI.SUM) compare_arrays(n3.vector, coeffs, rank) @@ -420,12 +393,12 @@ def test_eval_field(Nel, p, spl_kind): tmp, 3, ) - val_legacy = np.squeeze(tmp.copy()) + val_legacy = xp.squeeze(tmp.copy()) tmp[:] = 0 # distributed evaluation and comparison val = n3(eta1, eta2, eta3, squeeze_out=True) - assert np.allclose(val, val_legacy) + assert xp.allclose(val, val_legacy) # marker evaluation m_vals = n3(markers) @@ -438,19 +411,19 @@ def test_eval_field(Nel, p, spl_kind): m_vals_ref_2 = n3(0.0, eta2, 0.0, squeeze_out=True) m_vals_ref_3 = n3(0.0, 0.0, eta3, squeeze_out=True) - assert np.allclose(m_vals_1, m_vals_ref_1) - assert np.allclose(m_vals_2, m_vals_ref_2) - assert np.allclose(m_vals_3, m_vals_ref_3) + assert xp.allclose(m_vals_1, m_vals_ref_1) + assert xp.allclose(m_vals_2, m_vals_ref_2) + assert xp.allclose(m_vals_3, m_vals_ref_3) ######### # V0vec # ######### # create legacy arrays with same coeffs - coeffs_loc = np.reshape(uv.vector[0].toarray(), uv.nbasis[0]) + coeffs_loc = xp.reshape(uv.vector[0].toarray(), uv.nbasis[0]) if isinstance(comm, MockComm): coeffs = coeffs_loc else: - coeffs = np.zeros_like(coeffs_loc) + coeffs = xp.zeros_like(coeffs_loc) comm.Allreduce(coeffs_loc, coeffs, op=MPI.SUM) compare_arrays(uv.vector[0], coeffs, rank) @@ -472,15 +445,15 @@ def test_eval_field(Nel, p, spl_kind): tmp, 0, ) - val_legacy_1 = np.squeeze(tmp.copy()) + val_legacy_1 = xp.squeeze(tmp.copy()) tmp[:] = 0 # create legacy arrays with same coeffs - coeffs_loc = np.reshape(uv.vector[1].toarray(), uv.nbasis[1]) + coeffs_loc = xp.reshape(uv.vector[1].toarray(), uv.nbasis[1]) if isinstance(comm, MockComm): coeffs = coeffs_loc else: - coeffs = np.zeros_like(coeffs_loc) + coeffs = xp.zeros_like(coeffs_loc) comm.Allreduce(coeffs_loc, coeffs, op=MPI.SUM) compare_arrays(uv.vector[1], coeffs, rank) @@ -502,15 +475,15 @@ def test_eval_field(Nel, p, spl_kind): tmp, 0, ) - val_legacy_2 = np.squeeze(tmp.copy()) + val_legacy_2 = xp.squeeze(tmp.copy()) tmp[:] = 0 # create legacy arrays with same coeffs - coeffs_loc = np.reshape(uv.vector[2].toarray(), uv.nbasis[2]) + coeffs_loc = xp.reshape(uv.vector[2].toarray(), uv.nbasis[2]) if isinstance(comm, MockComm): coeffs = coeffs_loc else: - coeffs = np.zeros_like(coeffs_loc) + coeffs = xp.zeros_like(coeffs_loc) comm.Allreduce(coeffs_loc, coeffs, op=MPI.SUM) compare_arrays(uv.vector[2], coeffs, rank) @@ -532,14 +505,14 @@ def test_eval_field(Nel, p, spl_kind): tmp, 0, ) - val_legacy_3 = np.squeeze(tmp.copy()) + val_legacy_3 = xp.squeeze(tmp.copy()) tmp[:] = 0 # distributed evaluation and comparison val1, val2, val3 = uv(eta1, eta2, eta3, squeeze_out=True) - assert np.allclose(val1, val_legacy_1) - assert np.allclose(val2, val_legacy_2) - assert np.allclose(val3, val_legacy_3) + assert xp.allclose(val1, val_legacy_1) + assert xp.allclose(val2, val_legacy_2) + assert xp.allclose(val3, val_legacy_3) # marker evaluation m_vals = uv(markers) @@ -552,16 +525,18 @@ def test_eval_field(Nel, p, spl_kind): m_vals_ref_2 = uv(0.0, eta2, 0.0, squeeze_out=True) m_vals_ref_3 = uv(0.0, 0.0, eta3, squeeze_out=True) - assert np.all( - [np.allclose(m_vals_1_i, m_vals_ref_1_i) for m_vals_1_i, m_vals_ref_1_i in zip(m_vals_1, m_vals_ref_1)] + assert xp.all( + [xp.allclose(m_vals_1_i, m_vals_ref_1_i) for m_vals_1_i, m_vals_ref_1_i in zip(m_vals_1, m_vals_ref_1)] ) - assert np.all( - [np.allclose(m_vals_2_i, m_vals_ref_2_i) for m_vals_2_i, m_vals_ref_2_i in zip(m_vals_2, m_vals_ref_2)] + assert xp.all( + [xp.allclose(m_vals_2_i, m_vals_ref_2_i) for m_vals_2_i, m_vals_ref_2_i in zip(m_vals_2, m_vals_ref_2)] ) - assert np.all( - [np.allclose(m_vals_3_i, m_vals_ref_3_i) for m_vals_3_i, m_vals_ref_3_i in zip(m_vals_3, m_vals_ref_3)] + assert xp.all( + [xp.allclose(m_vals_3_i, m_vals_ref_3_i) for m_vals_3_i, m_vals_ref_3_i in zip(m_vals_3, m_vals_ref_3)] ) + print("\nAll assertions passed.") + if __name__ == "__main__": test_eval_field([8, 9, 10], [3, 2, 4], [False, False, True]) diff --git a/src/struphy/feec/tests/test_field_init.py b/src/struphy/feec/tests/test_field_init.py index 9bcab94fd..292edf76a 100644 --- a/src/struphy/feec/tests/test_field_init.py +++ b/src/struphy/feec/tests/test_field_init.py @@ -9,10 +9,11 @@ def test_bckgr_init_const(Nel, p, spl_kind, spaces, vec_comps): """Test field background initialization of "LogicalConst" with multiple fields in params.""" + import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from struphy.feec.psydac_derham import Derham - from struphy.utils.arrays import xp as np + from struphy.io.options import FieldsBackground comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -21,14 +22,14 @@ def test_bckgr_init_const(Nel, p, spl_kind, spaces, vec_comps): derham = Derham(Nel, p, spl_kind, comm=comm) # evaluation grids for comparisons - e1 = np.linspace(0.0, 1.0, Nel[0]) - e2 = np.linspace(0.0, 1.0, Nel[1]) - e3 = np.linspace(0.0, 1.0, Nel[2]) - meshgrids = np.meshgrid(e1, e2, e3, indexing="ij") + e1 = xp.linspace(0.0, 1.0, Nel[0]) + e2 = xp.linspace(0.0, 1.0, Nel[1]) + e3 = xp.linspace(0.0, 1.0, Nel[2]) + meshgrids = xp.meshgrid(e1, e2, e3, indexing="ij") # test values - np.random.seed(1234) - val = np.random.rand() + xp.random.seed(1234) + val = xp.random.rand() if val > 0.5: val = int(val * 10) @@ -36,23 +37,23 @@ def test_bckgr_init_const(Nel, p, spl_kind, spaces, vec_comps): for i, space in enumerate(spaces): field = derham.create_spline_function("name_" + str(i), space) if space in ("H1", "L2"): - bckgr_params = {"LogicalConst": {"values": val}} - field.initialize_coeffs(bckgr_params=bckgr_params) + background = FieldsBackground(type="LogicalConst", values=(val,)) + field.initialize_coeffs(backgrounds=background) print( - f"\n{rank = }, {space = }, after init:\n {np.max(np.abs(field(*meshgrids) - val)) = }", + f"\n{rank = }, {space = }, after init:\n {xp.max(xp.abs(field(*meshgrids) - val)) = }", ) # print(f'{field(*meshgrids) = }') - assert np.allclose(field(*meshgrids), val) + assert xp.allclose(field(*meshgrids), val) else: - bckgr_params = {"LogicalConst": {"values": [val, None, val]}} - field.initialize_coeffs(bckgr_params=bckgr_params) - for j in range(3): - if bckgr_params["LogicalConst"]["values"][j]: + background = FieldsBackground(type="LogicalConst", values=(val, None, val)) + field.initialize_coeffs(backgrounds=background) + for j, val in enumerate(background.values): + if val is not None: print( - f"\n{rank = }, {space = }, after init:\n {j = }, {np.max(np.abs(field(*meshgrids)[j] - val)) = }", + f"\n{rank = }, {space = }, after init:\n {j = }, {xp.max(xp.abs(field(*meshgrids)[j] - val)) = }", ) # print(f'{field(*meshgrids)[i] = }') - assert np.allclose(field(*meshgrids)[j], val) + assert xp.allclose(field(*meshgrids)[j], val) @pytest.mark.parametrize("Nel", [[18, 24, 12]]) @@ -63,14 +64,15 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show import inspect + import cunumpy as xp from matplotlib import pyplot as plt from psydac.ddm.mpi import mpi as MPI from struphy.feec.psydac_derham import Derham from struphy.fields_background import equils - from struphy.fields_background.base import FluidEquilibriumWithB + from struphy.fields_background.base import FluidEquilibrium, FluidEquilibriumWithB from struphy.geometry import domains - from struphy.utils.arrays import xp as np + from struphy.io.options import FieldsBackground comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -79,17 +81,17 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show derham = Derham(Nel, p, spl_kind, comm=comm) # background parameters - bckgr_params_0 = {"MHD": {"variable": "absB0"}} - bckgr_params_1 = {"MHD": {"variable": "u1"}} - bckgr_params_2 = {"MHD": {"variable": "u2"}} - bckgr_params_3 = {"MHD": {"variable": "p3"}} - bckgr_params_4 = {"MHD": {"variable": "uv"}} + bckgr_0 = FieldsBackground(type="FluidEquilibrium", variable="absB0") + bckgr_1 = FieldsBackground(type="FluidEquilibrium", variable="u1") + bckgr_2 = FieldsBackground(type="FluidEquilibrium", variable="u2") + bckgr_3 = FieldsBackground(type="FluidEquilibrium", variable="p3") + bckgr_4 = FieldsBackground(type="FluidEquilibrium", variable="uv") # evaluation grids for comparisons - e1 = np.linspace(0.0, 1.0, Nel[0]) - e2 = np.linspace(0.0, 1.0, Nel[1]) - e3 = np.linspace(0.0, 1.0, Nel[2]) - meshgrids = np.meshgrid(e1, e2, e3, indexing="ij") + e1 = xp.linspace(0.0, 1.0, Nel[0]) + e2 = xp.linspace(0.0, 1.0, Nel[1]) + e3 = xp.linspace(0.0, 1.0, Nel[2]) + meshgrids = xp.meshgrid(e1, e2, e3, indexing="ij") # test for key, val in inspect.getmembers(equils): @@ -104,6 +106,9 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show continue mhd_equil = val() + if not isinstance(mhd_equil, FluidEquilibriumWithB): + continue + print(f"{mhd_equil.params = }") if "AdhocTorus" in key: @@ -127,8 +132,8 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show elif "ShearedSlab" in key: mhd_equil.domain = domains.Cuboid( r1=mhd_equil.params["a"], - r2=mhd_equil.params["a"] * 2 * np.pi, - r3=mhd_equil.params["R0"] * 2 * np.pi, + r2=mhd_equil.params["a"] * 2 * xp.pi, + r3=mhd_equil.params["R0"] * 2 * xp.pi, ) elif "ShearFluid" in key: mhd_equil.domain = domains.Cuboid( @@ -140,7 +145,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show mhd_equil.domain = domains.HollowCylinder( a1=1e-3, a2=mhd_equil.params["a"], - Lz=mhd_equil.params["R0"] * 2 * np.pi, + Lz=mhd_equil.params["R0"] * 2 * xp.pi, ) else: try: @@ -151,93 +156,87 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show field_0 = derham.create_spline_function( "name_0", "H1", - bckgr_params=bckgr_params_0, + backgrounds=bckgr_0, + equil=mhd_equil, ) field_1 = derham.create_spline_function( "name_1", "Hcurl", - bckgr_params=bckgr_params_1, + backgrounds=bckgr_1, + equil=mhd_equil, ) field_2 = derham.create_spline_function( "name_2", "Hdiv", - bckgr_params=bckgr_params_2, + backgrounds=bckgr_2, + equil=mhd_equil, ) field_3 = derham.create_spline_function( "name_3", "L2", - bckgr_params=bckgr_params_3, + backgrounds=bckgr_3, + equil=mhd_equil, ) field_4 = derham.create_spline_function( "name_4", "H1vec", - bckgr_params=bckgr_params_4, + backgrounds=bckgr_4, + equil=mhd_equil, ) - field_1.initialize_coeffs(bckgr_obj=mhd_equil) - print("field_1 initialized.") - field_2.initialize_coeffs(bckgr_obj=mhd_equil) - print("field_2 initialized.") - field_3.initialize_coeffs(bckgr_obj=mhd_equil) - print("field_3 initialized.") - field_4.initialize_coeffs(bckgr_obj=mhd_equil) - print("field_4 initialized.") - # scalar spaces print( - f"{np.max(np.abs(field_3(*meshgrids) - mhd_equil.p3(*meshgrids))) / np.max(np.abs(mhd_equil.p3(*meshgrids)))}" + f"{xp.max(xp.abs(field_3(*meshgrids) - mhd_equil.p3(*meshgrids))) / xp.max(xp.abs(mhd_equil.p3(*meshgrids)))}" ) assert ( - np.max( - np.abs(field_3(*meshgrids) - mhd_equil.p3(*meshgrids)), + xp.max( + xp.abs(field_3(*meshgrids) - mhd_equil.p3(*meshgrids)), ) - / np.max(np.abs(mhd_equil.p3(*meshgrids))) + / xp.max(xp.abs(mhd_equil.p3(*meshgrids))) < 0.54 ) if isinstance(mhd_equil, FluidEquilibriumWithB): - field_0.initialize_coeffs(bckgr_obj=mhd_equil) - print("field_0 initialized.") print( - f"{np.max(np.abs(field_0(*meshgrids) - mhd_equil.absB0(*meshgrids))) / np.max(np.abs(mhd_equil.absB0(*meshgrids)))}" + f"{xp.max(xp.abs(field_0(*meshgrids) - mhd_equil.absB0(*meshgrids))) / xp.max(xp.abs(mhd_equil.absB0(*meshgrids)))}" ) assert ( - np.max( - np.abs(field_0(*meshgrids) - mhd_equil.absB0(*meshgrids)), + xp.max( + xp.abs(field_0(*meshgrids) - mhd_equil.absB0(*meshgrids)), ) - / np.max(np.abs(mhd_equil.absB0(*meshgrids))) + / xp.max(xp.abs(mhd_equil.absB0(*meshgrids))) < 0.057 ) print("Scalar asserts passed.") # vector-valued spaces ref = mhd_equil.u1(*meshgrids) - if np.max(np.abs(ref[0])) < 1e-11: + if xp.max(xp.abs(ref[0])) < 1e-11: denom = 1.0 else: - denom = np.max(np.abs(ref[0])) + denom = xp.max(xp.abs(ref[0])) print( - f"{np.max(np.abs(field_1(*meshgrids)[0] - ref[0])) / denom = }", + f"{xp.max(xp.abs(field_1(*meshgrids)[0] - ref[0])) / denom = }", ) - assert np.max(np.abs(field_1(*meshgrids)[0] - ref[0])) / denom < 0.28 - if np.max(np.abs(ref[1])) < 1e-11: + assert xp.max(xp.abs(field_1(*meshgrids)[0] - ref[0])) / denom < 0.28 + if xp.max(xp.abs(ref[1])) < 1e-11: denom = 1.0 else: - denom = np.max(np.abs(ref[1])) + denom = xp.max(xp.abs(ref[1])) print( - f"{np.max(np.abs(field_1(*meshgrids)[1] - ref[1])) / denom = }", + f"{xp.max(xp.abs(field_1(*meshgrids)[1] - ref[1])) / denom = }", ) - assert np.max(np.abs(field_1(*meshgrids)[1] - ref[1])) / denom < 0.33 - if np.max(np.abs(ref[2])) < 1e-11: + assert xp.max(xp.abs(field_1(*meshgrids)[1] - ref[1])) / denom < 0.33 + if xp.max(xp.abs(ref[2])) < 1e-11: denom = 1.0 else: - denom = np.max(np.abs(ref[2])) + denom = xp.max(xp.abs(ref[2])) print( - f"{np.max(np.abs(field_1(*meshgrids)[2] - ref[2])) / denom = }", + f"{xp.max(xp.abs(field_1(*meshgrids)[2] - ref[2])) / denom = }", ) assert ( - np.max( - np.abs( + xp.max( + xp.abs( field_1(*meshgrids)[2] - ref[2], ), ) @@ -247,75 +246,75 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show print("u1 asserts passed.") ref = mhd_equil.u2(*meshgrids) - if np.max(np.abs(ref[0])) < 1e-11: + if xp.max(xp.abs(ref[0])) < 1e-11: denom = 1.0 else: - denom = np.max(np.abs(ref[0])) + denom = xp.max(xp.abs(ref[0])) print( - f"{np.max(np.abs(field_2(*meshgrids)[0] - ref[0])) / denom = }", + f"{xp.max(xp.abs(field_2(*meshgrids)[0] - ref[0])) / denom = }", ) - assert np.max(np.abs(field_2(*meshgrids)[0] - ref[0])) / denom < 0.86 - if np.max(np.abs(ref[1])) < 1e-11: + assert xp.max(xp.abs(field_2(*meshgrids)[0] - ref[0])) / denom < 0.86 + if xp.max(xp.abs(ref[1])) < 1e-11: denom = 1.0 else: - denom = np.max(np.abs(ref[1])) + denom = xp.max(xp.abs(ref[1])) print( - f"{np.max(np.abs(field_2(*meshgrids)[1] - ref[1])) / denom = }", + f"{xp.max(xp.abs(field_2(*meshgrids)[1] - ref[1])) / denom = }", ) assert ( - np.max( - np.abs( + xp.max( + xp.abs( field_2(*meshgrids)[1] - ref[1], ), ) / denom < 0.4 ) - if np.max(np.abs(ref[2])) < 1e-11: + if xp.max(xp.abs(ref[2])) < 1e-11: denom = 1.0 else: - denom = np.max(np.abs(ref[2])) + denom = xp.max(xp.abs(ref[2])) print( - f"{np.max(np.abs(field_2(*meshgrids)[2] - ref[2])) / denom = }", + f"{xp.max(xp.abs(field_2(*meshgrids)[2] - ref[2])) / denom = }", ) - assert np.max(np.abs(field_2(*meshgrids)[2] - ref[2])) / denom < 0.21 + assert xp.max(xp.abs(field_2(*meshgrids)[2] - ref[2])) / denom < 0.21 print("u2 asserts passed.") ref = mhd_equil.uv(*meshgrids) - if np.max(np.abs(ref[0])) < 1e-11: + if xp.max(xp.abs(ref[0])) < 1e-11: denom = 1.0 else: - denom = np.max(np.abs(ref[0])) + denom = xp.max(xp.abs(ref[0])) print( - f"{np.max(np.abs(field_4(*meshgrids)[0] - ref[0])) / denom = }", + f"{xp.max(xp.abs(field_4(*meshgrids)[0] - ref[0])) / denom = }", ) - assert np.max(np.abs(field_4(*meshgrids)[0] - ref[0])) / denom < 0.6 - if np.max(np.abs(ref[1])) < 1e-11: + assert xp.max(xp.abs(field_4(*meshgrids)[0] - ref[0])) / denom < 0.6 + if xp.max(xp.abs(ref[1])) < 1e-11: denom = 1.0 else: - denom = np.max(np.abs(ref[1])) + denom = xp.max(xp.abs(ref[1])) print( - f"{np.max(np.abs(field_4(*meshgrids)[1] - ref[1])) / denom = }", + f"{xp.max(xp.abs(field_4(*meshgrids)[1] - ref[1])) / denom = }", ) assert ( - np.max( - np.abs( + xp.max( + xp.abs( field_4(*meshgrids)[1] - ref[1], ), ) / denom < 0.2 ) - if np.max(np.abs(ref[2])) < 1e-11: + if xp.max(xp.abs(ref[2])) < 1e-11: denom = 1.0 else: - denom = np.max(np.abs(ref[2])) + denom = xp.max(xp.abs(ref[2])) print( - f"{np.max(np.abs(field_4(*meshgrids)[2] - ref[2])) / denom = }", + f"{xp.max(xp.abs(field_4(*meshgrids)[2] - ref[2])) / denom = }", ) assert ( - np.max( - np.abs( + xp.max( + xp.abs( field_4(*meshgrids)[2] - ref[2], ), ) @@ -356,7 +355,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show absB0_h = mhd_equil.domain.push(field_0, *meshgrids) absB0 = mhd_equil.domain.push(mhd_equil.absB0, *meshgrids) - levels = np.linspace(np.min(absB0) - 1e-10, np.max(absB0), 20) + levels = xp.linspace(xp.min(absB0) - 1e-10, xp.max(absB0), 20) plt.figure(f"0/3-forms top, {mhd_equil = }") plt.subplot(2, 3, 1) @@ -494,7 +493,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show p3_h = mhd_equil.domain.push(field_3, *meshgrids) p3 = mhd_equil.domain.push(mhd_equil.p3, *meshgrids) - levels = np.linspace(np.min(p3) - 1e-10, np.max(p3), 20) + levels = xp.linspace(xp.min(p3) - 1e-10, xp.max(p3), 20) plt.figure(f"0/3-forms top, {mhd_equil = }") plt.subplot(2, 3, 2) @@ -641,7 +640,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show ) for i, (bh, b) in enumerate(zip(b1h, b1)): - levels = np.linspace(np.min(b) - 1e-10, np.max(b), 20) + levels = xp.linspace(xp.min(b) - 1e-10, xp.max(b), 20) plt.figure(f"1-forms top, {mhd_equil = }") plt.subplot(2, 3, 1 + i) @@ -790,7 +789,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show ) for i, (bh, b) in enumerate(zip(b2h, b2)): - levels = np.linspace(np.min(b) - 1e-10, np.max(b), 20) + levels = xp.linspace(xp.min(b) - 1e-10, xp.max(b), 20) plt.figure(f"2-forms top, {mhd_equil = }") plt.subplot(2, 3, 1 + i) @@ -939,7 +938,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show ) for i, (bh, b) in enumerate(zip(bvh, bv)): - levels = np.linspace(np.min(b) - 1e-10, np.max(b), 20) + levels = xp.linspace(xp.min(b) - 1e-10, xp.max(b), 20) plt.figure(f"vector-fields top, {mhd_equil = }") plt.subplot(2, 3, 1 + i) @@ -1084,34 +1083,40 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show def test_sincos_init_const(Nel, p, spl_kind, show_plot=False): """Test field perturbation with ModesSin + ModesCos on top of of "LogicalConst" with multiple fields in params.""" + import cunumpy as xp from matplotlib import pyplot as plt from psydac.ddm.mpi import mpi as MPI from struphy.feec.psydac_derham import Derham from struphy.initial.perturbations import ModesCos, ModesSin - from struphy.utils.arrays import xp as np + from struphy.io.options import FieldsBackground comm = MPI.COMM_WORLD rank = comm.Get_rank() # background parameters - avg_0 = 1.2 - avg_1 = [None, 2.6, 3.7] - avg_2 = [2, 3, 4.2] + avg_0 = (1.2,) + avg_1 = (0.0, 2.6, 3.7) + avg_2 = (2, 3, 4.2) - bckgr_params_0 = {"LogicalConst": {"values": avg_0}} - bckgr_params_1 = {"LogicalConst": {"values": avg_1}} - bckgr_params_2 = {"LogicalConst": {"values": avg_2}} + bckgr_0 = FieldsBackground(type="LogicalConst", values=avg_0) + bckgr_1 = FieldsBackground(type="LogicalConst", values=avg_1) + bckgr_2 = FieldsBackground(type="LogicalConst", values=avg_2) # perturbations ms_s = [0, 2] ns_s = [1, 1] amps = [0.2] - f_sin = ModesSin(ms=ms_s, ns=ns_s, amps=amps) + f_sin_0 = ModesSin(ms=ms_s, ns=ns_s, amps=amps) + f_sin_11 = ModesSin(ms=ms_s, ns=ns_s, amps=amps, given_in_basis="1", comp=0) + f_sin_13 = ModesSin(ms=ms_s, ns=ns_s, amps=amps, given_in_basis="1", comp=2) ms_c = [1] ns_c = [0] - f_cos = ModesCos(ms=ms_c, ns=ns_c, amps=amps) + f_cos_0 = ModesCos(ms=ms_c, ns=ns_c, amps=amps) + f_cos_11 = ModesCos(ms=ms_c, ns=ns_c, amps=amps, given_in_basis="1", comp=0) + f_cos_12 = ModesCos(ms=ms_c, ns=ns_c, amps=amps, given_in_basis="1", comp=1) + f_cos_22 = ModesCos(ms=ms_c, ns=ns_c, amps=amps, given_in_basis="2", comp=1) pert_params_0 = { "ModesSin": { @@ -1155,38 +1160,28 @@ def test_sincos_init_const(Nel, p, spl_kind, show_plot=False): # Psydac discrete Derham sequence and fields derham = Derham(Nel, p, spl_kind, comm=comm) - field_0 = derham.create_spline_function("name_0", "H1") - field_1 = derham.create_spline_function("name_1", "Hcurl") - field_2 = derham.create_spline_function("name_2", "Hdiv") - - field_0.initialize_coeffs(bckgr_params=bckgr_params_0, pert_params=pert_params_0) - field_1.initialize_coeffs(bckgr_params=bckgr_params_1, pert_params=pert_params_1) - field_2.initialize_coeffs(bckgr_params=bckgr_params_2, pert_params=pert_params_2) + field_0 = derham.create_spline_function("name_0", "H1", backgrounds=bckgr_0, perturbations=[f_sin_0, f_cos_0]) + field_1 = derham.create_spline_function( + "name_1", "Hcurl", backgrounds=bckgr_1, perturbations=[f_sin_11, f_sin_13, f_cos_11, f_cos_12] + ) + field_2 = derham.create_spline_function("name_2", "Hdiv", backgrounds=bckgr_2, perturbations=[f_cos_22]) # evaluation grids for comparisons - e1 = np.linspace(0.0, 1.0, Nel[0]) - e2 = np.linspace(0.0, 1.0, Nel[1]) - e3 = np.linspace(0.0, 1.0, Nel[2]) - meshgrids = np.meshgrid(e1, e2, e3, indexing="ij") - - fun_0 = avg_0 + f_sin(*meshgrids) + f_cos(*meshgrids) + e1 = xp.linspace(0.0, 1.0, Nel[0]) + e2 = xp.linspace(0.0, 1.0, Nel[1]) + e3 = xp.linspace(0.0, 1.0, Nel[2]) + meshgrids = xp.meshgrid(e1, e2, e3, indexing="ij") - for i, a in enumerate(avg_1): - if a is None: - avg_1[i] = 0.0 - - for i, a in enumerate(avg_2): - if a is None: - avg_2[i] = 0.0 + fun_0 = avg_0 + f_sin_0(*meshgrids) + f_cos_0(*meshgrids) fun_1 = [ - avg_1[0] + f_sin(*meshgrids) + +f_cos(*meshgrids), - avg_1[1] + f_cos(*meshgrids), - avg_1[2] + f_sin(*meshgrids), + avg_1[0] + f_sin_11(*meshgrids) + f_cos_11(*meshgrids), + avg_1[1] + f_cos_12(*meshgrids), + avg_1[2] + f_sin_13(*meshgrids), ] fun_2 = [ avg_2[0] + 0.0 * meshgrids[0], - avg_2[1] + f_cos(*meshgrids), + avg_2[1] + f_cos_22(*meshgrids), avg_2[2] + 0.0 * meshgrids[0], ] @@ -1194,24 +1189,24 @@ def test_sincos_init_const(Nel, p, spl_kind, show_plot=False): f1_h = field_1(*meshgrids) f2_h = field_2(*meshgrids) - print(f"{np.max(np.abs(fun_0 - f0_h)) = }") - print(f"{np.max(np.abs(fun_1[0] - f1_h[0])) = }") - print(f"{np.max(np.abs(fun_1[1] - f1_h[1])) = }") - print(f"{np.max(np.abs(fun_1[2] - f1_h[2])) = }") - print(f"{np.max(np.abs(fun_2[0] - f2_h[0])) = }") - print(f"{np.max(np.abs(fun_2[1] - f2_h[1])) = }") - print(f"{np.max(np.abs(fun_2[2] - f2_h[2])) = }") - - assert np.max(np.abs(fun_0 - f0_h)) < 3e-5 - assert np.max(np.abs(fun_1[0] - f1_h[0])) < 3e-5 - assert np.max(np.abs(fun_1[1] - f1_h[1])) < 3e-5 - assert np.max(np.abs(fun_1[2] - f1_h[2])) < 3e-5 - assert np.max(np.abs(fun_2[0] - f2_h[0])) < 3e-5 - assert np.max(np.abs(fun_2[1] - f2_h[1])) < 3e-5 - assert np.max(np.abs(fun_2[2] - f2_h[2])) < 3e-5 + print(f"{xp.max(xp.abs(fun_0 - f0_h)) = }") + print(f"{xp.max(xp.abs(fun_1[0] - f1_h[0])) = }") + print(f"{xp.max(xp.abs(fun_1[1] - f1_h[1])) = }") + print(f"{xp.max(xp.abs(fun_1[2] - f1_h[2])) = }") + print(f"{xp.max(xp.abs(fun_2[0] - f2_h[0])) = }") + print(f"{xp.max(xp.abs(fun_2[1] - f2_h[1])) = }") + print(f"{xp.max(xp.abs(fun_2[2] - f2_h[2])) = }") + + assert xp.max(xp.abs(fun_0 - f0_h)) < 3e-5 + assert xp.max(xp.abs(fun_1[0] - f1_h[0])) < 3e-5 + assert xp.max(xp.abs(fun_1[1] - f1_h[1])) < 3e-5 + assert xp.max(xp.abs(fun_1[2] - f1_h[2])) < 3e-5 + assert xp.max(xp.abs(fun_2[0] - f2_h[0])) < 3e-5 + assert xp.max(xp.abs(fun_2[1] - f2_h[1])) < 3e-5 + assert xp.max(xp.abs(fun_2[2] - f2_h[2])) < 3e-5 if show_plot and rank == 0: - levels = np.linspace(np.min(fun_0) - 1e-10, np.max(fun_0), 40) + levels = xp.linspace(xp.min(fun_0) - 1e-10, xp.max(fun_0), 40) plt.figure("0-form", figsize=(10, 16)) plt.subplot(2, 1, 1) @@ -1244,7 +1239,7 @@ def test_sincos_init_const(Nel, p, spl_kind, show_plot=False): plt.figure("1-form", figsize=(30, 16)) for i, (f_h, fun) in enumerate(zip(f1_h, fun_1)): - levels = np.linspace(np.min(fun) - 1e-10, np.max(fun), 40) + levels = xp.linspace(xp.min(fun) - 1e-10, xp.max(fun), 40) plt.subplot(2, 3, 1 + i) plt.contourf( @@ -1276,7 +1271,7 @@ def test_sincos_init_const(Nel, p, spl_kind, show_plot=False): plt.figure("2-form", figsize=(30, 16)) for i, (f_h, fun) in enumerate(zip(f2_h, fun_2)): - levels = np.linspace(np.min(fun) - 1e-10, np.max(fun), 40) + levels = xp.linspace(xp.min(fun) - 1e-10, xp.max(fun), 40) plt.subplot(2, 3, 1 + i) plt.contourf( @@ -1317,11 +1312,12 @@ def test_sincos_init_const(Nel, p, spl_kind, show_plot=False): def test_noise_init(Nel, p, spl_kind, space, direction): """Only tests 1d noise ('e1', 'e2', 'e3') !!""" + import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from struphy.feec.psydac_derham import Derham from struphy.feec.utilities import compare_arrays - from struphy.utils.arrays import xp as np + from struphy.initial.perturbations import Noise comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -1334,16 +1330,10 @@ def test_noise_init(Nel, p, spl_kind, space, direction): field_np = derham_np.create_spline_function("field", space) # initial conditions - pert_params = { - "noise": { - "comps": [True, False, False], - "direction": direction, - "amp": 0.0001, - "seed": 1234, - }, - } - field.initialize_coeffs(pert_params=pert_params) - field_np.initialize_coeffs(pert_params=pert_params) + pert = Noise(direction=direction, amp=0.0001, seed=1234, comp=0) + + field.initialize_coeffs(perturbations=pert) + field_np.initialize_coeffs(perturbations=pert) # print('#'*80) # print(f'npts={field.vector[0].space.npts}, npts_np={field_np.vector[0].space.npts}') @@ -1361,15 +1351,15 @@ def test_noise_init(Nel, p, spl_kind, space, direction): if __name__ == "__main__": # test_bckgr_init_const([8, 10, 12], [1, 2, 3], [False, False, True], [ # 'H1', 'Hcurl', 'Hdiv'], [True, True, False]) - test_bckgr_init_mhd( - [18, 24, 12], - [1, 2, 1], - [ - False, - True, - True, - ], - show_plot=True, - ) - # test_sincos_init_const([1, 32, 32], [1, 3, 3], [True]*3, show_plot=True) - # test_noise_init([4, 8, 6], [1, 1, 1], [True, True, True], "Hcurl", "e1") + # test_bckgr_init_mhd( + # [18, 24, 12], + # [1, 2, 1], + # [ + # False, + # True, + # True, + # ], + # show_plot=False, + # ) + test_sincos_init_const([1, 32, 32], [1, 3, 3], [True] * 3, show_plot=True) + test_noise_init([4, 8, 6], [1, 1, 1], [True, True, True], "Hcurl", "e1") diff --git a/src/struphy/feec/tests/test_l2_projectors.py b/src/struphy/feec/tests/test_l2_projectors.py index 999215c36..6d3695caa 100644 --- a/src/struphy/feec/tests/test_l2_projectors.py +++ b/src/struphy/feec/tests/test_l2_projectors.py @@ -1,5 +1,6 @@ import inspect +import cunumpy as xp import matplotlib.pyplot as plt import pytest from psydac.ddm.mpi import mpi as MPI @@ -8,7 +9,6 @@ from struphy.feec.projectors import L2Projector from struphy.feec.psydac_derham import Derham from struphy.geometry import domains -from struphy.utils.arrays import xp as np @pytest.mark.parametrize("Nel", [[16, 32, 1]]) @@ -28,7 +28,7 @@ def test_l2_projectors_mappings(Nel, p, spl_kind, array_input, with_desc, do_plo derham = Derham(Nel, p, spl_kind, comm=comm) # constant function - f = lambda e1, e2, e3: np.sin(np.pi * e1) * np.cos(2 * np.pi * e2) + f = lambda e1, e2, e3: xp.sin(xp.pi * e1) * xp.cos(2 * xp.pi * e2) # create domain object dom_types = [] @@ -39,11 +39,11 @@ def test_l2_projectors_mappings(Nel, p, spl_kind, array_input, with_desc, do_plo dom_classes += [val] # evaluation points - e1 = np.linspace(0.0, 1.0, 30) - e2 = np.linspace(0.0, 1.0, 40) + e1 = xp.linspace(0.0, 1.0, 30) + e2 = xp.linspace(0.0, 1.0, 40) e3 = 0.0 - ee1, ee2, ee3 = np.meshgrid(e1, e2, e3, indexing="ij") + ee1, ee2, ee3 = xp.meshgrid(e1, e2, e3, indexing="ij") for dom_type, dom_class in zip(dom_types, dom_classes): print("#" * 80) @@ -76,12 +76,12 @@ def test_l2_projectors_mappings(Nel, p, spl_kind, array_input, with_desc, do_plo if array_input: pts_q = derham.quad_grid_pts[sp_key] if sp_id in ("H1", "L2"): - ee = np.meshgrid(*[pt.flatten() for pt in pts_q], indexing="ij") + ee = xp.meshgrid(*[pt.flatten() for pt in pts_q], indexing="ij") f_array = f(*ee) else: f_array = [] for pts in pts_q: - ee = np.meshgrid(*[pt.flatten() for pt in pts], indexing="ij") + ee = xp.meshgrid(*[pt.flatten() for pt in pts], indexing="ij") f_array += [f(*ee)] f_args = f_array else: @@ -91,27 +91,27 @@ def test_l2_projectors_mappings(Nel, p, spl_kind, array_input, with_desc, do_plo veco = P_L2(f_args, out=out) assert veco is out - assert np.all(vec.toarray() == veco.toarray()) + assert xp.all(vec.toarray() == veco.toarray()) field.vector = vec field_vals = field(e1, e2, e3) if sp_id in ("H1", "L2"): - err = np.max(np.abs(f_analytic(ee1, ee2, ee3) - field_vals)) + err = xp.max(xp.abs(f_analytic(ee1, ee2, ee3) - field_vals)) f_plot = field_vals else: - err = [np.max(np.abs(exact(ee1, ee2, ee3) - field_v)) for exact, field_v in zip(f_analytic, field_vals)] + err = [xp.max(xp.abs(exact(ee1, ee2, ee3) - field_v)) for exact, field_v in zip(f_analytic, field_vals)] f_plot = field_vals[0] - print(f"{sp_id = }, {np.max(err) = }") + print(f"{sp_id = }, {xp.max(err) = }") if sp_id in ("H1", "H1vec"): - assert np.max(err) < 0.004 + assert xp.max(err) < 0.004 else: - assert np.max(err) < 0.12 + assert xp.max(err) < 0.12 if do_plot and rank == 0: plt.figure(f"{dom_type}, {sp_id}") - plt.contourf(e1, e2, np.squeeze(f_plot[:, :, 0].T)) + plt.contourf(e1, e2, xp.squeeze(f_plot[:, :, 0].T)) plt.show() @@ -134,7 +134,7 @@ def test_l2_projectors_convergence(direction, pi, spl_kindi, do_plot=False): for n, Neli in enumerate(Nels): # test function def fun(eta): - return np.cos(4 * np.pi * eta) + return xp.cos(4 * xp.pi * eta) # create derham object, test functions and evaluation points e1 = 0.0 @@ -144,7 +144,7 @@ def fun(eta): Nel = [Neli, 1, 1] p = [pi, 1, 1] spl_kind = [spl_kindi, True, True] - e1 = np.linspace(0.0, 1.0, 100) + e1 = xp.linspace(0.0, 1.0, 100) e = e1 c = 0 @@ -154,7 +154,7 @@ def f(x, y, z): Nel = [1, Neli, 1] p = [1, pi, 1] spl_kind = [True, spl_kindi, True] - e2 = np.linspace(0.0, 1.0, 100) + e2 = xp.linspace(0.0, 1.0, 100) e = e2 c = 1 @@ -164,7 +164,7 @@ def f(x, y, z): Nel = [1, 1, Neli] p = [1, 1, pi] spl_kind = [True, True, spl_kindi] - e3 = np.linspace(0.0, 1.0, 100) + e3 = xp.linspace(0.0, 1.0, 100) e = e3 c = 2 @@ -199,19 +199,19 @@ def f(x, y, z): vec = P_L2(f_analytic) veco = P_L2(f_analytic, out=out) assert veco is out - assert np.all(vec.toarray() == veco.toarray()) + assert xp.all(vec.toarray() == veco.toarray()) field.vector = vec field_vals = field(e1, e2, e3, squeeze_out=True) if sp_id in ("H1", "L2"): - err = np.max(np.abs(f_analytic(e1, e2, e3) - field_vals)) + err = xp.max(xp.abs(f_analytic(e1, e2, e3) - field_vals)) f_plot = field_vals else: - err = [np.max(np.abs(exact(e1, e2, e3) - field_v)) for exact, field_v in zip(f_analytic, field_vals)] + err = [xp.max(xp.abs(exact(e1, e2, e3) - field_v)) for exact, field_v in zip(f_analytic, field_vals)] f_plot = field_vals[0] - errors[sp_id] += [np.max(err)] + errors[sp_id] += [xp.max(err)] if do_plot: plt.figure(sp_id + ", L2-proj. convergence") @@ -232,7 +232,7 @@ def f(x, y, z): line_for_rate_p1 = [Ne ** (-rate_p1) * errors[sp_id][0] / Nels[0] ** (-rate_p1) for Ne in Nels] line_for_rate_p0 = [Ne ** (-rate_p0) * errors[sp_id][0] / Nels[0] ** (-rate_p0) for Ne in Nels] - m, _ = np.polyfit(np.log(Nels), np.log(errors[sp_id]), deg=1) + m, _ = xp.polyfit(xp.log(Nels), xp.log(errors[sp_id]), deg=1) print(f"{sp_id = }, fitted convergence rate = {-m}, degree = {pi}") if sp_id in ("H1", "H1vec"): assert -m > (pi + 1 - 0.05) diff --git a/src/struphy/feec/tests/test_local_projectors.py b/src/struphy/feec/tests/test_local_projectors.py index 21e2392a4..ce2abda01 100644 --- a/src/struphy/feec/tests/test_local_projectors.py +++ b/src/struphy/feec/tests/test_local_projectors.py @@ -1,6 +1,7 @@ import inspect import time +import cunumpy as xp import matplotlib.pyplot as plt import pytest from psydac.ddm.mpi import MockComm @@ -12,7 +13,6 @@ from struphy.feec.local_projectors_kernels import fill_matrix_column from struphy.feec.psydac_derham import Derham from struphy.feec.utilities_local_projectors import get_one_spline, get_span_and_basis, get_values_and_indices_splines -from struphy.utils.arrays import xp as np def get_span_and_basis(pts, space): @@ -20,7 +20,7 @@ def get_span_and_basis(pts, space): Parameters ---------- - pts : np.array + pts : xp.array 2d array of points (ii, iq) = (interval, quadrature point). space : SplineSpace @@ -28,10 +28,10 @@ def get_span_and_basis(pts, space): Returns ------- - span : np.array + span : xp.array 2d array indexed by (n, nq), where n is the interval and nq is the quadrature point in the interval. - basis : np.array + basis : xp.array 3d array of values of basis functions indexed by (n, nq, basis function). """ @@ -41,8 +41,8 @@ def get_span_and_basis(pts, space): T = space.knots p = space.degree - span = np.zeros(pts.shape, dtype=int) - basis = np.zeros((*pts.shape, p + 1), dtype=float) + span = xp.zeros(pts.shape, dtype=int) + basis = xp.zeros((*pts.shape, p + 1), dtype=float) for n in range(pts.shape[0]): for nq in range(pts.shape[1]): @@ -79,15 +79,15 @@ def test_local_projectors_compare_global(Nel, p, spl_kind): # constant function def f(e1, e2, e3): - return np.sin(2.0 * np.pi * e1) * np.cos(4.0 * np.pi * e2) * np.sin(6.0 * np.pi * e3) + return xp.sin(2.0 * xp.pi * e1) * xp.cos(4.0 * xp.pi * e2) * xp.sin(6.0 * xp.pi * e3) - # f = lambda e1, e2, e3: np.sin(2.0*np.pi*e1) * np.cos(4.0*np.pi*e2) + # f = lambda e1, e2, e3: xp.sin(2.0*xp.pi*e1) * xp.cos(4.0*xp.pi*e2) # evaluation points - e1 = np.linspace(0.0, 1.0, 10) - e2 = np.linspace(0.0, 1.0, 9) - e3 = np.linspace(0.0, 1.0, 8) + e1 = xp.linspace(0.0, 1.0, 10) + e2 = xp.linspace(0.0, 1.0, 9) + e3 = xp.linspace(0.0, 1.0, 8) - ee1, ee2, ee3 = np.meshgrid(e1, e2, e3, indexing="ij") + ee1, ee2, ee3 = xp.meshgrid(e1, e2, e3, indexing="ij") # loop over spaces for sp_id, sp_key in derham.space_to_form.items(): @@ -126,29 +126,29 @@ def f(e1, e2, e3): fieldg_vals = fieldg(e1, e2, e3) if sp_id in ("H1", "L2"): - err = np.max(np.abs(f_analytic(ee1, ee2, ee3) - field_vals)) + err = xp.max(xp.abs(f_analytic(ee1, ee2, ee3) - field_vals)) # Error comparing the global and local projectors - errg = np.max(np.abs(fieldg_vals - field_vals)) + errg = xp.max(xp.abs(fieldg_vals - field_vals)) else: - err = np.zeros(3) - err[0] = np.max(np.abs(f(ee1, ee2, ee3) - field_vals[0])) - err[1] = np.max(np.abs(f(ee1, ee2, ee3) - field_vals[1])) - err[2] = np.max(np.abs(f(ee1, ee2, ee3) - field_vals[2])) + err = xp.zeros(3) + err[0] = xp.max(xp.abs(f(ee1, ee2, ee3) - field_vals[0])) + err[1] = xp.max(xp.abs(f(ee1, ee2, ee3) - field_vals[1])) + err[2] = xp.max(xp.abs(f(ee1, ee2, ee3) - field_vals[2])) # Error comparing the global and local projectors - errg = np.zeros(3) - errg[0] = np.max(np.abs(fieldg_vals[0] - field_vals[0])) - errg[1] = np.max(np.abs(fieldg_vals[1] - field_vals[1])) - errg[2] = np.max(np.abs(fieldg_vals[2] - field_vals[2])) + errg = xp.zeros(3) + errg[0] = xp.max(xp.abs(fieldg_vals[0] - field_vals[0])) + errg[1] = xp.max(xp.abs(fieldg_vals[1] - field_vals[1])) + errg[2] = xp.max(xp.abs(fieldg_vals[2] - field_vals[2])) - print(f"{sp_id = }, {np.max(err) = }, {np.max(errg) = },{exectime = }") + print(f"{sp_id = }, {xp.max(err) = }, {xp.max(errg) = },{exectime = }") if sp_id in ("H1", "H1vec"): - assert np.max(err) < 0.011 - assert np.max(errg) < 0.011 + assert xp.max(err) < 0.011 + assert xp.max(errg) < 0.011 else: - assert np.max(err) < 0.1 - assert np.max(errg) < 0.1 + assert xp.max(err) < 0.1 + assert xp.max(errg) < 0.1 @pytest.mark.parametrize("direction", [0, 1, 2]) @@ -173,7 +173,7 @@ def test_local_projectors_convergence(direction, pi, spl_kindi, do_plot=False): for n, Neli in enumerate(Nels): # test function def fun(eta): - return np.cos(4 * np.pi * eta) + return xp.cos(4 * xp.pi * eta) # create derham object, test functions and evaluation points e1 = 0.0 @@ -183,7 +183,7 @@ def fun(eta): Nel = [Neli, 1, 1] p = [pi, 1, 1] spl_kind = [spl_kindi, True, True] - e1 = np.linspace(0.0, 1.0, 100) + e1 = xp.linspace(0.0, 1.0, 100) e = e1 c = 0 @@ -193,7 +193,7 @@ def f(x, y, z): Nel = [1, Neli, 1] p = [1, pi, 1] spl_kind = [True, spl_kindi, True] - e2 = np.linspace(0.0, 1.0, 100) + e2 = xp.linspace(0.0, 1.0, 100) e = e2 c = 1 @@ -203,7 +203,7 @@ def f(x, y, z): Nel = [1, 1, Neli] p = [1, 1, pi] spl_kind = [True, True, spl_kindi] - e3 = np.linspace(0.0, 1.0, 100) + e3 = xp.linspace(0.0, 1.0, 100) e = e3 c = 2 @@ -232,13 +232,13 @@ def f(x, y, z): field_vals = field(e1, e2, e3, squeeze_out=True) if sp_id in ("H1", "L2"): - err = np.max(np.abs(f_analytic(e1, e2, e3) - field_vals)) + err = xp.max(xp.abs(f_analytic(e1, e2, e3) - field_vals)) f_plot = field_vals else: - err = [np.max(np.abs(exact(e1, e2, e3) - field_v)) for exact, field_v in zip(f_analytic, field_vals)] + err = [xp.max(xp.abs(exact(e1, e2, e3) - field_v)) for exact, field_v in zip(f_analytic, field_vals)] f_plot = field_vals[0] - errors[sp_id] += [np.max(err)] + errors[sp_id] += [xp.max(err)] if do_plot: plt.figure(sp_id + ", Local-proj. convergence") @@ -257,20 +257,20 @@ def f(x, y, z): line_for_rate_p1 = [Ne ** (-rate_p1) * errors[sp_id][0] / Nels[0] ** (-rate_p1) for Ne in Nels] line_for_rate_p0 = [Ne ** (-rate_p0) * errors[sp_id][0] / Nels[0] ** (-rate_p0) for Ne in Nels] - m, _ = np.polyfit(np.log(Nels), np.log(errors[sp_id]), deg=1) + m, _ = xp.polyfit(xp.log(Nels), xp.log(errors[sp_id]), deg=1) if sp_id in ("H1", "H1vec"): # Sometimes for very large number of elements the convergance rate falls of a bit since the error is already so small floating point impressions become relevant # for those cases is better to compute the convergance rate using only the information of Nel with smaller number if -m <= (pi + 1 - 0.1): - m = -np.log2(errors[sp_id][1] / errors[sp_id][2]) + m = -xp.log2(errors[sp_id][1] / errors[sp_id][2]) print(f"{sp_id = }, fitted convergence rate = {-m}, degree = {pi}") assert -m > (pi + 1 - 0.1) else: # Sometimes for very large number of elements the convergance rate falls of a bit since the error is already so small floating point impressions become relevant # for those cases is better to compute the convergance rate using only the information of Nel with smaller number if -m <= (pi - 0.1): - m = -np.log2(errors[sp_id][1] / errors[sp_id][2]) + m = -xp.log2(errors[sp_id][1] / errors[sp_id][2]) print(f"{sp_id = }, fitted convergence rate = {-m}, degree = {pi}") assert -m > (pi - 0.1) @@ -315,12 +315,12 @@ def aux_test_replication_of_basis(Nel, plist, spl_kind): def make_basis_fun(i): def fun(etas, eta2, eta3): if isinstance(etas, float) or isinstance(etas, int): - etas = np.array([etas]) - out = np.zeros_like(etas) + etas = xp.array([etas]) + out = xp.zeros_like(etas) for j, eta in enumerate(etas): span = find_span(T, p, eta) - inds = np.arange(span - p, span + 1) % N - pos = np.argwhere(inds == i) + inds = xp.arange(span - p, span + 1) % N + pos = xp.argwhere(inds == i) # print(f'{pos = }') if pos.size > 0: pos = pos[0, 0] @@ -335,18 +335,18 @@ def fun(etas, eta2, eta3): fun = make_basis_fun(j) lambdas = P_Loc(fun).toarray() - etas = np.linspace(0.0, 1.0, 100) - fun_h = np.zeros(100) + etas = xp.linspace(0.0, 1.0, 100) + fun_h = xp.zeros(100) for k, eta in enumerate(etas): span = find_span(T, p, eta) - ind1 = np.arange(span - p, span + 1) % N + ind1 = xp.arange(span - p, span + 1) % N basis = basis_funs(T, p, eta, span, normalize=normalize) fun_h[k] = evaluation_kernel_1d(p, basis, ind1, lambdas) - if np.max(np.abs(fun(etas, 0.0, 0.0) - fun_h)) >= 10.0**-10: - print(np.max(np.abs(fun(etas, 0.0, 0.0) - fun_h))) - assert np.max(np.abs(fun(etas, 0.0, 0.0) - fun_h)) < 10.0**-10 - # print(f'{j = }, max error: {np.max(np.abs(fun(etas,0.0,0.0) - fun_h))}') + if xp.max(xp.abs(fun(etas, 0.0, 0.0) - fun_h)) >= 10.0**-10: + print(xp.max(xp.abs(fun(etas, 0.0, 0.0) - fun_h))) + assert xp.max(xp.abs(fun(etas, 0.0, 0.0) - fun_h)) < 10.0**-10 + # print(f'{j = }, max error: {xp.max(xp.abs(fun(etas,0.0,0.0) - fun_h))}') # For D-splines @@ -421,7 +421,7 @@ def test_basis_projection_operator_local(Nel, plist, spl_kind, out_sp_key, in_sp # Helper function to handle reshaping and getting spans and basis def process_eta(eta, w1d): if isinstance(eta, (float, int)): - eta = np.array([eta]) + eta = xp.array([eta]) if len(eta.shape) == 1: eta = eta.reshape((eta.shape[0], 1)) spans, values = get_span_and_basis(eta, w1d) @@ -434,7 +434,7 @@ def fun(eta1, eta2, eta3): eta = eta_map[dim_idx] w1d = W1ds[0][dim_idx] if is_B else V1ds[0][dim_idx] - out = np.zeros_like(eta) + out = xp.zeros_like(eta) for j1 in range(eta.shape[0]): for j2 in range(eta.shape[1]): for j3 in range(eta.shape[2]): @@ -477,21 +477,21 @@ def fun(eta1, eta2, eta3): if out_sp_key == "0" or out_sp_key == "3": npts_out = derham.Vh[out_sp_key].npts - starts = np.array(out.starts, dtype=int) - ends = np.array(out.ends, dtype=int) - pds = np.array(out.pads, dtype=int) + starts = xp.array(out.starts, dtype=int) + ends = xp.array(out.ends, dtype=int) + pds = xp.array(out.pads, dtype=int) VFEM1ds = [VFEM.spaces] - nbasis_out = np.array([VFEM1ds[0][0].nbasis, VFEM1ds[0][1].nbasis, VFEM1ds[0][2].nbasis]) + nbasis_out = xp.array([VFEM1ds[0][0].nbasis, VFEM1ds[0][1].nbasis, VFEM1ds[0][2].nbasis]) else: - npts_out = np.array([sp.npts for sp in P_Loc.coeff_space.spaces]) - pds = np.array([vi.pads for vi in P_Loc.coeff_space.spaces]) - starts = np.array([vi.starts for vi in P_Loc.coeff_space.spaces]) - ends = np.array([vi.ends for vi in P_Loc.coeff_space.spaces]) - starts = np.array(starts, dtype=int) - ends = np.array(ends, dtype=int) - pds = np.array(pds, dtype=int) + npts_out = xp.array([sp.npts for sp in P_Loc.coeff_space.spaces]) + pds = xp.array([vi.pads for vi in P_Loc.coeff_space.spaces]) + starts = xp.array([vi.starts for vi in P_Loc.coeff_space.spaces]) + ends = xp.array([vi.ends for vi in P_Loc.coeff_space.spaces]) + starts = xp.array(starts, dtype=int) + ends = xp.array(ends, dtype=int) + pds = xp.array(pds, dtype=int) VFEM1ds = [comp.spaces for comp in VFEM.spaces] - nbasis_out = np.array( + nbasis_out = xp.array( [ [VFEM1ds[0][0].nbasis, VFEM1ds[0][1].nbasis, VFEM1ds[0][2].nbasis], [ @@ -506,7 +506,7 @@ def fun(eta1, eta2, eta3): if in_sp_key == "0" or in_sp_key == "3": npts_in = derham.Vh[in_sp_key].npts else: - npts_in = np.array([sp.npts for sp in derham.Vh_fem[in_sp_key].coeff_space.spaces]) + npts_in = xp.array([sp.npts for sp in derham.Vh_fem[in_sp_key].coeff_space.spaces]) def define_basis(in_sp_key): def wrapper(dim, index, h=None): @@ -556,13 +556,13 @@ def basis3(i3, h=None): input[random_i0, random_i1, random_i2] = 1.0 input.update_ghost_regions() else: - npts_in = np.array([sp.npts for sp in derham.Vh_fem[in_sp_key].coeff_space.spaces]) + npts_in = xp.array([sp.npts for sp in derham.Vh_fem[in_sp_key].coeff_space.spaces]) random_h = random.randrange(0, 3) random_i0 = random.randrange(0, npts_in[random_h][0]) random_i1 = random.randrange(0, npts_in[random_h][1]) random_i2 = random.randrange(0, npts_in[random_h][2]) - starts_in = np.array([sp.starts for sp in derham.Vh_fem[in_sp_key].coeff_space.spaces]) - ends_in = np.array([sp.ends for sp in derham.Vh_fem[in_sp_key].coeff_space.spaces]) + starts_in = xp.array([sp.starts for sp in derham.Vh_fem[in_sp_key].coeff_space.spaces]) + ends_in = xp.array([sp.ends for sp in derham.Vh_fem[in_sp_key].coeff_space.spaces]) if starts_in[random_h][0] <= random_i0 and random_i0 <= ends_in[random_h][0]: input[random_h][random_i0, random_i1, random_i2] = 1.0 input.update_ghost_regions() @@ -570,9 +570,9 @@ def basis3(i3, h=None): # We define the matrix if out_sp_key == "0" or out_sp_key == "3": if in_sp_key == "0" or in_sp_key == "3": - matrix = np.zeros((npts_out[0] * npts_out[1] * npts_out[2], npts_in[0] * npts_in[1] * npts_in[2])) + matrix = xp.zeros((npts_out[0] * npts_out[1] * npts_out[2], npts_in[0] * npts_in[1] * npts_in[2])) else: - matrix = np.zeros( + matrix = xp.zeros( ( npts_out[0] * npts_out[1] * npts_out[2], npts_in[0][0] * npts_in[0][1] * npts_in[0][2] @@ -583,61 +583,61 @@ def basis3(i3, h=None): else: if in_sp_key == "0" or in_sp_key == "3": - matrix0 = np.zeros((npts_out[0][0] * npts_out[0][1] * npts_out[0][2], npts_in[0] * npts_in[1] * npts_in[2])) - matrix1 = np.zeros((npts_out[1][0] * npts_out[1][1] * npts_out[1][2], npts_in[0] * npts_in[1] * npts_in[2])) - matrix2 = np.zeros((npts_out[2][0] * npts_out[2][1] * npts_out[2][2], npts_in[0] * npts_in[1] * npts_in[2])) + matrix0 = xp.zeros((npts_out[0][0] * npts_out[0][1] * npts_out[0][2], npts_in[0] * npts_in[1] * npts_in[2])) + matrix1 = xp.zeros((npts_out[1][0] * npts_out[1][1] * npts_out[1][2], npts_in[0] * npts_in[1] * npts_in[2])) + matrix2 = xp.zeros((npts_out[2][0] * npts_out[2][1] * npts_out[2][2], npts_in[0] * npts_in[1] * npts_in[2])) else: - matrix00 = np.zeros( + matrix00 = xp.zeros( ( npts_out[0][0] * npts_out[0][1] * npts_out[0][2], npts_in[0][0] * npts_in[0][1] * npts_in[0][2], ) ) - matrix10 = np.zeros( + matrix10 = xp.zeros( ( npts_out[1][0] * npts_out[1][1] * npts_out[1][2], npts_in[0][0] * npts_in[0][1] * npts_in[0][2], ) ) - matrix20 = np.zeros( + matrix20 = xp.zeros( ( npts_out[2][0] * npts_out[2][1] * npts_out[2][2], npts_in[0][0] * npts_in[0][1] * npts_in[0][2], ) ) - matrix01 = np.zeros( + matrix01 = xp.zeros( ( npts_out[0][0] * npts_out[0][1] * npts_out[0][2], npts_in[1][0] * npts_in[1][1] * npts_in[1][2], ) ) - matrix11 = np.zeros( + matrix11 = xp.zeros( ( npts_out[1][0] * npts_out[1][1] * npts_out[1][2], npts_in[1][0] * npts_in[1][1] * npts_in[1][2], ) ) - matrix21 = np.zeros( + matrix21 = xp.zeros( ( npts_out[2][0] * npts_out[2][1] * npts_out[2][2], npts_in[1][0] * npts_in[1][1] * npts_in[1][2], ) ) - matrix02 = np.zeros( + matrix02 = xp.zeros( ( npts_out[0][0] * npts_out[0][1] * npts_out[0][2], npts_in[2][0] * npts_in[2][1] * npts_in[2][2], ) ) - matrix12 = np.zeros( + matrix12 = xp.zeros( ( npts_out[1][0] * npts_out[1][1] * npts_out[1][2], npts_in[2][0] * npts_in[2][1] * npts_in[2][2], ) ) - matrix22 = np.zeros( + matrix22 = xp.zeros( ( npts_out[2][0] * npts_out[2][1] * npts_out[2][2], npts_in[2][0] * npts_in[2][1] * npts_in[2][2], @@ -647,7 +647,7 @@ def basis3(i3, h=None): # We build the BasisProjectionOperator by hand if out_sp_key == "0" or out_sp_key == "3": if in_sp_key == "0" or in_sp_key == "3": - # def f_analytic(e1,e2,e3): return (np.sin(2.0*np.pi*e1)+np.cos(4.0*np.pi*e2))*basis1(random_i0)(e1,e2,e3)*basis2(random_i1)(e1,e2,e3)*basis3(random_i2)(e1,e2,e3) + # def f_analytic(e1,e2,e3): return (xp.sin(2.0*xp.pi*e1)+xp.cos(4.0*xp.pi*e2))*basis1(random_i0)(e1,e2,e3)*basis2(random_i1)(e1,e2,e3)*basis3(random_i2)(e1,e2,e3) # out = P_Loc(f_analytic) counter = 0 @@ -657,7 +657,7 @@ def basis3(i3, h=None): def f_analytic(e1, e2, e3): return ( - (np.sin(2.0 * np.pi * e1) + np.cos(4.0 * np.pi * e2)) + (xp.sin(2.0 * xp.pi * e1) + xp.cos(4.0 * xp.pi * e2)) * basis1(col0)(e1, e2, e3) * basis2(col1)(e1, e2, e3) * basis3(col2)(e1, e2, e3) @@ -677,7 +677,7 @@ def f_analytic(e1, e2, e3): def f_analytic(e1, e2, e3): return ( - (np.sin(2.0 * np.pi * e1) + np.cos(4.0 * np.pi * e2)) + (xp.sin(2.0 * xp.pi * e1) + xp.cos(4.0 * xp.pi * e2)) * basis1(col0, h)(e1, e2, e3) * basis2(col1, h)(e1, e2, e3) * basis3(col2, h)(e1, e2, e3) @@ -696,7 +696,7 @@ def f_analytic(e1, e2, e3): def f_analytic1(e1, e2, e3): return ( - (np.sin(2.0 * np.pi * e1) + np.cos(4.0 * np.pi * e2)) + (xp.sin(2.0 * xp.pi * e1) + xp.cos(4.0 * xp.pi * e2)) * basis1(col0)(e1, e2, e3) * basis2(col1)(e1, e2, e3) * basis3(col2)(e1, e2, e3) @@ -704,7 +704,7 @@ def f_analytic1(e1, e2, e3): def f_analytic2(e1, e2, e3): return ( - (np.cos(2.0 * np.pi * e2) + np.cos(6.0 * np.pi * e3)) + (xp.cos(2.0 * xp.pi * e2) + xp.cos(6.0 * xp.pi * e3)) * basis1(col0)(e1, e2, e3) * basis2(col1)(e1, e2, e3) * basis3(col2)(e1, e2, e3) @@ -712,7 +712,7 @@ def f_analytic2(e1, e2, e3): def f_analytic3(e1, e2, e3): return ( - (np.sin(6.0 * np.pi * e1) + np.sin(4.0 * np.pi * e3)) + (xp.sin(6.0 * xp.pi * e1) + xp.sin(4.0 * xp.pi * e3)) * basis1(col0)(e1, e2, e3) * basis2(col1)(e1, e2, e3) * basis3(col2)(e1, e2, e3) @@ -724,7 +724,7 @@ def f_analytic3(e1, e2, e3): fill_matrix_column(starts[2], ends[2], pds[2], counter, nbasis_out[2], matrix2, out[2]._data) counter += 1 - matrix = np.vstack((matrix0, matrix1, matrix2)) + matrix = xp.vstack((matrix0, matrix1, matrix2)) else: for h in range(3): @@ -736,7 +736,7 @@ def f_analytic3(e1, e2, e3): def f_analytic0(e1, e2, e3): return ( - (np.sin(2.0 * np.pi * e1) + np.cos(4.0 * np.pi * e2)) + (xp.sin(2.0 * xp.pi * e1) + xp.cos(4.0 * xp.pi * e2)) * basis1(col0, h)(e1, e2, e3) * basis2(col1, h)(e1, e2, e3) * basis3(col2, h)(e1, e2, e3) @@ -744,7 +744,7 @@ def f_analytic0(e1, e2, e3): def f_analytic1(e1, e2, e3): return ( - (np.sin(10.0 * np.pi * e1) + np.cos(41.0 * np.pi * e2)) + (xp.sin(10.0 * xp.pi * e1) + xp.cos(41.0 * xp.pi * e2)) * basis1(col0, h)(e1, e2, e3) * basis2(col1, h)(e1, e2, e3) * basis3(col2, h)(e1, e2, e3) @@ -752,7 +752,7 @@ def f_analytic1(e1, e2, e3): def f_analytic2(e1, e2, e3): return ( - (np.sin(25.0 * np.pi * e1) + np.cos(49.0 * np.pi * e2)) + (xp.sin(25.0 * xp.pi * e1) + xp.cos(49.0 * xp.pi * e2)) * basis1(col0, h)(e1, e2, e3) * basis2(col1, h)(e1, e2, e3) * basis3(col2, h)(e1, e2, e3) @@ -762,7 +762,7 @@ def f_analytic2(e1, e2, e3): def f_analytic0(e1, e2, e3): return ( - (np.cos(2.0 * np.pi * e2) + np.cos(6.0 * np.pi * e3)) + (xp.cos(2.0 * xp.pi * e2) + xp.cos(6.0 * xp.pi * e3)) * basis1(col0, h)(e1, e2, e3) * basis2(col1, h)(e1, e2, e3) * basis3(col2, h)(e1, e2, e3) @@ -770,7 +770,7 @@ def f_analytic0(e1, e2, e3): def f_analytic1(e1, e2, e3): return ( - (np.cos(12.0 * np.pi * e2) + np.cos(62.0 * np.pi * e3)) + (xp.cos(12.0 * xp.pi * e2) + xp.cos(62.0 * xp.pi * e3)) * basis1(col0, h)(e1, e2, e3) * basis2(col1, h)(e1, e2, e3) * basis3(col2, h)(e1, e2, e3) @@ -778,7 +778,7 @@ def f_analytic1(e1, e2, e3): def f_analytic2(e1, e2, e3): return ( - (np.cos(25.0 * np.pi * e2) + np.cos(68.0 * np.pi * e3)) + (xp.cos(25.0 * xp.pi * e2) + xp.cos(68.0 * xp.pi * e3)) * basis1(col0, h)(e1, e2, e3) * basis2(col1, h)(e1, e2, e3) * basis3(col2, h)(e1, e2, e3) @@ -787,7 +787,7 @@ def f_analytic2(e1, e2, e3): def f_analytic0(e1, e2, e3): return ( - (np.sin(6.0 * np.pi * e1) + np.sin(4.0 * np.pi * e3)) + (xp.sin(6.0 * xp.pi * e1) + xp.sin(4.0 * xp.pi * e3)) * basis1(col0, h)(e1, e2, e3) * basis2(col1, h)(e1, e2, e3) * basis3(col2, h)(e1, e2, e3) @@ -795,7 +795,7 @@ def f_analytic0(e1, e2, e3): def f_analytic1(e1, e2, e3): return ( - (np.sin(16.0 * np.pi * e1) + np.sin(43.0 * np.pi * e3)) + (xp.sin(16.0 * xp.pi * e1) + xp.sin(43.0 * xp.pi * e3)) * basis1(col0, h)(e1, e2, e3) * basis2(col1, h)(e1, e2, e3) * basis3(col2, h)(e1, e2, e3) @@ -803,7 +803,7 @@ def f_analytic1(e1, e2, e3): def f_analytic2(e1, e2, e3): return ( - (np.sin(65.0 * np.pi * e1) + np.sin(47.0 * np.pi * e3)) + (xp.sin(65.0 * xp.pi * e1) + xp.sin(47.0 * xp.pi * e3)) * basis1(col0, h)(e1, e2, e3) * basis2(col1, h)(e1, e2, e3) * basis3(col2, h)(e1, e2, e3) @@ -898,23 +898,23 @@ def f_analytic2(e1, e2, e3): ) counter += 1 - matrix0 = np.hstack((matrix00, matrix01, matrix02)) - matrix1 = np.hstack((matrix10, matrix11, matrix12)) - matrix2 = np.hstack((matrix20, matrix21, matrix22)) - matrix = np.vstack((matrix0, matrix1, matrix2)) + matrix0 = xp.hstack((matrix00, matrix01, matrix02)) + matrix1 = xp.hstack((matrix10, matrix11, matrix12)) + matrix2 = xp.hstack((matrix20, matrix21, matrix22)) + matrix = xp.vstack((matrix0, matrix1, matrix2)) # Now we build the same matrix using the BasisProjectionOperatorLocal if out_sp_key == "0" or out_sp_key == "3": if in_sp_key == "0" or in_sp_key == "3": def f_analytic(e1, e2, e3): - return np.sin(2.0 * np.pi * e1) + np.cos(4.0 * np.pi * e2) + return xp.sin(2.0 * xp.pi * e1) + xp.cos(4.0 * xp.pi * e2) matrix_new = BasisProjectionOperatorLocal(P_Loc, derham.Vh_fem[in_sp_key], [[f_analytic]], transposed=False) else: def f_analytic(e1, e2, e3): - return np.sin(2.0 * np.pi * e1) + np.cos(4.0 * np.pi * e2) + return xp.sin(2.0 * xp.pi * e1) + xp.cos(4.0 * xp.pi * e2) matrix_new = BasisProjectionOperatorLocal( P_Loc, @@ -929,13 +929,13 @@ def f_analytic(e1, e2, e3): if in_sp_key == "0" or in_sp_key == "3": def f_analytic1(e1, e2, e3): - return np.sin(2.0 * np.pi * e1) + np.cos(4.0 * np.pi * e2) + return xp.sin(2.0 * xp.pi * e1) + xp.cos(4.0 * xp.pi * e2) def f_analytic2(e1, e2, e3): - return np.cos(2.0 * np.pi * e2) + np.cos(6.0 * np.pi * e3) + return xp.cos(2.0 * xp.pi * e2) + xp.cos(6.0 * xp.pi * e3) def f_analytic3(e1, e2, e3): - return np.sin(6.0 * np.pi * e1) + np.sin(4.0 * np.pi * e3) + return xp.sin(6.0 * xp.pi * e1) + xp.sin(4.0 * xp.pi * e3) matrix_new = BasisProjectionOperatorLocal( P_Loc, @@ -952,31 +952,31 @@ def f_analytic3(e1, e2, e3): else: def f_analytic00(e1, e2, e3): - return np.sin(2.0 * np.pi * e1) + np.cos(4.0 * np.pi * e2) + return xp.sin(2.0 * xp.pi * e1) + xp.cos(4.0 * xp.pi * e2) def f_analytic01(e1, e2, e3): - return np.cos(2.0 * np.pi * e2) + np.cos(6.0 * np.pi * e3) + return xp.cos(2.0 * xp.pi * e2) + xp.cos(6.0 * xp.pi * e3) def f_analytic02(e1, e2, e3): - return np.sin(6.0 * np.pi * e1) + np.sin(4.0 * np.pi * e3) + return xp.sin(6.0 * xp.pi * e1) + xp.sin(4.0 * xp.pi * e3) def f_analytic10(e1, e2, e3): - return np.sin(10.0 * np.pi * e1) + np.cos(41.0 * np.pi * e2) + return xp.sin(10.0 * xp.pi * e1) + xp.cos(41.0 * xp.pi * e2) def f_analytic11(e1, e2, e3): - return np.cos(12.0 * np.pi * e2) + np.cos(62.0 * np.pi * e3) + return xp.cos(12.0 * xp.pi * e2) + xp.cos(62.0 * xp.pi * e3) def f_analytic12(e1, e2, e3): - return np.sin(16.0 * np.pi * e1) + np.sin(43.0 * np.pi * e3) + return xp.sin(16.0 * xp.pi * e1) + xp.sin(43.0 * xp.pi * e3) def f_analytic20(e1, e2, e3): - return np.sin(25.0 * np.pi * e1) + np.cos(49.0 * np.pi * e2) + return xp.sin(25.0 * xp.pi * e1) + xp.cos(49.0 * xp.pi * e2) def f_analytic21(e1, e2, e3): - return np.cos(25.0 * np.pi * e2) + np.cos(68.0 * np.pi * e3) + return xp.cos(25.0 * xp.pi * e2) + xp.cos(68.0 * xp.pi * e3) def f_analytic22(e1, e2, e3): - return np.sin(65.0 * np.pi * e1) + np.sin(47.0 * np.pi * e3) + return xp.sin(65.0 * xp.pi * e1) + xp.sin(47.0 * xp.pi * e3) matrix_new = BasisProjectionOperatorLocal( P_Loc, @@ -993,7 +993,7 @@ def f_analytic22(e1, e2, e3): transposed=False, ) - compare_arrays(matrix_new.dot(v), np.matmul(matrix, varr), rank) + compare_arrays(matrix_new.dot(v), xp.matmul(matrix, varr), rank) print("BasisProjectionOperatorLocal test passed.") @@ -1029,7 +1029,7 @@ def test_basis_projection_operator_local_new(Nel, plist, spl_kind, out_sp_key, i # Helper function to handle reshaping and getting spans and basis def process_eta(eta, w1d): if isinstance(eta, (float, int)): - eta = np.array([eta]) + eta = xp.array([eta]) if len(eta.shape) == 1: eta = eta.reshape((eta.shape[0], 1)) spans, values = get_span_and_basis(eta, w1d) @@ -1042,7 +1042,7 @@ def fun(eta1, eta2, eta3): eta = eta_map[dim_idx] w1d = W1ds[0][dim_idx] if is_B else V1ds[0][dim_idx] - out = np.zeros_like(eta) + out = xp.zeros_like(eta) for j1 in range(eta.shape[0]): for j2 in range(eta.shape[1]): for j3 in range(eta.shape[2]): @@ -1119,22 +1119,22 @@ def basis3(i3, h=None): input[random_i0, random_i1, random_i2] = 1.0 input.update_ghost_regions() else: - npts_in = np.array([sp.npts for sp in derham.Vh_fem[in_sp_key].coeff_space.spaces]) + npts_in = xp.array([sp.npts for sp in derham.Vh_fem[in_sp_key].coeff_space.spaces]) random_h = random.randrange(0, 3) random_i0 = random.randrange(0, npts_in[random_h][0]) random_i1 = random.randrange(0, npts_in[random_h][1]) random_i2 = random.randrange(0, npts_in[random_h][2]) - starts = np.array([sp.starts for sp in derham.Vh_fem[in_sp_key].coeff_space.spaces]) - ends = np.array([sp.ends for sp in derham.Vh_fem[in_sp_key].coeff_space.spaces]) + starts = xp.array([sp.starts for sp in derham.Vh_fem[in_sp_key].coeff_space.spaces]) + ends = xp.array([sp.ends for sp in derham.Vh_fem[in_sp_key].coeff_space.spaces]) if starts[random_h][0] <= random_i0 and random_i0 <= ends[random_h][0]: input[random_h][random_i0, random_i1, random_i2] = 1.0 input.update_ghost_regions() - etas1 = np.linspace(0.0, 1.0, 1000) - etas2 = np.array([0.5]) + etas1 = xp.linspace(0.0, 1.0, 1000) + etas2 = xp.array([0.5]) - etas3 = np.array([0.5]) - meshgrid = np.meshgrid(*[etas1, etas2, etas3], indexing="ij") + etas3 = xp.array([0.5]) + meshgrid = xp.meshgrid(*[etas1, etas2, etas3], indexing="ij") # Now we build the same matrix using the BasisProjectionOperatorLocal and BasisProjectionOperator @@ -1142,7 +1142,7 @@ def basis3(i3, h=None): if in_sp_key == "0" or in_sp_key == "3": def f_analytic(e1, e2, e3): - return np.sin(2.0 * np.pi * e1) + np.sin(4.0 * np.pi * e1) + return xp.sin(2.0 * xp.pi * e1) + xp.sin(4.0 * xp.pi * e1) matrix_new = BasisProjectionOperatorLocal(P_Loc, derham.Vh_fem[in_sp_key], [[f_analytic]], transposed=False) matrix_global = BasisProjectionOperator(P, derham.Vh_fem[in_sp_key], [[f_analytic]], transposed=False) @@ -1156,7 +1156,7 @@ def f_analytic(e1, e2, e3): else: def f_analytic(e1, e2, e3): - return np.sin(2.0 * np.pi * e1) + np.cos(4.0 * np.pi * e1) + return xp.sin(2.0 * xp.pi * e1) + xp.cos(4.0 * xp.pi * e1) matrix_new = BasisProjectionOperatorLocal( P_Loc, @@ -1186,13 +1186,13 @@ def f_analytic(e1, e2, e3): if in_sp_key == "0" or in_sp_key == "3": def f_analytic1(e1, e2, e3): - return np.sin(2.0 * np.pi * e1) + np.cos(4.0 * np.pi * e1) + return xp.sin(2.0 * xp.pi * e1) + xp.cos(4.0 * xp.pi * e1) def f_analytic2(e1, e2, e3): - return np.cos(2.0 * np.pi * e1) + np.cos(6.0 * np.pi * e1) + return xp.cos(2.0 * xp.pi * e1) + xp.cos(6.0 * xp.pi * e1) def f_analytic3(e1, e2, e3): - return np.sin(6.0 * np.pi * e1) + np.sin(4.0 * np.pi * e1) + return xp.sin(6.0 * xp.pi * e1) + xp.sin(4.0 * xp.pi * e1) matrix_new = BasisProjectionOperatorLocal( P_Loc, @@ -1219,7 +1219,7 @@ def f_analytic3(e1, e2, e3): transposed=False, ) - analytic_vals = np.array( + analytic_vals = xp.array( [ f_analytic1(*meshgrid) * basis1(random_i0)(*meshgrid) @@ -1238,31 +1238,31 @@ def f_analytic3(e1, e2, e3): else: def f_analytic00(e1, e2, e3): - return np.sin(2.0 * np.pi * e1) + np.cos(4.0 * np.pi * e1) + return xp.sin(2.0 * xp.pi * e1) + xp.cos(4.0 * xp.pi * e1) def f_analytic01(e1, e2, e3): - return np.cos(2.0 * np.pi * e1) + np.cos(6.0 * np.pi * e1) + return xp.cos(2.0 * xp.pi * e1) + xp.cos(6.0 * xp.pi * e1) def f_analytic02(e1, e2, e3): - return np.sin(6.0 * np.pi * e1) + np.sin(4.0 * np.pi * e1) + return xp.sin(6.0 * xp.pi * e1) + xp.sin(4.0 * xp.pi * e1) def f_analytic10(e1, e2, e3): - return np.sin(3.0 * np.pi * e1) + np.cos(4.0 * np.pi * e1) + return xp.sin(3.0 * xp.pi * e1) + xp.cos(4.0 * xp.pi * e1) def f_analytic11(e1, e2, e3): - return np.cos(2.0 * np.pi * e1) + np.cos(3.0 * np.pi * e1) + return xp.cos(2.0 * xp.pi * e1) + xp.cos(3.0 * xp.pi * e1) def f_analytic12(e1, e2, e3): - return np.sin(5.0 * np.pi * e1) + np.sin(3.0 * np.pi * e1) + return xp.sin(5.0 * xp.pi * e1) + xp.sin(3.0 * xp.pi * e1) def f_analytic20(e1, e2, e3): - return np.sin(5.0 * np.pi * e1) + np.cos(4.0 * np.pi * e1) + return xp.sin(5.0 * xp.pi * e1) + xp.cos(4.0 * xp.pi * e1) def f_analytic21(e1, e2, e3): - return np.cos(5.0 * np.pi * e1) + np.cos(6.0 * np.pi * e1) + return xp.cos(5.0 * xp.pi * e1) + xp.cos(6.0 * xp.pi * e1) def f_analytic22(e1, e2, e3): - return np.sin(5.0 * np.pi * e1) + np.sin(4.0 * np.pi * e1) + return xp.sin(5.0 * xp.pi * e1) + xp.sin(4.0 * xp.pi * e1) matrix_new = BasisProjectionOperatorLocal( P_Loc, @@ -1300,7 +1300,7 @@ def f_analytic22(e1, e2, e3): } # Use the map to get analytic values - analytic_vals = np.array( + analytic_vals = xp.array( [ f_analytic_map[dim][random_h](*meshgrid) * basis1(random_i0, random_h)(*meshgrid) @@ -1330,14 +1330,14 @@ def f_analytic22(e1, e2, e3): fieldglo = derham.create_spline_function("fh", out_sp_id) fieldglo.vector = FE_glo - errorloc = np.abs(fieldloc(*meshgrid) - analytic_vals) - errorglo = np.abs(fieldglo(*meshgrid) - analytic_vals) + errorloc = xp.abs(fieldloc(*meshgrid) - analytic_vals) + errorglo = xp.abs(fieldglo(*meshgrid) - analytic_vals) - meanlocal = np.mean(errorloc) - maxlocal = np.max(errorloc) + meanlocal = xp.mean(errorloc) + maxlocal = xp.max(errorloc) - meanglobal = np.mean(errorglo) - maxglobal = np.max(errorglo) + meanglobal = xp.mean(errorglo) + maxglobal = xp.max(errorglo) if isinstance(comm, MockComm): reducemeanlocal = meanlocal @@ -1424,7 +1424,7 @@ def aux_test_spline_evaluation(Nel, plist, spl_kind): # Helper function to handle reshaping and getting spans and basis def process_eta(eta, w1d): if isinstance(eta, (float, int)): - eta = np.array([eta]) + eta = xp.array([eta]) if len(eta.shape) == 1: eta = eta.reshape((eta.shape[0], 1)) spans, values = get_span_and_basis(eta, w1d) @@ -1437,7 +1437,7 @@ def fun(eta1, eta2, eta3): eta = eta_map[dim_idx] w1d = W1ds[0][dim_idx] if is_B else V1ds[0][dim_idx] - out = np.zeros_like(eta) + out = xp.zeros_like(eta) for j1 in range(eta.shape[0]): for j2 in range(eta.shape[1]): for j3 in range(eta.shape[2]): @@ -1471,10 +1471,10 @@ def fun(eta1, eta2, eta3): fieldD = derham.create_spline_function("fh", "L2") npts_in_D = derham.Vh["3"].npts - etas1 = np.linspace(0.0, 1.0, 20) - etas2 = np.linspace(0.0, 1.0, 20) - etas3 = np.linspace(0.0, 1.0, 20) - meshgrid = np.meshgrid(*[etas1, etas2, etas3], indexing="ij") + etas1 = xp.linspace(0.0, 1.0, 20) + etas2 = xp.linspace(0.0, 1.0, 20) + etas3 = xp.linspace(0.0, 1.0, 20) + meshgrid = xp.meshgrid(*[etas1, etas2, etas3], indexing="ij") maxerrorB = 0.0 @@ -1487,7 +1487,7 @@ def fun(eta1, eta2, eta3): fieldB.vector = inputB def error(e1, e2, e3): - return np.abs( + return xp.abs( fieldB(e1, e2, e3) - ( make_basis_fun(True, 0, col0)(e1, e2, e3) @@ -1496,7 +1496,7 @@ def error(e1, e2, e3): ), ) - auxerror = np.max(error(*meshgrid)) + auxerror = xp.max(error(*meshgrid)) if auxerror > maxerrorB: maxerrorB = auxerror @@ -1515,7 +1515,7 @@ def error(e1, e2, e3): fieldD.vector = inputD def error(e1, e2, e3): - return np.abs( + return xp.abs( fieldD(e1, e2, e3) - ( make_basis_fun(False, 0, col0)(e1, e2, e3) @@ -1524,7 +1524,7 @@ def error(e1, e2, e3): ), ) - auxerror = np.max(error(*meshgrid)) + auxerror = xp.max(error(*meshgrid)) if auxerror > maxerrorD: maxerrorD = auxerror diff --git a/src/struphy/feec/tests/test_lowdim_nel_is_1.py b/src/struphy/feec/tests/test_lowdim_nel_is_1.py index cdc7e0705..fdbe6be3d 100644 --- a/src/struphy/feec/tests/test_lowdim_nel_is_1.py +++ b/src/struphy/feec/tests/test_lowdim_nel_is_1.py @@ -7,13 +7,13 @@ def test_lowdim_derham(Nel, p, spl_kind, do_plot=False): """Test Nel=1 in various directions.""" + import cunumpy as xp from matplotlib import pyplot as plt from psydac.ddm.mpi import mpi as MPI from psydac.linalg.block import BlockVector from psydac.linalg.stencil import StencilVector from struphy.feec.psydac_derham import Derham - from struphy.utils.arrays import xp as np comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -74,17 +74,17 @@ def test_lowdim_derham(Nel, p, spl_kind, do_plot=False): ### TEST COMMUTING PROJECTORS ### ################################# def fun(eta): - return np.cos(2 * np.pi * eta) + return xp.cos(2 * xp.pi * eta) def dfun(eta): - return -2 * np.pi * np.sin(2 * np.pi * eta) + return -2 * xp.pi * xp.sin(2 * xp.pi * eta) # evaluation points and gradient e1 = 0.0 e2 = 0.0 e3 = 0.0 if Nel[0] > 1: - e1 = np.linspace(0.0, 1.0, 100) + e1 = xp.linspace(0.0, 1.0, 100) e = e1 c = 0 @@ -95,12 +95,12 @@ def dfx(x, y, z): return dfun(x) def dfy(x, y, z): - return np.zeros_like(x) + return xp.zeros_like(x) def dfz(x, y, z): - return np.zeros_like(x) + return xp.zeros_like(x) elif Nel[1] > 1: - e2 = np.linspace(0.0, 1.0, 100) + e2 = xp.linspace(0.0, 1.0, 100) e = e2 c = 1 @@ -108,15 +108,15 @@ def f(x, y, z): return fun(y) def dfx(x, y, z): - return np.zeros_like(y) + return xp.zeros_like(y) def dfy(x, y, z): return dfun(y) def dfz(x, y, z): - return np.zeros_like(y) + return xp.zeros_like(y) elif Nel[2] > 1: - e3 = np.linspace(0.0, 1.0, 100) + e3 = xp.linspace(0.0, 1.0, 100) e = e3 c = 2 @@ -124,10 +124,10 @@ def f(x, y, z): return fun(z) def dfx(x, y, z): - return np.zeros_like(z) + return xp.zeros_like(z) def dfy(x, y, z): - return np.zeros_like(z) + return xp.zeros_like(z) def dfz(x, y, z): return dfun(z) @@ -160,22 +160,22 @@ def div_f(x, y, z): field_f0_vals = field_f0(e1, e2, e3, squeeze_out=True) # a) projection error - err_f0 = np.max(np.abs(f(e1, e2, e3) - field_f0_vals)) + err_f0 = xp.max(xp.abs(f(e1, e2, e3) - field_f0_vals)) print(f"\n{err_f0 = }") assert err_f0 < 1e-2 # b) commuting property df0_h = derham.grad.dot(f0_h) - assert np.allclose(df0_h.toarray(), proj_of_grad_f.toarray()) + assert xp.allclose(df0_h.toarray(), proj_of_grad_f.toarray()) # c) derivative error field_df0 = derham.create_spline_function("df0", "Hcurl") field_df0.vector = df0_h field_df0_vals = field_df0(e1, e2, e3, squeeze_out=True) - err_df0 = [np.max(np.abs(exact(e1, e2, e3) - field_v)) for exact, field_v in zip(grad_f, field_df0_vals)] + err_df0 = [xp.max(xp.abs(exact(e1, e2, e3) - field_v)) for exact, field_v in zip(grad_f, field_df0_vals)] print(f"{err_df0 = }") - assert np.max(err_df0) < 0.64 + assert xp.max(err_df0) < 0.64 # d) plotting plt.figure(figsize=(8, 12)) @@ -202,22 +202,22 @@ def div_f(x, y, z): field_f1_vals = field_f1(e1, e2, e3, squeeze_out=True) # a) projection error - err_f1 = [np.max(np.abs(exact(e1, e2, e3) - field_v)) for exact, field_v in zip([f, f, f], field_f1_vals)] + err_f1 = [xp.max(xp.abs(exact(e1, e2, e3) - field_v)) for exact, field_v in zip([f, f, f], field_f1_vals)] print(f"{err_f1 = }") - assert np.max(err_f1) < 0.09 + assert xp.max(err_f1) < 0.09 # b) commuting property df1_h = derham.curl.dot(f1_h) - assert np.allclose(df1_h.toarray(), proj_of_curl_fff.toarray()) + assert xp.allclose(df1_h.toarray(), proj_of_curl_fff.toarray()) # c) derivative error field_df1 = derham.create_spline_function("df1", "Hdiv") field_df1.vector = df1_h field_df1_vals = field_df1(e1, e2, e3, squeeze_out=True) - err_df1 = [np.max(np.abs(exact(e1, e2, e3) - field_v)) for exact, field_v in zip(curl_f, field_df1_vals)] + err_df1 = [xp.max(xp.abs(exact(e1, e2, e3) - field_v)) for exact, field_v in zip(curl_f, field_df1_vals)] print(f"{err_df1 = }") - assert np.max(err_df1) < 0.64 + assert xp.max(err_df1) < 0.64 # d) plotting plt.figure(figsize=(8, 12)) @@ -249,22 +249,22 @@ def div_f(x, y, z): field_f2_vals = field_f2(e1, e2, e3, squeeze_out=True) # a) projection error - err_f2 = [np.max(np.abs(exact(e1, e2, e3) - field_v)) for exact, field_v in zip([f, f, f], field_f2_vals)] + err_f2 = [xp.max(xp.abs(exact(e1, e2, e3) - field_v)) for exact, field_v in zip([f, f, f], field_f2_vals)] print(f"{err_f2 = }") - assert np.max(err_f2) < 0.09 + assert xp.max(err_f2) < 0.09 # b) commuting property df2_h = derham.div.dot(f2_h) - assert np.allclose(df2_h.toarray(), proj_of_div_fff.toarray()) + assert xp.allclose(df2_h.toarray(), proj_of_div_fff.toarray()) # c) derivative error field_df2 = derham.create_spline_function("df2", "L2") field_df2.vector = df2_h field_df2_vals = field_df2(e1, e2, e3, squeeze_out=True) - err_df2 = np.max(np.abs(div_f(e1, e2, e3) - field_df2_vals)) + err_df2 = xp.max(xp.abs(div_f(e1, e2, e3) - field_df2_vals)) print(f"{err_df2 = }") - assert np.max(err_df2) < 0.64 + assert xp.max(err_df2) < 0.64 # d) plotting plt.figure(figsize=(8, 12)) @@ -291,7 +291,7 @@ def div_f(x, y, z): field_f3_vals = field_f3(e1, e2, e3, squeeze_out=True) # a) projection error - err_f3 = np.max(np.abs(f(e1, e2, e3) - field_f3_vals)) + err_f3 = xp.max(xp.abs(f(e1, e2, e3) - field_f3_vals)) print(f"{err_f3 = }") assert err_f3 < 0.09 diff --git a/src/struphy/feec/tests/test_mass_matrices.py b/src/struphy/feec/tests/test_mass_matrices.py index 272a5280b..a57d67cdc 100644 --- a/src/struphy/feec/tests/test_mass_matrices.py +++ b/src/struphy/feec/tests/test_mass_matrices.py @@ -6,12 +6,13 @@ @pytest.mark.parametrize("spl_kind", [[False, True, True], [True, False, True]]) @pytest.mark.parametrize( "dirichlet_bc", - [None, [[False, True], [True, False], [False, False]], [[True, False], [False, True], [False, False]]], + [None, [(False, True), (True, False), (False, False)], [(True, False), (False, True), (False, False)]], ) @pytest.mark.parametrize("mapping", [["Colella", {"Lx": 1.0, "Ly": 6.0, "alpha": 0.1, "Lz": 10.0}]]) def test_mass(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): """Compare Struphy mass matrices to Struphy-legacy mass matrices.""" + import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from struphy.eigenvalue_solvers.mhd_operators import MHDOperators @@ -21,7 +22,6 @@ def test_mass(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): from struphy.feec.utilities import RotationMatrix, compare_arrays, create_equal_random_arrays from struphy.fields_background.equils import ScrewPinch, ShearedSlab from struphy.geometry import domains - from struphy.utils.arrays import xp as np mpi_comm = MPI.COMM_WORLD mpi_rank = mpi_comm.Get_rank() @@ -48,7 +48,7 @@ def test_mass(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): eq_mhd = ShearedSlab( **{ "a": (mapping[1]["r1"] - mapping[1]["l1"]), - "R0": (mapping[1]["r3"] - mapping[1]["l3"]) / (2 * np.pi), + "R0": (mapping[1]["r3"] - mapping[1]["l3"]) / (2 * xp.pi), "B0": 1.0, "q0": 1.05, "q1": 1.8, @@ -63,7 +63,7 @@ def test_mass(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): eq_mhd = ShearedSlab( **{ "a": mapping[1]["Lx"], - "R0": mapping[1]["Lz"] / (2 * np.pi), + "R0": mapping[1]["Lz"] / (2 * xp.pi), "B0": 1.0, "q0": 1.05, "q1": 1.8, @@ -101,10 +101,11 @@ def test_mass(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): if dirichlet_bc is not None: for i, knd in enumerate(spl_kind): if knd: - dirichlet_bc[i] = [False, False] + dirichlet_bc[i] = (False, False) else: - dirichlet_bc = [[False, False]] * 3 + dirichlet_bc = [(False, False)] * 3 + dirichlet_bc = tuple(dirichlet_bc) print(f"{dirichlet_bc = }") # derham object @@ -369,12 +370,13 @@ def test_mass(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): @pytest.mark.parametrize("spl_kind", [[False, True, True], [False, True, False]]) @pytest.mark.parametrize( "dirichlet_bc", - [None, [[False, True], [False, False], [False, True]], [[False, False], [False, False], [True, False]]], + [None, [(False, True), (False, False), (False, True)], [(False, False), (False, False), (True, False)]], ) @pytest.mark.parametrize("mapping", [["IGAPolarCylinder", {"a": 1.0, "Lz": 3.0}]]) def test_mass_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): """Compare Struphy polar mass matrices to Struphy-legacy polar mass matrices.""" + import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from struphy.eigenvalue_solvers.mhd_operators import MHDOperators @@ -385,7 +387,6 @@ def test_mass_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): from struphy.fields_background.equils import ScrewPinch from struphy.geometry import domains from struphy.polar.basic import PolarVector - from struphy.utils.arrays import xp as np mpi_comm = MPI.COMM_WORLD mpi_rank = mpi_comm.Get_rank() @@ -431,9 +432,11 @@ def test_mass_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): if dirichlet_bc is not None: for i, knd in enumerate(spl_kind): if knd: - dirichlet_bc[i] = [False, False] + dirichlet_bc[i] = (False, False) else: - dirichlet_bc = [[False, False]] * 3 + dirichlet_bc = [(False, False)] * 3 + + dirichlet_bc = tuple(dirichlet_bc) # derham object derham = Derham( @@ -496,11 +499,11 @@ def test_mass_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): x2_pol_psy.tp = x2_psy x3_pol_psy.tp = x3_psy - np.random.seed(1607) - x0_pol_psy.pol = [np.random.rand(x0_pol_psy.pol[0].shape[0], x0_pol_psy.pol[0].shape[1])] - x1_pol_psy.pol = [np.random.rand(x1_pol_psy.pol[n].shape[0], x1_pol_psy.pol[n].shape[1]) for n in range(3)] - x2_pol_psy.pol = [np.random.rand(x2_pol_psy.pol[n].shape[0], x2_pol_psy.pol[n].shape[1]) for n in range(3)] - x3_pol_psy.pol = [np.random.rand(x3_pol_psy.pol[0].shape[0], x3_pol_psy.pol[0].shape[1])] + xp.random.seed(1607) + x0_pol_psy.pol = [xp.random.rand(x0_pol_psy.pol[0].shape[0], x0_pol_psy.pol[0].shape[1])] + x1_pol_psy.pol = [xp.random.rand(x1_pol_psy.pol[n].shape[0], x1_pol_psy.pol[n].shape[1]) for n in range(3)] + x2_pol_psy.pol = [xp.random.rand(x2_pol_psy.pol[n].shape[0], x2_pol_psy.pol[n].shape[1]) for n in range(3)] + x3_pol_psy.pol = [xp.random.rand(x3_pol_psy.pol[0].shape[0], x3_pol_psy.pol[0].shape[1])] # apply boundary conditions to old STRUPHY x0_pol_str = x0_pol_psy.toarray(True) @@ -530,12 +533,12 @@ def test_mass_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): rn_pol_psy = mass_mats.M2n.dot(x2_pol_psy, apply_bc=True) rJ_pol_psy = mass_mats.M2J.dot(x2_pol_psy, apply_bc=True) - assert np.allclose(r0_pol_str, r0_pol_psy.toarray(True)) - assert np.allclose(r1_pol_str, r1_pol_psy.toarray(True)) - assert np.allclose(r2_pol_str, r2_pol_psy.toarray(True)) - assert np.allclose(r3_pol_str, r3_pol_psy.toarray(True)) - assert np.allclose(rn_pol_str, rn_pol_psy.toarray(True)) - assert np.allclose(rJ_pol_str, rJ_pol_psy.toarray(True)) + assert xp.allclose(r0_pol_str, r0_pol_psy.toarray(True)) + assert xp.allclose(r1_pol_str, r1_pol_psy.toarray(True)) + assert xp.allclose(r2_pol_str, r2_pol_psy.toarray(True)) + assert xp.allclose(r3_pol_str, r3_pol_psy.toarray(True)) + assert xp.allclose(rn_pol_str, rn_pol_psy.toarray(True)) + assert xp.allclose(rJ_pol_str, rJ_pol_psy.toarray(True)) # perfrom matrix-vector products (without boundary conditions) r0_pol_str = space.M0(x0_pol_str) @@ -548,12 +551,12 @@ def test_mass_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): r2_pol_psy = mass_mats.M2.dot(x2_pol_psy, apply_bc=False) r3_pol_psy = mass_mats.M3.dot(x3_pol_psy, apply_bc=False) - assert np.allclose(r0_pol_str, r0_pol_psy.toarray(True)) - assert np.allclose(r1_pol_str, r1_pol_psy.toarray(True)) - assert np.allclose(r2_pol_str, r2_pol_psy.toarray(True)) - assert np.allclose(r3_pol_str, r3_pol_psy.toarray(True)) - assert np.allclose(rn_pol_str, rn_pol_psy.toarray(True)) - assert np.allclose(rJ_pol_str, rJ_pol_psy.toarray(True)) + assert xp.allclose(r0_pol_str, r0_pol_psy.toarray(True)) + assert xp.allclose(r1_pol_str, r1_pol_psy.toarray(True)) + assert xp.allclose(r2_pol_str, r2_pol_psy.toarray(True)) + assert xp.allclose(r3_pol_str, r3_pol_psy.toarray(True)) + assert xp.allclose(rn_pol_str, rn_pol_psy.toarray(True)) + assert xp.allclose(rJ_pol_str, rJ_pol_psy.toarray(True)) print(f"Rank {mpi_rank} | All tests passed!") @@ -563,7 +566,7 @@ def test_mass_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): @pytest.mark.parametrize("spl_kind", [[False, True, True], [False, True, False]]) @pytest.mark.parametrize( "dirichlet_bc", - [None, [[False, True], [False, False], [False, True]], [[False, False], [False, False], [True, False]]], + [None, [(False, True), (False, False), (False, True)], [(False, False), (False, False), (True, False)]], ) @pytest.mark.parametrize("mapping", [["HollowCylinder", {"a1": 0.1, "a2": 1.0, "Lz": 18.84955592153876}]]) def test_mass_preconditioner(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): @@ -572,6 +575,7 @@ def test_mass_preconditioner(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots import time + import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from psydac.linalg.solvers import inverse @@ -581,7 +585,6 @@ def test_mass_preconditioner(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots from struphy.feec.utilities import create_equal_random_arrays from struphy.fields_background.equils import ScrewPinch, ShearedSlab from struphy.geometry import domains - from struphy.utils.arrays import xp as np mpi_comm = MPI.COMM_WORLD mpi_rank = mpi_comm.Get_rank() @@ -608,7 +611,7 @@ def test_mass_preconditioner(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots eq_mhd = ShearedSlab( **{ "a": (mapping[1]["r1"] - mapping[1]["l1"]), - "R0": (mapping[1]["r3"] - mapping[1]["l3"]) / (2 * np.pi), + "R0": (mapping[1]["r3"] - mapping[1]["l3"]) / (2 * xp.pi), "B0": 1.0, "q0": 1.05, "q1": 1.8, @@ -623,7 +626,7 @@ def test_mass_preconditioner(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots eq_mhd = ShearedSlab( **{ "a": mapping[1]["Lx"], - "R0": mapping[1]["Lz"] / (2 * np.pi), + "R0": mapping[1]["Lz"] / (2 * xp.pi), "B0": 1.0, "q0": 1.05, "q1": 1.8, @@ -661,9 +664,11 @@ def test_mass_preconditioner(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots if dirichlet_bc is not None: for i, knd in enumerate(spl_kind): if knd: - dirichlet_bc[i] = [False, False] + dirichlet_bc[i] = (False, False) else: - dirichlet_bc = [[False, False]] * 3 + dirichlet_bc = [(False, False)] * 3 + + dirichlet_bc = tuple(dirichlet_bc) # derham object derham = Derham(Nel, p, spl_kind, comm=mpi_comm, dirichlet_bc=dirichlet_bc) @@ -746,27 +751,27 @@ def test_mass_preconditioner(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots print("Done") # compare output arrays - assert np.allclose(r0.toarray(), r0_pre.toarray()) - assert np.allclose(r1.toarray(), r1_pre.toarray()) - assert np.allclose(r2.toarray(), r2_pre.toarray()) - assert np.allclose(r3.toarray(), r3_pre.toarray()) - assert np.allclose(rv.toarray(), rv_pre.toarray()) + assert xp.allclose(r0.toarray(), r0_pre.toarray()) + assert xp.allclose(r1.toarray(), r1_pre.toarray()) + assert xp.allclose(r2.toarray(), r2_pre.toarray()) + assert xp.allclose(r3.toarray(), r3_pre.toarray()) + assert xp.allclose(rv.toarray(), rv_pre.toarray()) - assert np.allclose(r1n.toarray(), r1n_pre.toarray()) - assert np.allclose(r2n.toarray(), r2n_pre.toarray()) - assert np.allclose(rvn.toarray(), rvn_pre.toarray()) + assert xp.allclose(r1n.toarray(), r1n_pre.toarray()) + assert xp.allclose(r2n.toarray(), r2n_pre.toarray()) + assert xp.allclose(rvn.toarray(), rvn_pre.toarray()) - assert np.allclose(r1Bninv.toarray(), r1Bninv_pre.toarray()) - assert np.allclose(r1Bninv.toarray(), r1Bninvold_pre.toarray()) - assert np.allclose(r1Bninvold.toarray(), r1Bninv_pre.toarray()) + assert xp.allclose(r1Bninv.toarray(), r1Bninv_pre.toarray()) + assert xp.allclose(r1Bninv.toarray(), r1Bninvold_pre.toarray()) + assert xp.allclose(r1Bninvold.toarray(), r1Bninv_pre.toarray()) # test if preconditioner satisfies PC * M = Identity if mapping[0] == "Cuboid" or mapping[0] == "HollowCylinder": - assert np.allclose(mass_mats.M0.dot(M0pre.solve(x0)).toarray(), derham.boundary_ops["0"].dot(x0).toarray()) - assert np.allclose(mass_mats.M1.dot(M1pre.solve(x1)).toarray(), derham.boundary_ops["1"].dot(x1).toarray()) - assert np.allclose(mass_mats.M2.dot(M2pre.solve(x2)).toarray(), derham.boundary_ops["2"].dot(x2).toarray()) - assert np.allclose(mass_mats.M3.dot(M3pre.solve(x3)).toarray(), derham.boundary_ops["3"].dot(x3).toarray()) - assert np.allclose(mass_mats.Mv.dot(Mvpre.solve(xv)).toarray(), derham.boundary_ops["v"].dot(xv).toarray()) + assert xp.allclose(mass_mats.M0.dot(M0pre.solve(x0)).toarray(), derham.boundary_ops["0"].dot(x0).toarray()) + assert xp.allclose(mass_mats.M1.dot(M1pre.solve(x1)).toarray(), derham.boundary_ops["1"].dot(x1).toarray()) + assert xp.allclose(mass_mats.M2.dot(M2pre.solve(x2)).toarray(), derham.boundary_ops["2"].dot(x2).toarray()) + assert xp.allclose(mass_mats.M3.dot(M3pre.solve(x3)).toarray(), derham.boundary_ops["3"].dot(x3).toarray()) + assert xp.allclose(mass_mats.Mv.dot(Mvpre.solve(xv)).toarray(), derham.boundary_ops["v"].dot(xv).toarray()) # test preconditioner in iterative solver M0inv = inverse(mass_mats.M0, "pcg", pc=M0pre, tol=1e-8, maxiter=1000) @@ -868,7 +873,7 @@ def test_mass_preconditioner(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots @pytest.mark.parametrize("spl_kind", [[False, True, True], [False, True, False]]) @pytest.mark.parametrize( "dirichlet_bc", - [None, [[False, True], [False, False], [False, True]], [[False, False], [False, False], [True, False]]], + [None, [(False, True), (False, False), (False, True)], [(False, False), (False, False), (True, False)]], ) @pytest.mark.parametrize("mapping", [["IGAPolarCylinder", {"a": 1.0, "Lz": 3.0}]]) def test_mass_preconditioner_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): @@ -877,6 +882,7 @@ def test_mass_preconditioner_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show import time + import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from psydac.linalg.solvers import inverse @@ -887,7 +893,6 @@ def test_mass_preconditioner_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show from struphy.fields_background.equils import ScrewPinch from struphy.geometry import domains from struphy.polar.basic import PolarVector - from struphy.utils.arrays import xp as np mpi_comm = MPI.COMM_WORLD mpi_rank = mpi_comm.Get_rank() @@ -933,9 +938,11 @@ def test_mass_preconditioner_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show if dirichlet_bc is not None: for i, knd in enumerate(spl_kind): if knd: - dirichlet_bc[i] = [False, False] + dirichlet_bc[i] = (False, False) else: - dirichlet_bc = [[False, False]] * 3 + dirichlet_bc = [(False, False)] * 3 + + dirichlet_bc = tuple(dirichlet_bc) # derham object derham = Derham( @@ -979,11 +986,11 @@ def test_mass_preconditioner_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show x2_pol.tp = x2 x3_pol.tp = x3 - np.random.seed(1607) - x0_pol.pol = [np.random.rand(x0_pol.pol[0].shape[0], x0_pol.pol[0].shape[1])] - x1_pol.pol = [np.random.rand(x1_pol.pol[n].shape[0], x1_pol.pol[n].shape[1]) for n in range(3)] - x2_pol.pol = [np.random.rand(x2_pol.pol[n].shape[0], x2_pol.pol[n].shape[1]) for n in range(3)] - x3_pol.pol = [np.random.rand(x3_pol.pol[0].shape[0], x3_pol.pol[0].shape[1])] + xp.random.seed(1607) + x0_pol.pol = [xp.random.rand(x0_pol.pol[0].shape[0], x0_pol.pol[0].shape[1])] + x1_pol.pol = [xp.random.rand(x1_pol.pol[n].shape[0], x1_pol.pol[n].shape[1]) for n in range(3)] + x2_pol.pol = [xp.random.rand(x2_pol.pol[n].shape[0], x2_pol.pol[n].shape[1]) for n in range(3)] + x3_pol.pol = [xp.random.rand(x3_pol.pol[0].shape[0], x3_pol.pol[0].shape[1])] # test preconditioner in iterative solver and compare to case without preconditioner M0inv = inverse(mass_mats.M0, "pcg", pc=M0pre, tol=1e-8, maxiter=500) diff --git a/src/struphy/feec/tests/test_toarray_struphy.py b/src/struphy/feec/tests/test_toarray_struphy.py index c1d03249d..0279293ea 100644 --- a/src/struphy/feec/tests/test_toarray_struphy.py +++ b/src/struphy/feec/tests/test_toarray_struphy.py @@ -12,13 +12,13 @@ def test_toarray_struphy(Nel, p, spl_kind, mapping): TODO """ + import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from struphy.feec.mass import WeightedMassOperators from struphy.feec.psydac_derham import Derham from struphy.feec.utilities import compare_arrays, create_equal_random_arrays from struphy.geometry import domains - from struphy.utils.arrays import xp as np comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -70,30 +70,30 @@ def test_toarray_struphy(Nel, p, spl_kind, mapping): v3arr = v3arr[0].flatten() # not in-place - compare_arrays(M0.dot(v0), np.matmul(M0arr, v0arr), rank) - compare_arrays(M1.dot(v1), np.matmul(M1arr, v1arr), rank) - compare_arrays(M2.dot(v2), np.matmul(M2arr, v2arr), rank) - compare_arrays(M3.dot(v3), np.matmul(M3arr, v3arr), rank) + compare_arrays(M0.dot(v0), xp.matmul(M0arr, v0arr), rank) + compare_arrays(M1.dot(v1), xp.matmul(M1arr, v1arr), rank) + compare_arrays(M2.dot(v2), xp.matmul(M2arr, v2arr), rank) + compare_arrays(M3.dot(v3), xp.matmul(M3arr, v3arr), rank) # Now we test the in-place version - IM0 = np.zeros([M0.codomain.dimension, M0.domain.dimension], dtype=M0.dtype) - IM1 = np.zeros([M1.codomain.dimension, M1.domain.dimension], dtype=M1.dtype) - IM2 = np.zeros([M2.codomain.dimension, M2.domain.dimension], dtype=M2.dtype) - IM3 = np.zeros([M3.codomain.dimension, M3.domain.dimension], dtype=M3.dtype) + IM0 = xp.zeros([M0.codomain.dimension, M0.domain.dimension], dtype=M0.dtype) + IM1 = xp.zeros([M1.codomain.dimension, M1.domain.dimension], dtype=M1.dtype) + IM2 = xp.zeros([M2.codomain.dimension, M2.domain.dimension], dtype=M2.dtype) + IM3 = xp.zeros([M3.codomain.dimension, M3.domain.dimension], dtype=M3.dtype) M0.toarray_struphy(out=IM0) M1.toarray_struphy(out=IM1) M2.toarray_struphy(out=IM2) M3.toarray_struphy(out=IM3) - compare_arrays(M0.dot(v0), np.matmul(IM0, v0arr), rank) - compare_arrays(M1.dot(v1), np.matmul(IM1, v1arr), rank) - compare_arrays(M2.dot(v2), np.matmul(IM2, v2arr), rank) - compare_arrays(M3.dot(v3), np.matmul(IM3, v3arr), rank) + compare_arrays(M0.dot(v0), xp.matmul(IM0, v0arr), rank) + compare_arrays(M1.dot(v1), xp.matmul(IM1, v1arr), rank) + compare_arrays(M2.dot(v2), xp.matmul(IM2, v2arr), rank) + compare_arrays(M3.dot(v3), xp.matmul(IM3, v3arr), rank) print("test_toarray_struphy passed!") - # assert np.allclose(out1.toarray(), v1.toarray(), atol=1e-5) + # assert xp.allclose(out1.toarray(), v1.toarray(), atol=1e-5) if __name__ == "__main__": diff --git a/src/struphy/feec/tests/test_tosparse_struphy.py b/src/struphy/feec/tests/test_tosparse_struphy.py index e4dc75de9..020ab9e29 100644 --- a/src/struphy/feec/tests/test_tosparse_struphy.py +++ b/src/struphy/feec/tests/test_tosparse_struphy.py @@ -14,6 +14,7 @@ def test_tosparse_struphy(Nel, p, spl_kind, mapping): TODO """ + import cunumpy as xp from psydac.ddm.mpi import MockComm from psydac.ddm.mpi import mpi as MPI @@ -21,7 +22,6 @@ def test_tosparse_struphy(Nel, p, spl_kind, mapping): from struphy.feec.psydac_derham import Derham from struphy.feec.utilities import create_equal_random_arrays from struphy.geometry import domains - from struphy.utils.arrays import xp as np comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -102,13 +102,13 @@ def test_tosparse_struphy(Nel, p, spl_kind, mapping): comm.Allreduce(v3_local, v3_global, op=MPI.SUM) # not in-place - assert np.allclose(v0_global, M0arr.dot(v0arr)) - assert np.allclose(v1_global, M1arr.dot(v1arr)) - assert np.allclose(v2_global, M2arr.dot(v2arr)) - assert np.allclose(v3_global, M3arr.dot(v3arr)) - assert np.allclose(v0_global, M0arrad.dot(v0arr)) - assert np.allclose(v1_global, M1arrad.dot(v1arr)) - assert np.allclose(v2_global, M2arrad.dot(v2arr)) + assert xp.allclose(v0_global, M0arr.dot(v0arr)) + assert xp.allclose(v1_global, M1arr.dot(v1arr)) + assert xp.allclose(v2_global, M2arr.dot(v2arr)) + assert xp.allclose(v3_global, M3arr.dot(v3arr)) + assert xp.allclose(v0_global, M0arrad.dot(v0arr)) + assert xp.allclose(v1_global, M1arrad.dot(v1arr)) + assert xp.allclose(v2_global, M2arrad.dot(v2arr)) print("test_tosparse_struphy passed!") diff --git a/src/struphy/feec/tests/xx_test_preconds.py b/src/struphy/feec/tests/xx_test_preconds.py index a5e14b8d7..267e0279a 100644 --- a/src/struphy/feec/tests/xx_test_preconds.py +++ b/src/struphy/feec/tests/xx_test_preconds.py @@ -12,6 +12,7 @@ ], ) def test_mass_preconditioner(Nel, p, spl_kind, mapping): + import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from psydac.linalg.block import BlockVector from psydac.linalg.stencil import StencilVector @@ -21,7 +22,6 @@ def test_mass_preconditioner(Nel, p, spl_kind, mapping): from struphy.feec.preconditioner import MassMatrixPreconditioner from struphy.feec.psydac_derham import Derham from struphy.geometry import domains - from struphy.utils.arrays import xp as np MPI_COMM = MPI.COMM_WORLD @@ -40,22 +40,22 @@ def test_mass_preconditioner(Nel, p, spl_kind, mapping): v = [] v += [StencilVector(derham.V0.coeff_space)] - v[-1]._data = np.random.rand(*v[-1]._data.shape) + v[-1]._data = xp.random.rand(*v[-1]._data.shape) v += [BlockVector(derham.V1.coeff_space)] for v1i in v[-1]: - v1i._data = np.random.rand(*v1i._data.shape) + v1i._data = xp.random.rand(*v1i._data.shape) v += [BlockVector(derham.V2.coeff_space)] for v1i in v[-1]: - v1i._data = np.random.rand(*v1i._data.shape) + v1i._data = xp.random.rand(*v1i._data.shape) v += [StencilVector(derham.V3.coeff_space)] - v[-1]._data = np.random.rand(*v[-1]._data.shape) + v[-1]._data = xp.random.rand(*v[-1]._data.shape) v += [BlockVector(derham.V0vec.coeff_space)] for v1i in v[-1]: - v1i._data = np.random.rand(*v1i._data.shape) + v1i._data = xp.random.rand(*v1i._data.shape) # assemble preconditioners M_pre = [] @@ -68,7 +68,7 @@ def test_mass_preconditioner(Nel, p, spl_kind, mapping): n = "v" if domain.kind_map == 10 or domain.kind_map == 11: - assert np.allclose(M._mat.toarray(), M_p.matrix.toarray()) + assert xp.allclose(M._mat.toarray(), M_p.matrix.toarray()) print(f'Matrix assertion for space {n} case "Cuboid/HollowCylinder" passed.') inv_A = InverseLinearOperator(M, pc=M_p, tol=1e-8, maxiter=5000) diff --git a/src/struphy/feec/utilities.py b/src/struphy/feec/utilities.py index 4ceb7842a..aa3912857 100644 --- a/src/struphy/feec/utilities.py +++ b/src/struphy/feec/utilities.py @@ -1,13 +1,14 @@ +import cunumpy as xp from psydac.api.essential_bc import apply_essential_bc_stencil from psydac.fem.tensor import TensorFemSpace from psydac.fem.vector import VectorFemSpace +from psydac.linalg.basic import Vector from psydac.linalg.block import BlockLinearOperator, BlockVector from psydac.linalg.stencil import StencilMatrix, StencilVector import struphy.feec.utilities_kernels as kernels from struphy.feec import banded_to_stencil_kernels as bts from struphy.polar.basic import PolarVector -from struphy.utils.arrays import xp as np class RotationMatrix: @@ -40,7 +41,7 @@ def __init__(self, *vec_fun): def __call__(self, e1, e2, e3): # array from 2d list gives 3x3 array is in the first two indices - tmp = np.array( + tmp = xp.array( [ [self._cross_mask[m][n] * fun(e1, e2, e3) for n, fun in enumerate(row)] for m, row in enumerate(self._funs) @@ -48,7 +49,7 @@ def __call__(self, e1, e2, e3): ) # numpy operates on the last two indices with @ - return np.transpose(tmp, axes=(2, 3, 4, 0, 1)) + return xp.transpose(tmp, axes=(2, 3, 4, 0, 1)) def create_equal_random_arrays(V, seed=123, flattened=False): @@ -76,7 +77,7 @@ def create_equal_random_arrays(V, seed=123, flattened=False): assert isinstance(V, (TensorFemSpace, VectorFemSpace)) - np.random.seed(seed) + xp.random.seed(seed) arr = [] @@ -92,7 +93,7 @@ def create_equal_random_arrays(V, seed=123, flattened=False): dims = V.coeff_space.npts - arr += [np.random.rand(*dims)] + arr += [xp.random.rand(*dims)] s = arr_psy.starts e = arr_psy.ends @@ -110,7 +111,7 @@ def create_equal_random_arrays(V, seed=123, flattened=False): for d, block in enumerate(arr_psy.blocks): dims = V.spaces[d].coeff_space.npts - arr += [np.random.rand(*dims)] + arr += [xp.random.rand(*dims)] s = block.starts e = block.ends @@ -120,7 +121,7 @@ def create_equal_random_arrays(V, seed=123, flattened=False): ] if flattened: - arr = np.concatenate( + arr = xp.concatenate( ( arr[0].flatten(), arr[1].flatten(), @@ -167,11 +168,11 @@ def compare_arrays(arr_psy, arr, rank, atol=1e-14, verbose=False): arr_psy.space.npts[2], )[s[0] : e[0] + 1, s[1] : e[1] + 1, s[2] : e[2] + 1] - assert np.allclose(tmp1, tmp2, atol=atol) + assert xp.allclose(tmp1, tmp2, atol=atol) elif isinstance(arr_psy, BlockVector): if not (isinstance(arr, tuple) or isinstance(arr, list)): - arrs = np.split( + arrs = xp.split( arr, [ arr_psy.blocks[0].shape[0], @@ -196,7 +197,7 @@ def compare_arrays(arr_psy, arr, rank, atol=1e-14, verbose=False): s[2] : e[2] + 1, ] - assert np.allclose(tmp1, tmp2, atol=atol) + assert xp.allclose(tmp1, tmp2, atol=atol) elif isinstance(arr_psy, StencilMatrix): s = arr_psy.codomain.starts @@ -215,7 +216,7 @@ def compare_arrays(arr_psy, arr, rank, atol=1e-14, verbose=False): if tmp_arr.shape == tmp1.shape: tmp2 = tmp_arr else: - tmp2 = np.zeros( + tmp2 = xp.zeros( ( e[0] + 1 - s[0], e[1] + 1 - s[1], @@ -228,7 +229,7 @@ def compare_arrays(arr_psy, arr, rank, atol=1e-14, verbose=False): ) bts.band_to_stencil_3d(tmp_arr, tmp2) - assert np.allclose(tmp1, tmp2, atol=atol) + assert xp.allclose(tmp1, tmp2, atol=atol) elif isinstance(arr_psy, BlockLinearOperator): for row_psy, row in zip(arr_psy.blocks, arr): @@ -259,7 +260,7 @@ def compare_arrays(arr_psy, arr, rank, atol=1e-14, verbose=False): if tmp_mat.shape == tmp1.shape: tmp2 = tmp_mat else: - tmp2 = np.zeros( + tmp2 = xp.zeros( ( e[0] + 1 - s[0], e[1] + 1 - s[1], @@ -272,7 +273,7 @@ def compare_arrays(arr_psy, arr, rank, atol=1e-14, verbose=False): ) bts.band_to_stencil_3d(tmp_mat, tmp2) - assert np.allclose(tmp1, tmp2, atol=atol) + assert xp.allclose(tmp1, tmp2, atol=atol) else: raise AssertionError("Wrong input type.") @@ -283,7 +284,7 @@ def compare_arrays(arr_psy, arr, rank, atol=1e-14, verbose=False): ) -def apply_essential_bc_to_array(space_id, vector, bc): +def apply_essential_bc_to_array(space_id: str, vector: Vector, bc: tuple): """ Sets entries corresponding to boundary B-splines to zero. @@ -292,15 +293,15 @@ def apply_essential_bc_to_array(space_id, vector, bc): space_id : str The name of the continuous functions space the given vector belongs to (H1, Hcurl, Hdiv, L2 or H1vec). - vector : StencilVector | BlockVector + vector : Vector The vector whose boundary values shall be set to zero. - bc : list[list[bool]] + bc : tuple[tuple[bool]] Whether to apply homogeneous Dirichlet boundary conditions (at left or right boundary in each direction). """ assert isinstance(vector, (StencilVector, BlockVector, PolarVector)) - assert isinstance(bc, list) + assert isinstance(bc, tuple) assert len(bc) == 3 if isinstance(vector, PolarVector): diff --git a/src/struphy/feec/utilities_local_projectors.py b/src/struphy/feec/utilities_local_projectors.py index 5aa9a5b61..9ff91a120 100644 --- a/src/struphy/feec/utilities_local_projectors.py +++ b/src/struphy/feec/utilities_local_projectors.py @@ -1,5 +1,6 @@ +import cunumpy as xp + from struphy.feec.local_projectors_kernels import are_quadrature_points_zero, get_rows, select_quasi_points -from struphy.utils.arrays import xp as np def split_points( @@ -32,7 +33,7 @@ def split_points( shifts : 1d int array For each one of the three spatial directions it determines by which amount to shift the position index (pos) in case we have to loop over the evaluation points. - pts : list of np.array + pts : list of xp.array 3D list of 2D array with the quasi-interpolation points (or Gauss-Legendre quadrature points for histopolation). In format (ns, nb, np) = (spatial direction, B-spline index, point) for StencilVector spaces . @@ -49,7 +50,7 @@ def split_points( npts : list of ints Contains the number of B-splines for each one of the three spatial directions. - periodic : 1D bool np.array + periodic : 1D bool xp.array For each one of the three spatial directions contains the information of whether the B-splines are periodic or not. wij: 3d float array @@ -74,18 +75,18 @@ def split_points( """ # We iterate over the three spatial directions for n, pt in enumerate(pts): - original_pts_size[n] = np.shape(pt)[0] + original_pts_size[n] = xp.shape(pt)[0] # We initialize localpts with as many entries as the global pt, but with all entries being -1 # This function will change the values of the needed entries from -1 to the value of the point. if IoH[n] == "I": - localpts = np.full((np.shape(pt)[0]), fill_value=-1, dtype=float) + localpts = xp.full((xp.shape(pt)[0]), fill_value=-1, dtype=float) elif IoH[n] == "H": - localpts = np.full((np.shape(pt)), fill_value=-1, dtype=float) + localpts = xp.full((xp.shape(pt)), fill_value=-1, dtype=float) for i in range(starts[n], ends[n] + 1): startj1, endj1 = select_quasi_points(int(i), int(p[n]), int(npts[n]), bool(periodic[n])) for j1 in range(lenj[n]): - if startj1 + j1 < np.shape(pt)[0]: + if startj1 + j1 < xp.shape(pt)[0]: pos = startj1 + j1 else: pos = int(startj1 + j1 + shift[n]) @@ -97,42 +98,42 @@ def split_points( localpts[pos] = pt[pos] # We get the local points by grabing only the values different from -1. if IoH[n] == "I": - localpos = np.where(localpts != -1)[0] + localpos = xp.where(localpts != -1)[0] elif IoH[n] == "H": - localpos = np.where(localpts[:, 0] != -1)[0] + localpos = xp.where(localpts[:, 0] != -1)[0] localpts = localpts[localpos] - localptsout.append(np.array(localpts)) + localptsout.append(xp.array(localpts)) ## # We build the index_translation array that shall turn global indices into local indices ## - mini_indextranslation = np.full( - (np.shape(pt)[0]), + mini_indextranslation = xp.full( + (xp.shape(pt)[0]), fill_value=-1, dtype=int, ) for i, j in enumerate(localpos): mini_indextranslation[j] = i - index_translation.append(np.array(mini_indextranslation)) + index_translation.append(xp.array(mini_indextranslation)) ## # We build the inv_index_translation that shall turn local indices into global indices ## - inv_mini_indextranslation = np.full( - (np.shape(localptsout[-1])[0]), + inv_mini_indextranslation = xp.full( + (xp.shape(localptsout[-1])[0]), fill_value=-1, dtype=int, ) for i, j in enumerate(localpos): inv_mini_indextranslation[i] = j - inv_index_translation.append(np.array(inv_mini_indextranslation)) + inv_index_translation.append(xp.array(inv_mini_indextranslation)) def get_values_and_indices_splines(Nbasis, degree, periodic, spans, values): - """Given an array with the values of the splines evaluated at certain points this function returns a np.array that tell us the index of each spline. So we can know to which spline each + """Given an array with the values of the splines evaluated at certain points this function returns a xp.array that tell us the index of each spline. So we can know to which spline each value corresponds. It also modifies the evaluation values in the case we have one spline of degree one with periodic boundary conditions, so it is artificially equal to the identity. Parameters @@ -146,31 +147,31 @@ def get_values_and_indices_splines(Nbasis, degree, periodic, spans, values): periodic : bool Whether we have periodic boundary conditions or nor. - span : np.array + span : xp.array 2d array indexed by (n, nq), where n is the interval and nq is the quadrature point in the interval. - values : np.array + values : xp.array 3d array of values of basis functions indexed by (n, nq, basis function). Returns ------- - eval_indeces : np.array + eval_indeces : xp.array 3d array of basis functions indices, indexed by (n, nq, basis function). - values : np.array + values : xp.array 3d array of values of basis functions indexed by (n, nq, basis function). """ # In this case we want this spatial direction to be "neglected", that means we artificially set the values of the B-spline to 1 at all points. So it becomes the multiplicative identity. if Nbasis == 1 and degree == 1 and periodic: # Set all values to 1 for the identity case - values = np.ones((values.shape[0], values.shape[1], 1)) - eval_indeces = np.zeros_like(values, dtype=int) + values = xp.ones((values.shape[0], values.shape[1], 1)) + eval_indeces = xp.zeros_like(values, dtype=int) else: - eval_indeces = np.zeros_like(values, dtype=int) - for i in range(np.shape(spans)[0]): - for k in range(np.shape(spans)[1]): + eval_indeces = xp.zeros_like(values, dtype=int) + for i in range(xp.shape(spans)[0]): + for k in range(xp.shape(spans)[1]): for j in range(degree + 1): eval_indeces[i, k, j] = (spans[i][k] - degree + j) % Nbasis @@ -179,31 +180,31 @@ def get_values_and_indices_splines(Nbasis, degree, periodic, spans, values): def get_one_spline(a, values, eval_indeces): """Given the spline index, an array with the splines evaluated at the evaluation points and another array with the indices indicating to which spline each value corresponds, this function returns - a 1d np.array with the desired spline evaluated at all evaluation points. + a 1d xp.array with the desired spline evaluated at all evaluation points. Parameters ---------- a : int Spline index - values : np.array + values : xp.array 3d array of values of basis functions indexed by (n, nq, basis function). - eval_indeces : np.array + eval_indeces : xp.array 3d array of basis functions indices, indexed by (n, nq, basis function). Returns ------- - my_values : np.array + my_values : xp.array 1d array of values for the spline evaluated at all evaluation points. """ - my_values = np.zeros(np.shape(values)[0] * np.shape(values)[1]) - for i in range(np.shape(values)[0]): - for j in range(np.shape(values)[1]): - for k in range(np.shape(values)[2]): + my_values = xp.zeros(xp.shape(values)[0] * xp.shape(values)[1]) + for i in range(xp.shape(values)[0]): + for j in range(xp.shape(values)[1]): + for k in range(xp.shape(values)[2]): if eval_indeces[i, j, k] == a: - my_values[i * np.shape(values)[1] + j] = values[i, j, k] + my_values[i * xp.shape(values)[1] + j] = values[i, j, k] break return my_values @@ -213,7 +214,7 @@ def get_span_and_basis(pts, space): Parameters ---------- - pts : np.array + pts : xp.array 2d array of points (ii, iq) = (interval, quadrature point). space : SplineSpace @@ -221,10 +222,10 @@ def get_span_and_basis(pts, space): Returns ------- - span : np.array + span : xp.array 2d array indexed by (n, nq), where n is the interval and nq is the quadrature point in the interval. - basis : np.array + basis : xp.array 3d array of values of basis functions indexed by (n, nq, basis function). """ @@ -234,8 +235,8 @@ def get_span_and_basis(pts, space): T = space.knots p = space.degree - span = np.zeros(pts.shape, dtype=int) - basis = np.zeros((*pts.shape, p + 1), dtype=float) + span = xp.zeros(pts.shape, dtype=int) + basis = xp.zeros((*pts.shape, p + 1), dtype=float) for n in range(pts.shape[0]): for nq in range(pts.shape[1]): @@ -463,7 +464,7 @@ def get_non_zero_B_spline_indices(periodic, IoH, p, B_nbasis, starts, ends, Basi stuck, ) - Basis_functions_indices_B.append(np.array(aux_indices)) + Basis_functions_indices_B.append(xp.array(aux_indices)) def get_non_zero_D_spline_indices(periodic, IoH, p, D_nbasis, starts, ends, Basis_functions_indices_D): @@ -521,7 +522,7 @@ def get_non_zero_D_spline_indices(periodic, IoH, p, D_nbasis, starts, ends, Basi stuck, ) - Basis_functions_indices_D.append(np.array(aux_indices)) + Basis_functions_indices_D.append(xp.array(aux_indices)) def build_translation_list_for_non_zero_spline_indices( @@ -583,17 +584,17 @@ def build_translation_list_for_non_zero_spline_indices( """ translation_indices_B_or_D_splines = [ { - "B": np.full((B_nbasis[h]), fill_value=-1, dtype=int), - "D": np.full((D_nbasis[h]), fill_value=-1, dtype=int), + "B": xp.full((B_nbasis[h]), fill_value=-1, dtype=int), + "D": xp.full((D_nbasis[h]), fill_value=-1, dtype=int), } for h in range(3) ] for h in range(3): - translation_indices_B_or_D_splines[h]["B"][Basis_functions_indices_B[h]] = np.arange( + translation_indices_B_or_D_splines[h]["B"][Basis_functions_indices_B[h]] = xp.arange( len(Basis_functions_indices_B[h]) ) - translation_indices_B_or_D_splines[h]["D"][Basis_functions_indices_D[h]] = np.arange( + translation_indices_B_or_D_splines[h]["D"][Basis_functions_indices_D[h]] = xp.arange( len(Basis_functions_indices_D[h]) ) @@ -653,7 +654,7 @@ def evaluate_relevant_splines_at_relevant_points( for h in range(3): # Reshape localpts[h] if necessary localpts_reshaped = ( - localpts[h].reshape((np.shape(localpts[h])[0], 1)) if len(np.shape(localpts[h])) == 1 else localpts[h] + localpts[h].reshape((xp.shape(localpts[h])[0], 1)) if len(xp.shape(localpts[h])) == 1 else localpts[h] ) # Get spans and evaluation values for B-splines and D-splines @@ -759,7 +760,7 @@ def determine_non_zero_rows_for_each_spline( def process_splines(indices, nbasis, is_D, h): for i in indices[h]: - aux = np.zeros((ends[h] + 1 - starts[h]), dtype=int) + aux = xp.zeros((ends[h] + 1 - starts[h]), dtype=int) get_rows( int(i), int(starts[h]), @@ -773,8 +774,8 @@ def process_splines(indices, nbasis, is_D, h): ) rangestart, rangeend = transform_into_ranges(aux) key = "D" if is_D else "B" - rows_B_or_D_splines[h][key].append(np.array(rangestart, dtype=int)) - rowe_B_or_D_splines[h][key].append(np.array(rangeend, dtype=int)) + rows_B_or_D_splines[h][key].append(xp.array(rangestart, dtype=int)) + rowe_B_or_D_splines[h][key].append(xp.array(rangeend, dtype=int)) for h in range(3): process_splines(Basis_functions_indices_B, B_nbasis, False, h) @@ -890,7 +891,7 @@ def is_spline_zero_at_quadrature_points( for h in range(3): if necessary_direction[h]: for i in Basis_functions_indices_B[h]: - Auxiliar = np.ones((np.shape(localpts[h])[0]), dtype=int) + Auxiliar = xp.ones((xp.shape(localpts[h])[0]), dtype=int) are_quadrature_points_zero( Auxiliar, int( @@ -901,7 +902,7 @@ def is_spline_zero_at_quadrature_points( are_zero_B_or_D_splines[h]["B"].append(Auxiliar) for i in Basis_functions_indices_D[h]: - Auxiliar = np.ones((np.shape(localpts[h])[0]), dtype=int) + Auxiliar = xp.ones((xp.shape(localpts[h])[0]), dtype=int) are_quadrature_points_zero( Auxiliar, int( diff --git a/src/struphy/feec/variational_utilities.py b/src/struphy/feec/variational_utilities.py index 61773122e..12695bfa2 100644 --- a/src/struphy/feec/variational_utilities.py +++ b/src/struphy/feec/variational_utilities.py @@ -1,5 +1,6 @@ from copy import deepcopy +import cunumpy as xp from psydac.linalg.basic import IdentityOperator, Vector from psydac.linalg.block import BlockVector from psydac.linalg.solvers import inverse @@ -12,7 +13,6 @@ ) from struphy.feec.linear_operators import LinOpWithTransp from struphy.feec.psydac_derham import Derham -from struphy.utils.arrays import xp as np class BracketOperator(LinOpWithTransp): @@ -192,10 +192,10 @@ def __init__( # Create tmps for later use in evaluating on the grid grid_shape = tuple([len(loc_grid) for loc_grid in interpolation_grid]) - self._vf_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] - self._gvf1_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] - self._gvf2_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] - self._gvf3_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] + self._vf_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] + self._gvf1_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] + self._gvf2_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] + self._gvf3_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] # gradient of the component of the vector field grad = derham.grad_bcfree @@ -378,13 +378,13 @@ def __init__(self, derham, transposed=False, weights=None): ) grid_shape = tuple([len(loc_grid) for loc_grid in hist_grid_0]) - self._f_0_values = np.zeros(grid_shape, dtype=float) + self._f_0_values = xp.zeros(grid_shape, dtype=float) grid_shape = tuple([len(loc_grid) for loc_grid in hist_grid_1]) - self._f_1_values = np.zeros(grid_shape, dtype=float) + self._f_1_values = xp.zeros(grid_shape, dtype=float) grid_shape = tuple([len(loc_grid) for loc_grid in hist_grid_2]) - self._f_2_values = np.zeros(grid_shape, dtype=float) + self._f_2_values = xp.zeros(grid_shape, dtype=float) @property def domain(self): @@ -533,7 +533,7 @@ def __init__(self, derham, transposed=False, weights=None): ) grid_shape = tuple([len(loc_grid) for loc_grid in hist_grid_0]) - self._bf0_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] + self._bf0_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] self.hist_grid_0_b = [ [self.hist_grid_0_bn[0], self.hist_grid_0_bd[1], self.hist_grid_0_bd[2]], [ @@ -544,7 +544,7 @@ def __init__(self, derham, transposed=False, weights=None): [self.hist_grid_0_bd[0], self.hist_grid_0_bd[1], self.hist_grid_0_bn[2]], ] grid_shape = tuple([len(loc_grid) for loc_grid in hist_grid_1]) - self._bf1_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] + self._bf1_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] self.hist_grid_1_b = [ [self.hist_grid_1_bn[0], self.hist_grid_1_bd[1], self.hist_grid_1_bd[2]], [ @@ -556,7 +556,7 @@ def __init__(self, derham, transposed=False, weights=None): ] grid_shape = tuple([len(loc_grid) for loc_grid in hist_grid_2]) - self._bf2_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] + self._bf2_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] self.hist_grid_2_b = [ [self.hist_grid_2_bn[0], self.hist_grid_2_bd[1], self.hist_grid_2_bd[2]], [ @@ -727,8 +727,8 @@ def __init__(self, derham, phys_domain, Uv, gamma, transposed=False, weights1=No self._proj_p_metric = deepcopy(metric) grid_shape = tuple([len(loc_grid) for loc_grid in int_grid]) - self._pf_values = np.zeros(grid_shape, dtype=float) - self._mapped_pf_values = np.zeros(grid_shape, dtype=float) + self._pf_values = xp.zeros(grid_shape, dtype=float) + self._mapped_pf_values = xp.zeros(grid_shape, dtype=float) # gradient of the component of the vector field @@ -749,13 +749,13 @@ def __init__(self, derham, phys_domain, Uv, gamma, transposed=False, weights1=No ) grid_shape = tuple([len(loc_grid) for loc_grid in hist_grid_20]) - self._pf_0_values = np.zeros(grid_shape, dtype=float) + self._pf_0_values = xp.zeros(grid_shape, dtype=float) grid_shape = tuple([len(loc_grid) for loc_grid in hist_grid_21]) - self._pf_1_values = np.zeros(grid_shape, dtype=float) + self._pf_1_values = xp.zeros(grid_shape, dtype=float) grid_shape = tuple([len(loc_grid) for loc_grid in hist_grid_22]) - self._pf_2_values = np.zeros(grid_shape, dtype=float) + self._pf_2_values = xp.zeros(grid_shape, dtype=float) @property def domain(self): @@ -877,21 +877,21 @@ def __init__(self, derham, gamma): self.rhof1 = self._derham.create_spline_function("rhof1", "L2") grid_shape = tuple([len(loc_grid) for loc_grid in integration_grid]) - self._rhof_values = np.zeros(grid_shape, dtype=float) - self._rhof1_values = np.zeros(grid_shape, dtype=float) - self._sf_values = np.zeros(grid_shape, dtype=float) - self._sf1_values = np.zeros(grid_shape, dtype=float) - self._delta_values = np.zeros(grid_shape, dtype=float) - self._rhof_mid_values = np.zeros(grid_shape, dtype=float) - self._sf_mid_values = np.zeros(grid_shape, dtype=float) - self._eta_values = np.zeros(grid_shape, dtype=float) - self._en_values = np.zeros(grid_shape, dtype=float) - self._en1_values = np.zeros(grid_shape, dtype=float) - self._de_values = np.zeros(grid_shape, dtype=float) - self._d2e_values = np.zeros(grid_shape, dtype=float) - self._tmp_int_grid = np.zeros(grid_shape, dtype=float) - self._tmp_int_grid2 = np.zeros(grid_shape, dtype=float) - self._DG_values = np.zeros(grid_shape, dtype=float) + self._rhof_values = xp.zeros(grid_shape, dtype=float) + self._rhof1_values = xp.zeros(grid_shape, dtype=float) + self._sf_values = xp.zeros(grid_shape, dtype=float) + self._sf1_values = xp.zeros(grid_shape, dtype=float) + self._delta_values = xp.zeros(grid_shape, dtype=float) + self._rhof_mid_values = xp.zeros(grid_shape, dtype=float) + self._sf_mid_values = xp.zeros(grid_shape, dtype=float) + self._eta_values = xp.zeros(grid_shape, dtype=float) + self._en_values = xp.zeros(grid_shape, dtype=float) + self._en1_values = xp.zeros(grid_shape, dtype=float) + self._de_values = xp.zeros(grid_shape, dtype=float) + self._d2e_values = xp.zeros(grid_shape, dtype=float) + self._tmp_int_grid = xp.zeros(grid_shape, dtype=float) + self._tmp_int_grid2 = xp.zeros(grid_shape, dtype=float) + self._DG_values = xp.zeros(grid_shape, dtype=float) def ener(self, rho, s, out=None): r"""Themodynamical energy as a function of rho and s, usign the perfect gaz hypothesis. @@ -901,13 +901,13 @@ def ener(self, rho, s, out=None): """ gam = self._gamma if out is None: - out = np.power(rho, gam) * np.exp(s / rho) + out = xp.power(rho, gam) * xp.exp(s / rho) else: out *= 0.0 out += s out /= rho - np.exp(out, out=out) - np.power(rho, gam, out=self._tmp_int_grid) + xp.exp(out, out=out) + xp.power(rho, gam, out=self._tmp_int_grid) out *= self._tmp_int_grid return out @@ -919,17 +919,17 @@ def dener_drho(self, rho, s, out=None): """ gam = self._gamma if out is None: - out = (gam * np.power(rho, gam - 1) - s * np.power(rho, gam - 2)) * np.exp(s / rho) + out = (gam * xp.power(rho, gam - 1) - s * xp.power(rho, gam - 2)) * xp.exp(s / rho) else: out *= 0.0 out += s out /= rho - np.exp(out, out=out) + xp.exp(out, out=out) - np.power(rho, gam - 1, out=self._tmp_int_grid) + xp.power(rho, gam - 1, out=self._tmp_int_grid) self._tmp_int_grid *= gam - np.power(rho, gam - 2, out=self._tmp_int_grid2) + xp.power(rho, gam - 2, out=self._tmp_int_grid2) self._tmp_int_grid2 *= s self._tmp_int_grid -= self._tmp_int_grid2 @@ -944,13 +944,13 @@ def dener_ds(self, rho, s, out=None): """ gam = self._gamma if out is None: - out = np.power(rho, gam - 1) * np.exp(s / rho) + out = xp.power(rho, gam - 1) * xp.exp(s / rho) else: out *= 0.0 out += s out /= rho - np.exp(out, out=out) - np.power(rho, gam - 1, out=self._tmp_int_grid) + xp.exp(out, out=out) + xp.power(rho, gam - 1, out=self._tmp_int_grid) out *= self._tmp_int_grid return out @@ -963,25 +963,25 @@ def d2ener_drho2(self, rho, s, out=None): gam = self._gamma if out is None: out = ( - gam * (gam - 1) * np.power(rho, gam - 2) - - s * 2 * (gam - 1) * np.power(rho, gam - 3) - + s**2 * np.power(rho, gam - 4) - ) * np.exp(s / rho) + gam * (gam - 1) * xp.power(rho, gam - 2) + - s * 2 * (gam - 1) * xp.power(rho, gam - 3) + + s**2 * xp.power(rho, gam - 4) + ) * xp.exp(s / rho) else: out *= 0.0 out += s out /= rho - np.exp(out, out=out) + xp.exp(out, out=out) - np.power(rho, gam - 2, out=self._tmp_int_grid) + xp.power(rho, gam - 2, out=self._tmp_int_grid) self._tmp_int_grid *= gam * (gam - 1) - np.power(rho, gam - 3, out=self._tmp_int_grid2) + xp.power(rho, gam - 3, out=self._tmp_int_grid2) self._tmp_int_grid2 *= s self._tmp_int_grid2 *= 2 * (gam - 1) self._tmp_int_grid -= self._tmp_int_grid2 - np.power(rho, gam - 4, out=self._tmp_int_grid2) + xp.power(rho, gam - 4, out=self._tmp_int_grid2) self._tmp_int_grid2 *= s self._tmp_int_grid2 *= s self._tmp_int_grid += self._tmp_int_grid2 @@ -996,27 +996,27 @@ def d2ener_ds2(self, rho, s, out=None): """ gam = self._gamma if out is None: - out = np.power(rho, gam - 2) * np.exp(s / rho) + out = xp.power(rho, gam - 2) * xp.exp(s / rho) else: out *= 0.0 out += s out /= rho - np.exp(out, out=out) - np.power(rho, gam - 2, out=self._tmp_int_grid) + xp.exp(out, out=out) + xp.power(rho, gam - 2, out=self._tmp_int_grid) out *= self._tmp_int_grid return out def eta(self, delta_x, out=None): r"""Switch function :math:`\eta(\delta) = 1- \text{exp}((-\delta/10^{-5})^2)`.""" if out is None: - out = 1.0 - np.exp(-((delta_x / 1e-5) ** 2)) + out = 1.0 - xp.exp(-((delta_x / 1e-5) ** 2)) else: out *= 0.0 out += delta_x out /= 1e-5 out **= 2 out *= -1 - np.exp(out, out=out) + xp.exp(out, out=out) out *= -1 out += 1.0 return out @@ -1328,7 +1328,7 @@ def __init__(self, derham, mass_ops, domain): ) grid_shape = tuple([len(loc_grid) for loc_grid in integration_grid]) - self._f_values = np.zeros(grid_shape, dtype=float) + self._f_values = xp.zeros(grid_shape, dtype=float) metric = domain.metric(*integration_grid) self._mass_metric_term = deepcopy(metric) @@ -1437,10 +1437,10 @@ def __init__(self, derham, domain, mass_ops): self.uf = derham.create_spline_function("uf", "H1vec") self.uf1 = derham.create_spline_function("uf1", "H1vec") - self._uf_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] - self._uf1_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] - self._Guf_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] - self._tmp_int_grid = np.zeros(grid_shape, dtype=float) + self._uf_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] + self._uf1_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] + self._Guf_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] + self._tmp_int_grid = xp.zeros(grid_shape, dtype=float) metric = domain.metric( *integration_grid, diff --git a/src/struphy/fields_background/base.py b/src/struphy/fields_background/base.py index 275fb13f9..4696acf54 100644 --- a/src/struphy/fields_background/base.py +++ b/src/struphy/fields_background/base.py @@ -2,11 +2,11 @@ from abc import ABCMeta, abstractmethod +import cunumpy as xp from matplotlib import pyplot as plt from pyevtk.hl import gridToVTK from struphy.geometry.base import Domain -from struphy.utils.arrays import xp as np class FluidEquilibrium(metaclass=ABCMeta): @@ -47,7 +47,7 @@ def domain(self): @domain.setter def domain(self, new_domain): - assert isinstance(new_domain, Domain) + assert isinstance(new_domain, Domain) or new_domain is None self._domain = new_domain ########################### @@ -140,7 +140,7 @@ def t3(self, *etas, squeeze_out=False): def vth0(self, *etas, squeeze_out=False): """0-form thermal velocity on logical cube [0, 1]^3.""" - return np.sqrt(self.t0(*etas, squeeze_out=squeeze_out)) + return xp.sqrt(self.t0(*etas, squeeze_out=squeeze_out)) def vth3(self, *etas, squeeze_out=False): """3-form thermal velocity on logical cube [0, 1]^3.""" @@ -156,7 +156,7 @@ def q0(self, *etas, squeeze_out=False): """0-form square root of the pressure on logical cube [0, 1]^3.""" # xyz = self.domain(*etas, squeeze_out=False) p = self.p0(*etas) - q = np.sqrt(p) + q = xp.sqrt(p) return self.domain.pull(q, *etas, kind="0", squeeze_out=squeeze_out) def q3(self, *etas, squeeze_out=False): @@ -176,7 +176,7 @@ def s0_monoatomic(self, *etas, squeeze_out=False): # xyz = self.domain(*etas, squeeze_out=False) p = self.p0(*etas) n = self.n0(*etas) - s = n * np.log(p / (2 / 3 * np.power(n, 5 / 3))) + s = n * xp.log(p / (2 / 3 * xp.power(n, 5 / 3))) return self.domain.pull(s, *etas, kind="0", squeeze_out=squeeze_out) def s3_monoatomic(self, *etas, squeeze_out=False): @@ -198,7 +198,7 @@ def s0_diatomic(self, *etas, squeeze_out=False): # xyz = self.domain(*etas, squeeze_out=False) p = self.p0(*etas) n = self.n0(*etas) - s = n * np.log(p / (2 / 5 * np.power(n, 7 / 5))) + s = n * xp.log(p / (2 / 5 * xp.power(n, 7 / 5))) return self.domain.pull(s, *etas, kind="0", squeeze_out=squeeze_out) def s3_diatomic(self, *etas, squeeze_out=False): @@ -395,7 +395,7 @@ def unit_b_cart(self, *etas, squeeze_out=False): """Unit vector Cartesian components of magnetic field evaluated on logical cube [0, 1]^3. Returns also (x,y,z).""" b, xyz = self.b_cart(*etas, squeeze_out=squeeze_out) absB = self.absB0(*etas, squeeze_out=squeeze_out) - out = np.array([b[0] / absB, b[1] / absB, b[2] / absB], dtype=float) + out = xp.array([b[0] / absB, b[1] / absB, b[2] / absB], dtype=float) return out, xyz def gradB1(self, *etas, squeeze_out=False): @@ -481,7 +481,7 @@ def av(self, *etas, squeeze_out=False): def absB0(self, *etas, squeeze_out=False): """0-form absolute value of magnetic field on logical cube [0, 1]^3.""" b, xyz = self.b_cart(*etas, squeeze_out=squeeze_out) - return np.sqrt(b[0] ** 2 + b[1] ** 2 + b[2] ** 2) + return xp.sqrt(b[0] ** 2 + b[1] ** 2 + b[2] ** 2) def absB3(self, *etas, squeeze_out=False): """3-form absolute value of magnetic field on logical cube [0, 1]^3.""" @@ -804,7 +804,7 @@ def curl_unit_b_cart(self, *etas, squeeze_out=False): j, xyz = self.j_cart(*etas, squeeze_out=squeeze_out) gradB, xyz = self.gradB_cart(*etas, squeeze_out=squeeze_out) absB = self.absB0(*etas, squeeze_out=squeeze_out) - out = np.array( + out = xp.array( [ j[0] / absB + (b[1] * gradB[2] - b[2] * gradB[1]) / absB**2, j[1] / absB + (b[2] * gradB[0] - b[0] * gradB[2]) / absB**2, @@ -908,9 +908,9 @@ def show(self, n1=16, n2=33, n3=21, n_planes=5): "HollowTorus", ) - e1 = np.linspace(0.0001, 1, n1) - e2 = np.linspace(0, 1, n2) - e3 = np.linspace(0, 1, n3) + e1 = xp.linspace(0.0001, 1, n1) + e2 = xp.linspace(0, 1, n2) + e3 = xp.linspace(0, 1, n3) if self.domain.__class__.__name__ in ("GVECunit", "DESCunit"): if n_planes > 1: @@ -935,7 +935,7 @@ def show(self, n1=16, n2=33, n3=21, n_planes=5): print("Computation of abs(B) done.") j_cart, xyz = self.j_cart(e1, e2, e3) print("Computation of current density done.") - absJ = np.sqrt(j_cart[0] ** 2 + j_cart[1] ** 2 + j_cart[2] ** 2) + absJ = xp.sqrt(j_cart[0] ** 2 + j_cart[1] ** 2 + j_cart[2] ** 2) _path = struphy.__path__[0] + "/fields_background/mhd_equil/gvec/output/" gridToVTK( @@ -958,24 +958,24 @@ def show(self, n1=16, n2=33, n3=21, n_planes=5): print(key, ": ", val) # poloidal plane grid - fig = plt.figure(figsize=(13, np.ceil(n_planes / 2) * 6.5)) + fig = plt.figure(figsize=(13, xp.ceil(n_planes / 2) * 6.5)) for n in range(n_planes): - xp = x[:, :, int(n * jump)].squeeze() + xpp = x[:, :, int(n * jump)].squeeze() yp = y[:, :, int(n * jump)].squeeze() zp = z[:, :, int(n * jump)].squeeze() if self.domain.__class__.__name__ in torus_mappings: - pc1 = np.sqrt(xp**2 + yp**2) + pc1 = xp.sqrt(xpp**2 + yp**2) pc2 = zp l1 = "R" l2 = "Z" else: - pc1 = xp + pc1 = xpp pc2 = yp l1 = "x" l2 = "y" - ax = fig.add_subplot(int(np.ceil(n_planes / 2)), 2, n + 1) + ax = fig.add_subplot(int(xp.ceil(n_planes / 2)), 2, n + 1) for i in range(pc1.shape[0]): for j in range(pc1.shape[1] - 1): if i < pc1.shape[0] - 1: @@ -1004,26 +1004,26 @@ def show(self, n1=16, n2=33, n3=21, n_planes=5): ) # top view - e1 = np.linspace(0, 1, n1) # radial coordinate in [0, 1] - e2 = np.linspace(0, 1, 3) # poloidal angle in [0, 1] - e3 = np.linspace(0, 1, n3) # toroidal angle in [0, 1] + e1 = xp.linspace(0, 1, n1) # radial coordinate in [0, 1] + e2 = xp.linspace(0, 1, 3) # poloidal angle in [0, 1] + e3 = xp.linspace(0, 1, n3) # toroidal angle in [0, 1] xt, yt, zt = self.domain(e1, e2, e3) fig = plt.figure(figsize=(13, 2 * 6.5)) ax = fig.add_subplot() for m in range(2): - xp = xt[:, m, :].squeeze() + xpp = xt[:, m, :].squeeze() yp = yt[:, m, :].squeeze() zp = zt[:, m, :].squeeze() if self.domain.__class__.__name__ in torus_mappings: - tc1 = xp + tc1 = xpp tc2 = yp l1 = "x" l2 = "y" else: - tc1 = xp + tc1 = xpp tc2 = zp l1 = "x" l2 = "z" @@ -1058,26 +1058,26 @@ def show(self, n1=16, n2=33, n3=21, n_planes=5): ax.set_title("Device top view") # Jacobian determinant - fig = plt.figure(figsize=(13, np.ceil(n_planes / 2) * 6.5)) + fig = plt.figure(figsize=(13, xp.ceil(n_planes / 2) * 6.5)) for n in range(n_planes): - xp = x[:, :, int(n * jump)].squeeze() + xpp = x[:, :, int(n * jump)].squeeze() yp = y[:, :, int(n * jump)].squeeze() zp = z[:, :, int(n * jump)].squeeze() if self.domain.__class__.__name__ in torus_mappings: - pc1 = np.sqrt(xp**2 + yp**2) + pc1 = xp.sqrt(xpp**2 + yp**2) pc2 = zp l1 = "R" l2 = "Z" else: - pc1 = xp + pc1 = xpp pc2 = yp l1 = "x" l2 = "y" detp = det_df[:, :, int(n * jump)].squeeze() - ax = fig.add_subplot(int(np.ceil(n_planes / 2)), 2, n + 1) + ax = fig.add_subplot(int(xp.ceil(n_planes / 2)), 2, n + 1) map = ax.contourf(pc1, pc2, detp, 30) ax.set_xlabel(l1) ax.set_ylabel(l2) @@ -1088,26 +1088,26 @@ def show(self, n1=16, n2=33, n3=21, n_planes=5): fig.colorbar(map, ax=ax, location="right") # pressure - fig = plt.figure(figsize=(15, np.ceil(n_planes / 2) * 6.5)) + fig = plt.figure(figsize=(15, xp.ceil(n_planes / 2) * 6.5)) for n in range(n_planes): - xp = x[:, :, int(n * jump)].squeeze() + xpp = x[:, :, int(n * jump)].squeeze() yp = y[:, :, int(n * jump)].squeeze() zp = z[:, :, int(n * jump)].squeeze() if self.domain.__class__.__name__ in torus_mappings: - pc1 = np.sqrt(xp**2 + yp**2) + pc1 = xp.sqrt(xpp**2 + yp**2) pc2 = zp l1 = "R" l2 = "Z" else: - pc1 = xp + pc1 = xpp pc2 = yp l1 = "x" l2 = "y" pp = p[:, :, int(n * jump)].squeeze() - ax = fig.add_subplot(int(np.ceil(n_planes / 2)), 2, n + 1) + ax = fig.add_subplot(int(xp.ceil(n_planes / 2)), 2, n + 1) map = ax.contourf(pc1, pc2, pp, 30) ax.set_xlabel(l1) ax.set_ylabel(l2) @@ -1118,26 +1118,26 @@ def show(self, n1=16, n2=33, n3=21, n_planes=5): fig.colorbar(map, ax=ax, location="right") # density - fig = plt.figure(figsize=(15, np.ceil(n_planes / 2) * 6.5)) + fig = plt.figure(figsize=(15, xp.ceil(n_planes / 2) * 6.5)) for n in range(n_planes): - xp = x[:, :, int(n * jump)].squeeze() + xpp = x[:, :, int(n * jump)].squeeze() yp = y[:, :, int(n * jump)].squeeze() zp = z[:, :, int(n * jump)].squeeze() if self.domain.__class__.__name__ in torus_mappings: - pc1 = np.sqrt(xp**2 + yp**2) + pc1 = xp.sqrt(xpp**2 + yp**2) pc2 = zp l1 = "R" l2 = "Z" else: - pc1 = xp + pc1 = xpp pc2 = yp l1 = "x" l2 = "y" nn = n_dens[:, :, int(n * jump)].squeeze() - ax = fig.add_subplot(int(np.ceil(n_planes / 2)), 2, n + 1) + ax = fig.add_subplot(int(xp.ceil(n_planes / 2)), 2, n + 1) map = ax.contourf(pc1, pc2, nn, 30) ax.set_xlabel(l1) ax.set_ylabel(l2) @@ -1148,26 +1148,26 @@ def show(self, n1=16, n2=33, n3=21, n_planes=5): fig.colorbar(map, ax=ax, location="right") # magnetic field strength - fig = plt.figure(figsize=(15, np.ceil(n_planes / 2) * 6.5)) + fig = plt.figure(figsize=(15, xp.ceil(n_planes / 2) * 6.5)) for n in range(n_planes): - xp = x[:, :, int(n * jump)].squeeze() + xpp = x[:, :, int(n * jump)].squeeze() yp = y[:, :, int(n * jump)].squeeze() zp = z[:, :, int(n * jump)].squeeze() if self.domain.__class__.__name__ in torus_mappings: - pc1 = np.sqrt(xp**2 + yp**2) + pc1 = xp.sqrt(xpp**2 + yp**2) pc2 = zp l1 = "R" l2 = "Z" else: - pc1 = xp + pc1 = xpp pc2 = yp l1 = "x" l2 = "y" ab = absB[:, :, int(n * jump)].squeeze() - ax = fig.add_subplot(int(np.ceil(n_planes / 2)), 2, n + 1) + ax = fig.add_subplot(int(xp.ceil(n_planes / 2)), 2, n + 1) map = ax.contourf(pc1, pc2, ab, 30) ax.set_xlabel(l1) ax.set_ylabel(l2) @@ -1178,26 +1178,26 @@ def show(self, n1=16, n2=33, n3=21, n_planes=5): fig.colorbar(map, ax=ax, location="right") # current density - fig = plt.figure(figsize=(15, np.ceil(n_planes / 2) * 6.5)) + fig = plt.figure(figsize=(15, xp.ceil(n_planes / 2) * 6.5)) for n in range(n_planes): - xp = x[:, :, int(n * jump)].squeeze() + xpp = x[:, :, int(n * jump)].squeeze() yp = y[:, :, int(n * jump)].squeeze() zp = z[:, :, int(n * jump)].squeeze() if self.domain.__class__.__name__ in torus_mappings: - pc1 = np.sqrt(xp**2 + yp**2) + pc1 = xp.sqrt(xpp**2 + yp**2) pc2 = zp l1 = "R" l2 = "Z" else: - pc1 = xp + pc1 = xpp pc2 = yp l1 = "x" l2 = "y" ab = absJ[:, :, int(n * jump)].squeeze() - ax = fig.add_subplot(int(np.ceil(n_planes / 2)), 2, n + 1) + ax = fig.add_subplot(int(xp.ceil(n_planes / 2)), 2, n + 1) map = ax.contourf(pc1, pc2, ab, 30) ax.set_xlabel(l1) ax.set_ylabel(l2) @@ -1307,8 +1307,8 @@ def b_xyz(self, x, y, z): BZ = self.psi(R, Z, dR=1) / R # push-forward to Cartesian components - Bx = BR * np.cos(Phi) - BP * np.sin(Phi) - By = BR * np.sin(Phi) + BP * np.cos(Phi) + Bx = BR * xp.cos(Phi) - BP * xp.sin(Phi) + By = BR * xp.sin(Phi) + BP * xp.cos(Phi) Bz = 1 * BZ return Bx, By, Bz @@ -1324,8 +1324,8 @@ def j_xyz(self, x, y, z): jZ = self.g_tor(R, Z, dR=1) / R # push-forward to Cartesian components - jx = jR * np.cos(Phi) - jP * np.sin(Phi) - jy = jR * np.sin(Phi) + jP * np.cos(Phi) + jx = jR * xp.cos(Phi) - jP * xp.sin(Phi) + jy = jR * xp.sin(Phi) + jP * xp.cos(Phi) jz = 1 * jZ return jx, jy, jz @@ -1335,7 +1335,7 @@ def gradB_xyz(self, x, y, z): R, Phi, Z = self.inverse_map(x, y, z) - RabsB = np.sqrt( + RabsB = xp.sqrt( self.psi(R, Z, dZ=1) ** 2 + self.g_tor(R, Z) ** 2 + self.psi(R, Z, dR=1) ** 2, ) @@ -1363,8 +1363,8 @@ def gradB_xyz(self, x, y, z): ) # push-forward to Cartesian components - gradBx = gradBR * np.cos(Phi) - gradBP * np.sin(Phi) - gradBy = gradBR * np.sin(Phi) + gradBP * np.cos(Phi) + gradBx = gradBR * xp.cos(Phi) - gradBP * xp.sin(Phi) + gradBy = gradBR * xp.sin(Phi) + gradBP * xp.cos(Phi) gradBz = 1 * gradBZ return gradBx, gradBy, gradBz @@ -1373,8 +1373,8 @@ def gradB_xyz(self, x, y, z): def inverse_map(x, y, z): """Inverse cylindrical mapping.""" - R = np.sqrt(x**2 + y**2) - P = np.arctan2(y, x) + R = xp.sqrt(x**2 + y**2) + P = xp.arctan2(y, x) Z = 1 * z return R, P, Z diff --git a/src/struphy/fields_background/coil_fields/base.py b/src/struphy/fields_background/coil_fields/base.py index 540268ef3..3b068c62d 100644 --- a/src/struphy/fields_background/coil_fields/base.py +++ b/src/struphy/fields_background/coil_fields/base.py @@ -1,10 +1,9 @@ from abc import ABCMeta, abstractmethod +import cunumpy as xp from matplotlib import pyplot as plt from pyevtk.hl import gridToVTK -from struphy.utils.arrays import xp as np - class CoilMagneticField(metaclass=ABCMeta): """ @@ -32,7 +31,7 @@ def b_xyz(self, x, y, z): def absB0(self, *etas, squeeze_out=False): """0-form absolute value of equilibrium magnetic field on logical cube [0, 1]^3.""" b, xyz = self.b_cart(*etas, squeeze_out=squeeze_out) - return np.sqrt(b[0] ** 2 + b[1] ** 2 + b[2] ** 2) + return xp.sqrt(b[0] ** 2 + b[1] ** 2 + b[2] ** 2) def absB3(self, *etas, squeeze_out=False): """3-form absolute value of equilibrium magnetic field on logical cube [0, 1]^3.""" @@ -92,7 +91,7 @@ def unit_b_cart(self, *etas, squeeze_out=False): """Unit vector Cartesian components of equilibrium magnetic field evaluated on logical cube [0, 1]^3. Returns also (x,y,z).""" b, xyz = self.b_cart(*etas, squeeze_out=squeeze_out) absB = self.absB0(*etas, squeeze_out=squeeze_out) - out = np.array([b[0] / absB, b[1] / absB, b[2] / absB], dtype=float) + out = xp.array([b[0] / absB, b[1] / absB, b[2] / absB], dtype=float) return out, xyz diff --git a/src/struphy/fields_background/coil_fields/coil_fields.py b/src/struphy/fields_background/coil_fields/coil_fields.py index 75895c465..e1f66c71d 100644 --- a/src/struphy/fields_background/coil_fields/coil_fields.py +++ b/src/struphy/fields_background/coil_fields/coil_fields.py @@ -1,6 +1,7 @@ +import cunumpy as xp + from struphy.feec.psydac_derham import Derham from struphy.fields_background.coil_fields.base import CoilMagneticField, load_csv_data -from struphy.utils.arrays import xp as np class RatGUI(CoilMagneticField): @@ -79,8 +80,8 @@ def bfield_RZphi(self): def b_xyz(self, x, y, z): """Cartesian coil magnetic field in physical space. Must return the components as a tuple.""" # compute (R, Z, phi) corrdinates from (x, y, z), for example: - R = np.sqrt(x**2 + y**2) + R = xp.sqrt(x**2 + y**2) Z = z - phi = -np.arctan2(y / x) + phi = -xp.arctan2(y / x) return self.bfield_RZphi(R, Z, phi) diff --git a/src/struphy/fields_background/equils.py b/src/struphy/fields_background/equils.py index 71488ce7f..93688b55f 100644 --- a/src/struphy/fields_background/equils.py +++ b/src/struphy/fields_background/equils.py @@ -7,6 +7,7 @@ import warnings from time import time +import cunumpy as xp from scipy.integrate import odeint, quad from scipy.interpolate import RectBivariateSpline, UnivariateSpline from scipy.optimize import fsolve, minimize @@ -28,7 +29,6 @@ NumericalMHDequilibrium, ) from struphy.fields_background.mhd_equil.eqdsk import readeqdsk -from struphy.utils.arrays import xp as np from struphy.utils.utils import read_state, subp_run @@ -178,7 +178,8 @@ class ShearedSlab(CartesianMHDequilibrium): Ion number density at x=a (default: 1.). beta : float Plasma beta (ratio of kinematic pressure to B^2/2, default: 0.1). - + q_kind : int + Kind of safety factor profile, (0 or 1, default: 0). Note ---- In the parameter .yml, use the following in the section ``fluid_background``:: @@ -193,6 +194,7 @@ class ShearedSlab(CartesianMHDequilibrium): n2 : 0. # 2nd shape factor for ion number density profile na : 1. # number density at r=a beta : .1 # plasma beta = p*2/B^2 + q_kind : 0. # kind of safety factor profile """ def __init__( @@ -206,6 +208,7 @@ def __init__( n2: float = 0.0, na: float = 1.0, beta: float = 0.1, + q_kind: int = 0, ): # use params setter self.params = copy.deepcopy(locals()) @@ -226,10 +229,19 @@ def q_x(self, x, der=0): qout = 0 * x else: - if der == 0: - qout = self.params["q0"] + (self.params["q1"] - self.params["q0"]) * (x / self.params["a"]) ** 2 + if self.params["q_kind"] == 0: + if der == 0: + qout = self.params["q0"] + (self.params["q1"] - self.params["q0"]) * (x / self.params["a"]) ** 2 + else: + qout = 2 * (self.params["q1"] - self.params["q0"]) * x / self.params["a"] ** 2 + else: - qout = 2 * (self.params["q1"] - self.params["q0"]) * x / self.params["a"] ** 2 + if der == 0: + qout = self.params["q0"] + self.params["q1"] * xp.sin(2.0 * xp.pi * x / self.params["a"]) + else: + qout = ( + 2.0 * xp.pi / self.params["a"] * self.params["q1"] * xp.cos(2.0 * xp.pi * x / self.params["a"]) + ) return qout @@ -239,7 +251,7 @@ def p_x(self, x): eps = self.params["a"] / self.params["R0"] - if np.all(q >= 100.0): + if xp.all(q >= 100.0): pout = self.params["B0"] ** 2 * self.params["beta"] / 2.0 - 0 * x else: pout = self.params["B0"] ** 2 * self.params["beta"] / 2.0 * (1 + eps**2 / q**2) + self.params[ @@ -261,7 +273,7 @@ def plot_profiles(self, n_pts=501): import matplotlib.pyplot as plt - x = np.linspace(0.0, self.params["a"], n_pts) + x = xp.linspace(0.0, self.params["a"], n_pts) fig, ax = plt.subplots(1, 3) @@ -295,7 +307,7 @@ def b_xyz(self, x, y, z): q = self.q_x(x) eps = self.params["a"] / self.params["R0"] - if np.all(q >= 100.0): + if xp.all(q >= 100.0): by = 0 * x bz = self.params["B0"] - 0 * x else: @@ -312,7 +324,7 @@ def j_xyz(self, x, y, z): q = self.q_x(x) eps = self.params["a"] / self.params["R0"] - if np.all(q >= 100.0): + if xp.all(q >= 100.0): jz = 0 * x else: jz = -self.params["B0"] * eps * self.q_x(x, der=1) / q**2 @@ -341,13 +353,13 @@ def gradB_xyz(self, x, y, z): q = self.q_x(x) eps = self.params["a"] / self.params["R0"] - if np.all(q >= 100.0): + if xp.all(q >= 100.0): gradBx = 0 * x else: gradBx = ( -self.params["B0"] * eps**2 - / np.sqrt(1 + eps**2 / self.q_x(x) ** 2) + / xp.sqrt(1 + eps**2 / self.q_x(x) ** 2) * self.q_x(x, der=1) / self.q_x(x) ** 3 ) @@ -445,8 +457,8 @@ def __init__( def T_z(self, z): r"""Swap function T(z) = \tanh(z - z_1)/\delta) - \tanh(z - z_2)/\delta)""" Tout = ( - np.tanh((z - self.params["z1"]) / self.params["delta"]) - - np.tanh((z - self.params["z2"]) / self.params["delta"]) + xp.tanh((z - self.params["z1"]) / self.params["delta"]) + - xp.tanh((z - self.params["z2"]) / self.params["delta"]) ) / 2.0 return Tout @@ -468,7 +480,7 @@ def plot_profiles(self, n_pts=501): import matplotlib.pyplot as plt - z = np.linspace(0.0, self.params["c"], n_pts) + z = xp.linspace(0.0, self.params["c"], n_pts) fig, ax = plt.subplots(1, 3) @@ -635,8 +647,8 @@ def __init__( self.params = copy.deepcopy(locals()) # inverse cylindrical coordinate transformation (x, y, z) --> (r, theta, phi) - self.r = lambda x, y, z: np.sqrt(x**2 + y**2) - self.theta = lambda x, y, z: np.arctan2(y, x) + self.r = lambda x, y, z: xp.sqrt(x**2 + y**2) + self.theta = lambda x, y, z: xp.arctan2(y, x) self.z = lambda x, y, z: 1 * z # =============================================================== @@ -695,7 +707,7 @@ def plot_profiles(self, n_pts=501): import matplotlib.pyplot as plt - r = np.linspace(0.0, self.params["a"], n_pts) + r = xp.linspace(0.0, self.params["a"], n_pts) fig, ax = plt.subplots(1, 3) @@ -706,7 +718,7 @@ def plot_profiles(self, n_pts=501): ax[0].set_xlabel("r") ax[0].set_ylabel("q") - ax[0].plot(r, np.ones(r.size), "k--") + ax[0].plot(r, xp.ones(r.size), "k--") ax[1].plot(r, self.p_r(r)) ax[1].set_xlabel("r") @@ -731,13 +743,13 @@ def b_xyz(self, x, y, z): theta = self.theta(x, y, z) q = self.q_r(r) # azimuthal component - if np.all(q >= 100.0): + if xp.all(q >= 100.0): b_theta = 0 * r else: b_theta = self.params["B0"] * r / (self.params["R0"] * q) # cartesian x-component - bx = -b_theta * np.sin(theta) - by = b_theta * np.cos(theta) + bx = -b_theta * xp.sin(theta) + by = b_theta * xp.cos(theta) bz = self.params["B0"] - 0 * x return bx, by, bz @@ -751,7 +763,7 @@ def j_xyz(self, x, y, z): r = self.r(x, y, z) q = self.q_r(r) q_p = self.q_r(r, der=1) - if np.all(q >= 100.0): + if xp.all(q >= 100.0): jz = 0 * x else: jz = self.params["B0"] / (self.params["R0"] * q**2) * (2 * q - r * q_p) @@ -778,13 +790,13 @@ def gradB_xyz(self, x, y, z): r = self.r(x, y, z) theta = self.theta(x, y, z) q = self.q_r(r) - if np.all(q >= 100.0): + if xp.all(q >= 100.0): gradBr = 0 * x else: gradBr = ( self.params["B0"] / self.params["R0"] ** 2 - / np.sqrt( + / xp.sqrt( 1 + r**2 / self.q_r( @@ -795,8 +807,8 @@ def gradB_xyz(self, x, y, z): ) * (r / self.q_r(r) ** 2 - r**2 / self.q_r(r) ** 3 * self.q_r(r, der=1)) ) - gradBx = gradBr * np.cos(theta) - gradBy = gradBr * np.sin(theta) + gradBx = gradBr * xp.cos(theta) + gradBy = gradBr * xp.sin(theta) gradBz = 0 * x return gradBx, gradBy, gradBz @@ -935,10 +947,10 @@ def __init__( self.params = copy.deepcopy(locals()) # plasma boundary contour - ths = np.linspace(0.0, 2 * np.pi, 201) + ths = xp.linspace(0.0, 2 * xp.pi, 201) - self._rbs = self.params["R0"] * (1 + self.params["a"] / self.params["R0"] * np.cos(ths)) - self._zbs = self.params["a"] * np.sin(ths) + self._rbs = self.params["R0"] * (1 + self.params["a"] / self.params["R0"] * xp.cos(ths)) + self._zbs = self.params["a"] * xp.sin(ths) # set on-axis and boundary fluxes if self.params["q_kind"] == 0: @@ -949,12 +961,12 @@ def __init__( self._p_i = None else: - r_i = np.linspace(0.0, self.params["a"], self.params["psi_nel"] + 1) + r_i = xp.linspace(0.0, self.params["a"], self.params["psi_nel"] + 1) def dpsi_dr(r): - return self.params["B0"] * r / (self.q_r(r) * np.sqrt(1 - r**2 / self.params["R0"] ** 2)) + return self.params["B0"] * r / (self.q_r(r) * xp.sqrt(1 - r**2 / self.params["R0"] ** 2)) - psis = np.zeros_like(r_i) + psis = xp.zeros_like(r_i) for i, rr in enumerate(r_i): psis[i] = quad(dpsi_dr, 0.0, rr)[0] @@ -977,7 +989,7 @@ def dp_dr(r): * (2 * self.q_r(r) - r * self.q_r(r, der=1)) ) - ps = np.zeros_like(r_i) + ps = xp.zeros_like(r_i) for i, rr in enumerate(r_i): ps[i] = quad(dp_dr, 0.0, rr)[0] @@ -1032,7 +1044,7 @@ def psi_r(self, r, der=0): dq = q1 - q0 # geometric correction factor and its first derivative - gf_0 = np.sqrt(1 - (r / self.params["R0"]) ** 2) + gf_0 = xp.sqrt(1 - (r / self.params["R0"]) ** 2) gf_1 = -r / (self.params["R0"] ** 2 * gf_0) # safety factors @@ -1043,9 +1055,9 @@ def psi_r(self, r, der=0): q_bar_1 = q_1 * gf_0 + q_0 * gf_1 if der == 0: - out = -self.params["B0"] * self.params["a"] ** 2 / np.sqrt(dq * q0 * eps**2 + dq**2) - out *= np.arctanh( - np.sqrt((dq - dq * (r / self.params["R0"]) ** 2) / (q0 * eps**2 + dq)), + out = -self.params["B0"] * self.params["a"] ** 2 / xp.sqrt(dq * q0 * eps**2 + dq**2) + out *= xp.arctanh( + xp.sqrt((dq - dq * (r / self.params["R0"]) ** 2) / (q0 * eps**2 + dq)), ) elif der == 1: out = self.params["B0"] * r / q_bar_0 @@ -1115,10 +1127,10 @@ def q_r(self, r, der=0): r_flat = r.flatten() - r_zeros = np.where(r_flat == 0.0)[0] - r_nzero = np.where(r_flat != 0.0)[0] + r_zeros = xp.where(r_flat == 0.0)[0] + r_nzero = xp.where(r_flat != 0.0)[0] - qout = np.zeros(r_flat.size, dtype=float) + qout = xp.zeros(r_flat.size, dtype=float) if der == 0: if self.params["q0"] == self.params["q1"]: @@ -1211,7 +1223,7 @@ def plot_profiles(self, n_pts=501): import matplotlib.pyplot as plt - r = np.linspace(0.0, self.params["a"], n_pts) + r = xp.linspace(0.0, self.params["a"], n_pts) fig, ax = plt.subplots(2, 2) @@ -1245,7 +1257,7 @@ def plot_profiles(self, n_pts=501): def psi(self, R, Z, dR=0, dZ=0): """Poloidal flux function psi = psi(R, Z).""" - r = np.sqrt(Z**2 + (R - self.params["R0"]) ** 2) + r = xp.sqrt(Z**2 + (R - self.params["R0"]) ** 2) if dR == 0 and dZ == 0: out = self.psi_r(r, der=0) @@ -1293,7 +1305,7 @@ def g_tor(self, R, Z, dR=0, dZ=0): def p_xyz(self, x, y, z): """Pressure p = p(x, y, z).""" - r = np.sqrt((np.sqrt(x**2 + y**2) - self._params["R0"]) ** 2 + z**2) + r = xp.sqrt((xp.sqrt(x**2 + y**2) - self._params["R0"]) ** 2 + z**2) pp = self.p_r(r) @@ -1301,7 +1313,7 @@ def p_xyz(self, x, y, z): def n_xyz(self, x, y, z): """Number density n = n(x, y, z).""" - r = np.sqrt((np.sqrt(x**2 + y**2) - self._params["R0"]) ** 2 + z**2) + r = xp.sqrt((xp.sqrt(x**2 + y**2) - self._params["R0"]) ** 2 + z**2) nn = self.n_r(r) @@ -1429,10 +1441,10 @@ def __init__( self.params = copy.deepcopy(locals()) # plasma boundary contour - ths = np.linspace(0.0, 2 * np.pi, 201) + ths = xp.linspace(0.0, 2 * xp.pi, 201) - self._rbs = self.params["R0"] * (1 + self.params["a"] / self.params["R0"] * np.cos(ths)) - self._zbs = self.params["a"] * np.sin(ths) + self._rbs = self.params["R0"] * (1 + self.params["a"] / self.params["R0"] * xp.cos(ths)) + self._zbs = self.params["a"] * xp.sin(ths) # on-axis flux (arbitrary value) self._psi0 = -10.0 @@ -1453,12 +1465,12 @@ def dpsi_dr(psi, r, psi1): q = q0 + psi_norm * (q1 - q0 + (q1p - q1 + q0) * (1 - psi_s) * (psi_norm - 1) / (psi_norm - psi_s)) - out = B0 * r / (q * np.sqrt(1 - r**2 / R0**2)) + out = B0 * r / (q * xp.sqrt(1 - r**2 / R0**2)) return out # solve differential equation and fix boundary flux - r_i = np.linspace(0.0, self.params["a"], self.params["psi_nel"] + 1) + r_i = xp.linspace(0.0, self.params["a"], self.params["psi_nel"] + 1) def fun(psi1): out = odeint(dpsi_dr, self._psi0, r_i, args=(psi1,)).flatten() @@ -1545,13 +1557,13 @@ def p_psi(self, psi, der=0): psi_norm = (psi - self._psi0) / (self._psi1 - self._psi0) if der == 0: - out = self.params["beta"] * self.params["B0"] ** 2 / 2.0 * np.exp(-psi_norm / p1) + out = self.params["beta"] * self.params["B0"] ** 2 / 2.0 * xp.exp(-psi_norm / p1) else: out = ( -self.params["beta"] * self.params["B0"] ** 2 / 2.0 - * np.exp(-psi_norm / p1) + * xp.exp(-psi_norm / p1) / (p1 * (self._psi1 - self._psi0)) ) @@ -1580,8 +1592,8 @@ def plot_profiles(self, n_pts=501): import matplotlib.pyplot as plt - r = np.linspace(0.0, self.params["a"], n_pts) - psi = np.linspace(self._psi0, self._psi1, n_pts) + r = xp.linspace(0.0, self.params["a"], n_pts) + psi = xp.linspace(self._psi0, self._psi1, n_pts) fig, ax = plt.subplots(2, 2) @@ -1615,7 +1627,7 @@ def plot_profiles(self, n_pts=501): def psi(self, R, Z, dR=0, dZ=0): """Poloidal flux function psi = psi(R, Z).""" - r = np.sqrt(Z**2 + (R - self.params["R0"]) ** 2) + r = xp.sqrt(Z**2 + (R - self.params["R0"]) ** 2) if dR == 0 and dZ == 0: out = self.psi_r(r, der=0) @@ -1659,13 +1671,13 @@ def g_tor(self, R, Z, dR=0, dZ=0): def p_xyz(self, x, y, z): """Pressure p = p(x, y, z).""" - r = np.sqrt((np.sqrt(x**2 + y**2) - self._params["R0"]) ** 2 + z**2) + r = xp.sqrt((xp.sqrt(x**2 + y**2) - self._params["R0"]) ** 2 + z**2) return self.p_psi(self.psi_r(r)) def n_xyz(self, x, y, z): """Number density n = n(x, y, z).""" - r = np.sqrt((np.sqrt(x**2 + y**2) - self._params["R0"]) ** 2 + z**2) + r = xp.sqrt((xp.sqrt(x**2 + y**2) - self._params["R0"]) ** 2 + z**2) return self.n_psi(self.psi_r(r)) @@ -1803,8 +1815,8 @@ def __init__( self._r_range = [rleft, rleft + rdim] self._z_range = [zmid - zdim / 2, zmid + zdim / 2] - R = np.linspace(self._r_range[0], self._r_range[1], nR) - Z = np.linspace(self._z_range[0], self._z_range[1], nZ) + R = xp.linspace(self._r_range[0], self._r_range[1], nR) + Z = xp.linspace(self._z_range[0], self._z_range[1], nZ) smooth_steps = [ int(1 / (self.params["psi_resolution"][0] * 0.01)), @@ -1834,7 +1846,7 @@ def __init__( self._psi1 = psi_edge # interpolate toroidal field function, pressure profile and q-profile on unifrom flux grid from axis to boundary - flux_grid = np.linspace(self._psi0, self._psi1, g_profile.size) + flux_grid = xp.linspace(self._psi0, self._psi1, g_profile.size) smooth_step = int(1 / (self.params["flux_resolution"] * 0.01)) @@ -2006,7 +2018,7 @@ def g_tor(self, R, Z, dR=0, dZ=0): def p_xyz(self, x, y, z): """Pressure p = p(x, y, z) in units 1 Tesla^2/mu_0.""" - R = np.sqrt(x**2 + y**2) + R = xp.sqrt(x**2 + y**2) Z = 1 * z out = self.p_psi(self.psi(R, Z)) @@ -2019,7 +2031,7 @@ def p_xyz(self, x, y, z): def n_xyz(self, x, y, z): """Number density in physical space. Units from parameter file.""" - R = np.sqrt(x**2 + y**2) + R = xp.sqrt(x**2 + y**2) Z = 1 * z out = self.n_psi(self.psi(R, Z)) @@ -2199,9 +2211,9 @@ def bv(self, *etas, squeeze_out=False): bt += "_B" bz += "_B" self.state.compute(ev, bt, bz) - bv_2 = getattr(ev, bt).data / (2 * np.pi) - bv_3 = getattr(ev, bz).data / (2 * np.pi) * self._nfp - out = (np.zeros_like(bv_2), bv_2, bv_3) + bv_2 = getattr(ev, bt).data / (2 * xp.pi) + bv_3 = getattr(ev, bz).data / (2 * xp.pi) * self._nfp + out = (xp.zeros_like(bv_2), bv_2, bv_3) # apply struphy units for o in out: @@ -2219,8 +2231,8 @@ def jv(self, *etas, squeeze_out=False): self.state.compute(ev, jr, jt, jz) rmin = self._params["rmin"] jv_1 = ev.J_contra_r.data / (1.0 - rmin) - jv_2 = ev.J_contra_t.data / (2 * np.pi) - jv_3 = ev.J_contra_z.data / (2 * np.pi) * self._nfp + jv_2 = ev.J_contra_t.data / (2 * xp.pi) + jv_3 = ev.J_contra_z.data / (2 * xp.pi) * self._nfp if self.params["use_boozer"]: warnings.warn("GVEC current density in Boozer coords not yet implemented, set to zero.") # jr += "_B" @@ -2245,11 +2257,11 @@ def p0(self, *etas, squeeze_out=False): if not flat_eval: eta2 = etas[1] eta3 = etas[2] - if isinstance(eta2, np.ndarray): + if isinstance(eta2, xp.ndarray): if eta2.ndim == 3: eta2 = eta2[0, :, 0] eta3 = eta3[0, 0, :] - tmp, _1, _2 = np.meshgrid(ev.p.data, eta2, eta3, indexing="ij") + tmp, _1, _2 = xp.meshgrid(ev.p.data, eta2, eta3, indexing="ij") else: tmp = ev.p.data @@ -2309,7 +2321,7 @@ def _gvec_evaluations(self, *etas): etas = list(etas) for i, eta in enumerate(etas): if isinstance(eta, (float, int)): - etas[i] = np.array((eta,)) + etas[i] = xp.array((eta,)) assert etas[0].ndim == etas[1].ndim == etas[2].ndim if etas[0].ndim == 1: eta1 = etas[0] @@ -2326,8 +2338,8 @@ def _gvec_evaluations(self, *etas): # gvec coordinates rho = rmin + eta1 * (1.0 - rmin) - theta = 2 * np.pi * eta2 - zeta = 2 * np.pi * eta3 + theta = 2 * xp.pi * eta2 + zeta = 2 * xp.pi * eta3 # evaluate if self.params["use_boozer"]: @@ -2497,7 +2509,7 @@ def bv(self, *etas, squeeze_out=False): li = [] for gi, ei in zip(grid, etas): if gi.shape == ei.shape: - li += [np.allclose(gi, ei)] + li += [xp.allclose(gi, ei)] else: li += [False] if all(li): @@ -2549,9 +2561,9 @@ def _eval_bv(self, *etas, squeeze_out=False): if var == "B^rho": tmp /= 1.0 - self.rmin elif var == "B^theta": - tmp /= 2.0 * np.pi + tmp /= 2.0 * xp.pi elif var == "B^zeta": - tmp /= 2.0 * np.pi / nfp + tmp /= 2.0 * xp.pi / nfp # adjust for Struphy units tmp /= self.units["B"] / self.units["x"] out += [tmp] @@ -2570,7 +2582,7 @@ def jv(self, *etas, squeeze_out=False): li = [] for gi, ei in zip(grid, etas): if gi.shape == ei.shape: - li += [np.allclose(gi, ei)] + li += [xp.allclose(gi, ei)] else: li += [False] if all(li): @@ -2622,9 +2634,9 @@ def _eval_jv(self, *etas, squeeze_out=False): if var == "J^rho": tmp /= 1.0 - self.rmin elif var == "J^theta": - tmp /= 2.0 * np.pi + tmp /= 2.0 * xp.pi elif var == "J^zeta": - tmp /= 2.0 * np.pi / nfp + tmp /= 2.0 * xp.pi / nfp # adjust for Struphy units tmp /= self.units["j"] / self.units["x"] out += [tmp] @@ -2694,7 +2706,7 @@ def gradB1(self, *etas, squeeze_out=False): li = [] for gi, ei in zip(grid, etas): if gi.shape == ei.shape: - li += [np.allclose(gi, ei)] + li += [xp.allclose(gi, ei)] else: li += [False] if all(li): @@ -2745,9 +2757,9 @@ def _eval_gradB1(self, *etas, squeeze_out=False): if var == "|B|_r": tmp *= 1.0 - self.rmin elif var == "|B|_t": - tmp *= 2.0 * np.pi + tmp *= 2.0 * xp.pi elif var == "|B|_z": - tmp *= 2.0 * np.pi / nfp + tmp *= 2.0 * xp.pi / nfp # adjust for Struphy units tmp /= self.units["B"] out += [tmp] @@ -2757,9 +2769,9 @@ def _eval_gradB1(self, *etas, squeeze_out=False): def desc_eval( self, var: str, - e1: np.ndarray, - e2: np.ndarray, - e3: np.ndarray, + e1: xp.ndarray, + e2: xp.ndarray, + e3: xp.ndarray, flat_eval: bool = False, nfp: int = 1, verbose: bool = False, @@ -2773,7 +2785,7 @@ def desc_eval( Desc equilibrium quantitiy to evaluate, from `https://desc-docs.readthedocs.io/en/latest/variables.html#list-of-variables`_. - e1, e2, e3 : np.ndarray + e1, e2, e3 : xp.ndarray Input grids, either 1d or 3d. flat_eval : bool @@ -2792,21 +2804,21 @@ def desc_eval( warnings.filterwarnings("ignore") ttime = time() # Fix issue 353 with float dummy etas - e1 = np.array([e1]) if isinstance(e1, float) else e1 - e2 = np.array([e2]) if isinstance(e2, float) else e2 - e3 = np.array([e3]) if isinstance(e3, float) else e3 + e1 = xp.array([e1]) if isinstance(e1, float) else e1 + e2 = xp.array([e2]) if isinstance(e2, float) else e2 + e3 = xp.array([e3]) if isinstance(e3, float) else e3 # transform input grids if e1.ndim == 3: assert e1.shape == e2.shape == e3.shape rho = self.rmin + e1[:, 0, 0] * (1.0 - self.rmin) - theta = 2 * np.pi * e2[0, :, 0] - zeta = 2 * np.pi * e3[0, 0, :] / nfp + theta = 2 * xp.pi * e2[0, :, 0] + zeta = 2 * xp.pi * e3[0, 0, :] / nfp else: assert e1.ndim == e2.ndim == e3.ndim == 1 rho = self.rmin + e1 * (1.0 - self.rmin) - theta = 2 * np.pi * e2 - zeta = 2 * np.pi * e3 / nfp + theta = 2 * xp.pi * e2 + zeta = 2 * xp.pi * e3 / nfp # eval type if flat_eval: @@ -2815,13 +2827,13 @@ def desc_eval( t = theta z = zeta else: - r, t, z = np.meshgrid(rho, theta, zeta, indexing="ij") + r, t, z = xp.meshgrid(rho, theta, zeta, indexing="ij") r = r.flatten() t = t.flatten() z = z.flatten() - nodes = np.stack((r, t, z)).T - grid_3d = Grid(nodes, spacing=np.ones_like(nodes), jitable=False) + nodes = xp.stack((r, t, z)).T + grid_3d = Grid(nodes, spacing=xp.ones_like(nodes), jitable=False) # compute output corresponding to the generated desc grid node_values = self.eq.compute( @@ -2862,9 +2874,9 @@ def desc_eval( )[0, 0, :] # make sure the desc grid is correct - assert np.all(rho == rho1) - assert np.all(theta == theta1) - assert np.all(zeta == zeta1) + assert xp.all(rho == rho1) + assert xp.all(theta == theta1) + assert xp.all(zeta == zeta1) if verbose: # import sys @@ -2887,7 +2899,7 @@ def desc_eval( print(f"{zeta1 = }") # make c-contiguous - out = np.ascontiguousarray(out) + out = xp.ascontiguousarray(out) print(f"desc_eval for {var}: {time() - ttime} seconds") return out @@ -2905,7 +2917,7 @@ def __init__( uz: float = 0.0, n: float = 1.0, n1: float = 0.0, - density_profile: str = "affine", + density_profile: str = "constant", p0: float = 1.0, ): # use params setter @@ -2935,12 +2947,12 @@ def n_xyz(self, x, y, z): elif self.params["density_profile"] == "affine": return self.params["n"] + self.params["n1"] * x elif self.params["density_profile"] == "gaussian_xy": - return self.params["n"] * np.exp(-(x**2 + y**2) / self.params["p0"]) + return self.params["n"] * xp.exp(-(x**2 + y**2) / self.params["p0"]) elif self.params["density_profile"] == "step_function_x": out = 1e-8 + 0 * x - # mask_x = np.logical_and(x < .6, x > .4) - # mask_y = np.logical_and(y < .6, y > .4) - # mask = np.logical_and(mask_x, mask_y) + # mask_x = xp.logical_and(x < .6, x > .4) + # mask_y = xp.logical_and(y < .6, y > .4) + # mask = xp.logical_and(mask_x, mask_y) mask = x < -2.0 out[mask] = self.params["n"] return out @@ -3266,7 +3278,7 @@ def plot_profiles(self, n_pts=501): import matplotlib.pyplot as plt - r = np.linspace(0.0, self.params["a"], n_pts) + r = xp.linspace(0.0, self.params["a"], n_pts) fig, ax = plt.subplots(1, 3) @@ -3277,7 +3289,7 @@ def plot_profiles(self, n_pts=501): ax[0].set_xlabel("r") ax[0].set_ylabel("q") - ax[0].plot(r, np.ones(r.size), "k--") + ax[0].plot(r, xp.ones(r.size), "k--") ax[1].plot(r, self.p_r(r)) ax[1].set_xlabel("r") @@ -3300,8 +3312,8 @@ def b_xyz(self, x, y, z): """Magnetic field.""" bz = 0 * x - by = np.tanh(z / self._params["delta"]) - bx = np.sqrt(1 - by**2) + by = xp.tanh(z / self._params["delta"]) + bx = xp.sqrt(1 - by**2) bxs = self._params["amp"] * bx bys = self._params["amp"] * by diff --git a/src/struphy/fields_background/generic.py b/src/struphy/fields_background/generic.py index f309f1eef..0b82d7e17 100644 --- a/src/struphy/fields_background/generic.py +++ b/src/struphy/fields_background/generic.py @@ -1,3 +1,5 @@ +import copy + from struphy.fields_background.base import ( CartesianFluidEquilibrium, CartesianFluidEquilibriumWithB, @@ -17,6 +19,9 @@ def __init__( p_xyz: callable = None, n_xyz: callable = None, ): + # use params setter + self.params = copy.deepcopy(locals()) + if u_xyz is None: u_xyz = lambda x, y, z: (0.0 * x, 0.0 * x, 0.0 * x) else: @@ -57,6 +62,9 @@ def __init__( b_xyz: callable = None, gradB_xyz: callable = None, ): + # use params setter + self.params = copy.deepcopy(locals()) + super().__init__(u_xyz=u_xyz, p_xyz=p_xyz, n_xyz=n_xyz) if b_xyz is None: diff --git a/src/struphy/fields_background/projected_equils.py b/src/struphy/fields_background/projected_equils.py index afd3d15fa..26fa4f9c8 100644 --- a/src/struphy/fields_background/projected_equils.py +++ b/src/struphy/fields_background/projected_equils.py @@ -1,3 +1,6 @@ +from psydac.linalg.block import BlockVector +from psydac.linalg.stencil import StencilVector + from struphy.feec.psydac_derham import Derham from struphy.fields_background.base import ( FluidEquilibrium, @@ -100,7 +103,7 @@ def absB3(self): return coeffs @property - def p3(self): + def p3(self) -> StencilVector: tmp = self._P3(self.equil.p3) coeffs = self._E3T.dot(tmp) coeffs.update_ghost_regions() @@ -256,7 +259,7 @@ def a1(self): # 2-forms # # ---------# @property - def b2(self): + def b2(self) -> BlockVector: tmp = self._P2([self.equil.b2_1, self.equil.b2_2, self.equil.b2_3]) coeffs = self._E2T.dot(tmp) coeffs.update_ghost_regions() diff --git a/src/struphy/fields_background/tests/test_desc_equil.py b/src/struphy/fields_background/tests/test_desc_equil.py index c8f1dc6fe..5aca31b8d 100644 --- a/src/struphy/fields_background/tests/test_desc_equil.py +++ b/src/struphy/fields_background/tests/test_desc_equil.py @@ -1,10 +1,9 @@ import importlib.util +import cunumpy as xp import pytest from matplotlib import pyplot as plt -from struphy.utils.arrays import xp as np - desc_spec = importlib.util.find_spec("desc") @@ -33,9 +32,9 @@ def test_desc_equil(do_plot=False): n2 = 9 n3 = 11 - e1 = np.linspace(0.0001, 1, n1) - e2 = np.linspace(0, 1, n2) - e3 = np.linspace(0, 1 - 1e-6, n3) + e1 = xp.linspace(0.0001, 1, n1) + e2 = xp.linspace(0, 1, n2) + e3 = xp.linspace(0, 1 - 1e-6, n3) # desc grid and evaluation vars = [ @@ -70,43 +69,43 @@ def test_desc_equil(do_plot=False): outs[nfp] = {} rho = rmin + e1 * (1.0 - rmin) - theta = 2 * np.pi * e2 - zeta = 2 * np.pi * e3 / nfp + theta = 2 * xp.pi * e2 + zeta = 2 * xp.pi * e3 / nfp - r, t, ze = np.meshgrid(rho, theta, zeta, indexing="ij") + r, t, ze = xp.meshgrid(rho, theta, zeta, indexing="ij") r = r.flatten() t = t.flatten() ze = ze.flatten() - nodes = np.stack((r, t, ze)).T - grid_3d = Grid(nodes, spacing=np.ones_like(nodes), jitable=False) + nodes = xp.stack((r, t, ze)).T + grid_3d = Grid(nodes, spacing=xp.ones_like(nodes), jitable=False) for var in vars: node_values = desc_eq.compute(var, grid=grid_3d, override_grid=False) if node_values[var].ndim == 1: out = node_values[var].reshape((rho.size, theta.size, zeta.size), order="C") - outs[nfp][var] = np.ascontiguousarray(out) + outs[nfp][var] = xp.ascontiguousarray(out) else: B = [] for i in range(3): Bcomp = node_values[var][:, i].reshape((rho.size, theta.size, zeta.size), order="C") - Bcomp = np.ascontiguousarray(Bcomp) + Bcomp = xp.ascontiguousarray(Bcomp) B += [Bcomp] outs[nfp][var + str(i + 1)] = Bcomp - outs[nfp][var] = np.sqrt(B[0] ** 2 + B[1] ** 2 + B[2] ** 2) + outs[nfp][var] = xp.sqrt(B[0] ** 2 + B[1] ** 2 + B[2] ** 2) - assert np.allclose(outs[nfp]["B1"], outs[nfp]["B_R"]) - assert np.allclose(outs[nfp]["B2"], outs[nfp]["B_phi"]) - assert np.allclose(outs[nfp]["B3"], outs[nfp]["B_Z"]) + assert xp.allclose(outs[nfp]["B1"], outs[nfp]["B_R"]) + assert xp.allclose(outs[nfp]["B2"], outs[nfp]["B_phi"]) + assert xp.allclose(outs[nfp]["B3"], outs[nfp]["B_Z"]) - assert np.allclose(outs[nfp]["J1"], outs[nfp]["J_R"]) - assert np.allclose(outs[nfp]["J2"], outs[nfp]["J_phi"]) - assert np.allclose(outs[nfp]["J3"], outs[nfp]["J_Z"]) + assert xp.allclose(outs[nfp]["J1"], outs[nfp]["J_R"]) + assert xp.allclose(outs[nfp]["J2"], outs[nfp]["J_phi"]) + assert xp.allclose(outs[nfp]["J3"], outs[nfp]["J_Z"]) - outs[nfp]["Bx"] = np.cos(outs[nfp]["phi"]) * outs[nfp]["B_R"] - np.sin(outs[nfp]["phi"]) * outs[nfp]["B_phi"] + outs[nfp]["Bx"] = xp.cos(outs[nfp]["phi"]) * outs[nfp]["B_R"] - xp.sin(outs[nfp]["phi"]) * outs[nfp]["B_phi"] - outs[nfp]["By"] = np.sin(outs[nfp]["phi"]) * outs[nfp]["B_R"] + np.cos(outs[nfp]["phi"]) * outs[nfp]["B_phi"] + outs[nfp]["By"] = xp.sin(outs[nfp]["phi"]) * outs[nfp]["B_R"] + xp.cos(outs[nfp]["phi"]) * outs[nfp]["B_phi"] outs[nfp]["Bz"] = outs[nfp]["B_Z"] @@ -123,32 +122,32 @@ def test_desc_equil(do_plot=False): outs_struphy[nfp]["Y"] = y outs_struphy[nfp]["Z"] = z - outs_struphy[nfp]["R"] = np.sqrt(x**2 + y**2) - tmp = np.arctan2(y, x) - tmp[tmp < -1e-6] += 2 * np.pi + outs_struphy[nfp]["R"] = xp.sqrt(x**2 + y**2) + tmp = xp.arctan2(y, x) + tmp[tmp < -1e-6] += 2 * xp.pi outs_struphy[nfp]["phi"] = tmp - outs_struphy[nfp]["sqrt(g)"] = s_eq.domain.jacobian_det(e1, e2, e3) / (4 * np.pi**2 / nfp) + outs_struphy[nfp]["sqrt(g)"] = s_eq.domain.jacobian_det(e1, e2, e3) / (4 * xp.pi**2 / nfp) outs_struphy[nfp]["p"] = s_eq.p0(e1, e2, e3) # include push forward to DESC logical coordinates bv = s_eq.bv(e1, e2, e3) outs_struphy[nfp]["B^rho"] = bv[0] * (1 - rmin) - outs_struphy[nfp]["B^theta"] = bv[1] * 2 * np.pi - outs_struphy[nfp]["B^zeta"] = bv[2] * 2 * np.pi / nfp + outs_struphy[nfp]["B^theta"] = bv[1] * 2 * xp.pi + outs_struphy[nfp]["B^zeta"] = bv[2] * 2 * xp.pi / nfp outs_struphy[nfp]["B"] = s_eq.absB0(e1, e2, e3) # include push forward to DESC logical coordinates jv = s_eq.jv(e1, e2, e3) outs_struphy[nfp]["J^rho"] = jv[0] * (1 - rmin) - outs_struphy[nfp]["J^theta"] = jv[1] * 2 * np.pi - outs_struphy[nfp]["J^zeta"] = jv[2] * 2 * np.pi / nfp + outs_struphy[nfp]["J^theta"] = jv[1] * 2 * xp.pi + outs_struphy[nfp]["J^zeta"] = jv[2] * 2 * xp.pi / nfp j1 = s_eq.j1(e1, e2, e3) - outs_struphy[nfp]["J"] = np.sqrt(jv[0] * j1[0] + jv[1] * j1[1] + jv[2] * j1[2]) + outs_struphy[nfp]["J"] = xp.sqrt(jv[0] * j1[0] + jv[1] * j1[1] + jv[2] * j1[2]) b_cart, xyz = s_eq.b_cart(e1, e2, e3) outs_struphy[nfp]["Bx"] = b_cart[0] @@ -158,8 +157,8 @@ def test_desc_equil(do_plot=False): # include push forward to DESC logical coordinates gradB1 = s_eq.gradB1(e1, e2, e3) outs_struphy[nfp]["|B|_r"] = gradB1[0] / (1 - rmin) - outs_struphy[nfp]["|B|_t"] = gradB1[1] / (2 * np.pi) - outs_struphy[nfp]["|B|_z"] = gradB1[2] / (2 * np.pi / nfp) + outs_struphy[nfp]["|B|_t"] = gradB1[1] / (2 * xp.pi) + outs_struphy[nfp]["|B|_z"] = gradB1[2] / (2 * xp.pi / nfp) # comparisons vars += ["Bx", "By", "Bz"] @@ -173,10 +172,10 @@ def test_desc_equil(do_plot=False): if var in ("B_R", "B_phi", "B_Z", "J_R", "J_phi", "J_Z"): continue else: - max_norm = np.max(np.abs(outs[nfp][var])) + max_norm = xp.max(xp.abs(outs[nfp][var])) if max_norm < 1e-16: max_norm = 1.0 - err = np.max(np.abs(outs[nfp][var] - outs_struphy[nfp][var])) / max_norm + err = xp.max(xp.abs(outs[nfp][var] - outs_struphy[nfp][var])) / max_norm assert err < err_lim print( @@ -186,7 +185,7 @@ def test_desc_equil(do_plot=False): if do_plot: fig = plt.figure(figsize=(12, 13)) - levels = np.linspace(np.min(outs[nfp][var]) - 1e-10, np.max(outs[nfp][var]), 20) + levels = xp.linspace(xp.min(outs[nfp][var]) - 1e-10, xp.max(outs[nfp][var]), 20) # poloidal plot R = outs[nfp]["R"][:, :, 0].squeeze() diff --git a/src/struphy/fields_background/tests/test_generic_equils.py b/src/struphy/fields_background/tests/test_generic_equils.py index 8c10eb80e..77ca8baaa 100644 --- a/src/struphy/fields_background/tests/test_generic_equils.py +++ b/src/struphy/fields_background/tests/test_generic_equils.py @@ -1,3 +1,4 @@ +import cunumpy as xp import pytest from matplotlib import pyplot as plt @@ -5,12 +6,11 @@ GenericCartesianFluidEquilibrium, GenericCartesianFluidEquilibriumWithB, ) -from struphy.utils.arrays import xp as np def test_generic_equils(show=False): - fun_vec = lambda x, y, z: (np.cos(2 * np.pi * x), np.cos(2 * np.pi * y), z) - fun_n = lambda x, y, z: np.exp(-((x - 1) ** 2) - (y) ** 2) + fun_vec = lambda x, y, z: (xp.cos(2 * xp.pi * x), xp.cos(2 * xp.pi * y), z) + fun_n = lambda x, y, z: xp.exp(-((x - 1) ** 2) - (y) ** 2) fun_p = lambda x, y, z: x**2 gen_eq = GenericCartesianFluidEquilibrium( u_xyz=fun_vec, @@ -25,22 +25,22 @@ def test_generic_equils(show=False): gradB_xyz=fun_vec, ) - x = np.linspace(-3, 3, 32) - y = np.linspace(-4, 4, 32) + x = xp.linspace(-3, 3, 32) + y = xp.linspace(-4, 4, 32) z = 1.0 - xx, yy, zz = np.meshgrid(x, y, z) + xx, yy, zz = xp.meshgrid(x, y, z) # gen_eq - assert all([np.all(tmp == fun_i) for tmp, fun_i in zip(gen_eq.u_xyz(xx, yy, zz), fun_vec(xx, yy, zz))]) - assert np.all(gen_eq.p_xyz(xx, yy, zz) == fun_p(xx, yy, zz)) - assert np.all(gen_eq.n_xyz(xx, yy, zz) == fun_n(xx, yy, zz)) + assert all([xp.all(tmp == fun_i) for tmp, fun_i in zip(gen_eq.u_xyz(xx, yy, zz), fun_vec(xx, yy, zz))]) + assert xp.all(gen_eq.p_xyz(xx, yy, zz) == fun_p(xx, yy, zz)) + assert xp.all(gen_eq.n_xyz(xx, yy, zz) == fun_n(xx, yy, zz)) # gen_eq_B - assert all([np.all(tmp == fun_i) for tmp, fun_i in zip(gen_eq_B.u_xyz(xx, yy, zz), fun_vec(xx, yy, zz))]) - assert np.all(gen_eq_B.p_xyz(xx, yy, zz) == fun_p(xx, yy, zz)) - assert np.all(gen_eq_B.n_xyz(xx, yy, zz) == fun_n(xx, yy, zz)) - assert all([np.all(tmp == fun_i) for tmp, fun_i in zip(gen_eq_B.b_xyz(xx, yy, zz), fun_vec(xx, yy, zz))]) - assert all([np.all(tmp == fun_i) for tmp, fun_i in zip(gen_eq_B.gradB_xyz(xx, yy, zz), fun_vec(xx, yy, zz))]) + assert all([xp.all(tmp == fun_i) for tmp, fun_i in zip(gen_eq_B.u_xyz(xx, yy, zz), fun_vec(xx, yy, zz))]) + assert xp.all(gen_eq_B.p_xyz(xx, yy, zz) == fun_p(xx, yy, zz)) + assert xp.all(gen_eq_B.n_xyz(xx, yy, zz) == fun_n(xx, yy, zz)) + assert all([xp.all(tmp == fun_i) for tmp, fun_i in zip(gen_eq_B.b_xyz(xx, yy, zz), fun_vec(xx, yy, zz))]) + assert all([xp.all(tmp == fun_i) for tmp, fun_i in zip(gen_eq_B.gradB_xyz(xx, yy, zz), fun_vec(xx, yy, zz))]) if show: plt.figure(figsize=(12, 12)) diff --git a/src/struphy/fields_background/tests/test_mhd_equils.py b/src/struphy/fields_background/tests/test_mhd_equils.py index bd750e9d9..494d707b3 100644 --- a/src/struphy/fields_background/tests/test_mhd_equils.py +++ b/src/struphy/fields_background/tests/test_mhd_equils.py @@ -1,7 +1,7 @@ +import cunumpy as xp import pytest from struphy.fields_background import equils -from struphy.utils.arrays import xp as np @pytest.mark.parametrize( @@ -9,44 +9,44 @@ [ ("HomogenSlab", {}, "Cuboid", {}), ("HomogenSlab", {}, "Colella", {"alpha": 0.06}), - ("ShearedSlab", {"a": 0.75, "R0": 3.5}, "Cuboid", {"r1": 0.75, "r2": 2 * np.pi * 0.75, "r3": 2 * np.pi * 3.5}), + ("ShearedSlab", {"a": 0.75, "R0": 3.5}, "Cuboid", {"r1": 0.75, "r2": 2 * xp.pi * 0.75, "r3": 2 * xp.pi * 3.5}), ( "ShearedSlab", {"a": 0.75, "R0": 3.5, "q0": "inf", "q1": "inf"}, "Cuboid", - {"r1": 0.75, "r2": 2 * np.pi * 0.75, "r3": 2 * np.pi * 3.5}, + {"r1": 0.75, "r2": 2 * xp.pi * 0.75, "r3": 2 * xp.pi * 3.5}, ), ( "ShearedSlab", {"a": 0.55, "R0": 4.5}, "Orthogonal", - {"Lx": 0.55, "Ly": 2 * np.pi * 0.55, "Lz": 2 * np.pi * 4.5}, + {"Lx": 0.55, "Ly": 2 * xp.pi * 0.55, "Lz": 2 * xp.pi * 4.5}, ), - ("ScrewPinch", {"a": 0.45, "R0": 2.5}, "HollowCylinder", {"a1": 0.05, "a2": 0.45, "Lz": 2 * np.pi * 2.5}), - ("ScrewPinch", {"a": 1.45, "R0": 6.5}, "IGAPolarCylinder", {"a": 1.45, "Lz": 2 * np.pi * 6.5}), + ("ScrewPinch", {"a": 0.45, "R0": 2.5}, "HollowCylinder", {"a1": 0.05, "a2": 0.45, "Lz": 2 * xp.pi * 2.5}), + ("ScrewPinch", {"a": 1.45, "R0": 6.5}, "IGAPolarCylinder", {"a": 1.45, "Lz": 2 * xp.pi * 6.5}), ( "ScrewPinch", {"a": 0.45, "R0": 2.5, "q0": 1.5, "q1": 1.5}, "HollowCylinder", - {"a1": 0.05, "a2": 0.45, "Lz": 2 * np.pi * 2.5}, + {"a1": 0.05, "a2": 0.45, "Lz": 2 * xp.pi * 2.5}, ), ( "ScrewPinch", {"a": 1.45, "R0": 6.5, "q0": 1.5, "q1": 1.5}, "IGAPolarCylinder", - {"a": 1.45, "Lz": 2 * np.pi * 6.5}, + {"a": 1.45, "Lz": 2 * xp.pi * 6.5}, ), ( "ScrewPinch", {"a": 0.45, "R0": 2.5, "q0": "inf", "q1": "inf"}, "HollowCylinder", - {"a1": 0.05, "a2": 0.45, "Lz": 2 * np.pi * 2.5}, + {"a1": 0.05, "a2": 0.45, "Lz": 2 * xp.pi * 2.5}, ), ( "ScrewPinch", {"a": 1.45, "R0": 6.5, "q0": "inf", "q1": "inf"}, "IGAPolarCylinder", - {"a": 1.45, "Lz": 2 * np.pi * 6.5}, + {"a": 1.45, "Lz": 2 * xp.pi * 6.5}, ), ( "AdhocTorus", @@ -141,24 +141,24 @@ def test_equils(equil_domain_pair): from struphy.geometry import domains # logical evalution point - pt = (np.random.rand(), np.random.rand(), np.random.rand()) + pt = (xp.random.rand(), xp.random.rand(), xp.random.rand()) # logical arrays: - e1 = np.random.rand(4) - e2 = np.random.rand(5) - e3 = np.random.rand(6) + e1 = xp.random.rand(4) + e2 = xp.random.rand(5) + e3 = xp.random.rand(6) # 2d slices - mat_12_1, mat_12_2 = np.meshgrid(e1, e2, indexing="ij") - mat_13_1, mat_13_3 = np.meshgrid(e1, e3, indexing="ij") - mat_23_2, mat_23_3 = np.meshgrid(e2, e3, indexing="ij") + mat_12_1, mat_12_2 = xp.meshgrid(e1, e2, indexing="ij") + mat_13_1, mat_13_3 = xp.meshgrid(e1, e3, indexing="ij") + mat_23_2, mat_23_3 = xp.meshgrid(e2, e3, indexing="ij") # 3d - mat_123_1, mat_123_2, mat_123_3 = np.meshgrid(e1, e2, e3, indexing="ij") - mat_123_1_sp, mat_123_2_sp, mat_123_3_sp = np.meshgrid(e1, e2, e3, indexing="ij", sparse=True) + mat_123_1, mat_123_2, mat_123_3 = xp.meshgrid(e1, e2, e3, indexing="ij") + mat_123_1_sp, mat_123_2_sp, mat_123_3_sp = xp.meshgrid(e1, e2, e3, indexing="ij", sparse=True) # markers - markers = np.random.rand(33, 10) + markers = xp.random.rand(33, 10) # create MHD equilibrium eq_mhd = getattr(equils, equil_domain_pair[0])(**equil_domain_pair[1]) @@ -274,8 +274,8 @@ def test_equils(equil_domain_pair): # --------- eta1 evaluation --------- results = [] - e2_pt = np.random.rand() - e3_pt = np.random.rand() + e2_pt = xp.random.rand() + e3_pt = xp.random.rand() # scalar functions results.append(eq_mhd.absB0(e1, e2_pt, e3_pt, squeeze_out=True)) @@ -321,8 +321,8 @@ def test_equils(equil_domain_pair): # --------- eta2 evaluation --------- results = [] - e1_pt = np.random.rand() - e3_pt = np.random.rand() + e1_pt = xp.random.rand() + e3_pt = xp.random.rand() # scalar functions results.append(eq_mhd.absB0(e1_pt, e2, e3_pt, squeeze_out=True)) @@ -370,8 +370,8 @@ def test_equils(equil_domain_pair): # --------- eta3 evaluation --------- results = [] - e1_pt = np.random.rand() - e2_pt = np.random.rand() + e1_pt = xp.random.rand() + e2_pt = xp.random.rand() # scalar functions results.append(eq_mhd.absB0(e1_pt, e2_pt, e3, squeeze_out=True)) @@ -419,7 +419,7 @@ def test_equils(equil_domain_pair): # --------- eta1-eta2 evaluation --------- results = [] - e3_pt = np.random.rand() + e3_pt = xp.random.rand() # scalar functions results.append(eq_mhd.absB0(e1, e2, e3_pt, squeeze_out=True)) @@ -467,7 +467,7 @@ def test_equils(equil_domain_pair): # --------- eta1-eta3 evaluation --------- results = [] - e2_pt = np.random.rand() + e2_pt = xp.random.rand() # scalar functions results.append(eq_mhd.absB0(e1, e2_pt, e3, squeeze_out=True)) @@ -515,7 +515,7 @@ def test_equils(equil_domain_pair): # --------- eta2-eta3 evaluation --------- results = [] - e1_pt = np.random.rand() + e1_pt = xp.random.rand() # scalar functions results.append(eq_mhd.absB0(e1_pt, e2, e3, squeeze_out=True)) @@ -609,7 +609,7 @@ def test_equils(equil_domain_pair): # --------- 12 matrix evaluation --------- results = [] - e3_pt = np.random.rand() + e3_pt = xp.random.rand() # scalar functions results.append(eq_mhd.absB0(mat_12_1, mat_12_2, e3_pt, squeeze_out=True)) @@ -657,7 +657,7 @@ def test_equils(equil_domain_pair): # --------- 13 matrix evaluation --------- results = [] - e2_pt = np.random.rand() + e2_pt = xp.random.rand() # scalar functions results.append(eq_mhd.absB0(mat_13_1, e2_pt, mat_13_3, squeeze_out=True)) @@ -705,7 +705,7 @@ def test_equils(equil_domain_pair): # --------- 23 matrix evaluation --------- results = [] - e1_pt = np.random.rand() + e1_pt = xp.random.rand() # scalar functions results.append(eq_mhd.absB0(e1_pt, mat_23_2, mat_23_3, squeeze_out=True)) @@ -848,22 +848,22 @@ def assert_scalar(result, kind, *etas): markers = etas[0] n_p = markers.shape[0] - assert isinstance(result, np.ndarray) + assert isinstance(result, xp.ndarray) assert result.shape == (n_p,) for ip in range(n_p): assert isinstance(result[ip], float) - assert not np.isnan(result[ip]) + assert not xp.isnan(result[ip]) else: # point-wise if kind == "point": assert isinstance(result, float) - assert not np.isnan(result) + assert not xp.isnan(result) # slices else: - assert isinstance(result, np.ndarray) + assert isinstance(result, xp.ndarray) # eta1-array if kind == "e1": @@ -915,27 +915,27 @@ def assert_vector(result, kind, *etas): markers = etas[0] n_p = markers.shape[0] - assert isinstance(result, np.ndarray) + assert isinstance(result, xp.ndarray) assert result.shape == (3, n_p) for c in range(3): for ip in range(n_p): assert isinstance(result[c, ip], float) - assert not np.isnan(result[c, ip]) + assert not xp.isnan(result[c, ip]) else: # point-wise if kind == "point": - assert isinstance(result, np.ndarray) + assert isinstance(result, xp.ndarray) assert result.shape == (3,) for c in range(3): assert isinstance(result[c], float) - assert not np.isnan(result[c]) + assert not xp.isnan(result[c]) # slices else: - assert isinstance(result, np.ndarray) + assert isinstance(result, xp.ndarray) # eta1-array if kind == "e1": diff --git a/src/struphy/fields_background/tests/test_numerical_mhd_equil.py b/src/struphy/fields_background/tests/test_numerical_mhd_equil.py index 4d34e8352..aa1278d5d 100644 --- a/src/struphy/fields_background/tests/test_numerical_mhd_equil.py +++ b/src/struphy/fields_background/tests/test_numerical_mhd_equil.py @@ -1,7 +1,7 @@ +import cunumpy as xp import pytest from struphy.fields_background.base import FluidEquilibrium, LogicalMHDequilibrium -from struphy.utils.arrays import xp as np @pytest.mark.parametrize( @@ -50,53 +50,53 @@ def test_transformations(mapping, mhd_equil): num_equil = NumEqTest(domain, proxy) # compare values: - eta1 = np.random.rand(4) - eta2 = np.random.rand(5) - eta3 = np.random.rand(6) + eta1 = xp.random.rand(4) + eta2 = xp.random.rand(5) + eta3 = xp.random.rand(6) - assert np.allclose(ana_equil.absB0(eta1, eta2, eta3), num_equil.absB0(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.absB0(eta1, eta2, eta3), num_equil.absB0(eta1, eta2, eta3)) - assert np.allclose(ana_equil.bv(eta1, eta2, eta3)[0], num_equil.bv(eta1, eta2, eta3)[0]) - assert np.allclose(ana_equil.bv(eta1, eta2, eta3)[1], num_equil.bv(eta1, eta2, eta3)[1]) - assert np.allclose(ana_equil.bv(eta1, eta2, eta3)[2], num_equil.bv(eta1, eta2, eta3)[2]) + assert xp.allclose(ana_equil.bv(eta1, eta2, eta3)[0], num_equil.bv(eta1, eta2, eta3)[0]) + assert xp.allclose(ana_equil.bv(eta1, eta2, eta3)[1], num_equil.bv(eta1, eta2, eta3)[1]) + assert xp.allclose(ana_equil.bv(eta1, eta2, eta3)[2], num_equil.bv(eta1, eta2, eta3)[2]) - assert np.allclose(ana_equil.b1_1(eta1, eta2, eta3), num_equil.b1_1(eta1, eta2, eta3)) - assert np.allclose(ana_equil.b1_2(eta1, eta2, eta3), num_equil.b1_2(eta1, eta2, eta3)) - assert np.allclose(ana_equil.b1_3(eta1, eta2, eta3), num_equil.b1_3(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.b1_1(eta1, eta2, eta3), num_equil.b1_1(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.b1_2(eta1, eta2, eta3), num_equil.b1_2(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.b1_3(eta1, eta2, eta3), num_equil.b1_3(eta1, eta2, eta3)) - assert np.allclose(ana_equil.b2_1(eta1, eta2, eta3), num_equil.b2_1(eta1, eta2, eta3)) - assert np.allclose(ana_equil.b2_2(eta1, eta2, eta3), num_equil.b2_2(eta1, eta2, eta3)) - assert np.allclose(ana_equil.b2_3(eta1, eta2, eta3), num_equil.b2_3(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.b2_1(eta1, eta2, eta3), num_equil.b2_1(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.b2_2(eta1, eta2, eta3), num_equil.b2_2(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.b2_3(eta1, eta2, eta3), num_equil.b2_3(eta1, eta2, eta3)) - assert np.allclose(ana_equil.unit_bv(eta1, eta2, eta3)[0], num_equil.unit_bv(eta1, eta2, eta3)[0]) - assert np.allclose(ana_equil.unit_bv(eta1, eta2, eta3)[1], num_equil.unit_bv(eta1, eta2, eta3)[1]) - assert np.allclose(ana_equil.unit_bv(eta1, eta2, eta3)[2], num_equil.unit_bv(eta1, eta2, eta3)[2]) + assert xp.allclose(ana_equil.unit_bv(eta1, eta2, eta3)[0], num_equil.unit_bv(eta1, eta2, eta3)[0]) + assert xp.allclose(ana_equil.unit_bv(eta1, eta2, eta3)[1], num_equil.unit_bv(eta1, eta2, eta3)[1]) + assert xp.allclose(ana_equil.unit_bv(eta1, eta2, eta3)[2], num_equil.unit_bv(eta1, eta2, eta3)[2]) - assert np.allclose(ana_equil.unit_b1_1(eta1, eta2, eta3), num_equil.unit_b1_1(eta1, eta2, eta3)) - assert np.allclose(ana_equil.unit_b1_2(eta1, eta2, eta3), num_equil.unit_b1_2(eta1, eta2, eta3)) - assert np.allclose(ana_equil.unit_b1_3(eta1, eta2, eta3), num_equil.unit_b1_3(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.unit_b1_1(eta1, eta2, eta3), num_equil.unit_b1_1(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.unit_b1_2(eta1, eta2, eta3), num_equil.unit_b1_2(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.unit_b1_3(eta1, eta2, eta3), num_equil.unit_b1_3(eta1, eta2, eta3)) - assert np.allclose(ana_equil.unit_b2_1(eta1, eta2, eta3), num_equil.unit_b2_1(eta1, eta2, eta3)) - assert np.allclose(ana_equil.unit_b2_2(eta1, eta2, eta3), num_equil.unit_b2_2(eta1, eta2, eta3)) - assert np.allclose(ana_equil.unit_b2_3(eta1, eta2, eta3), num_equil.unit_b2_3(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.unit_b2_1(eta1, eta2, eta3), num_equil.unit_b2_1(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.unit_b2_2(eta1, eta2, eta3), num_equil.unit_b2_2(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.unit_b2_3(eta1, eta2, eta3), num_equil.unit_b2_3(eta1, eta2, eta3)) - assert np.allclose(ana_equil.jv(eta1, eta2, eta3)[0], num_equil.jv(eta1, eta2, eta3)[0]) - assert np.allclose(ana_equil.jv(eta1, eta2, eta3)[1], num_equil.jv(eta1, eta2, eta3)[1]) - assert np.allclose(ana_equil.jv(eta1, eta2, eta3)[2], num_equil.jv(eta1, eta2, eta3)[2]) + assert xp.allclose(ana_equil.jv(eta1, eta2, eta3)[0], num_equil.jv(eta1, eta2, eta3)[0]) + assert xp.allclose(ana_equil.jv(eta1, eta2, eta3)[1], num_equil.jv(eta1, eta2, eta3)[1]) + assert xp.allclose(ana_equil.jv(eta1, eta2, eta3)[2], num_equil.jv(eta1, eta2, eta3)[2]) - assert np.allclose(ana_equil.j1_1(eta1, eta2, eta3), num_equil.j1_1(eta1, eta2, eta3)) - assert np.allclose(ana_equil.j1_2(eta1, eta2, eta3), num_equil.j1_2(eta1, eta2, eta3)) - assert np.allclose(ana_equil.j1_3(eta1, eta2, eta3), num_equil.j1_3(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.j1_1(eta1, eta2, eta3), num_equil.j1_1(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.j1_2(eta1, eta2, eta3), num_equil.j1_2(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.j1_3(eta1, eta2, eta3), num_equil.j1_3(eta1, eta2, eta3)) - assert np.allclose(ana_equil.j2_1(eta1, eta2, eta3), num_equil.j2_1(eta1, eta2, eta3)) - assert np.allclose(ana_equil.j2_2(eta1, eta2, eta3), num_equil.j2_2(eta1, eta2, eta3)) - assert np.allclose(ana_equil.j2_3(eta1, eta2, eta3), num_equil.j2_3(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.j2_1(eta1, eta2, eta3), num_equil.j2_1(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.j2_2(eta1, eta2, eta3), num_equil.j2_2(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.j2_3(eta1, eta2, eta3), num_equil.j2_3(eta1, eta2, eta3)) - assert np.allclose(ana_equil.p0(eta1, eta2, eta3), num_equil.p0(eta1, eta2, eta3)) - assert np.allclose(ana_equil.p3(eta1, eta2, eta3), num_equil.p3(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.p0(eta1, eta2, eta3), num_equil.p0(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.p3(eta1, eta2, eta3), num_equil.p3(eta1, eta2, eta3)) - assert np.allclose(ana_equil.n0(eta1, eta2, eta3), num_equil.n0(eta1, eta2, eta3)) - assert np.allclose(ana_equil.n3(eta1, eta2, eta3), num_equil.n3(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.n0(eta1, eta2, eta3), num_equil.n0(eta1, eta2, eta3)) + assert xp.allclose(ana_equil.n3(eta1, eta2, eta3), num_equil.n3(eta1, eta2, eta3)) class NumEqTest(LogicalMHDequilibrium): diff --git a/src/struphy/geometry/base.py b/src/struphy/geometry/base.py index 2a3141e96..a789913e7 100644 --- a/src/struphy/geometry/base.py +++ b/src/struphy/geometry/base.py @@ -3,6 +3,7 @@ from abc import ABCMeta, abstractmethod +import cunumpy as xp import h5py from scipy.sparse import csc_matrix, kron from scipy.sparse.linalg import splu, spsolve @@ -11,7 +12,6 @@ from struphy.geometry import evaluation_kernels, transform_kernels from struphy.kernel_arguments.pusher_args_kernels import DomainArguments from struphy.linear_algebra import linalg_kron -from struphy.utils.arrays import xp as np class Domain(metaclass=ABCMeta): @@ -56,12 +56,12 @@ def __init__( self._NbaseN = [Nel + p - kind * p for Nel, p, kind in zip(Nel, p, spl_kind)] - el_b = [np.linspace(0.0, 1.0, Nel + 1) for Nel in Nel] + el_b = [xp.linspace(0.0, 1.0, Nel + 1) for Nel in Nel] self._T = [bsp.make_knots(el_b, p, kind) for el_b, p, kind in zip(el_b, p, spl_kind)] self._indN = [ - (np.indices((Nel, p + 1))[1] + np.arange(Nel)[:, None]) % NbaseN + (xp.indices((Nel, p + 1))[1] + xp.arange(Nel)[:, None]) % NbaseN for Nel, p, NbaseN in zip(Nel, p, self._NbaseN) ] @@ -71,15 +71,15 @@ def __init__( self._p = (*self._p, 0) self._NbaseN = self._NbaseN + [0] - self._T = self._T + [np.zeros((1,), dtype=float)] + self._T = self._T + [xp.zeros((1,), dtype=float)] - self._indN = self._indN + [np.zeros((1, 1), dtype=int)] + self._indN = self._indN + [xp.zeros((1, 1), dtype=int)] # create dummy attributes for analytical mappings if self.kind_map >= 10: - self._cx = np.zeros((1, 1, 1), dtype=float) - self._cy = np.zeros((1, 1, 1), dtype=float) - self._cz = np.zeros((1, 1, 1), dtype=float) + self._cx = xp.zeros((1, 1, 1), dtype=float) + self._cy = xp.zeros((1, 1, 1), dtype=float) + self._cz = xp.zeros((1, 1, 1), dtype=float) self._transformation_ids = { "pull": 0, @@ -120,7 +120,7 @@ def __init__( self._args_domain = DomainArguments( self.kind_map, self.params_numpy, - np.array(self.p), + xp.array(self.p), self.T[0], self.T[1], self.T[2], @@ -165,15 +165,15 @@ def params(self, new): self._params = new @property - def params_numpy(self) -> np.ndarray: + def params_numpy(self) -> xp.ndarray: """Mapping parameters as numpy array (can be empty).""" if not hasattr(self, "_params_numpy"): - self._params_numpy = np.array([0], dtype=float) + self._params_numpy = xp.array([0], dtype=float) return self._params_numpy @params_numpy.setter def params_numpy(self, new): - assert isinstance(new, np.ndarray) + assert isinstance(new, xp.ndarray) assert new.ndim == 1 self._params_numpy = new @@ -768,7 +768,7 @@ def _evaluate_metric_coefficient(self, *etas, which=0, **kwargs): markers = etas[0] # to keep C-ordering the (3, 3)-part is in the last indices - out = np.empty((markers.shape[0], 3, 3), dtype=float) + out = xp.empty((markers.shape[0], 3, 3), dtype=float) n_inside = evaluation_kernels.kernel_evaluate_pic( markers, @@ -780,24 +780,24 @@ def _evaluate_metric_coefficient(self, *etas, which=0, **kwargs): ) # move the (3, 3)-part to front - out = np.transpose(out, axes=(1, 2, 0)) + out = xp.transpose(out, axes=(1, 2, 0)) # remove holes out = out[:, :, :n_inside] if transposed: - out = np.transpose(out, axes=(1, 0, 2)) + out = xp.transpose(out, axes=(1, 0, 2)) # change size of "out" depending on which metric coeff has been evaluated if which == 0 or which == -1: out = out[:, 0, :] if change_out_order: - out = np.transpose(out, axes=(1, 0)) + out = xp.transpose(out, axes=(1, 0)) elif which == 2: out = out[0, 0, :] else: if change_out_order: - out = np.transpose(out, axes=(2, 0, 1)) + out = xp.transpose(out, axes=(2, 0, 1)) # tensor-product/slice evaluation else: @@ -809,7 +809,7 @@ def _evaluate_metric_coefficient(self, *etas, which=0, **kwargs): ) # to keep C-ordering the (3, 3)-part is in the last indices - out = np.empty( + out = xp.empty( (E1.shape[0], E2.shape[1], E3.shape[2], 3, 3), dtype=float, ) @@ -825,20 +825,20 @@ def _evaluate_metric_coefficient(self, *etas, which=0, **kwargs): ) # move the (3, 3)-part to front - out = np.transpose(out, axes=(3, 4, 0, 1, 2)) + out = xp.transpose(out, axes=(3, 4, 0, 1, 2)) if transposed: - out = np.transpose(out, axes=(1, 0, 2, 3, 4)) + out = xp.transpose(out, axes=(1, 0, 2, 3, 4)) if which == 0: out = out[:, 0, :, :, :] if change_out_order: - out = np.transpose(out, axes=(1, 2, 3, 0)) + out = xp.transpose(out, axes=(1, 2, 3, 0)) elif which == 2: out = out[0, 0, :, :, :] else: if change_out_order: - out = np.transpose(out, axes=(2, 3, 4, 0, 1)) + out = xp.transpose(out, axes=(2, 3, 4, 0, 1)) # remove singleton dimensions for slice evaluation if squeeze_out: @@ -903,7 +903,7 @@ def _pull_push_transform(self, which, a, kind_fun, *etas, flat_eval=False, **kwa assert len(etas) == 3 assert etas[0].shape == etas[1].shape == etas[2].shape assert etas[0].ndim == 1 - markers = np.stack(etas, axis=1) + markers = xp.stack(etas, axis=1) else: markers = etas[0] @@ -955,7 +955,7 @@ def _pull_push_transform(self, which, a, kind_fun, *etas, flat_eval=False, **kwa A_has_holes = False # call evaluation kernel - out = np.empty((markers.shape[0], 3), dtype=float) + out = xp.empty((markers.shape[0], 3), dtype=float) # make sure we don't have stride = 0 A = A.copy() @@ -971,7 +971,7 @@ def _pull_push_transform(self, which, a, kind_fun, *etas, flat_eval=False, **kwa ) # move the (3, 3)-part to front - out = np.transpose(out, axes=(1, 0)) + out = xp.transpose(out, axes=(1, 0)) # remove holes out = out[:, :n_inside] @@ -985,7 +985,7 @@ def _pull_push_transform(self, which, a, kind_fun, *etas, flat_eval=False, **kwa out = out[0, :] else: if change_out_order: - out = np.transpose(out, axes=(1, 0)) + out = xp.transpose(out, axes=(1, 0)) # tensor-product/slice evaluation else: @@ -1012,7 +1012,7 @@ def _pull_push_transform(self, which, a, kind_fun, *etas, flat_eval=False, **kwa A = Domain.prepare_arg(a, X[0], X[1], X[2], a_kwargs=a_kwargs) # call evaluation kernel - out = np.empty( + out = xp.empty( (E1.shape[0], E2.shape[1], E3.shape[2], 3), dtype=float, ) @@ -1029,14 +1029,14 @@ def _pull_push_transform(self, which, a, kind_fun, *etas, flat_eval=False, **kwa ) # move the (3, 3)-part to front - out = np.transpose(out, axes=(3, 0, 1, 2)) + out = xp.transpose(out, axes=(3, 0, 1, 2)) # change output order if kind_int < 10: out = out[0, :, :, :] else: if change_out_order: - out = np.transpose(out, axes=(1, 2, 3, 0)) + out = xp.transpose(out, axes=(1, 2, 3, 0)) # remove singleton dimensions for slice evaluation if squeeze_out: @@ -1083,22 +1083,22 @@ def prepare_eval_pts(x, y, z, flat_eval=False): if flat_eval: # convert list type data to numpy array: if isinstance(x, list): - arg_x = np.array(x) - elif isinstance(x, np.ndarray): + arg_x = xp.array(x) + elif isinstance(x, xp.ndarray): arg_x = x else: raise ValueError("Input x must be a 1d list or numpy array") if isinstance(y, list): - arg_y = np.array(y) - elif isinstance(y, np.ndarray): + arg_y = xp.array(y) + elif isinstance(y, xp.ndarray): arg_y = y else: raise ValueError("Input y must be a 1d list or numpy array") if isinstance(z, list): - arg_z = np.array(z) - elif isinstance(z, np.ndarray): + arg_z = xp.array(z) + elif isinstance(z, xp.ndarray): arg_z = z else: raise ValueError("Input z must be a 1d list or numpy array") @@ -1117,56 +1117,56 @@ def prepare_eval_pts(x, y, z, flat_eval=False): else: # convert list type data to numpy array: if isinstance(x, float): - arg_x = np.array([x]) + arg_x = xp.array([x]) elif isinstance(x, int): - arg_x = np.array([float(x)]) + arg_x = xp.array([float(x)]) elif isinstance(x, list): - arg_x = np.array(x) - elif isinstance(x, np.ndarray): + arg_x = xp.array(x) + elif isinstance(x, xp.ndarray): arg_x = x.copy() else: raise ValueError(f"data type {type(x)} not supported") if isinstance(y, float): - arg_y = np.array([y]) + arg_y = xp.array([y]) elif isinstance(y, int): - arg_y = np.array([float(y)]) + arg_y = xp.array([float(y)]) elif isinstance(y, list): - arg_y = np.array(y) - elif isinstance(y, np.ndarray): + arg_y = xp.array(y) + elif isinstance(y, xp.ndarray): arg_y = y.copy() else: raise ValueError(f"data type {type(y)} not supported") if isinstance(z, float): - arg_z = np.array([z]) + arg_z = xp.array([z]) elif isinstance(z, int): - arg_z = np.array([float(z)]) + arg_z = xp.array([float(z)]) elif isinstance(z, list): - arg_z = np.array(z) - elif isinstance(z, np.ndarray): + arg_z = xp.array(z) + elif isinstance(z, xp.ndarray): arg_z = z.copy() else: raise ValueError(f"data type {type(z)} not supported") # tensor-product for given three 1D arrays if arg_x.ndim == 1 and arg_y.ndim == 1 and arg_z.ndim == 1: - E1, E2, E3 = np.meshgrid(arg_x, arg_y, arg_z, indexing="ij") + E1, E2, E3 = xp.meshgrid(arg_x, arg_y, arg_z, indexing="ij") # given xy-plane at point z: elif arg_x.ndim == 2 and arg_y.ndim == 2 and arg_z.size == 1: E1 = arg_x[:, :, None] E2 = arg_y[:, :, None] - E3 = arg_z * np.ones(E1.shape) + E3 = arg_z * xp.ones(E1.shape) # given xz-plane at point y: elif arg_x.ndim == 2 and arg_y.size == 1 and arg_z.ndim == 2: E1 = arg_x[:, None, :] - E2 = arg_y * np.ones(E1.shape) + E2 = arg_y * xp.ones(E1.shape) E3 = arg_z[:, None, :] # given yz-plane at point x: elif arg_x.size == 1 and arg_y.ndim == 2 and arg_z.ndim == 2: E2 = arg_y[None, :, :] E3 = arg_z[None, :, :] - E1 = arg_x * np.ones(E2.shape) + E1 = arg_x * xp.ones(E2.shape) # given three 3D arrays elif arg_x.ndim == 3 and arg_y.ndim == 3 and arg_z.ndim == 3: # Distinguish if input coordinates are from sparse or dense meshgrid. @@ -1224,7 +1224,7 @@ def prepare_arg(a_in, *Xs, is_sparse_meshgrid=False, a_kwargs={}): # float (point-wise, scalar function) if isinstance(a_in, float): - a_out = np.array([[[[a_in]]]]) + a_out = xp.array([[[[a_in]]]]) # single callable: # scalar function -> must return a 3d array for 3d evaluation points @@ -1237,7 +1237,7 @@ def prepare_arg(a_in, *Xs, is_sparse_meshgrid=False, a_kwargs={}): else: if is_sparse_meshgrid: a_out = a_in( - *np.meshgrid(Xs[0][:, 0, 0], Xs[1][0, :, 0], Xs[2][0, 0, :], indexing="ij"), + *xp.meshgrid(Xs[0][:, 0, 0], Xs[1][0, :, 0], Xs[2][0, 0, :], indexing="ij"), **a_kwargs, ) else: @@ -1245,7 +1245,7 @@ def prepare_arg(a_in, *Xs, is_sparse_meshgrid=False, a_kwargs={}): # case of Field.__call__ if isinstance(a_out, list): - a_out = np.array(a_out) + a_out = xp.array(a_out) if a_out.ndim == 3: a_out = a_out[None, :, :, :] @@ -1273,7 +1273,7 @@ def prepare_arg(a_in, *Xs, is_sparse_meshgrid=False, a_kwargs={}): if is_sparse_meshgrid: a_out += [ component( - *np.meshgrid( + *xp.meshgrid( Xs[0][:, 0, 0], Xs[1][0, :, 0], Xs[2][0, 0, :], @@ -1285,7 +1285,7 @@ def prepare_arg(a_in, *Xs, is_sparse_meshgrid=False, a_kwargs={}): else: a_out += [component(*Xs, **a_kwargs)] - elif isinstance(component, np.ndarray): + elif isinstance(component, xp.ndarray): if flat_eval: assert component.ndim == 1, print(f"{component.ndim = }") else: @@ -1294,16 +1294,16 @@ def prepare_arg(a_in, *Xs, is_sparse_meshgrid=False, a_kwargs={}): a_out += [component] elif isinstance(component, float): - a_out += [np.array([component])[:, None, None]] + a_out += [xp.array([component])[:, None, None]] - a_out = np.array(a_out, dtype=float) + a_out = xp.array(a_out, dtype=float) # numpy array: # 1d array (flat_eval=True and scalar input or flat_eval=False and length 1 (scalar) or length 3 (vector)) # 2d array (flat_eval=True and vector-valued input of shape (3,:)) # 3d array (flat_eval=False and scalar input) # 4d array (flat_eval=False and vector-valued input of shape (3,:,:,:)) - elif isinstance(a_in, np.ndarray): + elif isinstance(a_in, xp.ndarray): if flat_eval: if a_in.ndim == 1: a_out = a_in[None, :] @@ -1344,26 +1344,26 @@ def prepare_arg(a_in, *Xs, is_sparse_meshgrid=False, a_kwargs={}): if flat_eval: assert a_out.ndim == 2 assert a_out.shape[0] == 1 or a_out.shape[0] == 3 - a_out = np.ascontiguousarray(np.transpose(a_out, axes=(1, 0))).copy() # Make sure we don't have stride 0 + a_out = xp.ascontiguousarray(xp.transpose(a_out, axes=(1, 0))).copy() # Make sure we don't have stride 0 # make sure that output array is 4d and of shape (:,:,:, 1) or (:,:,:, 3) for tensor-product/slice evaluation else: assert a_out.ndim == 4 assert a_out.shape[0] == 1 or a_out.shape[0] == 3 - a_out = np.ascontiguousarray( - np.transpose(a_out, axes=(1, 2, 3, 0)), + a_out = xp.ascontiguousarray( + xp.transpose(a_out, axes=(1, 2, 3, 0)), ).copy() # Make sure we don't have stride 0 return a_out # ================================ - def get_params_numpy(self) -> np.ndarray: + def get_params_numpy(self) -> xp.ndarray: """Convert parameter dict into numpy array.""" params_numpy = [] for k, v in self.params.items(): params_numpy.append(v) - return np.array(params_numpy) + return xp.array(params_numpy) def show( self, @@ -1414,12 +1414,12 @@ def show( # plot domain without MPI decomposition and high resolution if grid_info is None: - e1 = np.linspace(0.0, 1.0, 16) - e2 = np.linspace(0.0, 1.0, 65) + e1 = xp.linspace(0.0, 1.0, 16) + e2 = xp.linspace(0.0, 1.0, 65) if logical: - E1, E2 = np.meshgrid(e1, e2, indexing="ij") - X = np.stack((E1, E2), axis=0) + E1, E2 = xp.meshgrid(e1, e2, indexing="ij") + X = xp.stack((E1, E2), axis=0) else: XYZ = self(e1, e2, 0.0, squeeze_out=True) @@ -1459,11 +1459,11 @@ def show( ) # top view - e3 = np.linspace(0.0, 1.0, 65) + e3 = xp.linspace(0.0, 1.0, 65) if logical: - E1, E2 = np.meshgrid(e1, e2, indexing="ij") - X = np.stack((E1, E2), axis=0) + E1, E2 = xp.meshgrid(e1, e2, indexing="ij") + X = xp.stack((E1, E2), axis=0) else: theta_0 = self(e1, 0.0, e3, squeeze_out=True) theta_pi = self(e1, 0.5, e3, squeeze_out=True) @@ -1524,7 +1524,7 @@ def show( # coordinates # e3 = [0., .25, .5, .75] # x, y, z = self(e1, e2, e3) - # R = np.sqrt(x**2 + y**2) + # R = xp.sqrt(x**2 + y**2) # fig = plt.figure(figsize=(13, 13)) # for n in range(4): @@ -1551,14 +1551,14 @@ def show( elif isinstance(grid_info, list): assert len(grid_info) > 1 - e1 = np.linspace(0.0, 1.0, grid_info[0] + 1) - e2 = np.linspace(0.0, 1.0, grid_info[1] + 1) + e1 = xp.linspace(0.0, 1.0, grid_info[0] + 1) + e2 = xp.linspace(0.0, 1.0, grid_info[1] + 1) fig = plt.figure(figsize=figsize) ax = fig.add_subplot(1, 1, 1) if logical: - E1, E2 = np.meshgrid(e1, e2, indexing="ij") + E1, E2 = xp.meshgrid(e1, e2, indexing="ij") # eta1-isolines for i in range(e1.size): @@ -1586,7 +1586,7 @@ def show( ax.plot(X[co1, :, j], X[co2, :, j], "tab:blue", alpha=0.5) # plot domain with MPI decomposition - elif isinstance(grid_info, np.ndarray): + elif isinstance(grid_info, xp.ndarray): assert grid_info.ndim == 2 assert grid_info.shape[1] > 5 @@ -1594,7 +1594,7 @@ def show( ax = fig.add_subplot(1, 1, 1) for i in range(grid_info.shape[0]): - e1 = np.linspace( + e1 = xp.linspace( grid_info[i, 0], grid_info[i, 1], int( @@ -1602,7 +1602,7 @@ def show( ) + 1, ) - e2 = np.linspace( + e2 = xp.linspace( grid_info[i, 3], grid_info[i, 4], int( @@ -1612,7 +1612,7 @@ def show( ) if logical: - E1, E2 = np.meshgrid(e1, e2, indexing="ij") + E1, E2 = xp.meshgrid(e1, e2, indexing="ij") # eta1-isolines first_line = ax.plot( @@ -1737,7 +1737,7 @@ def show( ax.axis("equal") - if isinstance(grid_info, np.ndarray): + if isinstance(grid_info, xp.ndarray): plt.legend() if self.__class__.__name__ in torus_mappings: @@ -1772,9 +1772,9 @@ def __init__( Nel: tuple[int] = (8, 24, 6), p: tuple[int] = (2, 3, 1), spl_kind: tuple[bool] = (False, True, True), - cx: np.ndarray = None, - cy: np.ndarray = None, - cz: np.ndarray = None, + cx: xp.ndarray = None, + cy: xp.ndarray = None, + cz: xp.ndarray = None, ): self.kind_map = 0 @@ -1805,7 +1805,7 @@ def __init__( assert self.cz.shape == expected_shape # identify polar singularity at eta1=0 - if np.all(self.cx[0, :, 0] == self.cx[0, 0, 0]): + if xp.all(self.cx[0, :, 0] == self.cx[0, 0, 0]): self.pole = True else: self.pole = False @@ -1836,17 +1836,17 @@ def __init__( Nel: tuple[int] = (8, 24), p: tuple[int] = (2, 3), spl_kind: tuple[bool] = (False, True), - cx: np.ndarray = None, - cy: np.ndarray = None, + cx: xp.ndarray = None, + cy: xp.ndarray = None, ): # get default control points if cx is None or cy is None: def X(eta1, eta2): - return eta1 * np.cos(2 * np.pi * eta2) + 3.0 + return eta1 * xp.cos(2 * xp.pi * eta2) + 3.0 def Y(eta1, eta2): - return eta1 * np.sin(2 * np.pi * eta2) + return eta1 * xp.sin(2 * xp.pi * eta2) cx, cy = interp_mapping(Nel, p, spl_kind, X, Y) @@ -1869,7 +1869,7 @@ def Y(eta1, eta2): assert self.cy.shape == expected_shape # identify polar singularity at eta1=0 - if np.all(self.cx[0, :] == self.cx[0, 0]): + if xp.all(self.cx[0, :] == self.cx[0, 0]): self.pole = True else: self.pole = False @@ -1877,7 +1877,7 @@ def Y(eta1, eta2): # reshape control points to 3D self._cx = self.cx[:, :, None] self._cy = self.cy[:, :, None] - self._cz = np.zeros((1, 1, 1), dtype=float) + self._cz = xp.zeros((1, 1, 1), dtype=float) # init base class super().__init__(Nel=Nel, p=p, spl_kind=spl_kind) @@ -1902,8 +1902,8 @@ def __init__( Nel: tuple[int] = (8, 24), p: tuple[int] = (2, 3), spl_kind: tuple[bool] = (False, True), - cx: np.ndarray = None, - cy: np.ndarray = None, + cx: xp.ndarray = None, + cy: xp.ndarray = None, Lz: float = 4.0, ): self.kind_map = 1 @@ -1912,10 +1912,10 @@ def __init__( if cx is None or cy is None: def X(eta1, eta2): - return eta1 * np.cos(2 * np.pi * eta2) + return eta1 * xp.cos(2 * xp.pi * eta2) def Y(eta1, eta2): - return eta1 * np.sin(2 * np.pi * eta2) + return eta1 * xp.sin(2 * xp.pi * eta2) cx, cy = interp_mapping(Nel, p, spl_kind, X, Y) @@ -1923,7 +1923,7 @@ def Y(eta1, eta2): cx[0] = 0.0 cy[0] = 0.0 - self.params_numpy = np.array([Lz]) + self.params_numpy = xp.array([Lz]) self.periodic_eta3 = False # init base class @@ -1954,7 +1954,7 @@ class PoloidalSplineTorus(PoloidalSpline): spl_kind : tuple[bool] Kind of spline in each poloidal direction (True=periodic, False=clamped). - cx, cy : np.ndarray + cx, cy : xp.ndarray Control points (spline coefficients) of the poloidal mapping. If None, a default square-to-disc mapping of radius 1 centered around (x, y) = (3, 0) is interpolated. @@ -1967,23 +1967,23 @@ def __init__( Nel: tuple[int] = (8, 24), p: tuple[int] = (2, 3), spl_kind: tuple[bool] = (False, True), - cx: np.ndarray = None, - cy: np.ndarray = None, + cx: xp.ndarray = None, + cy: xp.ndarray = None, tor_period: int = 3, ): # use setters for mapping attributes self.kind_map = 2 - self.params_numpy = np.array([float(tor_period)]) + self.params_numpy = xp.array([float(tor_period)]) self.periodic_eta3 = True # get default control points if cx is None or cy is None: def X(eta1, eta2): - return eta1 * np.cos(2 * np.pi * eta2) + 3.0 + return eta1 * xp.cos(2 * xp.pi * eta2) + 3.0 def Y(eta1, eta2): - return eta1 * np.sin(2 * np.pi * eta2) + return eta1 * xp.sin(2 * xp.pi * eta2) cx, cy = interp_mapping(Nel, p, spl_kind, X, Y) @@ -2025,7 +2025,7 @@ def interp_mapping(Nel, p, spl_kind, X, Y, Z=None): NbaseN = [Nel + p - kind * p for Nel, p, kind in zip(Nel, p, spl_kind)] # element boundaries - el_b = [np.linspace(0.0, 1.0, Nel + 1) for Nel in Nel] + el_b = [xp.linspace(0.0, 1.0, Nel + 1) for Nel in Nel] # spline knot vectors T = [bsp.make_knots(el_b, p, kind) for el_b, p, kind in zip(el_b, p, spl_kind)] @@ -2040,7 +2040,7 @@ def interp_mapping(Nel, p, spl_kind, X, Y, Z=None): if len(Nel) == 2: I = kron(I_mat[0], I_mat[1], format="csc") - I_pts = np.meshgrid(I_pts[0], I_pts[1], indexing="ij") + I_pts = xp.meshgrid(I_pts[0], I_pts[1], indexing="ij") cx = spsolve(I, X(I_pts[0], I_pts[1]).flatten()).reshape( NbaseN[0], @@ -2073,7 +2073,7 @@ def interp_mapping(Nel, p, spl_kind, X, Y, Z=None): return 0.0 -def spline_interpolation_nd(p: list, spl_kind: list, grids_1d: list, values: np.ndarray): +def spline_interpolation_nd(p: list, spl_kind: list, grids_1d: list, values: xp.ndarray): """n-dimensional tensor-product spline interpolation with discrete input. The interpolation points are passed as a list of 1d arrays, each array with increasing entries g[0]=0 < g[1] < ... @@ -2095,7 +2095,7 @@ def spline_interpolation_nd(p: list, spl_kind: list, grids_1d: list, values: np. Returns -------- - coeffs : np.array + coeffs : xp.array spline coefficients as nd array. T : list[array] @@ -2110,11 +2110,11 @@ def spline_interpolation_nd(p: list, spl_kind: list, grids_1d: list, values: np. I_mat = [] I_LU = [] for sh, x_grid, p_i, kind_i in zip(values.shape, grids_1d, p, spl_kind): - assert isinstance(x_grid, np.ndarray) + assert isinstance(x_grid, xp.ndarray) assert sh == x_grid.size assert ( - np.all( - np.roll(x_grid, 1)[1:] < x_grid[1:], + xp.all( + xp.roll(x_grid, 1)[1:] < x_grid[1:], ) and x_grid[-1] > x_grid[-2] ) @@ -2122,17 +2122,17 @@ def spline_interpolation_nd(p: list, spl_kind: list, grids_1d: list, values: np. if kind_i: assert x_grid[-1] < 1.0, "Interpolation points must be <1 for periodic interpolation." - breaks = np.ones(x_grid.size + 1) + breaks = xp.ones(x_grid.size + 1) if p_i % 2 == 0: - breaks[1:-1] = (x_grid[1:] + np.roll(x_grid, 1)[1:]) / 2.0 + breaks[1:-1] = (x_grid[1:] + xp.roll(x_grid, 1)[1:]) / 2.0 breaks[0] = 0.0 else: breaks[:-1] = x_grid else: assert ( - np.abs( + xp.abs( x_grid[-1] - 1.0, ) < 1e-14 @@ -2149,12 +2149,12 @@ def spline_interpolation_nd(p: list, spl_kind: list, grids_1d: list, values: np. breaks[0] = 0.0 breaks[-1] = 1.0 - # breaks = np.linspace(0., 1., x_grid.size - (not kind_i)*p_i + 1) + # breaks = xp.linspace(0., 1., x_grid.size - (not kind_i)*p_i + 1) T += [bsp.make_knots(breaks, p_i, periodic=kind_i)] indN += [ - (np.indices((breaks.size - 1, p_i + 1))[1] + np.arange(breaks.size - 1)[:, None]) % x_grid.size, + (xp.indices((breaks.size - 1, p_i + 1))[1] + xp.arange(breaks.size - 1)[:, None]) % x_grid.size, ] I_mat += [bsp.collocation_matrix(T[-1], p_i, x_grid, periodic=kind_i)] diff --git a/src/struphy/geometry/domains.py b/src/struphy/geometry/domains.py index 02bad971f..024b980c9 100644 --- a/src/struphy/geometry/domains.py +++ b/src/struphy/geometry/domains.py @@ -2,6 +2,8 @@ import copy +import cunumpy as xp + from struphy.fields_background.base import AxisymmMHDequilibrium from struphy.fields_background.equils import EQDSKequilibrium from struphy.geometry.base import ( @@ -12,7 +14,6 @@ interp_mapping, ) from struphy.geometry.utilities import field_line_tracing -from struphy.utils.arrays import xp as np class Tokamak(PoloidalSplineTorus): @@ -156,8 +157,8 @@ def __init__(self, gvec_equil=None): def XYZ(e1, e2, e3): rho = _rmin + e1 * (1.0 - _rmin) - theta = 2 * np.pi * e2 - zeta = 2 * np.pi * e3 / gvec_equil._nfp + theta = 2 * xp.pi * e2 + zeta = 2 * xp.pi * e3 / gvec_equil._nfp if gvec_equil.params["use_boozer"]: ev = gvec.EvaluationsBoozer(rho=rho, theta_B=theta, zeta_B=zeta, state=gvec_equil.state) else: @@ -286,10 +287,10 @@ def __init__( # get control points def X(eta1, eta2): - return a * eta1 * np.cos(2 * np.pi * eta2) + return a * eta1 * xp.cos(2 * xp.pi * eta2) def Y(eta1, eta2): - return a * eta1 * np.sin(2 * np.pi * eta2) + return a * eta1 * xp.sin(2 * xp.pi * eta2) spl_kind = (False, True) @@ -363,17 +364,17 @@ def __init__( if sfl: def theta(eta1, eta2): - return 2 * np.arctan(np.sqrt((1 + a * eta1 / R0) / (1 - a * eta1 / R0)) * np.tan(np.pi * eta2)) + return 2 * xp.arctan(xp.sqrt((1 + a * eta1 / R0) / (1 - a * eta1 / R0)) * xp.tan(xp.pi * eta2)) else: def theta(eta1, eta2): - return 2 * np.pi * eta2 + return 2 * xp.pi * eta2 def R(eta1, eta2): - return a * eta1 * np.cos(theta(eta1, eta2)) + R0 + return a * eta1 * xp.cos(theta(eta1, eta2)) + R0 def Z(eta1, eta2): - return a * eta1 * np.sin(theta(eta1, eta2)) + return a * eta1 * xp.sin(theta(eta1, eta2)) spl_kind = (False, True) @@ -411,29 +412,15 @@ class Cuboid(Domain): l1 : float Start of x-interval (default: 0.). r1 : float - End of x-interval, r1>l1 (default: 2.). + End of x-interval, r1>l1 (default: 1.). l2 : float Start of y-interval (default: 0.). r2 : float - End of y-interval, r2>l2 (default: 3.). + End of y-interval, r2>l2 (default: 1.). l3 : float Start of z-interval (default: 0.). r3 : float - End of z-interval, r3>l3 (default: 6.). - - Note - ---- - In the parameter .yml, use the following in the section `geometry`:: - - geometry : - type : Cuboid - Cuboid : - l1 : 0. # start of x-interval - r1 : 2. # end of x-interval, r1>l1 - l2 : 0. # start of y-interval - r2 : 2. # end of y-interval, r2>l2 - l3 : 0. # start of z-interval - r3 : 1. # end of z-interval, r3>l3 + End of z-interval, r3>l3 (default: 1.). """ def __init__( @@ -776,24 +763,24 @@ def __init__( def inverse_map(self, x, y, z, bounded=True, change_out_order=False): """Analytical inverse map of HollowTorus""" - mr = np.sqrt(x**2 + y**2) - self.params["R0"] + mr = xp.sqrt(x**2 + y**2) - self.params["R0"] - eta3 = np.arctan2(-y, x) % (2 * np.pi / self.params["tor_period"]) / (2 * np.pi) * self.params["tor_period"] - eta2 = np.arctan2(z, mr) % (2 * np.pi / self.params["pol_period"]) / (2 * np.pi / self.params["pol_period"]) - eta1 = (z / np.sin(2 * np.pi * eta2 / self.params["pol_period"]) - self.params["a1"]) / ( + eta3 = xp.arctan2(-y, x) % (2 * xp.pi / self.params["tor_period"]) / (2 * xp.pi) * self.params["tor_period"] + eta2 = xp.arctan2(z, mr) % (2 * xp.pi / self.params["pol_period"]) / (2 * xp.pi / self.params["pol_period"]) + eta1 = (z / xp.sin(2 * xp.pi * eta2 / self.params["pol_period"]) - self.params["a1"]) / ( self.params["a2"] - self.params["a1"] ) if bounded: eta1[eta1 > 1] = 1.0 eta1[eta1 < 0] = 0.0 - assert np.all(np.logical_and(eta1 >= 0, eta1 <= 1)) + assert xp.all(xp.logical_and(eta1 >= 0, eta1 <= 1)) - assert np.all(np.logical_and(eta2 >= 0, eta2 <= 1)) - assert np.all(np.logical_and(eta3 >= 0, eta3 <= 1)) + assert xp.all(xp.logical_and(eta2 >= 0, eta2 <= 1)) + assert xp.all(xp.logical_and(eta3 >= 0, eta3 <= 1)) if change_out_order: - return np.transpose((eta1, eta2, eta3)) + return xp.transpose((eta1, eta2, eta3)) else: return eta1, eta2, eta3 diff --git a/src/struphy/geometry/evaluation_kernels.py b/src/struphy/geometry/evaluation_kernels.py index a357bbea1..4f97b9ce9 100644 --- a/src/struphy/geometry/evaluation_kernels.py +++ b/src/struphy/geometry/evaluation_kernels.py @@ -31,7 +31,7 @@ def f( args: DomainArguments Arguments for the mapping. - f_out : np.array + f_out : xp.array Output array of shape (3,). """ @@ -196,7 +196,7 @@ def df( args: DomainArguments Arguments for the mapping. - df_out : np.array + df_out : xp.array Output array of shape (3, 3). """ @@ -354,7 +354,7 @@ def det_df( args: DomainArguments Arguments for the mapping. - tmp1 : np.array + tmp1 : xp.array Temporary array of shape (3, 3). """ @@ -388,13 +388,13 @@ def df_inv( args: DomainArguments Arguments for the mapping. - tmp1: np.array + tmp1: xp.array Temporary array of shape (3, 3). avoid_round_off: bool Whether to manually set exact zeros in arrays. - dfinv_out: np.array + dfinv_out: xp.array Output array of shape (3, 3). """ @@ -484,13 +484,13 @@ def g( args: DomainArguments Arguments for the mapping. - tmp1, tmp2: np.array + tmp1, tmp2: xp.array Temporary arrays of shape (3, 3). avoid_round_off: bool Whether to manually set exact zeros in arrays. - g_out: np.array + g_out: xp.array Output array of shape (3, 3). """ df( @@ -601,13 +601,13 @@ def g_inv( args: DomainArguments Arguments for the mapping. - tmp1, tmp2, tmp3: np.array + tmp1, tmp2, tmp3: xp.array Temporary arrays of shape (3, 3). avoid_round_off: bool Whether to manually set exact zeros in arrays. - ginv_out: np.array + ginv_out: xp.array Output array of shape (3, 3). """ g( @@ -732,16 +732,16 @@ def select_metric_coeff( args: DomainArguments Arguments for the mapping. - tmp0: np.array + tmp0: xp.array Temporary array of shape (3,). - tmp1, tmp2, tmp3: np.array + tmp1, tmp2, tmp3: xp.array Temporary arrays of shape (3, 3). avoid_round_off: bool Whether to manually set exact zeros in arrays. - out: np.array + out: xp.array Output array of shape (3, 3). """ # identity map diff --git a/src/struphy/geometry/tests/test_domain.py b/src/struphy/geometry/tests/test_domain.py index 48348958b..c9a489331 100644 --- a/src/struphy/geometry/tests/test_domain.py +++ b/src/struphy/geometry/tests/test_domain.py @@ -4,8 +4,9 @@ def test_prepare_arg(): """Tests prepare_arg static method in domain base class.""" + import cunumpy as xp + from struphy.geometry.base import Domain - from struphy.utils.arrays import xp as np def a1(e1, e2, e3): return e1 * e2 @@ -21,12 +22,12 @@ def a_vec(e1, e2, e3): a_2 = e2 * e3 a_3 = e3 * e1 - return np.stack((a_1, a_2, a_3), axis=0) + return xp.stack((a_1, a_2, a_3), axis=0) # ========== tensor-product/slice evaluation =============== - e1 = np.random.rand(4) - e2 = np.random.rand(5) - e3 = np.random.rand(6) + e1 = xp.random.rand(4) + e2 = xp.random.rand(5) + e3 = xp.random.rand(6) E1, E2, E3, is_sparse_meshgrid = Domain.prepare_eval_pts(e1, e2, e3, flat_eval=False) @@ -84,7 +85,7 @@ def a_vec(e1, e2, e3): assert Domain.prepare_arg([A1, A2, A3], E1, E2, E3).shape == shape_vector # ============== markers evaluation ========================== - markers = np.random.rand(10, 6) + markers = xp.random.rand(10, 6) shape_scalar = (markers.shape[0], 1) shape_vector = (markers.shape[0], 3) @@ -158,15 +159,16 @@ def a_vec(e1, e2, e3): def test_evaluation_mappings(mapping): """Tests domain object creation with default parameters and evaluation of metric coefficients.""" + import cunumpy as xp + from struphy.geometry import domains from struphy.geometry.base import Domain - from struphy.utils.arrays import xp as np # arrays: - arr1 = np.linspace(0.0, 1.0, 4) - arr2 = np.linspace(0.0, 1.0, 5) - arr3 = np.linspace(0.0, 1.0, 6) - arrm = np.random.rand(10, 8) + arr1 = xp.linspace(0.0, 1.0, 4) + arr2 = xp.linspace(0.0, 1.0, 5) + arr3 = xp.linspace(0.0, 1.0, 6) + arrm = xp.random.rand(10, 8) print() print('Testing "evaluate"...') print("array shapes:", arr1.shape, arr2.shape, arr3.shape, arrm.shape) @@ -262,9 +264,9 @@ def test_evaluation_mappings(mapping): assert domain.metric_inv(arr1, arr2, arr3).shape == (3, 3) + arr1.shape + arr2.shape + arr3.shape # matrix evaluations at one point in third direction - mat12_x, mat12_y = np.meshgrid(arr1, arr2, indexing="ij") - mat13_x, mat13_z = np.meshgrid(arr1, arr3, indexing="ij") - mat23_y, mat23_z = np.meshgrid(arr2, arr3, indexing="ij") + mat12_x, mat12_y = xp.meshgrid(arr1, arr2, indexing="ij") + mat13_x, mat13_z = xp.meshgrid(arr1, arr3, indexing="ij") + mat23_y, mat23_z = xp.meshgrid(arr2, arr3, indexing="ij") # eta1-eta2 matrix evaluation: print("eta1-eta2 matrix evaluation, shape:", domain(mat12_x, mat12_y, 0.5, squeeze_out=True).shape) @@ -294,7 +296,7 @@ def test_evaluation_mappings(mapping): assert domain.metric_inv(0.5, mat23_y, mat23_z, squeeze_out=True).shape == (3, 3) + mat23_y.shape # matrix evaluations for sparse meshgrid - mat_x, mat_y, mat_z = np.meshgrid(arr1, arr2, arr3, indexing="ij", sparse=True) + mat_x, mat_y, mat_z = xp.meshgrid(arr1, arr2, arr3, indexing="ij", sparse=True) print("sparse meshgrid matrix evaluation, shape:", domain(mat_x, mat_y, mat_z).shape) assert domain(mat_x, mat_y, mat_z).shape == (3,) + (mat_x.shape[0], mat_y.shape[1], mat_z.shape[2]) assert domain.jacobian(mat_x, mat_y, mat_z).shape == (3, 3) + (mat_x.shape[0], mat_y.shape[1], mat_z.shape[2]) @@ -304,7 +306,7 @@ def test_evaluation_mappings(mapping): assert domain.metric_inv(mat_x, mat_y, mat_z).shape == (3, 3) + (mat_x.shape[0], mat_y.shape[1], mat_z.shape[2]) # matrix evaluations - mat_x, mat_y, mat_z = np.meshgrid(arr1, arr2, arr3, indexing="ij") + mat_x, mat_y, mat_z = xp.meshgrid(arr1, arr2, arr3, indexing="ij") print("matrix evaluation, shape:", domain(mat_x, mat_y, mat_z).shape) assert domain(mat_x, mat_y, mat_z).shape == (3,) + mat_x.shape assert domain.jacobian(mat_x, mat_y, mat_z).shape == (3, 3) + mat_x.shape @@ -317,23 +319,24 @@ def test_evaluation_mappings(mapping): def test_pullback(): """Tests pullbacks to p-forms.""" + import cunumpy as xp + from struphy.geometry import domains from struphy.geometry.base import Domain - from struphy.utils.arrays import xp as np # arrays: - arr1 = np.linspace(0.0, 1.0, 4) - arr2 = np.linspace(0.0, 1.0, 5) - arr3 = np.linspace(0.0, 1.0, 6) + arr1 = xp.linspace(0.0, 1.0, 4) + arr2 = xp.linspace(0.0, 1.0, 5) + arr3 = xp.linspace(0.0, 1.0, 6) print() print('Testing "pull"...') print("array shapes:", arr1.shape, arr2.shape, arr3.shape) - markers = np.random.rand(13, 6) + markers = xp.random.rand(13, 6) # physical function to pull back (used as components of forms too): def fun(x, y, z): - return np.exp(x) * np.sin(y) * np.cos(z) + return xp.exp(x) * xp.sin(y) * xp.cos(z) domain_class = getattr(domains, "Colella") domain = domain_class() @@ -421,9 +424,9 @@ def fun(x, y, z): ) # matrix pullbacks at one point in third direction - mat12_x, mat12_y = np.meshgrid(arr1, arr2, indexing="ij") - mat13_x, mat13_z = np.meshgrid(arr1, arr3, indexing="ij") - mat23_y, mat23_z = np.meshgrid(arr2, arr3, indexing="ij") + mat12_x, mat12_y = xp.meshgrid(arr1, arr2, indexing="ij") + mat13_x, mat13_z = xp.meshgrid(arr1, arr3, indexing="ij") + mat23_y, mat23_z = xp.meshgrid(arr2, arr3, indexing="ij") # eta1-eta2 matrix pullback: if p_str == "0" or p_str == "3": @@ -450,7 +453,7 @@ def fun(x, y, z): ) # matrix pullbacks for sparse meshgrid - mat_x, mat_y, mat_z = np.meshgrid(arr1, arr2, arr3, indexing="ij", sparse=True) + mat_x, mat_y, mat_z = xp.meshgrid(arr1, arr2, arr3, indexing="ij", sparse=True) if p_str == "0" or p_str == "3": assert domain.pull(fun_form, mat_x, mat_y, mat_z, kind=p_str).shape == ( mat_x.shape[0], @@ -466,7 +469,7 @@ def fun(x, y, z): ) # matrix pullbacks - mat_x, mat_y, mat_z = np.meshgrid(arr1, arr2, arr3, indexing="ij") + mat_x, mat_y, mat_z = xp.meshgrid(arr1, arr2, arr3, indexing="ij") if p_str == "0" or p_str == "3": assert domain.pull(fun_form, mat_x, mat_y, mat_z, kind=p_str).shape == mat_x.shape else: @@ -476,23 +479,24 @@ def fun(x, y, z): def test_pushforward(): """Tests pushforward of p-forms.""" + import cunumpy as xp + from struphy.geometry import domains from struphy.geometry.base import Domain - from struphy.utils.arrays import xp as np # arrays: - arr1 = np.linspace(0.0, 1.0, 4) - arr2 = np.linspace(0.0, 1.0, 5) - arr3 = np.linspace(0.0, 1.0, 6) + arr1 = xp.linspace(0.0, 1.0, 4) + arr2 = xp.linspace(0.0, 1.0, 5) + arr3 = xp.linspace(0.0, 1.0, 6) print() print('Testing "push"...') print("array shapes:", arr1.shape, arr2.shape, arr3.shape) - markers = np.random.rand(13, 6) + markers = xp.random.rand(13, 6) # logical function to push (used as components of forms too): def fun(e1, e2, e3): - return np.exp(e1) * np.sin(e2) * np.cos(e3) + return xp.exp(e1) * xp.sin(e2) * xp.cos(e3) domain_class = getattr(domains, "Colella") domain = domain_class() @@ -580,9 +584,9 @@ def fun(e1, e2, e3): ) # matrix pushs at one point in third direction - mat12_x, mat12_y = np.meshgrid(arr1, arr2, indexing="ij") - mat13_x, mat13_z = np.meshgrid(arr1, arr3, indexing="ij") - mat23_y, mat23_z = np.meshgrid(arr2, arr3, indexing="ij") + mat12_x, mat12_y = xp.meshgrid(arr1, arr2, indexing="ij") + mat13_x, mat13_z = xp.meshgrid(arr1, arr3, indexing="ij") + mat23_y, mat23_z = xp.meshgrid(arr2, arr3, indexing="ij") # eta1-eta2 matrix push: if p_str == "0" or p_str == "3": @@ -609,7 +613,7 @@ def fun(e1, e2, e3): ) # matrix pushs for sparse meshgrid - mat_x, mat_y, mat_z = np.meshgrid(arr1, arr2, arr3, indexing="ij", sparse=True) + mat_x, mat_y, mat_z = xp.meshgrid(arr1, arr2, arr3, indexing="ij", sparse=True) if p_str == "0" or p_str == "3": assert domain.push(fun_form, mat_x, mat_y, mat_z, kind=p_str).shape == ( mat_x.shape[0], @@ -625,7 +629,7 @@ def fun(e1, e2, e3): ) # matrix pushs - mat_x, mat_y, mat_z = np.meshgrid(arr1, arr2, arr3, indexing="ij") + mat_x, mat_y, mat_z = xp.meshgrid(arr1, arr2, arr3, indexing="ij") if p_str == "0" or p_str == "3": assert domain.push(fun_form, mat_x, mat_y, mat_z, kind=p_str).shape == mat_x.shape else: @@ -635,23 +639,24 @@ def fun(e1, e2, e3): def test_transform(): """Tests transformation of p-forms.""" + import cunumpy as xp + from struphy.geometry import domains from struphy.geometry.base import Domain - from struphy.utils.arrays import xp as np # arrays: - arr1 = np.linspace(0.0, 1.0, 4) - arr2 = np.linspace(0.0, 1.0, 5) - arr3 = np.linspace(0.0, 1.0, 6) + arr1 = xp.linspace(0.0, 1.0, 4) + arr2 = xp.linspace(0.0, 1.0, 5) + arr3 = xp.linspace(0.0, 1.0, 6) print() print('Testing "transform"...') print("array shapes:", arr1.shape, arr2.shape, arr3.shape) - markers = np.random.rand(13, 6) + markers = xp.random.rand(13, 6) # logical function to push (used as components of forms too): def fun(e1, e2, e3): - return np.exp(e1) * np.sin(e2) * np.cos(e3) + return xp.exp(e1) * xp.sin(e2) * xp.cos(e3) domain_class = getattr(domains, "Colella") domain = domain_class() @@ -751,9 +756,9 @@ def fun(e1, e2, e3): ) # matrix transforms at one point in third direction - mat12_x, mat12_y = np.meshgrid(arr1, arr2, indexing="ij") - mat13_x, mat13_z = np.meshgrid(arr1, arr3, indexing="ij") - mat23_y, mat23_z = np.meshgrid(arr2, arr3, indexing="ij") + mat12_x, mat12_y = xp.meshgrid(arr1, arr2, indexing="ij") + mat13_x, mat13_z = xp.meshgrid(arr1, arr3, indexing="ij") + mat23_y, mat23_z = xp.meshgrid(arr2, arr3, indexing="ij") # eta1-eta2 matrix transform: if p_str == "0_to_3" or p_str == "3_to_0": @@ -789,7 +794,7 @@ def fun(e1, e2, e3): ) # matrix transforms for sparse meshgrid - mat_x, mat_y, mat_z = np.meshgrid(arr1, arr2, arr3, indexing="ij", sparse=True) + mat_x, mat_y, mat_z = xp.meshgrid(arr1, arr2, arr3, indexing="ij", sparse=True) if p_str == "0_to_3" or p_str == "3_to_0": assert domain.transform(fun_form, mat_x, mat_y, mat_z, kind=p_str).shape == ( mat_x.shape[0], @@ -805,7 +810,7 @@ def fun(e1, e2, e3): ) # matrix transforms - mat_x, mat_y, mat_z = np.meshgrid(arr1, arr2, arr3, indexing="ij") + mat_x, mat_y, mat_z = xp.meshgrid(arr1, arr2, arr3, indexing="ij") if p_str == "0_to_3" or p_str == "3_to_0": assert domain.transform(fun_form, mat_x, mat_y, mat_z, kind=p_str).shape == mat_x.shape else: @@ -817,18 +822,18 @@ def fun(e1, e2, e3): # """ # # from struphy.geometry import domains -# from struphy.utils.arrays import xp as np +# import cunumpy as xp # # # arrays: -# arr1 = np.linspace(0., 1., 4) -# arr2 = np.linspace(0., 1., 5) -# arr3 = np.linspace(0., 1., 6) +# arr1 = xp.linspace(0., 1., 4) +# arr2 = xp.linspace(0., 1., 5) +# arr3 = xp.linspace(0., 1., 6) # print() # print('Testing "transform"...') # print('array shapes:', arr1.shape, arr2.shape, arr3.shape) # # # logical function to tranform (used as components of forms too): -# fun = lambda eta1, eta2, eta3: np.exp(eta1)*np.sin(eta2)*np.cos(eta3) +# fun = lambda eta1, eta2, eta3: xp.exp(eta1)*xp.sin(eta2)*xp.cos(eta3) # # domain_class = getattr(domains, 'Colella') # domain = domain_class() @@ -885,9 +890,9 @@ def fun(e1, e2, e3): # assert a.shape[0] == arr1.size and a.shape[1] == arr2.size and a.shape[2] == arr3.size # # # matrix transformation at one point in third direction -# mat12_x, mat12_y = np.meshgrid(arr1, arr2, indexing='ij') -# mat13_x, mat13_z = np.meshgrid(arr1, arr3, indexing='ij') -# mat23_y, mat23_z = np.meshgrid(arr2, arr3, indexing='ij') +# mat12_x, mat12_y = xp.meshgrid(arr1, arr2, indexing='ij') +# mat13_x, mat13_z = xp.meshgrid(arr1, arr3, indexing='ij') +# mat23_y, mat23_z = xp.meshgrid(arr2, arr3, indexing='ij') # # # eta1-eta2 matrix transformation: # a = domain.transform(fun_form, mat12_x, mat12_y, .5, p_str) @@ -903,13 +908,13 @@ def fun(e1, e2, e3): # assert a.shape == mat23_y.shape # # # matrix transformation for sparse meshgrid -# mat_x, mat_y, mat_z = np.meshgrid(arr1, arr2, arr3, indexing='ij', sparse=True) +# mat_x, mat_y, mat_z = xp.meshgrid(arr1, arr2, arr3, indexing='ij', sparse=True) # a = domain.transform(fun_form, mat_x, mat_y, mat_z, p_str) # #print('sparse meshgrid matrix transformation, shape:', a.shape) # assert a.shape[0] == mat_x.shape[0] and a.shape[1] == mat_y.shape[1] and a.shape[2] == mat_z.shape[2] # # # matrix transformation -# mat_x, mat_y, mat_z = np.meshgrid(arr1, arr2, arr3, indexing='ij') +# mat_x, mat_y, mat_z = xp.meshgrid(arr1, arr2, arr3, indexing='ij') # a = domain.transform(fun_form, mat_x, mat_y, mat_z, p_str) # #print('matrix transformation, shape:', a.shape) # assert a.shape == mat_x.shape diff --git a/src/struphy/geometry/utilities.py b/src/struphy/geometry/utilities.py index 732cf48c0..ccc159692 100644 --- a/src/struphy/geometry/utilities.py +++ b/src/struphy/geometry/utilities.py @@ -1,14 +1,23 @@ +# from __future__ import annotations "Domain-related utility functions." +from typing import Callable + +import cunumpy as xp +import numpy as np + +# from typing import TYPE_CHECKING from scipy.optimize import newton, root, root_scalar from scipy.sparse import csc_matrix from scipy.sparse.linalg import splu from struphy.bsplines import bsplines as bsp -from struphy.geometry.base import PoloidalSplineTorus + +# if TYPE_CHECKING: +from struphy.geometry.base import Domain, PoloidalSplineTorus from struphy.geometry.utilities_kernels import weighted_arc_lengths_flux_surface +from struphy.io.options import GivenInBasis from struphy.linear_algebra.linalg_kron import kron_lusolve_2d -from struphy.utils.arrays import xp as np def field_line_tracing( @@ -120,10 +129,10 @@ def field_line_tracing( Returns ------- - cR : np.ndarray + cR : xp.ndarray Control points (2d) of flux aligned spline mapping (R-component). - cZ : np.ndarray + cZ : xp.ndarray Control points (2d) of flux aligned spline mapping (Z-component). """ @@ -136,8 +145,8 @@ def field_line_tracing( ps, px = p_pre # spline knots - Ts = bsp.make_knots(np.linspace(0.0, 1.0, ns + 1), ps, False) - Tx = bsp.make_knots(np.linspace(0.0, 1.0, nx + 1), px, True) + Ts = bsp.make_knots(xp.linspace(0.0, 1.0, ns + 1), ps, False) + Tx = bsp.make_knots(xp.linspace(0.0, 1.0, nx + 1), px, True) # interpolation (Greville) points s_gr = bsp.greville(Ts, ps, False) @@ -156,13 +165,13 @@ def field_line_tracing( ] # check if pole is included - if np.abs(psi(psi_axis_R, psi_axis_Z) - psi0) < 1e-14: + if xp.abs(psi(psi_axis_R, psi_axis_Z) - psi0) < 1e-14: pole = True else: pole = False - R = np.zeros((s_gr.size, x_gr.size), dtype=float) - Z = np.zeros((s_gr.size, x_gr.size), dtype=float) + R = xp.zeros((s_gr.size, x_gr.size), dtype=float) + Z = xp.zeros((s_gr.size, x_gr.size), dtype=float) # function whose root must be found for j, x in enumerate(x_gr): @@ -179,8 +188,8 @@ def field_line_tracing( # function whose root must be found def f(r): - _R = psi_axis_R + r * np.cos(2 * np.pi * x) - _Z = psi_axis_Z + r * np.sin(2 * np.pi * x) + _R = psi_axis_R + r * xp.cos(2 * xp.pi * x) + _Z = psi_axis_Z + r * xp.sin(2 * xp.pi * x) psi_norm = (psi(_R, _Z) - psi0) / (psi1 - psi0) @@ -191,8 +200,8 @@ def f(r): r_flux_surface = newton(f, x0=r_guess) - R[i, j] = psi_axis_R + r_flux_surface * np.cos(2 * np.pi * x) - Z[i, j] = psi_axis_Z + r_flux_surface * np.sin(2 * np.pi * x) + R[i, j] = psi_axis_R + r_flux_surface * xp.cos(2 * xp.pi * x) + Z[i, j] = psi_axis_Z + r_flux_surface * xp.sin(2 * xp.pi * x) # get control points cR_equal_angle = kron_lusolve_2d(ILUs, R) @@ -218,8 +227,8 @@ def f(r): ps, px = p # spline knots - Ts = bsp.make_knots(np.linspace(0.0, 1.0, ns + 1), ps, False) - Tx = bsp.make_knots(np.linspace(0.0, 1.0, nx + 1), px, True) + Ts = bsp.make_knots(xp.linspace(0.0, 1.0, ns + 1), ps, False) + Tx = bsp.make_knots(xp.linspace(0.0, 1.0, nx + 1), px, True) # interpolation (Greville) points s_gr = bsp.greville(Ts, ps, False) @@ -246,10 +255,10 @@ def f(r): # target function for xi parametrization def f_angles(xis, s_val): - assert np.all(np.logical_and(xis > 0.0, xis < 1.0)) + assert xp.all(xp.logical_and(xis > 0.0, xis < 1.0)) # add 0 and 1 to angles array - xis_extended = np.array([0.0] + list(xis) + [1.0]) + xis_extended = xp.array([0.0] + list(xis) + [1.0]) # compute (R, Z) coordinates for given xis on fixed flux surface corresponding to s_val _RZ = domain_eq_angle(s_val, xis_extended, 0.0) @@ -258,17 +267,17 @@ def f_angles(xis, s_val): _Z = _RZ[2] # |grad(psi)| at xis - gp = np.sqrt(psi(_R, _Z, dR=1) ** 2 + psi(_R, _Z, dZ=1) ** 2) + gp = xp.sqrt(psi(_R, _Z, dR=1) ** 2 + psi(_R, _Z, dZ=1) ** 2) # compute weighted arc_lengths between two successive points in xis_extended array - dl = np.zeros(xis_extended.size - 1, dtype=float) + dl = xp.zeros(xis_extended.size - 1, dtype=float) weighted_arc_lengths_flux_surface(_R, _Z, gp, dl, xi_param_dict[xi_param]) # total length of the flux surface - l = np.sum(dl) + l = xp.sum(dl) # cumulative sum of arc lengths, start with 0! - l_cum = np.cumsum(dl) + l_cum = xp.cumsum(dl) # odd spline degree if px % 2 == 1: @@ -280,8 +289,8 @@ def f_angles(xis, s_val): return xi_diff # loop over flux surfaces and find xi parametrization - R = np.zeros((s_gr.size, x_gr.size), dtype=float) - Z = np.zeros((s_gr.size, x_gr.size), dtype=float) + R = xp.zeros((s_gr.size, x_gr.size), dtype=float) + Z = xp.zeros((s_gr.size, x_gr.size), dtype=float) if px % 2 == 1: xis0 = x_gr[1:].copy() @@ -333,10 +342,10 @@ class TransformedPformComponent: Parameters ---------- - fun : list - Callable function components. Has to be length three for 1-, 2-forms and vector fields, length one otherwise. + fun : Callable | list + Callable function (components). Has to be length three for vector-valued funnctions,. - fun_basis : str + given_in_basis : GivenInBasis In which basis fun is represented: either a p-form, then '0' or '3' for scalar and 'v', '1' or '2' for vector-valued, @@ -348,19 +357,24 @@ class TransformedPformComponent: The p-form representation of the output: '0', '1', '2' '3' or 'v'. comp : int - Which component of the transformed p-form is returned, 0, 1, or 2 (only needed for vector-valued fun). + Which component of the vector-valued function to return (=0 for scalars). domain: struphy.geometry.domains All things mapping. If None, the input fun is just evaluated and not transformed at __call__. - - Returns - ------- - out : array[float] - The values of the component comp of fun transformed from fun_basis to out_form. """ - def __init__(self, fun: list, fun_basis: str, out_form: str, comp=0, domain=None): - assert len(fun) == 1 or len(fun) == 3 + def __init__( + self, + fun: Callable | list, + given_in_basis: GivenInBasis, + out_form: str, + comp: int = 0, + domain: Domain = None, + ): + if isinstance(fun, list): + assert len(fun) == 1 or len(fun) == 3 + else: + fun = [fun] self._fun = [] for f in fun: @@ -374,7 +388,7 @@ def f_zero(x, y, z): assert callable(f) self._fun += [f] - self._fun_basis = fun_basis + self._given_in_basis = given_in_basis self._out_form = out_form self._comp = comp self._domain = domain @@ -391,19 +405,19 @@ def f_zero(x, y, z): def __call__(self, eta1, eta2, eta3): """ - Evaluate the component of the transformed p-form specified in self._comp. + Evaluate the component of the transformed p-form specified 'comp'. Depending on the dimension of eta1 either point-wise, tensor-product, slice plane or general (see :ref:`struphy.geometry.base.prepare_arg`). """ - if self._fun_basis == self._out_form or self._domain is None: + if self._given_in_basis == self._out_form or self._domain is None: if self._is_scalar: out = self._fun(eta1, eta2, eta3) else: out = self._fun[self._comp](eta1, eta2, eta3) - elif self._fun_basis == "physical": + elif self._given_in_basis == "physical": if self._is_scalar: out = self._domain.pull( self._fun, @@ -421,7 +435,7 @@ def __call__(self, eta1, eta2, eta3): kind=self._out_form, )[self._comp] - elif self._fun_basis == "physical_at_eta": + elif self._given_in_basis == "physical_at_eta": if self._is_scalar: out = self._domain.pull( self._fun, @@ -442,7 +456,7 @@ def __call__(self, eta1, eta2, eta3): )[self._comp] else: - dict_tran = self._fun_basis + "_to_" + self._out_form + dict_tran = self._given_in_basis + "_to_" + self._out_form if self._is_scalar: out = self._domain.transform( diff --git a/src/struphy/geometry/utilities_kernels.py b/src/struphy/geometry/utilities_kernels.py index 85ccdbcf9..1c26b25c5 100644 --- a/src/struphy/geometry/utilities_kernels.py +++ b/src/struphy/geometry/utilities_kernels.py @@ -18,16 +18,16 @@ def weighted_arc_lengths_flux_surface(r: "float[:]", z: "float[:]", grad_psi: "f Parameters ---------- - r : np.ndarray + r : xp.ndarray R coordinates of the flux surface. - z : np.ndarray + z : xp.ndarray Z coordinates of the flux surface. - grad_psi : np.ndarray + grad_psi : xp.ndarray Absolute values of the flux function gradient on the flux surface: |grad(psi)| = sqrt[ (d_R psi)**2 + (d_Z psi)**2 ]. - dwls : np.ndarray + dwls : xp.ndarray The weighted arc lengths will be written into this array. Length must be one smaller than lengths of r, z and grad_psi. kind : int diff --git a/src/struphy/initial/base.py b/src/struphy/initial/base.py new file mode 100644 index 000000000..cafca9dbe --- /dev/null +++ b/src/struphy/initial/base.py @@ -0,0 +1,47 @@ +from abc import ABCMeta, abstractmethod +from typing import Callable + +from struphy.io.options import GivenInBasis, check_option + + +class Perturbation(metaclass=ABCMeta): + """Base class for perturbations that can be chosen as initial conditions.""" + + @abstractmethod + def __call__(self, eta1, eta2, eta3, flat_eval=False): + pass + + def prepare_eval_pts(self): + # TODO: we could prepare the arguments via a method in this base class (flat_eval, sparse meshgrid, etc.). + pass + + @property + def given_in_basis(self) -> str: + r"""In which basis the perturbation is represented, must be set in child class (use the setter below). + + Either + * '0', '1', '2' or '3' for a p-form basis + * 'v' for a vector-field basis + * 'physical' when defined on the physical (mapped) domain + * 'physical_at_eta' when given the physical components evaluated on the logical domain, u(F(eta)) + * 'norm' when given in the normalized co-variant basis (:math:`\delta_i / |\delta_i|`) + """ + return self._given_in_basis + + @given_in_basis.setter + def given_in_basis(self, new: str): + check_option(new, GivenInBasis) + self._given_in_basis = new + + @property + def comp(self) -> int: + """Which component of vector is perturbed (=0 for scalar-valued functions). + Can be set in child class (use the setter below).""" + if not hasattr(self, "_comp"): + self._comp = 0 + return self._comp + + @comp.setter + def comp(self, new: int): + assert new in (0, 1, 2) + self._comp = new diff --git a/src/struphy/initial/eigenfunctions.py b/src/struphy/initial/eigenfunctions.py index 172386bdf..2f66fcacb 100644 --- a/src/struphy/initial/eigenfunctions.py +++ b/src/struphy/initial/eigenfunctions.py @@ -1,11 +1,11 @@ import os +import cunumpy as xp import yaml from psydac.api.discretization import discretize from sympde.topology import Derham, Line from struphy.fields_background.equils import set_defaults -from struphy.utils.arrays import xp as np class InitialMHDAxisymHdivEigFun: @@ -54,11 +54,11 @@ def __init__(self, derham, **params): spec_path = params["spec_abs"] # load eigenvector for velocity field - omega2, U2_eig = np.split(np.load(spec_path), [1], axis=0) + omega2, U2_eig = xp.split(xp.load(spec_path), [1], axis=0) omega2 = omega2.flatten() # find eigenvector corresponding to given squared eigenfrequency range - mode = np.where((np.real(omega2) < params["eig_freq_upper"]) & (np.real(omega2) > params["eig_freq_lower"]))[0] + mode = xp.where((xp.real(omega2) < params["eig_freq_upper"]) & (xp.real(omega2) > params["eig_freq_lower"]))[0] assert mode.size == 1 mode = mode[0] @@ -89,28 +89,28 @@ def __init__(self, derham, **params): n_tor = int(os.path.split(spec_path)[-1][-6:-4]) - N_cos = p0(lambda phi: np.cos(2 * np.pi * n_tor * phi)).coeffs.toarray() - N_sin = p0(lambda phi: np.sin(2 * np.pi * n_tor * phi)).coeffs.toarray() + N_cos = p0(lambda phi: xp.cos(2 * xp.pi * n_tor * phi)).coeffs.toarray() + N_sin = p0(lambda phi: xp.sin(2 * xp.pi * n_tor * phi)).coeffs.toarray() - D_cos = p1(lambda phi: np.cos(2 * np.pi * n_tor * phi)).coeffs.toarray() - D_sin = p1(lambda phi: np.sin(2 * np.pi * n_tor * phi)).coeffs.toarray() + D_cos = p1(lambda phi: xp.cos(2 * xp.pi * n_tor * phi)).coeffs.toarray() + D_sin = p1(lambda phi: xp.sin(2 * xp.pi * n_tor * phi)).coeffs.toarray() # select real part or imaginary part assert params["kind"] == "r" or params["kind"] == "i" if params["kind"] == "r": - eig_vec_1 = (np.outer(np.real(eig_vec_1), D_cos) - np.outer(np.imag(eig_vec_1), D_sin)).flatten() - eig_vec_2 = (np.outer(np.real(eig_vec_2), D_cos) - np.outer(np.imag(eig_vec_2), D_sin)).flatten() - eig_vec_3 = (np.outer(np.real(eig_vec_3), N_cos) - np.outer(np.imag(eig_vec_3), N_sin)).flatten() + eig_vec_1 = (xp.outer(xp.real(eig_vec_1), D_cos) - xp.outer(xp.imag(eig_vec_1), D_sin)).flatten() + eig_vec_2 = (xp.outer(xp.real(eig_vec_2), D_cos) - xp.outer(xp.imag(eig_vec_2), D_sin)).flatten() + eig_vec_3 = (xp.outer(xp.real(eig_vec_3), N_cos) - xp.outer(xp.imag(eig_vec_3), N_sin)).flatten() else: - eig_vec_1 = (np.outer(np.imag(eig_vec_1), D_cos) + np.outer(np.real(eig_vec_1), D_sin)).flatten() - eig_vec_2 = (np.outer(np.imag(eig_vec_2), D_cos) + np.outer(np.real(eig_vec_2), D_sin)).flatten() - eig_vec_3 = (np.outer(np.imag(eig_vec_3), N_cos) + np.outer(np.real(eig_vec_3), N_sin)).flatten() + eig_vec_1 = (xp.outer(xp.imag(eig_vec_1), D_cos) + xp.outer(xp.real(eig_vec_1), D_sin)).flatten() + eig_vec_2 = (xp.outer(xp.imag(eig_vec_2), D_cos) + xp.outer(xp.real(eig_vec_2), D_sin)).flatten() + eig_vec_3 = (xp.outer(xp.imag(eig_vec_3), N_cos) + xp.outer(xp.real(eig_vec_3), N_sin)).flatten() # set coefficients in full space - eigvec_1_ten = np.zeros(derham.nbasis["2"][0], dtype=float) - eigvec_2_ten = np.zeros(derham.nbasis["2"][1], dtype=float) - eigvec_3_ten = np.zeros(derham.nbasis["2"][2], dtype=float) + eigvec_1_ten = xp.zeros(derham.nbasis["2"][0], dtype=float) + eigvec_2_ten = xp.zeros(derham.nbasis["2"][1], dtype=float) + eigvec_3_ten = xp.zeros(derham.nbasis["2"][2], dtype=float) bc1_1 = derham.dirichlet_bc[0][0] bc1_2 = derham.dirichlet_bc[0][1] @@ -138,19 +138,19 @@ def __init__(self, derham, **params): else: # split into polar/tensor product parts - eig_vec_1 = np.split( + eig_vec_1 = xp.split( eig_vec_1, [ derham.Vh_pol["2"].n_polar[0] * nnz_tor[0], ], ) - eig_vec_2 = np.split( + eig_vec_2 = xp.split( eig_vec_2, [ derham.Vh_pol["2"].n_polar[1] * nnz_tor[1], ], ) - eig_vec_3 = np.split( + eig_vec_3 = xp.split( eig_vec_3, [ derham.Vh_pol["2"].n_polar[2] * nnz_tor[2], diff --git a/src/struphy/initial/perturbations.py b/src/struphy/initial/perturbations.py index 9062c42b3..767f899c9 100644 --- a/src/struphy/initial/perturbations.py +++ b/src/struphy/initial/perturbations.py @@ -1,13 +1,48 @@ #!/usr/bin/env python3 -"Analytical perturbations (modes)." +"Analytical perturbations." +from dataclasses import dataclass + +import cunumpy as xp import scipy import scipy.special -from struphy.utils.arrays import xp as np +from struphy.initial.base import Perturbation +from struphy.io.options import GivenInBasis, NoiseDirections, check_option + + +@dataclass +class Noise(Perturbation): + """White noise for FEEC coefficients. + + Parameters + ---------- + direction: str + The direction(s) of variation of the noise: 'e1', 'e2', 'e3', 'e1e2', etc. + amp: float + Noise amplitude. -class ModesSin: + seed: int + Seed for the random number generator. + """ + + direction: NoiseDirections = "e3" + amp: float = 0.0001 + seed: int = None + comp: int = 0 + given_in_basis: GivenInBasis = "0" + + def __post_init__( + self, + ): + check_option(self.direction, NoiseDirections) + + def __call__(self): + pass + + +class ModesSin(Perturbation): r"""Sinusoidal function in 3D. .. math:: @@ -26,25 +61,25 @@ class ModesSin: \end{aligned} \right. - Can be used in logical space, where :math:`x \to \eta_1,\, y\to \eta_2,\, z \to \eta_3` + Can be used in logical space (use 'given_in_basis'), where :math:`x \to \eta_1,\, y\to \eta_2,\, z \to \eta_3` and :math:`L_x=L_y=L_z=1.0` (default). Parameters ---------- - ls : tuple | list + ls : tuple[int] Mode numbers in x-direction (kx = l*2*pi/Lx). - ms : tuple | list + ms : tuple[int] Mode numbers in y-direction (ky = m*2*pi/Ly). - ns : tuple | list + ns : tuple[int] Mode numbers in z-direction (kz = n*2*pi/Lz). - amps : tuple | list + amps : tuple[float] Amplitude of each mode. theta : tuple | list - Phase of each mode + Phase of each mode. pfuns : tuple | list[str] "Id" or "localize" define the profile functions. @@ -57,43 +92,27 @@ class ModesSin: Lx, Ly, Lz : float Domain lengths. - Note - ---- - Example of use in a ``.yml`` parameter file:: - - perturbations : - type : ModesSin - ModesSin : - comps : - scalar_name : '0' # choices: null, 'physical', '0', '3' - vector_name : [null , 'v', '2'] # choices: null, 'physical', '1', '2', 'v', 'norm' - ls : - scalar_name: [1, 3] # two x-modes for scalar variable - vector_name: [null, [0, 1], [4]] # two x-modes for 2nd comp. and one x-mode for third component of vector-valued variable - theta : - scalar_name: [0, 3.1415] - vector_name: [null, [0, 0], [1.5708]] - pfuns : - vector_name: [null, ['localize'], ['Id']] - pfuns_params - vector_name: [null, ['0.1'], [0.]] - Lx : 7.853981633974483 - Ly : 1. - Lz : 1. + given_in_basis : str + In which basis the perturbation is represented (see base class). + + comp : int + Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) """ def __init__( self, - ls=None, - ms=None, - ns=None, - amps=(1e-4,), - theta=None, + ls: tuple[int] = None, + ms: tuple[int] = None, + ns: tuple[int] = None, + amps: tuple[float] = (1e-4,), + theta: tuple[float] = None, pfuns=("Id",), pfuns_params=(0.0,), Lx=1.0, Ly=1.0, Lz=1.0, + given_in_basis: GivenInBasis = "0", + comp: int = 0, ): if ls is not None: n_modes = len(ls) @@ -143,26 +162,30 @@ def __init__( else: assert len(pfuns_params) == n_modes - self._ls = ls - self._ms = ms - self._ns = ns - self._amps = amps - self._Lx = Lx - self._Ly = Ly - self._Lz = Lz - self._theta = theta - self._pfuns = [] for pfun, params in zip(pfuns, pfuns_params): if pfun == "Id": self._pfuns += [lambda eta3: 1.0] elif pfun == "localize": self._pfuns += [ - lambda eta3: np.tanh((eta3 - 0.5) / params) / np.cosh((eta3 - 0.5) / params), + lambda eta3: xp.tanh((eta3 - 0.5) / params) / xp.cosh((eta3 - 0.5) / params), ] else: raise ValueError(f"Profile function {pfun} is not defined..") + self._ls = ls + self._ms = ms + self._ns = ns + self._amps = amps + self._Lx = Lx + self._Ly = Ly + self._Lz = Lz + self._theta = theta + + # use the setters + self.given_in_basis = given_in_basis + self.comp = comp + def __call__(self, x, y, z): val = 0.0 @@ -170,10 +193,10 @@ def __call__(self, x, y, z): val += ( amp * pfun(z) - * np.sin( - l * 2.0 * np.pi / self._Lx * x - + m * 2.0 * np.pi / self._Ly * y - + n * 2.0 * np.pi / self._Lz * z + * xp.sin( + l * 2.0 * xp.pi / self._Lx * x + + m * 2.0 * xp.pi / self._Ly * y + + n * 2.0 * xp.pi / self._Lz * z + t, ) ) @@ -181,52 +204,52 @@ def __call__(self, x, y, z): return val -class ModesCos: +class ModesCos(Perturbation): r"""Cosinusoidal function in 3D. .. math:: u(x, y, z) = \sum_{s} A_s \cos \left(l_s \frac{2\pi}{L_x} x + m_s \frac{2\pi}{L_y} y + n_s \frac{2\pi}{L_z} z \right) \,. - Can be used in logical space, where :math:`x \to \eta_1,\, y\to \eta_2,\, z \to \eta_3` + Can be used in logical space (use 'given_in_basis'), where :math:`x \to \eta_1,\, y\to \eta_2,\, z \to \eta_3` and :math:`L_x=L_y=L_z=1.0` (default). Parameters ---------- - ls : tuple | list + ls : tuple[int] Mode numbers in x-direction (kx = l*2*pi/Lx). - ms : tuple | list + ms : tuple[int] Mode numbers in y-direction (ky = m*2*pi/Ly). - ns : tuple | list + ns : tuple[int] Mode numbers in z-direction (kz = n*2*pi/Lz). - amps : tuple | list + amps : tuple[float] Amplitude of each mode. Lx, Ly, Lz : float Domain lengths. - Note - ---- - Example of use in a ``.yml`` parameter file:: - - perturbations : - type : ModesCos - ModesCos : - comps : - scalar_name : '0' # choices: null, 'physical', '0', '3' - vector_name : [null , 'v', '2'] # choices: null, 'physical', '1', '2', 'v', 'norm' - ls : - scalar_name: [1, 3] # two x-modes for scalar variable - vector_name: [null, [0, 1], [4]] # two x-modes for 2nd comp. and one x-mode for third component of vector-valued variable - Lx : 7.853981633974483 - Ly : 1. - Lz : 1. + given_in_basis : str + In which basis the perturbation is represented (see base class). + + comp : int + Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) """ - def __init__(self, ls=None, ms=None, ns=None, amps=(1e-4,), Lx=1.0, Ly=1.0, Lz=1.0): + def __init__( + self, + ls: tuple[int] = None, + ms: tuple[int] = None, + ns: tuple[int] = None, + amps: tuple[float] = (1e-4,), + Lx=1.0, + Ly=1.0, + Lz=1.0, + given_in_basis: GivenInBasis = "0", + comp: int = 0, + ): if ls is not None: n_modes = len(ls) elif ms is not None: @@ -265,18 +288,22 @@ def __init__(self, ls=None, ms=None, ns=None, amps=(1e-4,), Lx=1.0, Ly=1.0, Lz=1 self._Ly = Ly self._Lz = Lz + # use the setters + self.given_in_basis = given_in_basis + self.comp = comp + def __call__(self, x, y, z): val = 0.0 for amp, l, m, n in zip(self._amps, self._ls, self._ms, self._ns): - val += amp * np.cos( - l * 2.0 * np.pi / self._Lx * x + m * 2.0 * np.pi / self._Ly * y + n * 2.0 * np.pi / self._Lz * z, + val += amp * xp.cos( + l * 2.0 * xp.pi / self._Lx * x + m * 2.0 * xp.pi / self._Ly * y + n * 2.0 * xp.pi / self._Lz * z, ) # print( "Cos max value", val.max()) return val -class CoaxialWaveguideElectric_r: +class CoaxialWaveguideElectric_r(Perturbation): r"""Initializes function for Coaxial Waveguide electric field in radial direction. Solutions taken from TUM master thesis of Alicia Robles Pérez: @@ -299,21 +326,25 @@ def __init__(self, m=1, a1=1.0, a2=2.0, a=1, b=-0.28): self._a = a self._b = b + # use the setters + self.given_in_basis = "norm" + self.comp = 0 + def __call__(self, eta1, eta2, eta3): val = 0.0 r = eta1 * (self._r2 - self._r1) + self._r1 - theta = eta2 * 2.0 * np.pi + theta = eta2 * 2.0 * xp.pi val += ( -self._m / r - * np.cos(self._m * theta) + * xp.cos(self._m * theta) * (self._a * scipy.special.jv(self._m, r) + self._b * scipy.special.yn(self._m, r)) ) return val -class CoaxialWaveguideElectric_theta: +class CoaxialWaveguideElectric_theta(Perturbation): r""" Initializes funtion for Coaxial Waveguide electric field in the azimuthal direction. @@ -337,19 +368,23 @@ def __init__(self, m=1, a1=1.0, a2=2.0, a=1, b=-0.28): self._a = a self._b = b + # use the setters + self.given_in_basis = "norm" + self.comp = 1 + def __call__(self, eta1, eta2, eta3): val = 0.0 r = eta1 * (self._r2 - self._r1) + self._r1 - theta = eta2 * 2.0 * np.pi + theta = eta2 * 2.0 * xp.pi val += ( self._a * ((self._m / r) * scipy.special.jv(self._m, r) - scipy.special.jv(self._m + 1, r)) + (self._b * ((self._m / r) * scipy.special.yn(self._m, r) - scipy.special.yn(self._m + 1, r))) - ) * np.sin(self._m * theta) + ) * xp.sin(self._m * theta) return val -class CoaxialWaveguideMagnetic: +class CoaxialWaveguideMagnetic(Perturbation): r"""Initializes funtion for Coaxial Waveguide magnetic field in $z$-direction. Solutions taken from TUM master thesis of Alicia Robles Pérez: @@ -372,19 +407,23 @@ def __init__(self, m=1, a1=1.0, a2=2.0, a=1, b=-0.28): self._a = a self._b = b + # use the setters + self.given_in_basis = "norm" + self.comp = 2 + def __call__(self, eta1, eta2, eta3): val = 0.0 r = eta1 * (self._r2 - self._r1) + self._r1 - theta = eta2 * 2.0 * np.pi + theta = eta2 * 2.0 * xp.pi z = eta3 - val += (self._a * scipy.special.jv(self._m, r) + self._b * scipy.special.yn(self._m, r)) * np.cos( + val += (self._a * scipy.special.jv(self._m, r) + self._b * scipy.special.yn(self._m, r)) * xp.cos( self._m * theta ) return val -class ModesCosCos: +class ModesCosCos(Perturbation): r""" .. math:: @@ -409,6 +448,8 @@ def __init__( Lx=1.0, Ly=1.0, Lz=1.0, + given_in_basis: GivenInBasis = "0", + comp: int = 0, ): if ls is not None: n_modes = len(ls) @@ -468,23 +509,27 @@ def __init__( if pfun == "Id": self._pfuns += [lambda z: 1.0] elif pfun == "localize": - self._pfuns += [lambda z, p=params: np.tanh((z - 0.5) / p) / np.cosh((z - 0.5) / p)] + self._pfuns += [lambda z, p=params: xp.tanh((z - 0.5) / p) / xp.cosh((z - 0.5) / p)] else: raise ValueError(f"Profile function {pfun} is not defined..") + # use the setters + self.given_in_basis = given_in_basis + self.comp = comp + def __call__(self, x, y, z): val = 0.0 for amp, l, m, thx, thy, pfun in zip(self._amps, self._ls, self._ms, self._theta_x, self._theta_y, self._pfuns): val += ( amp * pfun(z) - * np.cos(l * 2.0 * np.pi / self._Lx * x + thx) - * np.cos(m * 2.0 * np.pi / self._Ly * y + thy) + * xp.cos(l * 2.0 * xp.pi / self._Lx * x + thx) + * xp.cos(m * 2.0 * xp.pi / self._Ly * y + thy) ) return val -class ModesSinSin: +class ModesSinSin(Perturbation): r""" .. math:: @@ -508,6 +553,8 @@ def __init__( Lx=1.0, Ly=1.0, Lz=1.0, + given_in_basis: GivenInBasis = "0", + comp: int = 0, ): if ls is not None: n_modes = len(ls) @@ -567,23 +614,27 @@ def __init__( if pfun == "Id": self._pfuns += [lambda z: 1.0] elif pfun == "localize": - self._pfuns += [lambda z, p=params: np.tanh((z - 0.5) / p) / np.cosh((z - 0.5) / p)] + self._pfuns += [lambda z, p=params: xp.tanh((z - 0.5) / p) / xp.cosh((z - 0.5) / p)] else: raise ValueError(f"Profile function {pfun} is not defined..") + # use the setters + self.given_in_basis = given_in_basis + self.comp = comp + def __call__(self, x, y, z): val = 0.0 for amp, l, m, thx, thy, pfun in zip(self._amps, self._ls, self._ms, self._theta_x, self._theta_y, self._pfuns): val += ( amp * pfun(z) - * np.sin(l * 2.0 * np.pi / self._Lx * x + thx) - * np.sin(m * 2.0 * np.pi / self._Ly * y + thy) + * xp.sin(l * 2.0 * xp.pi / self._Lx * x + thx) + * xp.sin(m * 2.0 * xp.pi / self._Ly * y + thy) ) return val -class ModesSinCos: +class ModesSinCos(Perturbation): r""" .. math:: @@ -607,6 +658,8 @@ def __init__( Lx=1.0, Ly=1.0, Lz=1.0, + given_in_basis: GivenInBasis = "0", + comp: int = 0, ): # number of modes if ls is not None: @@ -668,23 +721,27 @@ def __init__( if pfun == "Id": self._pfuns += [lambda z: 1.0] elif pfun == "localize": - self._pfuns += [lambda z, p=params: np.tanh((z - 0.5) / p) / np.cosh((z - 0.5) / p)] + self._pfuns += [lambda z, p=params: xp.tanh((z - 0.5) / p) / xp.cosh((z - 0.5) / p)] else: raise ValueError(f"Profile function {pfun} is not defined..") + # use the setters + self.given_in_basis = given_in_basis + self.comp = comp + def __call__(self, x, y, z): val = 0.0 for amp, l, m, thx, thy, pfun in zip(self._amps, self._ls, self._ms, self._theta_x, self._theta_y, self._pfuns): val += ( amp * pfun(z) - * np.sin(l * 2.0 * np.pi / self._Lx * x + thx) - * np.cos(m * 2.0 * np.pi / self._Ly * y + thy) + * xp.sin(l * 2.0 * xp.pi / self._Lx * x + thx) + * xp.cos(m * 2.0 * xp.pi / self._Ly * y + thy) ) return val -class ModesCosSin: +class ModesCosSin(Perturbation): r""" .. math:: @@ -708,6 +765,8 @@ def __init__( Lx=1.0, Ly=1.0, Lz=1.0, + given_in_basis: GivenInBasis = "0", + comp: int = 0, ): # number of modes if ls is not None: @@ -769,23 +828,27 @@ def __init__( if pfun == "Id": self._pfuns += [lambda z: 1.0] elif pfun == "localize": - self._pfuns += [lambda z, p=params: np.tanh((z - 0.5) / p) / np.cosh((z - 0.5) / p)] + self._pfuns += [lambda z, p=params: xp.tanh((z - 0.5) / p) / xp.cosh((z - 0.5) / p)] else: raise ValueError(f"Profile function {pfun} is not defined..") + # use the setters + self.given_in_basis = given_in_basis + self.comp = comp + def __call__(self, x, y, z): val = 0.0 for amp, l, m, thx, thy, pfun in zip(self._amps, self._ls, self._ms, self._theta_x, self._theta_y, self._pfuns): val += ( amp * pfun(z) - * np.cos(l * 2.0 * np.pi / self._Lx * x + thx) - * np.sin(m * 2.0 * np.pi / self._Ly * y + thy) + * xp.cos(l * 2.0 * xp.pi / self._Lx * x + thx) + * xp.sin(m * 2.0 * xp.pi / self._Ly * y + thy) ) return val -class TorusModesSin: +class TorusModesSin(Perturbation): r"""Sinusoidal function in the periodic coordinates of a Torus. .. math:: @@ -806,7 +869,7 @@ class TorusModesSin: \end{aligned} \right. - Can only be defined in logical coordinates. + Can ony be used in logical space (use 'given_in_basis'). Parameters ---------- @@ -825,40 +888,25 @@ class TorusModesSin: pfun_params : tuple | list Provides :math:`[r_0, \sigma]` parameters for each "exp" profile fucntion, and l_s for "sin" and "cos". - Note - ---- - In the parameter .yml, use the following template in the section ``fluid/``:: - - perturbations : - type : TorusModesSin - TorusModesSin : - comps : - n3 : null # choices: null, 'physical', '0', '3' - u2 : ['physical', 'v', '2'] # choices: null, 'physical', '1', '2', 'v', 'norm' - p3 : '0' # choices: null, 'physical', '0', '3' - ms : - n3: null # poloidal mode numbers - u2: [[0], [0], [0]] # poloidal mode numbers - p3: [0] # poloidal mode numbers - ns : - n3: null # toroidal mode numbers - u2: [[1], [1], [1]] # toroidal mode numbers - p3: [1] # toroidal mode numbers - amps : - n3: null # amplitudes of each mode - u2: [[0.001], [0.001], [0.001]] # amplitudes of each mode - p3: [0.01] # amplitudes of each mode - pfuns : - n3: null # profile function in eta1-direction ('sin' or 'cos' or 'exp' or 'd_exp') - u2: [['sin'], ['sin'], ['exp']] # profile function in eta1-direction ('sin' or 'cos' or 'exp' or 'd_exp') - p3: [0.01] # profile function in eta1-direction ('sin' or 'cos' or 'exp' or 'd_exp') - pfun_params : - n3: null # Provides [r_0, sigma] parameters for each "exp" and "d_exp" profile fucntion, and l_s for "sin" and "cos" - u2: [2, null, [[0.5, 1.]]] # Provides [r_0, sigma] parameters for each "exp" and "d_exp" profile fucntion, and l_s for "sin" and "cos" - p3: [0.01] # Provides [r_0, sigma] parameters for each "exp" and "d_exp" profile fucntion, and l_s for "sin" and "cos" + given_in_basis : str + In which basis the perturbation is represented (see base class). + + comp : int + Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) """ - def __init__(self, ms=None, ns=None, amps=(1e-4,), pfuns=("sin",), pfun_params=None): + def __init__( + self, + ms=None, + ns=None, + amps=(1e-4,), + pfuns=("sin",), + pfun_params=None, + given_in_basis: GivenInBasis = "0", + comp: int = 0, + ): + assert "physical" not in given_in_basis + if ms is not None: n_modes = len(ms) elif ns is not None: @@ -898,37 +946,41 @@ def __init__(self, ms=None, ns=None, amps=(1e-4,), pfuns=("sin",), pfun_params=N ls = 1 else: ls = params - self._pfuns += [lambda eta1: np.sin(ls * np.pi * eta1)] + self._pfuns += [lambda eta1: xp.sin(ls * xp.pi * eta1)] elif pfun == "exp": self._pfuns += [ - lambda eta1: np.exp(-((eta1 - params[0]) ** 2) / (2 * params[1] ** 2)) - / np.sqrt(2 * np.pi * params[1] ** 2), + lambda eta1: xp.exp(-((eta1 - params[0]) ** 2) / (2 * params[1] ** 2)) + / xp.sqrt(2 * xp.pi * params[1] ** 2), ] elif pfun == "d_exp": self._pfuns += [ lambda eta1: -(eta1 - params[0]) / params[1] ** 2 - * np.exp(-((eta1 - params[0]) ** 2) / (2 * params[1] ** 2)) - / np.sqrt(2 * np.pi * params[1] ** 2), + * xp.exp(-((eta1 - params[0]) ** 2) / (2 * params[1] ** 2)) + / xp.sqrt(2 * xp.pi * params[1] ** 2), ] else: raise ValueError(f"Profile function {pfun} is not defined..") + # use the setters + self.given_in_basis = given_in_basis + self.comp = comp + def __call__(self, eta1, eta2, eta3): val = 0.0 for mi, ni, pfun, amp in zip(self._ms, self._ns, self._pfuns, self._amps): val += ( amp * pfun(eta1) - * np.sin( - mi * 2.0 * np.pi * eta2 + ni * 2.0 * np.pi * eta3, + * xp.sin( + mi * 2.0 * xp.pi * eta2 + ni * 2.0 * xp.pi * eta3, ) ) return val -class TorusModesCos: +class TorusModesCos(Perturbation): r"""Cosinusoidal function in the periodic coordinates of a Torus. .. math:: @@ -949,59 +1001,44 @@ class TorusModesCos: \end{aligned} \right. - Can only be defined in logical coordinates. + Can only be used in logical space (use 'given_in_basis'). Parameters ---------- - ms : tuple | list[int] + ms : tuple[int] Poloidal mode numbers. - ns : tuple | list[int] + ns : tuple[int] Toroidal mode numbers. - pfuns : tuple | list[str] + pfuns : tuple[str] "sin" or "cos" or "exp" to define the profile functions. - amps : tuple | list[float] + amps : tuple[float] Amplitudes of each mode (m_i, n_i). pfun_params : tuple | list Provides :math:`[r_0, \sigma]` parameters for each "exp" profile fucntion, and l_s for "sin" and "cos". - Note - ---- - In the parameter .yml, use the following template in the section ``fluid/``:: - - perturbations : - type : TorusModesCos - TorusModesCos : - comps : - n3 : null # choices: null, 'physical', '0', '3' - u2 : ['physical', 'v', '2'] # choices: null, 'physical', '1', '2', 'v', 'norm' - p3 : H1 # choices: null, 'physical', '0', '3' - ms : - n3: null # poloidal mode numbers - u2: [[0], [0], [0]] # poloidal mode numbers - p3: [0] # poloidal mode numbers - ns : - n3: null # toroidal mode numbers - u2: [[1], [1], [1]] # toroidal mode numbers - p3: [1] # toroidal mode numbers - amps : - n3: null # amplitudes of each mode - u2: [[0.001], [0.001], [0.001]] # amplitudes of each mode - p3: [0.01] # amplitudes of each mode - pfuns : - n3: null # profile function in eta1-direction ('sin' or 'cos' or 'exp' or 'd_exp') - u2: [['sin'], ['sin'], ['exp']] # profile function in eta1-direction ('sin' or 'cos' or 'exp' or 'd_exp') - p3: [0.01] # profile function in eta1-direction ('sin' or 'cos' or 'exp' or 'd_exp') - pfun_params : - n3: null # Provides [r_0, sigma] parameters for each "exp" and "d_exp" profile fucntion, and l_s for "sin" and "cos". - u2: [2, null, [[0.5, 1.]]] # Provides [r_0, sigma] parameters for each "exp" and "d_exp" profile fucntion, and l_s for "sin" and "cos". - p3: [0.01] # Provides [r_0, sigma] parameters for each "exp" and "d_exp" profile fucntion, and l_s for "sin" and "cos". + given_in_basis : str + In which basis the perturbation is represented (see base class). + + comp : int + Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) """ - def __init__(self, ms=None, ns=None, amps=(1e-4,), pfuns=("sin",), pfun_params=None): + def __init__( + self, + ms: tuple = (2,), + ns: tuple = (1,), + amps: tuple = (0.1,), + pfuns: tuple = ("sin",), + pfun_params=None, + given_in_basis: GivenInBasis = "0", + comp: int = 0, + ): + assert "physical" not in given_in_basis + if ms is not None: n_modes = len(ms) elif ns is not None: @@ -1041,41 +1078,45 @@ def __init__(self, ms=None, ns=None, amps=(1e-4,), pfuns=("sin",), pfun_params=N ls = 1 else: ls = params - self._pfuns += [lambda eta1: np.sin(ls * np.pi * eta1)] + self._pfuns += [lambda eta1: xp.sin(ls * xp.pi * eta1)] elif pfun == "cos": - self._pfuns += [lambda eta1: np.cos(np.pi * eta1)] + self._pfuns += [lambda eta1: xp.cos(xp.pi * eta1)] elif pfun == "exp": self._pfuns += [ - lambda eta1: np.exp(-((eta1 - params[0]) ** 2) / (2 * params[1] ** 2)) - / np.sqrt(2 * np.pi * params[1] ** 2), + lambda eta1: xp.exp(-((eta1 - params[0]) ** 2) / (2 * params[1] ** 2)) + / xp.sqrt(2 * xp.pi * params[1] ** 2), ] elif pfun == "d_exp": self._pfuns += [ lambda eta1: -(eta1 - params[0]) / params[1] ** 2 - * np.exp(-((eta1 - params[0]) ** 2) / (2 * params[1] ** 2)) - / np.sqrt(2 * np.pi * params[1] ** 2), + * xp.exp(-((eta1 - params[0]) ** 2) / (2 * params[1] ** 2)) + / xp.sqrt(2 * xp.pi * params[1] ** 2), ] else: raise ValueError( 'Profile function must be "sin" or "cos" or "exp".', ) + # use the setters + self.given_in_basis = given_in_basis + self.comp = comp + def __call__(self, eta1, eta2, eta3): val = 0.0 for mi, ni, pfun, amp in zip(self._ms, self._ns, self._pfuns, self._amps): val += ( amp * pfun(eta1) - * np.cos( - mi * 2.0 * np.pi * eta2 + ni * 2.0 * np.pi * eta3, + * xp.cos( + mi * 2.0 * xp.pi * eta2 + ni * 2.0 * xp.pi * eta3, ) ) return val -class Shear_x: +class Shear_x(Perturbation): r"""Double shear layer in eta1 (-1 in outer regions, 1 in inner regions). .. math:: @@ -1092,32 +1133,36 @@ class Shear_x: delta : float Characteristic size of the shear layer - Note - ---- - In the parameter .yml, use the following in the section ``fluid/``:: - - perturbations : - type : Shear_x - Shear_x : - comps : - rho3 : null # choices: null, 'physical', '0', '3' - uv : ['physical', 'v', '2'] # choices: null, 'physical', '1', '2', 'v', 'norm' - s3 : H1 # choices: null, 'physical', '0', '3' - amp : 0.001 # amplitudes of each mode - delta : 0.03333 # characteristic size of the shear layer + given_in_basis : str + In which basis the perturbation is represented (see base class). + + comp : int + Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) """ - def __init__(self, amp=1e-4, delta=1 / 15): + def __init__( + self, + amp=1e-4, + delta=1 / 15, + given_in_basis: GivenInBasis = "0", + comp: int = 0, + ): + assert "physical" not in given_in_basis, f"Perturbation {self.__name__} can only be used in logical space." + self._amp = amp self._delta = delta + # use the setters + self.given_in_basis = given_in_basis + self.comp = comp + def __call__(self, e1, e2, e3): - val = self._amp * (-np.tanh((e1 - 0.75) / self._delta) + np.tanh((e1 - 0.25) / self._delta) - 1) + val = self._amp * (-xp.tanh((e1 - 0.75) / self._delta) + xp.tanh((e1 - 0.25) / self._delta) - 1) return val -class Shear_y: +class Shear_y(Perturbation): r"""Double shear layer in eta2 (-1 in outer regions, 1 in inner regions). .. math:: @@ -1134,32 +1179,36 @@ class Shear_y: delta : float Characteristic size of the shear layer - Note - ---- - In the parameter .yml, use the following in the section ``fluid/``:: - - perturbations : - type : Shear_y - Shear_y : - comps : - rho3 : null # choices: null, 'physical', '0', '3' - uv : ['physical', 'v', '2'] # choices: null, 'physical', '1', '2', 'v', 'norm' - s3 : H1 # choices: null, 'physical', '0', '3' - amp : 0.001 # amplitudes of each mode - delta : 0.03333 # characteristic size of the shear layer + given_in_basis : str + In which basis the perturbation is represented (see base class). + + comp : int + Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) """ - def __init__(self, amp=1e-4, delta=1 / 15): + def __init__( + self, + amp=1e-4, + delta=1 / 15, + given_in_basis: GivenInBasis = "0", + comp: int = 0, + ): + assert "physical" not in given_in_basis, f"Perturbation {self.__name__} can only be used in logical space." + self._amp = amp self._delta = delta + # use the setters + self.given_in_basis = given_in_basis + self.comp = comp + def __call__(self, e1, e2, e3): - val = self._amp * (-np.tanh((e2 - 0.75) / self._delta) + np.tanh((e2 - 0.25) / self._delta) - 1) + val = self._amp * (-xp.tanh((e2 - 0.75) / self._delta) + xp.tanh((e2 - 0.25) / self._delta) - 1) return val -class Shear_z: +class Shear_z(Perturbation): r"""Double shear layer in eta3 (-1 in outer regions, 1 in inner regions). .. math:: @@ -1176,32 +1225,36 @@ class Shear_z: delta : float Characteristic size of the shear layer - Note - ---- - In the parameter .yml, use the following in the section ``fluid/``:: - - perturbations : - type : Shear_y - Shear_y : - comps : - rho3 : null # choices: null, 'physical', '0', '3' - uv : ['physical', 'v', '2'] # choices: null, 'physical', '1', '2', 'v', 'norm' - s3 : H1 # choices: null, 'physical', '0', '3' - amp : 0.001 # amplitudes of each mode - delta : 0.03333 # characteristic size of the shear layer + given_in_basis : str + In which basis the perturbation is represented (see base class). + + comp : int + Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) """ - def __init__(self, amp=1e-4, delta=1 / 15): + def __init__( + self, + amp=1e-4, + delta=1 / 15, + given_in_basis: GivenInBasis = "0", + comp: int = 0, + ): + assert "physical" not in given_in_basis, f"Perturbation {self.__name__} can only be used in logical space." + self._amp = amp self._delta = delta + # use the setters + self.given_in_basis = given_in_basis + self.comp = comp + def __call__(self, e1, e2, e3): - val = self._amp * (-np.tanh((e3 - 0.75) / self._delta) + np.tanh((e3 - 0.25) / self._delta) - 1) + val = self._amp * (-xp.tanh((e3 - 0.75) / self._delta) + xp.tanh((e3 - 0.25) / self._delta) - 1) return val -class Erf_z: +class Erf_z(Perturbation): r"""Shear layer in eta3 (-1 in lower regions, 1 in upper regions). .. math:: @@ -1218,25 +1271,29 @@ class Erf_z: delta : float Characteristic size of the shear layer - Note - ---- - In the parameter .yml, use the following in the section ``fluid/``:: - - perturbations : - type : Erf_z - Erf_z : - comps : - b2 : ['2', null, null] # choices: null, 'physical', '0', '3' - amp : - b2 : [0.001] # amplitudes of each mode - delta : - b2 : [0.02] # characteristic size of the shear layer + given_in_basis : str + In which basis the perturbation is represented (see base class). + + comp : int + Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) """ - def __init__(self, amp=1e-4, delta=1 / 15): + def __init__( + self, + amp=1e-4, + delta=1 / 15, + given_in_basis: GivenInBasis = "0", + comp: int = 0, + ): + assert "physical" not in given_in_basis, f"Perturbation {self.__name__} can only be used in logical space." + self._amp = amp self._delta = delta + # use the setters + self.given_in_basis = given_in_basis + self.comp = comp + def __call__(self, e1, e2, e3): from scipy.special import erf @@ -1245,7 +1302,7 @@ def __call__(self, e1, e2, e3): return val -class RestelliAnalyticSolutionVelocity: +class RestelliAnalyticSolutionVelocity(Perturbation): r"""Analytic solution :math:`u=u_e` of the system: .. math:: @@ -1274,8 +1331,6 @@ class RestelliAnalyticSolutionVelocity: Parameters ---------- - comp : string - Which component of the solution ('0', '1' or '2'). a : float Minor radius of torus (default: 1.). R0 : float @@ -1288,6 +1343,8 @@ class RestelliAnalyticSolutionVelocity: (default: 0.1) beta : float (default: 1.0) + comp : int + Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) References ---------- @@ -1295,8 +1352,16 @@ class RestelliAnalyticSolutionVelocity: in plasma physics, Journal of Computational Physics 2018. """ - def __init__(self, comp="0", a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1.0): - self._comp = comp + def __init__( + self, + a=1.0, + R0=2.0, + B0=10.0, + Bp=12.5, + alpha=0.1, + beta=1.0, + comp: int = 0, + ): self._a = a self._R0 = R0 self._B0 = B0 @@ -1304,12 +1369,16 @@ def __init__(self, comp="0", a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1. self._alpha = alpha self._beta = beta + # use the setters + self.given_in_basis = "physical" + self.comp = comp + # equilibrium ion velocity def __call__(self, x, y, z): """Velocity of ions and electrons.""" - R = np.sqrt(x**2 + y**2) - R = np.where(R == 0.0, 1e-9, R) - phi = np.arctan2(-y, x) + R = xp.sqrt(x**2 + y**2) + R = xp.where(R == 0.0, 1e-9, R) + phi = xp.arctan2(-y, x) ustarR = ( self._alpha * R / (self._a * self._R0) * (-z) + self._beta * self._Bp * self._R0 / (self._B0 * self._a * R) * z @@ -1326,20 +1395,20 @@ def __call__(self, x, y, z): # from cylindrical to cartesian: - if self._comp == "0": - ux = np.cos(phi) * uR - R * np.sin(phi) * uphi + if self.comp == 0: + ux = xp.cos(phi) * uR - R * xp.sin(phi) * uphi return ux - elif self._comp == "1": - uy = -np.sin(phi) * uR - R * np.cos(phi) * uphi + elif self.comp == 1: + uy = -xp.sin(phi) * uR - R * xp.cos(phi) * uphi return uy - elif self._comp == "2": + elif self.comp == 2: uz = uZ return uz else: - raise ValueError(f"Invalid component '{self._comp}'. Must be '0', '1', or '2'.") + raise ValueError(f"Invalid component '{self._comp}'. Must be 0, 1, or 2.") -class RestelliAnalyticSolutionVelocity_2: +class RestelliAnalyticSolutionVelocity_2(Perturbation): r"""Analytic solution :math:`u=u_e` of the system: .. math:: @@ -1368,8 +1437,6 @@ class RestelliAnalyticSolutionVelocity_2: Parameters ---------- - comp : string - Which component of the solution ('0', '1' or '2'). a : float Minor radius of torus (default: 1.). R0 : float @@ -1382,6 +1449,8 @@ class RestelliAnalyticSolutionVelocity_2: (default: 0.1) beta : float (default: 1.0) + comp : int + Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) References ---------- @@ -1389,8 +1458,16 @@ class RestelliAnalyticSolutionVelocity_2: in plasma physics, Journal of Computational Physics 2018. """ - def __init__(self, comp="0", a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1.0): - self._comp = comp + def __init__( + self, + a=1.0, + R0=2.0, + B0=10.0, + Bp=12.5, + alpha=0.1, + beta=1.0, + comp: int = 0, + ): self._a = a self._R0 = R0 self._B0 = B0 @@ -1398,12 +1475,16 @@ def __init__(self, comp="0", a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1. self._alpha = alpha self._beta = beta + # use the setter + self.given_in_basis = "physical" + self.comp = comp + # equilibrium ion velocity def __call__(self, x, y, z): """Velocity of ions and electrons.""" - R = np.sqrt(x**2 + y**2) - R = np.where(R == 0.0, 1e-9, R) - phi = np.arctan2(-y, x) + R = xp.sqrt(x**2 + y**2) + R = xp.where(R == 0.0, 1e-9, R) + phi = xp.arctan2(-y, x) ustarR = ( self._alpha * R / (self._a * self._R0) * (-z) + self._beta * self._Bp * self._R0 / (self._B0 * self._a * R) * z @@ -1420,20 +1501,20 @@ def __call__(self, x, y, z): # from cylindrical to cartesian: - if self._comp == "0": - ux = np.cos(phi) * uR - R * np.sin(phi) * uphi + if self.comp == 0: + ux = xp.cos(phi) * uR - R * xp.sin(phi) * uphi return ux - elif self._comp == "1": - uy = -np.sin(phi) * uR - R * np.cos(phi) * uphi + elif self.comp == 1: + uy = -xp.sin(phi) * uR - R * xp.cos(phi) * uphi return uy - elif self._comp == "2": + elif self.comp == 2: uz = uZ return uz else: raise ValueError(f"Invalid component '{self._comp}'. Must be '0', '1', or '2'.") -class RestelliAnalyticSolutionVelocity_3: +class RestelliAnalyticSolutionVelocity_3(Perturbation): r"""Analytic solution :math:`u=u_e` of the system: .. math:: @@ -1462,8 +1543,6 @@ class RestelliAnalyticSolutionVelocity_3: Parameters ---------- - comp : string - Which component of the solution ('0', '1' or '2'). a : float Minor radius of torus (default: 1.). R0 : float @@ -1476,6 +1555,8 @@ class RestelliAnalyticSolutionVelocity_3: (default: 0.1) beta : float (default: 1.0) + comp : int + Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) References ---------- @@ -1483,8 +1564,16 @@ class RestelliAnalyticSolutionVelocity_3: in plasma physics, Journal of Computational Physics 2018. """ - def __init__(self, comp="0", a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1.0): - self._comp = comp + def __init__( + self, + a=1.0, + R0=2.0, + B0=10.0, + Bp=12.5, + alpha=0.1, + beta=1.0, + comp: int = 0, + ): self._a = a self._R0 = R0 self._B0 = B0 @@ -1492,12 +1581,16 @@ def __init__(self, comp="0", a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1. self._alpha = alpha self._beta = beta + # use the setters + self.given_in_basis = "physical" + self.comp = comp + # equilibrium ion velocity def __call__(self, x, y, z): """Velocity of ions and electrons.""" - R = np.sqrt(x**2 + y**2) - R = np.where(R == 0.0, 1e-9, R) - phi = np.arctan2(-y, x) + R = xp.sqrt(x**2 + y**2) + R = xp.where(R == 0.0, 1e-9, R) + phi = xp.arctan2(-y, x) ustarR = ( self._alpha * R / (self._a * self._R0) * (-z) + self._beta * self._Bp * self._R0 / (self._B0 * self._a * R) * z @@ -1514,20 +1607,20 @@ def __call__(self, x, y, z): # from cylindrical to cartesian: - if self._comp == "0": - ux = np.cos(phi) * uR - R * np.sin(phi) * uphi + if self.comp == 0: + ux = xp.cos(phi) * uR - R * xp.sin(phi) * uphi return ux - elif self._comp == "1": - uy = -np.sin(phi) * uR - R * np.cos(phi) * uphi + elif self.comp == 1: + uy = -xp.sin(phi) * uR - R * xp.cos(phi) * uphi return uy - elif self._comp == "2": + elif self.comp == 2: uz = uZ return uz else: raise ValueError(f"Invalid component '{self._comp}'. Must be '0', '1', or '2'.") -class RestelliAnalyticSolutionPotential: +class RestelliAnalyticSolutionPotential(Perturbation): r"""Analytic solution :math:`\phi` of the system: .. math:: @@ -1583,16 +1676,19 @@ def __init__(self, a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1.0): self._alpha = alpha self._beta = beta + # use the setter + self.given_in_basis = "physical" + # equilibrium potential def __call__(self, x, y, z): """Equilibrium potential.""" - R = np.sqrt(x**2 + y**2) + R = xp.sqrt(x**2 + y**2) pp = 0.5 * self._a * self._B0 * self._alpha * (((R - self._R0) ** 2 + z**2) / self._a**2 - 2.0 / 3.0) return pp -class ManufacturedSolutionVelocity: +class ManufacturedSolutionVelocity(Perturbation): r"""Analytic solutions :math:`u` and :math:`u_e` of the system: .. math:: @@ -1624,38 +1720,49 @@ class ManufacturedSolutionVelocity: Defines the manufactured solution to be selected ('1D' or '2D'). b0 : float Magnetic field (default: 1.0). + comp : int + Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) """ - def __init__(self, species="Ions", comp="0", dimension="1D", b0=1.0): + def __init__( + self, + species="Ions", + dimension="1D", + b0=1.0, + comp: int = 0, + ): self._b = b0 self._species = species - self._comp = comp self._dimension = dimension + # use the setters + self.given_in_basis = "physical" + self.comp = comp + # equilibrium ion velocity def __call__(self, x, y, z): if self._species == "Ions": """Velocity of ions.""" """x component""" if self._dimension == "2D": - ux = -np.sin(2 * np.pi * x) * np.sin(2 * np.pi * y) + ux = -xp.sin(2 * xp.pi * x) * xp.sin(2 * xp.pi * y) elif self._dimension == "1D": - ux = np.sin(2 * np.pi * x) + 1.0 + ux = xp.sin(2 * xp.pi * x) + 1.0 """y component""" if self._dimension == "2D": - uy = -np.cos(2 * np.pi * x) * np.cos(2 * np.pi * y) + uy = -xp.cos(2 * xp.pi * x) * xp.cos(2 * xp.pi * y) elif self._dimension == "1D": - uy = np.cos(2 * np.pi * x) + uy = xp.cos(2 * xp.pi * x) """z component""" uz = 0.0 * x - if self._comp == "0": + if self.comp == 0: return ux - elif self._comp == "1": + elif self.comp == 1: return uy - elif self._comp == "2": + elif self.comp == 2: return uz else: raise ValueError(f"Invalid component '{self._comp}'. Must be '0', '1', or '2'.") @@ -1664,24 +1771,24 @@ def __call__(self, x, y, z): """Velocity of electrons.""" """x component""" if self._dimension == "2D": - ux = -np.sin(4 * np.pi * x) * np.sin(4 * np.pi * y) + ux = -xp.sin(4 * xp.pi * x) * xp.sin(4 * xp.pi * y) elif self._dimension == "1D": - ux = np.sin(2.0 * np.pi * x) + ux = xp.sin(2.0 * xp.pi * x) """y component""" if self._dimension == "2D": - uy = -np.cos(4 * np.pi * x) * np.cos(4 * np.pi * y) + uy = -xp.cos(4 * xp.pi * x) * xp.cos(4 * xp.pi * y) elif self._dimension == "1D": - uy = np.cos(2 * np.pi * x) + uy = xp.cos(2 * xp.pi * x) """z component""" uz = 0.0 * x - if self._comp == "0": + if self.comp == 0: return ux - if self._comp == "1": + if self.comp == 1: return uy - if self._comp == "2": + if self.comp == 2: return uz else: raise ValueError(f"Invalid component '{self._comp}'. Must be '0', '1', or '2'.") @@ -1690,7 +1797,7 @@ def __call__(self, x, y, z): raise ValueError(f"Invalid species '{self._species}'. Must be 'Ions' or 'Electrons'.") -class ManufacturedSolutionPotential: +class ManufacturedSolutionPotential(Perturbation): r"""Analytic solution :math:`\phi` of the system: .. math:: @@ -1730,18 +1837,21 @@ def __init__(self, dimension="1D", b0=1.0): self._ab = b0 self._dimension = dimension + # use the setter + self.given_in_basis = "physical" + # equilibrium ion velocity def __call__(self, x, y, z): """Potential.""" if self._dimension == "2D": - phi = np.cos(2 * np.pi * x) + np.sin(2 * np.pi * y) + phi = xp.cos(2 * xp.pi * x) + xp.sin(2 * xp.pi * y) elif self._dimension == "1D": - phi = np.sin(2.0 * np.pi * x) + phi = xp.sin(2.0 * xp.pi * x) return phi -class ManufacturedSolutionVelocity_2: +class ManufacturedSolutionVelocity_2(Perturbation): r"""Analytic solutions :math:`u` and :math:`u_e` of the system: .. math:: @@ -1767,44 +1877,53 @@ class ManufacturedSolutionVelocity_2: ---------- species : string 'Ions' or 'Electrons'. - comp : string - Which component of the solution ('0', '1' or '2'). dimension: string Defines the manufactured solution to be selected ('1D' or '2D'). b0 : float Magnetic field (default: 1.0). + comp : int + Which component (0, 1 or 2) of vector is perturbed (=0 for scalar-valued functions) """ - def __init__(self, species="Ions", comp="0", dimension="1D", b0=1.0): + def __init__( + self, + species="Ions", + dimension="1D", + b0=1.0, + comp: int = 0, + ): self._b = b0 self._species = species - self._comp = comp self._dimension = dimension + # use the setters + self.given_in_basis = "physical" + self.comp = comp + # equilibrium ion velocity def __call__(self, x, y, z): if self._species == "Ions": """Velocity of ions.""" """x component""" if self._dimension == "2D": - ux = -np.sin(2 * np.pi * x) * np.sin(2 * np.pi * y) + ux = -xp.sin(2 * xp.pi * x) * xp.sin(2 * xp.pi * y) elif self._dimension == "1D": - ux = np.sin(2 * np.pi * x) + 1.0 + ux = xp.sin(2 * xp.pi * x) + 1.0 """y component""" if self._dimension == "2D": - uy = -np.cos(2 * np.pi * x) * np.cos(2 * np.pi * y) + uy = -xp.cos(2 * xp.pi * x) * xp.cos(2 * xp.pi * y) elif self._dimension == "1D": - uy = np.cos(2 * np.pi * x) + uy = xp.cos(2 * xp.pi * x) """z component""" uz = 0.0 * x - if self._comp == "0": + if self.comp == 0: return ux - elif self._comp == "1": + elif self.comp == 1: return uy - elif self._comp == "2": + elif self.comp == 2: return uz else: raise ValueError(f"Invalid component '{self._comp}'. Must be '0', '1', or '2'.") @@ -1813,24 +1932,24 @@ def __call__(self, x, y, z): """Velocity of electrons.""" """x component""" if self._dimension == "2D": - ux = -np.sin(4 * np.pi * x) * np.sin(4 * np.pi * y) + ux = -xp.sin(4 * xp.pi * x) * xp.sin(4 * xp.pi * y) elif self._dimension == "1D": - ux = np.sin(2.0 * np.pi * x) + ux = xp.sin(2.0 * xp.pi * x) """y component""" if self._dimension == "2D": - uy = -np.cos(4 * np.pi * x) * np.cos(4 * np.pi * y) + uy = -xp.cos(4 * xp.pi * x) * xp.cos(4 * xp.pi * y) elif self._dimension == "1D": - uy = np.cos(2 * np.pi * x) + uy = xp.cos(2 * xp.pi * x) """z component""" uz = 0.0 * x - if self._comp == "0": + if self.comp == 0: return ux - if self._comp == "1": + if self.comp == 1: return uy - if self._comp == "2": + if self.comp == 2: return uz else: raise ValueError(f"Invalid component '{self._comp}'. Must be '0', '1', or '2'.") @@ -1839,7 +1958,58 @@ def __call__(self, x, y, z): raise ValueError(f"Invalid species '{self._species}'. Must be 'Ions' or 'Electrons'.") -class TokamakManufacturedSolutionVelocity: +class ITPA_density(Perturbation): + r"""ITPA radial density profile in `A. Könies et al. 2018 `_ + + .. math:: + + n(\eta_1) = n_0*c_3\exp\left[-\frac{c_2}{c_1}\tanh\left(\frac{\eta_1 - c_0}{c_2}\right)\right]\,. + """ + + def __init__( + self, + n0: float = 0.00720655, + c: tuple = (0.491230, 0.298228, 0.198739, 0.521298), + given_in_basis: GivenInBasis = "0", + comp: int = 0, + ): + """ + Parameters + ---------- + n0 : float + ITPA profile density + + c : tuple | list + 4 ITPA profile coefficients + """ + + assert len(c) == 4 + + self._n0 = n0 + self._c = c + + # use the setters + self.given_in_basis = "physical" + self.comp = comp + + def __call__(self, eta1, eta2=None, eta3=None): + val = 0.0 + + if self._c[2] == 0.0: + val = self._c[3] - 0 * eta1 + else: + val = ( + self._n0 + * self._c[3] + * xp.exp( + -self._c[2] / self._c[1] * xp.tanh((eta1 - self._c[0]) / self._c[2]), + ) + ) + + return val + + +class TokamakManufacturedSolutionVelocity(Perturbation): r"""Analytic solution :math:`u=u_e` of the system: .. math:: @@ -1893,7 +2063,16 @@ class TokamakManufacturedSolutionVelocity: in plasma physics, Journal of Computational Physics 2018. """ - def __init__(self, comp="0", a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1.0): + def __init__( + self, + comp=0, + a=1.0, + R0=2.0, + B0=10.0, + Bp=12.5, + alpha=0.1, + beta=1.0, + ): self._comp = comp self._a = a self._R0 = R0 @@ -1902,12 +2081,16 @@ def __init__(self, comp="0", a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1. self._alpha = alpha self._beta = beta + # use the setters + self.given_in_basis = "physical" + self.comp = comp + # equilibrium ion velocity def __call__(self, x, y, z): """Velocity of ions and electrons.""" - R = np.sqrt(x**2 + y**2) - R = np.where(R == 0.0, 1e-9, R) - phi = np.arctan2(-y, x) + R = xp.sqrt(x**2 + y**2) + R = xp.where(R == 0.0, 1e-9, R) + phi = xp.arctan2(-y, x) A = self._alpha / (self._a * self._R0) C = self._beta * self._Bp * self._R0 / (self._B0 * self._a) @@ -1917,20 +2100,20 @@ def __call__(self, x, y, z): # from cylindrical to cartesian: - if self._comp == "0": - ux = np.cos(phi) * uR - R * np.sin(phi) * uphi + if self.comp == 0: + ux = xp.cos(phi) * uR - R * xp.sin(phi) * uphi return ux - elif self._comp == "1": - uy = -np.sin(phi) * uR - R * np.cos(phi) * uphi + elif self.comp == 1: + uy = -xp.sin(phi) * uR - R * xp.cos(phi) * uphi return uy - elif self._comp == "2": + elif self.comp == 2: uz = uZ return uz else: raise ValueError(f"Invalid component '{self._comp}'. Must be '0', '1', or '2'.") -class TokamakManufacturedSolutionVelocity_1: +class TokamakManufacturedSolutionVelocity_1(Perturbation): r"""Analytic solution :math:`u=u_e` of the system: .. math:: @@ -1984,7 +2167,16 @@ class TokamakManufacturedSolutionVelocity_1: in plasma physics, Journal of Computational Physics 2018. """ - def __init__(self, comp="0", a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1.0): + def __init__( + self, + comp=0, + a=1.0, + R0=2.0, + B0=10.0, + Bp=12.5, + alpha=0.1, + beta=1.0, + ): self._comp = comp self._a = a self._R0 = R0 @@ -1993,12 +2185,16 @@ def __init__(self, comp="0", a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1. self._alpha = alpha self._beta = beta + # use the setters + self.given_in_basis = "physical" + self.comp = comp + # equilibrium ion velocity def __call__(self, x, y, z): """Velocity of ions and electrons.""" - R = np.sqrt(x**2 + y**2) - R = np.where(R == 0.0, 1e-9, R) - phi = np.arctan2(-y, x) + R = xp.sqrt(x**2 + y**2) + R = xp.where(R == 0.0, 1e-9, R) + phi = xp.arctan2(-y, x) A = self._alpha / (self._a * self._R0) C = self._beta * self._Bp * self._R0 / (self._B0 * self._a) @@ -2008,20 +2204,20 @@ def __call__(self, x, y, z): # from cylindrical to cartesian: - if self._comp == "0": - ux = np.cos(phi) * uR - R * np.sin(phi) * uphi + if self.comp == 0: + ux = xp.cos(phi) * uR - R * xp.sin(phi) * uphi return ux - elif self._comp == "1": - uy = -np.sin(phi) * uR - R * np.cos(phi) * uphi + elif self.comp == 1: + uy = -xp.sin(phi) * uR - R * xp.cos(phi) * uphi return uy - elif self._comp == "2": + elif self.comp == 2: uz = uZ return uz else: raise ValueError(f"Invalid component '{self._comp}'. Must be '0', '1', or '2'.") -class TokamakManufacturedSolutionVelocity_2: +class TokamakManufacturedSolutionVelocity_2(Perturbation): r"""Analytic solution :math:`u=u_e` of the system: .. math:: @@ -2075,7 +2271,16 @@ class TokamakManufacturedSolutionVelocity_2: in plasma physics, Journal of Computational Physics 2018. """ - def __init__(self, comp="0", a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1.0): + def __init__( + self, + comp=0, + a=1.0, + R0=2.0, + B0=10.0, + Bp=12.5, + alpha=0.1, + beta=1.0, + ): self._comp = comp self._a = a self._R0 = R0 @@ -2084,12 +2289,16 @@ def __init__(self, comp="0", a=1.0, R0=2.0, B0=10.0, Bp=12.5, alpha=0.1, beta=1. self._alpha = alpha self._beta = beta + # use the setters + self.given_in_basis = "physical" + self.comp = comp + # equilibrium ion velocity def __call__(self, x, y, z): """Velocity of ions and electrons.""" - R = np.sqrt(x**2 + y**2) - R = np.where(R == 0.0, 1e-9, R) - phi = np.arctan2(-y, x) + R = xp.sqrt(x**2 + y**2) + R = xp.where(R == 0.0, 1e-9, R) + phi = xp.arctan2(-y, x) A = self._alpha / (self._a * self._R0) C = self._beta * self._Bp * self._R0 / (self._B0 * self._a) @@ -2099,13 +2308,13 @@ def __call__(self, x, y, z): # from cylindrical to cartesian: - if self._comp == "0": - ux = np.cos(phi) * uR - R * np.sin(phi) * uphi + if self.comp == 0: + ux = xp.cos(phi) * uR - R * xp.sin(phi) * uphi return ux - elif self._comp == "1": - uy = -np.sin(phi) * uR - R * np.cos(phi) * uphi + elif self.comp == 1: + uy = -xp.sin(phi) * uR - R * xp.cos(phi) * uphi return uy - elif self._comp == "2": + elif self.comp == 2: uz = uZ return uz else: diff --git a/src/struphy/initial/tests/test_init_perturbations.py b/src/struphy/initial/tests/test_init_perturbations.py index c59565058..2f5fa3176 100644 --- a/src/struphy/initial/tests/test_init_perturbations.py +++ b/src/struphy/initial/tests/test_init_perturbations.py @@ -1,4 +1,5 @@ import inspect +from copy import deepcopy import pytest @@ -19,6 +20,7 @@ def test_init_modes(Nel, p, spl_kind, mapping, combine_comps=None, do_plot=False): """Test the initialization Field.initialize_coeffs with all "Modes" classes in perturbations.py.""" + import cunumpy as xp from matplotlib import pyplot as plt from psydac.ddm.mpi import mpi as MPI @@ -26,7 +28,8 @@ def test_init_modes(Nel, p, spl_kind, mapping, combine_comps=None, do_plot=False from struphy.geometry import domains from struphy.geometry.base import Domain from struphy.initial import perturbations - from struphy.utils.arrays import xp as np + from struphy.initial.base import Perturbation + from struphy.models.variables import FEECVariable comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -47,10 +50,10 @@ def test_init_modes(Nel, p, spl_kind, mapping, combine_comps=None, do_plot=False form_vector = ["1", "2", "v", "norm", "physical_at_eta"] # evaluation points - e1 = np.linspace(0.0, 1.0, 30) - e2 = np.linspace(0.0, 1.0, 40) - e3 = np.linspace(0.0, 1.0, 50) - eee1, eee2, eee3 = np.meshgrid(e1, e2, e3, indexing="ij") + e1 = xp.linspace(0.0, 1.0, 30) + e2 = xp.linspace(0.0, 1.0, 40) + e3 = xp.linspace(0.0, 1.0, 50) + eee1, eee2, eee3 = xp.meshgrid(e1, e2, e3, indexing="ij") # mode paramters kwargs = {} @@ -76,7 +79,7 @@ def test_init_modes(Nel, p, spl_kind, mapping, combine_comps=None, do_plot=False form_vector += ["physical"] for key, val in inspect.getmembers(perturbations): - if inspect.isclass(val): + if inspect.isclass(val) and val.__module__ == perturbations.__name__: print(key, val) if key not in ("ModesCos", "ModesSin", "TorusModesCos", "TorusModesSin"): @@ -88,63 +91,60 @@ def test_init_modes(Nel, p, spl_kind, mapping, combine_comps=None, do_plot=False ): continue - # functions to compare to + # instance of perturbation if "Torus" in key: - fun = val(**kwargs, pfuns=pfuns) + perturbation = val(**kwargs, pfuns=pfuns) else: - fun = val(**kwargs, ls=ls) + perturbation = val(**kwargs, ls=ls) if isinstance(domain, domains.Cuboid) or isinstance(domain, domains.Colella): - fun_xyz = val(**kwargs, ls=ls, Lx=Lx, Ly=Ly, Lz=Lz) + perturbation_xyz = val(**kwargs, ls=ls, Lx=Lx, Ly=Ly, Lz=Lz) + assert isinstance(perturbation, Perturbation) # single component is initialized - for space, name in derham.space_to_form.items(): + for space, form in derham.space_to_form.items(): if do_plot: - plt.figure(key + "_" + name + "-form_e1e2 " + mapping[0], figsize=(24, 16)) - plt.figure(key + "_" + name + "-form_e1e3 " + mapping[0], figsize=(24, 16)) + plt.figure(key + "_" + form + "-form_e1e2 " + mapping[0], figsize=(24, 16)) + plt.figure(key + "_" + form + "-form_e1e3 " + mapping[0], figsize=(24, 16)) - if name in ("0", "3"): + if form in ("0", "3"): for n, fun_form in enumerate(form_scalar): - params = {key: {"given_in_basis": fun_form}} + if "Torus" in key and fun_form == "physical": + continue - if "Modes" in key: - params[key]["ls"] = ls - params[key]["ms"] = kwargs["ms"] - params[key]["ns"] = kwargs["ns"] - params[key]["amps"] = kwargs["amps"] - if fun_form == "physical": - params[key]["Lx"] = Lx - params[key]["Ly"] = Ly - params[key]["Lz"] = Lz + if "Modes" in key and fun_form == "physical": + perturbation._Lx = Lx + perturbation._Ly = Ly + perturbation._Lz = Lz else: - raise ValueError(f'Perturbation {key} not implemented, only "Modes" are testes.') - - if "Torus" in key: - params[key].pop("ls") - if fun_form == "physical": - continue - params[key]["pfuns"] = pfuns + perturbation._Lx = 1.0 + perturbation._Ly = 1.0 + perturbation._Lz = 1.0 + # use the setter + perturbation.given_in_basis = fun_form - field = derham.create_spline_function(name, space, pert_params=params) - field.initialize_coeffs(domain=domain) + var = FEECVariable(space=space) + var.add_perturbation(perturbation) + var.allocate(derham, domain) + field = var.spline - field_vals_xyz = domain.push(field, e1, e2, e3, kind=name) + field_vals_xyz = domain.push(field, e1, e2, e3, kind=form) x, y, z = domain(e1, e2, e3) - r = np.sqrt(x**2 + y**2) + r = xp.sqrt(x**2 + y**2) if fun_form == "physical": - fun_vals_xyz = fun_xyz(x, y, z) + fun_vals_xyz = perturbation_xyz(x, y, z) elif fun_form == "physical_at_eta": - fun_vals_xyz = fun(eee1, eee2, eee3) + fun_vals_xyz = perturbation(eee1, eee2, eee3) else: - fun_vals_xyz = domain.push(fun, eee1, eee2, eee3, kind=fun_form) + fun_vals_xyz = domain.push(perturbation, eee1, eee2, eee3, kind=fun_form) - error = np.max(np.abs(field_vals_xyz - fun_vals_xyz)) / np.max(np.abs(fun_vals_xyz)) - print(f"{rank=}, {key=}, {name=}, {fun_form=}, {error=}") + error = xp.max(xp.abs(field_vals_xyz - fun_vals_xyz)) / xp.max(xp.abs(fun_vals_xyz)) + print(f"{rank=}, {key=}, {form=}, {fun_form=}, {error=}") assert error < 0.02 if do_plot: - plt.figure(key + "_" + name + "-form_e1e2 " + mapping[0]) + plt.figure(key + "_" + form + "-form_e1e2 " + mapping[0]) plt.subplot(2, 4, n + 1) if isinstance(domain, domains.HollowTorus): plt.contourf(r[:, :, 0], z[:, :, 0], field_vals_xyz[:, :, 0]) @@ -173,7 +173,7 @@ def test_init_modes(Nel, p, spl_kind, mapping, combine_comps=None, do_plot=False ax = plt.gca() ax.set_aspect("equal", adjustable="box") - plt.figure(key + "_" + name + "-form_e1e3 " + mapping[0]) + plt.figure(key + "_" + form + "-form_e1e3 " + mapping[0]) plt.subplot(2, 4, n + 1) if isinstance(domain, domains.HollowTorus): plt.contourf(x[:, 0, :], y[:, 0, :], field_vals_xyz[:, 0, :]) @@ -204,6 +204,21 @@ def test_init_modes(Nel, p, spl_kind, mapping, combine_comps=None, do_plot=False else: for n, fun_form in enumerate(form_vector): + if "Torus" in key and fun_form == "physical": + continue + + if "Modes" in key and fun_form == "physical": + perturbation._Lx = Lx + perturbation._Ly = Ly + perturbation._Lz = Lz + else: + perturbation._Lx = 1.0 + perturbation._Ly = 1.0 + perturbation._Lz = 1.0 + perturbation_0 = perturbation + perturbation_1 = deepcopy(perturbation) + perturbation_2 = deepcopy(perturbation) + params = { key: { "given_in_basis": [fun_form] * 3, @@ -217,59 +232,62 @@ def test_init_modes(Nel, p, spl_kind, mapping, combine_comps=None, do_plot=False else: raise ValueError(f'Perturbation {key} not implemented, only "Modes" are testes.') - if "Torus" in key: - # params[key].pop('ls') - if fun_form == "physical": - continue - params[key]["pfuns"] = [pfuns] * 3 - else: - params[key]["ls"] = [ls] * 3 - if fun_form == "physical": - params[key]["Lx"] = Lx - params[key]["Ly"] = Ly - params[key]["Lz"] = Lz - if isinstance(domain, domains.HollowTorus): - continue - - field = derham.create_spline_function(name, space, pert_params=params) - field.initialize_coeffs(domain=domain) - - f1_xyz, f2_xyz, f3_xyz = domain.push(field, e1, e2, e3, kind=name) + if "Torus" not in key and isinstance(domain, domains.HollowTorus): + continue + + # use the setters + perturbation_0.given_in_basis = fun_form + perturbation_0.comp = 0 + perturbation_1.given_in_basis = fun_form + perturbation_1.comp = 1 + perturbation_2.given_in_basis = fun_form + perturbation_2.comp = 2 + + var = FEECVariable(space=space) + var.add_perturbation(perturbation_0) + var.add_perturbation(perturbation_1) + var.add_perturbation(perturbation_2) + var.allocate(derham, domain) + field = var.spline + + f1_xyz, f2_xyz, f3_xyz = domain.push(field, e1, e2, e3, kind=form) f_xyz = [f1_xyz, f2_xyz, f3_xyz] x, y, z = domain(e1, e2, e3) - r = np.sqrt(x**2 + y**2) + r = xp.sqrt(x**2 + y**2) # exact values if fun_form == "physical": - fun1_xyz = fun_xyz(x, y, z) - fun2_xyz = fun_xyz(x, y, z) - fun3_xyz = fun_xyz(x, y, z) + fun1_xyz = perturbation_xyz(x, y, z) + fun2_xyz = perturbation_xyz(x, y, z) + fun3_xyz = perturbation_xyz(x, y, z) elif fun_form == "physical_at_eta": - fun1_xyz = fun(eee1, eee2, eee3) - fun2_xyz = fun(eee1, eee2, eee3) - fun3_xyz = fun(eee1, eee2, eee3) + fun1_xyz = perturbation(eee1, eee2, eee3) + fun2_xyz = perturbation(eee1, eee2, eee3) + fun3_xyz = perturbation(eee1, eee2, eee3) elif fun_form == "norm": tmp1, tmp2, tmp3 = domain.transform( - [fun, fun, fun], eee1, eee2, eee3, kind=fun_form + "_to_v" + [perturbation, perturbation, perturbation], eee1, eee2, eee3, kind=fun_form + "_to_v" ) fun1_xyz, fun2_xyz, fun3_xyz = domain.push([tmp1, tmp2, tmp3], eee1, eee2, eee3, kind="v") else: - fun1_xyz, fun2_xyz, fun3_xyz = domain.push([fun, fun, fun], eee1, eee2, eee3, kind=fun_form) + fun1_xyz, fun2_xyz, fun3_xyz = domain.push( + [perturbation, perturbation, perturbation], eee1, eee2, eee3, kind=fun_form + ) fun_xyz_vec = [fun1_xyz, fun2_xyz, fun3_xyz] error = 0.0 for fi, funi in zip(f_xyz, fun_xyz_vec): - error += np.max(np.abs(fi - funi)) / np.max(np.abs(funi)) + error += xp.max(xp.abs(fi - funi)) / xp.max(xp.abs(funi)) error /= 3.0 - print(f"{rank=}, {key=}, {name=}, {fun_form=}, {error=}") + print(f"{rank=}, {key=}, {form=}, {fun_form=}, {error=}") assert error < 0.02 if do_plot: rn = len(form_vector) for c, (fi, f) in enumerate(zip(f_xyz, fun_xyz_vec)): - plt.figure(key + "_" + name + "-form_e1e2 " + mapping[0]) + plt.figure(key + "_" + form + "-form_e1e2 " + mapping[0]) plt.subplot(3, rn, rn * c + n + 1) if isinstance(domain, domains.HollowTorus): plt.contourf(r[:, :, 0], z[:, :, 0], fi[:, :, 0]) @@ -286,7 +304,7 @@ def test_init_modes(Nel, p, spl_kind, mapping, combine_comps=None, do_plot=False ax = plt.gca() ax.set_aspect("equal", adjustable="box") - plt.figure(key + "_" + name + "-form_e1e3 " + mapping[0]) + plt.figure(key + "_" + form + "-form_e1e3 " + mapping[0]) plt.subplot(3, rn, rn * c + n + 1) if isinstance(domain, domains.HollowTorus): plt.contourf(x[:, 0, :], y[:, 0, :], fi[:, 0, :]) @@ -308,8 +326,8 @@ def test_init_modes(Nel, p, spl_kind, mapping, combine_comps=None, do_plot=False if __name__ == "__main__": - mapping = ["Colella", {"Lx": 4.0, "Ly": 5.0, "alpha": 0.07, "Lz": 6.0}] - # mapping = ['HollowCylinder', {'a1': 0.1}] + # mapping = ['Colella', {'Lx': 4., 'Ly': 5., 'alpha': .07, 'Lz': 6.}] + mapping = ["HollowCylinder", {"a1": 0.1}] # mapping = ['Cuboid', {'l1': 0., 'r1': 4., 'l2': 0., 'r2': 5., 'l3': 0., 'r3': 6.}] test_init_modes([16, 16, 16], [2, 3, 4], [False, True, True], mapping, combine_comps=None, do_plot=False) # mapping = ["HollowTorus", {"tor_period": 1}] diff --git a/src/struphy/initial/utilities.py b/src/struphy/initial/utilities.py index da5edd45a..7af042249 100644 --- a/src/struphy/initial/utilities.py +++ b/src/struphy/initial/utilities.py @@ -1,10 +1,10 @@ import os +import cunumpy as xp import h5py from struphy.fields_background.equils import set_defaults from struphy.io.output_handling import DataContainer -from struphy.utils.arrays import xp as np class InitFromOutput: @@ -98,6 +98,6 @@ def __init__( self._amp = amp def __call__(self, x, y, z): - val = self._amp * np.random.rand(*x.shape).squeeze() + val = self._amp * xp.random.rand(*x.shape).squeeze() return val diff --git a/src/struphy/io/inp/params_Maxwell.py b/src/struphy/io/inp/params_Maxwell.py new file mode 100644 index 000000000..984b81620 --- /dev/null +++ b/src/struphy/io/inp/params_Maxwell.py @@ -0,0 +1,63 @@ +from struphy import main +from struphy.fields_background import equils +from struphy.geometry import domains +from struphy.initial import perturbations +from struphy.io.options import DerhamOptions, EnvironmentOptions, FieldsBackground, Time, Units +from struphy.kinetic_background import maxwellians + +# import model, set verbosity +from struphy.models.toy import Maxwell as Model +from struphy.topology import grids + +verbose = True + +# environment options +env = EnvironmentOptions() + +# units +units = Units() + +# time stepping +time_opts = Time() + +# geometry +domain = domains.Cuboid() + +# fluid equilibrium (can be used as part of initial conditions) +equil = equils.HomogenSlab() + +# grid +grid = grids.TensorProductGrid() + +# derham options +derham_opts = DerhamOptions() + +# light-weight model instance +model = Model() + +# propagator options +model.propagators.maxwell.set_options() + +# initial conditions (background + perturbation) +model.em_fields.b_field.add_background(FieldsBackground()) +model.em_fields.b_field.add_perturbation(perturbations.TorusModesCos(given_in_basis="v", comp=0)) +model.em_fields.b_field.add_perturbation(perturbations.TorusModesCos(given_in_basis="v", comp=1)) +model.em_fields.b_field.add_perturbation(perturbations.TorusModesCos(given_in_basis="v", comp=2)) + +# optional: exclude variables from saving +# model.em_fields.b_field.save_data = False + +if __name__ == "__main__": + # start run + main.run( + model, + params_path=__file__, + env=env, + units=units, + time_opts=time_opts, + domain=domain, + equil=equil, + grid=grid, + derham_opts=derham_opts, + verbose=verbose, + ) diff --git a/src/struphy/io/inp/params_Maxwell_lw.py b/src/struphy/io/inp/params_Maxwell_lw.py new file mode 100644 index 000000000..f39582146 --- /dev/null +++ b/src/struphy/io/inp/params_Maxwell_lw.py @@ -0,0 +1,66 @@ +from struphy.fields_background import equils +from struphy.geometry import domains +from struphy.initial import perturbations +from struphy.io.options import DerhamOptions, FieldsBackground, Time, Units +from struphy.kinetic_background import maxwellians + +# import model +from struphy.models.toy import Maxwell as Model +from struphy.topology import grids + +# light-weight model instance +model = Model() + +# units +units = Units() + +# geometry +domain = domains.Cuboid() + +# fluid equilibrium (can be used as part of initial conditions) +equil = equils.HomogenSlab() + +# time +time = Time() + +# grid +grid = grids.TensorProductGrid(Nel=(12, 14, 1)) + +# derham options +derham = DerhamOptions(p=(2, 3, 1), spl_kind=(False, True, True)) + +# propagator options +model.propagators.maxwell.set_options(algo="explicit") + +# initial conditions (background + perturbation) +model.em_fields.e_field.add_background( + FieldsBackground( + type="LogicalConst", + values=(0.3, 0.15, None), + ), +) +model.em_fields.e_field.add_perturbation( + perturbations.TorusModesCos( + ms=[1, 3], + given_in_basis="v", + comp=1, + ), +) + +model.em_fields.b_field.add_background( + FieldsBackground( + type="LogicalConst", + values=(0.3, 0.15, None), + ), +) +model.em_fields.b_field.add_perturbation( + perturbations.TorusModesCos( + ms=[1, 3], + given_in_basis="v", + comp=1, + ), +) + +# exclude variables from saving +# model.em_fields.e_field.save_data = False +# model.em_fields.b_field.save_data = False diff --git a/src/struphy/io/inp/verification/Maxwell_coaxial.py b/src/struphy/io/inp/verification/Maxwell_coaxial.py new file mode 100644 index 000000000..394242234 --- /dev/null +++ b/src/struphy/io/inp/verification/Maxwell_coaxial.py @@ -0,0 +1,60 @@ +from struphy.fields_background import equils +from struphy.geometry import domains +from struphy.initial import perturbations +from struphy.io.options import DerhamOptions, FieldsBackground, Time, Units +from struphy.kinetic_background import maxwellians + +# import model +from struphy.models.toy import Maxwell as Model +from struphy.topology import grids + +verbose = True + +# units +units = Units() + +# geometry +a1 = 2.326744 +a2 = 3.686839 +domain = domains.HollowCylinder(a1=a1, a2=a2, Lz=2.0) + +# fluid equilibrium (can be used as part of initial conditions) +equil = equils.HomogenSlab() + +# time +time = Time(dt=0.05, Tend=10.0) + +# grid +grid = grids.TensorProductGrid( + Nel=(32, 64, 1), + p=(3, 3, 1), + spl_kind=(False, True, True), + dirichlet_bc=[[True, True], [False, False], [False, False]], +) + +# derham options +derham = DerhamOptions() + +# light-weight instance of model +model = Model(units, domain, equil, verbose=verbose) +# model.fluid.set_phys_params("mhd", options.PhysParams()) +# model.kinetic.set_phys_params("mhd", options.PhysParams()) + +# propagator options +model.propagators.maxwell.set_options(algo="implicit") + +# initial conditions for model variables (background + perturbation) +model.em_fields.e_field.add_perturbation( + perturbations.CoaxialWaveguideElectric_r(m=3, a1=a1, a2=a2), + verbose=verbose, +) + +model.em_fields.e_field.add_perturbation( + perturbations.CoaxialWaveguideElectric_theta(m=3, a1=a1, a2=a2), + verbose=verbose, +) + +model.em_fields.b_field.add_perturbation( + perturbations.CoaxialWaveguideMagnetic(m=3, a1=a1, a2=a2), + verbose=verbose, +) diff --git a/src/struphy/io/options.py b/src/struphy/io/options.py new file mode 100644 index 000000000..cecd4b72c --- /dev/null +++ b/src/struphy/io/options.py @@ -0,0 +1,363 @@ +import os +from dataclasses import dataclass +from typing import Literal, get_args + +import cunumpy as xp +from psydac.ddm.mpi import mpi as MPI + +from struphy.physics.physics import ConstantsOfNature + +## Literal options + +# time +SplitAlgos = Literal["LieTrotter", "Strang"] + +# derham +PolarRegularity = Literal[-1, 1] +OptsFEECSpace = Literal["H1", "Hcurl", "Hdiv", "L2", "H1vec"] +OptsVecSpace = Literal["Hcurl", "Hdiv", "H1vec"] + +# fields background +BackgroundTypes = Literal["LogicalConst", "FluidEquilibrium"] + +# perturbations +NoiseDirections = Literal["e1", "e2", "e3", "e1e2", "e1e3", "e2e3", "e1e2e3"] +GivenInBasis = Literal["0", "1", "2", "3", "v", "physical", "physical_at_eta", "norm", None] + +# solvers +OptsSymmSolver = Literal["pcg", "cg"] +OptsGenSolver = Literal["pbicgstab", "bicgstab", "GMRES"] +OptsMassPrecond = Literal["MassMatrixPreconditioner", "MassMatrixDiagonalPreconditioner", None] +OptsSaddlePointSolver = Literal["Uzawa", "GMRES"] +OptsDirectSolver = Literal["SparseSolver", "ScipySparse", "InexactNPInverse", "DirectNPInverse"] +OptsNonlinearSolver = Literal["Picard", "Newton"] + +# markers +OptsPICSpace = Literal["Particles6D", "DeltaFParticles6D", "Particles5D", "Particles3D"] +OptsMarkerBC = Literal["periodic", "reflect"] +OptsRecontructBC = Literal["periodic", "mirror", "fixed"] +OptsLoading = Literal[ + "pseudo_random", + "sobol_standard", + "sobol_antithetic", + "external", + "restart", + "tesselation", +] +OptsSpatialLoading = Literal["uniform", "disc"] +OptsMPIsort = Literal["each", "last", None] + +# filters +OptsFilter = Literal["fourier_in_tor", "hybrid", "three_point", None] + +# sph +OptsKernel = Literal[ + "trigonometric_1d", + "gaussian_1d", + "linear_1d", + "trigonometric_2d", + "gaussian_2d", + "linear_2d", + "trigonometric_3d", + "gaussian_3d", + "linear_isotropic_3d", + "linear_3d", +] + + +## Option classes + + +@dataclass +class Time: + """Time stepping options. + + Parameters + ---------- + dt : float + Time step. + + Tend : float + End time. + + split_algo : SplitAlgos + Splitting algorithm (the order of the propagators is defined in the model). + """ + + dt: float = 0.01 + Tend: float = 0.03 + split_algo: SplitAlgos = "LieTrotter" + + def __post_init__(self): + check_option(self.split_algo, SplitAlgos) + + +@dataclass +class BaseUnits: + """ + Base units are passed to __init__, other units derive from these. + + Parameters + ---------- + x : float + Unit of length in meters. + + B : float + Unit of magnetic field in Tesla. + + n : float + Unit of particle number density in 1e20/m^3. + + kBT : float, optional + Unit of internal energy in keV. + Only in effect if the velocity scale is set to 'thermal'. + """ + + x: float = 1.0 + B: float = 1.0 + n: float = 1.0 + kBT: float = None + + +class Units: + """ + Colllects base units and derives other units from these. + """ + + def __init__(self, base: BaseUnits = None): + if base is None: + base = BaseUnits() + + self._x = base.x + self._B = base.B + self._n = base.n * 1e20 + self._kBT = base.kBT + + @property + def x(self): + return self._x + + @property + def B(self): + return self._B + + @property + def n(self): + """Unit of particle number density in 1/m^3.""" + return self._n + + @property + def kBT(self): + return self._kBT + + @property + def v(self): + """Unit of velocity in m/s.""" + return self._v + + @property + def t(self): + """Unit of time in s.""" + return self._t + + @property + def p(self): + """Unit of pressure in Pa, equal to B^2/mu0 if velocity_scale='alfvén'.""" + return self._p + + @property + def rho(self): + """Unit of mass density in kg/m^3.""" + return self._rho + + @property + def j(self): + """Unit of current density in A/m^2.""" + return self._j + + def derive_units(self, velocity_scale: str = "light", A_bulk: int = None, Z_bulk: int = None, verbose=False): + """Derive the remaining units from the base units, velocity scale and bulk species' A and Z.""" + + con = ConstantsOfNature() + + # velocity (m/s) + if velocity_scale is None: + self._v = 1.0 + + elif velocity_scale == "light": + self._v = con.c + + elif velocity_scale == "alfvén": + assert A_bulk is not None, 'Need bulk species to choose velocity scale "alfvén".' + self._v = self.B / xp.sqrt(self.n * A_bulk * con.mH * con.mu0) + + elif velocity_scale == "cyclotron": + assert Z_bulk is not None, 'Need bulk species to choose velocity scale "cyclotron".' + assert A_bulk is not None, 'Need bulk species to choose velocity scale "cyclotron".' + self._v = Z_bulk * con.e * self.B / (A_bulk * con.mH) * self.x + + elif velocity_scale == "thermal": + assert A_bulk is not None, 'Need bulk species to choose velocity scale "thermal".' + assert self.kBT is not None + self._v = xp.sqrt(self.kBT * 1000 * con.e / (con.mH * A_bulk)) + + # time (s) + self._t = self.x / self.v + + # return if no bulk is present + if A_bulk is None: + self._p = None + self._rho = None + self._j = None + else: + # pressure (Pa), equal to B^2/mu0 if velocity_scale='alfvén' + self._p = A_bulk * con.mH * self.n * self.v**2 + + # mass density (kg/m^3) + self._rho = A_bulk * con.mH * self.n + + # current density (A/m^2) + self._j = con.e * self.n * self.v + + # print to screen + if verbose and MPI.COMM_WORLD.Get_rank() == 0: + units_used = ( + " m", + " T", + " m⁻³", + "keV", + " m/s", + " s", + " bar", + " kg/m³", + " A/m²", + ) + print("") + for (k, v), u in zip(self.__dict__.items(), units_used): + if v is None: + print(f"Unit of {k[1:]} not specified.") + else: + print( + f"Unit of {k[1:]}:".ljust(25), + "{:4.3e}".format(v) + u, + ) + + +@dataclass +class DerhamOptions: + """Options for the Derham spaces. + + Parameters + ---------- + p : tuple[int] + Spline degree in each direction. + + spl_kind : tuple[bool] + Kind of spline in each direction (True=periodic, False=clamped). + + dirichlet_bc : tuple[tuple[bool]] + Whether to apply homogeneous Dirichlet boundary conditions (at left or right boundary in each direction). + + nquads : tuple[int] + Number of Gauss-Legendre quadrature points in each direction (default = p, leads to exact integration of degree 2p-1 polynomials). + + nq_pr : tuple[int] + Number of Gauss-Legendre quadrature points in each direction for geometric projectors (default = p+1, leads to exact integration of degree 2p+1 polynomials). + + polar_ck : PolarRegularity + Smoothness at a polar singularity at eta_1=0 (default -1 : standard tensor product splines, OR 1 : C1 polar splines) + + local_projectors : bool + Whether to build the local commuting projectors based on quasi-inter-/histopolation. + """ + + p: tuple = (1, 1, 1) + spl_kind: tuple = (True, True, True) + dirichlet_bc: tuple = ((False, False), (False, False), (False, False)) + nquads: tuple = None + nq_pr: tuple = None + polar_ck: PolarRegularity = -1 + local_projectors: bool = False + + def __post_init__(self): + check_option(self.polar_ck, PolarRegularity) + + +@dataclass +class FieldsBackground: + """Options for backgrounds in configuration (=position) space. + + Parameters + ---------- + type : BackgroundTypes + Type of background. + + values : tuple[float] + Values for LogicalConst on the unit cube. + Can be length 1 for scalar functions; must be length 3 for vector-valued functions. + + variable : str + Name of the function in FluidEquilibrium that should be the background. + """ + + type: BackgroundTypes = "LogicalConst" + values: tuple = (1.5, 0.7, 2.4) + variable: str = None + + def __post_init__(self): + check_option(self.type, BackgroundTypes) + + +@dataclass +class EnvironmentOptions: + """Environment options for launching run on current architecture + (these options do not influence the simulation result). + + Parameters + ---------- + out_folders : str + The directory where all sim_folders are stored. + + sim_folder : str + Folder in 'out_folders/' for the current simulation (default='sim_1'). + Will create the folder if it does not exist OR cleans the folder for new runs. + + restart : bool + Whether to restart a run (default=False). + + max_runtime : int, + Maximum run time of simulation in minutes. Will finish the time integration once this limit is reached (default=300). + + save_step : int + When to save data output: every time step (save_step=1), every second time step (save_step=2), etc (default=1). + + sort_step: int, optional + Sort markers in memory every N time steps (default=0, which means markers are sorted only at the start of simulation) + + num_clones: int, optional + Number of domain clones (default=1) + """ + + out_folders: str = os.getcwd() + sim_folder: str = "sim_1" + restart: bool = False + max_runtime: int = 300 + save_step: int = 1 + sort_step: int = 0 + num_clones: int = 1 + + def __post_init__(self): + self.path_out: str = os.path.join(self.out_folders, self.sim_folder) + + def __repr__(self): + for k, v in self.__dict__.items(): + print(f"{k}:".ljust(20), v) + + +def check_option(opt, options): + """Check if opt is contained in options; if opt is a list, checks for each element.""" + opts = get_args(options) + if not isinstance(opt, list): + opt = [opt] + for o in opt: + assert o in opts, f"Option '{o}' is not in {opts}." diff --git a/src/struphy/io/output_handling.py b/src/struphy/io/output_handling.py index 79e23692b..734a2d1a3 100644 --- a/src/struphy/io/output_handling.py +++ b/src/struphy/io/output_handling.py @@ -1,10 +1,9 @@ import ctypes import os +import cunumpy as xp import h5py -from struphy.utils.arrays import xp as np - class DataContainer: """ @@ -83,11 +82,11 @@ def add_data(self, data_dict): Parameters ---------- data_dict : dict - Name-object pairs to save during time stepping, e.g. {key : val}. key must be a string and val must be a np.array of fixed shape. Scalar values (floats) must therefore be passed as 1d arrays of size 1. + Name-object pairs to save during time stepping, e.g. {key : val}. key must be a string and val must be a xp.array of fixed shape. Scalar values (floats) must therefore be passed as 1d arrays of size 1. """ for key, val in data_dict.items(): - assert isinstance(val, np.ndarray) + assert isinstance(val, xp.ndarray) # if dataset already exists, check for compatibility with given array if key in self._dset_dict: diff --git a/src/struphy/io/setup.py b/src/struphy/io/setup.py index f7ec8dfbf..78a295b35 100644 --- a/src/struphy/io/setup.py +++ b/src/struphy/io/setup.py @@ -1,200 +1,93 @@ -from dataclasses import dataclass - +import glob +import importlib.util +import os +import shutil +import sys +from types import ModuleType + +import cunumpy as xp from psydac.ddm.mpi import mpi as MPI -from struphy.utils.arrays import xp as np -from struphy.utils.utils import dict_to_yaml - - -def derive_units( - Z_bulk: int = None, - A_bulk: int = None, - x: float = 1.0, - B: float = 1.0, - n: float = 1.0, - kBT: float = None, - velocity_scale: str = "alfvén", -): - """Computes units used in Struphy model's :ref:`normalization`. - - Input units from parameter file: - - * Length (m) - * Magnetic field (T) - * Number density (10^20 1/m^3) - * Thermal energy (keV), optional - - Velocity unit is defined here: - - * Velocity (m/s) - - Derived units using mass and charge number of bulk species: - - * Time (s) - * Pressure (Pa) - * Mass density (kg/m^3) - * Current density (A/m^2) - - Parameters - --------- - Z_bulk : int - Charge number of bulk species. - - A_bulk : int - Mass number of bulk species. - - x : float - Unit of length (in meters). - - B : float - Unit of magnetic field (in Tesla). - - n : float - Unit of particle number density (in 1e20 per cubic meter). - - kBT : float - Unit of internal energy (in keV). Only in effect if the velocity scale is set to 'thermal'. - - velocity_scale : str - Velocity scale to be used ("alfvén", "cyclotron", "light" or "thermal"). - - Returns - ------- - units : dict - The Struphy units defined above and some Physics constants. - """ - - units = {} - - # physics constants - units["elementary charge"] = 1.602176634e-19 # elementary charge (C) - units["proton mass"] = 1.67262192369e-27 # proton mass (kg) - units["mu0"] = 1.25663706212e-6 # magnetic constant (N/A^2) - units["eps0"] = 8.8541878128e-12 # vacuum permittivity (F/m) - units["kB"] = 1.380649e-23 # Boltzmann constant (J/K) - units["speed of light"] = 299792458 # speed of light (m/s) +from struphy.geometry.base import Domain +from struphy.io.options import DerhamOptions +from struphy.topology.grids import TensorProductGrid - e = units["elementary charge"] - mH = units["proton mass"] - mu0 = units["mu0"] - eps0 = units["eps0"] - kB = units["kB"] - c = units["speed of light"] - # length (m) - units["x"] = x - # magnetic field (T) - units["B"] = B - # number density (1/m^3) - units["n"] = n * 1e20 +def import_parameters_py(params_path: str) -> ModuleType: + """Import a .py parameter file under the module name 'parameters' and return it.""" + assert ".py" in params_path + spec = importlib.util.spec_from_file_location("parameters", params_path) + params_in = importlib.util.module_from_spec(spec) + sys.modules["parameters"] = params_in + spec.loader.exec_module(params_in) + return params_in - # velocity (m/s) - if velocity_scale is None: - units["v"] = 1.0 - elif velocity_scale == "light": - units["v"] = 1.0 * c - - elif velocity_scale == "alfvén": - assert A_bulk is not None, 'Need bulk species to choose velocity scale "alfvén".' - units["v"] = units["B"] / np.sqrt(units["n"] * A_bulk * mH * mu0) - - elif velocity_scale == "cyclotron": - assert Z_bulk is not None, 'Need bulk species to choose velocity scale "cyclotron".' - assert A_bulk is not None, 'Need bulk species to choose velocity scale "cyclotron".' - units["v"] = Z_bulk * e * units["B"] / (A_bulk * mH) * units["x"] - - elif velocity_scale == "thermal": - assert A_bulk is not None, 'Need bulk species to choose velocity scale "thermal".' - assert kBT is not None - units["v"] = np.sqrt(kBT * 1000 * e / (mH * A_bulk)) - - # time (s) - units["t"] = units["x"] / units["v"] - if A_bulk is None: - return units - - # pressure (Pa), equal to B^2/mu0 if velocity_scale='alfvén' - units["p"] = A_bulk * mH * units["n"] * units["v"] ** 2 - - # mass density (kg/m^3) - units["rho"] = A_bulk * mH * units["n"] - - # current density (A/m^2) - units["j"] = e * units["n"] * units["v"] - - return units - - -def setup_domain_and_equil(params: dict, units: dict = None): +def setup_folders( + path_out: str, + restart: bool, + verbose: bool = False, +): """ - Creates the domain object and equilibrium for a given parameter file. - - Parameters - ---------- - params : dict - The full simulation parameter dictionary. - - units : dict - All Struphy units. - - Returns - ------- - domain : Domain - The Struphy domain object for evaluating the mapping F : [0, 1]^3 --> R^3 and the corresponding metric coefficients. - - equil : FluidEquilibrium - The equilibrium object. + Setup output folders. """ + if MPI.COMM_WORLD.Get_rank() == 0: + if verbose: + print("\nPREPARATION AND CLEAN-UP:") - from struphy.fields_background import equils - from struphy.fields_background.base import ( - NumericalFluidEquilibrium, - NumericalFluidEquilibriumWithB, - NumericalMHDequilibrium, - ) - from struphy.geometry import domains - - if "fluid_background" in params: - for eq_type, eq_params in params["fluid_background"].items(): - eq_class = getattr(equils, eq_type) - if eq_type in ("EQDSKequilibrium", "GVECequilibrium", "DESCequilibrium"): - equil = eq_class(**eq_params, units=units) - else: - equil = eq_class(**eq_params) + # create output folder if it does not exit + if not os.path.exists(path_out): + os.makedirs(path_out, exist_ok=True) + if verbose: + print("Created folder " + path_out) - # for numerical equilibria, the domain comes from the equilibrium - if isinstance(equil, (NumericalMHDequilibrium, NumericalFluidEquilibrium, NumericalFluidEquilibriumWithB)): - domain = equil.domain - # for all other equilibria, the domain can be chosen idependently + # create data folder in output folder if it does not exist + if not os.path.exists(os.path.join(path_out, "data/")): + os.mkdir(os.path.join(path_out, "data/")) + if verbose: + print("Created folder " + os.path.join(path_out, "data/")) else: - dom_type = params["geometry"]["type"] - dom_class = getattr(domains, dom_type) - - if dom_type == "Tokamak": - domain = dom_class(**params["geometry"][dom_type], equilibrium=equil) - else: - domain = dom_class(**params["geometry"][dom_type]) + # remove post_processing folder + folder = os.path.join(path_out, "post_processing") + if os.path.exists(folder): + shutil.rmtree(folder) + if verbose: + print("Removed existing folder " + folder) - # set domain attribute in mhd object - equil.domain = domain + # remove meta file + file = os.path.join(path_out, "meta.txt") + if os.path.exists(file): + os.remove(file) + if verbose: + print("Removed existing file " + file) - # no equilibrium (just load domain) - else: - dom_type = params["geometry"]["type"] - dom_class = getattr(domains, dom_type) - domain = dom_class(**params["geometry"][dom_type]) + # remove profiling file + file = os.path.join(path_out, "profile_tmp") + if os.path.exists(file): + os.remove(file) + if verbose: + print("Removed existing file " + file) - equil = None + # remove .png files (if NOT a restart) + if not restart: + files = glob.glob(os.path.join(path_out, "*.png")) + for n, file in enumerate(files): + os.remove(file) + if verbose and n < 10: # print only ten statements in case of many processes + print("Removed existing file " + file) - return domain, equil + files = glob.glob(os.path.join(path_out, "data", "*.hdf5")) + for n, file in enumerate(files): + os.remove(file) + if verbose and n < 10: # print only ten statements in case of many processes + print("Removed existing file " + file) def setup_derham( - params_grid, - comm=None, - domain=None, - mpi_dims_mask=None, + grid: TensorProductGrid, + options: DerhamOptions, + comm: MPI.Intracomm = None, + domain: Domain = None, verbose=False, ): """ @@ -202,19 +95,15 @@ def setup_derham( Parameters ---------- - params_grid : dict - Grid parameters dictionary. + grid : TensorProductGrid + The FEEC grid. comm: Intracomm MPI communicator (sub_comm if clones are used). - domain : struphy.geometry.base.Domain, optional + domain : Domain, optional The Struphy domain object for evaluating the mapping F : [0, 1]^3 --> R^3 and the corresponding metric coefficients. - mpi_dims_mask: list of bool - True if the dimension is to be used in the domain decomposition (=default for each dimension). - If mpi_dims_mask[i]=False, the i-th dimension will not be decomposed. - verbose : bool Show info on screen. @@ -227,28 +116,31 @@ def setup_derham( from struphy.feec.psydac_derham import Derham # number of grid cells - Nel = params_grid["Nel"] + Nel = grid.Nel + # mpi + mpi_dims_mask = grid.mpi_dims_mask + # spline degrees - p = params_grid["p"] + p = options.p # spline types (clamped vs. periodic) - spl_kind = params_grid["spl_kind"] + spl_kind = options.spl_kind # boundary conditions (Homogeneous Dirichlet or None) - dirichlet_bc = params_grid["dirichlet_bc"] + dirichlet_bc = options.dirichlet_bc # Number of quadrature points per histopolation cell - nq_pr = params_grid["nq_pr"] + nq_pr = options.nq_pr # Number of quadrature points per grid cell for L^2 - nq_el = params_grid["nq_el"] + nquads = options.nquads # C^k smoothness at eta_1=0 for polar domains - polar_ck = params_grid["polar_ck"] + polar_ck = options.polar_ck # local commuting projectors - local_projectors = params_grid["local_projectors"] + local_projectors = options.local_projectors derham = Derham( Nel, p, spl_kind, dirichlet_bc=dirichlet_bc, - nquads=nq_el, + nquads=nquads, nq_pr=nq_pr, comm=comm, mpi_dims_mask=mpi_dims_mask, @@ -264,7 +156,7 @@ def setup_derham( print(f"spline degrees:".ljust(25), p) print(f"periodic bcs:".ljust(25), spl_kind) print(f"hom. Dirichlet bc:".ljust(25), dirichlet_bc) - print(f"GL quad pts (L2):".ljust(25), nq_el) + print(f"GL quad pts (L2):".ljust(25), nquads) print(f"GL quad pts (hist):".ljust(25), nq_pr) print( "MPI proc. per dir.:".ljust(25), @@ -276,203 +168,6 @@ def setup_derham( return derham -def pre_processing( - model_name: str, - parameters: dict | str, - path_out: str, - restart: bool, - max_sim_time: int, - save_step: int, - mpi_rank: int, - mpi_size: int, - use_mpi: bool, - num_clones: int, - verbose: bool = False, -): - """ - Prepares simulation parameters, output folder and prints some information of the run to the screen. - - Parameters - ---------- - model_name : str - The name of the model to run. - - parameters : dict | str - The simulation parameters. Can either be a dictionary OR a string (path of .yml parameter file) - - path_out : str - The output directory. Will create a folder if it does not exist OR cleans the folder for new runs. - - restart : bool - Whether to restart a run. - - max_sim_time : int - Maximum run time of simulation in minutes. Will finish the time integration once this limit is reached. - - save_step : int - When to save data output: every time step (save_step=1), every second time step (save_step=2). - - mpi_rank : int - The rank of the calling process. - - mpi_size : int - Total number of MPI processes of the run. - - use_mpi: bool - True if MPI.COMM_WORLD is not None. - - num_clones: int - Number of domain clones. - - verbose : bool - Show full screen output. - - Returns - ------- - params : dict - The simulation parameters. - """ - - import datetime - import glob - import os - import shutil - import sysconfig - - import yaml - - from struphy.models import fluid, hybrid, kinetic, toy - - # prepare output folder - if mpi_rank == 0: - if verbose: - print("\nPREPARATION AND CLEAN-UP:") - - # create output folder if it does not exit - if not os.path.exists(path_out): - os.makedirs(path_out, exist_ok=True) - if verbose: - print("Created folder " + path_out) - - # create data folder in output folder if it does not exist - if not os.path.exists(os.path.join(path_out, "data/")): - os.mkdir(os.path.join(path_out, "data/")) - if verbose: - print("Created folder " + os.path.join(path_out, "data/")) - - # clean output folder if it already exists - else: - # remove post_processing folder - folder = os.path.join(path_out, "post_processing") - if os.path.exists(folder): - shutil.rmtree(folder) - if verbose: - print("Removed existing folder " + folder) - - # remove meta file - file = os.path.join(path_out, "meta.txt") - if os.path.exists(file): - os.remove(file) - if verbose: - print("Removed existing file " + file) - - # remove profiling file - file = os.path.join(path_out, "profile_tmp") - if os.path.exists(file): - os.remove(file) - if verbose: - print("Removed existing file " + file) - - # remove .png files (if NOT a restart) - if not restart: - files = glob.glob(os.path.join(path_out, "*.png")) - for n, file in enumerate(files): - os.remove(file) - if verbose and n < 10: # print only ten statements in case of many processes - print("Removed existing file " + file) - - files = glob.glob(os.path.join(path_out, "data", "*.hdf5")) - for n, file in enumerate(files): - os.remove(file) - if verbose and n < 10: # print only ten statements in case of many processes - print("Removed existing file " + file) - - # save "parameters" dictionary as .yml file - if isinstance(parameters, dict): - parameters_path = os.path.join(path_out, "parameters.yml") - - # write parameters to file and save it in output folder - if mpi_rank == 0: - dict_to_yaml(parameters, parameters_path) - - params = parameters - - # OR load parameters if "parameters" is a string (path) - else: - parameters_path = parameters - - with open(parameters) as file: - params = yaml.load(file, Loader=yaml.FullLoader) - - if model_name is None: - assert "model" in params, "If model is not specified, then model: MODEL must be specified in the params!" - model_name = params["model"] - - if mpi_rank == 0: - # copy parameter file to output folder - if parameters_path != os.path.join(path_out, "parameters.yml"): - shutil.copy2( - parameters_path, - os.path.join( - path_out, - "parameters.yml", - ), - ) - - # print simulation info - print("\nMETADATA:") - print("platform:".ljust(25), sysconfig.get_platform()) - print("python version:".ljust(25), sysconfig.get_python_version()) - print("model:".ljust(25), model_name) - print("MPI processes:".ljust(25), mpi_size) - print("use MPI.COMM_WORLD:".ljust(25), use_mpi) - print("number of domain clones:".ljust(25), num_clones) - print("parameter file:".ljust(25), parameters_path) - print("output folder:".ljust(25), path_out) - print("restart:".ljust(25), restart) - print("max wall-clock [min]:".ljust(25), max_sim_time) - print("save interval [steps]:".ljust(25), save_step) - - # write meta data to output folder - with open(path_out + "/meta.txt", "w") as f: - f.write( - "date of simulation: ".ljust( - 30, - ) - + str(datetime.datetime.now()) - + "\n", - ) - f.write("platform: ".ljust(30) + sysconfig.get_platform() + "\n") - f.write( - "python version: ".ljust( - 30, - ) - + sysconfig.get_python_version() - + "\n", - ) - f.write("model_name: ".ljust(30) + model_name + "\n") - f.write("processes: ".ljust(30) + str(mpi_size) + "\n") - f.write("use MPI.COMM_WORLD: ".ljust(30) + str(use_mpi) + "\n") - f.write("output folder:".ljust(30) + path_out + "\n") - f.write("restart:".ljust(30) + str(restart) + "\n") - f.write( - "max wall-clock time [min]:".ljust(30) + str(max_sim_time) + "\n", - ) - f.write("save interval (steps):".ljust(30) + str(save_step) + "\n") - - return params - - def descend_options_dict( d: dict, out: list | dict, diff --git a/src/struphy/kinetic_background/base.py b/src/struphy/kinetic_background/base.py index a694a0b1e..41c7b6575 100644 --- a/src/struphy/kinetic_background/base.py +++ b/src/struphy/kinetic_background/base.py @@ -1,14 +1,12 @@ "Base classes for kinetic backgrounds." -import copy from abc import ABCMeta, abstractmethod +from typing import Callable -from struphy.fields_background.base import FluidEquilibrium -from struphy.fields_background.equils import set_defaults -from struphy.initial import perturbations -from struphy.initial.utilities import Noise -from struphy.kinetic_background import moment_functions -from struphy.utils.arrays import xp as np +import cunumpy as xp + +from struphy.fields_background.base import FluidEquilibriumWithB +from struphy.initial.base import Perturbation class KineticBackground(metaclass=ABCMeta): @@ -46,8 +44,8 @@ def is_polar(self): @property @abstractmethod - def volume_form(self): - """Boolean. True if the background is represented as a volume form (thus including the velocity Jacobian).""" + def volume_form(self) -> bool: + """True if the background is represented as a volume form (thus including the velocity Jacobian).""" pass @abstractmethod @@ -106,7 +104,7 @@ def __call__(self, *args): Returns ------- - f0 : np.ndarray + f0 : xp.ndarray The evaluated background. """ pass @@ -121,12 +119,12 @@ def __rmul__(self, a): return ScalarMultiplyKineticBackground(self, a) def __div__(self, a): - assert isinstance(a, float) or isinstance(a, int) or isinstance(a, np.int64) + assert isinstance(a, float) or isinstance(a, int) or isinstance(a, xp.int64) assert a != 0, "Cannot divide by zero!" return ScalarMultiplyKineticBackground(self, 1 / a) def __rdiv__(self, a): - assert isinstance(a, float) or isinstance(a, int) or isinstance(a, np.int64) + assert isinstance(a, float) or isinstance(a, int) or isinstance(a, xp.int64) assert a != 0, "Cannot divide by zero!" return ScalarMultiplyKineticBackground(self, 1 / a) @@ -145,6 +143,10 @@ def __init__(self, f1, f2): self._f1 = f1 self._f2 = f2 + if hasattr(f1, "_equil"): + assert f1.equil is f2.equil + self._equil = f1.equil + @property def coords(self): """Coordinates of the distribution.""" @@ -165,6 +167,13 @@ def volume_form(self): """Boolean. True if the background is represented as a volume form (thus including the velocity Jacobian).""" return self._f1.volume_form + @property + def equil(self) -> FluidEquilibriumWithB: + """Fluid background with B-field.""" + if not hasattr(self, "_equil"): + self._equil = None + return self._equil + def velocity_jacobian_det(self, eta1, eta2, eta3, *v): """Jacobian determinant of the velocity coordinate transformation.""" return self._f1.velocity_jacobian_det(eta1, eta2, eta3, *v) @@ -198,8 +207,10 @@ def u(self, *etas): n1 = self._f1.n(*etas) n2 = self._f2.n(*etas) + u1s = self._f1.u(*etas) + u2s = self._f2.u(*etas) - return [(n1 * u1 + n2 * u2) / (n1 + n2) for u1, u2 in zip(self._f1.u(*etas), self._f2.u(*etas))] + return [(n1 * u1 + n2 * u2) / (n1 + n2) for u1, u2 in zip(u1s, u2s)] def __call__(self, *args): """Evaluates the background distribution function f0(etas, v1, ..., vn). @@ -221,7 +232,7 @@ def __call__(self, *args): Returns ------- - f0 : np.ndarray + f0 : xp.ndarray The evaluated background. """ return self._f1(*args) + self._f2(*args) @@ -230,7 +241,7 @@ def __call__(self, *args): class ScalarMultiplyKineticBackground(KineticBackground): def __init__(self, f0, a): assert isinstance(f0, KineticBackground) - assert isinstance(a, float) or isinstance(a, int) or isinstance(a, np.int64) + assert isinstance(a, float) or isinstance(a, int) or isinstance(a, xp.int64) self._f = f0 self._a = a @@ -307,7 +318,7 @@ def __call__(self, *args): Returns ------- - f0 : np.ndarray + f0 : xp.ndarray The evaluated background. """ return self._a * self._f(*args) @@ -328,40 +339,6 @@ class Maxwellian(KineticBackground): and the thermal velocities :math:`v_{\mathrm{th},i}(\boldsymbol{\eta})`. """ - def __init__( - self, - maxw_params: dict = None, - pert_params: dict = None, - equil: FluidEquilibrium = None, - ): - # Set background parameters - if maxw_params is None: - maxw_params = {} - assert isinstance(maxw_params, dict) - self._maxw_params = set_defaults( - maxw_params, - self.default_maxw_params(), - ) - - # check if fluid background is needed - for key, val in self.maxw_params.items(): - if val == "fluid_background": - assert equil is not None - - # parameters for perturbation - if pert_params is None: - pert_params = {} - assert isinstance(pert_params, dict) - self._pert_params = pert_params - - # Fluid equilibrium - self._equil = equil - - @classmethod - def default_maxw_params(cls): - """Default parameters dictionary defining constant moments of the Maxwellian.""" - pass - @abstractmethod def vth(self, *etas): """Thermal velocities (0-forms). @@ -378,21 +355,18 @@ def vth(self, *etas): pass @property - def maxw_params(self): - """Parameters dictionary defining constant moments of the Maxwellian.""" - return self._maxw_params + @abstractmethod + def maxw_params(self) -> dict: + """Parameters dictionary defining moments of the Maxwellian.""" - @property - def pert_params(self): - """Parameters dictionary defining the perturbations.""" - return self._pert_params + def check_maxw_params(self): + for k, v in self.maxw_params.items(): + assert isinstance(k, str) + assert isinstance(v, tuple), f"Maxwallian parameter {k} must be tuple, but is {v}" + assert len(v) == 2 - @property - def equil(self): - """One of :mod:`~struphy.fields_background.equils` - in case that moments are to be set in that way, None otherwise. - """ - return self._equil + assert isinstance(v[0], (float, int, Callable)) + assert isinstance(v[1], Perturbation) or v[1] is None @classmethod def gaussian(self, v, u=0.0, vth=1.0, polar=False, volume_form=False): @@ -420,14 +394,14 @@ def gaussian(self, v, u=0.0, vth=1.0, polar=False, volume_form=False): An array of size(v). """ - if isinstance(v, np.ndarray) and isinstance(u, np.ndarray): + if isinstance(v, xp.ndarray) and isinstance(u, xp.ndarray): assert v.shape == u.shape, f"{v.shape = } but {u.shape = }" if not polar: - out = 1.0 / vth * 1.0 / np.sqrt(2.0 * np.pi) * np.exp(-((v - u) ** 2) / (2.0 * vth**2)) + out = 1.0 / vth * 1.0 / xp.sqrt(2.0 * xp.pi) * xp.exp(-((v - u) ** 2) / (2.0 * vth**2)) else: - assert np.all(v >= 0.0) - out = 1.0 / vth**2 * np.exp(-((v - u) ** 2) / (2.0 * vth**2)) + assert xp.all(v >= 0.0) + out = 1.0 / vth**2 * xp.exp(-((v - u) ** 2) / (2.0 * vth**2)) if volume_form: out *= v @@ -453,16 +427,16 @@ def __call__(self, *args): Returns ------- - f : np.ndarray + f : xp.ndarray The evaluated Maxwellian. """ # Check that all args have the same shape - shape0 = np.shape(args[0]) + shape0 = xp.shape(args[0]) for i, arg in enumerate(args): - assert np.shape(arg) == shape0, f"Argument {i} has {np.shape(arg) = }, but must be {shape0 = }." - assert np.ndim(arg) == 1 or np.ndim(arg) == 3 + self.vdim, ( - f"{np.ndim(arg) = } not allowed for Maxwellian evaluation." + assert xp.shape(arg) == shape0, f"Argument {i} has {xp.shape(arg) = }, but must be {shape0 = }." + assert xp.ndim(arg) == 1 or xp.ndim(arg) == 3 + self.vdim, ( + f"{xp.ndim(arg) = } not allowed for Maxwellian evaluation." ) # flat or meshgrid evaluation # Get result evaluated at eta's @@ -471,33 +445,33 @@ def __call__(self, *args): vths = self.vth(*args[: -self.vdim]) # take care of correct broadcasting, assuming args come from phase space meshgrid - if np.ndim(args[0]) > 3: + if xp.ndim(args[0]) > 3: # move eta axes to the back - arg_t = np.moveaxis(args[0], 0, -1) - arg_t = np.moveaxis(arg_t, 0, -1) - arg_t = np.moveaxis(arg_t, 0, -1) + arg_t = xp.moveaxis(args[0], 0, -1) + arg_t = xp.moveaxis(arg_t, 0, -1) + arg_t = xp.moveaxis(arg_t, 0, -1) # broadcast res_broad = res + 0.0 * arg_t # move eta axes to the front - res = np.moveaxis(res_broad, -1, 0) - res = np.moveaxis(res, -1, 0) - res = np.moveaxis(res, -1, 0) + res = xp.moveaxis(res_broad, -1, 0) + res = xp.moveaxis(res, -1, 0) + res = xp.moveaxis(res, -1, 0) # Multiply result with gaussian in v's for i, v in enumerate(args[-self.vdim :]): # correct broadcasting - if np.ndim(args[0]) > 3: + if xp.ndim(args[0]) > 3: u_broad = us[i] + 0.0 * arg_t - u = np.moveaxis(u_broad, -1, 0) - u = np.moveaxis(u, -1, 0) - u = np.moveaxis(u, -1, 0) + u = xp.moveaxis(u_broad, -1, 0) + u = xp.moveaxis(u, -1, 0) + u = xp.moveaxis(u, -1, 0) vth_broad = vths[i] + 0.0 * arg_t - vth = np.moveaxis(vth_broad, -1, 0) - vth = np.moveaxis(vth, -1, 0) - vth = np.moveaxis(vth, -1, 0) + vth = xp.moveaxis(vth_broad, -1, 0) + vth = xp.moveaxis(vth, -1, 0) + vth = xp.moveaxis(vth, -1, 0) else: u = us[i] vth = vths[i] @@ -506,7 +480,7 @@ def __call__(self, *args): return res - def _evaluate_moment(self, eta1, eta2, eta3, *, name="n"): + def _evaluate_moment(self, eta1, eta2, eta3, *, name: str = "n", add_perturbation: bool = None): """Scalar moment evaluation as background + perturbation. Parameters @@ -517,21 +491,28 @@ def _evaluate_moment(self, eta1, eta2, eta3, *, name="n"): name : str Which moment to evaluate (see varaible "dct" below). + add_perturbation : bool | None + Whether to add the perturbation defined in maxw_params. If None, is taken from self.add_perturbation. + Returns ------- A float (background value) or a numpy.array of the evaluated scalar moment. """ # collect arguments - assert isinstance(eta1, np.ndarray) - assert isinstance(eta2, np.ndarray) - assert isinstance(eta3, np.ndarray) + assert isinstance(eta1, xp.ndarray) + assert isinstance(eta2, xp.ndarray) + assert isinstance(eta3, xp.ndarray) assert eta1.shape == eta2.shape == eta3.shape + params = self.maxw_params[name] + assert isinstance(params, tuple) + assert len(params) == 2 + # flat evaluation for markers if eta1.ndim == 1: etas = [ - np.concatenate( + xp.concatenate( (eta1[:, None], eta2[:, None], eta3[:, None]), axis=1, ), @@ -564,232 +545,38 @@ def _evaluate_moment(self, eta1, eta2, eta3, *, name="n"): else: out = 0.0 * etas[0] - # correspondence name -> equilibrium attribute - dct = { - "n": "n0", - "u1": "u_cart_1", - "u2": "u_cart_2", - "u3": "u_cart_3", - "vth1": "vth0", - "vth2": "vth0", - "vth3": "vth0", - "u_para": "u_para0", - "u_perp": None, - "vth_para": "vth0", - "vth_perp": "vth0", - } - - # fluid background - if self.maxw_params[name] == "fluid_background": - if dct[name] is not None: - out += getattr(self.equil, dct[name])(*etas) - if name in ("n") or "vth" in name: - assert np.all(out > 0.0), f"{name} must be positive!" - else: - print(f'Moment evaluation with "fluid_background" not implemented for {name}.') - - # when using moment functions, see test https://gitlab.mpcdf.mpg.de/struphy/struphy/-/blob/devel/src/struphy/kinetic_background/tests/test_maxwellians.py?ref_type=heads#L1760 - elif isinstance(self.maxw_params[name], dict): - mom_funcs = copy.deepcopy(self.maxw_params[name]) - for typ, params in mom_funcs.items(): - assert params["given_in_basis"] == "0", "Moment functions must be passed as 0-forms to Maxwellians." - params.pop("given_in_basis") - nfun = getattr(moment_functions, typ)(**params) - if eta1.ndim == 1: - out += nfun(eta1, eta2, eta3) - else: - out += nfun(*etas) - - # constant background + # evaluate background + background = params[0] + if isinstance(background, (float, int)): + out += background else: + assert callable(background) + # if eta1.ndim == 1: + # out += background(eta1, eta2, eta3) + # else: + out += background(*etas) + + # add perturbation + if add_perturbation is None: + add_perturbation = self.add_perturbation + + perturbation = params[1] + if perturbation is not None and add_perturbation: + assert isinstance(perturbation, Perturbation) if eta1.ndim == 1: - out += self.maxw_params[name] + out += perturbation(eta1, eta2, eta3) else: - out += self.maxw_params[name] - - # add possible perturbations - if name in self.pert_params: - pp_copy = copy.deepcopy(self.pert_params) - for pert, params in pp_copy[name].items(): - if pert == "Noise": - noise = Noise(**params) - if eta1.ndim == 1: - out += noise(eta1, eta2, eta3) - else: - out += noise(*etas) - else: - assert params["given_in_basis"] == "0", ( - "Moment perturbations must be passed as 0-forms to Maxwellians." - ) - params.pop("given_in_basis") - - perturbation = getattr(perturbations, pert)( - **params, - ) - - if eta1.ndim == 1: - out += perturbation(eta1, eta2, eta3) - else: - out += perturbation(*etas) + out += perturbation(*etas) return out - -class CanonicalMaxwellian(metaclass=ABCMeta): - r"""Base class for a canonical Maxwellian distribution function. - It is defined by three constants of motion in the axissymmetric toroidal system: - - - Shifted canonical toroidal momentum - - .. math:: - - \psi_c = \psi + \frac{m_s F}{q_s B}v_\parallel - \text{sign}(v_\parallel)\sqrt{2(\epsilon - \mu B)}\frac{m_sF}{q_sB} \mathcal{H}(\epsilon - \mu B), - - - Energy - - .. math:: - - \epsilon = \frac{1}{2}m_sv_\parallel² + \mu B, - - - Magnetic moment - - .. math:: - - \mu = \frac{m_s v_\perp²}{2B}, - - where :math:`\psi` is the poloidal magnetic flux function, :math:`F=F(\psi)` is the poloidal current function and :math:`\mathcal{H}` is the Heaviside function. - - With the three constants of motion, a canonical Maxwellian distribution function is defined as - - .. math:: - - F(\psi_c, \epsilon, \mu) = \frac{n(\psi_c)}{(2\pi)^{3/2}v_\text{th}³(\psi_c)} \text{exp}\left[ - \frac{\epsilon}{v_\text{th}²(\psi_c)}\right]. - - """ - @property - @abstractmethod - def coords(self): - """Coordinates of the distribution.""" - pass - - @abstractmethod - def velocity_jacobian_det(self, eta1, eta2, eta3, *v): - """Jacobian determinant of the velocity coordinate transformation.""" - pass - - @abstractmethod - def n(self, psic): - """Number density (0-form). - - Parameters - ---------- - psic : numpy.arrays - Shifted canonical toroidal momentum. - - Returns - ------- - A numpy.array with the density evaluated at evaluation points (same shape as etas). - """ - pass - - @abstractmethod - def vth(self, psic): - """Thermal velocities (0-forms). - - Parameters - ---------- - psic : numpy.arrays - Shifted canonical toroidal momentum. - - Returns - ------- - A numpy.array with the thermal velocity evaluated at evaluation points (one dimension more than etas). - The additional dimension is in the first index. - """ - pass - - def gaussian(self, e, vth=1.0): - """3-dim. normal distribution, to which array-valued thermal velocities can be passed. - - Parameters - ---------- - e : float | array-like - Energy. - - vth : float | array-like - Thermal velocity evaluated at psic. - - Returns - ------- - An array of size(e). - """ - - if isinstance(vth, np.ndarray): - assert e.shape == vth.shape, f"{e.shape = } but {vth.shape = }" - - return 2.0 * np.sqrt(e / np.pi) / vth**3 * np.exp(-e / vth**2) - - def __call__(self, *args): - """Evaluates the canonical Maxwellian distribution function. - - There are two use-cases for this function in the code: - - 1. Evaluating for particles ("flat evaluation", inputs are all 1D of length N_p) - 2. Evaluating the function on a meshgrid (in phase space). - - Hence all arguments must always have - - 1. the same shape - 2. either ndim = 1 or ndim = 3. - - Parameters - ---------- - *args : array_like - Position-velocity arguments in the order energy, magnetic moment, canonical toroidal momentum. - - Returns - ------- - f : np.ndarray - The evaluated Maxwellian. - """ - - # Check that all args have the same shape - shape0 = np.shape(args[0]) - for i, arg in enumerate(args): - assert np.shape(arg) == shape0, f"Argument {i} has {np.shape(arg) = }, but must be {shape0 = }." - assert np.ndim(arg) == 1 or np.ndim(arg) == 3, ( - f"{np.ndim(arg) = } not allowed for canonical Maxwellian evaluation." - ) # flat or meshgrid evaluation - - # Get result evaluated with each particles' psic - res = self.n(args[2]) - vths = self.vth(args[2]) - - # take care of correct broadcasting, assuming args come from phase space meshgrid - if np.ndim(args[0]) == 3: - # move eta axes to the back - arg_t = np.moveaxis(args[0], 0, -1) - arg_t = np.moveaxis(arg_t, 0, -1) - arg_t = np.moveaxis(arg_t, 0, -1) - - # broadcast - res_broad = res + 0.0 * arg_t - - # move eta axes to the front - res = np.moveaxis(res_broad, -1, 0) - res = np.moveaxis(res, -1, 0) - res = np.moveaxis(res, -1, 0) - - # Multiply result with gaussian in energy - if np.ndim(args[0]) == 3: - vth_broad = vths + 0.0 * arg_t - vth = np.moveaxis(vth_broad, -1, 0) - vth = np.moveaxis(vth, -1, 0) - vth = np.moveaxis(vth, -1, 0) - else: - vth = vths - - res *= self.gaussian(args[0], vth=vth) - - return res + def add_perturbation(self) -> bool: + if not hasattr(self, "_add_perturbation"): + self._add_perturbation = True + return self._add_perturbation + + @add_perturbation.setter + def add_perturbation(self, new): + assert isinstance(new, bool) + self._add_perturbation = new diff --git a/src/struphy/kinetic_background/maxwellians.py b/src/struphy/kinetic_background/maxwellians.py index 7965b4e04..c7dea067a 100644 --- a/src/struphy/kinetic_background/maxwellians.py +++ b/src/struphy/kinetic_background/maxwellians.py @@ -1,10 +1,13 @@ "Maxwellian (Gaussian) distributions in velocity space." -from struphy.fields_background.base import FluidEquilibrium +from typing import Callable + +import cunumpy as xp + +from struphy.fields_background.base import FluidEquilibriumWithB from struphy.fields_background.equils import set_defaults -from struphy.kinetic_background import moment_functions -from struphy.kinetic_background.base import CanonicalMaxwellian, Maxwellian -from struphy.utils.arrays import xp as np +from struphy.initial.base import Perturbation +from struphy.kinetic_background.base import Maxwellian class Maxwellian3D(Maxwellian): @@ -12,40 +15,31 @@ class Maxwellian3D(Maxwellian): Parameters ---------- - maxw_params : dict - Parameters for the kinetic background. - - pert_params : dict - Parameters for the kinetic perturbation added to the background. - - equil : FluidEquilibrium - One of :mod:`~struphy.fields_background.equils`. + n, ui, vthi : tuple + Moments of the Maxwellian as tuples. The first entry defines the background + (float for constant background or callable), the second entry defines a Perturbation (can be None). """ - @classmethod - def default_maxw_params(cls): - """Default parameters dictionary defining constant moments of the Maxwellian.""" - return { - "n": 1.0, - "u1": 0.0, - "u2": 0.0, - "u3": 0.0, - "vth1": 1.0, - "vth2": 1.0, - "vth3": 1.0, - } - def __init__( self, - maxw_params: dict = None, - pert_params: dict = None, - equil: FluidEquilibrium = None, + n: tuple[float | Callable, Perturbation] = (1.0, None), + u1: tuple[float | Callable, Perturbation] = (0.0, None), + u2: tuple[float | Callable, Perturbation] = (0.0, None), + u3: tuple[float | Callable, Perturbation] = (0.0, None), + vth1: tuple[float | Callable, Perturbation] = (1.0, None), + vth2: tuple[float | Callable, Perturbation] = (1.0, None), + vth3: tuple[float | Callable, Perturbation] = (1.0, None), ): - super().__init__( - maxw_params=maxw_params, - pert_params=pert_params, - equil=equil, - ) + self._maxw_params = {} + self._maxw_params["n"] = n + self._maxw_params["u1"] = u1 + self._maxw_params["u2"] = u2 + self._maxw_params["u3"] = u3 + self._maxw_params["vth1"] = vth1 + self._maxw_params["vth2"] = vth2 + self._maxw_params["vth3"] = vth3 + + self.check_maxw_params() # factors multiplied onto the defined moments n, u and vth (can be set via setter) self._moment_factors = { @@ -54,6 +48,10 @@ def __init__( "vth": [1.0, 1.0, 1.0], } + @property + def maxw_params(self): + return self._maxw_params + @property def coords(self): """Coordinates of the Maxwellian6D, :math:`(v_1, v_2, v_3)`.""" @@ -144,14 +142,18 @@ class GyroMaxwellian2D(Maxwellian): Parameters ---------- + n, u_para, u_perp, vth_para, vth_perp : tuple + Moments of the Maxwellian as tuples. The first entry defines the background + (float for constant background or callable), the second entry defines a Perturbation (can be None). + maxw_params : dict Parameters for the kinetic background. pert_params : dict Parameters for the kinetic perturbation added to the background. - equil : FluidEquilibrium - One of :mod:`~struphy.fields_background.equils`. + equil : FluidEquilibriumWithB + Fluid background. volume_form : bool Whether to represent the Maxwellian as a volume form; @@ -159,32 +161,28 @@ class GyroMaxwellian2D(Maxwellian): of the polar coordinate transofrmation (default = False). """ - @classmethod - def default_maxw_params(cls): - """Default parameters dictionary defining constant moments of the Maxwellian.""" - return { - "n": 1.0, - "u_para": 0.0, - "u_perp": 0.0, - "vth_para": 1.0, - "vth_perp": 1.0, - } - def __init__( self, - maxw_params: dict = None, - pert_params: dict = None, - equil: FluidEquilibrium = None, + n: tuple[float | Callable, Perturbation] = (1.0, None), + u_para: tuple[float | Callable, Perturbation] = (0.0, None), + u_perp: tuple[float | Callable, Perturbation] = (0.0, None), + vth_para: tuple[float | Callable, Perturbation] = (1.0, None), + vth_perp: tuple[float | Callable, Perturbation] = (1.0, None), + equil: FluidEquilibriumWithB = None, volume_form: bool = True, ): - super().__init__( - maxw_params=maxw_params, - pert_params=pert_params, - equil=equil, - ) + self._maxw_params = {} + self._maxw_params["n"] = n + self._maxw_params["u_para"] = u_para + self._maxw_params["u_perp"] = u_perp + self._maxw_params["vth_para"] = vth_para + self._maxw_params["vth_perp"] = vth_perp + + self.check_maxw_params() # volume form represenation self._volume_form = volume_form + self._equil = equil # factors multiplied onto the defined moments n, u and vth (can be set via setter) self._moment_factors = { @@ -193,6 +191,10 @@ def __init__( "vth": [1.0, 1.0], } + @property + def maxw_params(self): + return self._maxw_params + @property def coords(self): r"""Coordinates of the Maxwellian5D, :math:`(v_\parallel, v_\perp)`.""" @@ -249,7 +251,7 @@ def velocity_jacobian_det(self, eta1, eta2, eta3, *v): assert len(v) == 2 # call equilibrium - etas = (np.vstack((eta1, eta2, eta3)).T).copy() + etas = (xp.vstack((eta1, eta2, eta3)).T).copy() absB0 = self.equil.absB0(etas) # J = v_perp/B @@ -258,10 +260,15 @@ def velocity_jacobian_det(self, eta1, eta2, eta3, *v): return jacobian_det @property - def volume_form(self): + def volume_form(self) -> bool: """Boolean. True if the background is represented as a volume form (thus including the velocity Jacobian |v_perp|).""" return self._volume_form + @property + def equil(self) -> FluidEquilibriumWithB: + """Fluid background with B-field.""" + return self._equil + @property def moment_factors(self): """Collection of factors multiplied onto the defined moments n, u, and vth.""" @@ -294,8 +301,35 @@ def vth(self, eta1, eta2, eta3): return [ou * mom_fac for ou, mom_fac in zip(out, self.moment_factors["vth"])] -class CanonicalMaxwellian(CanonicalMaxwellian): - r"""A :class:`~struphy.kinetic_background.base.CanonicalMaxwellian`. +class CanonicalMaxwellian: + r"""canonical Maxwellian distribution function. + It is defined by three constants of motion in the axissymmetric toroidal system: + + - Shifted canonical toroidal momentum + + .. math:: + + \psi_c = \psi + \frac{m_s F}{q_s B}v_\parallel - \text{sign}(v_\parallel)\sqrt{2(\epsilon - \mu B)}\frac{m_sF}{q_sB} \mathcal{H}(\epsilon - \mu B), + + - Energy + + .. math:: + + \epsilon = \frac{1}{2}m_sv_\parallel² + \mu B, + + - Magnetic moment + + .. math:: + + \mu = \frac{m_s v_\perp²}{2B}, + + where :math:`\psi` is the poloidal magnetic flux function, :math:`F=F(\psi)` is the poloidal current function and :math:`\mathcal{H}` is the Heaviside function. + + With the three constants of motion, a canonical Maxwellian distribution function is defined as + + .. math:: + + F(\psi_c, \epsilon, \mu) = \frac{n(\psi_c)}{(2\pi)^{3/2}v_\text{th}³(\psi_c)} \text{exp}\left[ - \frac{\epsilon}{v_\text{th}²(\psi_c)}\right]. Parameters ---------- @@ -305,8 +339,8 @@ class CanonicalMaxwellian(CanonicalMaxwellian): pert_params : dict Parameters for the kinetic perturbation added to the background. - equil : FluidEquilibrium - One of :mod:`~struphy.fields_background.equils`. + equil : FluidEquilibriumWithB + Fluid background. volume_form : bool Whether to represent the Maxwellian as a volume form; @@ -314,46 +348,22 @@ class CanonicalMaxwellian(CanonicalMaxwellian): of the polar coordinate transofrmation (default = False). """ - @classmethod - def default_maxw_params(cls): - """Default parameters dictionary defining constant moments of the Maxwellian.""" - return { - "n": 1.0, - "vth": 1.0, - "type": "Particles5D", - } - def __init__( self, - maxw_params: dict = None, - pert_params: dict = None, - equil: FluidEquilibrium = None, + n: tuple[float | Callable, Perturbation] = (1.0, None), + vth: tuple[float | Callable, Perturbation] = (1.0, None), + equil: FluidEquilibriumWithB = None, volume_form: bool = True, ): - # Set background parameters - self._maxw_params = self.default_maxw_params() - - if maxw_params is not None: - assert isinstance(maxw_params, dict) - self._maxw_params = set_defaults( - maxw_params, - self.default_maxw_params(), - ) - - # Set parameters for perturbation - self._pert_params = pert_params - - if self.pert_params is not None: - assert isinstance(pert_params, dict) - assert "type" in self.pert_params, '"type" is mandatory in perturbation dictionary.' - ptype = self.pert_params["type"] - assert ptype in self.pert_params, f"{ptype} is mandatory in perturbation dictionary." - self._pert_type = ptype + self._maxw_params = {} + self._maxw_params["n"] = n + self._maxw_params["vth"] = vth - self._equil = equil + self.check_maxw_params() # volume form represenation self._volume_form = volume_form + self._equil = equil # factors multiplied onto the defined moments n and vth (can be set via setter) self._moment_factors = { @@ -372,17 +382,21 @@ def maxw_params(self): return self._maxw_params @property - def pert_params(self): - """Parameters dictionary defining the perturbations of the :meth:`~Maxwellian5D.maxw_params`.""" - return self._pert_params - - @property - def equil(self): + def equil(self) -> FluidEquilibriumWithB: """One of :mod:`~struphy.fields_background.equils` in case that moments are to be set in that way, None otherwise. """ return self._equil + def check_maxw_params(self): + for k, v in self.maxw_params.items(): + assert isinstance(k, str) + assert isinstance(v, tuple), f"Maxwallian parameter {k} must be tuple, but is {v}" + assert len(v) == 2 + + assert isinstance(v[0], (float, int, Callable)) + assert isinstance(v[1], Perturbation) or v[1] is None + def velocity_jacobian_det(self, eta1, eta2, eta3, energy): r"""TODO""" @@ -391,14 +405,99 @@ def velocity_jacobian_det(self, eta1, eta2, eta3, energy): assert eta3.ndim == 1 if self.maxw_params["type"] == "Particles6D": - return np.sqrt(2.0 * energy) * 4.0 * np.pi + return xp.sqrt(2.0 * energy) * 4.0 * xp.pi else: # call equilibrium - etas = (np.vstack((eta1, eta2, eta3)).T).copy() + etas = (xp.vstack((eta1, eta2, eta3)).T).copy() absB0 = self.equil.absB0(etas) - return np.sqrt(energy) * 2.0 * np.sqrt(2.0) / absB0 + return xp.sqrt(energy) * 2.0 * xp.sqrt(2.0) / absB0 + + def gaussian(self, e, vth=1.0): + """3-dim. normal distribution, to which array-valued thermal velocities can be passed. + + Parameters + ---------- + e : float | array-like + Energy. + + vth : float | array-like + Thermal velocity evaluated at psic. + + Returns + ------- + An array of size(e). + """ + + if isinstance(vth, xp.ndarray): + assert e.shape == vth.shape, f"{e.shape = } but {vth.shape = }" + + return 2.0 * xp.sqrt(e / xp.pi) / vth**3 * xp.exp(-e / vth**2) + + def __call__(self, *args): + """Evaluates the canonical Maxwellian distribution function. + + There are two use-cases for this function in the code: + + 1. Evaluating for particles ("flat evaluation", inputs are all 1D of length N_p) + 2. Evaluating the function on a meshgrid (in phase space). + + Hence all arguments must always have + + 1. the same shape + 2. either ndim = 1 or ndim = 3. + + Parameters + ---------- + *args : array_like + Position-velocity arguments in the order energy, magnetic moment, canonical toroidal momentum. + + Returns + ------- + f : xp.ndarray + The evaluated Maxwellian. + """ + + # Check that all args have the same shape + shape0 = xp.shape(args[0]) + for i, arg in enumerate(args): + assert xp.shape(arg) == shape0, f"Argument {i} has {xp.shape(arg) = }, but must be {shape0 = }." + assert xp.ndim(arg) == 1 or xp.ndim(arg) == 3, ( + f"{xp.ndim(arg) = } not allowed for canonical Maxwellian evaluation." + ) # flat or meshgrid evaluation + + # Get result evaluated with each particles' psic + res = self.n(args[2]) + vths = self.vth(args[2]) + + # take care of correct broadcasting, assuming args come from phase space meshgrid + if xp.ndim(args[0]) == 3: + # move eta axes to the back + arg_t = xp.moveaxis(args[0], 0, -1) + arg_t = xp.moveaxis(arg_t, 0, -1) + arg_t = xp.moveaxis(arg_t, 0, -1) + + # broadcast + res_broad = res + 0.0 * arg_t + + # move eta axes to the front + res = xp.moveaxis(res_broad, -1, 0) + res = xp.moveaxis(res, -1, 0) + res = xp.moveaxis(res, -1, 0) + + # Multiply result with gaussian in energy + if xp.ndim(args[0]) == 3: + vth_broad = vths + 0.0 * arg_t + vth = xp.moveaxis(vth_broad, -1, 0) + vth = xp.moveaxis(vth, -1, 0) + vth = xp.moveaxis(vth, -1, 0) + else: + vth = vths + + res *= self.gaussian(args[0], vth=vth) + + return res @property def volume_form(self): @@ -445,18 +544,18 @@ def rc(self, psic): rc_squared = (psic - self.equil.psi_range[0]) / (self.equil.psi_range[1] - self.equil.psi_range[0]) # sorting out indices of negative rc² - neg_index = np.logical_not(rc_squared >= 0) + neg_index = xp.logical_not(rc_squared >= 0) # make them positive rc_squared[neg_index] *= -1 # calculate rc - rc = np.sqrt(rc_squared) + rc = xp.sqrt(rc_squared) rc[neg_index] *= -1 return rc - def n(self, psic): + def n(self, psic, add_perturbation: bool = None): """Density as background + perturbation. Parameters @@ -468,24 +567,34 @@ def n(self, psic): ------- A float (background value) or a numpy.array of the evaluated density. """ - # collect arguments - assert isinstance(psic, np.ndarray) + assert isinstance(psic, xp.ndarray) # assuming that input comes from meshgrid. if psic.ndim == 3: psic = psic[0, 0, :] # set background density - if isinstance(self.maxw_params["n"], dict): - mom_funcs = self.maxw_params["n"] - for typ, params in mom_funcs.items(): - nfun = getattr(moment_functions, typ)(**params) - res = nfun(eta1=self.rc(psic)) + if isinstance(self.maxw_params["n"][0], (float, int)): + res = self.maxw_params["n"][0] + 0.0 * psic else: - res = self.maxw_params["n"] + 0.0 * psic + nfun = self.maxw_params["n"][1] + # for typ, params in mom_funcs.items(): + # nfun = getattr(moment_functions, typ)(**params) + res = nfun(eta1=self.rc(psic)) - # TODO: add perturbation + # add perturbation + if add_perturbation is None: + add_perturbation = self.add_perturbation + + perturbation = self.maxw_params["n"][1] + if perturbation is not None and add_perturbation: + assert isinstance(perturbation, Perturbation) + res = perturbation(eta1=self.rc(psic)) + # if eta1.ndim == 1: + # out += perturbation(eta1, eta2, eta3) + # else: + # out += perturbation(*etas) return res * self.moment_factors["n"] @@ -503,18 +612,29 @@ def vth(self, psic): """ # collect arguments - assert isinstance(psic, np.ndarray) + assert isinstance(psic, xp.ndarray) # assuming that input comes from meshgrid. if psic.ndim == 3: psic = psic[0, 0, :] - res = self.maxw_params["vth"] + 0.0 * psic + res = self.maxw_params["vth"][0] + 0.0 * psic # TODO: add perturbation return res * self.moment_factors["vth"] + @property + def add_perturbation(self) -> bool: + if not hasattr(self, "_add_perturbation"): + self._add_perturbation = True + return self._add_perturbation + + @add_perturbation.setter + def add_perturbation(self, new): + assert isinstance(new, bool) + self._add_perturbation = new + class ColdPlasma(Maxwellian): r"""Base class for a distribution as a Dirac-delta in velocity (vth = 0). @@ -535,20 +655,28 @@ def default_maxw_params(cls): def __init__( self, - maxw_params: dict = None, - pert_params: dict = None, - equil: FluidEquilibrium = None, + n: tuple[float | Callable, Perturbation] = (1.0, None), + u1: tuple[float | Callable, Perturbation] = (0.0, None), + u2: tuple[float | Callable, Perturbation] = (0.0, None), + u3: tuple[float | Callable, Perturbation] = (0.0, None), + equil: FluidEquilibriumWithB = None, ): - super().__init__( - maxw_params=maxw_params, - pert_params=pert_params, - equil=equil, - ) + self._maxw_params = {} + self._maxw_params["n"] = n + self._maxw_params["u1"] = u1 + self._maxw_params["u2"] = u2 + self._maxw_params["u3"] = u3 + self._maxw_params["vth1"] = (0.0, None) + self._maxw_params["vth2"] = (0.0, None) + self._maxw_params["vth3"] = (0.0, None) + + self.check_maxw_params() - # make sure temperatures are zero - self._maxw_params["vth1"] = 0.0 - self._maxw_params["vth2"] = 0.0 - self._maxw_params["vth3"] = 0.0 + self._equil = equil + + @property + def maxw_params(self): + return self._maxw_params @property def coords(self): @@ -571,6 +699,10 @@ def volume_form(self): return False @property + def equil(self) -> FluidEquilibriumWithB: + """Fluid background with B-field.""" + return self._equil + def velocity_jacobian_det(self, eta1, eta2, eta3, *v): """Jacobian determinant of the velocity coordinate transformation.""" return 1.0 diff --git a/src/struphy/kinetic_background/moment_functions.py b/src/struphy/kinetic_background/moment_functions.py index 8d17f670c..39b22c6e1 100644 --- a/src/struphy/kinetic_background/moment_functions.py +++ b/src/struphy/kinetic_background/moment_functions.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 "Analytical moment functions." -from struphy.utils.arrays import xp as np +import cunumpy as xp class ITPA_density: @@ -46,8 +46,8 @@ def __call__(self, eta1, eta2=None, eta3=None): val = ( self._n0 * self._c[3] - * np.exp( - -self._c[2] / self._c[1] * np.tanh((eta1 - self._c[0]) / self._c[2]), + * xp.exp( + -self._c[2] / self._c[1] * xp.tanh((eta1 - self._c[0]) / self._c[2]), ) ) diff --git a/src/struphy/kinetic_background/tests/test_base.py b/src/struphy/kinetic_background/tests/test_base.py index 2556d27b1..8a2e89d28 100644 --- a/src/struphy/kinetic_background/tests/test_base.py +++ b/src/struphy/kinetic_background/tests/test_base.py @@ -1,32 +1,32 @@ def test_kinetic_background_magics(show_plot=False): """Test the magic commands __sum__, __mul__ and __sub__ of the Maxwellian base class.""" + import cunumpy as xp import matplotlib.pyplot as plt from struphy.kinetic_background.maxwellians import Maxwellian3D - from struphy.utils.arrays import xp as np Nel = [32, 1, 1] - e1 = np.linspace(0.0, 1.0, Nel[0]) - e2 = np.linspace(0.0, 1.0, Nel[1]) - e3 = np.linspace(0.0, 1.0, Nel[2]) - v1 = np.linspace(-7.0, 7.0, 128) + e1 = xp.linspace(0.0, 1.0, Nel[0]) + e2 = xp.linspace(0.0, 1.0, Nel[1]) + e3 = xp.linspace(0.0, 1.0, Nel[2]) + v1 = xp.linspace(-7.0, 7.0, 128) m1_params = {"n": 0.5, "u1": 3.0} m2_params = {"n": 0.5, "u1": -3.0} - m1 = Maxwellian3D(maxw_params=m1_params) - m2 = Maxwellian3D(maxw_params=m2_params) + m1 = Maxwellian3D(n=(0.5, None), u1=(3.0, None)) + m2 = Maxwellian3D(n=(0.5, None), u1=(-3.0, None)) m_add = m1 + m2 m_rmul_int = 2 * m1 m_mul_int = m1 * 2 m_mul_float = 2.0 * m1 - m_mul_npint = np.ones(1, dtype=int)[0] * m1 + m_mul_npint = xp.ones(1, dtype=int)[0] * m1 m_sub = m1 - m2 # compare distribution function - meshgrids = np.meshgrid(e1, e2, e3, v1, [0.0], [0.0]) + meshgrids = xp.meshgrid(e1, e2, e3, v1, [0.0], [0.0]) m1_vals = m1(*meshgrids) m2_vals = m2(*meshgrids) @@ -38,15 +38,15 @@ def test_kinetic_background_magics(show_plot=False): m_mul_npint_vals = m_mul_npint(*meshgrids) m_sub_vals = m_sub(*meshgrids) - assert np.allclose(m1_vals + m2_vals, m_add_vals) - assert np.allclose(2 * m1_vals, m_rmul_int_vals) - assert np.allclose(2 * m1_vals, m_mul_int_vals) - assert np.allclose(2.0 * m1_vals, m_mul_float_vals) - assert np.allclose(np.ones(1, dtype=int)[0] * m1_vals, m_mul_npint_vals) - assert np.allclose(m1_vals - m2_vals, m_sub_vals) + assert xp.allclose(m1_vals + m2_vals, m_add_vals) + assert xp.allclose(2 * m1_vals, m_rmul_int_vals) + assert xp.allclose(2 * m1_vals, m_mul_int_vals) + assert xp.allclose(2.0 * m1_vals, m_mul_float_vals) + assert xp.allclose(xp.ones(1, dtype=int)[0] * m1_vals, m_mul_npint_vals) + assert xp.allclose(m1_vals - m2_vals, m_sub_vals) # compare first two moments - meshgrids = np.meshgrid(e1, e2, e3) + meshgrids = xp.meshgrid(e1, e2, e3) n1_vals = m1.n(*meshgrids) n2_vals = m2.n(*meshgrids) @@ -57,11 +57,11 @@ def test_kinetic_background_magics(show_plot=False): u_add1, u_add2, u_add3 = m_add.u(*meshgrids) n_sub_vals = m_sub.n(*meshgrids) - assert np.allclose(n1_vals + n2_vals, n_add_vals) - assert np.allclose(u11 + u21, u_add1) - assert np.allclose(u12 + u22, u_add2) - assert np.allclose(u13 + u23, u_add3) - assert np.allclose(n1_vals - n2_vals, n_sub_vals) + assert xp.allclose(n1_vals + n2_vals, n_add_vals) + assert xp.allclose(u11 + u21, u_add1) + assert xp.allclose(u12 + u22, u_add2) + assert xp.allclose(u13 + u23, u_add3) + assert xp.allclose(n1_vals - n2_vals, n_sub_vals) if show_plot: plt.figure(figsize=(12, 8)) diff --git a/src/struphy/kinetic_background/tests/test_maxwellians.py b/src/struphy/kinetic_background/tests/test_maxwellians.py index 8378ffe17..edf24af4c 100644 --- a/src/struphy/kinetic_background/tests/test_maxwellians.py +++ b/src/struphy/kinetic_background/tests/test_maxwellians.py @@ -8,33 +8,31 @@ def test_maxwellian_3d_uniform(Nel, show_plot=False): Asserts that the results over the domain and velocity space correspond to the analytical computation. """ + import cunumpy as xp import matplotlib.pyplot as plt from struphy.kinetic_background.maxwellians import Maxwellian3D - from struphy.utils.arrays import xp as np - e1 = np.linspace(0.0, 1.0, Nel[0]) - e2 = np.linspace(0.0, 1.0, Nel[1]) - e3 = np.linspace(0.0, 1.0, Nel[2]) + e1 = xp.linspace(0.0, 1.0, Nel[0]) + e2 = xp.linspace(0.0, 1.0, Nel[1]) + e3 = xp.linspace(0.0, 1.0, Nel[2]) # ========================================================== # ==== Test uniform non-shifted, isothermal Maxwellian ===== # ========================================================== - maxw_params = {"n": 2.0} + maxwellian = Maxwellian3D(n=(2.0, None)) - maxwellian = Maxwellian3D(maxw_params=maxw_params) - - meshgrids = np.meshgrid(e1, e2, e3, [0.0], [0.0], [0.0]) + meshgrids = xp.meshgrid(e1, e2, e3, [0.0], [0.0], [0.0]) # Test constant value at v=0 res = maxwellian(*meshgrids).squeeze() - assert np.allclose(res, 2.0 / (2 * np.pi) ** (3 / 2) + 0 * e1, atol=10e-10), ( - f"{res=},\n {2.0 / (2 * np.pi) ** (3 / 2)}" + assert xp.allclose(res, 2.0 / (2 * xp.pi) ** (3 / 2) + 0 * e1, atol=10e-10), ( + f"{res=},\n {2.0 / (2 * xp.pi) ** (3 / 2)}" ) # test Maxwellian profile in v - v1 = np.linspace(-5, 5, 128) - meshgrids = np.meshgrid( + v1 = xp.linspace(-5, 5, 128) + meshgrids = xp.meshgrid( [0.0], [0.0], [0.0], @@ -43,8 +41,8 @@ def test_maxwellian_3d_uniform(Nel, show_plot=False): [0.0], ) res = maxwellian(*meshgrids).squeeze() - res_ana = 2.0 * np.exp(-(v1**2) / 2.0) / (2 * np.pi) ** (3 / 2) - assert np.allclose(res, res_ana, atol=10e-10), f"{res=},\n {res_ana}" + res_ana = 2.0 * xp.exp(-(v1**2) / 2.0) / (2 * xp.pi) ** (3 / 2) + assert xp.allclose(res, res_ana, atol=10e-10), f"{res=},\n {res_ana}" # ======================================================= # ===== Test non-zero shifts and thermal velocities ===== @@ -56,21 +54,28 @@ def test_maxwellian_3d_uniform(Nel, show_plot=False): vth1 = 1.2 vth2 = 0.5 vth3 = 0.3 - maxw_params = {"n": n, "u1": u1, "u2": u2, "u3": u3, "vth1": vth1, "vth2": vth2, "vth3": vth3} - maxwellian = Maxwellian3D(maxw_params=maxw_params) + maxwellian = Maxwellian3D( + n=(2.0, None), + u1=(1.0, None), + u2=(-0.2, None), + u3=(0.1, None), + vth1=(1.2, None), + vth2=(0.5, None), + vth3=(0.3, None), + ) # test Maxwellian profile in v for i in range(3): vs = [0, 0, 0] - vs[i] = np.linspace(-5, 5, 128) - meshgrids = np.meshgrid([0.0], [0.0], [0.0], *vs) + vs[i] = xp.linspace(-5, 5, 128) + meshgrids = xp.meshgrid([0.0], [0.0], [0.0], *vs) res = maxwellian(*meshgrids).squeeze() - res_ana = np.exp(-((vs[0] - u1) ** 2) / (2 * vth1**2)) - res_ana *= np.exp(-((vs[1] - u2) ** 2) / (2 * vth2**2)) - res_ana *= np.exp(-((vs[2] - u3) ** 2) / (2 * vth3**2)) - res_ana *= n / ((2 * np.pi) ** (3 / 2) * vth1 * vth2 * vth3) + res_ana = xp.exp(-((vs[0] - u1) ** 2) / (2 * vth1**2)) + res_ana *= xp.exp(-((vs[1] - u2) ** 2) / (2 * vth2**2)) + res_ana *= xp.exp(-((vs[2] - u3) ** 2) / (2 * vth3**2)) + res_ana *= n / ((2 * xp.pi) ** (3 / 2) * vth1 * vth2 * vth3) if show_plot: plt.plot(vs[i], res_ana, label="analytical") @@ -81,20 +86,21 @@ def test_maxwellian_3d_uniform(Nel, show_plot=False): plt.xlabel("v_" + str(i + 1)) plt.show() - assert np.allclose(res, res_ana, atol=10e-10), f"{res=},\n {res_ana =}" + assert xp.allclose(res, res_ana, atol=10e-10), f"{res=},\n {res_ana =}" @pytest.mark.parametrize("Nel", [[64, 1, 1]]) def test_maxwellian_3d_perturbed(Nel, show_plot=False): """Tests the Maxwellian3D class for perturbations.""" + import cunumpy as xp import matplotlib.pyplot as plt + from struphy.initial import perturbations from struphy.kinetic_background.maxwellians import Maxwellian3D - from struphy.utils.arrays import xp as np - e1 = np.linspace(0.0, 1.0, Nel[0]) - v1 = np.linspace(-5.0, 5.0, 128) + e1 = xp.linspace(0.0, 1.0, Nel[0]) + v1 = xp.linspace(-5.0, 5.0, 128) # =============================================== # ===== Test cosine perturbation in density ===== @@ -102,23 +108,14 @@ def test_maxwellian_3d_perturbed(Nel, show_plot=False): amp = 0.1 mode = 1 - maxw_params = {"n": 2.0} - pert_params = { - "n": { - "ModesCos": { - "given_in_basis": "0", - "ls": [mode], - "amps": [amp], - } - } - } + pert = perturbations.ModesCos(ls=(mode,), amps=(amp,)) - maxwellian = Maxwellian3D(maxw_params=maxw_params, pert_params=pert_params) + maxwellian = Maxwellian3D(n=(2.0, pert)) - meshgrids = np.meshgrid(e1, [0.0], [0.0], [0.0], [0.0], [0.0]) + meshgrids = xp.meshgrid(e1, [0.0], [0.0], [0.0], [0.0], [0.0]) res = maxwellian(*meshgrids).squeeze() - ana_res = (2.0 + amp * np.cos(2 * np.pi * mode * e1)) / (2 * np.pi) ** (3 / 2) + ana_res = (2.0 + amp * xp.cos(2 * xp.pi * mode * e1)) / (2 * xp.pi) ** (3 / 2) if show_plot: plt.plot(e1, ana_res, label="analytical") @@ -129,7 +126,7 @@ def test_maxwellian_3d_perturbed(Nel, show_plot=False): plt.ylabel("f(eta_1)") plt.show() - assert np.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" + assert xp.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" # ============================================= # ===== Test cosine perturbation in shift ===== @@ -139,20 +136,11 @@ def test_maxwellian_3d_perturbed(Nel, show_plot=False): n = 2.0 u1 = 1.2 - maxw_params = {"n": n, "u1": u1} - pert_params = { - "u1": { - "ModesCos": { - "given_in_basis": "0", - "ls": [mode], - "amps": [amp], - } - } - } + pert = perturbations.ModesCos(ls=(mode,), amps=(amp,)) - maxwellian = Maxwellian3D(maxw_params=maxw_params, pert_params=pert_params) + maxwellian = Maxwellian3D(n=(n, None), u1=(u1, pert)) - meshgrids = np.meshgrid( + meshgrids = xp.meshgrid( e1, [0.0], [0.0], @@ -162,9 +150,9 @@ def test_maxwellian_3d_perturbed(Nel, show_plot=False): ) res = maxwellian(*meshgrids).squeeze() - shift = u1 + amp * np.cos(2 * np.pi * mode * e1) - ana_res = np.exp(-((v1 - shift[:, None]) ** 2) / 2) - ana_res *= n / (2 * np.pi) ** (3 / 2) + shift = u1 + amp * xp.cos(2 * xp.pi * mode * e1) + ana_res = xp.exp(-((v1 - shift[:, None]) ** 2) / 2) + ana_res *= n / (2 * xp.pi) ** (3 / 2) if show_plot: plt.figure(1) @@ -185,7 +173,7 @@ def test_maxwellian_3d_perturbed(Nel, show_plot=False): plt.show() - assert np.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" + assert xp.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" # =========================================== # ===== Test cosine perturbation in vth ===== @@ -195,20 +183,11 @@ def test_maxwellian_3d_perturbed(Nel, show_plot=False): n = 2.0 vth1 = 1.2 - maxw_params = {"n": n, "vth1": vth1} - pert_params = { - "vth1": { - "ModesCos": { - "given_in_basis": "0", - "ls": [mode], - "amps": [amp], - } - } - } + pert = perturbations.ModesCos(ls=(mode,), amps=(amp,)) - maxwellian = Maxwellian3D(maxw_params=maxw_params, pert_params=pert_params) + maxwellian = Maxwellian3D(n=(n, None), vth1=(vth1, pert)) - meshgrids = np.meshgrid( + meshgrids = xp.meshgrid( e1, [0.0], [0.0], @@ -218,9 +197,9 @@ def test_maxwellian_3d_perturbed(Nel, show_plot=False): ) res = maxwellian(*meshgrids).squeeze() - thermal = vth1 + amp * np.cos(2 * np.pi * mode * e1) - ana_res = np.exp(-(v1**2) / (2.0 * thermal[:, None] ** 2)) - ana_res *= n / ((2 * np.pi) ** (3 / 2) * thermal[:, None]) + thermal = vth1 + amp * xp.cos(2 * xp.pi * mode * e1) + ana_res = xp.exp(-(v1**2) / (2.0 * thermal[:, None] ** 2)) + ana_res *= n / ((2 * xp.pi) ** (3 / 2) * thermal[:, None]) if show_plot: plt.figure(1) @@ -241,30 +220,22 @@ def test_maxwellian_3d_perturbed(Nel, show_plot=False): plt.show() - assert np.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" + assert xp.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" # ============================================= # ===== Test ITPA perturbation in density ===== # ============================================= n0 = 0.00720655 - c = [0.491230, 0.298228, 0.198739, 0.521298] + c = (0.491230, 0.298228, 0.198739, 0.521298) - maxw_params = { - "n": { - "ITPA_density": { - "given_in_basis": "0", - "n0": n0, - "c": c, - } - } - } + pert = perturbations.ITPA_density(n0=n0, c=c) - maxwellian = Maxwellian3D(maxw_params=maxw_params) + maxwellian = Maxwellian3D(n=(0.0, pert)) - meshgrids = np.meshgrid(e1, [0.0], [0.0], [0.0], [0.0], [0.0]) + meshgrids = xp.meshgrid(e1, [0.0], [0.0], [0.0], [0.0], [0.0]) res = maxwellian(*meshgrids).squeeze() - ana_res = n0 * c[3] * np.exp(-c[2] / c[1] * np.tanh((e1 - c[0]) / c[2])) / (2 * np.pi) ** (3 / 2) + ana_res = n0 * c[3] * xp.exp(-c[2] / c[1] * xp.tanh((e1 - c[0]) / c[2])) / (2 * xp.pi) ** (3 / 2) if show_plot: plt.plot(e1, ana_res, label="analytical") @@ -275,7 +246,7 @@ def test_maxwellian_3d_perturbed(Nel, show_plot=False): plt.ylabel("f(eta_1)") plt.show() - assert np.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" + assert xp.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" @pytest.mark.parametrize("Nel", [[8, 11, 12]]) @@ -284,53 +255,35 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): import inspect + import cunumpy as xp import matplotlib.pyplot as plt from struphy.fields_background import equils + from struphy.fields_background.base import FluidEquilibrium from struphy.geometry import domains from struphy.initial import perturbations + from struphy.initial.base import Perturbation from struphy.kinetic_background.maxwellians import Maxwellian3D - from struphy.utils.arrays import xp as np - - maxw_params_mhd = { - "n": "fluid_background", - "u1": "fluid_background", - "u2": "fluid_background", - "u3": "fluid_background", - "vth1": "fluid_background", - "vth2": "fluid_background", - "vth3": "fluid_background", - } - - maxw_params_1 = { - "n": 1.0, - "u1": "fluid_background", - "u2": "fluid_background", - "u3": "fluid_background", - "vth1": "fluid_background", - "vth2": "fluid_background", - "vth3": "fluid_background", - } - e1 = np.linspace(0.0, 1.0, Nel[0]) - e2 = np.linspace(0.0, 1.0, Nel[1]) - e3 = np.linspace(0.0, 1.0, Nel[2]) + e1 = xp.linspace(0.0, 1.0, Nel[0]) + e2 = xp.linspace(0.0, 1.0, Nel[1]) + e3 = xp.linspace(0.0, 1.0, Nel[2]) v1 = [0.0] v2 = [0.0, -1.0] v3 = [0.0, -1.0, -1.3] - meshgrids = np.meshgrid(e1, e2, e3, v1, v2, v3, indexing="ij") - e_meshgrids = np.meshgrid(e1, e2, e3, indexing="ij") + meshgrids = xp.meshgrid(e1, e2, e3, v1, v2, v3, indexing="ij") + e_meshgrids = xp.meshgrid(e1, e2, e3, indexing="ij") n_mks = 17 - e1_fl = np.random.rand(n_mks) - e2_fl = np.random.rand(n_mks) - e3_fl = np.random.rand(n_mks) - v1_fl = np.random.randn(n_mks) - v2_fl = np.random.randn(n_mks) - v3_fl = np.random.randn(n_mks) + e1_fl = xp.random.rand(n_mks) + e2_fl = xp.random.rand(n_mks) + e3_fl = xp.random.rand(n_mks) + v1_fl = xp.random.randn(n_mks) + v2_fl = xp.random.randn(n_mks) + v3_fl = xp.random.randn(n_mks) args_fl = [e1_fl, e2_fl, e3_fl, v1_fl, v2_fl, v3_fl] - e_args_fl = np.concatenate((e1_fl[:, None], e2_fl[:, None], e3_fl[:, None]), axis=1) + e_args_fl = xp.concatenate((e1_fl[:, None], e2_fl[:, None], e3_fl[:, None]), axis=1) for key, val in inspect.getmembers(equils): if inspect.isclass(val) and val.__module__ == equils.__name__: @@ -344,6 +297,7 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): print(f"Attention: flat (marker) evaluation not tested for GVEC at the moment.") mhd_equil = val() + assert isinstance(mhd_equil, FluidEquilibrium) print(f"{mhd_equil.params = }") if "AdhocTorus" in key: mhd_equil.domain = domains.HollowTorus( @@ -360,8 +314,8 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): elif "ShearedSlab" in key: mhd_equil.domain = domains.Cuboid( r1=mhd_equil.params["a"], - r2=mhd_equil.params["a"] * 2 * np.pi, - r3=mhd_equil.params["R0"] * 2 * np.pi, + r2=mhd_equil.params["a"] * 2 * xp.pi, + r3=mhd_equil.params["R0"] * 2 * xp.pi, ) elif "ShearFluid" in key: mhd_equil.domain = domains.Cuboid( @@ -369,7 +323,7 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): ) elif "ScrewPinch" in key: mhd_equil.domain = domains.HollowCylinder( - a1=1e-3, a2=mhd_equil.params["a"], Lz=mhd_equil.params["R0"] * 2 * np.pi + a1=1e-3, a2=mhd_equil.params["a"], Lz=mhd_equil.params["R0"] * 2 * xp.pi ) else: try: @@ -377,17 +331,33 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): except: print(f"Not setting domain for {key}.") - maxwellian = Maxwellian3D(maxw_params=maxw_params_mhd, equil=mhd_equil) + maxwellian = Maxwellian3D( + n=(mhd_equil.n0, None), + u1=(mhd_equil.u_cart_1, None), + u2=(mhd_equil.u_cart_2, None), + u3=(mhd_equil.u_cart_3, None), + vth1=(mhd_equil.vth0, None), + vth2=(mhd_equil.vth0, None), + vth3=(mhd_equil.vth0, None), + ) - maxwellian_1 = Maxwellian3D(maxw_params=maxw_params_1, equil=mhd_equil) + maxwellian_1 = Maxwellian3D( + n=(1.0, None), + u1=(mhd_equil.u_cart_1, None), + u2=(mhd_equil.u_cart_2, None), + u3=(mhd_equil.u_cart_3, None), + vth1=(mhd_equil.vth0, None), + vth2=(mhd_equil.vth0, None), + vth3=(mhd_equil.vth0, None), + ) # test meshgrid evaluation n0 = mhd_equil.n0(*e_meshgrids) - assert np.allclose( + assert xp.allclose( maxwellian(*meshgrids)[:, :, :, 0, 0, 0], n0 * maxwellian_1(*meshgrids)[:, :, :, 0, 0, 0] ) - assert np.allclose( + assert xp.allclose( maxwellian(*meshgrids)[:, :, :, 0, 1, 2], n0 * maxwellian_1(*meshgrids)[:, :, :, 0, 1, 2] ) @@ -395,16 +365,16 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): if "GVECequilibrium" in key: pass else: - assert np.allclose(maxwellian(*args_fl), mhd_equil.n0(e_args_fl) * maxwellian_1(*args_fl)) - assert np.allclose(maxwellian.n(e1_fl, e2_fl, e3_fl), mhd_equil.n0(e_args_fl)) + assert xp.allclose(maxwellian(*args_fl), mhd_equil.n0(e_args_fl) * maxwellian_1(*args_fl)) + assert xp.allclose(maxwellian.n(e1_fl, e2_fl, e3_fl), mhd_equil.n0(e_args_fl)) u_maxw = maxwellian.u(e1_fl, e2_fl, e3_fl) u_eq = mhd_equil.u_cart(e_args_fl)[0] - assert all([np.allclose(m, e) for m, e in zip(u_maxw, u_eq)]) + assert all([xp.allclose(m, e) for m, e in zip(u_maxw, u_eq)]) vth_maxw = maxwellian.vth(e1_fl, e2_fl, e3_fl) - vth_eq = np.sqrt(mhd_equil.p0(e_args_fl) / mhd_equil.n0(e_args_fl)) - assert all([np.allclose(v, vth_eq) for v in vth_maxw]) + vth_eq = xp.sqrt(mhd_equil.p0(e_args_fl) / mhd_equil.n0(e_args_fl)) + assert all([xp.allclose(v, vth_eq) for v in vth_maxw]) # plotting moments if show_plot: @@ -414,7 +384,7 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): # density plots n_cart = mhd_equil.domain.push(maxwellian.n, *e_meshgrids) - levels = np.linspace(np.min(n_cart) - 1e-10, np.max(n_cart), 20) + levels = xp.linspace(xp.min(n_cart) - 1e-10, xp.max(n_cart), 20) plt.subplot(2, 5, 1) if "Slab" in key or "Pinch" in key: @@ -450,7 +420,7 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): # velocity plots us = maxwellian.u(*e_meshgrids) for i, u in enumerate(us): - levels = np.linspace(np.min(u) - 1e-10, np.max(u), 20) + levels = xp.linspace(xp.min(u) - 1e-10, xp.max(u), 20) plt.subplot(2, 5, 2 + i) if "Slab" in key or "Pinch" in key: @@ -483,7 +453,7 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): vth = maxwellian.vth(*e_meshgrids)[0] vth_cart = mhd_equil.domain.push(vth, *e_meshgrids) - levels = np.linspace(np.min(vth_cart) - 1e-10, np.max(vth_cart), 20) + levels = xp.linspace(xp.min(vth_cart) - 1e-10, xp.max(vth_cart), 20) plt.subplot(2, 5, 5) if "Slab" in key or "Pinch" in key: @@ -523,23 +493,22 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): maxw_params_zero = {"n": 0.0, "vth1": 0.0, "vth2": 0.0, "vth3": 0.0} for key_2, val_2 in inspect.getmembers(perturbations): - if inspect.isclass(val_2): - print(f"{key_2 = }") + if inspect.isclass(val_2) and val_2.__module__ == perturbations.__name__: pert = val_2() + assert isinstance(pert, Perturbation) print(f"{pert = }") - pert_params = { - "n": {key_2: {"given_in_basis": "0"}}, - "u1": {key_2: {"given_in_basis": "0"}}, - "u2": {key_2: {"given_in_basis": "0"}}, - "u3": {key_2: {"given_in_basis": "0"}}, - "vth1": {key_2: {"given_in_basis": "0"}}, - "vth2": {key_2: {"given_in_basis": "0"}}, - "vth3": {key_2: {"given_in_basis": "0"}}, - } + if isinstance(pert, perturbations.Noise): + continue # background + perturbation maxwellian_perturbed = Maxwellian3D( - maxw_params=maxw_params_mhd, pert_params=pert_params, equil=mhd_equil + n=(mhd_equil.n0, pert), + u1=(mhd_equil.u_cart_1, pert), + u2=(mhd_equil.u_cart_2, pert), + u3=(mhd_equil.u_cart_3, pert), + vth1=(mhd_equil.vth0, pert), + vth2=(mhd_equil.vth0, pert), + vth3=(mhd_equil.vth0, pert), ) # test meshgrid evaluation @@ -550,16 +519,22 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): # pure perturbation maxwellian_zero_bckgr = Maxwellian3D( - maxw_params=maxw_params_zero, pert_params=pert_params, equil=mhd_equil + n=(0.0, pert), + u1=(0.0, pert), + u2=(0.0, pert), + u3=(0.0, pert), + vth1=(0.0, pert), + vth2=(0.0, pert), + vth3=(0.0, pert), ) - assert np.allclose(maxwellian_zero_bckgr.n(*e_meshgrids), pert(*e_meshgrids)) - assert np.allclose(maxwellian_zero_bckgr.u(*e_meshgrids)[0], pert(*e_meshgrids)) - assert np.allclose(maxwellian_zero_bckgr.u(*e_meshgrids)[1], pert(*e_meshgrids)) - assert np.allclose(maxwellian_zero_bckgr.u(*e_meshgrids)[2], pert(*e_meshgrids)) - assert np.allclose(maxwellian_zero_bckgr.vth(*e_meshgrids)[0], pert(*e_meshgrids)) - assert np.allclose(maxwellian_zero_bckgr.vth(*e_meshgrids)[1], pert(*e_meshgrids)) - assert np.allclose(maxwellian_zero_bckgr.vth(*e_meshgrids)[2], pert(*e_meshgrids)) + assert xp.allclose(maxwellian_zero_bckgr.n(*e_meshgrids), pert(*e_meshgrids)) + assert xp.allclose(maxwellian_zero_bckgr.u(*e_meshgrids)[0], pert(*e_meshgrids)) + assert xp.allclose(maxwellian_zero_bckgr.u(*e_meshgrids)[1], pert(*e_meshgrids)) + assert xp.allclose(maxwellian_zero_bckgr.u(*e_meshgrids)[2], pert(*e_meshgrids)) + assert xp.allclose(maxwellian_zero_bckgr.vth(*e_meshgrids)[0], pert(*e_meshgrids)) + assert xp.allclose(maxwellian_zero_bckgr.vth(*e_meshgrids)[1], pert(*e_meshgrids)) + assert xp.allclose(maxwellian_zero_bckgr.vth(*e_meshgrids)[2], pert(*e_meshgrids)) # plotting perturbations if show_plot: # and 'Torus' in key_2: @@ -569,7 +544,7 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): # density plots n_cart = mhd_equil.domain.push(maxwellian_zero_bckgr.n, *e_meshgrids) - levels = np.linspace(np.min(n_cart) - 1e-10, np.max(n_cart), 20) + levels = xp.linspace(xp.min(n_cart) - 1e-10, xp.max(n_cart), 20) plt.subplot(2, 5, 1) if "Slab" in key or "Pinch" in key: @@ -605,7 +580,7 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): # velocity plots us = maxwellian_zero_bckgr.u(*e_meshgrids) for i, u in enumerate(us): - levels = np.linspace(np.min(u) - 1e-10, np.max(u), 20) + levels = xp.linspace(xp.min(u) - 1e-10, xp.max(u), 20) plt.subplot(2, 5, 2 + i) if "Slab" in key or "Pinch" in key: @@ -642,7 +617,7 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): vth = maxwellian_zero_bckgr.vth(*e_meshgrids)[0] vth_cart = mhd_equil.domain.push(vth, *e_meshgrids) - levels = np.linspace(np.min(vth_cart) - 1e-10, np.max(vth_cart), 20) + levels = xp.linspace(xp.min(vth_cart) - 1e-10, xp.max(vth_cart), 20) plt.subplot(2, 5, 5) if "Slab" in key or "Pinch" in key: @@ -691,36 +666,34 @@ def test_maxwellian_2d_uniform(Nel, show_plot=False): Asserts that the results over the domain and velocity space correspond to the analytical computation. """ + import cunumpy as xp import matplotlib.pyplot as plt from struphy.kinetic_background.maxwellians import GyroMaxwellian2D - from struphy.utils.arrays import xp as np - e1 = np.linspace(0.0, 1.0, Nel[0]) - e2 = np.linspace(0.0, 1.0, Nel[1]) - e3 = np.linspace(0.0, 1.0, Nel[2]) + e1 = xp.linspace(0.0, 1.0, Nel[0]) + e2 = xp.linspace(0.0, 1.0, Nel[1]) + e3 = xp.linspace(0.0, 1.0, Nel[2]) # =========================================================== # ===== Test uniform non-shifted, isothermal Maxwellian ===== # =========================================================== - maxw_params = {"n": 2.0} - - maxwellian = GyroMaxwellian2D(maxw_params=maxw_params, volume_form=False) + maxwellian = GyroMaxwellian2D(n=(2.0, None), volume_form=False) - meshgrids = np.meshgrid(e1, e2, e3, [0.01], [0.01]) + meshgrids = xp.meshgrid(e1, e2, e3, [0.01], [0.01]) # Test constant value at v_para = v_perp = 0.01 res = maxwellian(*meshgrids).squeeze() - assert np.allclose(res, 2.0 / (2 * np.pi) ** (1 / 2) * np.exp(-(0.01**2)) + 0 * e1, atol=10e-10), ( - f"{res=},\n {2.0 / (2 * np.pi) ** (3 / 2)}" + assert xp.allclose(res, 2.0 / (2 * xp.pi) ** (1 / 2) * xp.exp(-(0.01**2)) + 0 * e1, atol=10e-10), ( + f"{res=},\n {2.0 / (2 * xp.pi) ** (3 / 2)}" ) # test Maxwellian profile in v - v_para = np.linspace(-5, 5, 64) - v_perp = np.linspace(0, 2.5, 64) - vpara, vperp = np.meshgrid(v_para, v_perp) + v_para = xp.linspace(-5, 5, 64) + v_perp = xp.linspace(0, 2.5, 64) + vpara, vperp = xp.meshgrid(v_para, v_perp) - meshgrids = np.meshgrid( + meshgrids = xp.meshgrid( [0.0], [0.0], [0.0], @@ -729,8 +702,8 @@ def test_maxwellian_2d_uniform(Nel, show_plot=False): ) res = maxwellian(*meshgrids).squeeze() - res_ana = 2.0 / (2 * np.pi) ** (1 / 2) * np.exp(-(vpara.T**2) / 2.0 - vperp.T**2 / 2.0) - assert np.allclose(res, res_ana, atol=10e-10), f"{res=},\n {res_ana}" + res_ana = 2.0 / (2 * xp.pi) ** (1 / 2) * xp.exp(-(vpara.T**2) / 2.0 - vperp.T**2 / 2.0) + assert xp.allclose(res, res_ana, atol=10e-10), f"{res=},\n {res_ana}" # ======================================================= # ===== Test non-zero shifts and thermal velocities ===== @@ -740,21 +713,27 @@ def test_maxwellian_2d_uniform(Nel, show_plot=False): u_perp = 0.2 vth_para = 1.2 vth_perp = 0.5 - maxw_params = {"n": n, "u_para": u_para, "u_perp": u_perp, "vth_para": vth_para, "vth_perp": vth_perp} - maxwellian = GyroMaxwellian2D(maxw_params=maxw_params, volume_form=False) + maxwellian = GyroMaxwellian2D( + n=(n, None), + u_para=(u_para, None), + u_perp=(u_perp, None), + vth_para=(vth_para, None), + vth_perp=(vth_perp, None), + volume_form=False, + ) # test Maxwellian profile in v - v_para = np.linspace(-5, 5, 64) - v_perp = np.linspace(0, 2.5, 64) - vpara, vperp = np.meshgrid(v_para, v_perp) + v_para = xp.linspace(-5, 5, 64) + v_perp = xp.linspace(0, 2.5, 64) + vpara, vperp = xp.meshgrid(v_para, v_perp) - meshgrids = np.meshgrid([0.0], [0.0], [0.0], v_para, v_perp) + meshgrids = xp.meshgrid([0.0], [0.0], [0.0], v_para, v_perp) res = maxwellian(*meshgrids).squeeze() - res_ana = np.exp(-((vpara.T - u_para) ** 2) / (2 * vth_para**2)) - res_ana *= np.exp(-((vperp.T - u_perp) ** 2) / (2 * vth_perp**2)) - res_ana *= n / ((2 * np.pi) ** (1 / 2) * vth_para * vth_perp**2) + res_ana = xp.exp(-((vpara.T - u_para) ** 2) / (2 * vth_para**2)) + res_ana *= xp.exp(-((vperp.T - u_perp) ** 2) / (2 * vth_perp**2)) + res_ana *= n / ((2 * xp.pi) ** (1 / 2) * vth_para * vth_perp**2) if show_plot: plt.plot(v_para, res_ana[:, 32], label="analytical") @@ -773,38 +752,38 @@ def test_maxwellian_2d_uniform(Nel, show_plot=False): plt.xlabel("v_" + "perp") plt.show() - assert np.allclose(res, res_ana, atol=10e-10), f"{res=},\n {res_ana =}" + assert xp.allclose(res, res_ana, atol=10e-10), f"{res=},\n {res_ana =}" @pytest.mark.parametrize("Nel", [[6, 1, 1]]) def test_maxwellian_2d_perturbed(Nel, show_plot=False): """Tests the GyroMaxwellian2D class for perturbations.""" + import cunumpy as xp import matplotlib.pyplot as plt + from struphy.initial import perturbations from struphy.kinetic_background.maxwellians import GyroMaxwellian2D - from struphy.utils.arrays import xp as np - e1 = np.linspace(0.0, 1.0, Nel[0]) - v1 = np.linspace(-5.0, 5.0, 128) - v2 = np.linspace(0, 2.5, 128) + e1 = xp.linspace(0.0, 1.0, Nel[0]) + v1 = xp.linspace(-5.0, 5.0, 128) + v2 = xp.linspace(0, 2.5, 128) # =============================================== # ===== Test cosine perturbation in density ===== # =============================================== amp = 0.1 mode = 1 - maxw_params = {"n": 2.0} - pert_params = {"n": {"ModesCos": {"given_in_basis": "0", "ls": [mode], "amps": [amp]}}} + pert = perturbations.ModesCos(ls=(mode,), amps=(amp,)) - maxwellian = GyroMaxwellian2D(maxw_params=maxw_params, pert_params=pert_params, volume_form=False) + maxwellian = GyroMaxwellian2D(n=(2.0, pert), volume_form=False) v_perp = 0.1 - meshgrids = np.meshgrid(e1, [0.0], [0.0], [0.0], v_perp) + meshgrids = xp.meshgrid(e1, [0.0], [0.0], [0.0], v_perp) res = maxwellian(*meshgrids).squeeze() - ana_res = (2.0 + amp * np.cos(2 * np.pi * mode * e1)) / (2 * np.pi) ** (1 / 2) - ana_res *= np.exp(-(v_perp**2) / 2) + ana_res = (2.0 + amp * xp.cos(2 * xp.pi * mode * e1)) / (2 * xp.pi) ** (1 / 2) + ana_res *= xp.exp(-(v_perp**2) / 2) if show_plot: plt.plot(e1, ana_res, label="analytical") @@ -815,7 +794,7 @@ def test_maxwellian_2d_perturbed(Nel, show_plot=False): plt.ylabel("f(eta_1)") plt.show() - assert np.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" + assert xp.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" # ==================================================== # ===== Test cosine perturbation in shift (para) ===== @@ -824,18 +803,21 @@ def test_maxwellian_2d_perturbed(Nel, show_plot=False): mode = 1 n = 2.0 u_para = 1.2 - maxw_params = {"n": n, "u_para": u_para} - pert_params = {"u_para": {"ModesCos": {"given_in_basis": "0", "ls": [mode], "amps": [amp]}}} + pert = perturbations.ModesCos(ls=(mode,), amps=(amp,)) - maxwellian = GyroMaxwellian2D(maxw_params=maxw_params, pert_params=pert_params, volume_form=False) + maxwellian = GyroMaxwellian2D( + n=(2.0, None), + u_para=(u_para, pert), + volume_form=False, + ) v_perp = 0.1 - meshgrids = np.meshgrid(e1, [0.0], [0.0], v1, v_perp) + meshgrids = xp.meshgrid(e1, [0.0], [0.0], v1, v_perp) res = maxwellian(*meshgrids).squeeze() - shift = u_para + amp * np.cos(2 * np.pi * mode * e1) - ana_res = np.exp(-((v1 - shift[:, None]) ** 2) / 2.0) - ana_res *= n / (2 * np.pi) ** (1 / 2) * np.exp(-(v_perp**2) / 2.0) + shift = u_para + amp * xp.cos(2 * xp.pi * mode * e1) + ana_res = xp.exp(-((v1 - shift[:, None]) ** 2) / 2.0) + ana_res *= n / (2 * xp.pi) ** (1 / 2) * xp.exp(-(v_perp**2) / 2.0) if show_plot: plt.figure(1) @@ -856,7 +838,7 @@ def test_maxwellian_2d_perturbed(Nel, show_plot=False): plt.show() - assert np.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" + assert xp.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" # ==================================================== # ===== Test cosine perturbation in shift (perp) ===== @@ -865,17 +847,20 @@ def test_maxwellian_2d_perturbed(Nel, show_plot=False): mode = 1 n = 2.0 u_perp = 1.2 - maxw_params = {"n": n, "u_perp": u_perp} - pert_params = {"u_perp": {"ModesCos": {"given_in_basis": "0", "ls": [mode], "amps": [amp]}}} + pert = perturbations.ModesCos(ls=(mode,), amps=(amp,)) - maxwellian = GyroMaxwellian2D(maxw_params=maxw_params, pert_params=pert_params, volume_form=False) + maxwellian = GyroMaxwellian2D( + n=(2.0, None), + u_perp=(u_perp, pert), + volume_form=False, + ) - meshgrids = np.meshgrid(e1, [0.0], [0.0], 0.0, v2) + meshgrids = xp.meshgrid(e1, [0.0], [0.0], 0.0, v2) res = maxwellian(*meshgrids).squeeze() - shift = u_perp + amp * np.cos(2 * np.pi * mode * e1) - ana_res = np.exp(-((v2 - shift[:, None]) ** 2) / 2.0) - ana_res *= n / (2 * np.pi) ** (1 / 2) + shift = u_perp + amp * xp.cos(2 * xp.pi * mode * e1) + ana_res = xp.exp(-((v2 - shift[:, None]) ** 2) / 2.0) + ana_res *= n / (2 * xp.pi) ** (1 / 2) if show_plot: plt.figure(1) @@ -896,7 +881,7 @@ def test_maxwellian_2d_perturbed(Nel, show_plot=False): plt.show() - assert np.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" + assert xp.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" # ================================================== # ===== Test cosine perturbation in vth (para) ===== @@ -905,13 +890,16 @@ def test_maxwellian_2d_perturbed(Nel, show_plot=False): mode = 1 n = 2.0 vth_para = 1.2 - maxw_params = {"n": n, "vth_para": vth_para} - pert_params = {"vth_para": {"ModesCos": {"given_in_basis": "0", "ls": [mode], "amps": [amp]}}} + pert = perturbations.ModesCos(ls=(mode,), amps=(amp,)) - maxwellian = GyroMaxwellian2D(maxw_params=maxw_params, pert_params=pert_params, volume_form=False) + maxwellian = GyroMaxwellian2D( + n=(2.0, None), + vth_para=(vth_para, pert), + volume_form=False, + ) v_perp = 0.1 - meshgrids = np.meshgrid( + meshgrids = xp.meshgrid( e1, [0.0], [0.0], @@ -920,10 +908,10 @@ def test_maxwellian_2d_perturbed(Nel, show_plot=False): ) res = maxwellian(*meshgrids).squeeze() - thermal = vth_para + amp * np.cos(2 * np.pi * mode * e1) - ana_res = np.exp(-(v1**2) / (2.0 * thermal[:, None] ** 2)) - ana_res *= n / ((2 * np.pi) ** (1 / 2) * thermal[:, None]) - ana_res *= np.exp(-(v_perp**2) / 2.0) + thermal = vth_para + amp * xp.cos(2 * xp.pi * mode * e1) + ana_res = xp.exp(-(v1**2) / (2.0 * thermal[:, None] ** 2)) + ana_res *= n / ((2 * xp.pi) ** (1 / 2) * thermal[:, None]) + ana_res *= xp.exp(-(v_perp**2) / 2.0) if show_plot: plt.figure(1) @@ -944,7 +932,7 @@ def test_maxwellian_2d_perturbed(Nel, show_plot=False): plt.show() - assert np.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" + assert xp.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" # ================================================== # ===== Test cosine perturbation in vth (perp) ===== @@ -953,12 +941,15 @@ def test_maxwellian_2d_perturbed(Nel, show_plot=False): mode = 1 n = 2.0 vth_perp = 1.2 - maxw_params = {"n": n, "vth_perp": vth_perp} - pert_params = {"vth_perp": {"ModesCos": {"given_in_basis": "0", "ls": [mode], "amps": [amp]}}} + pert = perturbations.ModesCos(ls=(mode,), amps=(amp,)) - maxwellian = GyroMaxwellian2D(maxw_params=maxw_params, pert_params=pert_params, volume_form=False) + maxwellian = GyroMaxwellian2D( + n=(2.0, None), + vth_perp=(vth_perp, pert), + volume_form=False, + ) - meshgrids = np.meshgrid( + meshgrids = xp.meshgrid( e1, [0.0], [0.0], @@ -967,9 +958,9 @@ def test_maxwellian_2d_perturbed(Nel, show_plot=False): ) res = maxwellian(*meshgrids).squeeze() - thermal = vth_perp + amp * np.cos(2 * np.pi * mode * e1) - ana_res = np.exp(-(v2**2) / (2.0 * thermal[:, None] ** 2)) - ana_res *= n / ((2 * np.pi) ** (1 / 2) * thermal[:, None] ** 2) + thermal = vth_perp + amp * xp.cos(2 * xp.pi * mode * e1) + ana_res = xp.exp(-(v2**2) / (2.0 * thermal[:, None] ** 2)) + ana_res *= n / ((2 * xp.pi) ** (1 / 2) * thermal[:, None] ** 2) if show_plot: plt.figure(1) @@ -990,31 +981,23 @@ def test_maxwellian_2d_perturbed(Nel, show_plot=False): plt.show() - assert np.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" + assert xp.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" # ============================================= # ===== Test ITPA perturbation in density ===== # ============================================= n0 = 0.00720655 c = [0.491230, 0.298228, 0.198739, 0.521298] - maxw_params = { - "n": { - "ITPA_density": { - "given_in_basis": "0", - "n0": n0, - "c": c, - } - } - } + pert = perturbations.ITPA_density(n0=n0, c=c) - maxwellian = GyroMaxwellian2D(maxw_params=maxw_params, volume_form=False) + maxwellian = GyroMaxwellian2D(n=(0.0, pert), volume_form=False) v_perp = 0.1 - meshgrids = np.meshgrid(e1, [0.0], [0.0], [0.0], v_perp) + meshgrids = xp.meshgrid(e1, [0.0], [0.0], [0.0], v_perp) res = maxwellian(*meshgrids).squeeze() - ana_res = n0 * c[3] * np.exp(-c[2] / c[1] * np.tanh((e1 - c[0]) / c[2])) / (2 * np.pi) ** (1 / 2) - ana_res *= np.exp(-(v_perp**2) / 2.0) + ana_res = n0 * c[3] * xp.exp(-c[2] / c[1] * xp.tanh((e1 - c[0]) / c[2])) / (2 * xp.pi) ** (1 / 2) + ana_res *= xp.exp(-(v_perp**2) / 2.0) if show_plot: plt.plot(e1, ana_res, label="analytical") @@ -1025,7 +1008,7 @@ def test_maxwellian_2d_perturbed(Nel, show_plot=False): plt.ylabel("f(eta_1)") plt.show() - assert np.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" + assert xp.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" @pytest.mark.parametrize("Nel", [[8, 12, 12]]) @@ -1034,46 +1017,33 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): import inspect + import cunumpy as xp import matplotlib.pyplot as plt from struphy.fields_background import equils from struphy.fields_background.base import FluidEquilibriumWithB from struphy.geometry import domains from struphy.initial import perturbations + from struphy.initial.base import Perturbation from struphy.kinetic_background.maxwellians import GyroMaxwellian2D - from struphy.utils.arrays import xp as np - - maxw_params_mhd = { - "n": "fluid_background", - "u_para": "fluid_background", - "vth_para": "fluid_background", - "vth_perp": "fluid_background", - } - maxw_params_1 = { - "n": 1.0, - "u_para": "fluid_background", - "vth_para": "fluid_background", - "vth_perp": "fluid_background", - } - - e1 = np.linspace(0.0, 1.0, Nel[0]) - e2 = np.linspace(0.0, 1.0, Nel[1]) - e3 = np.linspace(0.0, 1.0, Nel[2]) + e1 = xp.linspace(0.0, 1.0, Nel[0]) + e2 = xp.linspace(0.0, 1.0, Nel[1]) + e3 = xp.linspace(0.0, 1.0, Nel[2]) v1 = [0.0] v2 = [0.0, 2.0] - meshgrids = np.meshgrid(e1, e2, e3, v1, v2, indexing="ij") - e_meshgrids = np.meshgrid(e1, e2, e3, indexing="ij") + meshgrids = xp.meshgrid(e1, e2, e3, v1, v2, indexing="ij") + e_meshgrids = xp.meshgrid(e1, e2, e3, indexing="ij") n_mks = 17 - e1_fl = np.random.rand(n_mks) - e2_fl = np.random.rand(n_mks) - e3_fl = np.random.rand(n_mks) - v1_fl = np.random.randn(n_mks) - v2_fl = np.random.rand(n_mks) + e1_fl = xp.random.rand(n_mks) + e2_fl = xp.random.rand(n_mks) + e3_fl = xp.random.rand(n_mks) + v1_fl = xp.random.randn(n_mks) + v2_fl = xp.random.rand(n_mks) args_fl = [e1_fl, e2_fl, e3_fl, v1_fl, v2_fl] - e_args_fl = np.concatenate((e1_fl[:, None], e2_fl[:, None], e3_fl[:, None]), axis=1) + e_args_fl = xp.concatenate((e1_fl[:, None], e2_fl[:, None], e3_fl[:, None]), axis=1) for key, val in inspect.getmembers(equils): if inspect.isclass(val) and val.__module__ == equils.__name__: @@ -1106,8 +1076,8 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): elif "ShearedSlab" in key: mhd_equil.domain = domains.Cuboid( r1=mhd_equil.params["a"], - r2=mhd_equil.params["a"] * 2 * np.pi, - r3=mhd_equil.params["R0"] * 2 * np.pi, + r2=mhd_equil.params["a"] * 2 * xp.pi, + r3=mhd_equil.params["R0"] * 2 * xp.pi, ) elif "ShearFluid" in key: mhd_equil.domain = domains.Cuboid( @@ -1115,7 +1085,7 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): ) elif "ScrewPinch" in key: mhd_equil.domain = domains.HollowCylinder( - a1=1e-3, a2=mhd_equil.params["a"], Lz=mhd_equil.params["R0"] * 2 * np.pi + a1=1e-3, a2=mhd_equil.params["a"], Lz=mhd_equil.params["R0"] * 2 * xp.pi ) else: try: @@ -1123,33 +1093,45 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): except: print(f"Not setting domain for {key}.") - maxwellian = GyroMaxwellian2D(maxw_params=maxw_params_mhd, equil=mhd_equil, volume_form=False) + maxwellian = GyroMaxwellian2D( + n=(mhd_equil.n0, None), + u_para=(mhd_equil.u_para0, None), + vth_para=(mhd_equil.vth0, None), + vth_perp=(mhd_equil.vth0, None), + volume_form=False, + ) - maxwellian_1 = GyroMaxwellian2D(maxw_params=maxw_params_1, equil=mhd_equil, volume_form=False) + maxwellian_1 = GyroMaxwellian2D( + n=(1.0, None), + u_para=(mhd_equil.u_para0, None), + vth_para=(mhd_equil.vth0, None), + vth_perp=(mhd_equil.vth0, None), + volume_form=False, + ) # test meshgrid evaluation n0 = mhd_equil.n0(*e_meshgrids) - assert np.allclose(maxwellian(*meshgrids)[:, :, :, 0, 0], n0 * maxwellian_1(*meshgrids)[:, :, :, 0, 0]) + assert xp.allclose(maxwellian(*meshgrids)[:, :, :, 0, 0], n0 * maxwellian_1(*meshgrids)[:, :, :, 0, 0]) - assert np.allclose(maxwellian(*meshgrids)[:, :, :, 0, 1], n0 * maxwellian_1(*meshgrids)[:, :, :, 0, 1]) + assert xp.allclose(maxwellian(*meshgrids)[:, :, :, 0, 1], n0 * maxwellian_1(*meshgrids)[:, :, :, 0, 1]) # test flat evaluation if "GVECequilibrium" in key: pass else: - assert np.allclose(maxwellian(*args_fl), mhd_equil.n0(e_args_fl) * maxwellian_1(*args_fl)) - assert np.allclose(maxwellian.n(e1_fl, e2_fl, e3_fl), mhd_equil.n0(e_args_fl)) + assert xp.allclose(maxwellian(*args_fl), mhd_equil.n0(e_args_fl) * maxwellian_1(*args_fl)) + assert xp.allclose(maxwellian.n(e1_fl, e2_fl, e3_fl), mhd_equil.n0(e_args_fl)) u_maxw = maxwellian.u(e1_fl, e2_fl, e3_fl) tmp_jv = mhd_equil.jv(e_args_fl) / mhd_equil.n0(e_args_fl) tmp_unit_b1 = mhd_equil.unit_b1(e_args_fl) # j_parallel = jv.b1 j_para = sum([ji * bi for ji, bi in zip(tmp_jv, tmp_unit_b1)]) - assert np.allclose(u_maxw[0], j_para) + assert xp.allclose(u_maxw[0], j_para) vth_maxw = maxwellian.vth(e1_fl, e2_fl, e3_fl) - vth_eq = np.sqrt(mhd_equil.p0(e_args_fl) / mhd_equil.n0(e_args_fl)) - assert all([np.allclose(v, vth_eq) for v in vth_maxw]) + vth_eq = xp.sqrt(mhd_equil.p0(e_args_fl) / mhd_equil.n0(e_args_fl)) + assert all([xp.allclose(v, vth_eq) for v in vth_maxw]) # plotting moments if show_plot: @@ -1159,7 +1141,7 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): # density plots n_cart = mhd_equil.domain.push(maxwellian.n, *e_meshgrids) - levels = np.linspace(np.min(n_cart) - 1e-10, np.max(n_cart), 20) + levels = xp.linspace(xp.min(n_cart) - 1e-10, xp.max(n_cart), 20) plt.subplot(2, 4, 1) if "Slab" in key or "Pinch" in key: @@ -1195,7 +1177,7 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): # velocity plots us = maxwellian.u(*e_meshgrids) for i, u in enumerate(us[:1]): - levels = np.linspace(np.min(u) - 1e-10, np.max(u), 20) + levels = xp.linspace(xp.min(u) - 1e-10, xp.max(u), 20) plt.subplot(2, 4, 2 + i) if "Slab" in key or "Pinch" in key: @@ -1228,7 +1210,7 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): vth = maxwellian.vth(*e_meshgrids)[0] vth_cart = mhd_equil.domain.push(vth, *e_meshgrids) - levels = np.linspace(np.min(vth_cart) - 1e-10, np.max(vth_cart), 20) + levels = xp.linspace(xp.min(vth_cart) - 1e-10, xp.max(vth_cart), 20) plt.subplot(2, 4, 4) if "Slab" in key or "Pinch" in key: @@ -1265,24 +1247,22 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): # test perturbations if "EQDSKequilibrium" in key: - maxw_params_zero = {"n": 0.0, "vth_para": 0.0, "vth_perp": 0.0} - for key_2, val_2 in inspect.getmembers(perturbations): - if inspect.isclass(val_2): - print(f"{key_2 = }") + if inspect.isclass(val_2) and val_2.__module__ == perturbations.__name__: pert = val_2() print(f"{pert = }") - pert_params = { - "n": {key_2: {"given_in_basis": "0"}}, - "u_para": {key_2: {"given_in_basis": "0"}}, - "u_perp": {key_2: {"given_in_basis": "0"}}, - "vth_para": {key_2: {"given_in_basis": "0"}}, - "vth_perp": {key_2: {"given_in_basis": "0"}}, - } + assert isinstance(pert, Perturbation) + + if isinstance(pert, perturbations.Noise): + continue # background + perturbation maxwellian_perturbed = GyroMaxwellian2D( - maxw_params=maxw_params_mhd, pert_params=pert_params, equil=mhd_equil, volume_form=False + n=(mhd_equil.n0, pert), + u_para=(mhd_equil.u_para0, pert), + vth_para=(mhd_equil.vth0, pert), + vth_perp=(mhd_equil.vth0, pert), + volume_form=False, ) # test meshgrid evaluation @@ -1293,17 +1273,19 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): # pure perturbation maxwellian_zero_bckgr = GyroMaxwellian2D( - maxw_params=maxw_params_zero, - pert_params=pert_params, - equil=mhd_equil, + n=(0.0, pert), + u_para=(0.0, pert), + u_perp=(0.0, pert), + vth_para=(0.0, pert), + vth_perp=(0.0, pert), volume_form=False, ) - assert np.allclose(maxwellian_zero_bckgr.n(*e_meshgrids), pert(*e_meshgrids)) - assert np.allclose(maxwellian_zero_bckgr.u(*e_meshgrids)[0], pert(*e_meshgrids)) - assert np.allclose(maxwellian_zero_bckgr.u(*e_meshgrids)[1], pert(*e_meshgrids)) - assert np.allclose(maxwellian_zero_bckgr.vth(*e_meshgrids)[0], pert(*e_meshgrids)) - assert np.allclose(maxwellian_zero_bckgr.vth(*e_meshgrids)[1], pert(*e_meshgrids)) + assert xp.allclose(maxwellian_zero_bckgr.n(*e_meshgrids), pert(*e_meshgrids)) + assert xp.allclose(maxwellian_zero_bckgr.u(*e_meshgrids)[0], pert(*e_meshgrids)) + assert xp.allclose(maxwellian_zero_bckgr.u(*e_meshgrids)[1], pert(*e_meshgrids)) + assert xp.allclose(maxwellian_zero_bckgr.vth(*e_meshgrids)[0], pert(*e_meshgrids)) + assert xp.allclose(maxwellian_zero_bckgr.vth(*e_meshgrids)[1], pert(*e_meshgrids)) # plotting perturbations if show_plot and "EQDSKequilibrium" in key: # and 'Torus' in key_2: @@ -1313,7 +1295,7 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): # density plots n_cart = mhd_equil.domain.push(maxwellian_zero_bckgr.n, *e_meshgrids) - levels = np.linspace(np.min(n_cart) - 1e-10, np.max(n_cart), 20) + levels = xp.linspace(xp.min(n_cart) - 1e-10, xp.max(n_cart), 20) plt.subplot(2, 4, 1) if "Slab" in key or "Pinch" in key: @@ -1349,7 +1331,7 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): # velocity plots us = maxwellian_zero_bckgr.u(*e_meshgrids) for i, u in enumerate(us): - levels = np.linspace(np.min(u) - 1e-10, np.max(u), 20) + levels = xp.linspace(xp.min(u) - 1e-10, xp.max(u), 20) plt.subplot(2, 4, 2 + i) if "Slab" in key or "Pinch" in key: @@ -1386,7 +1368,7 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): vth = maxwellian_zero_bckgr.vth(*e_meshgrids)[0] vth_cart = mhd_equil.domain.push(vth, *e_meshgrids) - levels = np.linspace(np.min(vth_cart) - 1e-10, np.max(vth_cart), 20) + levels = xp.linspace(xp.min(vth_cart) - 1e-10, xp.max(vth_cart), 20) plt.subplot(2, 4, 4) if "Slab" in key or "Pinch" in key: @@ -1435,18 +1417,19 @@ def test_canonical_maxwellian_uniform(Nel, show_plot=False): Asserts that the results over the domain and velocity space correspond to the analytical computation. """ + import cunumpy as xp import matplotlib.pyplot as plt from struphy.fields_background import equils from struphy.geometry import domains + from struphy.initial import perturbations from struphy.kinetic_background.maxwellians import CanonicalMaxwellian - from struphy.utils.arrays import xp as np - e1 = np.linspace(0.0, 1.0, Nel[0]) - e2 = np.linspace(0.0, 1.0, Nel[1]) - e3 = np.linspace(0.0, 1.0, Nel[2]) + e1 = xp.linspace(0.0, 1.0, Nel[0]) + e2 = xp.linspace(0.0, 1.0, Nel[1]) + e3 = xp.linspace(0.0, 1.0, Nel[2]) - eta_meshgrid = np.meshgrid(e1, e2, e3) + eta_meshgrid = xp.meshgrid(e1, e2, e3) v_para = 0.01 v_perp = 0.01 @@ -1493,28 +1476,28 @@ def test_canonical_maxwellian_uniform(Nel, show_plot=False): psi = mhd_equil.psi_r(r) psic = psi - epsilon * B0 * R0 / absB * v_para - psic += epsilon * np.sign(v_para) * np.sqrt(2 * (energy - mu * B0)) * R0 * np.heaviside(energy - mu * B0, 0) + psic += epsilon * xp.sign(v_para) * xp.sqrt(2 * (energy - mu * B0)) * R0 * xp.heaviside(energy - mu * B0, 0) # =========================================================== # ===== Test uniform, isothermal canonical Maxwellian ===== # =========================================================== maxw_params = {"n": 2.0, "vth": 1.0} - maxwellian = CanonicalMaxwellian(maxw_params=maxw_params) + maxwellian = CanonicalMaxwellian(n=(2.0, None), vth=(1.0, None)) # Test constant value at v_para = v_perp = 0.01 res = maxwellian(energy, mu, psic).squeeze() res_ana = ( maxw_params["n"] * 2 - * np.sqrt(energy / np.pi) + * xp.sqrt(energy / xp.pi) / maxw_params["vth"] ** 3 - * np.exp(-energy / maxw_params["vth"] ** 2) + * xp.exp(-energy / maxw_params["vth"] ** 2) ) - assert np.allclose(res, res_ana, atol=10e-10), f"{res=},\n {res_ana}" + assert xp.allclose(res, res_ana, atol=10e-10), f"{res=},\n {res_ana}" # test canonical Maxwellian profile in v_para - v_para = np.linspace(-5, 5, 64) + v_para = xp.linspace(-5, 5, 64) v_perp = 0.1 absB = mhd_equil.absB0(0.0, 0.0, 0.0)[0, 0, 0] @@ -1531,18 +1514,18 @@ def test_canonical_maxwellian_uniform(Nel, show_plot=False): psi = mhd_equil.psi_r(r) psic = psi - epsilon * B0 * R0 / absB * v_para - psic += epsilon * np.sign(v_para) * np.sqrt(2 * (energy - mu * B0)) * R0 * np.heaviside(energy - mu * B0, 0) + psic += epsilon * xp.sign(v_para) * xp.sqrt(2 * (energy - mu * B0)) * R0 * xp.heaviside(energy - mu * B0, 0) - com_meshgrids = np.meshgrid(energy, mu, psic) + com_meshgrids = xp.meshgrid(energy, mu, psic) res = maxwellian(*com_meshgrids).squeeze() res_ana = ( maxw_params["n"] * 2 - * np.sqrt(com_meshgrids[0] / np.pi) + * xp.sqrt(com_meshgrids[0] / xp.pi) / maxw_params["vth"] ** 3 - * np.exp(-com_meshgrids[0] / maxw_params["vth"] ** 2) + * xp.exp(-com_meshgrids[0] / maxw_params["vth"] ** 2) ) if show_plot: @@ -1554,11 +1537,11 @@ def test_canonical_maxwellian_uniform(Nel, show_plot=False): plt.xlabel("v_para") plt.show() - assert np.allclose(res, res_ana, atol=10e-10), f"{res=},\n {res_ana}" + assert xp.allclose(res, res_ana, atol=10e-10), f"{res=},\n {res_ana}" # test canonical Maxwellian profile in v_perp v_para = 0.1 - v_perp = np.linspace(0, 2.5, 64) + v_perp = xp.linspace(0, 2.5, 64) absB = mhd_equil.absB0(0.5, 0.5, 0.5)[0, 0, 0] @@ -1574,18 +1557,18 @@ def test_canonical_maxwellian_uniform(Nel, show_plot=False): psi = mhd_equil.psi_r(r) psic = psi - epsilon * B0 * R0 / absB * v_para - psic += epsilon * np.sign(v_para) * np.sqrt(2 * (energy - mu * B0)) * R0 * np.heaviside(energy - mu * B0, 0) + psic += epsilon * xp.sign(v_para) * xp.sqrt(2 * (energy - mu * B0)) * R0 * xp.heaviside(energy - mu * B0, 0) - com_meshgrids = np.meshgrid(energy, mu, psic) + com_meshgrids = xp.meshgrid(energy, mu, psic) res = maxwellian(*com_meshgrids).squeeze() res_ana = ( maxw_params["n"] * 2 - * np.sqrt(com_meshgrids[0] / np.pi) + * xp.sqrt(com_meshgrids[0] / xp.pi) / maxw_params["vth"] ** 3 - * np.exp(-com_meshgrids[0] / maxw_params["vth"] ** 2) + * xp.exp(-com_meshgrids[0] / maxw_params["vth"] ** 2) ) if show_plot: @@ -1597,7 +1580,7 @@ def test_canonical_maxwellian_uniform(Nel, show_plot=False): plt.xlabel("v_perp") plt.show() - assert np.allclose(res, res_ana, atol=10e-10), f"{res=},\n {res_ana}" + assert xp.allclose(res, res_ana, atol=10e-10), f"{res=},\n {res_ana}" # ============================================= # ===== Test ITPA perturbation in density ===== @@ -1608,14 +1591,15 @@ def test_canonical_maxwellian_uniform(Nel, show_plot=False): "n": {"ITPA_density": {"n0": n0, "c": c}}, "vth": 1.0, } + pert = perturbations.ITPA_density(n0=n0, c=c) - maxwellian = CanonicalMaxwellian(maxw_params=maxw_params, equil=mhd_equil) + maxwellian = CanonicalMaxwellian(n=(0.0, pert), equil=mhd_equil, volume_form=False) - e1 = np.linspace(0.0, 1.0, Nel[0]) - e2 = np.linspace(0.0, 1.0, Nel[1]) - e3 = np.linspace(0.0, 1.0, Nel[2]) + e1 = xp.linspace(0.0, 1.0, Nel[0]) + e2 = xp.linspace(0.0, 1.0, Nel[1]) + e3 = xp.linspace(0.0, 1.0, Nel[2]) - eta_meshgrid = np.meshgrid(e1, e2, e3) + eta_meshgrid = xp.meshgrid(e1, e2, e3) v_para = 0.01 v_perp = 0.01 @@ -1634,16 +1618,16 @@ def test_canonical_maxwellian_uniform(Nel, show_plot=False): psi = mhd_equil.psi_r(r[0, :, 0]) psic = psi - epsilon * B0 * R0 / absB * v_para - psic += epsilon * np.sign(v_para) * np.sqrt(2 * (energy - mu * B0)) * R0 * np.heaviside(energy - mu * B0, 0) + psic += epsilon * xp.sign(v_para) * xp.sqrt(2 * (energy - mu * B0)) * R0 * xp.heaviside(energy - mu * B0, 0) - com_meshgrids = np.meshgrid(energy, mu, psic) + com_meshgrids = xp.meshgrid(energy, mu, psic) res = maxwellian(energy, mu, psic).squeeze() # calculate rc rc = maxwellian.rc(psic) - ana_res = n0 * c[3] * np.exp(-c[2] / c[1] * np.tanh((rc - c[0]) / c[2])) - ana_res *= 2 * np.sqrt(energy / np.pi) / maxw_params["vth"] ** 3 * np.exp(-energy / maxw_params["vth"] ** 2) + ana_res = n0 * c[3] * xp.exp(-c[2] / c[1] * xp.tanh((rc - c[0]) / c[2])) + ana_res *= 2 * xp.sqrt(energy / xp.pi) / maxw_params["vth"] ** 3 * xp.exp(-energy / maxw_params["vth"] ** 2) if show_plot: plt.plot(e1, ana_res, label="analytical") @@ -1654,14 +1638,14 @@ def test_canonical_maxwellian_uniform(Nel, show_plot=False): plt.ylabel("f(eta_1)") plt.show() - assert np.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" + assert xp.allclose(res, ana_res, atol=10e-10), f"{res=},\n {ana_res}" if __name__ == "__main__": - # test_maxwellian_3d_uniform(Nel=[64, 1, 1], show_plot=False) - # test_maxwellian_3d_perturbed(Nel=[64, 1, 1], show_plot=False) + # test_maxwellian_3d_uniform(Nel=[64, 1, 1], show_plot=True) + # test_maxwellian_3d_perturbed(Nel=[64, 1, 1], show_plot=True) # test_maxwellian_3d_mhd(Nel=[8, 11, 12], with_desc=None, show_plot=False) # test_maxwellian_2d_uniform(Nel=[64, 1, 1], show_plot=True) # test_maxwellian_2d_perturbed(Nel=[64, 1, 1], show_plot=True) - test_maxwellian_2d_mhd(Nel=[8, 12, 12], with_desc=None, show_plot=False) - # test_canonical_maxwellian_uniform(Nel=[64, 1, 1], show_plot=True) + # test_maxwellian_2d_mhd(Nel=[8, 12, 12], with_desc=None, show_plot=False) + test_canonical_maxwellian_uniform(Nel=[64, 1, 1], show_plot=True) diff --git a/src/struphy/linear_algebra/linalg_kron.py b/src/struphy/linear_algebra/linalg_kron.py index 68e2513f7..2e0dd57dc 100644 --- a/src/struphy/linear_algebra/linalg_kron.py +++ b/src/struphy/linear_algebra/linalg_kron.py @@ -13,11 +13,10 @@ [r_M11, rM12, ... , r_MNO]] """ +import cunumpy as xp from scipy.linalg import solve_circulant from scipy.sparse.linalg import splu -from struphy.utils.arrays import xp as np - def kron_matvec_2d(kmat, vec2d): """ @@ -197,9 +196,9 @@ def kron_matmat_fft_3d(a_vec, b_vec): c_vec = [0, 0, 0] - c_vec[0] = np.fft.ifft(np.fft.fft(a_vec[0]) * np.fft.fft(b_vec[0])) - c_vec[1] = np.fft.ifft(np.fft.fft(a_vec[1]) * np.fft.fft(b_vec[1])) - c_vec[2] = np.fft.ifft(np.fft.fft(a_vec[2]) * np.fft.fft(b_vec[2])) + c_vec[0] = xp.fft.ifft(xp.fft.fft(a_vec[0]) * xp.fft.fft(b_vec[0])) + c_vec[1] = xp.fft.ifft(xp.fft.fft(a_vec[1]) * xp.fft.fft(b_vec[1])) + c_vec[2] = xp.fft.ifft(xp.fft.fft(a_vec[2]) * xp.fft.fft(b_vec[2])) return c_vec diff --git a/src/struphy/linear_algebra/saddle_point.py b/src/struphy/linear_algebra/saddle_point.py index 5da783b3a..47dd36190 100644 --- a/src/struphy/linear_algebra/saddle_point.py +++ b/src/struphy/linear_algebra/saddle_point.py @@ -1,5 +1,6 @@ from typing import Union +import cunumpy as xp import scipy as sc from psydac.linalg.basic import LinearOperator, Vector from psydac.linalg.block import BlockLinearOperator, BlockVector, BlockVectorSpace @@ -7,7 +8,6 @@ from psydac.linalg.solvers import inverse from struphy.linear_algebra.tests.test_saddlepoint_massmatrices import _plot_residual_norms -from struphy.utils.arrays import xp as np class SaddlePointSolver: @@ -28,7 +28,7 @@ class SaddlePointSolver: } \right) using either the Uzawa iteration :math:`BA^{-1}B^{\top} y = BA^{-1} f` or using on of the solvers given in :mod:`psydac.linalg.solvers`. The prefered solver is GMRES. - The decission which variant to use is given by the type of A. If A is of type list of np.ndarrays or sc.sparse.csr_matrices, then this class uses the Uzawa algorithm. + The decission which variant to use is given by the type of A. If A is of type list of xp.ndarrays or sc.sparse.csr_matrices, then this class uses the Uzawa algorithm. If A is of type LinearOperator or BlockLinearOperator, a solver is used for the inverse. Using the Uzawa algorithm, solution is given by: @@ -41,7 +41,7 @@ class SaddlePointSolver: ---------- A : list, LinearOperator or BlockLinearOperator Upper left block. - Either the entries on the diagonals of block A are given as list of np.ndarray or sc.sparse.csr_matrix. + Either the entries on the diagonals of block A are given as list of xp.ndarray or sc.sparse.csr_matrix. Alternative: Give whole matrice A as LinearOperator or BlockLinearOperator. list: Uzawa algorithm is used. LinearOperator: A solver given in :mod:`psydac.linalg.solvers` is used. Specified by solver_name. @@ -49,16 +49,16 @@ class SaddlePointSolver: B : list, LinearOperator or BlockLinearOperator Lower left block. - Uzwaw Algorithm: All entries of block B are given either as list of np.ndarray or sc.sparse.csr_matrix. + Uzwaw Algorithm: All entries of block B are given either as list of xp.ndarray or sc.sparse.csr_matrix. Solver: Give whole B as LinearOperator or BlocklinearOperator F : list Right hand side of the upper block. - Uzawa: Given as list of np.ndarray or sc.sparse.csr_matrix. + Uzawa: Given as list of xp.ndarray or sc.sparse.csr_matrix. Solver: Given as LinearOperator or BlockLinearOperator Apre : list - The non-inverted preconditioner for entries on the diagonals of block A are given as list of np.ndarray or sc.sparse.csr_matrix. Only required for the Uzawa algorithm. + The non-inverted preconditioner for entries on the diagonals of block A are given as list of xp.ndarray or sc.sparse.csr_matrix. Only required for the Uzawa algorithm. method_to_solve : str Method for the inverses. Choose from 'DirectNPInverse', 'ScipySparse', 'InexactNPInverse' ,'SparseSolver'. Only required for the Uzawa algorithm. @@ -98,14 +98,14 @@ def __init__( if isinstance(A, list): self._variant = "Uzawa" for i in A: - assert isinstance(i, np.ndarray) or isinstance(i, sc.sparse.csr_matrix) + assert isinstance(i, xp.ndarray) or isinstance(i, sc.sparse.csr_matrix) for i in B: - assert isinstance(i, np.ndarray) or isinstance(i, sc.sparse.csr_matrix) + assert isinstance(i, xp.ndarray) or isinstance(i, sc.sparse.csr_matrix) for i in F: - assert isinstance(i, np.ndarray) or isinstance(i, sc.sparse.csr_matrix) + assert isinstance(i, xp.ndarray) or isinstance(i, sc.sparse.csr_matrix) for i in Apre: assert ( - isinstance(i, np.ndarray) + isinstance(i, xp.ndarray) or isinstance(i, sc.sparse.csr_matrix) or isinstance(i, sc.sparse.csr_array) ) @@ -169,9 +169,9 @@ def __init__( self._setup_inverses() # Solution vectors numpy - self._Pnp = np.zeros(self._B1np.shape[0]) - self._Unp = np.zeros(self._A[0].shape[1]) - self._Uenp = np.zeros(self._A[1].shape[1]) + self._Pnp = xp.zeros(self._B1np.shape[0]) + self._Unp = xp.zeros(self._A[0].shape[1]) + self._Uenp = xp.zeros(self._A[1].shape[1]) # Allocate memory for matrices used in solving the system self._rhs0np = self._F[0].copy() self._rhs1np = self._F[1].copy() @@ -195,8 +195,8 @@ def A(self, a): same_A0 = (A0_old != A0_new).nnz == 0 same_A1 = (A1_old != A1_new).nnz == 0 else: - same_A0 = np.allclose(A0_old, A0_new, atol=1e-10) - same_A1 = np.allclose(A1_old, A1_new, atol=1e-10) + same_A0 = xp.allclose(A0_old, A0_new, atol=1e-10) + same_A1 = xp.allclose(A1_old, A1_new, atol=1e-10) if same_A0 and same_A1: need_update = False self._A = a @@ -240,8 +240,8 @@ def Apre(self, a): same_A0 = (A0_old != A0_new).nnz == 0 same_A1 = (A1_old != A1_new).nnz == 0 else: - same_A0 = np.allclose(A0_old, A0_new, atol=1e-10) - same_A1 = np.allclose(A1_old, A1_new, atol=1e-10) + same_A0 = xp.allclose(A0_old, A0_new, atol=1e-10) + same_A1 = xp.allclose(A1_old, A1_new, atol=1e-10) if same_A0 and same_A1: need_update = False self._Apre = a @@ -256,11 +256,11 @@ def __call__(self, U_init=None, Ue_init=None, P_init=None, out=None): Parameters ---------- - U_init : Vector, np.ndarray or sc.sparse.csr.csr_matrix, optional - Initial guess for the velocity of the ions. If None, initializes to zero. Types np.ndarray and sc.sparse.csr.csr_matrix can only be given if system should be solved with Uzawa algorithm. + U_init : Vector, xp.ndarray or sc.sparse.csr.csr_matrix, optional + Initial guess for the velocity of the ions. If None, initializes to zero. Types xp.ndarray and sc.sparse.csr.csr_matrix can only be given if system should be solved with Uzawa algorithm. - Ue_init : Vector, np.ndarray or sc.sparse.csr.csr_matrix, optional - Initial guess for the velocity of the electrons. If None, initializes to zero. Types np.ndarray and sc.sparse.csr.csr_matrix can only be given if system should be solved with Uzawa algorithm. + Ue_init : Vector, xp.ndarray or sc.sparse.csr.csr_matrix, optional + Initial guess for the velocity of the electrons. If None, initializes to zero. Types xp.ndarray and sc.sparse.csr.csr_matrix can only be given if system should be solved with Uzawa algorithm. P_init : Vector, optional Initial guess for the potential. If None, initializes to zero. @@ -310,7 +310,7 @@ def __call__(self, U_init=None, Ue_init=None, P_init=None, out=None): self._spectralresult = [] # Initialize P to zero or given initial guess - if isinstance(U_init, np.ndarray) or isinstance(U_init, sc.sparse.csr.csr_matrix): + if isinstance(U_init, xp.ndarray) or isinstance(U_init, sc.sparse.csr.csr_matrix): self._Pnp = P_init if P_init is not None else self._P self._Unp = U_init if U_init is not None else self._U self._Uenp = Ue_init if U_init is not None else self._Ue @@ -353,8 +353,8 @@ def __call__(self, U_init=None, Ue_init=None, P_init=None, out=None): # Step 2: Compute residual R = BU (divergence of U) R = R1 + R2 # self._B1np.dot(self._Unp) + self._B2np.dot(self._Uenp) - residual_norm = np.linalg.norm(R) - residual_normR1 = np.linalg.norm(R) + residual_norm = xp.linalg.norm(R) + residual_normR1 = xp.linalg.norm(R) self._residual_norms.append(residual_normR1) # Store residual norm # Check for convergence based on residual norm if residual_norm < self._tol: @@ -444,10 +444,10 @@ def _is_inverse_still_valid(self, inv, mat, name="", pre=None): I_approx = inv @ test_mat if self._method_to_solve in ("DirectNPInverse", "InexactNPInverse"): - I_exact = np.eye(test_mat.shape[0]) - if not np.allclose(I_approx, I_exact, atol=1e-6): + I_exact = xp.eye(test_mat.shape[0]) + if not xp.allclose(I_approx, I_exact, atol=1e-6): diff = I_approx - I_exact - max_abs = np.abs(diff).max() + max_abs = xp.abs(diff).max() print(f"{name} inverse is NOT valid anymore. Max diff: {max_abs:.2e}") return False print(f"{name} inverse is still valid.") @@ -455,7 +455,7 @@ def _is_inverse_still_valid(self, inv, mat, name="", pre=None): elif self._method_to_solve == "ScipySparse": I_exact = sc.sparse.identity(I_approx.shape[0], format=I_approx.format) diff = (I_approx - I_exact).tocoo() - max_abs = np.abs(diff.data).max() if diff.nnz > 0 else 0.0 + max_abs = xp.abs(diff.data).max() if diff.nnz > 0 else 0.0 if max_abs > 1e-6: print(f"{name} inverse is NOT valid anymore.") @@ -468,12 +468,12 @@ def _is_inverse_still_valid(self, inv, mat, name="", pre=None): def _compute_inverse(self, mat, which="matrix"): print(f"Computing inverse for {which} using method {self._method_to_solve}") if self._method_to_solve in ("DirectNPInverse", "InexactNPInverse"): - return np.linalg.inv(mat) + return xp.linalg.inv(mat) elif self._method_to_solve == "ScipySparse": return sc.sparse.linalg.inv(mat) elif self._method_to_solve == "SparseSolver": solver = SparseSolver(mat) - return solver.solve(np.eye(mat.shape[0])) + return solver.solve(xp.eye(mat.shape[0])) else: raise ValueError(f"Unknown solver method {self._method_to_solve}") @@ -481,14 +481,14 @@ def _spectral_analysis(self): # Spectral analysis # A11 before if self._method_to_solve in ("DirectNPInverse", "InexactNPInverse"): - eigvalsA11_before, eigvecs_before = np.linalg.eig(self._A[0]) - condA11_before = np.linalg.cond(self._A[0]) + eigvalsA11_before, eigvecs_before = xp.linalg.eig(self._A[0]) + condA11_before = xp.linalg.cond(self._A[0]) elif self._method_to_solve in ("SparseSolver", "ScipySparse"): - eigvalsA11_before, eigvecs_before = np.linalg.eig(self._A[0].toarray()) - condA11_before = np.linalg.cond(self._A[0].toarray()) + eigvalsA11_before, eigvecs_before = xp.linalg.eig(self._A[0].toarray()) + condA11_before = xp.linalg.cond(self._A[0].toarray()) maxbeforeA11 = max(eigvalsA11_before) - maxbeforeA11_abs = np.max(np.abs(eigvalsA11_before)) - minbeforeA11_abs = np.min(np.abs(eigvalsA11_before)) + maxbeforeA11_abs = xp.max(xp.abs(eigvalsA11_before)) + minbeforeA11_abs = xp.min(xp.abs(eigvalsA11_before)) minbeforeA11 = min(eigvalsA11_before) specA11_bef = maxbeforeA11 / minbeforeA11 specA11_bef_abs = maxbeforeA11_abs / minbeforeA11_abs @@ -501,14 +501,14 @@ def _spectral_analysis(self): # A22 before if self._method_to_solve in ("DirectNPInverse", "InexactNPInverse"): - eigvalsA22_before, eigvecs_before = np.linalg.eig(self._A[1]) - condA22_before = np.linalg.cond(self._A[1]) + eigvalsA22_before, eigvecs_before = xp.linalg.eig(self._A[1]) + condA22_before = xp.linalg.cond(self._A[1]) elif self._method_to_solve in ("SparseSolver", "ScipySparse"): - eigvalsA22_before, eigvecs_before = np.linalg.eig(self._A[1].toarray()) - condA22_before = np.linalg.cond(self._A[1].toarray()) + eigvalsA22_before, eigvecs_before = xp.linalg.eig(self._A[1].toarray()) + condA22_before = xp.linalg.cond(self._A[1].toarray()) maxbeforeA22 = max(eigvalsA22_before) - maxbeforeA22_abs = np.max(np.abs(eigvalsA22_before)) - minbeforeA22_abs = np.min(np.abs(eigvalsA22_before)) + maxbeforeA22_abs = xp.max(xp.abs(eigvalsA22_before)) + minbeforeA22_abs = xp.min(xp.abs(eigvalsA22_before)) minbeforeA22 = min(eigvalsA22_before) specA22_bef = maxbeforeA22 / minbeforeA22 specA22_bef_abs = maxbeforeA22_abs / minbeforeA22_abs @@ -523,13 +523,13 @@ def _spectral_analysis(self): if self._preconditioner == True: # A11 after preconditioning with its inverse if self._method_to_solve in ("DirectNPInverse", "InexactNPInverse"): - eigvalsA11_after_prec, eigvecs_after = np.linalg.eig(self._A11npinv @ self._A[0]) # Implement this + eigvalsA11_after_prec, eigvecs_after = xp.linalg.eig(self._A11npinv @ self._A[0]) # Implement this elif self._method_to_solve in ("SparseSolver", "ScipySparse"): - eigvalsA11_after_prec, eigvecs_after = np.linalg.eig((self._A11npinv @ self._A[0]).toarray()) + eigvalsA11_after_prec, eigvecs_after = xp.linalg.eig((self._A11npinv @ self._A[0]).toarray()) maxafterA11_prec = max(eigvalsA11_after_prec) minafterA11_prec = min(eigvalsA11_after_prec) - maxafterA11_abs_prec = np.max(np.abs(eigvalsA11_after_prec)) - minafterA11_abs_prec = np.min(np.abs(eigvalsA11_after_prec)) + maxafterA11_abs_prec = xp.max(xp.abs(eigvalsA11_after_prec)) + minafterA11_abs_prec = xp.min(xp.abs(eigvalsA11_after_prec)) specA11_aft_prec = maxafterA11_prec / minafterA11_prec specA11_aft_abs_prec = maxafterA11_abs_prec / minafterA11_abs_prec # print(f'{maxafterA11_prec = }') @@ -541,15 +541,15 @@ def _spectral_analysis(self): # A22 after preconditioning with its inverse if self._method_to_solve in ("DirectNPInverse", "InexactNPInverse"): - eigvalsA22_after_prec, eigvecs_after = np.linalg.eig(self._A22npinv @ self._A[1]) # Implement this - condA22_after = np.linalg.cond(self._A22npinv @ self._A[1]) + eigvalsA22_after_prec, eigvecs_after = xp.linalg.eig(self._A22npinv @ self._A[1]) # Implement this + condA22_after = xp.linalg.cond(self._A22npinv @ self._A[1]) elif self._method_to_solve in ("SparseSolver", "ScipySparse"): - eigvalsA22_after_prec, eigvecs_after = np.linalg.eig((self._A22npinv @ self._A[1]).toarray()) - condA22_after = np.linalg.cond((self._A22npinv @ self._A[1]).toarray()) + eigvalsA22_after_prec, eigvecs_after = xp.linalg.eig((self._A22npinv @ self._A[1]).toarray()) + condA22_after = xp.linalg.cond((self._A22npinv @ self._A[1]).toarray()) maxafterA22_prec = max(eigvalsA22_after_prec) minafterA22_prec = min(eigvalsA22_after_prec) - maxafterA22_abs_prec = np.max(np.abs(eigvalsA22_after_prec)) - minafterA22_abs_prec = np.min(np.abs(eigvalsA22_after_prec)) + maxafterA22_abs_prec = xp.max(xp.abs(eigvalsA22_after_prec)) + minafterA22_abs_prec = xp.min(xp.abs(eigvalsA22_after_prec)) specA22_aft_prec = maxafterA22_prec / minafterA22_prec specA22_aft_abs_prec = maxafterA22_abs_prec / minafterA22_abs_prec # print(f'{maxafterA22_prec = }') diff --git a/src/struphy/linear_algebra/schur_solver.py b/src/struphy/linear_algebra/schur_solver.py index c29a50db8..dd41af54b 100644 --- a/src/struphy/linear_algebra/schur_solver.py +++ b/src/struphy/linear_algebra/schur_solver.py @@ -2,6 +2,8 @@ from psydac.linalg.block import BlockLinearOperator, BlockVector from psydac.linalg.solvers import inverse +from struphy.linear_algebra.solver import SolverParameters + class SchurSolver: r"""Solves for :math:`x^{n+1}` in the block system @@ -46,13 +48,23 @@ class SchurSolver: Must correspond to the chosen solver. """ - def __init__(self, A: LinearOperator, BC: LinearOperator, solver_name: str, **solver_params): + def __init__( + self, + A: LinearOperator, + BC: LinearOperator, + solver_name: str, + precond=None, # TODO: add Preconditioner base class + solver_params: SolverParameters = None, + ): assert isinstance(A, LinearOperator) assert isinstance(BC, LinearOperator) assert A.domain == BC.domain assert A.codomain == BC.codomain + if solver_params is None: + solver_params = SolverParameters() + # linear operators self._A = A self._BC = BC @@ -64,10 +76,12 @@ def __init__(self, A: LinearOperator, BC: LinearOperator, solver_name: str, **so # initialize solver with dummy matrix A self._solver_name = solver_name - if solver_params["pc"] is None: - solver_params.pop("pc") + kwargs = solver_params.__dict__ + kwargs.pop("info") + if precond is not None: + kwargs["pc"] = precond - self._solver = inverse(A, solver_name, **solver_params) + self._solver = inverse(A, solver_name, **kwargs) # right-hand side vector (avoids temporary memory allocation!) self._rhs = A.codomain.zeros() diff --git a/src/struphy/linear_algebra/solver.py b/src/struphy/linear_algebra/solver.py new file mode 100644 index 000000000..217326309 --- /dev/null +++ b/src/struphy/linear_algebra/solver.py @@ -0,0 +1,37 @@ +from dataclasses import dataclass + +from struphy.io.options import OptsNonlinearSolver + + +@dataclass +class SolverParameters: + """Parameters for psydac solvers.""" + + tol: float = 1e-8 + maxiter: int = 3000 + info: bool = False + verbose: bool = False + recycle: bool = True + + +@dataclass +class DiscreteGradientSolverParameters: + """Parameters for discrete gradient solvers.""" + + relaxation_factor: float = 0.5 + tol: float = 1e-12 + maxiter: int = 20 + verbose: bool = False + info: bool = False + + +@dataclass +class NonlinearSolverParameters: + """Parameters for psydac solvers.""" + + tol: float = 1e-8 + maxiter: int = 100 + info: bool = False + verbose: bool = False + type: OptsNonlinearSolver = "Picard" + linearize: bool = False diff --git a/src/struphy/linear_algebra/tests/test_saddle_point_propagator.py b/src/struphy/linear_algebra/tests/test_saddle_point_propagator.py index 94c4e7fab..06482f958 100644 --- a/src/struphy/linear_algebra/tests/test_saddle_point_propagator.py +++ b/src/struphy/linear_algebra/tests/test_saddle_point_propagator.py @@ -1,11 +1,12 @@ import pytest +@pytest.mark.skip @pytest.mark.mpi_skip @pytest.mark.parametrize("Nel", [[16, 1, 1], [32, 1, 1]]) @pytest.mark.parametrize("p", [[1, 1, 1], [2, 1, 1]]) @pytest.mark.parametrize("spl_kind", [[True, True, True]]) -@pytest.mark.parametrize("dirichlet_bc", [[[False, False], [False, False], [False, False]]]) +@pytest.mark.parametrize("dirichlet_bc", [((False, False), (False, False), (False, False))]) @pytest.mark.parametrize("mapping", [["Cuboid", {"l1": 0.0, "r1": 1.0, "l2": 0.0, "r2": 1.0, "l3": 0.0, "r3": 1.0}]]) @pytest.mark.parametrize("epsilon", [0.000000001]) @pytest.mark.parametrize("dt", [0.001]) @@ -20,6 +21,8 @@ def test_propagator1D(Nel, p, spl_kind, dirichlet_bc, mapping, epsilon, dt): from struphy.feec.utilities import compare_arrays from struphy.fields_background.equils import HomogenSlab from struphy.geometry import domains + from struphy.initial import perturbations + from struphy.models.variables import FEECVariable from struphy.propagators.propagators_fields import TwoFluidQuasiNeutralFull mpi_comm = MPI.COMM_WORLD @@ -57,42 +60,57 @@ def test_propagator1D(Nel, p, spl_kind, dirichlet_bc, mapping, epsilon, dt): bas_ops = BasisProjectionOperators(derham, domain, eq_mhd=eq_mhd) # Manufactured solutions - uvec = derham.create_spline_function("u", "Hdiv") - u_evec = derham.create_spline_function("u_e", "Hdiv") - potentialvec = derham.create_spline_function("potential", "L2") - uinitial = derham.create_spline_function("u", "Hdiv") - - pp_u = { - "ManufacturedSolutionVelocity": { - "given_in_basis": ["physical", None, None], - "species": "Ions", - "comp": "0", - "dimension": "1D", - } - } - pp_ue = { - "ManufacturedSolutionVelocity": { - "given_in_basis": ["physical", None, None], - "species": "Electrons", - "comp": "0", - "dimension": "1D", - } - } - pp_potential = { - "ManufacturedSolutionPotential": { - "given_in_basis": "physical", - "dimension": "1D", - } - } - - uvec.initialize_coeffs(domain=domain, pert_params=pp_u) - u_evec.initialize_coeffs(domain=domain, pert_params=pp_ue) - potentialvec.initialize_coeffs(domain=domain, pert_params=pp_potential) + uvec = FEECVariable(space="Hdiv") + u_evec = FEECVariable(space="Hdiv") + potentialvec = FEECVariable(space="L2") + uinitial = FEECVariable(space="Hdiv") + + pp_u = perturbations.ManufacturedSolutionVelocity() + pp_ue = perturbations.ManufacturedSolutionVelocity(species="Electrons") + pp_potential = perturbations.ManufacturedSolutionPotential() + + # pp_u = { + # "ManufacturedSolutionVelocity": { + # "given_in_basis": ["physical", None, None], + # "species": "Ions", + # "comp": "0", + # "dimension": "1D", + # } + # } + # pp_ue = { + # "ManufacturedSolutionVelocity": { + # "given_in_basis": ["physical", None, None], + # "species": "Electrons", + # "comp": "0", + # "dimension": "1D", + # } + # } + # pp_potential = { + # "ManufacturedSolutionPotential": { + # "given_in_basis": "physical", + # "dimension": "1D", + # } + # } + + uvec.add_perturbation(pp_u) + uvec.allocate(derham, domain, eq_mhd) + + u_evec.add_perturbation(pp_ue) + u_evec.allocate(derham, domain, eq_mhd) + + potentialvec.add_perturbation(pp_potential) + potentialvec.allocate(derham, domain, eq_mhd) + + uinitial.allocate(derham, domain, eq_mhd) + + # uvec.initialize_coeffs(domain=domain, pert_params=pp_u) + # u_evec.initialize_coeffs(domain=domain, pert_params=pp_ue) + # potentialvec.initialize_coeffs(domain=domain, pert_params=pp_potential) # Save manufactured solution to compare it later with the outcome of the propagator - uvec_initial = uvec.vector.copy() - u_evec_initial = u_evec.vector.copy() - potentialvec_initial = potentialvec.vector.copy() + uvec_initial = uvec.spline.vector.copy() + u_evec_initial = u_evec.spline.vector.copy() + potentialvec_initial = potentialvec.spline.vector.copy() solver = {} solver["type"] = ["gmres", None] @@ -109,9 +127,9 @@ def test_propagator1D(Nel, p, spl_kind, dirichlet_bc, mapping, epsilon, dt): # Starting with initial condition u=0 and ue and phi start with manufactured solution prop = TwoFluidQuasiNeutralFull( - uinitial.vector, - u_evec.vector, - potentialvec.vector, + uinitial.spline.vector, + u_evec.spline.vector, + potentialvec.spline.vector, stab_sigma=epsilon, D1_dt=dt, variant="Uzawa", @@ -194,11 +212,12 @@ def test_propagator1D(Nel, p, spl_kind, dirichlet_bc, mapping, epsilon, dt): import pytest +@pytest.mark.skip @pytest.mark.mpi_skip @pytest.mark.parametrize("Nel", [[16, 16, 1], [32, 32, 1]]) @pytest.mark.parametrize("p", [[1, 1, 1], [2, 2, 1]]) @pytest.mark.parametrize("spl_kind", [[True, True, True]]) -@pytest.mark.parametrize("dirichlet_bc", [[[False, False], [False, False], [False, False]]]) +@pytest.mark.parametrize("dirichlet_bc", [((False, False), (False, False), (False, False))]) @pytest.mark.parametrize("mapping", [["Cuboid", {"l1": 0.0, "r1": 1.0, "l2": 0.0, "r2": 1.0, "l3": 0.0, "r3": 1.0}]]) @pytest.mark.parametrize("epsilon", [0.001]) @pytest.mark.parametrize("dt", [0.01]) @@ -213,7 +232,8 @@ def test_propagator2D(Nel, p, spl_kind, dirichlet_bc, mapping, epsilon, dt): from struphy.feec.utilities import compare_arrays from struphy.fields_background.equils import HomogenSlab from struphy.geometry import domains - from struphy.propagators import TwoFluidQuasiNeutralFull + from struphy.models.variables import FEECVariable + from struphy.propagators.propagators_fields import TwoFluidQuasiNeutralFull mpi_comm = MPI.COMM_WORLD mpi_rank = mpi_comm.Get_rank() @@ -250,9 +270,9 @@ def test_propagator2D(Nel, p, spl_kind, dirichlet_bc, mapping, epsilon, dt): bas_ops = BasisProjectionOperators(derham, domain, eq_mhd=eq_mhd) # Manufactured solutions - uvec = derham.create_spline_function("u", "Hdiv") - u_evec = derham.create_spline_function("u_e", "Hdiv") - potentialvec = derham.create_spline_function("potential", "L2") + uvec = FEECVariable(space="Hdiv") + u_evec = FEECVariable(space="Hdiv") + potentialvec = FEECVariable(space="L2") pp_u = { "ManufacturedSolutionVelocity": { @@ -386,6 +406,15 @@ def test_propagator2D(Nel, p, spl_kind, dirichlet_bc, mapping, epsilon, dt): if __name__ == "__main__": + test_propagator1D( + [16, 1, 1], + [2, 2, 1], + [True, True, True], + [[False, False], [False, False], [False, False]], + ["Cuboid", {"l1": 0.0, "r1": 1.0, "l2": 0.0, "r2": 1.0, "l3": 0.0, "r3": 1.0}], + 0.001, + 0.01, + ) # test_propagator2D( # [16, 16, 1], # [1, 1, 1], @@ -395,15 +424,15 @@ def test_propagator2D(Nel, p, spl_kind, dirichlet_bc, mapping, epsilon, dt): # 0.001, # 0.01, # ) - test_propagator2D( - [16, 16, 1], - [2, 2, 1], - [True, True, True], - [[False, False], [False, False], [False, False]], - ["Cuboid", {"l1": 0.0, "r1": 1.0, "l2": 0.0, "r2": 1.0, "l3": 0.0, "r3": 1.0}], - 0.001, - 0.01, - ) + # test_propagator2D( + # [16, 16, 1], + # [2, 2, 1], + # [True, True, True], + # [[False, False], [False, False], [False, False]], + # ["Cuboid", {"l1": 0.0, "r1": 1.0, "l2": 0.0, "r2": 1.0, "l3": 0.0, "r3": 1.0}], + # 0.001, + # 0.01, + # ) # test_propagator2D( # [32, 32, 1], # [2, 2, 1], diff --git a/src/struphy/linear_algebra/tests/test_saddlepoint_massmatrices.py b/src/struphy/linear_algebra/tests/test_saddlepoint_massmatrices.py index 43d07a895..ed5ff4d8a 100644 --- a/src/struphy/linear_algebra/tests/test_saddlepoint_massmatrices.py +++ b/src/struphy/linear_algebra/tests/test_saddlepoint_massmatrices.py @@ -6,13 +6,14 @@ @pytest.mark.parametrize("Nel", [[12, 8, 1]]) @pytest.mark.parametrize("p", [[3, 3, 1]]) @pytest.mark.parametrize("spl_kind", [[False, True, True]]) -@pytest.mark.parametrize("dirichlet_bc", [[[False, False], [False, False], [False, False]]]) +@pytest.mark.parametrize("dirichlet_bc", [((False, False), (False, False), (False, False))]) @pytest.mark.parametrize("mapping", [["Cuboid", {"l1": 0.0, "r1": 2.0, "l2": 0.0, "r2": 3.0, "l3": 0.0, "r3": 6.0}]]) def test_saddlepointsolver(method_for_solving, Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): """Test saddle-point-solver with manufactured solutions.""" import time + import cunumpy as xp import scipy as sc from psydac.ddm.mpi import mpi as MPI from psydac.linalg.basic import IdentityOperator @@ -29,7 +30,6 @@ def test_saddlepointsolver(method_for_solving, Nel, p, spl_kind, dirichlet_bc, m from struphy.geometry import domains from struphy.initial import perturbations from struphy.linear_algebra.saddle_point import SaddlePointSolver - from struphy.utils.arrays import xp as np mpi_comm = MPI.COMM_WORLD mpi_rank = mpi_comm.Get_rank() @@ -132,12 +132,12 @@ def test_saddlepointsolver(method_for_solving, Nel, p, spl_kind, dirichlet_bc, m A11np = M2np / dt + nu * (Dnp.T @ M3np @ Dnp + S21np.T @ Cnp.T @ M2np @ Cnp @ S21np) - M2Bnp if method_to_solve in ("DirectNPInverse", "InexactNPInverse"): A22np = ( - stab_sigma * np.identity(A11np.shape[0]) + stab_sigma * xp.identity(A11np.shape[0]) + nue * (Dnp.T @ M3np @ Dnp + S21np.T @ Cnp.T @ M2np @ Cnp @ S21np) + M2Bnp ) # Preconditioner - _A22np_pre = stab_sigma * np.identity(A22np.shape[0]) # + nue*(Dnp.T @ M3np @ Dnp) + _A22np_pre = stab_sigma * xp.identity(A22np.shape[0]) # + nue*(Dnp.T @ M3np @ Dnp) _A11np_pre = M2np / dt # + nu * (Dnp.T @ M3np @ Dnp) elif method_to_solve in ("SparseSolver", "ScipySparse"): A22np = ( @@ -201,9 +201,9 @@ def test_saddlepointsolver(method_for_solving, Nel, p, spl_kind, dirichlet_bc, m - (B[0, 1].T).dot(y1_rdm) ) TestDiv = -B1.dot(x1) + B2.dot(x2) - RestDiv = np.linalg.norm(TestDiv.toarray()) - RestA = np.linalg.norm(TestA.toarray()) - RestAe = np.linalg.norm(TestAe.toarray()) + RestDiv = xp.linalg.norm(TestDiv.toarray()) + RestA = xp.linalg.norm(TestA.toarray()) + RestAe = xp.linalg.norm(TestAe.toarray()) print(f"{RestA =}") print(f"{RestAe =}") print(f"{RestDiv =}") @@ -218,10 +218,10 @@ def test_saddlepointsolver(method_for_solving, Nel, p, spl_kind, dirichlet_bc, m - (nue * (Dnp.T @ M3np @ Dnp + S21np.T @ Cnp.T @ M2np @ Cnp @ S21np) + M2Bnp).dot(x2np) - B2np.T.dot(ynp) ) - RestAnp = np.linalg.norm(TestAnp) - RestAenp = np.linalg.norm(TestAenp) + RestAnp = xp.linalg.norm(TestAnp) + RestAenp = xp.linalg.norm(TestAenp) TestDivnp = -B1np.dot(x1np) + B2np.dot(x2np) - RestDivnp = np.linalg.norm(TestDivnp) + RestDivnp = xp.linalg.norm(TestDivnp) print(f"{RestAnp =}") print(f"{RestAenp =}") print(f"{RestDivnp =}") @@ -296,22 +296,22 @@ def test_saddlepointsolver(method_for_solving, Nel, p, spl_kind, dirichlet_bc, m elapsed_time = end_time - start_time print(f"Method execution time: {elapsed_time:.6f} seconds") - if isinstance(x_uzawa[0], np.ndarray): - # Output as np.ndarray + if isinstance(x_uzawa[0], xp.ndarray): + # Output as xp.ndarray Rx1 = x1np - x_uzawa[0] Rx2 = x2np - x_uzawa[1] Ry = ynp - y_uzawa - residualx_normx1 = np.linalg.norm(Rx1) - residualx_normx2 = np.linalg.norm(Rx2) - residualy_norm = np.linalg.norm(Ry) + residualx_normx1 = xp.linalg.norm(Rx1) + residualx_normx2 = xp.linalg.norm(Rx2) + residualy_norm = xp.linalg.norm(Ry) TestRest1 = F1np - A11np.dot(x_uzawa[0]) - B1np.T.dot(y_uzawa) - TestRest1val = np.max(abs(TestRest1)) + TestRest1val = xp.max(abs(TestRest1)) Testoldy1 = F1np - A11np.dot(x_uzawa[0]) - B1np.T.dot(ynp) - Testoldy1val = np.max(abs(Testoldy1)) + Testoldy1val = xp.max(abs(Testoldy1)) TestRest2 = F2np - A22np.dot(x_uzawa[1]) - B2np.T.dot(y_uzawa) - TestRest2val = np.max(abs(TestRest2)) + TestRest2val = xp.max(abs(TestRest2)) Testoldy2 = F2np - A22np.dot(x_uzawa[1]) - B2np.T.dot(ynp) - Testoldy2val = np.max(abs(Testoldy2)) + Testoldy2val = xp.max(abs(Testoldy2)) print(f"{TestRest1val =}") print(f"{TestRest2val =}") print(f"{Testoldy1val =}") @@ -329,18 +329,18 @@ def test_saddlepointsolver(method_for_solving, Nel, p, spl_kind, dirichlet_bc, m Rx1 = x1 - x_uzawa[0] Rx2 = x2 - x_uzawa[1] Ry = y1_rdm - y_uzawa - residualx_normx1 = np.linalg.norm(Rx1.toarray()) - residualx_normx2 = np.linalg.norm(Rx2.toarray()) - residualy_norm = np.linalg.norm(Ry.toarray()) + residualx_normx1 = xp.linalg.norm(Rx1.toarray()) + residualx_normx2 = xp.linalg.norm(Rx2.toarray()) + residualy_norm = xp.linalg.norm(Ry.toarray()) TestRest1 = F1 - A11.dot(x_uzawa[0]) - B1T.dot(y_uzawa) - TestRest1val = np.max(abs(TestRest1.toarray())) + TestRest1val = xp.max(abs(TestRest1.toarray())) Testoldy1 = F1 - A11.dot(x_uzawa[0]) - B1T.dot(y1_rdm) - Testoldy1val = np.max(abs(Testoldy1.toarray())) + Testoldy1val = xp.max(abs(Testoldy1.toarray())) TestRest2 = F2 - A22.dot(x_uzawa[1]) - B2T.dot(y_uzawa) - TestRest2val = np.max(abs(TestRest2.toarray())) + TestRest2val = xp.max(abs(TestRest2.toarray())) Testoldy2 = F2 - A22.dot(x_uzawa[1]) - B2T.dot(y1_rdm) - Testoldy2val = np.max(abs(Testoldy2.toarray())) + Testoldy2val = xp.max(abs(Testoldy2.toarray())) # print(f"{TestRest1val =}") # print(f"{TestRest2val =}") # print(f"{Testoldy1val =}") @@ -372,16 +372,15 @@ def _plot_residual_norms(residual_norms): def _plot_velocity(data_reshaped): + import cunumpy as xp import matplotlib import matplotlib.pyplot as plt - from struphy.utils.arrays import xp as np - matplotlib.use("Agg") - x = np.linspace(0, 1, 30) - y = np.linspace(0, 1, 30) - X, Y = np.meshgrid(x, y) + x = xp.linspace(0, 1, 30) + y = xp.linspace(0, 1, 30) + X, Y = xp.meshgrid(x, y) plt.figure(figsize=(6, 5)) plt.imshow(data_reshaped.T, cmap="viridis", origin="lower", extent=[0, 1, 0, 1]) diff --git a/src/struphy/linear_algebra/tests/test_stencil_dot_kernels.py b/src/struphy/linear_algebra/tests/test_stencil_dot_kernels.py index f91d6872d..d2c2238ff 100644 --- a/src/struphy/linear_algebra/tests/test_stencil_dot_kernels.py +++ b/src/struphy/linear_algebra/tests/test_stencil_dot_kernels.py @@ -13,13 +13,13 @@ def test_1d(Nel, p, spl_kind, domain_ind, codomain_ind): a) the result from kernel in struphy.linear_algebra.stencil_dot_kernels.matvec_1d_kernel b) the result from Stencil .dot with precompiled=True""" + import cunumpy as xp from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL from psydac.ddm.mpi import mpi as MPI from psydac.linalg.stencil import StencilMatrix, StencilVector from struphy.feec.psydac_derham import Derham from struphy.linear_algebra.stencil_dot_kernels import matvec_1d_kernel - from struphy.utils.arrays import xp as np # only for M1 Mac users PSYDAC_BACKEND_GPYCCEL["flags"] = "-O3 -march=native -mtune=native -ffast-math -ffree-line-length-none" @@ -78,8 +78,8 @@ def test_1d(Nel, p, spl_kind, domain_ind, codomain_ind): mat_pre._data[p_out + i_loc, d1] = m - i # random vector - # np.random.seed(123) - x[s_in : e_in + 1] = np.random.rand(domain.coeff_space.npts[0]) + # xp.random.seed(123) + x[s_in : e_in + 1] = xp.random.rand(domain.coeff_space.npts[0]) if rank == 0: print(f"spl_kind={spl_kind}") @@ -118,8 +118,8 @@ def test_1d(Nel, p, spl_kind, domain_ind, codomain_ind): print("\nout_ker=", out_ker._data) print("\nout_pre=", out_pre._data) - assert np.allclose(out_ker._data, out._data) - assert np.allclose(out_pre._data, out._data) + assert xp.allclose(out_ker._data, out._data) + assert xp.allclose(out_pre._data, out._data) @pytest.mark.parametrize("Nel", [[12, 16, 20]]) @@ -134,13 +134,13 @@ def test_3d(Nel, p, spl_kind, domain_ind, codomain_ind): a) the result from kernel in struphy.linear_algebra.stencil_dot_kernels.matvec_1d_kernel b) the result from Stencil .dot with precompiled=True""" + import cunumpy as xp from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL from psydac.ddm.mpi import mpi as MPI from psydac.linalg.stencil import StencilMatrix, StencilVector from struphy.feec.psydac_derham import Derham from struphy.linear_algebra.stencil_dot_kernels import matvec_3d_kernel - from struphy.utils.arrays import xp as np # only for M1 Mac users PSYDAC_BACKEND_GPYCCEL["flags"] = "-O3 -march=native -mtune=native -ffast-math -ffree-line-length-none" @@ -177,16 +177,16 @@ def test_3d(Nel, p, spl_kind, domain_ind, codomain_ind): x = StencilVector(domain.coeff_space) out_ker = StencilVector(codomain.coeff_space) - s_out = np.array(mat.codomain.starts) - e_out = np.array(mat.codomain.ends) - p_out = np.array(mat.codomain.pads) - s_in = np.array(mat.domain.starts) - e_in = np.array(mat.domain.ends) - p_in = np.array(mat.domain.pads) + s_out = xp.array(mat.codomain.starts) + e_out = xp.array(mat.codomain.ends) + p_out = xp.array(mat.codomain.pads) + s_in = xp.array(mat.domain.starts) + e_in = xp.array(mat.domain.ends) + p_in = xp.array(mat.domain.pads) # random matrix - np.random.seed(123) - tmp1 = np.random.rand(*codomain.coeff_space.npts, *[2 * q + 1 for q in p]) + xp.random.seed(123) + tmp1 = xp.random.rand(*codomain.coeff_space.npts, *[2 * q + 1 for q in p]) mat[ s_out[0] : e_out[0] + 1, s_out[1] : e_out[1] + 1, @@ -207,7 +207,7 @@ def test_3d(Nel, p, spl_kind, domain_ind, codomain_ind): ] # random vector - tmp2 = np.random.rand(*domain.coeff_space.npts) + tmp2 = xp.random.rand(*domain.coeff_space.npts) x[ s_in[0] : e_in[0] + 1, s_in[1] : e_in[1] + 1, @@ -226,7 +226,7 @@ def test_3d(Nel, p, spl_kind, domain_ind, codomain_ind): # kernel matvec add = [int(end_in >= end_out) for end_in, end_out in zip(mat.domain.ends, mat.codomain.ends)] - add = np.array(add) + add = xp.array(add) matvec_3d_kernel(mat._data, x._data, out_ker._data, s_in, p_in, add, s_out, e_out, p_out) # precompiled .dot @@ -253,12 +253,12 @@ def test_3d(Nel, p, spl_kind, domain_ind, codomain_ind): print("\nout_ker[2]=", out_ker._data[p_out[0], p_out[1], :]) print("\nout_pre[2]=", out_pre._data[p_out[0], p_out[1], :]) - assert np.allclose( + assert xp.allclose( out_ker[s_out[0] : e_out[0] + 1, s_out[1] : e_out[1] + 1, s_out[2] : e_out[2] + 1], out[s_out[0] : e_out[0] + 1, s_out[1] : e_out[1] + 1, s_out[2] : e_out[2] + 1], ) - assert np.allclose( + assert xp.allclose( out_pre[s_out[0] : e_out[0] + 1, s_out[1] : e_out[1] + 1, s_out[2] : e_out[2] + 1], out[s_out[0] : e_out[0] + 1, s_out[1] : e_out[1] + 1, s_out[2] : e_out[2] + 1], ) diff --git a/src/struphy/linear_algebra/tests/test_stencil_transpose_kernels.py b/src/struphy/linear_algebra/tests/test_stencil_transpose_kernels.py index 0265ba741..1125a980c 100644 --- a/src/struphy/linear_algebra/tests/test_stencil_transpose_kernels.py +++ b/src/struphy/linear_algebra/tests/test_stencil_transpose_kernels.py @@ -13,13 +13,13 @@ def test_1d(Nel, p, spl_kind, domain_ind, codomain_ind): a) the result from kernel in struphy.linear_algebra.stencil_transpose_kernels.transpose_1d_kernel b) the result from Stencil .transpose with precompiled=True""" + import cunumpy as xp from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL from psydac.ddm.mpi import mpi as MPI from psydac.linalg.stencil import StencilMatrix from struphy.feec.psydac_derham import Derham from struphy.linear_algebra.stencil_transpose_kernels import transpose_1d_kernel - from struphy.utils.arrays import xp as np # only for M1 Mac users PSYDAC_BACKEND_GPYCCEL["flags"] = "-O3 -march=native -mtune=native -ffast-math -ffree-line-length-none" @@ -112,8 +112,8 @@ def test_1d(Nel, p, spl_kind, domain_ind, codomain_ind): print("\nmatT_pre=", matT_pre._data) print("\nmatT_pre.toarray=\n", matT_pre.toarray()) - assert np.allclose(matT_ker[s_in : e_in + 1, :], matT[s_in : e_in + 1, :]) - assert np.allclose(matT_pre[s_in : e_in + 1, :], matT[s_in : e_in + 1, :]) + assert xp.allclose(matT_ker[s_in : e_in + 1, :], matT[s_in : e_in + 1, :]) + assert xp.allclose(matT_pre[s_in : e_in + 1, :], matT[s_in : e_in + 1, :]) @pytest.mark.parametrize("Nel", [[12, 16, 20]]) @@ -128,13 +128,13 @@ def test_3d(Nel, p, spl_kind, domain_ind, codomain_ind): a) the result from kernel in struphy.linear_algebra.stencil_transpose_kernels.transpose_3d_kernel b) the result from Stencil .transpose with precompiled=True""" + import cunumpy as xp from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL from psydac.ddm.mpi import mpi as MPI from psydac.linalg.stencil import StencilMatrix from struphy.feec.psydac_derham import Derham from struphy.linear_algebra.stencil_transpose_kernels import transpose_3d_kernel - from struphy.utils.arrays import xp as np # only for M1 Mac users PSYDAC_BACKEND_GPYCCEL["flags"] = "-O3 -march=native -mtune=native -ffast-math -ffree-line-length-none" @@ -170,16 +170,16 @@ def test_3d(Nel, p, spl_kind, domain_ind, codomain_ind): mat_pre = StencilMatrix(domain.coeff_space, codomain.coeff_space, backend=PSYDAC_BACKEND_GPYCCEL, precompiled=True) matT_ker = StencilMatrix(codomain.coeff_space, domain.coeff_space) - s_out = np.array(mat.codomain.starts) - e_out = np.array(mat.codomain.ends) - p_out = np.array(mat.codomain.pads) - s_in = np.array(mat.domain.starts) - e_in = np.array(mat.domain.ends) - p_in = np.array(mat.domain.pads) + s_out = xp.array(mat.codomain.starts) + e_out = xp.array(mat.codomain.ends) + p_out = xp.array(mat.codomain.pads) + s_in = xp.array(mat.domain.starts) + e_in = xp.array(mat.domain.ends) + p_in = xp.array(mat.domain.pads) # random matrix - np.random.seed(123) - tmp1 = np.random.rand(*codomain.coeff_space.npts, *[2 * q + 1 for q in p]) + xp.random.seed(123) + tmp1 = xp.random.rand(*codomain.coeff_space.npts, *[2 * q + 1 for q in p]) mat[ s_out[0] : e_out[0] + 1, s_out[1] : e_out[1] + 1, @@ -208,7 +208,7 @@ def test_3d(Nel, p, spl_kind, domain_ind, codomain_ind): # kernel transpose add = [int(end_out >= end_in) for end_in, end_out in zip(mat.domain.ends, mat.codomain.ends)] - add = np.array(add) + add = xp.array(add) transpose_3d_kernel(mat._data, matT_ker._data, s_out, p_out, add, s_in, e_in, p_in) # precompiled transpose @@ -237,12 +237,12 @@ def test_3d(Nel, p, spl_kind, domain_ind, codomain_ind): print("\nmatT_ker[2]=", matT_ker._data[p_in[0], p_in[1], :, 1, 1, :]) print("\nmatT_pre[2]=", matT_pre._data[p_in[0], p_in[1], :, 1, 1, :]) - assert np.allclose( + assert xp.allclose( matT_ker[s_in[0] : e_in[0] + 1, s_in[1] : e_in[1] + 1, s_in[2] : e_in[2] + 1], matT[s_in[0] : e_in[0] + 1, s_in[1] : e_in[1] + 1, s_in[2] : e_in[2] + 1], ) - assert np.allclose( + assert xp.allclose( matT_pre[s_in[0] : e_in[0] + 1, s_in[1] : e_in[1] + 1, s_in[2] : e_in[2] + 1], matT[s_in[0] : e_in[0] + 1, s_in[1] : e_in[1] + 1, s_in[2] : e_in[2] + 1], ) diff --git a/src/struphy/main.py b/src/struphy/main.py index 4175d1024..524e19eb7 100644 --- a/src/struphy/main.py +++ b/src/struphy/main.py @@ -1,76 +1,74 @@ -from typing import Optional - - -def main( - model_name: Optional[str], - parameters: dict | str, - path_out: str, +import copy +import datetime +import glob +import os +import pickle +import shutil +import sysconfig +import time +from typing import Optional, TypedDict + +import cunumpy as xp +import h5py +from line_profiler import profile +from psydac.ddm.mpi import MockMPI +from psydac.ddm.mpi import mpi as MPI +from pyevtk.hl import gridToVTK + +from struphy.fields_background.base import FluidEquilibrium, FluidEquilibriumWithB +from struphy.fields_background.equils import HomogenSlab +from struphy.geometry import domains +from struphy.geometry.base import Domain +from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, Time, Units +from struphy.io.output_handling import DataContainer +from struphy.io.setup import import_parameters_py, setup_folders +from struphy.models.base import StruphyModel +from struphy.models.species import Species +from struphy.models.variables import FEECVariable +from struphy.pic.base import Particles +from struphy.post_processing.orbits import orbits_tools +from struphy.post_processing.post_processing_tools import ( + create_femfields, + create_vtk, + eval_femfields, + get_params_of_run, + post_process_f, + post_process_markers, + post_process_n_sph, +) +from struphy.profiling.profiling import ProfileManager +from struphy.topology import grids +from struphy.topology.grids import TensorProductGrid +from struphy.utils.clone_config import CloneConfig +from struphy.utils.utils import dict_to_yaml + + +@profile +def run( + model: StruphyModel, *, - restart: bool = False, - runtime: int = 300, - save_step: int = 1, + params_path: str = None, + env: EnvironmentOptions = EnvironmentOptions(), + base_units: BaseUnits = BaseUnits(), + time_opts: Time = Time(), + domain: Domain = domains.Cuboid(), + equil: FluidEquilibrium = HomogenSlab(), + grid: TensorProductGrid = None, + derham_opts: DerhamOptions = None, verbose: bool = False, - supress_out: bool = False, - sort_step: int = 0, - num_clones: int = 1, ): """ Run a Struphy model. Parameters ---------- - model_name : str - The name of the model to run. Type "struphy run --help" in your terminal to see a list of available models. - - parameters : dict | str - The simulation parameters. Can either be a dictionary OR a string (path of .yml parameter file) - - path_out : str - The output directory. Will create a folder if it does not exist OR cleans the folder for new runs. - - restart : bool, optional - Whether to restart a run (default=False). - - runtime : int, optional - Maximum run time of simulation in minutes. Will finish the time integration once this limit is reached (default=300). - - save_step : int, optional - When to save data output: every time step (save_step=1), every second time step (save_step=2), etc (default=1). + model : StruphyModel + The model to run. Check https://struphy.pages.mpcdf.de/struphy/sections/models.html for available models. - verbose : bool - Show full screen output. - - supress_out : bool - Whether to supress screen output during time integration. - - sort_step: int, optional - Sort markers in memory every N time steps (default=0, which means markers are sorted only at the start of simulation) - - num_clones: int, optional - Number of domain clones (default=1) + params_path : str + Absolute path to .py parameter file. """ - import copy - import os - import time - - from psydac.ddm.mpi import MockMPI - from psydac.ddm.mpi import mpi as MPI - from pyevtk.hl import gridToVTK - - from struphy.feec.psydac_derham import SplineFunction - from struphy.fields_background.base import FluidEquilibriumWithB - from struphy.io.output_handling import DataContainer - from struphy.io.setup import pre_processing - from struphy.models import fluid, hybrid, kinetic, toy - from struphy.models.base import StruphyModel - from struphy.profiling.profiling import ProfileManager - from struphy.utils.arrays import xp as np - from struphy.utils.clone_config import CloneConfig - - if sort_step: - from struphy.pic.base import Particles - if isinstance(MPI, MockMPI): comm = None rank = 0 @@ -90,30 +88,87 @@ def main( Barrier() start_simulation = time.time() - # loading of simulation parameters, creating output folder and printing information to screen - params = pre_processing( - model_name=model_name, - parameters=parameters, + # check model + assert hasattr(model, "propagators"), "Attribute 'self.propagators' must be set in model __init__!" + model_name = model.__class__.__name__ + model.verbose = verbose + + if rank == 0: + print(f"\n*** Starting run for model '{model_name}':") + + # meta-data + path_out = env.path_out + restart = env.restart + max_runtime = env.max_runtime + save_step = env.save_step + sort_step = env.sort_step + num_clones = env.num_clones + use_mpi = (not comm is None,) + + meta = {} + meta["platform"] = sysconfig.get_platform() + meta["python version"] = sysconfig.get_python_version() + meta["model name"] = model_name + meta["parameter file"] = params_path + meta["output folder"] = path_out + meta["MPI processes"] = size + meta["use MPI.COMM_WORLD"] = use_mpi + meta["number of domain clones"] = num_clones + meta["restart"] = restart + meta["max wall-clock [min]"] = max_runtime + meta["save interval [steps]"] = save_step + + if rank == 0: + print("\nMETADATA:") + for k, v in meta.items(): + print(f"{k}:".ljust(25), v) + + # creating output folders + setup_folders( path_out=path_out, restart=restart, - max_sim_time=runtime, - save_step=save_step, - mpi_rank=rank, - mpi_size=size, - use_mpi=not comm is None, - num_clones=num_clones, verbose=verbose, ) - if model_name is None: - assert "model" in params, "If model is not specified, then model: MODEL must be specified in the params!" - model_name = params["model"] - - if rank < 32: - print(f"Rank {rank}: calling struphy/main.py for model {model_name} ...") - if size > 32 and rank == 32: - print(f"Ranks > 31: calling struphy/main.py for model {model_name} ...") + # add derived units + units = Units(base_units) + # save parameter file + if rank == 0: + # save python param file + if params_path is not None: + assert params_path[-3:] == ".py" + shutil.copy2( + params_path, + os.path.join(path_out, "parameters.py"), + ) + # pickle struphy objects + else: + with open(os.path.join(path_out, "env.bin"), "wb") as f: + pickle.dump(env, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "base_units.bin"), "wb") as f: + pickle.dump(base_units, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "time_opts.bin"), "wb") as f: + pickle.dump(time_opts, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "domain.bin"), "wb") as f: + # WORKAROUND: cannot pickle pyccelized classes at the moment + tmp_dct = {"name": domain.__class__.__name__, "params": domain.params} + pickle.dump(tmp_dct, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "equil.bin"), "wb") as f: + # WORKAROUND: cannot pickle pyccelized classes at the moment + if equil is not None: + tmp_dct = {"name": equil.__class__.__name__, "params": equil.params} + else: + tmp_dct = {} + pickle.dump(tmp_dct, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "grid.bin"), "wb") as f: + pickle.dump(grid, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "derham_opts.bin"), "wb") as f: + pickle.dump(derham_opts, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path_out, "model_class.bin"), "wb") as f: + pickle.dump(model.__class__, f, pickle.HIGHEST_PROTOCOL) + + # config clones if comm is None: clone_config = None else: @@ -124,32 +179,65 @@ def main( # MPI.COMM_WORLD : comm # within a clone: : sub_comm # between the clones : inter_comm - clone_config = CloneConfig(comm=comm, params=params, num_clones=num_clones) + clone_config = CloneConfig(comm=comm, params=None, num_clones=num_clones) clone_config.print_clone_config() - if "kinetic" in params: + if model.particle_species: clone_config.print_particle_config() - # instantiate Struphy model (will allocate model objects and associated memory) - StruphyModel.verbose = verbose + model.clone_config = clone_config + Barrier() - objs = [fluid, kinetic, hybrid, toy] - for obj in objs: - try: - model_class = getattr(obj, model_name) - except AttributeError: - pass + ## configure model instance - with ProfileManager.profile_region("model_class_setup"): - model = model_class(params=params, comm=comm, clone_config=clone_config) + # units + model.units = units + if model.bulk_species is None: + A_bulk = None + Z_bulk = None + else: + A_bulk = model.bulk_species.mass_number + Z_bulk = model.bulk_species.charge_number + model.units.derive_units( + velocity_scale=model.velocity_scale, + A_bulk=A_bulk, + Z_bulk=Z_bulk, + verbose=verbose, + ) + + # domain and fluid background + model.setup_domain_and_equil(domain, equil) + + # feec + model.allocate_feec(grid, derham_opts) + + # equation paramters + model.setup_equation_params(units=model.units, verbose=verbose) + + # allocate variables + model.allocate_variables(verbose=verbose) + model.allocate_helpers() + + # pass info to propagators + model.allocate_propagators() - assert isinstance(model, StruphyModel) + # plasma parameters + model.compute_plasma_params(verbose=verbose) + + if rank < 32: + if rank == 0: + print("") + Barrier() + print(f"Rank {rank}: executing main.run() for model {model_name} ...") + + if size > 32 and rank == 32: + print(f"Ranks > 31: executing main.run() for model {model_name} ...") # store geometry vtk if rank == 0: grids_log = [ - np.linspace(1e-6, 1.0, 32), - np.linspace(0.0, 1.0, 32), - np.linspace(0.0, 1.0, 32), + xp.linspace(1e-6, 1.0, 32), + xp.linspace(0.0, 1.0, 32), + xp.linspace(0.0, 1.0, 32), ] tmp = model.domain(*grids_log) @@ -174,9 +262,9 @@ def main( # time quantities (current time value, value in seconds and index) time_state = {} - time_state["value"] = np.zeros(1, dtype=float) - time_state["value_sec"] = np.zeros(1, dtype=float) - time_state["index"] = np.zeros(1, dtype=int) + time_state["value"] = xp.zeros(1, dtype=float) + time_state["value_sec"] = xp.zeros(1, dtype=float) + time_state["index"] = xp.zeros(1, dtype=int) # add time quantities to data object for saving for key, val in time_state.items(): @@ -185,22 +273,22 @@ def main( data.add_data({key_time: val}) data.add_data({key_time_restart: val}) - time_params = params["time"] + # retrieve time parameters + dt = time_opts.dt + Tend = time_opts.Tend + split_algo = time_opts.split_algo # set initial conditions for all variables - if not restart: - model.initialize_from_params() - - total_steps = str(int(round(time_params["Tend"] / time_params["dt"]))) - - else: + if restart: model.initialize_from_restart(data) time_state["value"][0] = data.file["restart/time/value"][-1] time_state["value_sec"][0] = data.file["restart/time/value_sec"][-1] time_state["index"][0] = data.file["restart/time/index"][-1] - total_steps = str(int(round((time_params["Tend"] - time_state["value"][0]) / time_params["dt"]))) + total_steps = str(int(round((Tend - time_state["value"][0]) / dt))) + else: + total_steps = str(int(round(Tend / dt))) # compute initial scalars and kinetic data, pass time state to all propagators model.update_scalar_quantities() @@ -217,7 +305,6 @@ def main( print("\nINITIAL SCALAR QUANTITIES:") model.print_scalar_quantities() - split_algo = time_params["split_algo"] print(f"\nSTART TIME STEPPING WITH '{split_algo}' SPLITTING:") # time loop @@ -226,8 +313,8 @@ def main( Barrier() # stop time loop? - break_cond_1 = time_state["value"][0] >= time_params["Tend"] - break_cond_2 = run_time_now > runtime + break_cond_1 = time_state["value"][0] >= Tend + break_cond_2 = run_time_now > max_runtime if break_cond_1 or break_cond_2: # save restart data (other data already saved below) @@ -235,6 +322,7 @@ def main( data.file.close() end_simulation = time.time() if rank == 0: + print(f"\nTime steps done: {time_state['index'][0]}") print( "wall-clock time of simulation [sec]: ", end_simulation - start_simulation, @@ -248,24 +336,24 @@ def main( if isinstance(val, Particles): val.do_sort() t1 = time.time() - if rank == 0 and not supress_out: + if rank == 0 and verbose: message = "Particles sorted | wall clock [s]: {0:8.4f} | sorting duration [s]: {1:8.4f}".format( run_time_now * 60, t1 - t0 ) print(message, end="\n") print() + # update time and index (round time to 10 decimals for a clean time grid!) + time_state["value"][0] = round(time_state["value"][0] + dt, 10) + time_state["value_sec"][0] = round(time_state["value_sec"][0] + dt * model.units.t, 10) + time_state["index"][0] += 1 + # perform one time step dt t0 = time.time() with ProfileManager.profile_region("model.integrate"): - model.integrate(time_params["dt"], time_params["split_algo"]) + model.integrate(dt, split_algo) t1 = time.time() - # update time and index (round time to 10 decimals for a clean time grid!) - time_state["value"][0] = round(time_state["value"][0] + time_params["dt"], 10) - time_state["value_sec"][0] = round(time_state["value_sec"][0] + time_params["dt"] * model.units["t"], 10) - time_state["index"][0] += 1 - run_time_now = (time.time() - start_simulation) / 60 # update diagnostics data and save data @@ -275,40 +363,27 @@ def main( model.update_markers_to_be_saved() model.update_distr_functions() - # extract FEM coefficients - for key, val in model.em_fields.items(): - if "params" not in key: - field = val["obj"] - assert isinstance(field, SplineFunction) - # in-place extraction of FEM coefficients from field.vector --> field.vector_stencil! - field.extract_coeffs(update_ghost_regions=False) - - for _, val in model.fluid.items(): - for variable, subval in val.items(): - if "params" not in variable: - field = subval["obj"] - assert isinstance(field, SplineFunction) - # in-place extraction of FEM coefficients from field.vector --> field.vector_stencil! - field.extract_coeffs(update_ghost_regions=False) - - for key, val in model.diagnostics.items(): - if "params" not in key: - field = val["obj"] - assert isinstance(field, SplineFunction) + # extract FEEC coefficients + feec_species = model.field_species | model.fluid_species | model.diagnostic_species + for species, val in feec_species.items(): + assert isinstance(val, Species) + for variable, subval in val.variables.items(): + assert isinstance(subval, FEECVariable) + spline = subval.spline # in-place extraction of FEM coefficients from field.vector --> field.vector_stencil! - field.extract_coeffs(update_ghost_regions=False) + spline.extract_coeffs(update_ghost_regions=False) # save data (everything but restart data) data.save_data(keys=save_keys_all) # print current time and scalar quantities to screen - if rank == 0 and not supress_out: + if rank == 0 and verbose: step = str(time_state["index"][0]).zfill(len(total_steps)) message = "time step: " + step + "/" + str(total_steps) - message += " | " + "time: {0:10.5f}/{1:10.5f}".format(time_state["value"][0], time_params["Tend"]) + message += " | " + "time: {0:10.5f}/{1:10.5f}".format(time_state["value"][0], Tend) message += " | " + "phys. time [s]: {0:12.10f}/{1:12.10f}".format( - time_state["value_sec"][0], time_params["Tend"] * model.units["t"] + time_state["value_sec"][0], Tend * model.units.t ) message += " | " + "wall clock [s]: {0:8.4f} | last step duration [s]: {1:8.4f}".format( run_time_now * 60, t1 - t0 @@ -320,17 +395,443 @@ def main( # =================================================================== - with open(path_out + "/meta.txt", "a") as f: - # f.write('wall-clock time [min]:'.ljust(30) + str((end_simulation - start_simulation)/60.) + '\n') - f.write(f"{rank} {'wall-clock time[min]: '.ljust(30)}{(end_simulation - start_simulation) / 60}\n") + meta["wall-clock time[min]"] = (end_simulation - start_simulation) / 60 Barrier() + if rank == 0: + # save meta-data + dict_to_yaml(meta, os.path.join(path_out, "meta.yml")) print("Struphy run finished.") if clone_config is not None: clone_config.free() +def pproc( + path: str, + *, + step: int = 1, + celldivide: int = 1, + physical: bool = False, + guiding_center: bool = False, + classify: bool = False, + no_vtk: bool = False, + time_trace: bool = False, +): + """Post-processing finished Struphy runs. + + Parameters + ---------- + path : str + Absolute path of simulation output folder to post-process. + + step : int + Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. + + celldivide : int + Grid refinement in evaluation of FEM fields. E.g. celldivide=2 evaluates two points per grid cell. + + physical : bool + Wether to do post-processing into push-forwarded physical (xyz) components of fields. + + guiding_center : bool + Compute guiding-center coordinates (only from Particles6D). + + classify : bool + Classify guiding-center trajectories (passing, trapped or lost). + + no_vtk : bool + whether vtk files creation should be skipped + + time_trace : bool + whether to plot the time trace of each measured region + """ + + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\n*** Start post-processing of {path}:") + + # import parameters + params_in = get_params_of_run(path) + model = params_in.model + domain = params_in.domain + + # create post-processing folder + path_pproc = os.path.join(path, "post_processing") + + try: + os.mkdir(path_pproc) + except: + shutil.rmtree(path_pproc) + os.mkdir(path_pproc) + + if time_trace: + from struphy.post_processing.likwid.plot_time_traces import plot_gantt_chart, plot_time_vs_duration + + path_time_trace = os.path.join(path, "profiling_time_trace.pkl") + plot_time_vs_duration(path_time_trace, output_path=path_pproc) + plot_gantt_chart(path_time_trace, output_path=path_pproc) + return + + # check for fields and kinetic data in hdf5 file that need post processing + file = h5py.File(os.path.join(path, "data/", "data_proc0.hdf5"), "r") + + # save time grid at which post-processing data is created + xp.save(os.path.join(path_pproc, "t_grid.npy"), file["time/value"][::step].copy()) + + if "feec" in file.keys(): + exist_fields = True + else: + exist_fields = False + + if "kinetic" in file.keys(): + exist_kinetic = {"markers": False, "f": False, "n_sph": False} + kinetic_species = [] + kinetic_kinds = [] + for name in file["kinetic"].keys(): + kinetic_species += [name] + kinetic_kinds += [next(iter(model.species[name].variables.values())).space] + + # check for saved markers + if "markers" in file["kinetic"][name]: + exist_kinetic["markers"] = True + # check for saved distribution function + if "f" in file["kinetic"][name]: + exist_kinetic["f"] = True + # check for saved sph density + if "n_sph" in file["kinetic"][name]: + exist_kinetic["n_sph"] = True + else: + exist_kinetic = None + + file.close() + + # field post-processing + if exist_fields: + fields, t_grid = create_femfields(path, params_in=params_in, step=step) + + point_data, grids_log, grids_phy = eval_femfields(params_in, fields, celldivide=[celldivide] * 3) + + if physical: + point_data_phy, grids_log, grids_phy = eval_femfields( + params_in, fields, celldivide=[celldivide] * 3, physical=True + ) + + # directory for field data + path_fields = os.path.join(path_pproc, "fields_data") + + try: + os.mkdir(path_fields) + except: + shutil.rmtree(path_fields) + os.mkdir(path_fields) + + # save data dicts for each field + for species, vars in point_data.items(): + for name, val in vars.items(): + try: + os.mkdir(os.path.join(path_fields, species)) + except: + pass + + with open(os.path.join(path_fields, species, name + "_log.bin"), "wb") as handle: + pickle.dump(val, handle, protocol=pickle.HIGHEST_PROTOCOL) + + if physical: + with open(os.path.join(path_fields, species, name + "_phy.bin"), "wb") as handle: + pickle.dump(point_data_phy[species][name], handle, protocol=pickle.HIGHEST_PROTOCOL) + + # save grids + with open(os.path.join(path_fields, "grids_log.bin"), "wb") as handle: + pickle.dump(grids_log, handle, protocol=pickle.HIGHEST_PROTOCOL) + + with open(os.path.join(path_fields, "grids_phy.bin"), "wb") as handle: + pickle.dump(grids_phy, handle, protocol=pickle.HIGHEST_PROTOCOL) + + # create vtk files + if not no_vtk: + create_vtk(path_fields, t_grid, grids_phy, point_data) + if physical: + create_vtk(path_fields, t_grid, grids_phy, point_data_phy, physical=True) + + # kinetic post-processing + if exist_kinetic is not None: + # directory for kinetic data + path_kinetics = os.path.join(path_pproc, "kinetic_data") + + try: + os.mkdir(path_kinetics) + except: + shutil.rmtree(path_kinetics) + os.mkdir(path_kinetics) + + # kinetic post-processing for each species + for n, species in enumerate(kinetic_species): + # directory for each species + path_kinetics_species = os.path.join(path_kinetics, species) + + try: + os.mkdir(path_kinetics_species) + except: + shutil.rmtree(path_kinetics_species) + os.mkdir(path_kinetics_species) + + # markers + if exist_kinetic["markers"]: + post_process_markers( + path, + path_kinetics_species, + species, + domain, + kinetic_kinds[n], + step, + ) + + if guiding_center: + assert kinetic_kinds[n] == "Particles6D" + orbits_tools.post_process_orbit_guiding_center(path, path_kinetics_species, species) + + if classify: + orbits_tools.post_process_orbit_classification(path_kinetics_species, species) + + # distribution function + if exist_kinetic["f"]: + if kinetic_kinds[n] == "DeltaFParticles6D": + compute_bckgr = True + else: + compute_bckgr = False + + post_process_f( + path, + params_in, + path_kinetics_species, + species, + step, + compute_bckgr=compute_bckgr, + ) + + # sph density + if exist_kinetic["n_sph"]: + post_process_n_sph( + path, + params_in, + path_kinetics_species, + species, + step, + ) + + +class SimData: + """Holds post-processed Struphy data as attributes. + + Parameters + ---------- + path : str + Absolute path of simulation output folder to post-process. + """ + + def __init__(self, path: str): + self.path = path + self._orbits = {} + self._f = {} + self._spline_values = {} + self._n_sph = {} + self.grids_log: list[xp.ndarray] = None + self.grids_phy: list[xp.ndarray] = None + self.t_grid: xp.ndarray = None + + @property + def orbits(self) -> dict[str, xp.ndarray]: + """Keys: species name. Values: 3d arrays indexed by (n, p, a), where 'n' is the time index, 'p' the particle index and 'a' the attribute index.""" + return self._orbits + + @property + def f(self) -> dict[str, dict[str, dict[str, xp.ndarray]]]: + """Keys: species name. Values: dicts of slice names ('e1_v1' etc.) holding dicts of corresponding xp.arrays for plotting.""" + return self._f + + @property + def spline_values(self) -> dict[str, dict[str, xp.ndarray]]: + """Keys: species name. Values: dicts of variable names with values being 3d arrays on the grid.""" + return self._spline_values + + @property + def n_sph(self) -> dict[str, dict[str, dict[str, xp.ndarray]]]: + """Keys: species name. Values: dicts of view names ('view_0' etc.) holding dicts of corresponding xp.arrays for plotting.""" + return self._n_sph + + @property + def Nt(self) -> dict[str, int]: + """Number of available time points (snap shots) for each species.""" + if not hasattr(self, "_Nt"): + self._Nt = {} + for spec, orbs in self.orbits.items(): + self._Nt[spec] = orbs.shape[0] + return self._Nt + + @property + def Np(self) -> dict[str, int]: + """Number of particle orbits for each species.""" + if not hasattr(self, "_Np"): + self._Np = {} + for spec, orbs in self.orbits.items(): + self._Np[spec] = orbs.shape[1] + return self._Np + + @property + def Nattr(self) -> dict[str, int]: + """Number of particle attributes for each species.""" + if not hasattr(self, "_Nattr"): + self._Nattr = {} + for spec, orbs in self.orbits.items(): + self._Nattr[spec] = orbs.shape[2] + return self._Nattr + + +def load_data(path: str) -> SimData: + """Load data generated during post-processing. + + Parameters + ---------- + path : str + Absolute path of simulation output folder to post-process. + """ + + path_pproc = os.path.join(path, "post_processing") + assert os.path.exists(path_pproc), f"Path {path_pproc} does not exist, run 'pproc' first?" + print("\n*** Loading post-processed simulation data:") + print(f"{path = }") + + simdata = SimData(path) + + # load time grid + simdata.t_grid = xp.load(os.path.join(path_pproc, "t_grid.npy")) + + # data paths + path_fields = os.path.join(path_pproc, "fields_data") + path_kinetic = os.path.join(path_pproc, "kinetic_data") + + # load point data + if os.path.exists(path_fields): + # grids + with open(os.path.join(path_fields, "grids_log.bin"), "rb") as f: + simdata.grids_log = pickle.load(f) + with open(os.path.join(path_fields, "grids_phy.bin"), "rb") as f: + simdata.grids_phy = pickle.load(f) + + # species folders + species = next(os.walk(path_fields))[1] + for spec in species: + simdata._spline_values[spec] = {} + # simdata.arrays[spec] = {} + path_spec = os.path.join(path_fields, spec) + wlk = os.walk(path_spec) + files = next(wlk)[2] + print(f"\nFiles in {path_spec}: {files}") + for file in files: + if ".bin" in file: + var = file.split(".")[0] + with open(os.path.join(path_spec, file), "rb") as f: + # try: + simdata._spline_values[spec][var] = pickle.load(f) + # simdata.arrays[spec][var] = pickle.load(f) + + if os.path.exists(path_kinetic): + # species folders + species = next(os.walk(path_kinetic))[1] + print(f"{species = }") + for spec in species: + path_spec = os.path.join(path_kinetic, spec) + wlk = os.walk(path_spec) + sub_folders = next(wlk)[1] + for folder in sub_folders: + path_dat = os.path.join(path_spec, folder) + sub_wlk = os.walk(path_dat) + + if "orbits" in folder: + files = next(sub_wlk)[2] + Nt = len(files) // 2 + n = 0 + for file in files: + # print(f"{file = }") + if ".npy" in file: + step = int(file.split(".")[0].split("_")[-1]) + tmp = xp.load(os.path.join(path_dat, file)) + if n == 0: + simdata._orbits[spec] = xp.zeros((Nt, *tmp.shape), dtype=float) + simdata._orbits[spec][step] = tmp + n += 1 + + elif "distribution_function" in folder: + simdata._f[spec] = {} + slices = next(sub_wlk)[1] + # print(f"{slices = }") + for sli in slices: + simdata._f[spec][sli] = {} + # print(f"{sli = }") + files = next(sub_wlk)[2] + # print(f"{files = }") + for file in files: + name = file.split(".")[0] + tmp = xp.load(os.path.join(path_dat, sli, file)) + # print(f"{name = }") + simdata._f[spec][sli][name] = tmp + + elif "n_sph" in folder: + simdata._n_sph[spec] = {} + slices = next(sub_wlk)[1] + # print(f"{slices = }") + for sli in slices: + simdata._n_sph[spec][sli] = {} + # print(f"{sli = }") + files = next(sub_wlk)[2] + # print(f"{files = }") + for file in files: + name = file.split(".")[0] + tmp = xp.load(os.path.join(path_dat, sli, file)) + # print(f"{name = }") + simdata._n_sph[spec][sli][name] = tmp + + else: + print(f"{folder = }") + raise NotImplementedError + + print("\nThe following data has been loaded:") + print(f"\ngrids:") + print(f"{simdata.t_grid.shape = }") + if simdata.grids_log is not None: + print(f"{simdata.grids_log[0].shape = }") + print(f"{simdata.grids_log[1].shape = }") + print(f"{simdata.grids_log[2].shape = }") + if simdata.grids_phy is not None: + print(f"{simdata.grids_phy[0].shape = }") + print(f"{simdata.grids_phy[1].shape = }") + print(f"{simdata.grids_phy[2].shape = }") + print(f"\nsimdata.spline_values:") + for k, v in simdata.spline_values.items(): + print(f" {k}") + for kk, vv in v.items(): + print(f" {kk}") + print(f"\nsimdata.orbits:") + for k, v in simdata.orbits.items(): + print(f" {k}") + print(f"\nsimdata.f:") + for k, v in simdata.f.items(): + print(f" {k}") + for kk, vv in v.items(): + print(f" {kk}") + for kkk, vvv in vv.items(): + print(f" {kkk}") + print(f"\nsimdata.n_sph:") + for k, v in simdata.n_sph.items(): + print(f" {k}") + for kk, vv in v.items(): + print(f" {kk}") + for kkk, vvv in vv.items(): + print(f" {kkk}") + + return simdata + + if __name__ == "__main__": import argparse import os @@ -346,7 +847,6 @@ def main( # Read struphy state file state = utils.read_state() - o_path = state["o_path"] parser = argparse.ArgumentParser(description="Run an Struphy model.") @@ -367,7 +867,7 @@ def main( "--input", type=str, metavar="FILE", - help="absolute path of parameter file (.yml)", + help="absolute path of parameter file", ) # output (absolute path) @@ -388,9 +888,9 @@ def main( action="store_true", ) - # runtime + # max_runtime parser.add_argument( - "--runtime", + "--max-runtime", type=int, metavar="N", help="maximum wall-clock time of program in minutes (default=300)", @@ -432,13 +932,6 @@ def main( action="store_true", ) - # supress screen output - parser.add_argument( - "--supress-out", - help="supress screen output during time integration", - action="store_true", - ) - parser.add_argument( "--likwid", help="run with Likwid", @@ -472,8 +965,8 @@ def main( config.simulation_label = "" pylikwid_markerinit() with ProfileManager.profile_region("main"): - # Call main - main( + # solve the model + run( args.model, args.input, args.output, @@ -481,7 +974,6 @@ def main( runtime=args.runtime, save_step=args.save_step, verbose=args.verbose, - supress_out=args.supress_out, sort_step=args.sort_step, num_clones=args.nclones, ) diff --git a/src/struphy/models/__init__.py b/src/struphy/models/__init__.py index 22d1dcab8..0153467f6 100644 --- a/src/struphy/models/__init__.py +++ b/src/struphy/models/__init__.py @@ -1,76 +1,74 @@ -from struphy.models.fluid import ( - ColdPlasma, - HasegawaWakatani, - IsothermalEulerSPH, - LinearExtendedMHDuniform, - LinearMHD, - ViscoresistiveDeltafMHD, - ViscoresistiveDeltafMHD_with_q, - ViscoresistiveLinearMHD, - ViscoresistiveLinearMHD_with_q, - ViscoresistiveMHD, - ViscoresistiveMHD_with_p, - ViscoresistiveMHD_with_q, - ViscousEulerSPH, - ViscousFluid, -) -from struphy.models.hybrid import ColdPlasmaVlasov, LinearMHDDriftkineticCC, LinearMHDVlasovCC, LinearMHDVlasovPC -from struphy.models.kinetic import ( - DriftKineticElectrostaticAdiabatic, - LinearVlasovAmpereOneSpecies, - LinearVlasovMaxwellOneSpecies, - VlasovAmpereOneSpecies, - VlasovMaxwellOneSpecies, -) -from struphy.models.toy import ( - DeterministicParticleDiffusion, - GuidingCenter, - Maxwell, - Poisson, - PressureLessSPH, - RandomParticleDiffusion, - ShearAlfven, - TwoFluidQuasiNeutralToy, - VariationalBarotropicFluid, - VariationalCompressibleFluid, - VariationalPressurelessFluid, - Vlasov, -) +# from struphy.models.fluid import ( +# ColdPlasma, +# EulerSPH, +# HasegawaWakatani, +# LinearExtendedMHDuniform, +# LinearMHD, +# ViscoresistiveDeltafMHD, +# ViscoresistiveDeltafMHD_with_q, +# ViscoresistiveLinearMHD, +# ViscoresistiveLinearMHD_with_q, +# ViscoresistiveMHD, +# ViscoresistiveMHD_with_p, +# ViscoresistiveMHD_with_q, +# ViscousFluid, +# ) +# from struphy.models.hybrid import ColdPlasmaVlasov, LinearMHDDriftkineticCC, LinearMHDVlasovCC, LinearMHDVlasovPC +# from struphy.models.kinetic import ( +# DriftKineticElectrostaticAdiabatic, +# LinearVlasovAmpereOneSpecies, +# LinearVlasovMaxwellOneSpecies, +# VlasovAmpereOneSpecies, +# VlasovMaxwellOneSpecies, +# ) +# from struphy.models.toy import ( +# DeterministicParticleDiffusion, +# GuidingCenter, +# Maxwell, +# Poisson, +# PressureLessSPH, +# RandomParticleDiffusion, +# ShearAlfven, +# TwoFluidQuasiNeutralToy, +# VariationalBarotropicFluid, +# VariationalCompressibleFluid, +# VariationalPressurelessFluid, +# Vlasov, +# ) -__all__ = [ - "Maxwell", - "Vlasov", - "GuidingCenter", - "ShearAlfven", - "VariationalPressurelessFluid", - "VariationalBarotropicFluid", - "VariationalCompressibleFluid", - "Poisson", - "DeterministicParticleDiffusion", - "RandomParticleDiffusion", - "PressureLessSPH", - "TwoFluidQuasiNeutralToy", - "LinearMHD", - "LinearExtendedMHDuniform", - "ColdPlasma", - "ViscoresistiveMHD", - "ViscousFluid", - "ViscoresistiveMHD_with_p", - "ViscoresistiveLinearMHD", - "ViscoresistiveDeltafMHD", - "ViscoresistiveMHD_with_q", - "ViscoresistiveLinearMHD_with_q", - "ViscoresistiveDeltafMHD_with_q", - "IsothermalEulerSPH", - "ViscousEulerSPH", - "HasegawaWakatani", - "LinearMHDVlasovCC", - "LinearMHDVlasovPC", - "LinearMHDDriftkineticCC", - "ColdPlasmaVlasov", - "VlasovAmpereOneSpecies", - "VlasovMaxwellOneSpecies", - "LinearVlasovAmpereOneSpecies", - "LinearVlasovMaxwellOneSpecies", - "DriftKineticElectrostaticAdiabatic", -] +# __all__ = [ +# "Maxwell", +# "Vlasov", +# "GuidingCenter", +# "ShearAlfven", +# "VariationalPressurelessFluid", +# "VariationalBarotropicFluid", +# "VariationalCompressibleFluid", +# "Poisson", +# "DeterministicParticleDiffusion", +# "RandomParticleDiffusion", +# "PressureLessSPH", +# "TwoFluidQuasiNeutralToy", +# "LinearMHD", +# "LinearExtendedMHDuniform", +# "ColdPlasma", +# "ViscoresistiveMHD", +# "ViscousFluid", +# "ViscoresistiveMHD_with_p", +# "ViscoresistiveLinearMHD", +# "ViscoresistiveDeltafMHD", +# "ViscoresistiveMHD_with_q", +# "ViscoresistiveLinearMHD_with_q", +# "ViscoresistiveDeltafMHD_with_q", +# "EulerSPH", +# "HasegawaWakatani", +# "LinearMHDVlasovCC", +# "LinearMHDVlasovPC", +# "LinearMHDDriftkineticCC", +# "ColdPlasmaVlasov", +# "VlasovAmpereOneSpecies", +# "VlasovMaxwellOneSpecies", +# "LinearVlasovAmpereOneSpecies", +# "LinearVlasovMaxwellOneSpecies", +# "DriftKineticElectrostaticAdiabatic", +# ] diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index cf7467585..d18942154 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -1,115 +1,110 @@ import inspect import operator +import os from abc import ABCMeta, abstractmethod from functools import reduce +from textwrap import indent +import cunumpy as xp import yaml +from line_profiler import profile +from psydac.ddm.mpi import MockMPI from psydac.ddm.mpi import mpi as MPI from psydac.linalg.stencil import StencilVector +import struphy from struphy.feec.basis_projection_ops import BasisProjectionOperators from struphy.feec.mass import WeightedMassOperators from struphy.feec.psydac_derham import SplineFunction from struphy.fields_background.base import FluidEquilibrium, FluidEquilibriumWithB, MHDequilibrium +from struphy.fields_background.equils import HomogenSlab from struphy.fields_background.projected_equils import ( ProjectedFluidEquilibrium, ProjectedFluidEquilibriumWithB, ProjectedMHDequilibrium, ) -from struphy.io.setup import setup_derham, setup_domain_and_equil +from struphy.geometry.base import Domain +from struphy.geometry.domains import Cuboid +from struphy.io.options import BaseUnits, DerhamOptions, Time, Units +from struphy.io.output_handling import DataContainer +from struphy.io.setup import descend_options_dict, setup_derham +from struphy.kinetic_background import maxwellians +from struphy.models.species import DiagnosticSpecies, FieldSpecies, FluidSpecies, ParticleSpecies, Species +from struphy.models.variables import FEECVariable, PICVariable, SPHVariable +from struphy.pic import particles +from struphy.pic.base import Particles from struphy.profiling.profiling import ProfileManager from struphy.propagators.base import Propagator -from struphy.utils.arrays import xp as np +from struphy.topology.grids import TensorProductGrid from struphy.utils.clone_config import CloneConfig -from struphy.utils.utils import dict_to_yaml +from struphy.utils.utils import dict_to_yaml, read_state class StruphyModel(metaclass=ABCMeta): """ Base class for all Struphy models. - Parameters - ---------- - params : dict - Simulation parameters, see from :ref:`params_yml`. - - comm : mpi4py.MPI.Intracomm - MPI communicator for parallel runs. - - clone_config: struphy.utils.CloneConfig - Contains the # TODO - Note ---- All Struphy models are subclasses of ``StruphyModel`` and should be added to ``struphy/models/`` in one of the modules ``fluid.py``, ``kinetic.py``, ``hybrid.py`` or ``toy.py``. """ - def __init__( - self, - params: dict, - comm: MPI.Intracomm = None, - clone_config: CloneConfig = None, - ): - assert "em_fields" in self.species() - assert "fluid" in self.species() - assert "kinetic" in self.species() - - assert "em_fields" in self.options() - assert "fluid" in self.options() - assert "kinetic" in self.options() - - if params is None: - params = self.generate_default_parameter_file( - save=False, - prompt=False, - ) + ## abstract methods - self._comm_world = comm - self._clone_config = clone_config + @abstractmethod + class Propagators: + pass - self._params = params + @abstractmethod + def __init__(self): + """Light-weight init of model.""" - # get rank and size - if self.comm_world is None: - self._rank_world = 0 - else: - self._rank_world = self.comm_world.Get_rank() + @property + @abstractmethod + def bulk_species() -> Species: + """Bulk species of the plasma. Must be an attribute of species_static().""" - # initialize model variable dictionaries - self._init_variable_dicts() + @property + @abstractmethod + def velocity_scale() -> str: + """Velocity unit scale of the model. + Must be one of "alfvén", "cyclotron", "light" or "thermal".""" - # compute model units - self._units, self._equation_params = self.model_units( - self.params, - verbose=self.verbose, - comm=self.comm_world, - ) + @abstractmethod + def allocate_helpers(self): + """Allocate helper arrays that are needed during simulation.""" - # create domain, equilibrium - self._domain, self._equil = setup_domain_and_equil( - params, - units=self.units, - ) + @abstractmethod + def update_scalar_quantities(self): + """Specify an update rule for each item in ``scalar_quantities`` using :meth:`update_scalar`.""" - if self.rank_world == 0 and self.verbose: - print("\nTIME:") - print( - f"time step:".ljust(25), - "{0} ({1:4.2e} s)".format( - params["time"]["dt"], - params["time"]["dt"] * self.units["t"], - ), - ) - print( - f"final time:".ljust(25), - "{0} ({1:4.2e} s)".format( - params["time"]["Tend"], - params["time"]["Tend"] * self.units["t"], - ), - ) - print(f"splitting algo:".ljust(25), params["time"]["split_algo"]) + ## setup methods + + def setup_equation_params(self, units: Units, verbose=False): + """Set euqation parameters for each fluid and kinetic species.""" + for _, species in self.fluid_species.items(): + assert isinstance(species, FluidSpecies) + species.setup_equation_params(units=units, verbose=verbose) + + for _, species in self.particle_species.items(): + assert isinstance(species, ParticleSpecies) + species.setup_equation_params(units=units, verbose=verbose) + + def setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium): + """If a numerical equilibirum is used, the domain is taken from this equilibirum.""" + if equil is not None: + self._equil = equil + if "Numerical" in self.equil.__class__.__name__: + self._domain = self.equil.domain + else: + self._domain = domain + self._equil.domain = domain + else: + self._domain = domain + self._equil = None + if MPI.COMM_WORLD.Get_rank() == 0 and self.verbose: print("\nDOMAIN:") print(f"type:".ljust(25), self.domain.__class__.__name__) for key, val in self.domain.params.items(): @@ -117,39 +112,102 @@ def __init__( print((key + ":").ljust(25), val) print("\nFLUID BACKGROUND:") - if "fluid_background" in params: + if self.equil is not None: print("type:".ljust(25), self.equil.__class__.__name__) for key, val in self.equil.params.items(): print((key + ":").ljust(25), val) else: print("None.") - # create discrete derham sequence - if "grid" in params: - dims_mask = params["grid"]["dims_mask"] - if dims_mask is None: - dims_mask = [True] * 3 + ## species - if clone_config is None: - derham_comm = self.comm_world - else: - derham_comm = clone_config.sub_comm + @property + def field_species(self) -> dict: + if not hasattr(self, "_field_species"): + self._field_species = {} + for k, v in self.__dict__.items(): + if isinstance(v, FieldSpecies): + self._field_species[k] = v + return self._field_species + + @property + def fluid_species(self) -> dict: + if not hasattr(self, "_fluid_species"): + self._fluid_species = {} + for k, v in self.__dict__.items(): + if isinstance(v, FluidSpecies): + self._fluid_species[k] = v + return self._fluid_species + + @property + def particle_species(self) -> dict: + if not hasattr(self, "_particle_species"): + self._particle_species = {} + for k, v in self.__dict__.items(): + if isinstance(v, ParticleSpecies): + self._particle_species[k] = v + return self._particle_species + + @property + def diagnostic_species(self) -> dict: + if not hasattr(self, "_diagnostic_species"): + self._diagnostic_species = {} + for k, v in self.__dict__.items(): + if isinstance(v, DiagnosticSpecies): + self._diagnostic_species[k] = v + return self._diagnostic_species + + @property + def species(self): + if not hasattr(self, "_species"): + self._species = self.field_species | self.fluid_species | self.particle_species + return self._species + + ## allocate methods + def allocate_feec(self, grid: TensorProductGrid, derham_opts: DerhamOptions): + # create discrete derham sequence + if self.clone_config is None: + derham_comm = MPI.COMM_WORLD + else: + derham_comm = self.clone_config.sub_comm + + if grid is None or derham_opts is None: + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\n{grid = }, {derham_opts = }: no Derham object set up.") + self._derham = None + else: self._derham = setup_derham( - params["grid"], + grid, + derham_opts, comm=derham_comm, domain=self.domain, - mpi_dims_mask=dims_mask, verbose=self.verbose, ) + + # create weighted mass and basis operators + if self.derham is None: + self._mass_ops = None + self._basis_ops = None else: - self._derham = None - print("\nDERHAM:\nMeshless simulation - no Derham complex set up.") + self._mass_ops = WeightedMassOperators( + self.derham, + self.domain, + verbose=self.verbose, + eq_mhd=self.equil, + ) - self._projected_equil = None - self._mass_ops = None - if self.derham is not None: - # create projected equilibrium + self._basis_ops = BasisProjectionOperators( + self.derham, + self.domain, + verbose=self.verbose, + eq_mhd=self.equil, + ) + + # create projected equilibrium + if self.derham is None: + self._projected_equil = None + else: if isinstance(self.equil, MHDequilibrium): self._projected_equil = ProjectedMHDequilibrium( self.equil, @@ -165,99 +223,32 @@ def __init__( self.equil, self.derham, ) + else: + self._projected_equil = None - # create weighted mass operators - self._mass_ops = WeightedMassOperators( - self.derham, - self.domain, - verbose=self.verbose, - eq_mhd=self.equil, - ) - - # allocate memory for variables - self._pointer = {} - self._allocate_variables() - - # store plasma parameters - if self.rank_world == 0: - self._pparams = self._compute_plasma_params(verbose=self.verbose) - else: - self._pparams = self._compute_plasma_params(verbose=False) - - # if self.rank_world == 0: - # self._show_chosen_options() - + def allocate_propagators(self): # set propagators base class attributes (then available to all propagators) Propagator.derham = self.derham Propagator.domain = self.domain if self.derham is not None: Propagator.mass_ops = self.mass_ops - Propagator.basis_ops = BasisProjectionOperators( - self.derham, - self.domain, - verbose=self.verbose, - eq_mhd=self.equil, - ) + Propagator.basis_ops = self.basis_ops Propagator.projected_equil = self.projected_equil - # create dummy lists/dicts to be filled by the sub-class - self._propagators = [] - self._kwargs = {} - self._scalar_quantities = {} - - return params - - @staticmethod - @abstractmethod - def species(): - """Species dictionary of the form {'em_fields': {}, 'fluid': {}, 'kinetic': {}}. - - The dynamical fields and kinetic species of the model. - - Keys of the three sub-dicts are either: - - a) the electromagnetic field/potential names (b_field, e_field) - b) the fluid species names (e.g. mhd) - c) the names of the kinetic species (e.g. electrons, energetic_ions) - - Corresponding values are: - - a) a space ID ("H1", "Hcurl", "Hdiv", "L2" or "H1vec"), - b) a dict with key=variable_name (e.g. n, U, p, ...) and value=space ID ("H1", "Hcurl", "Hdiv", "L2" or "H1vec"), - c) the type of particles ("Particles6D", "Particles5D", ...).""" - pass - - @staticmethod - @abstractmethod - def bulk_species(): - """Name of the bulk species of the plasma. Must be a key of self.fluid or self.kinetic, or None.""" - pass - - @staticmethod - @abstractmethod - def velocity_scale(): - """String that sets the velocity scale unit of the model. - Must be one of "alfvén", "cyclotron" or "light".""" - pass + assert len(self.prop_list) > 0, "No propagators in this model, check the model class." + for prop in self.prop_list: + assert isinstance(prop, Propagator) + prop.allocate() + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nAllocated propagator '{prop.__class__.__name__}'.") @staticmethod def diagnostics_dct(): """Diagnostics dictionary. Model specific variables (FemField) which is going to be saved during the simulation. """ - pass - @staticmethod - @abstractmethod - def propagators_dct(cls): - """Dictionary holding the propagators of the model in the sequence they should be called. - Keys are the propagator classes and values are lists holding variable names (str) updated by the propagator.""" - pass - - @abstractmethod - def update_scalar_quantities(self): - """Specify an update rule for each item in ``scalar_quantities`` using :meth:`update_scalar`.""" - pass + ## basic properties @property def params(self): @@ -274,48 +265,15 @@ def equation_params(self): """Parameters appearing in model equation due to Struphy normalization.""" return self._equation_params - @property - def comm_world(self): - """MPI_COMM_WORLD communicator.""" - return self._comm_world - - @property - def rank_world(self): - """Global rank.""" - return self._rank_world - @property def clone_config(self): """Config in case domain clones are used.""" return self._clone_config - @property - def pointer(self): - """Dictionary pointing to the data structures of the species (Stencil/BlockVector or "Particle" class). - - The keys are the keys from the "species" property. - In case of a fluid species, the keys are like "species_variable".""" - return self._pointer - - @property - def em_fields(self): - """Dictionary of electromagnetic field/potential variables.""" - return self._em_fields - - @property - def fluid(self): - """Dictionary of fluid species.""" - return self._fluid - - @property - def kinetic(self): - """Dictionary of kinetic species.""" - return self._kinetic - - @property - def diagnostics(self): - """Dictionary of diagnostics.""" - return self._diagnostics + @clone_config.setter + def clone_config(self, new): + assert isinstance(new, CloneConfig) or new is None + self._clone_config = new @property def domain(self): @@ -338,15 +296,32 @@ def projected_equil(self): return self._projected_equil @property - def units(self): + def units(self) -> Units: """All Struphy units.""" return self._units + @units.setter + def units(self, new): + assert isinstance(new, Units) + self._units = new + @property def mass_ops(self): """WeighteMassOperators object, see :ref:`mass_ops`.""" return self._mass_ops + @property + def basis_ops(self): + """Basis projection operators.""" + return self._basis_ops + + @property + def prop_list(self): + """List of Propagator objects.""" + if not hasattr(self, "_prop_list"): + self._prop_list = list(self.propagators.__dict__.values()) + return self._prop_list + @property def prop_fields(self): """Module :mod:`struphy.propagators.propagators_fields`.""" @@ -362,11 +337,6 @@ def prop_markers(self): """Module :mod:`struphy.propagators.propagators_markers`.""" return self._prop_markers - @property - def propagators(self): - """A list of propagator instances for the model.""" - return self._propagators - @property def kwargs(self): """Dictionary holding the keyword arguments for each propagator specified in :attr:`~propagators_cls`. @@ -376,6 +346,8 @@ def kwargs(self): @property def scalar_quantities(self): """A dictionary of scalar quantities to be saved during the simulation.""" + if not hasattr(self, "_scalar_quantities"): + self._scalar_quantities = {} return self._scalar_quantities @property @@ -480,7 +452,7 @@ def setInDict(dataDict, mapList, value): assert key is not None, "Must provide key if option is not a class." setInDict(dct, species + ["options"] + key, option) - def add_scalar(self, name, species=None, compute=None, summands=None): + def add_scalar(self, name: str, variable: PICVariable | SPHVariable = None, compute=None, summands=None): """ Add a scalar to be saved during the simulation. @@ -488,8 +460,8 @@ def add_scalar(self, name, species=None, compute=None, summands=None): ---------- name : str Dictionary key for the scalar. - species : str, optional - The species associated with the scalar. Required if compute is 'from_particles'. + variable : PICVariable | SPHVariable, optional + The variable associated with the scalar. Required if compute is 'from_particles'. compute : str, optional Type of scalar, determines the compute operations. Options: 'from_particles' or 'from_field'. Default is None. @@ -500,14 +472,14 @@ def add_scalar(self, name, species=None, compute=None, summands=None): assert isinstance(name, str), "name must be a string" if compute == "from_particles": - assert isinstance( - species, - str, - ), "species must be a string when compute is 'from_particles'" + assert isinstance(variable, (PICVariable, SPHVariable)), f"Variable is needed when {compute = }" + + if not hasattr(self, "_scalar_quantities"): + self._scalar_quantities = {} self._scalar_quantities[name] = { - "value": np.empty(1, dtype=float), - "species": species, + "value": xp.empty(1, dtype=float), + "variable": variable, "compute": compute, "summands": summands, } @@ -527,7 +499,7 @@ def update_scalar(self, name, value=None): # Ensure the name is a string assert isinstance(name, str) - species = self._scalar_quantities[name]["species"] + variable: PICVariable | SPHVariable = self._scalar_quantities[name]["variable"] summands = self._scalar_quantities[name]["summands"] compute = self._scalar_quantities[name]["compute"] @@ -552,11 +524,11 @@ def update_scalar(self, name, value=None): assert isinstance(value, float) # Create a numpy array to hold the scalar value - value_array = np.array([value], dtype=np.float64) + value_array = xp.array([value], dtype=xp.float64) # Perform MPI operations based on the compute flags - if "sum_world" in compute_operations and self.comm_world is not None: - self.comm_world.Allreduce( + if "sum_world" in compute_operations and not isinstance(MPI, MockMPI): + MPI.COMM_WORLD.Allreduce( MPI.IN_PLACE, value_array, op=MPI.SUM, @@ -590,7 +562,7 @@ def update_scalar(self, name, value=None): if "divide_n_mks" in compute_operations: # Initialize the total number of markers - n_mks_tot = np.array([self.pointer[species].Np]) + n_mks_tot = xp.array([variable.particles.Np]) value_array /= n_mks_tot # Update the scalar value @@ -612,43 +584,90 @@ def add_time_state(self, time_state): """ assert time_state.size == 1 self._time_state = time_state - for prop in self.propagators: - prop.add_time_state(time_state) - - def init_propagators(self): - """Initialize the propagator objects specified in :attr:`~propagators_cls`.""" - if self.rank_world == 0 and self.verbose: - print("\nPROPAGATORS:") - for (prop, variables), (prop2, kwargs_i) in zip(self.propagators_dct().items(), self.kwargs.items()): - assert prop == prop2, ( - f'Propagators {prop} from "self.propagators_dct()" and {prop2} from "self.kwargs" must be identical !!' - ) + for _, prop in self.propagators.__dict__.items(): + if isinstance(prop, Propagator): + prop.add_time_state(time_state) - if kwargs_i is None: - if self.rank_world == 0: - print(f'\n-> Propagator "{prop.__name__}" will not be used.') - continue - else: - if self.rank_world == 0 and self.verbose: - print(f'\n-> Initializing propagator "{prop.__name__}"') - print(f"-> for variables {variables}") - print(f"-> with the following parameters:") - for k, v in kwargs_i.items(): - if isinstance(v, StencilVector): - print(f"{k}: {repr(v)}") - else: - print(f"{k}: {v}") + @profile + def allocate_variables(self, verbose: bool = False): + """ + Allocate memory for model variables and set initial conditions. + """ + # allocate memory for FE coeffs of electromagnetic fields/potentials + if self.field_species: + for species, spec in self.field_species.items(): + assert isinstance(spec, FieldSpecies) + for k, v in spec.variables.items(): + assert isinstance(v, FEECVariable) + v.allocate( + derham=self.derham, + domain=self.domain, + equil=self.equil, + ) - prop_instance = prop( - *[self.pointer[var] for var in variables], - **kwargs_i, - ) - assert isinstance(prop_instance, Propagator) - self._propagators += [prop_instance] + # allocate memory for FE coeffs of fluid variables + if self.fluid_species: + for species, spec in self.fluid_species.items(): + assert isinstance(spec, FluidSpecies) + for k, v in spec.variables.items(): + assert isinstance(v, FEECVariable) + v.allocate( + derham=self.derham, + domain=self.domain, + equil=self.equil, + ) + + # allocate memory for marker arrays of kinetic variables + if self.particle_species: + for species, spec in self.particle_species.items(): + assert isinstance(spec, ParticleSpecies) + for k, v in spec.variables.items(): + if isinstance(v, PICVariable): + v.allocate( + clone_config=self.clone_config, + derham=self.derham, + domain=self.domain, + equil=self.equil, + projected_equil=self.projected_equil, + verbose=verbose, + ) + if isinstance(v, SPHVariable): + v.allocate( + derham=self.derham, + domain=self.domain, + equil=self.equil, + projected_equil=self.projected_equil, + verbose=verbose, + ) - if self.rank_world == 0 and self.verbose: - print("\nInitialization of propagators complete.") + # allocate memory for FE coeffs of fluid variables + if self.diagnostic_species: + for species, spec in self.diagnostic_species.items(): + assert isinstance(spec, DiagnosticSpecies) + for k, v in spec.variables.items(): + assert isinstance(v, FEECVariable) + v.allocate( + derham=self.derham, + domain=self.domain, + equil=self.equil, + ) + # TODO: allocate memory for FE coeffs of diagnostics + # if self.params.diagnostic_fields is not None: + # for key, val in self.diagnostics.items(): + # if "params" in key: + # continue + # else: + # val["obj"] = self.derham.create_spline_function( + # key, + # val["space"], + # bckgr_params=None, + # pert_params=None, + # ) + + # self._pointer[key] = val["obj"].vector + + @profile def integrate(self, dt, split_algo="LieTrotter"): """ Advance the model by a time step ``dt`` by sequentially calling its Propagators. @@ -664,27 +683,27 @@ def integrate(self, dt, split_algo="LieTrotter"): # first order in time if split_algo == "LieTrotter": - for propagator in self.propagators: - prop_name = type(propagator).__name__ + for propagator in self.prop_list: + prop_name = propagator.__class__.__name__ with ProfileManager.profile_region(prop_name): propagator(dt) # second order in time elif split_algo == "Strang": - assert len(self.propagators) > 1 + assert len(self.prop_list) > 1 - for propagator in self.propagators[:-1]: + for propagator in self.prop_list[:-1]: prop_name = type(propagator).__name__ with ProfileManager.profile_region(prop_name): propagator(dt / 2) - propagator = self.propagators[-1] + propagator = self.prop_list[-1] prop_name = type(propagator).__name__ with ProfileManager.profile_region(prop_name): propagator(dt) - for propagator in self.propagators[:-1][::-1]: + for propagator in self.prop_list[:-1][::-1]: prop_name = type(propagator).__name__ with ProfileManager.profile_region(prop_name): propagator(dt / 2) @@ -694,90 +713,76 @@ def integrate(self, dt, split_algo="LieTrotter"): f"Splitting scheme {split_algo} not available.", ) + @profile def update_markers_to_be_saved(self): """ Writes markers with IDs that are supposed to be saved into corresponding array. """ - from struphy.pic.base import Particles - - for val in self.kinetic.values(): - obj = val["obj"] - assert isinstance(obj, Particles) - - # allocate array for saving markers if not present - if not hasattr(self, "_n_markers_saved"): - n_markers = val["params"]["save_data"].get("n_markers", 0) - - if isinstance(n_markers, float): - if n_markers > 1.0: - self._n_markers_saved = int(n_markers) - else: - self._n_markers_saved = int(obj.n_mks_global * n_markers) - else: - self._n_markers_saved = n_markers - - assert self._n_markers_saved <= obj.Np, ( - f"The number of markers for which data should be stored (={self._n_markers_saved}) murst be <= than the total number of markers (={obj.Np})" - ) - if self._n_markers_saved > 0: - val["kinetic_data"]["markers"] = np.zeros( - (self._n_markers_saved, obj.markers.shape[1]), - dtype=float, - ) + for name, species in self.particle_species.items(): + assert isinstance(species, ParticleSpecies) + assert len(species.variables) == 1, f"More than 1 variable per kinetic species is not allowed." + for _, var in species.variables.items(): + assert isinstance(var, PICVariable | SPHVariable) + obj = var.particles + assert isinstance(obj, Particles) - if self._n_markers_saved > 0: - markers_on_proc = np.logical_and( + if var.n_to_save > 0: + markers_on_proc = xp.logical_and( obj.markers[:, -1] >= 0.0, - obj.markers[:, -1] < self._n_markers_saved, + obj.markers[:, -1] < var.n_to_save, ) - n_markers_on_proc = np.count_nonzero(markers_on_proc) - val["kinetic_data"]["markers"][:] = -1.0 - val["kinetic_data"]["markers"][:n_markers_on_proc] = obj.markers[markers_on_proc] + n_markers_on_proc = xp.count_nonzero(markers_on_proc) + var.saved_markers[:] = -1.0 + var.saved_markers[:n_markers_on_proc] = obj.markers[markers_on_proc] + @profile def update_distr_functions(self): """ Writes distribution functions slices that are supposed to be saved into corresponding array. """ - from struphy.pic.base import Particles - dim_to_int = {"e1": 0, "e2": 1, "e3": 2, "v1": 3, "v2": 4, "v3": 5} - for val in self.kinetic.values(): - obj = val["obj"] - assert isinstance(obj, Particles) + for name, species in self.particle_species.items(): + assert isinstance(species, ParticleSpecies) + assert len(species.variables) == 1, f"More than 1 variable per kinetic species is not allowed." + for _, var in species.variables.items(): + assert isinstance(var, PICVariable | SPHVariable) + obj = var.particles + assert isinstance(obj, Particles) - if obj.n_cols_diagnostics > 0: - for i in range(obj.n_cols_diagnostics): - str_dn = f"d{i + 1}" - dim_to_int[str_dn] = 3 + obj.vdim + 3 + i + if obj.n_cols_diagnostics > 0: + for i in range(obj.n_cols_diagnostics): + str_dn = f"d{i + 1}" + dim_to_int[str_dn] = 3 + obj.vdim + 3 + i - if "f" in val["params"]["save_data"]: - for slice_i, edges in val["bin_edges"].items(): - comps = slice_i.split("_") + for bin_plot in species.binning_plots: + comps = bin_plot.slice.split("_") components = [False] * (3 + obj.vdim + 3 + obj.n_cols_diagnostics) for comp in comps: components[dim_to_int[comp]] = True - f_slice, df_slice = obj.binning(components, edges) + edges = bin_plot.bin_edges + divide_by_jac = bin_plot.divide_by_jac + f_slice, df_slice = obj.binning(components, edges, divide_by_jac=divide_by_jac) - val["kinetic_data"]["f"][slice_i][:] = f_slice - val["kinetic_data"]["df"][slice_i][:] = df_slice + bin_plot.f[:] = f_slice + bin_plot.df[:] = df_slice - if "n_sph" in val["params"]["save_data"]: - h1 = 1 / obj.boxes_per_dim[0] - h2 = 1 / obj.boxes_per_dim[1] - h3 = 1 / obj.boxes_per_dim[2] + for kd_plot in species.kernel_density_plots: + h1 = 1 / obj.boxes_per_dim[0] + h2 = 1 / obj.boxes_per_dim[1] + h3 = 1 / obj.boxes_per_dim[2] - ndim = np.count_nonzero([d > 1 for d in obj.boxes_per_dim]) - if ndim == 0: - kernel_type = "gaussian_3d" - else: - kernel_type = "gaussian_" + str(ndim) + "d" + ndim = xp.count_nonzero([d > 1 for d in obj.boxes_per_dim]) + if ndim == 0: + kernel_type = "gaussian_3d" + else: + kernel_type = "gaussian_" + str(ndim) + "d" - for i, pts in enumerate(val["plot_pts"]): + pts = kd_plot.plot_pts n_sph = obj.eval_density( *pts, h1=h1, @@ -786,7 +791,7 @@ def update_distr_functions(self): kernel_type=kernel_type, fast=True, ) - val["kinetic_data"]["n_sph"][i][:] = n_sph + kd_plot.n_sph[:] = n_sph def print_scalar_quantities(self): """ @@ -795,182 +800,176 @@ def print_scalar_quantities(self): sq_str = "" for key, scalar_dict in self._scalar_quantities.items(): val = scalar_dict["value"] - assert not np.isnan(val[0]), f"Scalar {key} is {val[0]}." + assert not xp.isnan(val[0]), f"Scalar {key} is {val[0]}." sq_str += key + ": {:14.11f}".format(val[0]) + " " print(sq_str) - def initialize_from_params(self): - """ - Set initial conditions for FE coefficients (electromagnetic and fluid) - and markers according to parameter file. - """ - - from struphy.feec.psydac_derham import Derham - from struphy.pic.base import Particles - - if self.rank_world == 0 and self.verbose: - print("\nINITIAL CONDITIONS:") - - # initialize em fields - if len(self.em_fields) > 0: - with ProfileManager.profile_region("initialize_em_fields"): - for key, val in self.em_fields.items(): - if "params" in key: - continue - else: - obj = val["obj"] - assert isinstance(obj, SplineFunction) - - obj.initialize_coeffs( - domain=self.domain, - bckgr_obj=self.equil, - ) - - if self.rank_world == 0 and self.verbose: - print(f'\nEM field "{key}" was initialized with:') - - _params = self.em_fields["params"] - - if "background" in _params: - if key in _params["background"]: - bckgr_types = _params["background"][key] - if bckgr_types is None: - pass - else: - print("background:") - for _type, _bp in bckgr_types.items(): - print(" " * 4 + _type, ":") - for _pname, _pval in _bp.items(): - print((" " * 8 + _pname + ":").ljust(25), _pval) - else: - print("No background.") - else: - print("No background.") - - if "perturbation" in _params: - if key in _params["perturbation"]: - pert_types = _params["perturbation"][key] - if pert_types is None: - pass - else: - print("perturbation:") - for _type, _pp in pert_types.items(): - print(" " * 4 + _type, ":") - for _pname, _pval in _pp.items(): - print((" " * 8 + _pname + ":").ljust(25), _pval) - else: - print("No perturbation.") - else: - print("No perturbation.") - - if len(self.fluid) > 0: - with ProfileManager.profile_region("initialize_fluids"): - for species, val in self.fluid.items(): - for variable, subval in val.items(): - if "params" in variable: - continue - else: - obj = subval["obj"] - assert isinstance(obj, SplineFunction) - obj.initialize_coeffs( - domain=self.domain, - bckgr_obj=self.equil, - species=species, - ) - - if self.rank_world == 0 and self.verbose: - print( - f'\nFluid species "{species}" was initialized with:', - ) - - _params = val["params"] - - if "background" in _params: - for variable in val: - if "params" in variable: - continue - if variable in _params["background"]: - bckgr_types = _params["background"][variable] - if bckgr_types is None: - pass - else: - print(f"{variable} background:") - for _type, _bp in bckgr_types.items(): - print(" " * 4 + _type, ":") - for _pname, _pval in _bp.items(): - print((" " * 8 + _pname + ":").ljust(25), _pval) - else: - print(f"{variable}: no background.") - else: - print("No background.") - - if "perturbation" in _params: - for variable in val: - if "params" in variable: - continue - if variable in _params["perturbation"]: - pert_types = _params["perturbation"][variable] - if pert_types is None: - pass - else: - print(f"{variable} perturbation:") - for _type, _pp in pert_types.items(): - print(" " * 4 + _type, ":") - for _pname, _pval in _pp.items(): - print((" " * 8 + _pname + ":").ljust(25), _pval) - else: - print(f"{variable}: no perturbation.") - else: - print("No perturbation.") - - # initialize particles - if len(self.kinetic) > 0: - with ProfileManager.profile_region("initialize_particles"): - for species, val in self.kinetic.items(): - obj = val["obj"] - assert isinstance(obj, Particles) - - if self.rank_world == 0 and self.verbose: - _params = val["params"] - assert "background" in _params, "Kinetic species must have background." - - bckgr_types = _params["background"] - print( - f'\nKinetic species "{species}" was initialized with:', - ) - for _type, _bp in bckgr_types.items(): - print(_type, ":") - for _pname, _pval in _bp.items(): - print((" " * 4 + _pname + ":").ljust(25), _pval) - - if "perturbation" in _params: - for variable, pert_types in _params["perturbation"].items(): - if pert_types is None: - pass - else: - print(f"{variable} perturbation:") - for _type, _pp in pert_types.items(): - print(" " * 4 + _type, ":") - for _pname, _pval in _pp.items(): - print((" " * 8 + _pname + ":").ljust(25), _pval) - else: - print("No perturbation.") - - obj.draw_markers(sort=True, verbose=self.verbose) - if self.comm_world is not None: - obj.mpi_sort_markers(do_test=True) - - if not val["params"]["markers"]["loading"] == "restart": - if obj.coords == "vpara_mu": - obj.save_magnetic_moment() - - if val["space"] != "ParticlesSPH" and obj.f0.coords == "constants_of_motion": - obj.save_constants_of_motion() - - obj.initialize_weights( - reject_weights=obj.weights_params["reject_weights"], - threshold=obj.weights_params["threshold"], - ) + # def initialize_from_params(self): + # """ + # Set initial conditions for FE coefficients (electromagnetic and fluid) + # and markers according to parameter file. + # """ + + # # initialize em fields + # if self.field_species: + # with ProfileManager.profile_region("initialize_em_fields"): + # for key, val in self.em_fields.items(): + # if "params" in key: + # continue + # else: + # obj = val["obj"] + # assert isinstance(obj, SplineFunction) + + # obj.initialize_coeffs( + # domain=self.domain, + # bckgr_obj=self.equil, + # ) + + # if self.rank_world == 0 and self.verbose: + # print(f'\nEM field "{key}" was initialized with:') + + # _params = self.em_fields["params"] + + # if "background" in _params: + # if key in _params["background"]: + # bckgr_types = _params["background"][key] + # if bckgr_types is None: + # pass + # else: + # print("background:") + # for _type, _bp in bckgr_types.items(): + # print(" " * 4 + _type, ":") + # for _pname, _pval in _bp.items(): + # print((" " * 8 + _pname + ":").ljust(25), _pval) + # else: + # print("No background.") + # else: + # print("No background.") + + # if "perturbation" in _params: + # if key in _params["perturbation"]: + # pert_types = _params["perturbation"][key] + # if pert_types is None: + # pass + # else: + # print("perturbation:") + # for _type, _pp in pert_types.items(): + # print(" " * 4 + _type, ":") + # for _pname, _pval in _pp.items(): + # print((" " * 8 + _pname + ":").ljust(25), _pval) + # else: + # print("No perturbation.") + # else: + # print("No perturbation.") + + # if len(self.fluid) > 0: + # with ProfileManager.profile_region("initialize_fluids"): + # for species, val in self.fluid.items(): + # for variable, subval in val.items(): + # if "params" in variable: + # continue + # else: + # obj = subval["obj"] + # assert isinstance(obj, SplineFunction) + # obj.initialize_coeffs( + # domain=self.domain, + # bckgr_obj=self.equil, + # species=species, + # ) + + # if self.rank_world == 0 and self.verbose: + # print( + # f'\nFluid species "{species}" was initialized with:', + # ) + + # _params = val["params"] + + # if "background" in _params: + # for variable in val: + # if "params" in variable: + # continue + # if variable in _params["background"]: + # bckgr_types = _params["background"][variable] + # if bckgr_types is None: + # pass + # else: + # print(f"{variable} background:") + # for _type, _bp in bckgr_types.items(): + # print(" " * 4 + _type, ":") + # for _pname, _pval in _bp.items(): + # print((" " * 8 + _pname + ":").ljust(25), _pval) + # else: + # print(f"{variable}: no background.") + # else: + # print("No background.") + + # if "perturbation" in _params: + # for variable in val: + # if "params" in variable: + # continue + # if variable in _params["perturbation"]: + # pert_types = _params["perturbation"][variable] + # if pert_types is None: + # pass + # else: + # print(f"{variable} perturbation:") + # for _type, _pp in pert_types.items(): + # print(" " * 4 + _type, ":") + # for _pname, _pval in _pp.items(): + # print((" " * 8 + _pname + ":").ljust(25), _pval) + # else: + # print(f"{variable}: no perturbation.") + # else: + # print("No perturbation.") + + # # initialize particles + # if len(self.kinetic) > 0: + # with ProfileManager.profile_region("initialize_particles"): + # for species, val in self.kinetic.items(): + # obj = val["obj"] + # assert isinstance(obj, Particles) + + # if self.rank_world == 0 and self.verbose: + # _params = val["params"] + # assert "background" in _params, "Kinetic species must have background." + + # bckgr_types = _params["background"] + # print( + # f'\nKinetic species "{species}" was initialized with:', + # ) + # for _type, _bp in bckgr_types.items(): + # print(_type, ":") + # for _pname, _pval in _bp.items(): + # print((" " * 4 + _pname + ":").ljust(25), _pval) + + # if "perturbation" in _params: + # for variable, pert_types in _params["perturbation"].items(): + # if pert_types is None: + # pass + # else: + # print(f"{variable} perturbation:") + # for _type, _pp in pert_types.items(): + # print(" " * 4 + _type, ":") + # for _pname, _pval in _pp.items(): + # print((" " * 8 + _pname + ":").ljust(25), _pval) + # else: + # print("No perturbation.") + + # obj.draw_markers(sort=True, verbose=self.verbose) + # obj.mpi_sort_markers(do_test=True) + + # if not val["params"]["markers"]["loading"] == "restart": + # if obj.coords == "vpara_mu": + # obj.save_magnetic_moment() + + # obj.draw_markers(sort=True, verbose=self.verbose) + # if self.comm_world is not None: + # obj.mpi_sort_markers(do_test=True) + + # obj.initialize_weights( + # reject_weights=obj.weights_params["reject_weights"], + # threshold=obj.weights_params["threshold"], + # ) def initialize_from_restart(self, data): """ @@ -982,9 +981,6 @@ def initialize_from_restart(self, data): The data object that links to the hdf5 files. """ - from struphy.feec.psydac_derham import Derham - from struphy.pic.base import Particles - # initialize em fields if len(self.em_fields) > 0: for key, val in self.em_fields.items(): @@ -1021,7 +1017,7 @@ def initialize_from_restart(self, data): if self.comm_world is not None: obj.mpi_sort_markers(do_test=True) - def initialize_data_output(self, data, size): + def initialize_data_output(self, data: DataContainer, size): """ Create datasets in hdf5 files according to model unknowns and diagnostics data. @@ -1042,14 +1038,6 @@ def initialize_data_output(self, data, size): Keys of datasets which are saved at the end of a simulation to enable restarts. """ - from psydac.linalg.stencil import StencilVector - - from struphy.feec.psydac_derham import Derham - from struphy.io.output_handling import DataContainer - from struphy.pic.base import Particles - - assert isinstance(data, DataContainer) - # save scalar quantities in group 'scalar/' for key, scalar in self.scalar_quantities.items(): val = scalar["value"] @@ -1065,190 +1053,102 @@ def initialize_data_output(self, data, size): else: pass - # save electromagentic fields/potentials data in group 'feec/' - for key, val in self.em_fields.items(): - if "params" in key: - continue - else: - obj = val["obj"] - assert isinstance(obj, SplineFunction) + # save feec data in group 'feec/' + feec_species = self.field_species | self.fluid_species | self.diagnostic_species + for species, val in feec_species.items(): + assert isinstance(val, Species) + + species_path = os.path.join("feec", species) + species_path_restart = os.path.join("restart", species) + + for variable, subval in val.variables.items(): + assert isinstance(subval, FEECVariable) + spline = subval.spline # in-place extraction of FEM coefficients from field.vector --> field.vector_stencil! - obj.extract_coeffs(update_ghost_regions=False) + spline.extract_coeffs(update_ghost_regions=False) # save numpy array to be updated each time step. - if val["save_data"]: - key_field = "feec/" + key + if subval.save_data: + key_field = os.path.join(species_path, variable) - if isinstance(obj.vector_stencil, StencilVector): + if isinstance(spline.vector_stencil, StencilVector): data.add_data( - {key_field: obj.vector_stencil._data}, + {key_field: spline.vector_stencil._data}, ) else: for n in range(3): - key_component = key_field + "/" + str(n + 1) + key_component = os.path.join(key_field, str(n + 1)) data.add_data( - {key_component: obj.vector_stencil[n]._data}, + {key_component: spline.vector_stencil[n]._data}, ) # save field meta data - data.file[key_field].attrs["space_id"] = obj.space_id - data.file[key_field].attrs["starts"] = obj.starts - data.file[key_field].attrs["ends"] = obj.ends - data.file[key_field].attrs["pads"] = obj.pads + data.file[key_field].attrs["space_id"] = spline.space_id + data.file[key_field].attrs["starts"] = spline.starts + data.file[key_field].attrs["ends"] = spline.ends + data.file[key_field].attrs["pads"] = spline.pads # save numpy array to be updated only at the end of the simulation for restart. - key_field_restart = "restart/" + key + key_field_restart = os.path.join(species_path_restart, variable) - if isinstance(obj.vector_stencil, StencilVector): + if isinstance(spline.vector_stencil, StencilVector): data.add_data( - {key_field_restart: obj.vector_stencil._data}, + {key_field_restart: spline.vector_stencil._data}, ) else: for n in range(3): - key_component_restart = key_field_restart + "/" + str(n + 1) - data.add_data( - {key_component_restart: obj.vector_stencil[n]._data}, - ) - - # save fluid data in group 'feec/' - for species, val in self.fluid.items(): - species_path = "feec/" + species + "_" - species_path_restart = "restart/" + species + "_" - - for variable, subval in val.items(): - if "params" in variable: - continue - else: - obj = subval["obj"] - assert isinstance(obj, SplineFunction) - - # in-place extraction of FEM coefficients from field.vector --> field.vector_stencil! - obj.extract_coeffs(update_ghost_regions=False) - - # save numpy array to be updated each time step. - if subval["save_data"]: - key_field = species_path + variable - - if isinstance(obj.vector_stencil, StencilVector): - data.add_data( - {key_field: obj.vector_stencil._data}, - ) - - else: - for n in range(3): - key_component = key_field + "/" + str(n + 1) - data.add_data( - {key_component: obj.vector_stencil[n]._data}, - ) - - # save field meta data - data.file[key_field].attrs["space_id"] = obj.space_id - data.file[key_field].attrs["starts"] = obj.starts - data.file[key_field].attrs["ends"] = obj.ends - data.file[key_field].attrs["pads"] = obj.pads - - # save numpy array to be updated only at the end of the simulation for restart. - key_field_restart = species_path_restart + variable - - if isinstance(obj.vector_stencil, StencilVector): + key_component_restart = os.path.join(key_field_restart, str(n + 1)) data.add_data( - {key_field_restart: obj.vector_stencil._data}, + {key_component_restart: spline.vector_stencil[n]._data}, ) - else: - for n in range(3): - key_component_restart = key_field_restart + "/" + str(n + 1) - data.add_data( - {key_component_restart: obj.vector_stencil[n]._data}, - ) # save kinetic data in group 'kinetic/' - for key, val in self.kinetic.items(): - obj = val["obj"] - assert isinstance(obj, Particles) + for name, species in self.particle_species.items(): + assert isinstance(species, ParticleSpecies) + assert len(species.variables) == 1, f"More than 1 variable per kinetic species is not allowed." + for varname, var in species.variables.items(): + assert isinstance(var, PICVariable | SPHVariable) + obj = var.particles + assert isinstance(obj, Particles) - key_spec = "kinetic/" + key - key_spec_restart = "restart/" + key + key_spec = os.path.join("kinetic", name) + key_spec_restart = os.path.join("restart", name) - data.add_data({key_spec_restart: obj._markers}) + # restart data + data.add_data({key_spec_restart: obj.markers}) - for key1, val1 in val["kinetic_data"].items(): - key_dat = key_spec + "/" + key1 + # marker data + key_mks = os.path.join(key_spec, "markers") + data.add_data({key_mks: var.saved_markers}) - # case of "f" and "df" - if isinstance(val1, dict): - for key2, val2 in val1.items(): - key_f = key_dat + "/" + key2 - data.add_data({key_f: val2}) + # binning plot data + for bin_plot in species.binning_plots: + key_f = os.path.join(key_spec, "f", bin_plot.slice) + key_df = os.path.join(key_spec, "df", bin_plot.slice) - dims = (len(key2) - 2) // 3 + 1 - for dim in range(dims): - data.file[key_f].attrs["bin_centers" + "_" + str(dim + 1)] = ( - val["bin_edges"][key2][dim][:-1] - + (val["bin_edges"][key2][dim][1] - val["bin_edges"][key2][dim][0]) / 2 - ) - # case of "n_sph" - elif isinstance(val1, list): - for i, v1 in enumerate(val1): - key_n = key_dat + "/view_" + str(i) - data.add_data({key_n: v1}) - # save 1d point values, not meshgrids, because attrs size is limited - eta1 = val["plot_pts"][i][0][:, 0, 0] - eta2 = val["plot_pts"][i][1][0, :, 0] - eta3 = val["plot_pts"][i][2][0, 0, :] - data.file[key_n].attrs["eta1"] = eta1 - data.file[key_n].attrs["eta2"] = eta2 - data.file[key_n].attrs["eta3"] = eta3 - else: - data.add_data({key_dat: val1}) + data.add_data({key_f: bin_plot.f}) + data.add_data({key_df: bin_plot.df}) - # save diagnostics data in group 'feec/' - for key, val in self.diagnostics.items(): - if "params" in key: - continue - else: - obj = val["obj"] - assert isinstance(obj, SplineFunction) + for dim, be in enumerate(bin_plot.bin_edges): + data.file[key_f].attrs["bin_centers" + "_" + str(dim + 1)] = be[:-1] + (be[1] - be[0]) / 2 - # in-place extraction of FEM coefficients from field.vector --> field.vector_stencil! - obj.extract_coeffs(update_ghost_regions=False) + for i, kd_plot in enumerate(species.kernel_density_plots): + key_n = os.path.join(key_spec, "n_sph", f"view_{i}") - # save numpy array to be updated each time step. - if val["save_data"]: - key_field = "feec/" + key + data.add_data({key_n: kd_plot.n_sph}) + # save 1d point values, not meshgrids, because attrs size is limited + eta1 = kd_plot.plot_pts[0][:, 0, 0] + eta2 = kd_plot.plot_pts[1][0, :, 0] + eta3 = kd_plot.plot_pts[2][0, 0, :] + data.file[key_n].attrs["eta1"] = eta1 + data.file[key_n].attrs["eta2"] = eta2 + data.file[key_n].attrs["eta3"] = eta3 - if isinstance(obj.vector_stencil, StencilVector): - data.add_data( - {key_field: obj.vector_stencil._data}, - ) - - else: - for n in range(3): - key_component = key_field + "/" + str(n + 1) - data.add_data( - {key_component: obj.vector_stencil[n]._data}, - ) - - # save field meta data - data.file[key_field].attrs["space_id"] = obj.space_id - data.file[key_field].attrs["starts"] = obj.starts - data.file[key_field].attrs["ends"] = obj.ends - data.file[key_field].attrs["pads"] = obj.pads - - # save numpy array to be updated only at the end of the simulation for restart. - key_field_restart = "restart/" + key - - if isinstance(obj.vector_stencil, StencilVector): - data.add_data( - {key_field_restart: obj.vector_stencil._data}, - ) - else: - for n in range(3): - key_component_restart = key_field_restart + "/" + str(n + 1) - data.add_data( - {key_component_restart: obj.vector_stencil[n]._data}, - ) + # TODO: maybe add other data + # else: + # data.add_data({key_dat: val1}) # keys to be saved at each time step and only at end (restart) save_keys_all = [] @@ -1266,151 +1166,6 @@ def initialize_data_output(self, data, size): # Class methods : ################### - @classmethod - def model_units(cls, params, verbose=False, comm=None): - """ - Return model units and print them to screen. - - Parameters - ---------- - params : dict - model parameters. - - verbose : bool, optional - print model units to screen. - - comm : obj - MPI communicator. - - Returns - ------- - units_basic : dict - Basic units for time, length, mass and magnetic field. - - units_der : dict - Derived units for velocity, pressure, mass density and particle density. - """ - - from struphy.io.setup import derive_units - - if comm is None: - rank = 0 - else: - rank = comm.Get_rank() - - # look for bulk species in fluid OR kinetic parameter dictionaries - Z_bulk = None - A_bulk = None - if "fluid" in params: - if cls.bulk_species() in params["fluid"]: - Z_bulk = params["fluid"][cls.bulk_species()]["phys_params"]["Z"] - A_bulk = params["fluid"][cls.bulk_species()]["phys_params"]["A"] - if "kinetic" in params: - if cls.bulk_species() in params["kinetic"]: - Z_bulk = params["kinetic"][cls.bulk_species()]["phys_params"]["Z"] - A_bulk = params["kinetic"][cls.bulk_species()]["phys_params"]["A"] - - # compute model units - if "kBT" in params["units"]: - kBT = params["units"]["kBT"] - else: - kBT = None - - units = derive_units( - Z_bulk=Z_bulk, - A_bulk=A_bulk, - x=params["units"]["x"], - B=params["units"]["B"], - n=params["units"]["n"], - kBT=kBT, - velocity_scale=cls.velocity_scale(), - ) - - # print to screen - if verbose and rank == 0: - print("\nUNITS:") - print( - f"Unit of length:".ljust(25), - "{:4.3e}".format(units["x"]) + " m", - ) - print( - f"Unit of time:".ljust(25), - "{:4.3e}".format(units["t"]) + " s", - ) - print( - f"Unit of velocity:".ljust(25), - "{:4.3e}".format(units["v"]) + " m/s", - ) - print( - f"Unit of magnetic field:".ljust(25), - "{:4.3e}".format(units["B"]) + " T", - ) - - if A_bulk is not None: - print( - f"Unit of particle density:".ljust(25), - "{:4.3e}".format(units["n"]) + " m⁻³", - ) - print( - f"Unit of mass density:".ljust(25), - "{:4.3e}".format(units["rho"]) + " kg/m³", - ) - print( - f"Unit of pressure:".ljust(25), - "{:4.3e}".format(units["p"] * 1e-5) + " bar", - ) - print( - f"Unit of current density:".ljust(25), - "{:4.3e}".format(units["j"]) + " A/m²", - ) - - # compute equation parameters for each species - e = 1.602176634e-19 # elementary charge (C) - mH = 1.67262192369e-27 # proton mass (kg) - eps0 = 8.8541878128e-12 # vacuum permittivity (F/m) - - equation_params = {} - if "fluid" in params: - for species in params["fluid"]: - Z = params["fluid"][species]["phys_params"]["Z"] - A = params["fluid"][species]["phys_params"]["A"] - - # compute equation parameters - om_p = np.sqrt(units["n"] * (Z * e) ** 2 / (eps0 * A * mH)) - om_c = Z * e * units["B"] / (A * mH) - equation_params[species] = {} - equation_params[species]["alpha"] = om_p / om_c - equation_params[species]["epsilon"] = 1.0 / (om_c * units["t"]) - equation_params[species]["kappa"] = om_p * units["t"] - - if verbose and rank == 0: - print("\nNORMALIZATION PARAMETERS:") - print("- " + species + ":") - for key, val in equation_params[species].items(): - print((key + ":").ljust(25), "{:4.3e}".format(val)) - - if "kinetic" in params: - for species in params["kinetic"]: - Z = params["kinetic"][species]["phys_params"]["Z"] - A = params["kinetic"][species]["phys_params"]["A"] - - # compute equation parameters - om_p = np.sqrt(units["n"] * (Z * e) ** 2 / (eps0 * A * mH)) - om_c = Z * e * units["B"] / (A * mH) - equation_params[species] = {} - equation_params[species]["alpha"] = om_p / om_c - equation_params[species]["epsilon"] = 1.0 / (om_c * units["t"]) - equation_params[species]["kappa"] = om_p * units["t"] - - if verbose and rank == 0: - if "fluid" not in params: - print("\nNORMALIZATION PARAMETERS:") - print("- " + species + ":") - for key, val in equation_params[species].items(): - print((key + ":").ljust(25), "{:4.3e}".format(val)) - - return units, equation_params - @classmethod def show_options(cls): """Print available model options to screen.""" @@ -1507,11 +1262,9 @@ def write_parameters_to_file(cls, parameters=None, file=None, save=True, prompt= else: pass - @classmethod def generate_default_parameter_file( - cls, - file: str = None, - save: bool = True, + self, + path: str = None, prompt: bool = True, ): """Generate a parameter file with default options for each species, @@ -1521,436 +1274,233 @@ def generate_default_parameter_file( Parameters ---------- - file : str - Alternative filename to params_.yml. - - save : bool - Whether to save the parameter file in the current input path. + path : str + Alternative path to getcwd()/params_MODEL.py. prompt : bool Whether to prompt for overwriting the specified .yml file. Returns ------- - The default parameter dictionary.""" - - import os - - import yaml - - import struphy - from struphy.io.setup import descend_options_dict - - libpath = struphy.__path__[0] - - # load a standard parameter file - with open(os.path.join(libpath, "io/inp/parameters.yml")) as tmp: - parameters = yaml.load(tmp, Loader=yaml.FullLoader) - - parameters["model"] = cls.__name__ - - # extract default em_fields parameters - bckgr_params_1_em = parameters["em_fields"]["background"]["var_1"] - bckgr_params_2_em = parameters["em_fields"]["background"]["var_2"] - parameters["em_fields"].pop("background") - - pert_params_1_em = parameters["em_fields"]["perturbation"]["var_1"] - pert_params_2_em = parameters["em_fields"]["perturbation"]["var_2"] - parameters["em_fields"].pop("perturbation") - - # extract default fluid parameters - bckgr_params_1_fluid = parameters["fluid"]["species_name"]["background"]["var_1"] - bckgr_params_2_fluid = parameters["fluid"]["species_name"]["background"]["var_2"] - parameters["fluid"]["species_name"].pop("background") - - pert_params_1_fluid = parameters["fluid"]["species_name"]["perturbation"]["var_1"] - pert_params_2_fluid = parameters["fluid"]["species_name"]["perturbation"]["var_2"] - parameters["fluid"]["species_name"].pop("perturbation") - - # standard Maxwellians - parameters["kinetic"]["species_name"].pop("background") - maxw_name = { - "6D": "Maxwellian3D", - "5D": "GyroMaxwellian2D", - "4D": "Maxwellian1D", - "3D": "ColdPlasma", - "PH": "ConstantVelocity", - } - - # init options dicts - d_opts = {"em_fields": [], "fluid": {}, "kinetic": {}} - - # set the correct names in the parameter file - if len(cls.species()["em_fields"]) > 0: - parameters["em_fields"]["background"] = {} - parameters["em_fields"]["perturbation"] = {} - for name, space in cls.species()["em_fields"].items(): - if space in {"H1", "L2"}: - parameters["em_fields"]["background"][name] = bckgr_params_1_em - parameters["em_fields"]["perturbation"][name] = pert_params_1_em - elif space in {"Hcurl", "Hdiv", "H1vec"}: - parameters["em_fields"]["background"][name] = bckgr_params_2_em - parameters["em_fields"]["perturbation"][name] = pert_params_2_em - else: - parameters.pop("em_fields") - - # find out the default em_fields options of the model - if "options" in cls.options()["em_fields"]: - # create the default options parameters - d_default = descend_options_dict( - cls.options()["em_fields"]["options"], - d_opts["em_fields"], - ) - parameters["em_fields"]["options"] = d_default - - # fluid - fluid_params = parameters["fluid"].pop("species_name") - - if len(cls.species()["fluid"]) > 0: - for name, dct in cls.species()["fluid"].items(): - parameters["fluid"][name] = fluid_params - parameters["fluid"][name]["background"] = {} - parameters["fluid"][name]["perturbation"] = {} - - # find out the default fluid options of the model - if name in cls.options()["fluid"]: - d_opts["fluid"][name] = [] - - # create the default options parameters - d_default = descend_options_dict( - cls.options()["fluid"][name]["options"], - d_opts["fluid"][name], - ) - - parameters["fluid"][name]["options"] = d_default - - # set the correct names parameter file - for sub_name, space in dct.items(): - if space in {"H1", "L2"}: - parameters["fluid"][name]["background"][sub_name] = bckgr_params_1_fluid - parameters["fluid"][name]["perturbation"][sub_name] = pert_params_1_fluid - elif space in {"Hcurl", "Hdiv", "H1vec"}: - parameters["fluid"][name]["background"][sub_name] = bckgr_params_2_fluid - parameters["fluid"][name]["perturbation"][sub_name] = pert_params_2_fluid - else: - parameters.pop("fluid") - - # kinetic - kinetic_params = parameters["kinetic"].pop("species_name") - - if len(cls.species()["kinetic"]) > 0: - parameters["kinetic"] = {} - - for name, kind in cls.species()["kinetic"].items(): - parameters["kinetic"][name] = kinetic_params - - # find out the default kinetic options of the model - if name in cls.options()["kinetic"]: - d_opts["kinetic"][name] = [] - - # create the default options parameters - d_default = descend_options_dict( - cls.options()["kinetic"][name]["options"], - d_opts["kinetic"][name], - ) - - parameters["kinetic"][name]["options"] = d_default - - # set the background - dim = kind[-2:] - parameters["kinetic"][name]["background"] = { - maxw_name[dim]: {"n": 0.05}, - } - else: - parameters.pop("kinetic") - - # diagnostics - if cls.diagnostics_dct() is not None: - parameters["diagnostics"] = {} - for name, space in cls.diagnostics_dct().items(): - parameters["diagnostics"][name] = {"save_data": True} - - cls.write_parameters_to_file( - parameters=parameters, - file=file, - save=save, - prompt=prompt, - ) - - return parameters - - ################### - # Private methods : - ################### - - def _init_variable_dicts(self): - """ - Initialize em-fields, fluid and kinetic dictionaries for information on the model variables. + params_path : str + The path of the parameter file. """ - # electromagnetic fields, fluid and/or kinetic species - self._em_fields = {} - self._fluid = {} - self._kinetic = {} - self._diagnostics = {} + if path is None: + path = os.path.join(os.getcwd(), f"params_{self.__class__.__name__}.py") - if self.rank_world == 0 and self.verbose: - print("\nMODEL SPECIES:") - - # create dictionaries for each em-field/species and fill in space/class name and parameters - for var_name, space in self.species()["em_fields"].items(): - assert space in {"H1", "Hcurl", "Hdiv", "L2", "H1vec"} - assert "em_fields" in self.params, 'Top-level key "em_fields" is missing in parameter file.' - - if self.rank_world == 0 and self.verbose: - print("em_field:".ljust(25), f'"{var_name}" ({space})') - - self._em_fields[var_name] = {} - - # space - self._em_fields[var_name]["space"] = space - - # initial conditions - if "background" in self.params["em_fields"]: - self._em_fields[var_name]["background"] = self.params["em_fields"]["background"].get(var_name) - if "perturbation" in self.params["em_fields"]: - self._em_fields[var_name]["perturbation"] = self.params["em_fields"]["perturbation"].get(var_name) - - # which components to save - if "save_data" in self.params["em_fields"]: - self._em_fields[var_name]["save_data"] = self.params["em_fields"]["save_data"]["comps"][var_name] + # create new default file + try: + file = open(path, "x") + except FileExistsError: + if not prompt: + yn = "Y" else: - self._em_fields[var_name]["save_data"] = True - - # overall parameters - self._em_fields["params"] = self.params["em_fields"] - - for var_name, space in self.species()["fluid"].items(): - assert isinstance(space, dict) - assert "fluid" in self.params, 'Top-level key "fluid" is missing in parameter file.' - assert var_name in self.params["fluid"], f"Fluid species {var_name} is missing in parameter file." - - if self.rank_world == 0 and self.verbose: - print("fluid:".ljust(25), f'"{var_name}" ({space})') - - self._fluid[var_name] = {} - for sub_var_name, sub_space in space.items(): - self._fluid[var_name][sub_var_name] = {} - - # space - self._fluid[var_name][sub_var_name]["space"] = sub_space - - # initial conditions - if "background" in self.params["fluid"][var_name]: - self._fluid[var_name][sub_var_name]["background"] = self.params["fluid"][var_name][ - "background" - ].get(sub_var_name) - if "perturbation" in self.params["fluid"][var_name]: - self._fluid[var_name][sub_var_name]["perturbation"] = self.params["fluid"][var_name][ - "perturbation" - ].get(sub_var_name) - - # which components to save - if "save_data" in self.params["fluid"][var_name]: - self._fluid[var_name][sub_var_name]["save_data"] = self.params["fluid"][var_name]["save_data"][ - "comps" - ][sub_var_name] - - else: - self._fluid[var_name][sub_var_name]["save_data"] = True - - # overall parameters - self._fluid[var_name]["params"] = self.params["fluid"][var_name] - - for var_name, space in self.species()["kinetic"].items(): - assert "Particles" in space - assert "kinetic" in self.params, 'Top-level key "kinetic" is missing in parameter file.' - assert var_name in self.params["kinetic"], f"Kinetic species {var_name} is missing in parameter file." - - if self.rank_world == 0 and self.verbose: - print("kinetic:".ljust(25), f'"{var_name}" ({space})') - - self._kinetic[var_name] = {} - self._kinetic[var_name]["space"] = space - self._kinetic[var_name]["params"] = self.params["kinetic"][var_name] - - if self.diagnostics_dct() is not None: - for var_name, space in self.diagnostics_dct().items(): - assert space in {"H1", "Hcurl", "Hdiv", "L2", "H1vec"} - - if self.rank_world == 0 and self.verbose: - print("diagnostics:".ljust(25), f'"{var_name}" ({space})') - - self._diagnostics[var_name] = {} - self._diagnostics[var_name]["space"] = space - self._diagnostics["params"] = self.params["diagnostics"][var_name] - - # which components to save - if "save_data" in self.params["diagnostics"][var_name]: - self._diagnostics[var_name]["save_data"] = self.params["diagnostics"][var_name]["save_data"] - - else: - self._diagnostics[var_name]["save_data"] = True - - def _allocate_variables(self): - """ - Allocate memory for model variables. - Creates FEM fields for em-fields and fluid variables and a particle class for kinetic species. - """ - - from struphy.feec.psydac_derham import Derham - from struphy.pic import particles - from struphy.pic.base import Particles - - # allocate memory for FE coeffs of electromagnetic fields/potentials - if "em_fields" in self.params: - for variable, dct in self.em_fields.items(): - if "params" in variable: - continue - else: - dct["obj"] = self.derham.create_spline_function( - variable, - dct["space"], - bckgr_params=dct.get("background"), - pert_params=dct.get("perturbation"), - ) - - self._pointer[variable] = dct["obj"].vector - - # allocate memory for FE coeffs of fluid variables - if "fluid" in self.params: - for species, dct in self.fluid.items(): - for variable, subdct in dct.items(): - if "params" in variable: - continue + yn = input(f"\nFile {path} exists, overwrite (Y/n)? ") + if yn in ("", "Y", "y", "yes", "Yes"): + file = open(path, "w") + else: + print("exiting ...") + exit() + except FileNotFoundError: + folder = os.path.join("/", *path.split("/")[:-1]) + if not prompt: + yn = "Y" + else: + yn = input(f"\nFolder {folder} does not exist, create (Y/n)? ") + if yn in ("", "Y", "y", "yes", "Yes"): + os.makedirs(folder) + file = open(path, "x") + else: + print("exiting ...") + exit() + + file.write("from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n") + file.write("from struphy.geometry import domains\n") + file.write("from struphy.fields_background import equils\n") + + species_params = "\n# species parameters\n" + particle_params = "" + has_plasma = False + has_feec = False + has_pic = False + has_sph = False + for sn, species in self.species.items(): + assert isinstance(species, Species) + + if isinstance(species, (FluidSpecies, ParticleSpecies)): + has_plasma = True + species_params += f"model.{sn}.set_phys_params()\n" + if isinstance(species, ParticleSpecies): + particle_params += f"\nloading_params = LoadingParameters()\n" + particle_params += f"weights_params = WeightsParameters()\n" + particle_params += f"boundary_params = BoundaryParameters()\n" + particle_params += f"model.{sn}.set_markers(loading_params=loading_params,\n" + txt = f"weights_params=weights_params,\n" + particle_params += indent(txt, " " * len(f"model.{sn}.set_markers(")) + txt = f"boundary_params=boundary_params,\n" + particle_params += indent(txt, " " * len(f"model.{sn}.set_markers(")) + txt = f")\n" + particle_params += indent(txt, " " * len(f"model.{sn}.set_markers(")) + particle_params += f"model.{sn}.set_sorting_boxes()\n" + particle_params += f"model.{sn}.set_save_data()\n" + + for vn, var in species.variables.items(): + if isinstance(var, FEECVariable): + has_feec = True + if var.space in ("H1", "L2"): + init_bckgr_feec = f"model.{sn}.{vn}.add_background(FieldsBackground())\n" + init_pert_feec = f"model.{sn}.{vn}.add_perturbation(perturbations.TorusModesCos())\n" else: - subdct["obj"] = self.derham.create_spline_function( - variable, - subdct["space"], - bckgr_params=subdct.get("background"), - pert_params=subdct.get("perturbation"), + init_bckgr_feec = f"model.{sn}.{vn}.add_background(FieldsBackground())\n" + init_pert_feec = ( + f"model.{sn}.{vn}.add_perturbation(perturbations.TorusModesCos(given_in_basis='v', comp=0))\n\ +model.{sn}.{vn}.add_perturbation(perturbations.TorusModesCos(given_in_basis='v', comp=1))\n\ +model.{sn}.{vn}.add_perturbation(perturbations.TorusModesCos(given_in_basis='v', comp=2))\n" ) - self._pointer[species + "_" + variable] = subdct["obj"].vector - - # marker arrays and plasma parameters of kinetic species - if "kinetic" in self.params: - for species, val in self.kinetic.items(): - assert any([key in val["params"]["markers"] for key in ["Np", "ppc", "ppb"]]) - - bckgr_params = val["params"].get("background", None) - pert_params = val["params"].get("perturbation", None) - boxes_per_dim = val["params"].get("boxes_per_dim", None) - mpi_dims_mask = val["params"].get("dims_mask", None) - weights_params = val["params"].get("weights", None) - - if self.derham is None: - domain_decomp = None - else: - domain_array = self.derham.domain_array - nprocs = self.derham.domain_decomposition.nprocs - domain_decomp = (domain_array, nprocs) - - kinetic_class = getattr(particles, val["space"]) - # print(f"{kinetic_class = }") - val["obj"] = kinetic_class( - comm_world=self.comm_world, - clone_config=self.clone_config, - **val["params"]["markers"], - weights_params=weights_params, - domain_decomp=domain_decomp, - mpi_dims_mask=mpi_dims_mask, - boxes_per_dim=boxes_per_dim, - name=species, - equation_params=self.equation_params[species], - domain=self.domain, - equil=self.equil, - projected_equil=self.projected_equil, - bckgr_params=bckgr_params, - pert_params=pert_params, - ) - - obj = val["obj"] - assert isinstance(obj, Particles) - - self._pointer[species] = obj - - # for storing markers - val["kinetic_data"] = {} - - # for storing the distribution function - if "f" in val["params"]["save_data"]: - slices = val["params"]["save_data"]["f"]["slices"] - n_bins = val["params"]["save_data"]["f"]["n_bins"] - ranges = val["params"]["save_data"]["f"]["ranges"] - - val["kinetic_data"]["f"] = {} - val["kinetic_data"]["df"] = {} - val["bin_edges"] = {} - if len(slices) > 0: - for i, sli in enumerate(slices): - assert ((len(sli) - 2) / 3).is_integer() - assert len(slices[i].split("_")) == len(ranges[i]) == len(n_bins[i]), ( - f"Number of slices names ({len(slices[i].split('_'))}), number of bins ({len(n_bins[i])}), and number of ranges ({len(ranges[i])}) are inconsistent with each other!\n\n" - ) - val["bin_edges"][sli] = [] - dims = (len(sli) - 2) // 3 + 1 - for j in range(dims): - val["bin_edges"][sli] += [ - np.linspace( - ranges[i][j][0], - ranges[i][j][1], - n_bins[i][j] + 1, - ), - ] - val["kinetic_data"]["f"][sli] = np.zeros( - n_bins[i], - dtype=float, - ) - val["kinetic_data"]["df"][sli] = np.zeros( - n_bins[i], - dtype=float, - ) - - # for storing an sph evaluation of the density n - if "n_sph" in val["params"]["save_data"]: - plot_pts = val["params"]["save_data"]["n_sph"]["plot_pts"] - - val["kinetic_data"]["n_sph"] = [] - val["plot_pts"] = [] - for i, pts in enumerate(plot_pts): - assert len(pts) == 3 - eta1 = np.linspace(0.0, 1.0, pts[0]) - eta2 = np.linspace(0.0, 1.0, pts[1]) - eta3 = np.linspace(0.0, 1.0, pts[2]) - ee1, ee2, ee3 = np.meshgrid( - eta1, - eta2, - eta3, - indexing="ij", + elif isinstance(var, PICVariable): + has_pic = True + init_pert_pic = f"\n# if .add_initial_condition is not called, the background is the kinetic initial condition\n" + init_pert_pic += f"perturbation = perturbations.TorusModesCos()\n" + if "6D" in var.space: + init_bckgr_pic = f"maxwellian_1 = maxwellians.Maxwellian3D(n=(1.0, None))\n" + init_bckgr_pic += f"maxwellian_2 = maxwellians.Maxwellian3D(n=(0.1, None))\n" + init_pert_pic += f"maxwellian_1pt = maxwellians.Maxwellian3D(n=(1.0, perturbation))\n" + init_pert_pic += f"init = maxwellian_1pt + maxwellian_2\n" + init_pert_pic += f"model.{sn}.{vn}.add_initial_condition(init)\n" + elif "5D" in var.space: + init_bckgr_pic = f"maxwellian_1 = maxwellians.GyroMaxwellian2D(n=(1.0, None), equil=equil)\n" + init_bckgr_pic += f"maxwellian_2 = maxwellians.GyroMaxwellian2D(n=(0.1, None), equil=equil)\n" + init_pert_pic += ( + f"maxwellian_1pt = maxwellians.GyroMaxwellian2D(n=(1.0, perturbation), equil=equil)\n" ) - val["plot_pts"] += [(ee1, ee2, ee3)] - val["kinetic_data"]["n_sph"] += [np.zeros(ee1.shape, dtype=float)] + init_pert_pic += f"init = maxwellian_1pt + maxwellian_2\n" + init_pert_pic += f"model.{sn}.{vn}.add_initial_condition(init)\n" + if "3D" in var.space: + init_bckgr_pic = f"maxwellian_1 = maxwellians.ColdPlasma(n=(1.0, None))\n" + init_bckgr_pic += f"maxwellian_2 = maxwellians.ColdPlasma(n=(0.1, None))\n" + init_pert_pic += f"maxwellian_1pt = maxwellians.ColdPlasma(n=(1.0, perturbation))\n" + init_pert_pic += f"init = maxwellian_1pt + maxwellian_2\n" + init_pert_pic += f"model.{sn}.{vn}.add_initial_condition(init)\n" + init_bckgr_pic += f"background = maxwellian_1 + maxwellian_2\n" + init_bckgr_pic += f"model.{sn}.{vn}.add_background(background)\n" + + exclude = f"# model.....save_data = False\n" + + elif isinstance(var, SPHVariable): + has_sph = True + init_bckgr_sph = f"background = equils.ConstantVelocity()\n" + init_bckgr_sph += f"model.{sn}.{vn}.add_background(background)\n" + init_pert_sph = f"perturbation = perturbations.TorusModesCos()\n" + init_pert_sph += f"model.{sn}.{vn}.add_perturbation(del_n=perturbation)\n" + exclude = f"# model.{sn}.{vn}.save_data = False\n" + + file.write("from struphy.topology import grids\n") + file.write("from struphy.io.options import DerhamOptions\n") + file.write("from struphy.io.options import FieldsBackground\n") + file.write("from struphy.initial import perturbations\n") + + file.write("from struphy.kinetic_background import maxwellians\n") + file.write( + "from struphy.pic.utilities import (LoadingParameters,\n\ + WeightsParameters,\n\ + BoundaryParameters,\n\ + BinningPlot,\n\ + KernelDensityPlot,\n\ + )\n" + ) + file.write("from struphy import main\n") + + file.write("\n# import model, set verbosity\n") + file.write(f"from {self.__module__} import {self.__class__.__name__}\n") + + file.write("\n# environment options\n") + file.write("env = EnvironmentOptions()\n") + + file.write("\n# units\n") + file.write("base_units = BaseUnits()\n") + + file.write("\n# time stepping\n") + file.write("time_opts = Time()\n") + + file.write("\n# geometry\n") + file.write("domain = domains.Cuboid()\n") + + file.write("\n# fluid equilibrium (can be used as part of initial conditions)\n") + file.write("equil = equils.HomogenSlab()\n") + + # if has_feec: + grid = "grid = grids.TensorProductGrid()\n" + derham = "derham_opts = DerhamOptions()\n" + # else: + # grid = "grid = None\n" + # derham = "derham_opts = None\n" + + file.write("\n# grid\n") + file.write(grid) + + file.write("\n# derham options\n") + file.write(derham) + + file.write("\n# light-weight model instance\n") + file.write(f"model = {self.__class__.__name__}()\n") + + if has_plasma: + file.write(species_params) + + if has_pic or has_sph: + file.write(particle_params) + + file.write("\n# propagator options\n") + for prop in self.propagators.__dict__: + file.write(f"model.propagators.{prop}.options = model.propagators.{prop}.Options()\n") + + file.write("\n# background, perturbations and initial conditions\n") + if has_feec: + file.write(init_bckgr_feec) + file.write(init_pert_feec) + if has_pic: + file.write(init_bckgr_pic) + file.write(init_pert_pic) + if has_sph: + file.write(init_bckgr_sph) + file.write(init_pert_sph) + + file.write("\n# optional: exclude variables from saving\n") + file.write(exclude) + + file.write('\nif __name__ == "__main__":\n') + file.write(" # start run\n") + file.write(" verbose = True\n\n") + file.write( + " main.run(model,\n\ + params_path=__file__,\n\ + env=env,\n\ + base_units=base_units,\n\ + time_opts=time_opts,\n\ + domain=domain,\n\ + equil=equil,\n\ + grid=grid,\n\ + derham_opts=derham_opts,\n\ + verbose=verbose,\n\ + )" + ) - # other data (wave-particle power exchange, etc.) - # TODO + file.close() - # allocate memory for FE coeffs of diagnostics - if "diagnostics" in self.params: - for key, val in self.diagnostics.items(): - if "params" in key: - continue - else: - val["obj"] = self.derham.create_spline_function( - key, - val["space"], - bckgr_params=None, - pert_params=None, - ) + print( + f"\nDefault parameter file for '{self.__class__.__name__}' has been created in the cwd ({path}).\n\ +You can now launch a simulation with 'python params_{self.__class__.__name__}.py'" + ) - self._pointer[key] = val["obj"].vector + return path - def _compute_plasma_params(self, verbose=True): + ################### + # Private methods : + ################### + + def compute_plasma_params(self, verbose=True): """ Compute and print volume averaged plasma parameters for each species of the model. @@ -1976,37 +1526,8 @@ def _compute_plasma_params(self, verbose=True): - rho/L - alpha = Omega_p/Omega_c - epsilon = 1/(t*Omega_c) - - Returns - ------- - pparams : dict - Plasma parameters for each species. """ - from struphy.fields_background import equils - from struphy.fields_background.base import FluidEquilibriumWithB - from struphy.kinetic_background import maxwellians - - pparams = {} - - # physics constants - e = 1.602176634e-19 # elementary charge (C) - m_p = 1.67262192369e-27 # proton mass (kg) - mu0 = 1.25663706212e-6 # magnetic constant (N*A^-2) - eps0 = 8.8541878128e-12 # vacuum permittivity (F*m^-1) - kB = 1.380649e-23 # Boltzmann constant (J*K^-1) - - # exit when there is not any plasma species - if len(self.fluid) == 0 and len(self.kinetic) == 0: - return - - # compute model units - units, equation_params = self.model_units( - self.params, - verbose=False, - comm=self.comm_world, - ) - # units affices for printing units_affix = {} units_affix["plasma volume"] = " m³" @@ -2033,32 +1554,33 @@ def _compute_plasma_params(self, verbose=True): units_affix["epsilon"] = "" h = 1 / 20 - eta1 = np.linspace(h / 2.0, 1.0 - h / 2.0, 20) - eta2 = np.linspace(h / 2.0, 1.0 - h / 2.0, 20) - eta3 = np.linspace(h / 2.0, 1.0 - h / 2.0, 20) + eta1 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) + eta2 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) + eta3 = xp.linspace(h / 2.0, 1.0 - h / 2.0, 20) + + ## global parameters - # global parameters # plasma volume (hat x^3) det_tmp = self.domain.jacobian_det(eta1, eta2, eta3) - vol1 = np.mean(np.abs(det_tmp)) + vol1 = xp.mean(xp.abs(det_tmp)) # plasma volume (m⁻³) - plasma_volume = vol1 * units["x"] ** 3 + plasma_volume = vol1 * self.units.x**3 # transit length (m) transit_length = plasma_volume ** (1 / 3) # magnetic field (T) if isinstance(self.equil, FluidEquilibriumWithB): B_tmp = self.equil.absB0(eta1, eta2, eta3) else: - B_tmp = np.zeros((eta1.size, eta2.size, eta3.size)) - magnetic_field = np.mean(B_tmp * np.abs(det_tmp)) / vol1 * units["B"] - B_max = np.max(B_tmp) * units["B"] - B_min = np.min(B_tmp) * units["B"] + B_tmp = xp.zeros((eta1.size, eta2.size, eta3.size)) + magnetic_field = xp.mean(B_tmp * xp.abs(det_tmp)) / vol1 * self.units.B + B_max = xp.max(B_tmp) * self.units.B + B_min = xp.min(B_tmp) * self.units.B if magnetic_field < 1e-14: - magnetic_field = np.nan + magnetic_field = xp.nan # print("\n+++++++ WARNING +++++++ magnetic field is zero - set to nan !!") - if verbose: + if verbose and MPI.COMM_WORLD.Get_rank() == 0: print("\nPLASMA PARAMETERS:") print( f"Plasma volume:".ljust(25), @@ -2081,188 +1603,186 @@ def _compute_plasma_params(self, verbose=True): "{:4.3e}".format(B_min) + units_affix["magnetic field"], ) - # species dependent parameters - pparams = {} - - if len(self.fluid) > 0: - for species, val in self.fluid.items(): - pparams[species] = {} - # type - pparams[species]["type"] = "fluid" - # mass (kg) - pparams[species]["mass"] = val["params"]["phys_params"]["A"] * m_p - # charge (C) - pparams[species]["charge"] = val["params"]["phys_params"]["Z"] * e - # density (m⁻³) - pparams[species]["density"] = ( - np.mean( - self.equil.n0( - eta1, - eta2, - eta3, - ) - * np.abs(det_tmp), - ) - * units["x"] ** 3 - / plasma_volume - * units["n"] - ) - # pressure (bar) - pparams[species]["pressure"] = ( - np.mean( - self.equil.p0( - eta1, - eta2, - eta3, - ) - * np.abs(det_tmp), - ) - * units["x"] ** 3 - / plasma_volume - * units["p"] - * 1e-5 - ) - # thermal energy (keV) - pparams[species]["kBT"] = pparams[species]["pressure"] * 1e5 / pparams[species]["density"] / e * 1e-3 - - if len(self.kinetic) > 0: - eta1mg, eta2mg, eta3mg = np.meshgrid( - eta1, - eta2, - eta3, - indexing="ij", - ) - - for species, val in self.kinetic.items(): - pparams[species] = {} - # type - pparams[species]["type"] = "kinetic" - # mass (kg) - pparams[species]["mass"] = val["params"]["phys_params"]["A"] * m_p - # charge (C) - pparams[species]["charge"] = val["params"]["phys_params"]["Z"] * e - - # create temp kinetic object for (default) parameter extraction - tmp_bckgr = val["params"]["background"] - - if val["space"] != "ParticlesSPH": - tmp = None - for fi, maxw_params in tmp_bckgr.items(): - if fi[-2] == "_": - fi_type = fi[:-2] - else: - fi_type = fi - - if tmp is None: - tmp = getattr(maxwellians, fi_type)( - maxw_params=maxw_params, - equil=self.equil, - ) - else: - tmp = tmp + getattr(maxwellians, fi_type)( - maxw_params=maxw_params, - equil=self.equil, - ) - - if val["space"] != "ParticlesSPH" and tmp.coords == "constants_of_motion": - # call parameters - a1 = self.domain.params["a1"] - r = eta1mg * (1 - a1) + a1 - psi = self.equil.psi_r(r) - - # density (m⁻³) - pparams[species]["density"] = ( - np.mean(tmp.n(psi) * np.abs(det_tmp)) * units["x"] ** 3 / plasma_volume * units["n"] - ) - # thermal speed (m/s) - pparams[species]["v_th"] = ( - np.mean(tmp.vth(psi) * np.abs(det_tmp)) * units["x"] ** 3 / plasma_volume * units["v"] - ) - # thermal energy (keV) - pparams[species]["kBT"] = pparams[species]["mass"] * pparams[species]["v_th"] ** 2 / e * 1e-3 - # pressure (bar) - pparams[species]["pressure"] = ( - pparams[species]["kBT"] * e * 1e3 * pparams[species]["density"] * 1e-5 - ) - - else: - # density (m⁻³) - # pparams[species]['density'] = np.mean(tmp.n( - # eta1mg, eta2mg, eta3mg) * np.abs(det_tmp)) * units['x']**3 / plasma_volume * units['n'] - pparams[species]["density"] = 99.0 - # thermal speeds (m/s) - vth = [] - # vths = tmp.vth(eta1mg, eta2mg, eta3mg) - vths = [99.0] - for k in range(len(vths)): - vth += [ - vths[k] * np.abs(det_tmp) * units["x"] ** 3 / plasma_volume * units["v"], - ] - thermal_speed = 0.0 - for dir in range(val["obj"].vdim): - # pparams[species]['vth' + str(dir + 1)] = np.mean(vth[dir]) - pparams[species]["vth" + str(dir + 1)] = 99.0 - thermal_speed += pparams[species]["vth" + str(dir + 1)] - # TODO: here it is assumed that background density parameter is called "n", - # and that background thermal speeds are called "vthn"; make this a convention? - # pparams[species]['v_th'] = thermal_speed / \ - # val['obj'].vdim - pparams[species]["v_th"] = 99.0 - # thermal energy (keV) - # pparams[species]['kBT'] = pparams[species]['mass'] * \ - # pparams[species]['v_th']**2 / e * 1e-3 - pparams[species]["kBT"] = 99.0 - # pressure (bar) - # pparams[species]['pressure'] = pparams[species]['kBT'] * \ - # e * 1e3 * pparams[species]['density'] * 1e-5 - pparams[species]["pressure"] = 99.0 - - for species in pparams: - # alfvén speed (m/s) - pparams[species]["v_A"] = magnetic_field / np.sqrt( - mu0 * pparams[species]["mass"] * pparams[species]["density"], - ) - # thermal speed (m/s) - pparams[species]["v_th"] = np.sqrt( - pparams[species]["kBT"] * 1e3 * e / pparams[species]["mass"], - ) - # thermal frequency (Mrad/s) - pparams[species]["Omega_th"] = pparams[species]["v_th"] / transit_length * 1e-6 - # cyclotron frequency (Mrad/s) - pparams[species]["Omega_c"] = pparams[species]["charge"] * magnetic_field / pparams[species]["mass"] * 1e-6 - # plasma frequency (Mrad/s) - pparams[species]["Omega_p"] = ( - np.sqrt( - pparams[species]["density"] * (pparams[species]["charge"]) ** 2 / eps0 / pparams[species]["mass"], - ) - * 1e-6 - ) - # alfvén frequency (Mrad/s) - pparams[species]["Omega_A"] = pparams[species]["v_A"] / transit_length * 1e-6 - # Larmor radius (m) - pparams[species]["rho_th"] = pparams[species]["v_th"] / (pparams[species]["Omega_c"] * 1e6) - # MHD length scale (m) - pparams[species]["v_A/Omega_c"] = pparams[species]["v_A"] / (np.abs(pparams[species]["Omega_c"]) * 1e6) - # dim-less ratios - pparams[species]["rho_th/L"] = pparams[species]["rho_th"] / transit_length - - if verbose: - print("\nSPECIES PARAMETERS:") - for species, ch in pparams.items(): - print(f"\nname:".ljust(26), species) - print(f"type:".ljust(25), ch["type"]) - ch.pop("type") - print(f"is bulk:".ljust(25), species == self.bulk_species()) - for kinds, vals in ch.items(): - print( - kinds.ljust(25), - "{:+4.3e}".format( - vals, - ), - units_affix[kinds], - ) - - return pparams + # # species dependent parameters + # self._pparams = {} + + # if len(self.fluid_species) > 0: + # for species, val in self.fluid_species.items(): + # self._pparams[species] = {} + # # type + # self._pparams[species]["type"] = "fluid" + # # mass (kg) + # self._pparams[species]["mass"] = val["params"]["phys_params"]["A"] * m_p + # # charge (C) + # self._pparams[species]["charge"] = val["params"]["phys_params"]["Z"] * e + # # density (m⁻³) + # self._pparams[species]["density"] = ( + # xp.mean( + # self.equil.n0( + # eta1, + # eta2, + # eta3, + # ) + # * xp.abs(det_tmp), + # ) + # * self.units.x ** 3 + # / plasma_volume + # * self.units.n + # ) + # # pressure (bar) + # self._pparams[species]["pressure"] = ( + # xp.mean( + # self.equil.p0( + # eta1, + # eta2, + # eta3, + # ) + # * xp.abs(det_tmp), + # ) + # * self.units.x ** 3 + # / plasma_volume + # * self.units.p + # * 1e-5 + # ) + # # thermal energy (keV) + # self._pparams[species]["kBT"] = self._pparams[species]["pressure"] * 1e5 / self._pparams[species]["density"] / e * 1e-3 + + # if len(self.kinetic) > 0: + # eta1mg, eta2mg, eta3mg = xp.meshgrid( + # eta1, + # eta2, + # eta3, + # indexing="ij", + # ) + + # for species, val in self.kinetic.items(): + # self._pparams[species] = {} + # # type + # self._pparams[species]["type"] = "kinetic" + # # mass (kg) + # self._pparams[species]["mass"] = val["params"]["phys_params"]["A"] * m_p + # # charge (C) + # self._pparams[species]["charge"] = val["params"]["phys_params"]["Z"] * e + + # # create temp kinetic object for (default) parameter extraction + # tmp_bckgr = val["params"]["background"] + + # if val["space"] != "ParticlesSPH": + # tmp = None + # for fi, maxw_params in tmp_bckgr.items(): + # if fi[-2] == "_": + # fi_type = fi[:-2] + # else: + # fi_type = fi + + # if tmp is None: + # tmp = getattr(maxwellians, fi_type)( + # maxw_params=maxw_params, + # equil=self.equil, + # ) + # else: + # tmp = tmp + getattr(maxwellians, fi_type)( + # maxw_params=maxw_params, + # equil=self.equil, + # ) + + # if val["space"] != "ParticlesSPH" and tmp.coords == "constants_of_motion": + # # call parameters + # a1 = self.domain.params_map["a1"] + # r = eta1mg * (1 - a1) + a1 + # psi = self.equil.psi_r(r) + + # # density (m⁻³) + # self._pparams[species]["density"] = ( + # xp.mean(tmp.n(psi) * xp.abs(det_tmp)) * self.units.x ** 3 / plasma_volume * self.units.n + # ) + # # thermal speed (m/s) + # self._pparams[species]["v_th"] = ( + # xp.mean(tmp.vth(psi) * xp.abs(det_tmp)) * self.units.x ** 3 / plasma_volume * self.units.v + # ) + # # thermal energy (keV) + # self._pparams[species]["kBT"] = self._pparams[species]["mass"] * self._pparams[species]["v_th"] ** 2 / e * 1e-3 + # # pressure (bar) + # self._pparams[species]["pressure"] = ( + # self._pparams[species]["kBT"] * e * 1e3 * self._pparams[species]["density"] * 1e-5 + # ) + + # else: + # # density (m⁻³) + # # self._pparams[species]['density'] = xp.mean(tmp.n( + # # eta1mg, eta2mg, eta3mg) * xp.abs(det_tmp)) * units['x']**3 / plasma_volume * units['n'] + # self._pparams[species]["density"] = 99.0 + # # thermal speeds (m/s) + # vth = [] + # # vths = tmp.vth(eta1mg, eta2mg, eta3mg) + # vths = [99.0] + # for k in range(len(vths)): + # vth += [ + # vths[k] * xp.abs(det_tmp) * self.units.x ** 3 / plasma_volume * self.units.v, + # ] + # thermal_speed = 0.0 + # for dir in range(val["obj"].vdim): + # # self._pparams[species]['vth' + str(dir + 1)] = xp.mean(vth[dir]) + # self._pparams[species]["vth" + str(dir + 1)] = 99.0 + # thermal_speed += self._pparams[species]["vth" + str(dir + 1)] + # # TODO: here it is assumed that background density parameter is called "n", + # # and that background thermal speeds are called "vthn"; make this a convention? + # # self._pparams[species]['v_th'] = thermal_speed / \ + # # val['obj'].vdim + # self._pparams[species]["v_th"] = 99.0 + # # thermal energy (keV) + # # self._pparams[species]['kBT'] = self._pparams[species]['mass'] * \ + # # self._pparams[species]['v_th']**2 / e * 1e-3 + # self._pparams[species]["kBT"] = 99.0 + # # pressure (bar) + # # self._pparams[species]['pressure'] = self._pparams[species]['kBT'] * \ + # # e * 1e3 * self._pparams[species]['density'] * 1e-5 + # self._pparams[species]["pressure"] = 99.0 + + # for species in self._pparams: + # # alfvén speed (m/s) + # self._pparams[species]["v_A"] = magnetic_field / xp.sqrt( + # mu0 * self._pparams[species]["mass"] * self._pparams[species]["density"], + # ) + # # thermal speed (m/s) + # self._pparams[species]["v_th"] = xp.sqrt( + # self._pparams[species]["kBT"] * 1e3 * e / self._pparams[species]["mass"], + # ) + # # thermal frequency (Mrad/s) + # self._pparams[species]["Omega_th"] = self._pparams[species]["v_th"] / transit_length * 1e-6 + # # cyclotron frequency (Mrad/s) + # self._pparams[species]["Omega_c"] = self._pparams[species]["charge"] * magnetic_field / self._pparams[species]["mass"] * 1e-6 + # # plasma frequency (Mrad/s) + # self._pparams[species]["Omega_p"] = ( + # xp.sqrt( + # self._pparams[species]["density"] * (self._pparams[species]["charge"]) ** 2 / eps0 / self._pparams[species]["mass"], + # ) + # * 1e-6 + # ) + # # alfvén frequency (Mrad/s) + # self._pparams[species]["Omega_A"] = self._pparams[species]["v_A"] / transit_length * 1e-6 + # # Larmor radius (m) + # self._pparams[species]["rho_th"] = self._pparams[species]["v_th"] / (self._pparams[species]["Omega_c"] * 1e6) + # # MHD length scale (m) + # self._pparams[species]["v_A/Omega_c"] = self._pparams[species]["v_A"] / (xp.abs(self._pparams[species]["Omega_c"]) * 1e6) + # # dim-less ratios + # self._pparams[species]["rho_th/L"] = self._pparams[species]["rho_th"] / transit_length + + # if verbose and self.rank_world == 0: + # print("\nSPECIES PARAMETERS:") + # for species, ch in self._pparams.items(): + # print(f"\nname:".ljust(26), species) + # print(f"type:".ljust(25), ch["type"]) + # ch.pop("type") + # print(f"is bulk:".ljust(25), species == self.bulk_species()) + # for kinds, vals in ch.items(): + # print( + # kinds.ljust(25), + # "{:+4.3e}".format( + # vals, + # ), + # units_affix[kinds], + # ) class MyDumper(yaml.SafeDumper): diff --git a/src/struphy/models/fluid.py b/src/struphy/models/fluid.py index 04f5fca1c..2b832ba00 100644 --- a/src/struphy/models/fluid.py +++ b/src/struphy/models/fluid.py @@ -1,6 +1,17 @@ +import cunumpy as xp +from psydac.ddm.mpi import mpi as MPI +from psydac.linalg.block import BlockVector +from psydac.linalg.stencil import StencilVector + +from struphy.feec.projectors import L2Projector +from struphy.feec.variational_utilities import H1vecMassMatrix_density, InternalEnergyEvaluator from struphy.models.base import StruphyModel +from struphy.models.species import DiagnosticSpecies, FieldSpecies, FluidSpecies, ParticleSpecies +from struphy.models.variables import FEECVariable, PICVariable, SPHVariable, Variable +from struphy.polar.basic import PolarVector from struphy.propagators import propagators_coupling, propagators_fields, propagators_markers -from struphy.utils.arrays import xp as np + +rank = MPI.COMM_WORLD.Get_rank() class LinearMHD(StruphyModel): @@ -31,91 +42,51 @@ class LinearMHD(StruphyModel): 1. :class:`~struphy.propagators.propagators_fields.ShearAlfven` 2. :class:`~struphy.propagators.propagators_fields.Magnetosonic` - - :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} + ## species - dct["em_fields"]["b_field"] = "Hdiv" - dct["fluid"]["mhd"] = {"density": "L2", "velocity": "Hdiv", "pressure": "L2"} - return dct + class EMFields(FieldSpecies): + def __init__(self): + self.b_field = FEECVariable(space="Hdiv") + self.init_variables() - @staticmethod - def bulk_species(): - return "mhd" + class MHD(FluidSpecies): + def __init__(self): + self.density = FEECVariable(space="L2") + self.velocity = FEECVariable(space="Hdiv") + self.pressure = FEECVariable(space="L2") + self.init_variables() - @staticmethod - def velocity_scale(): - return "alfvén" + ## propagators - @staticmethod - def propagators_dct(): - return { - propagators_fields.ShearAlfven: ["mhd_velocity", "b_field"], - propagators_fields.Magnetosonic: ["mhd_density", "mhd_velocity", "mhd_pressure"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - # add special options - @classmethod - def options(cls): - dct = super().options() - cls.add_option( - species=["fluid", "mhd"], - key="u_space", - option="Hdiv", - dct=dct, - ) - return dct + class Propagators: + def __init__(self): + self.shear_alf = propagators_fields.ShearAlfven() + self.mag_sonic = propagators_fields.Magnetosonic() - def __init__(self, params, comm, clone_config=None): - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) + ## abstract methods - from struphy.polar.basic import PolarVector + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") - # extract necessary parameters - u_space = params["fluid"]["mhd"]["options"]["u_space"] - alfven_solver = params["fluid"]["mhd"]["options"]["ShearAlfven"]["solver"] - alfven_algo = params["fluid"]["mhd"]["options"]["ShearAlfven"]["algo"] - sonic_solver = params["fluid"]["mhd"]["options"]["Magnetosonic"]["solver"] + # 1. instantiate all species + self.em_fields = self.EMFields() + self.mhd = self.MHD() - # project background magnetic field (2-form) and pressure (3-form) - self._b_eq = self.projected_equil.b2 - self._p_eq = self.projected_equil.p3 - self._ones = self._p_eq.space.zeros() + # 2. instantiate all propagators + self.propagators = self.Propagators() - if isinstance(self._ones, PolarVector): - self._ones.tp[:] = 1.0 - else: - self._ones[:] = 1.0 - - # set keyword arguments for propagators - self._kwargs[propagators_fields.ShearAlfven] = { - "u_space": u_space, - "solver": alfven_solver, - "algo": alfven_algo, - } - - self._kwargs[propagators_fields.Magnetosonic] = { - "b": self.pointer["b_field"], - "u_space": u_space, - "solver": sonic_solver, - } + # 3. assign variables to propagators + self.propagators.shear_alf.variables.u = self.mhd.velocity + self.propagators.shear_alf.variables.b = self.em_fields.b_field - # Initialize propagators used in splitting substeps - self.init_propagators() + self.propagators.mag_sonic.variables.n = self.mhd.density + self.propagators.mag_sonic.variables.u = self.mhd.velocity + self.propagators.mag_sonic.variables.p = self.mhd.pressure - # Scalar variables to be saved during simulation + # define scalars for update_scalar_quantities self.add_scalar("en_U") self.add_scalar("en_p") self.add_scalar("en_B") @@ -124,15 +95,35 @@ def __init__(self, params, comm, clone_config=None): self.add_scalar("en_B_tot") self.add_scalar("en_tot") - # vectors for computing scalar quantities - self._tmp_b1 = self.derham.Vh["2"].zeros() - self._tmp_b2 = self.derham.Vh["2"].zeros() + @property + def bulk_species(self): + return self.mhd + + @property + def velocity_scale(self): + return "alfvén" + + def allocate_helpers(self): + self._ones = self.projected_equil.p3.space.zeros() + if isinstance(self._ones, PolarVector): + self._ones.tp[:] = 1.0 + else: + self._ones[:] = 1.0 + + self._tmp_b1: BlockVector = self.derham.Vh["2"].zeros() # TODO: replace derham.Vh dict by class + self._tmp_b2: BlockVector = self.derham.Vh["2"].zeros() def update_scalar_quantities(self): # perturbed fields - en_U = 0.5 * self.mass_ops.M2n.dot_inner(self.pointer["mhd_velocity"], self.pointer["mhd_velocity"]) - en_B = 0.5 * self.mass_ops.M2.dot_inner(self.pointer["b_field"], self.pointer["b_field"]) - en_p = self.pointer["mhd_pressure"].inner(self._ones) / (5 / 3 - 1) + en_U = 0.5 * self.mass_ops.M2n.dot_inner( + self.mhd.velocity.spline.vector, + self.mhd.velocity.spline.vector, + ) + en_B = 0.5 * self.mass_ops.M2.dot_inner( + self.em_fields.b_field.spline.vector, + self.em_fields.b_field.spline.vector, + ) + en_p = self.mhd.pressure.spline.vector.inner(self._ones) / (5 / 3 - 1) self.update_scalar("en_U", en_U) self.update_scalar("en_B", en_B) @@ -140,17 +131,17 @@ def update_scalar_quantities(self): self.update_scalar("en_tot", en_U + en_B + en_p) # background fields - self.mass_ops.M2.dot(self._b_eq, apply_bc=False, out=self._tmp_b1) + self.mass_ops.M2.dot(self.projected_equil.b2, apply_bc=False, out=self._tmp_b1) - en_B0 = self._b_eq.inner(self._tmp_b1) / 2 - en_p0 = self._p_eq.inner(self._ones) / (5 / 3 - 1) + en_B0 = self.projected_equil.b2.inner(self._tmp_b1) / 2 + en_p0 = self.projected_equil.p3.inner(self._ones) / (5 / 3 - 1) self.update_scalar("en_B_eq", en_B0) self.update_scalar("en_p_eq", en_p0) # total magnetic field - self._b_eq.copy(out=self._tmp_b1) - self._tmp_b1 += self.pointer["b_field"] + self.projected_equil.b2.copy(out=self._tmp_b1) + self._tmp_b1 += self.em_fields.b_field.spline.vector self.mass_ops.M2.dot(self._tmp_b1, apply_bc=False, out=self._tmp_b2) @@ -158,6 +149,23 @@ def update_scalar_quantities(self): self.update_scalar("en_B_tot", en_Btot) + ## default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "mag_sonic.Options" in line: + new_file += [ + "model.propagators.mag_sonic.options = model.propagators.mag_sonic.Options(b_field=model.em_fields.b_field)\n" + ] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) + class LinearExtendedMHDuniform(StruphyModel): r"""Linear extended MHD with zero-flow equilibrium (:math:`\mathbf U_0 = 0`). @@ -198,87 +206,52 @@ class LinearExtendedMHDuniform(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} + ## species - dct["em_fields"]["b_field"] = "Hcurl" - dct["fluid"]["mhd"] = { - "rho": "L2", - "u": "Hdiv", - "p": "L2", - } - return dct + class EMFields(FieldSpecies): + def __init__(self): + self.b_field = FEECVariable(space="Hcurl") + self.init_variables() - @staticmethod - def bulk_species(): - return "mhd" + class MHD(FluidSpecies): + def __init__(self): + self.density = FEECVariable(space="L2") + self.velocity = FEECVariable(space="Hdiv") + self.pressure = FEECVariable(space="L2") + self.init_variables() - @staticmethod - def velocity_scale(): - return "alfvén" + ## propagators - @staticmethod - def propagators_dct(): - return { - propagators_fields.ShearAlfvenB1: ["mhd_u", "b_field"], - propagators_fields.Hall: ["b_field"], - propagators_fields.MagnetosonicUniform: ["mhd_rho", "mhd_u", "mhd_p"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - def __init__(self, params, comm, clone_config=None): - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) - - from struphy.polar.basic import PolarVector - - # extract necessary parameters - alfven_solver = params["fluid"]["mhd"]["options"]["ShearAlfvenB1"]["solver"] - M1_inv = params["fluid"]["mhd"]["options"]["ShearAlfvenB1"]["solver_M1"] - hall_solver = params["em_fields"]["options"]["Hall"]["solver"] - sonic_solver = params["fluid"]["mhd"]["options"]["MagnetosonicUniform"]["solver"] - - # project background magnetic field (1-form) and pressure (3-form) - self._b_eq = self.projected_equil.b1 - self._a_eq = self.projected_equil.a1 - self._p_eq = self.projected_equil.p3 - self._ones = self.pointer["mhd_p"].space.zeros() + class Propagators: + def __init__(self): + self.shear_alf = propagators_fields.ShearAlfvenB1() + self.hall = propagators_fields.Hall() + self.mag_sonic = propagators_fields.MagnetosonicUniform() - if isinstance(self._ones, PolarVector): - self._ones.tp[:] = 1.0 - else: - self._ones[:] = 1.0 + ## abstract methods - # compute coupling parameters - epsilon = self.equation_params["mhd"]["epsilon"] + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") - if abs(epsilon - 1) < 1e-6: - epsilon = 1.0 + # 1. instantiate all species + self.em_fields = self.EMFields() + self.mhd = self.MHD() - # set keyword arguments for propagators - self._kwargs[propagators_fields.ShearAlfvenB1] = { - "solver": alfven_solver, - "solver_M1": M1_inv, - } + # 2. instantiate all propagators + self.propagators = self.Propagators() - self._kwargs[propagators_fields.Hall] = { - "solver": hall_solver, - "epsilon": epsilon, - } + # 3. assign variables to propagators + self.propagators.shear_alf.variables.u = self.mhd.velocity + self.propagators.shear_alf.variables.b = self.em_fields.b_field - self._kwargs[propagators_fields.MagnetosonicUniform] = {"solver": sonic_solver} + self.propagators.hall.variables.b = self.em_fields.b_field - # Initialize propagators used in splitting substeps - self.init_propagators() + self.propagators.mag_sonic.variables.n = self.mhd.density + self.propagators.mag_sonic.variables.u = self.mhd.velocity + self.propagators.mag_sonic.variables.p = self.mhd.pressure - # Scalar variables to be saved during simulation + # define scalars for update_scalar_quantities self.add_scalar("en_U") self.add_scalar("en_p") self.add_scalar("en_B") @@ -288,17 +261,45 @@ def __init__(self, params, comm, clone_config=None): self.add_scalar("en_tot") self.add_scalar("helicity") - # temporary vectors for scalar quantities - self._tmp_b1 = self.derham.Vh["1"].zeros() - self._tmp_b2 = self.derham.Vh["1"].zeros() + @property + def bulk_species(self): + return self.mhd + + @property + def velocity_scale(self): + return "alfvén" + + def allocate_helpers(self): + self._b_eq = self.projected_equil.b1 + self._a_eq = self.projected_equil.a1 + self._p_eq = self.projected_equil.p3 + + self._ones = self.projected_equil.p3.space.zeros() + if isinstance(self._ones, PolarVector): + self._ones.tp[:] = 1.0 + else: + self._ones[:] = 1.0 + + self._tmp_b1: BlockVector = self.derham.Vh["1"].zeros() # TODO: replace derham.Vh dict by class + self._tmp_b2: BlockVector = self.derham.Vh["1"].zeros() + + # adjust coupling parameters + epsilon = self.mhd.equation_params.epsilon + + if abs(epsilon - 1) < 1e-6: + self.mhd.equation_params.epsilon = 1.0 def update_scalar_quantities(self): # perturbed fields - en_U = 0.5 * self.mass_ops.M2n.dot_inner(self.pointer["mhd_u"], self.pointer["mhd_u"]) - b1 = self.mass_ops.M1.dot(self.pointer["b_field"], out=self._tmp_b1) - en_B = 0.5 * self.pointer["b_field"].inner(b1) + u = self.mhd.velocity.spline.vector + p = self.mhd.pressure.spline.vector + b = self.em_fields.b_field.spline.vector + + en_U = 0.5 * self.mass_ops.M2n.dot_inner(u, u) + b1 = self.mass_ops.M1.dot(b, out=self._tmp_b1) + en_B = 0.5 * b.inner(b1) helicity = 2.0 * self._a_eq.inner(b1) - en_p_i = self.pointer["mhd_p"].inner(self._ones) / (5.0 / 3.0 - 1.0) + en_p_i = p.inner(self._ones) / (5.0 / 3.0 - 1.0) self.update_scalar("en_U", en_U) self.update_scalar("en_B", en_B) @@ -316,13 +317,30 @@ def update_scalar_quantities(self): # total magnetic field b1 = self._b_eq.copy(out=self._tmp_b1) - self._tmp_b1 += self.pointer["b_field"] + self._tmp_b1 += b b2 = self.mass_ops.M1.dot(b1, apply_bc=False, out=self._tmp_b2) en_Btot = b1.inner(b2) / 2.0 self.update_scalar("en_B_tot", en_Btot) + # default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "hall.Options" in line: + new_file += [ + "model.propagators.hall.options = model.propagators.hall.Options(epsilon_from=model.mhd)\n" + ] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) + class ColdPlasma(StruphyModel): r"""Cold plasma model. @@ -359,82 +377,74 @@ class ColdPlasma(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} + ## species - dct["em_fields"]["e_field"] = "Hcurl" - dct["em_fields"]["b_field"] = "Hdiv" - dct["fluid"]["electrons"] = {"j": "Hcurl"} - return dct + class EMFields(FieldSpecies): + def __init__(self): + self.e_field = FEECVariable(space="Hcurl") + self.b_field = FEECVariable(space="Hdiv") + self.init_variables() - @staticmethod - def bulk_species(): - return "electrons" + class Electrons(FluidSpecies): + def __init__(self): + self.current = FEECVariable(space="Hcurl") + self.init_variables() - @staticmethod - def velocity_scale(): - return "light" + ## propagators + + class Propagators: + def __init__(self): + self.maxwell = propagators_fields.Maxwell() + self.ohm = propagators_fields.OhmCold() + self.jxb = propagators_fields.JxBCold() + + ## abstract methods + + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + + # 1. instantiate all species + self.em_fields = self.EMFields() + self.electrons = self.Electrons() - @staticmethod - def propagators_dct(): - return { - propagators_fields.Maxwell: ["e_field", "b_field"], - propagators_fields.OhmCold: ["electrons_j", "e_field"], - propagators_fields.JxBCold: ["electrons_j"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - def __init__(self, params, comm, clone_config=None): - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) - - # model parameters - self._alpha = self.equation_params["electrons"]["alpha"] - self._epsilon = self.equation_params["electrons"]["epsilon"] - - # solver parameters - params_maxwell = params["em_fields"]["options"]["Maxwell"]["solver"] - params_ohmcold = params["fluid"]["electrons"]["options"]["OhmCold"]["solver"] - params_jxbcold = params["fluid"]["electrons"]["options"]["JxBCold"]["solver"] - - # set keyword arguments for propagators - self._kwargs[propagators_fields.Maxwell] = {"solver": params_maxwell} - - self._kwargs[propagators_fields.OhmCold] = { - "alpha": self._alpha, - "epsilon": self._epsilon, - "solver": params_ohmcold, - } - - self._kwargs[propagators_fields.JxBCold] = { - "epsilon": self._epsilon, - "solver": params_jxbcold, - } - - # Initialize propagators used in splitting substeps - self.init_propagators() - - # Scalar variables to be saved during simulation + # 2. instantiate all propagators + self.propagators = self.Propagators() + + # 3. assign variables to propagators + self.propagators.maxwell.variables.e = self.em_fields.e_field + self.propagators.maxwell.variables.b = self.em_fields.b_field + + self.propagators.ohm.variables.j = self.electrons.current + self.propagators.ohm.variables.e = self.em_fields.e_field + + self.propagators.jxb.variables.j = self.electrons.current + + # define scalars for update_scalar_quantities self.add_scalar("electric energy") self.add_scalar("magnetic energy") self.add_scalar("kinetic energy") self.add_scalar("total energy") + @property + def bulk_species(self): + return self.electrons + + @property + def velocity_scale(self): + return "light" + + def allocate_helpers(self): + self._alpha = self.electrons.equation_params.alpha + def update_scalar_quantities(self): - en_E = 0.5 * self.mass_ops.M1.dot_inner(self.pointer["e_field"], self.pointer["e_field"]) - en_B = 0.5 * self.mass_ops.M2.dot_inner(self.pointer["b_field"], self.pointer["b_field"]) - en_J = ( - 0.5 - * self._alpha**2 - * self.mass_ops.M1ninv.dot_inner(self.pointer["electrons_j"], self.pointer["electrons_j"]) - ) + e = self.em_fields.e_field.spline.vector + b = self.em_fields.b_field.spline.vector + j = self.electrons.current.spline.vector + + en_E = 0.5 * self.mass_ops.M1.dot_inner(e, e) + en_B = 0.5 * self.mass_ops.M2.dot_inner(b, b) + en_J = 0.5 * self._alpha**2 * self.mass_ops.M1ninv.dot_inner(j, j) self.update_scalar("electric energy", en_E) self.update_scalar("magnetic energy", en_B) @@ -442,7 +452,7 @@ def update_scalar_quantities(self): self.update_scalar("total energy", en_E + en_B + en_J) -class ViscoresistiveMHD(StruphyModel): +class ViscoResistiveMHD(StruphyModel): r"""Full (non-linear) visco-resistive MHD equations discretized with a variational method. :ref:`normalization`: @@ -478,139 +488,73 @@ class ViscoresistiveMHD(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} - dct["em_fields"]["b2"] = "Hdiv" - dct["fluid"]["mhd"] = {"rho3": "L2", "s3": "L2", "uv": "H1vec"} - return dct - - @staticmethod - def bulk_species(): - return "mhd" - - @staticmethod - def velocity_scale(): - return "alfvén" + ## species + + class EMFields(FieldSpecies): + def __init__(self): + self.b_field = FEECVariable(space="Hdiv") + self.init_variables() + + class MHD(FluidSpecies): + def __init__(self): + self.density = FEECVariable(space="L2") + self.velocity = FEECVariable(space="H1vec") + self.entropy = FEECVariable(space="L2") + self.init_variables() + + ## propagators + + class Propagators: + def __init__( + self, + with_viscosity: bool = True, + with_resistivity: bool = True, + ): + self.variat_dens = propagators_fields.VariationalDensityEvolve() + self.variat_mom = propagators_fields.VariationalMomentumAdvection() + self.variat_ent = propagators_fields.VariationalEntropyEvolve() + self.variat_mag = propagators_fields.VariationalMagFieldEvolve() + if with_viscosity: + self.variat_viscous = propagators_fields.VariationalViscosity() + if with_resistivity: + self.variat_resist = propagators_fields.VariationalResistivity() + + ## abstract methods + + def __init__( + self, + with_viscosity: bool = True, + with_resistivity: bool = True, + ): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + + # 1. instantiate all species + self.em_fields = self.EMFields() + self.mhd = self.MHD() + + # 2. instantiate all propagators + self.propagators = self.Propagators( + with_viscosity=with_viscosity, + with_resistivity=with_resistivity, + ) - @staticmethod - def propagators_dct(): - return { - propagators_fields.VariationalDensityEvolve: ["mhd_rho3", "mhd_uv"], - propagators_fields.VariationalMomentumAdvection: ["mhd_uv"], - propagators_fields.VariationalEntropyEvolve: ["mhd_s3", "mhd_uv"], - propagators_fields.VariationalMagFieldEvolve: ["b2", "mhd_uv"], - propagators_fields.VariationalViscosity: ["mhd_s3", "mhd_uv"], - propagators_fields.VariationalResistivity: ["mhd_s3", "b2"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - def __init__(self, params, comm, clone_config=None): - from struphy.feec.projectors import L2Projector - from struphy.feec.variational_utilities import H1vecMassMatrix_density, InternalEnergyEvaluator - from struphy.polar.basic import PolarVector - - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) - - self.WMM = H1vecMassMatrix_density(self.derham, self.mass_ops, self.domain) - - # Initialize propagators/integrators used in splitting substeps - lin_solver_momentum = params["fluid"]["mhd"]["options"]["VariationalMomentumAdvection"]["lin_solver"] - nonlin_solver_momentum = params["fluid"]["mhd"]["options"]["VariationalMomentumAdvection"]["nonlin_solver"] - lin_solver_density = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["lin_solver"] - nonlin_solver_density = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["nonlin_solver"] - lin_solver_entropy = params["fluid"]["mhd"]["options"]["VariationalEntropyEvolve"]["lin_solver"] - nonlin_solver_entropy = params["fluid"]["mhd"]["options"]["VariationalEntropyEvolve"]["nonlin_solver"] - lin_solver_magfield = params["em_fields"]["options"]["VariationalMagFieldEvolve"]["lin_solver"] - nonlin_solver_magfield = params["em_fields"]["options"]["VariationalMagFieldEvolve"]["nonlin_solver"] - lin_solver_viscosity = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["lin_solver"] - nonlin_solver_viscosity = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["nonlin_solver"] - lin_solver_resistivity = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["lin_solver"] - nonlin_solver_resistivity = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["nonlin_solver"] - if "linearize_current" in params["fluid"]["mhd"]["options"]["VariationalResistivity"].keys(): - self._linearize_current = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["linearize_current"] - else: - self._linearize_current = False - self._gamma = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["physics"]["gamma"] - self._mu = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["mu"] - self._mu_a = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["mu_a"] - self._alpha = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["alpha"] - self._eta = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["physics"]["eta"] - self._eta_a = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["physics"]["eta_a"] - model = "full" - - self._energy_evaluator = InternalEnergyEvaluator(self.derham, self._gamma) - - # set keyword arguments for propagators - self._kwargs[propagators_fields.VariationalDensityEvolve] = { - "model": model, - "s": self.pointer["mhd_s3"], - "gamma": self._gamma, - "mass_ops": self.WMM, - "lin_solver": lin_solver_density, - "nonlin_solver": nonlin_solver_density, - "energy_evaluator": self._energy_evaluator, - } - - self._kwargs[propagators_fields.VariationalMomentumAdvection] = { - "mass_ops": self.WMM, - "lin_solver": lin_solver_momentum, - "nonlin_solver": nonlin_solver_momentum, - } - - self._kwargs[propagators_fields.VariationalEntropyEvolve] = { - "model": model, - "rho": self.pointer["mhd_rho3"], - "gamma": self._gamma, - "mass_ops": self.WMM, - "lin_solver": lin_solver_entropy, - "nonlin_solver": nonlin_solver_entropy, - "energy_evaluator": self._energy_evaluator, - } - - self._kwargs[propagators_fields.VariationalMagFieldEvolve] = { - "model": model, - "mass_ops": self.WMM, - "lin_solver": lin_solver_magfield, - "nonlin_solver": nonlin_solver_magfield, - } - - self._kwargs[propagators_fields.VariationalViscosity] = { - "model": model, - "rho": self.pointer["mhd_rho3"], - "gamma": self._gamma, - "mu": self._mu, - "mu_a": self._mu_a, - "alpha": self._alpha, - "mass_ops": self.WMM, - "lin_solver": lin_solver_viscosity, - "nonlin_solver": nonlin_solver_viscosity, - "energy_evaluator": self._energy_evaluator, - } - - self._kwargs[propagators_fields.VariationalResistivity] = { - "model": model, - "rho": self.pointer["mhd_rho3"], - "gamma": self._gamma, - "eta": self._eta, - "eta_a": self._eta_a, - "lin_solver": lin_solver_resistivity, - "nonlin_solver": nonlin_solver_resistivity, - "linearize_current": self._linearize_current, - "energy_evaluator": self._energy_evaluator, - } - - # Initialize propagators used in splitting substeps - self.init_propagators() - - # Scalar variables to be saved during simulation + # 3. assign variables to propagators + self.propagators.variat_dens.variables.rho = self.mhd.density + self.propagators.variat_dens.variables.u = self.mhd.velocity + self.propagators.variat_mom.variables.u = self.mhd.velocity + self.propagators.variat_ent.variables.s = self.mhd.entropy + self.propagators.variat_ent.variables.u = self.mhd.velocity + self.propagators.variat_mag.variables.u = self.mhd.velocity + self.propagators.variat_mag.variables.b = self.em_fields.b_field + if with_viscosity: + self.propagators.variat_viscous.variables.s = self.mhd.entropy + self.propagators.variat_viscous.variables.u = self.mhd.velocity + if with_resistivity: + self.propagators.variat_resist.variables.s = self.mhd.entropy + self.propagators.variat_resist.variables.b = self.em_fields.b_field + + # define scalars for update_scalar_quantities self.add_scalar("en_U") self.add_scalar("en_thermo") self.add_scalar("en_mag") @@ -619,16 +563,24 @@ def __init__(self, params, comm, clone_config=None): self.add_scalar("entr_tot") self.add_scalar("tot_div_B") - # temporary vectors for scalar quantities - self._tmp_div_B = self.derham.Vh_pol["3"].zeros() - tmp_dof = self.derham.Vh_pol["3"].zeros() - projV3 = L2Projector("L2", self.mass_ops) + @property + def bulk_species(self): + return self.mhd + + @property + def velocity_scale(self): + return "alfvén" + + def allocate_helpers(self): + projV3 = L2Projector("L2", self._mass_ops) def f(e1, e2, e3): return 1 - f = np.vectorize(f) - self._integrator = projV3(f, dofs=tmp_dof) + f = xp.vectorize(f) + self._integrator = projV3(f) + + self._energy_evaluator = InternalEnergyEvaluator(self.derham, self.propagators.variat_ent.options.gamma) self._ones = self.derham.Vh_pol["3"].zeros() if isinstance(self._ones, PolarVector): @@ -636,12 +588,18 @@ def f(e1, e2, e3): else: self._ones[:] = 1.0 + self._tmp_div_B = self.derham.Vh_pol["3"].zeros() + def update_scalar_quantities(self): - # Update mass matrix - en_U = 0.5 * self.WMM.massop.dot_inner(self.pointer["mhd_uv"], self.pointer["mhd_uv"]) + rho = self.mhd.density.spline.vector + u = self.mhd.velocity.spline.vector + s = self.mhd.entropy.spline.vector + b = self.em_fields.b_field.spline.vector + + en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) - en_mag = 0.5 * self.mass_ops.M2.dot_inner(self.pointer["b2"], self.pointer["b2"]) + en_mag = 0.5 * self.mass_ops.M2.dot_inner(b, b) self.update_scalar("en_mag", en_mag) en_thermo = self.update_thermo_energy() @@ -649,12 +607,12 @@ def update_scalar_quantities(self): en_tot = en_U + en_thermo + en_mag self.update_scalar("en_tot", en_tot) - dens_tot = self._ones.inner(self.pointer["mhd_rho3"]) + dens_tot = self._ones.inner(rho) self.update_scalar("dens_tot", dens_tot) - entr_tot = self._ones.inner(self.pointer["mhd_s3"]) + entr_tot = self._ones.inner(s) self.update_scalar("entr_tot", entr_tot) - div_B = self.derham.div.dot(self.pointer["b2"], out=self._tmp_div_B) + div_B = self.derham.div.dot(b, out=self._tmp_div_B) L2_div_B = self._mass_ops.M3.dot_inner(div_B, div_B) self.update_scalar("tot_div_B", L2_div_B) @@ -663,9 +621,12 @@ def update_thermo_energy(self): :meta private: """ - en_prop = self._propagators[0] - self._energy_evaluator.sf.vector = self.pointer["mhd_s3"] - self._energy_evaluator.rhof.vector = self.pointer["mhd_rho3"] + rho = self.mhd.density.spline.vector + s = self.mhd.entropy.spline.vector + en_prop = self.propagators.variat_dens + + self._energy_evaluator.sf.vector = s + self._energy_evaluator.rhof.vector = rho sf_values = self._energy_evaluator.sf.eval_tp_fixed_loc( self._energy_evaluator.integration_grid_spans, self._energy_evaluator.integration_grid_bd, @@ -683,6 +644,44 @@ def update_thermo_energy(self): self.update_scalar("en_thermo", en_thermo) return en_thermo + # default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "variat_dens.Options" in line: + new_file += [ + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='full',\n" + ] + new_file += [ + " s=model.mhd.entropy)\n" + ] + elif "variat_ent.Options" in line: + new_file += [ + "model.propagators.variat_ent.options = model.propagators.variat_ent.Options(model='full',\n" + ] + new_file += [ + " rho=model.mhd.density)\n" + ] + elif "variat_viscous.Options" in line: + new_file += [ + "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(rho=model.mhd.density)\n" + ] + elif "variat_resist.Options" in line: + new_file += [ + "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(rho=model.mhd.density)\n" + ] + elif "entropy.add_background" in line: + new_file += ["model.mhd.density.add_background(FieldsBackground())\n"] + new_file += [line] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) + class ViscousFluid(StruphyModel): r"""Full (non-linear) viscous Navier-Stokes equations discretized with a variational method. @@ -716,122 +715,72 @@ class ViscousFluid(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} - dct["fluid"]["fluid"] = {"rho3": "L2", "s3": "L2", "uv": "H1vec"} - return dct + ## species - @staticmethod - def bulk_species(): - return "fluid" + class Fluid(FluidSpecies): + def __init__(self): + self.density = FEECVariable(space="L2") + self.velocity = FEECVariable(space="H1vec") + self.entropy = FEECVariable(space="L2") + self.init_variables() - @staticmethod - def velocity_scale(): - return "alfvén" + ## propagators + + class Propagators: + def __init__(self, with_viscosity: bool = True): + self.variat_dens = propagators_fields.VariationalDensityEvolve() + self.variat_mom = propagators_fields.VariationalMomentumAdvection() + self.variat_ent = propagators_fields.VariationalEntropyEvolve() + if with_viscosity: + self.variat_viscous = propagators_fields.VariationalViscosity() + + ## abstract methods + + def __init__(self, with_viscosity: bool = True): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") - @staticmethod - def propagators_dct(): - return { - propagators_fields.VariationalDensityEvolve: ["fluid_rho3", "fluid_uv"], - propagators_fields.VariationalMomentumAdvection: ["fluid_uv"], - propagators_fields.VariationalEntropyEvolve: ["fluid_s3", "fluid_uv"], - propagators_fields.VariationalViscosity: ["fluid_s3", "fluid_uv"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - def __init__(self, params, comm, clone_config=None): - from struphy.feec.projectors import L2Projector - from struphy.polar.basic import PolarVector - - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) - - from struphy.feec.variational_utilities import H1vecMassMatrix_density, InternalEnergyEvaluator - - self.WMM = H1vecMassMatrix_density(self.derham, self.mass_ops, self.domain) - - # Initialize propagators/integrators used in splitting substeps - lin_solver_momentum = params["fluid"]["fluid"]["options"]["VariationalMomentumAdvection"]["lin_solver"] - nonlin_solver_momentum = params["fluid"]["fluid"]["options"]["VariationalMomentumAdvection"]["nonlin_solver"] - lin_solver_density = params["fluid"]["fluid"]["options"]["VariationalDensityEvolve"]["lin_solver"] - nonlin_solver_density = params["fluid"]["fluid"]["options"]["VariationalDensityEvolve"]["nonlin_solver"] - lin_solver_entropy = params["fluid"]["fluid"]["options"]["VariationalEntropyEvolve"]["lin_solver"] - nonlin_solver_entropy = params["fluid"]["fluid"]["options"]["VariationalEntropyEvolve"]["nonlin_solver"] - lin_solver_viscosity = params["fluid"]["fluid"]["options"]["VariationalViscosity"]["lin_solver"] - nonlin_solver_viscosity = params["fluid"]["fluid"]["options"]["VariationalViscosity"]["nonlin_solver"] - - self._gamma = params["fluid"]["fluid"]["options"]["VariationalDensityEvolve"]["physics"]["gamma"] - self._mu = params["fluid"]["fluid"]["options"]["VariationalViscosity"]["physics"]["mu"] - self._mu_a = params["fluid"]["fluid"]["options"]["VariationalViscosity"]["physics"]["mu_a"] - model = "full" - - self._energy_evaluator = InternalEnergyEvaluator(self.derham, self._gamma) - - # set keyword arguments for propagators - self._kwargs[propagators_fields.VariationalDensityEvolve] = { - "model": model, - "s": self.pointer["fluid_s3"], - "gamma": self._gamma, - "mass_ops": self.WMM, - "lin_solver": lin_solver_density, - "nonlin_solver": nonlin_solver_density, - "energy_evaluator": self._energy_evaluator, - } - - self._kwargs[propagators_fields.VariationalMomentumAdvection] = { - "mass_ops": self.WMM, - "lin_solver": lin_solver_momentum, - "nonlin_solver": nonlin_solver_momentum, - } - - self._kwargs[propagators_fields.VariationalEntropyEvolve] = { - "model": model, - "rho": self.pointer["fluid_rho3"], - "gamma": self._gamma, - "mass_ops": self.WMM, - "lin_solver": lin_solver_entropy, - "nonlin_solver": nonlin_solver_entropy, - "energy_evaluator": self._energy_evaluator, - } - - self._kwargs[propagators_fields.VariationalViscosity] = { - "model": model, - "gamma": self._gamma, - "rho": self.pointer["fluid_rho3"], - "mu": self._mu, - "mu_a": self._mu_a, - "mass_ops": self.WMM, - "lin_solver": lin_solver_viscosity, - "nonlin_solver": nonlin_solver_viscosity, - "energy_evaluator": self._energy_evaluator, - } - - # Initialize propagators used in splitting substeps - self.init_propagators() - - # Scalar variables to be saved during simulation + # 1. instantiate all species + self.fluid = self.Fluid() + + # 2. instantiate all propagators + self.propagators = self.Propagators(with_viscosity=with_viscosity) + + # 3. assign variables to propagators + self.propagators.variat_dens.variables.rho = self.fluid.density + self.propagators.variat_dens.variables.u = self.fluid.velocity + self.propagators.variat_mom.variables.u = self.fluid.velocity + self.propagators.variat_ent.variables.s = self.fluid.entropy + self.propagators.variat_ent.variables.u = self.fluid.velocity + if with_viscosity: + self.propagators.variat_viscous.variables.s = self.fluid.entropy + self.propagators.variat_viscous.variables.u = self.fluid.velocity + + # define scalars for update_scalar_quantities self.add_scalar("en_U") self.add_scalar("en_thermo") self.add_scalar("en_tot") self.add_scalar("dens_tot") self.add_scalar("entr_tot") - # temporary vectors for scalar quantities - tmp_dof = self.derham.Vh_pol["3"].zeros() - projV3 = L2Projector("L2", self.mass_ops) + @property + def bulk_species(self): + return self.fluid + + @property + def velocity_scale(self): + return "alfvén" + + def allocate_helpers(self): + projV3 = L2Projector("L2", self._mass_ops) def f(e1, e2, e3): return 1 - f = np.vectorize(f) - self._integrator = projV3(f, dofs=tmp_dof) + f = xp.vectorize(f) + self._integrator = projV3(f) + + self._energy_evaluator = InternalEnergyEvaluator(self.derham, self.propagators.variat_ent.options.gamma) self._ones = self.derham.Vh_pol["3"].zeros() if isinstance(self._ones, PolarVector): @@ -840,8 +789,11 @@ def f(e1, e2, e3): self._ones[:] = 1.0 def update_scalar_quantities(self): - # Update mass matrix - en_U = 0.5 * self.WMM.massop.dot_inner(self.pointer["fluid_uv"], self.pointer["fluid_uv"]) + rho = self.fluid.density.spline.vector + u = self.fluid.velocity.spline.vector + s = self.fluid.entropy.spline.vector + + en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) en_thermo = self.update_thermo_energy() @@ -849,9 +801,9 @@ def update_scalar_quantities(self): en_tot = en_U + en_thermo self.update_scalar("en_tot", en_tot) - dens_tot = self._ones.inner(self.pointer["fluid_rho3"]) + dens_tot = self._ones.inner(rho) self.update_scalar("dens_tot", dens_tot) - entr_tot = self._ones.inner(self.pointer["fluid_s3"]) + entr_tot = self._ones.inner(s) self.update_scalar("entr_tot", entr_tot) def update_thermo_energy(self): @@ -859,9 +811,12 @@ def update_thermo_energy(self): :meta private: """ - en_prop = self._propagators[0] - self._energy_evaluator.sf.vector = self.pointer["fluid_s3"] - self._energy_evaluator.rhof.vector = self.pointer["fluid_rho3"] + rho = self.fluid.density.spline.vector + s = self.fluid.entropy.spline.vector + en_prop = self.propagators.variat_dens + + self._energy_evaluator.sf.vector = s + self._energy_evaluator.rhof.vector = rho sf_values = self._energy_evaluator.sf.eval_tp_fixed_loc( self._energy_evaluator.integration_grid_spans, self._energy_evaluator.integration_grid_bd, @@ -879,8 +834,42 @@ def update_thermo_energy(self): self.update_scalar("en_thermo", en_thermo) return en_thermo - -class ViscoresistiveMHD_with_p(StruphyModel): + # default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "variat_dens.Options" in line: + new_file += [ + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='full',\n" + ] + new_file += [ + " s=model.fluid.entropy)\n" + ] + elif "variat_ent.Options" in line: + new_file += [ + "model.propagators.variat_ent.options = model.propagators.variat_ent.Options(model='full',\n" + ] + new_file += [ + " rho=model.fluid.density)\n" + ] + elif "variat_viscous.Options" in line: + new_file += [ + "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(rho=model.fluid.density)\n" + ] + elif "entropy.add_background" in line: + new_file += ["model.fluid.density.add_background(FieldsBackground())\n"] + new_file += [line] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) + + +class ViscoResistiveMHD_with_p(StruphyModel): r"""Full (non-linear) visco-resistive MHD equations, with the pressure variable discretized with a variational method. :ref:`normalization`: @@ -914,121 +903,78 @@ class ViscoresistiveMHD_with_p(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} - dct["em_fields"]["b2"] = "Hdiv" - dct["fluid"]["mhd"] = {"rho3": "L2", "p3": "L2", "uv": "H1vec"} - return dct - - @staticmethod - def bulk_species(): - return "mhd" - - @staticmethod - def velocity_scale(): - return "alfvén" + ## species + + class EMFields(FieldSpecies): + def __init__(self): + self.b_field = FEECVariable(space="Hdiv") + self.init_variables() + + class MHD(FluidSpecies): + def __init__(self): + self.density = FEECVariable(space="L2") + self.velocity = FEECVariable(space="H1vec") + self.pressure = FEECVariable(space="L2") + self.init_variables() + + class Diagnostics(DiagnosticSpecies): + def __init__(self): + self.div_u = FEECVariable(space="L2") + self.u2 = FEECVariable(space="Hdiv") + self.init_variables() + + ## propagators + + class Propagators: + def __init__( + self, + with_viscosity: bool = True, + with_resistivity: bool = True, + ): + self.variat_dens = propagators_fields.VariationalDensityEvolve() + self.variat_mom = propagators_fields.VariationalMomentumAdvection() + self.variat_pb = propagators_fields.VariationalPBEvolve() + if with_viscosity: + self.variat_viscous = propagators_fields.VariationalViscosity() + if with_resistivity: + self.variat_resist = propagators_fields.VariationalResistivity() + + ## abstract methods + + def __init__( + self, + with_viscosity: bool = True, + with_resistivity: bool = True, + ): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + + # 1. instantiate all species + self.em_fields = self.EMFields() + self.mhd = self.MHD() + self.diagnostics = self.Diagnostics() + + # 2. instantiate all propagators + self.propagators = self.Propagators( + with_viscosity=with_viscosity, + with_resistivity=with_resistivity, + ) - @staticmethod - def propagators_dct(): - return { - propagators_fields.VariationalDensityEvolve: ["mhd_rho3", "mhd_uv"], - propagators_fields.VariationalMomentumAdvection: ["mhd_uv"], - propagators_fields.VariationalPBEvolve: ["mhd_p3", "b2", "mhd_uv"], - propagators_fields.VariationalViscosity: ["mhd_p3", "mhd_uv"], - propagators_fields.VariationalResistivity: ["mhd_p3", "b2"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - def __init__(self, params, comm, clone_config=None): - from struphy.feec.projectors import L2Projector - from struphy.feec.variational_utilities import H1vecMassMatrix_density - from struphy.polar.basic import PolarVector - - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) - - self.WMM = H1vecMassMatrix_density(self.derham, self.mass_ops, self.domain) - - # Initialize propagators/integrators used in splitting substeps - lin_solver_momentum = params["fluid"]["mhd"]["options"]["VariationalMomentumAdvection"]["lin_solver"] - nonlin_solver_momentum = params["fluid"]["mhd"]["options"]["VariationalMomentumAdvection"]["nonlin_solver"] - lin_solver_density = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["lin_solver"] - nonlin_solver_density = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["nonlin_solver"] - lin_solver_magfield = params["fluid"]["mhd"]["options"]["VariationalPBEvolve"]["lin_solver"] - nonlin_solver_magfield = params["fluid"]["mhd"]["options"]["VariationalPBEvolve"]["nonlin_solver"] - lin_solver_viscosity = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["lin_solver"] - nonlin_solver_viscosity = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["nonlin_solver"] - lin_solver_resistivity = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["lin_solver"] - nonlin_solver_resistivity = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["nonlin_solver"] - if "linearize_current" in params["fluid"]["mhd"]["options"]["VariationalResistivity"].keys(): - self._linearize_current = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["linearize_current"] - else: - self._linearize_current = False - self._gamma = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["physics"]["gamma"] - self._mu = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["mu"] - self._mu_a = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["mu_a"] - self._alpha = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["alpha"] - self._eta = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["physics"]["eta"] - self._eta_a = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["physics"]["eta_a"] - model = "full_p" - - # set keyword arguments for propagators - self._kwargs[propagators_fields.VariationalDensityEvolve] = { - "model": model, - "gamma": self._gamma, - "mass_ops": self.WMM, - "lin_solver": lin_solver_density, - "nonlin_solver": nonlin_solver_density, - } - - self._kwargs[propagators_fields.VariationalMomentumAdvection] = { - "mass_ops": self.WMM, - "lin_solver": lin_solver_momentum, - "nonlin_solver": nonlin_solver_momentum, - } - - self._kwargs[propagators_fields.VariationalPBEvolve] = { - "model": model, - "mass_ops": self.WMM, - "lin_solver": lin_solver_magfield, - "nonlin_solver": nonlin_solver_magfield, - "gamma": self._gamma, - } - - self._kwargs[propagators_fields.VariationalViscosity] = { - "model": model, - "rho": self.pointer["mhd_rho3"], - "gamma": self._gamma, - "mu": self._mu, - "mu_a": self._mu_a, - "alpha": self._alpha, - "mass_ops": self.WMM, - "lin_solver": lin_solver_viscosity, - "nonlin_solver": nonlin_solver_viscosity, - } - - self._kwargs[propagators_fields.VariationalResistivity] = { - "model": model, - "rho": self.pointer["mhd_rho3"], - "gamma": self._gamma, - "eta": self._eta, - "eta_a": self._eta_a, - "lin_solver": lin_solver_resistivity, - "nonlin_solver": nonlin_solver_resistivity, - "linearize_current": self._linearize_current, - } - - # Initialize propagators used in splitting substeps - self.init_propagators() - - # Scalar variables to be saved during simulation + # 3. assign variables to propagators + self.propagators.variat_dens.variables.rho = self.mhd.density + self.propagators.variat_dens.variables.u = self.mhd.velocity + self.propagators.variat_mom.variables.u = self.mhd.velocity + self.propagators.variat_pb.variables.u = self.mhd.velocity + self.propagators.variat_pb.variables.p = self.mhd.pressure + self.propagators.variat_pb.variables.b = self.em_fields.b_field + if with_viscosity: + self.propagators.variat_viscous.variables.s = self.mhd.pressure + self.propagators.variat_viscous.variables.u = self.mhd.velocity + if with_resistivity: + self.propagators.variat_resist.variables.s = self.mhd.pressure + self.propagators.variat_resist.variables.b = self.em_fields.b_field + + # define scalars for update_scalar_quantities self.add_scalar("en_U") self.add_scalar("en_thermo") self.add_scalar("en_mag") @@ -1036,12 +982,22 @@ def __init__(self, params, comm, clone_config=None): self.add_scalar("dens_tot") self.add_scalar("tot_div_B") - # temporary vectors for scalar quantities - self._tmp_div_B = self.derham.Vh_pol["3"].zeros() - tmp_dof = self.derham.Vh_pol["3"].zeros() - projV3 = L2Projector("L2", self.mass_ops) + @property + def bulk_species(self): + return self.mhd - self._integrator = projV3(self.domain.jacobian_det, dofs=tmp_dof) + @property + def velocity_scale(self): + return "alfvén" + + def allocate_helpers(self): + projV3 = L2Projector("L2", self._mass_ops) + + def f(e1, e2, e3): + return 1 + + f = xp.vectorize(f) + self._integrator = projV3(f) self._ones = self.derham.Vh_pol["3"].zeros() if isinstance(self._ones, PolarVector): @@ -1049,39 +1005,68 @@ def __init__(self, params, comm, clone_config=None): else: self._ones[:] = 1.0 + self._tmp_div_B = self.derham.Vh_pol["3"].zeros() + def update_scalar_quantities(self): - # Update mass matrix - en_U = 0.5 * self.WMM.massop.dot_inner(self.pointer["mhd_uv"], self.pointer["mhd_uv"]) + rho = self.mhd.density.spline.vector + u = self.mhd.velocity.spline.vector + p = self.mhd.pressure.spline.vector + b = self.em_fields.b_field.spline.vector + + gamma = self.propagators.variat_pb.options.gamma + + en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) - en_mag = 0.5 * self.mass_ops.M2.dot_inner(self.pointer["b2"], self.pointer["b2"]) + en_mag = 0.5 * self.mass_ops.M2.dot_inner(b, b) self.update_scalar("en_mag", en_mag) - en_thermo = self.mass_ops.M3.dot_inner(self.pointer["mhd_p3"], self._integrator) / (self._gamma - 1.0) + en_thermo = self.mass_ops.M3.dot_inner(p, self._integrator) / (gamma - 1.0) self.update_scalar("en_thermo", en_thermo) en_tot = en_U + en_thermo + en_mag self.update_scalar("en_tot", en_tot) - dens_tot = self._ones.inner(self.pointer["mhd_rho3"]) + dens_tot = self._ones.inner(rho) self.update_scalar("dens_tot", dens_tot) - div_B = self.derham.div.dot(self.pointer["b2"], out=self._tmp_div_B) + div_B = self.derham.div.dot(b, out=self._tmp_div_B) L2_div_B = self._mass_ops.M3.dot_inner(div_B, div_B) self.update_scalar("tot_div_B", L2_div_B) - @staticmethod - def diagnostics_dct(): - dct = {} - - dct["div_u"] = "L2" - dct["u2"] = "Hdiv" - return dct - - __diagnostics__ = diagnostics_dct() - - -class ViscoresistiveLinearMHD(StruphyModel): + # default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "variat_pb.Options" in line: + new_file += [ + "model.propagators.variat_pb.options = model.propagators.variat_pb.Options(div_u=model.diagnostics.div_u,\n" + ] + new_file += [ + " u2=model.diagnostics.u2)\n" + ] + elif "variat_viscous.Options" in line: + new_file += [ + "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(rho=model.mhd.density)\n" + ] + elif "variat_resist.Options" in line: + new_file += [ + "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(rho=model.mhd.density)\n" + ] + elif "pressure.add_background" in line: + new_file += ["model.mhd.density.add_background(FieldsBackground())\n"] + new_file += [line] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) + + +class ViscoResistiveLinearMHD(StruphyModel): r"""Linear visco-resistive MHD equations discretized with a variational method. :ref:`normalization`: @@ -1114,136 +1099,104 @@ class ViscoresistiveLinearMHD(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} - dct["em_fields"]["b2"] = "Hdiv" - dct["fluid"]["mhd"] = {"rho3": "L2", "p3": "L2", "uv": "H1vec"} - return dct - - @staticmethod - def bulk_species(): - return "mhd" - - @staticmethod - def velocity_scale(): - return "alfvén" + ## species + + class EMFields(FieldSpecies): + def __init__(self): + self.b_field = FEECVariable(space="Hdiv") + self.init_variables() + + class MHD(FluidSpecies): + def __init__(self): + self.density = FEECVariable(space="L2") + self.velocity = FEECVariable(space="H1vec") + self.pressure = FEECVariable(space="L2") + self.init_variables() + + class Diagnostics(DiagnosticSpecies): + def __init__(self): + self.div_u = FEECVariable(space="L2") + self.u2 = FEECVariable(space="Hdiv") + self.pt3 = FEECVariable(space="L2") + self.bt2 = FEECVariable(space="Hdiv") + self.init_variables() + + ## propagators + + class Propagators: + def __init__( + self, + with_viscosity: bool = True, + with_resistivity: bool = True, + ): + self.variat_dens = propagators_fields.VariationalDensityEvolve() + self.variat_pb = propagators_fields.VariationalPBEvolve() + if with_viscosity: + self.variat_viscous = propagators_fields.VariationalViscosity() + if with_resistivity: + self.variat_resist = propagators_fields.VariationalResistivity() + + ## abstract methods + + def __init__( + self, + with_viscosity: bool = True, + with_resistivity: bool = True, + ): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + + # 1. instantiate all species + self.em_fields = self.EMFields() + self.mhd = self.MHD() + self.diagnostics = self.Diagnostics() + + # 2. instantiate all propagators + self.propagators = self.Propagators( + with_viscosity=with_viscosity, + with_resistivity=with_resistivity, + ) - @staticmethod - def propagators_dct(): - return { - propagators_fields.VariationalDensityEvolve: ["mhd_rho3", "mhd_uv"], - propagators_fields.VariationalPBEvolve: ["mhd_p3", "b2", "mhd_uv"], - propagators_fields.VariationalViscosity: ["mhd_p3", "mhd_uv"], - propagators_fields.VariationalResistivity: ["mhd_p3", "b2"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - def __init__(self, params, comm, clone_config=None): - from struphy.feec.projectors import L2Projector - from struphy.feec.variational_utilities import H1vecMassMatrix_density - from struphy.polar.basic import PolarVector - - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) - - self.WMM = H1vecMassMatrix_density(self.derham, self.mass_ops, self.domain) - - # Initialize propagators/integrators used in splitting substeps - lin_solver_density = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["lin_solver"] - nonlin_solver_density = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["nonlin_solver"] - lin_solver_magfield = params["fluid"]["mhd"]["options"]["VariationalPBEvolve"]["lin_solver"] - nonlin_solver_magfield = params["fluid"]["mhd"]["options"]["VariationalPBEvolve"]["nonlin_solver"] - lin_solver_viscosity = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["lin_solver"] - nonlin_solver_viscosity = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["nonlin_solver"] - lin_solver_resistivity = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["lin_solver"] - nonlin_solver_resistivity = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["nonlin_solver"] - if "linearize_current" in params["fluid"]["mhd"]["options"]["VariationalResistivity"].keys(): - self._linearize_current = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["linearize_current"] - else: - self._linearize_current = False - self._gamma = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["physics"]["gamma"] - self._mu = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["mu"] - self._mu_a = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["mu_a"] - self._alpha = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["alpha"] - self._eta = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["physics"]["eta"] - self._eta_a = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["physics"]["eta_a"] - model = "linear" - - # set keyword arguments for propagators - self._kwargs[propagators_fields.VariationalDensityEvolve] = { - "model": model, - "gamma": self._gamma, - "mass_ops": self.WMM, - "lin_solver": lin_solver_density, - "nonlin_solver": nonlin_solver_density, - } - - self._kwargs[propagators_fields.VariationalPBEvolve] = { - "model": model, - "mass_ops": self.WMM, - "lin_solver": lin_solver_magfield, - "nonlin_solver": nonlin_solver_magfield, - "gamma": self._gamma, - "div_u": self.pointer["div_u"], - "u2": self.pointer["u2"], - "bt2": self.pointer["bt2"], - "pt3": self.pointer["pt3"], - } - - self._kwargs[propagators_fields.VariationalViscosity] = { - "model": "linear_p", - "rho": self.pointer["mhd_rho3"], - "gamma": self._gamma, - "mu": self._mu, - "mu_a": self._mu_a, - "alpha": self._alpha, - "mass_ops": self.WMM, - "lin_solver": lin_solver_viscosity, - "nonlin_solver": nonlin_solver_viscosity, - } - - self._kwargs[propagators_fields.VariationalResistivity] = { - "model": "linear_p", - "rho": self.pointer["mhd_rho3"], - "gamma": self._gamma, - "eta": self._eta, - "eta_a": self._eta_a, - "lin_solver": lin_solver_resistivity, - "nonlin_solver": nonlin_solver_resistivity, - "linearize_current": self._linearize_current, - "pt3": self.pointer["pt3"], - } - - # Initialize propagators used in splitting substeps - self.init_propagators() - - # Scalar variables to be saved during simulation + # 3. assign variables to propagators + self.propagators.variat_dens.variables.rho = self.mhd.density + self.propagators.variat_dens.variables.u = self.mhd.velocity + self.propagators.variat_pb.variables.u = self.mhd.velocity + self.propagators.variat_pb.variables.p = self.mhd.pressure + self.propagators.variat_pb.variables.b = self.em_fields.b_field + if with_viscosity: + self.propagators.variat_viscous.variables.s = self.mhd.pressure + self.propagators.variat_viscous.variables.u = self.mhd.velocity + if with_resistivity: + self.propagators.variat_resist.variables.s = self.mhd.pressure + self.propagators.variat_resist.variables.b = self.em_fields.b_field + + # define scalars for update_scalar_quantities self.add_scalar("en_U") self.add_scalar("en_thermo") self.add_scalar("en_mag_1") self.add_scalar("en_mag_2") self.add_scalar("en_tot") - # self.add_scalar("dens_tot") - # self.add_scalar("tot_div_B") - self.add_scalar("en_tot_l1") self.add_scalar("en_thermo_l1") self.add_scalar("en_mag_l1") - # temporary vectors for scalar quantities - self._tmp_div_B = self.derham.Vh_pol["3"].zeros() - tmp_dof = self.derham.Vh_pol["3"].zeros() - projV3 = L2Projector("L2", self.mass_ops) + @property + def bulk_species(self): + return self.mhd - self._integrator = projV3(self.domain.jacobian_det, dofs=tmp_dof) + @property + def velocity_scale(self): + return "alfvén" + + def allocate_helpers(self): + projV3 = L2Projector("L2", self._mass_ops) + + def f(e1, e2, e3): + return 1 + + f = xp.vectorize(f) + self._integrator = projV3(f) self._ones = self.derham.Vh_pol["3"].zeros() if isinstance(self._ones, PolarVector): @@ -1251,52 +1204,104 @@ def __init__(self, params, comm, clone_config=None): else: self._ones[:] = 1.0 + self._tmp_div_B = self.derham.Vh_pol["3"].zeros() + def update_scalar_quantities(self): - # Update mass matrix - en_U = 0.5 * self.WMM.massop.dot_inner(self.pointer["mhd_uv"], self.pointer["mhd_uv"]) + rho = self.mhd.density.spline.vector + u = self.mhd.velocity.spline.vector + p = self.mhd.pressure.spline.vector + b = self.em_fields.b_field.spline.vector + bt2 = self.propagators.variat_pb.options.bt2.spline.vector + pt3 = self.propagators.variat_pb.options.pt3.spline.vector + + gamma = self.propagators.variat_pb.options.gamma + + en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) - en_mag1 = 0.5 * self.mass_ops.M2.dot_inner(self.pointer["b2"], self.pointer["b2"]) + en_mag1 = 0.5 * self.mass_ops.M2.dot_inner(b, b) self.update_scalar("en_mag_1", en_mag1) - en_mag2 = self.mass_ops.M2.dot_inner(self.pointer["bt2"], self.projected_equil.b2) + en_mag2 = self.mass_ops.M2.dot_inner(bt2, self.projected_equil.b2) self.update_scalar("en_mag_2", en_mag2) - en_thermo = self.mass_ops.M3.dot_inner(self.pointer["pt3"], self._integrator) / (self._gamma - 1.0) + en_thermo = self.mass_ops.M3.dot_inner(pt3, self._integrator) / (gamma - 1.0) self.update_scalar("en_thermo", en_thermo) en_tot = en_U + en_thermo + en_mag1 + en_mag2 self.update_scalar("en_tot", en_tot) - # dens_tot = self._ones.inner(self.pointer["mhd_rho3"]) + # dens_tot = self._ones.inner(rho) # self.update_scalar("dens_tot", dens_tot) - # div_B = self.derham.div.dot(self.pointer["b2"], out=self._tmp_div_B) + # div_B = self.derham.div.dot(b, out=self._tmp_div_B) # L2_div_B = self._mass_ops.M3.dot_inner(div_B, div_B) # self.update_scalar("tot_div_B", L2_div_B) - en_thermo_l1 = self.mass_ops.M3.dot_inner(self.pointer["mhd_p3"], self._integrator) / (self._gamma - 1.0) + en_thermo_l1 = self.mass_ops.M3.dot_inner(p, self._integrator) / (gamma - 1.0) self.update_scalar("en_thermo_l1", en_thermo_l1) - en_mag_l1 = self.mass_ops.M2.dot_inner(self.pointer["b2"], self.projected_equil.b2) + en_mag_l1 = self.mass_ops.M2.dot_inner(b, self.projected_equil.b2) self.update_scalar("en_mag_l1", en_mag_l1) en_tot_l1 = en_thermo_l1 + en_mag_l1 self.update_scalar("en_tot_l1", en_tot_l1) - @staticmethod - def diagnostics_dct(): - dct = {} - dct["bt2"] = "Hdiv" - dct["pt3"] = "L2" - dct["div_u"] = "L2" - dct["u2"] = "Hdiv" - return dct - - __diagnostics__ = diagnostics_dct() - - -class ViscoresistiveDeltafMHD(StruphyModel): + # default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "variat_dens.Options" in line: + new_file += [ + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='linear')\n" + ] + elif "variat_pb.Options" in line: + new_file += [ + "model.propagators.variat_pb.options = model.propagators.variat_pb.Options(model='linear',\n" + ] + new_file += [ + " div_u=model.diagnostics.div_u,\n" + ] + new_file += [ + " u2=model.diagnostics.u2,\n" + ] + new_file += [ + " pt3=model.diagnostics.pt3,\n" + ] + new_file += [ + " bt2=model.diagnostics.bt2)\n" + ] + elif "variat_viscous.Options" in line: + new_file += [ + "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(model='linear_p',\n" + ] + new_file += [ + " rho=model.mhd.density)\n" + ] + elif "variat_resist.Options" in line: + new_file += [ + "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(model='linear_p',\n" + ] + new_file += [ + " rho=model.mhd.density,\n" + ] + new_file += [ + " pt3=model.diagnostics.pt3)\n" + ] + elif "pressure.add_background" in line: + new_file += ["model.mhd.density.add_background(FieldsBackground())\n"] + new_file += [line] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) + + +class ViscoResistiveDeltafMHD(StruphyModel): r""":math:`\delta f` visco-resistive MHD equations discretized with a variational method. :ref:`normalization`: @@ -1330,141 +1335,106 @@ class ViscoresistiveDeltafMHD(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} - dct["em_fields"]["b2"] = "Hdiv" - dct["fluid"]["mhd"] = {"rho3": "L2", "p3": "L2", "uv": "H1vec"} - return dct - - @staticmethod - def bulk_species(): - return "mhd" - - @staticmethod - def velocity_scale(): - return "alfvén" + ## species + + class EMFields(FieldSpecies): + def __init__(self): + self.b_field = FEECVariable(space="Hdiv") + self.init_variables() + + class MHD(FluidSpecies): + def __init__(self): + self.density = FEECVariable(space="L2") + self.velocity = FEECVariable(space="H1vec") + self.pressure = FEECVariable(space="L2") + self.init_variables() + + class Diagnostics(DiagnosticSpecies): + def __init__(self): + self.div_u = FEECVariable(space="L2") + self.u2 = FEECVariable(space="Hdiv") + self.pt3 = FEECVariable(space="L2") + self.bt2 = FEECVariable(space="Hdiv") + self.init_variables() + + ## propagators + + class Propagators: + def __init__( + self, + with_viscosity: bool = True, + with_resistivity: bool = True, + ): + self.variat_dens = propagators_fields.VariationalDensityEvolve() + self.variat_mom = propagators_fields.VariationalMomentumAdvection() + self.variat_pb = propagators_fields.VariationalPBEvolve() + if with_viscosity: + self.variat_viscous = propagators_fields.VariationalViscosity() + if with_resistivity: + self.variat_resist = propagators_fields.VariationalResistivity() + + ## abstract methods + + def __init__( + self, + with_viscosity: bool = True, + with_resistivity: bool = True, + ): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + + # 1. instantiate all species + self.em_fields = self.EMFields() + self.mhd = self.MHD() + self.diagnostics = self.Diagnostics() + + # 2. instantiate all propagators + self.propagators = self.Propagators( + with_viscosity=with_viscosity, + with_resistivity=with_resistivity, + ) - @staticmethod - def propagators_dct(): - return { - propagators_fields.VariationalDensityEvolve: ["mhd_rho3", "mhd_uv"], - propagators_fields.VariationalMomentumAdvection: ["mhd_uv"], - propagators_fields.VariationalPBEvolve: ["mhd_p3", "b2", "mhd_uv"], - propagators_fields.VariationalViscosity: ["mhd_p3", "mhd_uv"], - propagators_fields.VariationalResistivity: ["mhd_p3", "b2"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - def __init__(self, params, comm, clone_config=None): - from struphy.feec.projectors import L2Projector - from struphy.feec.variational_utilities import H1vecMassMatrix_density - from struphy.polar.basic import PolarVector - - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) - - self.WMM = H1vecMassMatrix_density(self.derham, self.mass_ops, self.domain) - - # Initialize propagators/integrators used in splitting substeps - lin_solver_momentum = params["fluid"]["mhd"]["options"]["VariationalMomentumAdvection"]["lin_solver"] - nonlin_solver_momentum = params["fluid"]["mhd"]["options"]["VariationalMomentumAdvection"]["nonlin_solver"] - lin_solver_density = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["lin_solver"] - nonlin_solver_density = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["nonlin_solver"] - lin_solver_magfield = params["fluid"]["mhd"]["options"]["VariationalPBEvolve"]["lin_solver"] - nonlin_solver_magfield = params["fluid"]["mhd"]["options"]["VariationalPBEvolve"]["nonlin_solver"] - lin_solver_viscosity = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["lin_solver"] - nonlin_solver_viscosity = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["nonlin_solver"] - lin_solver_resistivity = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["lin_solver"] - nonlin_solver_resistivity = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["nonlin_solver"] - if "linearize_current" in params["fluid"]["mhd"]["options"]["VariationalResistivity"].keys(): - self._linearize_current = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["linearize_current"] - else: - self._linearize_current = False - self._gamma = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["physics"]["gamma"] - self._mu = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["mu"] - self._mu_a = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["mu_a"] - self._alpha = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["alpha"] - self._eta = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["physics"]["eta"] - self._eta_a = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["physics"]["eta_a"] - model = "deltaf" - - # set keyword arguments for propagators - self._kwargs[propagators_fields.VariationalDensityEvolve] = { - "model": model, - "gamma": self._gamma, - "mass_ops": self.WMM, - "lin_solver": lin_solver_density, - "nonlin_solver": nonlin_solver_density, - } - - self._kwargs[propagators_fields.VariationalMomentumAdvection] = { - "mass_ops": self.WMM, - "lin_solver": lin_solver_momentum, - "nonlin_solver": nonlin_solver_momentum, - } - - self._kwargs[propagators_fields.VariationalPBEvolve] = { - "model": model, - "mass_ops": self.WMM, - "lin_solver": lin_solver_magfield, - "nonlin_solver": nonlin_solver_magfield, - "gamma": self._gamma, - "bt2": self.pointer["bt2"], - "pt3": self.pointer["pt3"], - } - - self._kwargs[propagators_fields.VariationalViscosity] = { - "model": "full_p", - "rho": self.pointer["mhd_rho3"], - "gamma": self._gamma, - "mu": self._mu, - "mu_a": self._mu_a, - "alpha": self._alpha, - "mass_ops": self.WMM, - "lin_solver": lin_solver_viscosity, - "nonlin_solver": nonlin_solver_viscosity, - } - - self._kwargs[propagators_fields.VariationalResistivity] = { - "model": "delta_p", - "rho": self.pointer["mhd_rho3"], - "gamma": self._gamma, - "eta": self._eta, - "eta_a": self._eta_a, - "lin_solver": lin_solver_resistivity, - "nonlin_solver": nonlin_solver_resistivity, - "linearize_current": self._linearize_current, - } - - # Initialize propagators used in splitting substeps - self.init_propagators() - - # Scalar variables to be saved during simulation + # 3. assign variables to propagators + self.propagators.variat_dens.variables.rho = self.mhd.density + self.propagators.variat_dens.variables.u = self.mhd.velocity + self.propagators.variat_mom.variables.u = self.mhd.velocity + self.propagators.variat_pb.variables.u = self.mhd.velocity + self.propagators.variat_pb.variables.p = self.mhd.pressure + self.propagators.variat_pb.variables.b = self.em_fields.b_field + if with_viscosity: + self.propagators.variat_viscous.variables.s = self.mhd.pressure + self.propagators.variat_viscous.variables.u = self.mhd.velocity + if with_resistivity: + self.propagators.variat_resist.variables.s = self.mhd.pressure + self.propagators.variat_resist.variables.b = self.em_fields.b_field + + # define scalars for update_scalar_quantities self.add_scalar("en_U") self.add_scalar("en_thermo") self.add_scalar("en_mag_1") self.add_scalar("en_mag_2") self.add_scalar("en_tot") - # self.add_scalar("dens_tot") - # self.add_scalar("tot_div_B") - self.add_scalar("en_tot_l1") self.add_scalar("en_thermo_l1") self.add_scalar("en_mag_l1") - # temporary vectors for scalar quantities - tmp_dof = self.derham.Vh_pol["3"].zeros() - projV3 = L2Projector("L2", self.mass_ops) + @property + def bulk_species(self): + return self.mhd - self._integrator = projV3(self.domain.jacobian_det, dofs=tmp_dof) + @property + def velocity_scale(self): + return "alfvén" + + def allocate_helpers(self): + projV3 = L2Projector("L2", self._mass_ops) + + def f(e1, e2, e3): + return 1 + + f = xp.vectorize(f) + self._integrator = projV3(f) self._ones = self.derham.Vh_pol["3"].zeros() if isinstance(self._ones, PolarVector): @@ -1472,52 +1442,95 @@ def __init__(self, params, comm, clone_config=None): else: self._ones[:] = 1.0 + self._tmp_div_B = self.derham.Vh_pol["3"].zeros() + def update_scalar_quantities(self): - # Update mass matrix - en_U = 0.5 * self.WMM.massop.dot_inner(self.pointer["mhd_uv"], self.pointer["mhd_uv"]) + rho = self.mhd.density.spline.vector + u = self.mhd.velocity.spline.vector + p = self.mhd.pressure.spline.vector + b = self.em_fields.b_field.spline.vector + bt2 = self.propagators.variat_pb.options.bt2.spline.vector + pt3 = self.propagators.variat_pb.options.pt3.spline.vector + + gamma = self.propagators.variat_pb.options.gamma + + en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) - en_mag1 = 0.5 * self.mass_ops.M2.dot_inner(self.pointer["b2"], self.pointer["b2"]) + en_mag1 = 0.5 * self.mass_ops.M2.dot_inner(b, b) self.update_scalar("en_mag_1", en_mag1) - en_mag2 = self.mass_ops.M2.dot_inner(self.pointer["bt2"], self.projected_equil.b2) + en_mag2 = self.mass_ops.M2.dot_inner(bt2, self.projected_equil.b2) self.update_scalar("en_mag_2", en_mag2) - en_thermo = self.mass_ops.M3.dot_inner(self.pointer["pt3"], self._integrator) / (self._gamma - 1.0) + en_thermo = self.mass_ops.M3.dot_inner(pt3, self._integrator) / (gamma - 1.0) self.update_scalar("en_thermo", en_thermo) en_tot = en_U + en_thermo + en_mag1 + en_mag2 self.update_scalar("en_tot", en_tot) - # dens_tot = self._ones.inner(self.pointer["mhd_rho3"]) + # dens_tot = self._ones.inner(rho) # self.update_scalar("dens_tot", dens_tot) - # div_B = self.derham.div.dot(self.pointer["b2"], out=self._tmp_div_B) + # div_B = self.derham.div.dot(b, out=self._tmp_div_B) # L2_div_B = self._mass_ops.M3.dot_inner(div_B, div_B) # self.update_scalar("tot_div_B", L2_div_B) - en_thermo_l1 = self.mass_ops.M3.dot_inner(self.pointer["mhd_p3"], self._integrator) / (self._gamma - 1.0) + en_thermo_l1 = self.mass_ops.M3.dot_inner(p, self._integrator) / (gamma - 1.0) self.update_scalar("en_thermo_l1", en_thermo_l1) - en_mag_l1 = self.mass_ops.M2.dot_inner(self.pointer["b2"], self.projected_equil.b2) + en_mag_l1 = self.mass_ops.M2.dot_inner(b, self.projected_equil.b2) self.update_scalar("en_mag_l1", en_mag_l1) en_tot_l1 = en_thermo_l1 + en_mag_l1 self.update_scalar("en_tot_l1", en_tot_l1) - @staticmethod - def diagnostics_dct(): - dct = {} - dct["bt2"] = "Hdiv" - dct["pt3"] = "L2" - dct["div_u"] = "L2" - dct["u2"] = "Hdiv" - return dct - - __diagnostics__ = diagnostics_dct() - - -class ViscoresistiveMHD_with_q(StruphyModel): + # default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "variat_dens.Options" in line: + new_file += [ + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='deltaf')\n" + ] + elif "variat_pb.Options" in line: + new_file += [ + "model.propagators.variat_pb.options = model.propagators.variat_pb.Options(model='deltaf',\n" + ] + new_file += [ + " pt3=model.diagnostics.pt3,\n" + ] + new_file += [ + " bt2=model.diagnostics.bt2)\n" + ] + elif "variat_viscous.Options" in line: + new_file += [ + "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(model='full_p',\n" + ] + new_file += [ + " rho=model.mhd.density)\n" + ] + elif "variat_resist.Options" in line: + new_file += [ + "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(model='full_p',\n" + ] + new_file += [ + " rho=model.mhd.density)\n" + ] + elif "pressure.add_background" in line: + new_file += ["model.mhd.density.add_background(FieldsBackground())\n"] + new_file += [line] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) + + +class ViscoResistiveMHD_with_q(StruphyModel): r"""Full (non-linear) visco-resistive MHD equations, with the q variable (square root of the pressure) discretized with a variational method. :ref:`normalization`: @@ -1553,121 +1566,78 @@ class ViscoresistiveMHD_with_q(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} - dct["em_fields"]["b2"] = "Hdiv" - dct["fluid"]["mhd"] = {"rho3": "L2", "q3": "L2", "uv": "H1vec"} - return dct - - @staticmethod - def bulk_species(): - return "mhd" - - @staticmethod - def velocity_scale(): - return "alfvén" + ## species + + class EMFields(FieldSpecies): + def __init__(self): + self.b_field = FEECVariable(space="Hdiv") + self.init_variables() + + class MHD(FluidSpecies): + def __init__(self): + self.density = FEECVariable(space="L2") + self.velocity = FEECVariable(space="H1vec") + self.sqrt_p = FEECVariable(space="L2") + self.init_variables() + + class Diagnostics(DiagnosticSpecies): + def __init__(self): + self.div_u = FEECVariable(space="L2") + self.u2 = FEECVariable(space="Hdiv") + self.init_variables() + + ## propagators + + class Propagators: + def __init__( + self, + with_viscosity: bool = True, + with_resistivity: bool = True, + ): + self.variat_dens = propagators_fields.VariationalDensityEvolve() + self.variat_mom = propagators_fields.VariationalMomentumAdvection() + self.variat_qb = propagators_fields.VariationalQBEvolve() + if with_viscosity: + self.variat_viscous = propagators_fields.VariationalViscosity() + if with_resistivity: + self.variat_resist = propagators_fields.VariationalResistivity() + + ## abstract methods + + def __init__( + self, + with_viscosity: bool = True, + with_resistivity: bool = True, + ): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + + # 1. instantiate all species + self.em_fields = self.EMFields() + self.mhd = self.MHD() + self.diagnostics = self.Diagnostics() + + # 2. instantiate all propagators + self.propagators = self.Propagators( + with_viscosity=with_viscosity, + with_resistivity=with_resistivity, + ) - @staticmethod - def propagators_dct(): - return { - propagators_fields.VariationalDensityEvolve: ["mhd_rho3", "mhd_uv"], - propagators_fields.VariationalMomentumAdvection: ["mhd_uv"], - propagators_fields.VariationalQBEvolve: ["mhd_q3", "b2", "mhd_uv"], - propagators_fields.VariationalViscosity: ["mhd_q3", "mhd_uv"], - propagators_fields.VariationalResistivity: ["mhd_q3", "b2"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - def __init__(self, params, comm, clone_config=None): - from struphy.feec.projectors import L2Projector - from struphy.feec.variational_utilities import H1vecMassMatrix_density - from struphy.polar.basic import PolarVector - - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) - - self.WMM = H1vecMassMatrix_density(self.derham, self.mass_ops, self.domain) - - # Initialize propagators/integrators used in splitting substeps - lin_solver_momentum = params["fluid"]["mhd"]["options"]["VariationalMomentumAdvection"]["lin_solver"] - nonlin_solver_momentum = params["fluid"]["mhd"]["options"]["VariationalMomentumAdvection"]["nonlin_solver"] - lin_solver_density = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["lin_solver"] - nonlin_solver_density = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["nonlin_solver"] - lin_solver_magfield = params["fluid"]["mhd"]["options"]["VariationalQBEvolve"]["lin_solver"] - nonlin_solver_magfield = params["fluid"]["mhd"]["options"]["VariationalQBEvolve"]["nonlin_solver"] - lin_solver_viscosity = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["lin_solver"] - nonlin_solver_viscosity = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["nonlin_solver"] - lin_solver_resistivity = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["lin_solver"] - nonlin_solver_resistivity = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["nonlin_solver"] - if "linearize_current" in params["fluid"]["mhd"]["options"]["VariationalResistivity"].keys(): - self._linearize_current = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["linearize_current"] - else: - self._linearize_current = False - self._gamma = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["physics"]["gamma"] - self._mu = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["mu"] - self._mu_a = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["mu_a"] - self._alpha = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["alpha"] - self._eta = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["physics"]["eta"] - self._eta_a = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["physics"]["eta_a"] - model = "full_q" - - # set keyword arguments for propagators - self._kwargs[propagators_fields.VariationalDensityEvolve] = { - "model": model, - "gamma": self._gamma, - "mass_ops": self.WMM, - "lin_solver": lin_solver_density, - "nonlin_solver": nonlin_solver_density, - } - - self._kwargs[propagators_fields.VariationalMomentumAdvection] = { - "mass_ops": self.WMM, - "lin_solver": lin_solver_momentum, - "nonlin_solver": nonlin_solver_momentum, - } - - self._kwargs[propagators_fields.VariationalQBEvolve] = { - "model": model, - "mass_ops": self.WMM, - "lin_solver": lin_solver_magfield, - "nonlin_solver": nonlin_solver_magfield, - "gamma": self._gamma, - } - - self._kwargs[propagators_fields.VariationalViscosity] = { - "model": model, - "rho": self.pointer["mhd_rho3"], - "gamma": self._gamma, - "mu": self._mu, - "mu_a": self._mu_a, - "alpha": self._alpha, - "mass_ops": self.WMM, - "lin_solver": lin_solver_viscosity, - "nonlin_solver": nonlin_solver_viscosity, - } - - self._kwargs[propagators_fields.VariationalResistivity] = { - "model": model, - "rho": self.pointer["mhd_rho3"], - "gamma": self._gamma, - "eta": self._eta, - "eta_a": self._eta_a, - "lin_solver": lin_solver_resistivity, - "nonlin_solver": nonlin_solver_resistivity, - "linearize_current": self._linearize_current, - } - - # Initialize propagators used in splitting substeps - self.init_propagators() - - # Scalar variables to be saved during simulation + # 3. assign variables to propagators + self.propagators.variat_dens.variables.rho = self.mhd.density + self.propagators.variat_dens.variables.u = self.mhd.velocity + self.propagators.variat_mom.variables.u = self.mhd.velocity + self.propagators.variat_qb.variables.u = self.mhd.velocity + self.propagators.variat_qb.variables.q = self.mhd.sqrt_p + self.propagators.variat_qb.variables.b = self.em_fields.b_field + if with_viscosity: + self.propagators.variat_viscous.variables.s = self.mhd.sqrt_p + self.propagators.variat_viscous.variables.u = self.mhd.velocity + if with_resistivity: + self.propagators.variat_resist.variables.s = self.mhd.sqrt_p + self.propagators.variat_resist.variables.b = self.em_fields.b_field + + # define scalars for update_scalar_quantities self.add_scalar("en_U") self.add_scalar("en_thermo") self.add_scalar("en_mag") @@ -1675,12 +1645,22 @@ def __init__(self, params, comm, clone_config=None): self.add_scalar("dens_tot") self.add_scalar("tot_div_B") - # temporary vectors for scalar quantities - self._tmp_div_B = self.derham.Vh_pol["3"].zeros() - tmp_dof = self.derham.Vh_pol["3"].zeros() - projV3 = L2Projector("L2", self.mass_ops) + @property + def bulk_species(self): + return self.mhd - self._integrator = projV3(self.domain.jacobian_det, dofs=tmp_dof) + @property + def velocity_scale(self): + return "alfvén" + + def allocate_helpers(self): + projV3 = L2Projector("L2", self._mass_ops) + + def f(e1, e2, e3): + return 1 + + f = xp.vectorize(f) + self._integrator = projV3(f) self._ones = self.derham.Vh_pol["3"].zeros() if isinstance(self._ones, PolarVector): @@ -1688,39 +1668,75 @@ def __init__(self, params, comm, clone_config=None): else: self._ones[:] = 1.0 + self._tmp_div_B = self.derham.Vh_pol["3"].zeros() + def update_scalar_quantities(self): - # Update mass matrix - en_U = 0.5 * self.WMM.massop.dot_inner(self.pointer["mhd_uv"], self.pointer["mhd_uv"]) + rho = self.mhd.density.spline.vector + u = self.mhd.velocity.spline.vector + q = self.mhd.sqrt_p.spline.vector + b = self.em_fields.b_field.spline.vector + + gamma = self.propagators.variat_qb.options.gamma + + en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) - en_mag = 0.5 * self._mass_ops.M2.dot_inner(self.pointer["b2"], self.pointer["b2"]) + en_mag = 0.5 * self.mass_ops.M2.dot_inner(b, b) self.update_scalar("en_mag", en_mag) - en_thermo = 1 / (self._gamma - 1) * self._mass_ops.M3.dot_inner(self.pointer["mhd_q3"], self.pointer["mhd_q3"]) + en_thermo = 1.0 / (gamma - 1.0) * self._mass_ops.M3.dot_inner(q, q) self.update_scalar("en_thermo", en_thermo) en_tot = en_U + en_thermo + en_mag self.update_scalar("en_tot", en_tot) - dens_tot = self._ones.inner(self.pointer["mhd_rho3"]) + dens_tot = self._ones.inner(rho) self.update_scalar("dens_tot", dens_tot) - div_B = self.derham.div.dot(self.pointer["b2"], out=self._tmp_div_B) + div_B = self.derham.div.dot(b, out=self._tmp_div_B) L2_div_B = self._mass_ops.M3.dot_inner(div_B, div_B) self.update_scalar("tot_div_B", L2_div_B) - @staticmethod - def diagnostics_dct(): - dct = {} - - dct["div_u"] = "L2" - dct["u2"] = "Hdiv" - return dct - - __diagnostics__ = diagnostics_dct() - - -class ViscoresistiveLinearMHD_with_q(StruphyModel): + # default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "variat_dens.Options" in line: + new_file += [ + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='full_q')\n" + ] + elif "variat_qb.Options" in line: + new_file += [ + "model.propagators.variat_qb.options = model.propagators.variat_qb.Options(model='full_q')\n" + ] + elif "variat_viscous.Options" in line: + new_file += [ + "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(model='full_q',\n" + ] + new_file += [ + " rho=model.mhd.density)\n" + ] + elif "variat_resist.Options" in line: + new_file += [ + "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(model='full_q',\n" + ] + new_file += [ + " rho=model.mhd.density)\n" + ] + elif "sqrt_p.add_background" in line: + new_file += ["model.mhd.density.add_background(FieldsBackground())\n"] + new_file += [line] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) + + +class ViscoResistiveLinearMHD_with_q(StruphyModel): r"""Linear visco-resistive MHD equations, with the q variable (square root of the pressure), discretized with a variational method. :ref:`normalization`: @@ -1753,138 +1769,101 @@ class ViscoresistiveLinearMHD_with_q(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} - dct["em_fields"]["b2"] = "Hdiv" - dct["fluid"]["mhd"] = {"rho3": "L2", "q3": "L2", "uv": "H1vec"} - return dct - - @staticmethod - def bulk_species(): - return "mhd" - - @staticmethod - def velocity_scale(): - return "alfvén" + ## species + + class EMFields(FieldSpecies): + def __init__(self): + self.b_field = FEECVariable(space="Hdiv") + self.init_variables() + + class MHD(FluidSpecies): + def __init__(self): + self.density = FEECVariable(space="L2") + self.velocity = FEECVariable(space="H1vec") + self.sqrt_p = FEECVariable(space="L2") + self.init_variables() + + class Diagnostics(DiagnosticSpecies): + def __init__(self): + self.div_u = FEECVariable(space="L2") + self.u2 = FEECVariable(space="Hdiv") + self.qt3 = FEECVariable(space="L2") + self.bt2 = FEECVariable(space="Hdiv") + self.init_variables() + + ## propagators + + class Propagators: + def __init__( + self, + with_viscosity: bool = True, + with_resistivity: bool = True, + ): + self.variat_dens = propagators_fields.VariationalDensityEvolve() + self.variat_qb = propagators_fields.VariationalQBEvolve() + if with_viscosity: + self.variat_viscous = propagators_fields.VariationalViscosity() + if with_resistivity: + self.variat_resist = propagators_fields.VariationalResistivity() + + ## abstract methods + + def __init__( + self, + with_viscosity: bool = True, + with_resistivity: bool = True, + ): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + + # 1. instantiate all species + self.em_fields = self.EMFields() + self.mhd = self.MHD() + self.diagnostics = self.Diagnostics() + + # 2. instantiate all propagators + self.propagators = self.Propagators( + with_viscosity=with_viscosity, + with_resistivity=with_resistivity, + ) - @staticmethod - def propagators_dct(): - return { - propagators_fields.VariationalDensityEvolve: ["mhd_rho3", "mhd_uv"], - propagators_fields.VariationalQBEvolve: ["mhd_q3", "b2", "mhd_uv"], - propagators_fields.VariationalViscosity: ["mhd_q3", "mhd_uv"], - propagators_fields.VariationalResistivity: ["mhd_q3", "b2"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - def __init__(self, params, comm, clone_config=None): - from struphy.feec.projectors import L2Projector - from struphy.feec.variational_utilities import H1vecMassMatrix_density - from struphy.polar.basic import PolarVector - - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) - - self.WMM = H1vecMassMatrix_density(self.derham, self.mass_ops, self.domain) - - # Initialize propagators/integrators used in splitting substeps - lin_solver_density = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["lin_solver"] - nonlin_solver_density = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["nonlin_solver"] - lin_solver_magfield = params["fluid"]["mhd"]["options"]["VariationalQBEvolve"]["lin_solver"] - nonlin_solver_magfield = params["fluid"]["mhd"]["options"]["VariationalQBEvolve"]["nonlin_solver"] - lin_solver_viscosity = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["lin_solver"] - nonlin_solver_viscosity = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["nonlin_solver"] - lin_solver_resistivity = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["lin_solver"] - nonlin_solver_resistivity = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["nonlin_solver"] - if "linearize_current" in params["fluid"]["mhd"]["options"]["VariationalResistivity"].keys(): - self._linearize_current = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["linearize_current"] - else: - self._linearize_current = False - self._gamma = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["physics"]["gamma"] - self._mu = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["mu"] - self._mu_a = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["mu_a"] - self._alpha = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["alpha"] - self._eta = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["physics"]["eta"] - self._eta_a = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["physics"]["eta_a"] - model = "linear_q" - - # set keyword arguments for propagators - self._kwargs[propagators_fields.VariationalDensityEvolve] = { - "model": model, - "gamma": self._gamma, - "mass_ops": self.WMM, - "lin_solver": lin_solver_density, - "nonlin_solver": nonlin_solver_density, - } - - self._kwargs[propagators_fields.VariationalQBEvolve] = { - "model": model, - "mass_ops": self.WMM, - "lin_solver": lin_solver_magfield, - "nonlin_solver": nonlin_solver_magfield, - "gamma": self._gamma, - "div_u": self.pointer["div_u"], - "u2": self.pointer["u2"], - "bt2": self.pointer["bt2"], - "qt3": self.pointer["qt3"], - } - - self._kwargs[propagators_fields.VariationalViscosity] = { - "model": model, - "rho": self.pointer["mhd_rho3"], - "gamma": self._gamma, - "mu": self._mu, - "mu_a": self._mu_a, - "alpha": self._alpha, - "mass_ops": self.WMM, - "lin_solver": lin_solver_viscosity, - "nonlin_solver": nonlin_solver_viscosity, - "pt3": self.pointer["qt3"], - } - - self._kwargs[propagators_fields.VariationalResistivity] = { - "model": model, - "rho": self.pointer["mhd_rho3"], - "gamma": self._gamma, - "eta": self._eta, - "eta_a": self._eta_a, - "lin_solver": lin_solver_resistivity, - "nonlin_solver": nonlin_solver_resistivity, - "linearize_current": self._linearize_current, - "pt3": self.pointer["qt3"], - } - - # Initialize propagators used in splitting substeps - self.init_propagators() - - # Scalar variables to be saved during simulation + # 3. assign variables to propagators + self.propagators.variat_dens.variables.rho = self.mhd.density + self.propagators.variat_dens.variables.u = self.mhd.velocity + self.propagators.variat_qb.variables.u = self.mhd.velocity + self.propagators.variat_qb.variables.q = self.mhd.sqrt_p + self.propagators.variat_qb.variables.b = self.em_fields.b_field + if with_viscosity: + self.propagators.variat_viscous.variables.s = self.mhd.sqrt_p + self.propagators.variat_viscous.variables.u = self.mhd.velocity + if with_resistivity: + self.propagators.variat_resist.variables.s = self.mhd.sqrt_p + self.propagators.variat_resist.variables.b = self.em_fields.b_field + + # define scalars for update_scalar_quantities self.add_scalar("en_U") - # self.add_scalar("en_thermo_1") - # self.add_scalar("en_thermo_2") - # self.add_scalar("en_mag_1") - # self.add_scalar("en_mag_2") + self.add_scalar("en_mag_1") + self.add_scalar("en_mag_2") + self.add_scalar("en_thermo_1") + self.add_scalar("en_thermo_2") self.add_scalar("en_tot") - # self.add_scalar("dens_tot") - # self.add_scalar("tot_div_B") + @property + def bulk_species(self): + return self.mhd - # self.add_scalar("en_tot_l1") - # self.add_scalar("en_thermo_l1") - # self.add_scalar("en_mag_l1") + @property + def velocity_scale(self): + return "alfvén" - # temporary vectors for scalar quantities - self._tmp_div_B = self.derham.Vh_pol["3"].zeros() - tmp_dof = self.derham.Vh_pol["3"].zeros() - projV3 = L2Projector("L2", self.mass_ops) + def allocate_helpers(self): + projV3 = L2Projector("L2", self._mass_ops) - self._integrator = projV3(self.domain.jacobian_det, dofs=tmp_dof) + def f(e1, e2, e3): + return 1 + + f = xp.vectorize(f) + self._integrator = projV3(f) self._ones = self.derham.Vh_pol["3"].zeros() if isinstance(self._ones, PolarVector): @@ -1892,56 +1871,94 @@ def __init__(self, params, comm, clone_config=None): else: self._ones[:] = 1.0 + self._tmp_div_B = self.derham.Vh_pol["3"].zeros() + def update_scalar_quantities(self): - # Update mass matrix - en_U = 0.5 * self.WMM.massop.dot_inner(self.pointer["mhd_uv"], self.pointer["mhd_uv"]) + rho = self.mhd.density.spline.vector + u = self.mhd.velocity.spline.vector + q = self.mhd.sqrt_p.spline.vector + b = self.em_fields.b_field.spline.vector + bt2 = self.propagators.variat_qb.options.bt2.spline.vector + qt3 = self.propagators.variat_qb.options.qt3.spline.vector + + gamma = self.propagators.variat_qb.options.gamma + + en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) - en_mag1 = self._mass_ops.M2.dot_inner(self.pointer["b2"], self.pointer["b2"]) - # self.update_scalar("en_mag_1", en_mag1) + en_mag1 = 0.5 * self.mass_ops.M2.dot_inner(b, b) + self.update_scalar("en_mag_1", en_mag1) - en_mag2 = self._mass_ops.M2.dot_inner(self.pointer["bt2"], self.projected_equil.b2) - # self.update_scalar("en_mag_2", en_mag2) + en_mag2 = self.mass_ops.M2.dot_inner(bt2, self.projected_equil.b2) + self.update_scalar("en_mag_2", en_mag2) - en_th_1 = 1 / (self._gamma - 1) * self._mass_ops.M3.dot_inner(self.pointer["mhd_q3"], self.pointer["mhd_q3"]) - # self.update_scalar("en_thermo_1", en_th_1) + en_th_1 = 1.0 / (gamma - 1.0) * self.mass_ops.M3.dot_inner(q, q) + self.update_scalar("en_thermo_1", en_th_1) - en_th_2 = 2 / (self._gamma - 1) * self._mass_ops.M3.dot_inner(self.pointer["qt3"], self.projected_equil.q3) - # self.update_scalar("en_thermo_2", en_th_2) + en_th_2 = 2.0 / (gamma - 1.0) * self.mass_ops.M3.dot_inner(qt3, self.projected_equil.q3) + self.update_scalar("en_thermo_2", en_th_2) en_tot = en_U + en_th_1 + en_th_2 + en_mag1 + en_mag2 self.update_scalar("en_tot", en_tot) - # dens_tot = self._ones.dot(self.pointer["mhd_rho3"]) - # self.update_scalar("dens_tot", dens_tot) - - # div_B = self.derham.div.dot(self.pointer["b2"], out=self._tmp_div_B) - # L2_div_B = self._mass_ops.M3.dot_inner(div_B, div_B) - # self.update_scalar("tot_div_B", L2_div_B) - - # en_thermo_l1 = self._integrator.dot(self.mass_ops.M3.dot(self.pointer["mhd_p3"])) / (self._gamma - 1.0) - # self.update_scalar("en_thermo_l1", en_thermo_l1) - - # wb2 = self._mass_ops.M2.dot(self.pointer["b2"], out=self._tmp_wb2) - # en_mag_l1 = wb2.dot(self.projected_equil.b2) - # self.update_scalar("en_mag_l1", en_mag_l1) - - # en_tot_l1 = en_thermo_l1 + en_mag_l1 - # self.update_scalar("en_tot_l1", en_tot_l1) - - @staticmethod - def diagnostics_dct(): - dct = {} - dct["bt2"] = "Hdiv" - dct["qt3"] = "L2" - dct["div_u"] = "L2" - dct["u2"] = "Hdiv" - return dct - - __diagnostics__ = diagnostics_dct() - - -class ViscoresistiveDeltafMHD_with_q(StruphyModel): + # default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "variat_dens.Options" in line: + new_file += [ + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='linear_q')\n" + ] + elif "variat_qb.Options" in line: + new_file += [ + "model.propagators.variat_qb.options = model.propagators.variat_qb.Options(model='linear_q',\n" + ] + new_file += [ + " div_u=model.diagnostics.div_u,\n" + ] + new_file += [ + " u2=model.diagnostics.u2,\n" + ] + new_file += [ + " qt3=model.diagnostics.qt3,\n" + ] + new_file += [ + " bt2=model.diagnostics.bt2)\n" + ] + elif "variat_viscous.Options" in line: + new_file += [ + "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(model='linear_q',\n" + ] + new_file += [ + " rho=model.mhd.density,\n" + ] + new_file += [ + " pt3=model.diagnostics.qt3)\n" + ] + elif "variat_resist.Options" in line: + new_file += [ + "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(model='linear_q',\n" + ] + new_file += [ + " rho=model.mhd.density,\n" + ] + new_file += [ + " pt3=model.diagnostics.qt3)\n" + ] + elif "sqrt_p.add_background" in line: + new_file += ["model.mhd.density.add_background(FieldsBackground())\n"] + new_file += [line] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) + + +class ViscoResistiveDeltafMHD_with_q(StruphyModel): r"""Linear visco-resistive MHD equations discretized with a variational method. :ref:`normalization`: @@ -1975,147 +1992,103 @@ class ViscoresistiveDeltafMHD_with_q(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} - dct["em_fields"]["b2"] = "Hdiv" - dct["fluid"]["mhd"] = {"rho3": "L2", "q3": "L2", "uv": "H1vec"} - return dct - - @staticmethod - def bulk_species(): - return "mhd" - - @staticmethod - def velocity_scale(): - return "alfvén" + ## species + + class EMFields(FieldSpecies): + def __init__(self): + self.b_field = FEECVariable(space="Hdiv") + self.init_variables() + + class MHD(FluidSpecies): + def __init__(self): + self.density = FEECVariable(space="L2") + self.velocity = FEECVariable(space="H1vec") + self.sqrt_p = FEECVariable(space="L2") + self.init_variables() + + class Diagnostics(DiagnosticSpecies): + def __init__(self): + self.div_u = FEECVariable(space="L2") + self.u2 = FEECVariable(space="Hdiv") + self.qt3 = FEECVariable(space="L2") + self.bt2 = FEECVariable(space="Hdiv") + self.init_variables() + + ## propagators + + class Propagators: + def __init__( + self, + with_viscosity: bool = True, + with_resistivity: bool = True, + ): + self.variat_dens = propagators_fields.VariationalDensityEvolve() + self.variat_mom = propagators_fields.VariationalMomentumAdvection() + self.variat_qb = propagators_fields.VariationalQBEvolve() + if with_viscosity: + self.variat_viscous = propagators_fields.VariationalViscosity() + if with_resistivity: + self.variat_resist = propagators_fields.VariationalResistivity() + + ## abstract methods + + def __init__( + self, + with_viscosity: bool = True, + with_resistivity: bool = True, + ): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + + # 1. instantiate all species + self.em_fields = self.EMFields() + self.mhd = self.MHD() + self.diagnostics = self.Diagnostics() + + # 2. instantiate all propagators + self.propagators = self.Propagators( + with_viscosity=with_viscosity, + with_resistivity=with_resistivity, + ) - @staticmethod - def propagators_dct(): - return { - propagators_fields.VariationalDensityEvolve: ["mhd_rho3", "mhd_uv"], - propagators_fields.VariationalMomentumAdvection: ["mhd_uv"], - propagators_fields.VariationalQBEvolve: ["mhd_q3", "b2", "mhd_uv"], - propagators_fields.VariationalViscosity: ["mhd_q3", "mhd_uv"], - propagators_fields.VariationalResistivity: ["mhd_q3", "b2"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - def __init__(self, params, comm, clone_config=None): - from struphy.feec.projectors import L2Projector - from struphy.feec.variational_utilities import H1vecMassMatrix_density - from struphy.polar.basic import PolarVector - - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) - - self.WMM = H1vecMassMatrix_density(self.derham, self.mass_ops, self.domain) - - # Initialize propagators/integrators used in splitting substeps - lin_solver_density = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["lin_solver"] - nonlin_solver_density = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["nonlin_solver"] - lin_solver_momentum = params["fluid"]["mhd"]["options"]["VariationalMomentumAdvection"]["lin_solver"] - nonlin_solver_momentum = params["fluid"]["mhd"]["options"]["VariationalMomentumAdvection"]["nonlin_solver"] - lin_solver_magfield = params["fluid"]["mhd"]["options"]["VariationalQBEvolve"]["lin_solver"] - nonlin_solver_magfield = params["fluid"]["mhd"]["options"]["VariationalQBEvolve"]["nonlin_solver"] - lin_solver_viscosity = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["lin_solver"] - nonlin_solver_viscosity = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["nonlin_solver"] - lin_solver_resistivity = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["lin_solver"] - nonlin_solver_resistivity = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["nonlin_solver"] - if "linearize_current" in params["fluid"]["mhd"]["options"]["VariationalResistivity"].keys(): - self._linearize_current = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["linearize_current"] - else: - self._linearize_current = False - self._gamma = params["fluid"]["mhd"]["options"]["VariationalDensityEvolve"]["physics"]["gamma"] - self._mu = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["mu"] - self._mu_a = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["mu_a"] - self._alpha = params["fluid"]["mhd"]["options"]["VariationalViscosity"]["physics"]["alpha"] - self._eta = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["physics"]["eta"] - self._eta_a = params["fluid"]["mhd"]["options"]["VariationalResistivity"]["physics"]["eta_a"] - model = "deltaf_q" - - # set keyword arguments for propagators - self._kwargs[propagators_fields.VariationalDensityEvolve] = { - "model": model, - "gamma": self._gamma, - "mass_ops": self.WMM, - "lin_solver": lin_solver_density, - "nonlin_solver": nonlin_solver_density, - } - - self._kwargs[propagators_fields.VariationalMomentumAdvection] = { - "mass_ops": self.WMM, - "lin_solver": lin_solver_momentum, - "nonlin_solver": nonlin_solver_momentum, - } - - self._kwargs[propagators_fields.VariationalQBEvolve] = { - "model": model, - "mass_ops": self.WMM, - "lin_solver": lin_solver_magfield, - "nonlin_solver": nonlin_solver_magfield, - "gamma": self._gamma, - "div_u": self.pointer["div_u"], - "u2": self.pointer["u2"], - "bt2": self.pointer["bt2"], - "qt3": self.pointer["qt3"], - } - - self._kwargs[propagators_fields.VariationalViscosity] = { - "model": model, - "rho": self.pointer["mhd_rho3"], - "gamma": self._gamma, - "mu": self._mu, - "mu_a": self._mu_a, - "alpha": self._alpha, - "mass_ops": self.WMM, - "lin_solver": lin_solver_viscosity, - "nonlin_solver": nonlin_solver_viscosity, - "pt3": self.pointer["qt3"], - } - - self._kwargs[propagators_fields.VariationalResistivity] = { - "model": model, - "rho": self.pointer["mhd_rho3"], - "gamma": self._gamma, - "eta": self._eta, - "eta_a": self._eta_a, - "lin_solver": lin_solver_resistivity, - "nonlin_solver": nonlin_solver_resistivity, - "linearize_current": self._linearize_current, - "pt3": self.pointer["qt3"], - } - - # Initialize propagators used in splitting substeps - self.init_propagators() - - # Scalar variables to be saved during simulation + # 3. assign variables to propagators + self.propagators.variat_dens.variables.rho = self.mhd.density + self.propagators.variat_dens.variables.u = self.mhd.velocity + self.propagators.variat_mom.variables.u = self.mhd.velocity + self.propagators.variat_qb.variables.u = self.mhd.velocity + self.propagators.variat_qb.variables.q = self.mhd.sqrt_p + self.propagators.variat_qb.variables.b = self.em_fields.b_field + if with_viscosity: + self.propagators.variat_viscous.variables.s = self.mhd.sqrt_p + self.propagators.variat_viscous.variables.u = self.mhd.velocity + if with_resistivity: + self.propagators.variat_resist.variables.s = self.mhd.sqrt_p + self.propagators.variat_resist.variables.b = self.em_fields.b_field + + # define scalars for update_scalar_quantities self.add_scalar("en_U") - self.add_scalar("en_thermo_1") - self.add_scalar("en_thermo_2") self.add_scalar("en_mag_1") self.add_scalar("en_mag_2") + self.add_scalar("en_thermo_1") + self.add_scalar("en_thermo_2") self.add_scalar("en_tot") - # self.add_scalar("dens_tot") - # self.add_scalar("tot_div_B") + @property + def bulk_species(self): + return self.mhd - # self.add_scalar("en_tot_l1") - # self.add_scalar("en_thermo_l1") - # self.add_scalar("en_mag_l1") + @property + def velocity_scale(self): + return "alfvén" - # temporary vectors for scalar quantities - self._tmp_div_B = self.derham.Vh_pol["3"].zeros() - tmp_dof = self.derham.Vh_pol["3"].zeros() - projV3 = L2Projector("L2", self.mass_ops) + def allocate_helpers(self): + projV3 = L2Projector("L2", self._mass_ops) - self._integrator = projV3(self.domain.jacobian_det, dofs=tmp_dof) + def f(e1, e2, e3): + return 1 + + f = xp.vectorize(f) + self._integrator = projV3(f) self._ones = self.derham.Vh_pol["3"].zeros() if isinstance(self._ones, PolarVector): @@ -2123,57 +2096,95 @@ def __init__(self, params, comm, clone_config=None): else: self._ones[:] = 1.0 + self._tmp_div_B = self.derham.Vh_pol["3"].zeros() + def update_scalar_quantities(self): - # Update mass matrix - en_U = 0.5 * self.WMM.massop.dot_inner(self.pointer["mhd_uv"], self.pointer["mhd_uv"]) + rho = self.mhd.density.spline.vector + u = self.mhd.velocity.spline.vector + q = self.mhd.sqrt_p.spline.vector + b = self.em_fields.b_field.spline.vector + bt2 = self.propagators.variat_qb.options.bt2.spline.vector + qt3 = self.propagators.variat_qb.options.qt3.spline.vector + + gamma = self.propagators.variat_qb.options.gamma + + en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) - en_mag1 = 0.5 * self._mass_ops.M2.dot_inner(self.pointer["b2"], self.pointer["b2"]) + en_mag1 = 0.5 * self.mass_ops.M2.dot_inner(b, b) self.update_scalar("en_mag_1", en_mag1) - en_mag2 = 0.5 * self._mass_ops.M2.dot_inner(self.pointer["bt2"], self.projected_equil.b2) + en_mag2 = self.mass_ops.M2.dot_inner(bt2, self.projected_equil.b2) self.update_scalar("en_mag_2", en_mag2) - en_th_1 = 1 / (self._gamma - 1) * self._mass_ops.M3.dot_inner(self.pointer["mhd_q3"], self.pointer["mhd_q3"]) + en_th_1 = 1.0 / (gamma - 1.0) * self.mass_ops.M3.dot_inner(q, q) self.update_scalar("en_thermo_1", en_th_1) - en_th_2 = 2 / (self._gamma - 1) * self._mass_ops.M3.dot_inner(self.pointer["qt3"], self.projected_equil.q3) + en_th_2 = 2.0 / (gamma - 1.0) * self.mass_ops.M3.dot_inner(qt3, self.projected_equil.q3) self.update_scalar("en_thermo_2", en_th_2) en_tot = en_U + en_th_1 + en_th_2 + en_mag1 + en_mag2 self.update_scalar("en_tot", en_tot) - # dens_tot = self._ones.dot(self.pointer["mhd_rho3"]) - # self.update_scalar("dens_tot", dens_tot) - - # div_B = self.derham.div.dot(self.pointer["b2"], out=self._tmp_div_B) - # L2_div_B = self._mass_ops.M3.dot_inner(div_B, div_B) - # self.update_scalar("tot_div_B", L2_div_B) - - # en_thermo_l1 = self._integrator.dot(self.mass_ops.M3.dot(self.pointer["mhd_p3"])) / (self._gamma - 1.0) - # self.update_scalar("en_thermo_l1", en_thermo_l1) - - # wb2 = self._mass_ops.M2.dot(self.pointer["b2"], out=self._tmp_wb2) - # en_mag_l1 = wb2.dot(self.projected_equil.b2) - # self.update_scalar("en_mag_l1", en_mag_l1) - - # en_tot_l1 = en_thermo_l1 + en_mag_l1 - # self.update_scalar("en_tot_l1", en_tot_l1) - - @staticmethod - def diagnostics_dct(): - dct = {} - dct["bt2"] = "Hdiv" - dct["qt3"] = "L2" - dct["div_u"] = "L2" - dct["u2"] = "Hdiv" - return dct - - __diagnostics__ = diagnostics_dct() - - -class IsothermalEulerSPH(StruphyModel): - r"""Isothermal Euler equations discretized with smoothed particle hydrodynamics (SPH). + # default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "variat_dens.Options" in line: + new_file += [ + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='deltaf_q')\n" + ] + elif "variat_qb.Options" in line: + new_file += [ + "model.propagators.variat_qb.options = model.propagators.variat_qb.Options(model='deltaf_q',\n" + ] + new_file += [ + " div_u=model.diagnostics.div_u,\n" + ] + new_file += [ + " u2=model.diagnostics.u2,\n" + ] + new_file += [ + " qt3=model.diagnostics.qt3,\n" + ] + new_file += [ + " bt2=model.diagnostics.bt2)\n" + ] + elif "variat_viscous.Options" in line: + new_file += [ + "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(model='deltaf_q',\n" + ] + new_file += [ + " rho=model.mhd.density,\n" + ] + new_file += [ + " pt3=model.diagnostics.qt3)\n" + ] + elif "variat_resist.Options" in line: + new_file += [ + "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(model='deltaf_q',\n" + ] + new_file += [ + " rho=model.mhd.density,\n" + ] + new_file += [ + " pt3=model.diagnostics.qt3)\n" + ] + elif "sqrt_p.add_background" in line: + new_file += ["model.mhd.density.add_background(FieldsBackground())\n"] + new_file += [line] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) + + +class EulerSPH(StruphyModel): + r"""Euler equations discretized with smoothed particle hydrodynamics (SPH). :ref:`normalization`: @@ -2193,222 +2204,107 @@ class IsothermalEulerSPH(StruphyModel): \partial_t S + \mathbf u \cdot \nabla S &= 0\,, \end{align} - where :math:`S` denotes the entropy per unit mass and the internal energy per unit mass is + where :math:`S` denotes the entropy per unit mass. + The internal energy per unit mass can be defined in two ways: .. math:: - \mathcal U(\rho, S) = \kappa(S) \log \rho\,. + \mathrm{"isothermal:"}\qquad &\mathcal U(\rho, S) = \kappa(S) \log \rho\,. + + \mathrm{"polytropic:"}\qquad &\mathcal U(\rho, S) = \kappa(S) \frac{\rho^{\gamma - 1}}{\gamma - 1}\,. :ref:`propagators` (called in sequence): 1. :class:`~struphy.propagators.propagators_markers.PushEta` 2. :class:`~struphy.propagators.propagators_markers.PushVxB` 3. :class:`~struphy.propagators.propagators_markers.PushVinSPHpressure` - - :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} - - dct["kinetic"]["euler_fluid"] = "ParticlesSPH" - return dct - - @staticmethod - def bulk_species(): - return "euler_fluid" - - @staticmethod - def velocity_scale(): - return "thermal" - - # @staticmethod - # def diagnostics_dct(): - # dct = {} - # dct["projected_density"] = "L2" - # return dct - - @staticmethod - def propagators_dct(): - return { - propagators_markers.PushEta: ["euler_fluid"], - # propagators_markers.PushVxB: ["euler_fluid"], - propagators_markers.PushVinSPHpressure: ["euler_fluid"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - def __init__(self, params, comm, clone_config=None): - super().__init__(params, comm=comm, clone_config=clone_config) - - # prelim - _p = params["kinetic"]["euler_fluid"] - algo_eta = _p["options"]["PushEta"]["algo"] - # algo_vxb = _p["options"]["PushVxB"]["algo"] - kernel_type = _p["options"]["PushVinSPHpressure"]["kernel_type"] - algo_sph = _p["options"]["PushVinSPHpressure"]["algo"] - gravity = _p["options"]["PushVinSPHpressure"]["gravity"] - thermodynamics = _p["options"]["PushVinSPHpressure"]["thermodynamics"] - - # magnetic field - # self._b_eq = self.projected_equil.b2 - - # set keyword arguments for propagators - self._kwargs[propagators_markers.PushEta] = { - "algo": algo_eta, - # "density_field": self.pointer["projected_density"], - } - - # self._kwargs[propagators_markers.PushVxB] = { - # "algo": algo_vxb, - # "kappa": 1.0, - # "b2": self._b_eq, - # "b2_add": None, - # } - - self._kwargs[propagators_markers.PushVinSPHpressure] = { - "kernel_type": kernel_type, - "algo": algo_sph, - "gravity": gravity, - "thermodynamics": thermodynamics, - } - - # Initialize propagators used in splitting substeps - self.init_propagators() - - # Scalar variables to be saved during simulation - self.add_scalar("en_kin", compute="from_sph", species="euler_fluid") - - def update_scalar_quantities(self): - valid_markers = self.pointer["euler_fluid"].markers_wo_holes_and_ghost - en_kin = valid_markers[:, 6].dot( - valid_markers[:, 3] ** 2 + valid_markers[:, 4] ** 2 + valid_markers[:, 5] ** 2 - ) / (2.0 * self.pointer["euler_fluid"].Np) - self.update_scalar("en_kin", en_kin) - - -class ViscousEulerSPH(StruphyModel): - r"""Isothermal Euler equations discretized with smoothed particle hydrodynamics (SPH). - - :ref:`normalization`: - - .. math:: - - \hat u = \hat v_\textnormal{th} \,. - - :ref:`Equations `: + ## species - .. math:: + class EulerFluid(ParticleSpecies): + def __init__(self): + self.var = SPHVariable() + self.init_variables() - \begin{align} - \partial_t \rho + \nabla \cdot (\rho \mathbf u) &= 0\,, - \\[2mm] - \rho(\partial_t \mathbf u + \mathbf u \cdot \nabla \mathbf u) &= - \nabla \left(\rho^2 \frac{\partial \mathcal U(\rho, S)}{\partial \rho} \right)\,, - \\[2mm] - \partial_t S + \mathbf u \cdot \nabla S &= 0\,, - \end{align} + ## propagators - where :math:`S` denotes the entropy per unit mass and the internal energy per unit mass is + class Propagators: + def __init__(self, with_B0: bool = True): + self.push_eta = propagators_markers.PushEta() + if with_B0: + self.push_vxb = propagators_markers.PushVxB() + self.push_sph_p = propagators_markers.PushVinSPHpressure() - .. math:: + ## abstract methods - \mathcal U(\rho, S) = \kappa(S) \log \rho\,. + def __init__(self, with_B0: bool = True): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") - :ref:`propagators` (called in sequence): + self.with_B0 = with_B0 - 1. :class:`~struphy.propagators.propagators_markers.PushEta` - 2. :class:`~struphy.propagators.propagators_markers.PushVinSPHpressure` + # 1. instantiate all species + self.euler_fluid = self.EulerFluid() - :ref:`Model info `: - """ + # 2. instantiate all propagators + self.propagators = self.Propagators(with_B0=with_B0) - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} + # 3. assign variables to propagators + self.propagators.push_eta.variables.var = self.euler_fluid.var + if with_B0: + self.propagators.push_vxb.variables.ions = self.euler_fluid.var + self.propagators.push_sph_p.variables.fluid = self.euler_fluid.var - dct["kinetic"]["euler_fluid"] = "ParticlesSPH" - return dct + # define scalars for update_scalar_quantities + self.add_scalar("en_kin", compute="from_sph", variable=self.euler_fluid.var) - @staticmethod - def bulk_species(): - return "euler_fluid" + @property + def bulk_species(self): + return self.euler_fluid - @staticmethod - def velocity_scale(): + @property + def velocity_scale(self): return "thermal" + def allocate_helpers(self): + pass + # @staticmethod # def diagnostics_dct(): # dct = {} # dct["projected_density"] = "L2" # return dct - @staticmethod - def propagators_dct(): - return { - propagators_markers.PushEta: ["euler_fluid"], - propagators_markers.PushVinSPHpressure: ["euler_fluid"], - propagators_markers.PushVinViscousPotential: ["euler_fluid"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - def __init__(self, params, comm, clone_config=None): - super().__init__(params, comm=comm, clone_config=clone_config) - - # prelim - _p = params["kinetic"]["euler_fluid"] - algo_eta = _p["options"]["PushEta"]["algo"] - kernel_type_1 = _p["options"]["PushVinSPHpressure"]["kernel_type"] - algo_sph = _p["options"]["PushVinSPHpressure"]["algo"] - gravity = _p["options"]["PushVinSPHpressure"]["gravity"] - thermodynamics = _p["options"]["PushVinSPHpressure"]["thermodynamics"] - kernel_type_2 = _p["options"]["PushVinViscousPotential"]["kernel_type"] - kernel_width = _p["options"]["PushVinViscousPotential"]["kernel_width"] - - # set keyword arguments for propagators - self._kwargs[propagators_markers.PushEta] = { - "algo": algo_eta, - # "density_field": self.pointer["projected_density"], - } - - self._kwargs[propagators_markers.PushVinSPHpressure] = { - "kernel_type": kernel_type_1, - "algo": algo_sph, - "gravity": gravity, - "thermodynamics": thermodynamics, - } - - self._kwargs[propagators_markers.PushVinViscousPotential] = { - "kernel_type": kernel_type_2, - "kernel_width": kernel_width, - "algo": algo_sph, - } - - # Initialize propagators used in splitting substeps - self.init_propagators() - - # Scalar variables to be saved during simulation - self.add_scalar("en_kin", compute="from_sph", species="euler_fluid") - def update_scalar_quantities(self): - valid_markers = self.pointer["euler_fluid"].markers_wo_holes_and_ghost + particles = self.euler_fluid.var.particles + valid_markers = particles.markers_wo_holes_and_ghost en_kin = valid_markers[:, 6].dot( valid_markers[:, 3] ** 2 + valid_markers[:, 4] ** 2 + valid_markers[:, 5] ** 2 - ) / (2.0 * self.pointer["euler_fluid"].Np) + ) / (2.0 * particles.Np) self.update_scalar("en_kin", en_kin) + ## default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "push_vxb.Options" in line: + new_file += ["if model.with_B0:\n"] + new_file += [" " + line] + elif "set_save_data" in line: + new_file += ["\nkd_plot = KernelDensityPlot()\n"] + new_file += ["model.euler_fluid.set_save_data(kernel_density_plots=(kd_plot,))\n"] + elif "base_units = BaseUnits" in line: + new_file += ["base_units = BaseUnits(kBT=1.0)\n"] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) + class HasegawaWakatani(StruphyModel): r"""Hasegawa-Wakatani equations in 2D. @@ -2440,119 +2336,102 @@ class HasegawaWakatani(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} + ## species - dct["em_fields"] = {"phi0": "H1"} - dct["fluid"]["hw"] = { - "n0": "H1", - "omega0": "H1", - } - return dct + class EMFields(FieldSpecies): + def __init__(self): + self.phi = FEECVariable(space="H1") + self.init_variables() - @staticmethod - def bulk_species(): - return "hw" + class Plasma(FluidSpecies): + def __init__(self): + self.density = FEECVariable(space="H1") + self.vorticity = FEECVariable(space="H1") + self.init_variables() - @staticmethod - def velocity_scale(): - return "alfvén" + ## propagators - # @staticmethod - # def diagnostics_dct(): - # dct = {} - # dct["projected_density"] = "L2" - # return dct + class Propagators: + def __init__(self): + self.poisson = propagators_fields.Poisson() + self.hw = propagators_fields.HasegawaWakatani() + + ## abstract methods + + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + + # 1. instantiate all species + self.em_fields = self.EMFields() + self.plasma = self.Plasma() + + # 2. instantiate all propagators + self.propagators = self.Propagators() + + # 3. assign variables to propagators + self.propagators.poisson.variables.phi = self.em_fields.phi + self.propagators.hw.variables.n = self.plasma.density + self.propagators.hw.variables.omega = self.plasma.vorticity + + # define scalars for update_scalar_quantities - @staticmethod - def propagators_dct(): - return { - propagators_fields.Poisson: ["phi0"], - propagators_fields.HasegawaWakatani: ["hw_n0", "hw_omega0"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - def __init__(self, params, comm, clone_config=None): - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) - - from struphy.polar.basic import PolarVector - - # extract necessary parameters - self._stab_eps = params["em_fields"]["options"]["Poisson"]["stabilization"]["stab_eps"] - self._stab_mat = params["em_fields"]["options"]["Poisson"]["stabilization"]["stab_mat"] - self._solver = params["em_fields"]["options"]["Poisson"]["solver"] - c_fun = params["fluid"]["hw"]["options"]["HasegawaWakatani"]["c_fun"] - kappa = params["fluid"]["hw"]["options"]["HasegawaWakatani"]["kappa"] - nu = params["fluid"]["hw"]["options"]["HasegawaWakatani"]["nu"] - algo = params["fluid"]["hw"]["options"]["HasegawaWakatani"]["algo"] - M0_solver = params["fluid"]["hw"]["options"]["HasegawaWakatani"]["M0_solver"] - - # rhs of Poisson - self._rho = self.derham.Vh["0"].zeros() + @property + def bulk_species(self): + return self.plasma + + @property + def velocity_scale(self): + return "alfvén" + + def allocate_helpers(self): + self._rho: StencilVector = self.derham.Vh["0"].zeros() self.update_rho() - # set keyword arguments for propagators - self._kwargs[propagators_fields.Poisson] = { - "stab_eps": self._stab_eps, - "stab_mat": self._stab_mat, - "rho": self.update_rho, - "solver": self._solver, - } - - self._kwargs[propagators_fields.HasegawaWakatani] = { - "phi": self.em_fields["phi0"]["obj"], - "c_fun": c_fun, - "kappa": kappa, - "nu": nu, - "algo": algo, - "M0_solver": M0_solver, - } - - # Initialize propagators used in splitting substeps - self.init_propagators() + def update_scalar_quantities(self): + pass def update_rho(self): - self._rho = self.mass_ops.M0.dot(self.pointer["hw_omega0"], out=self._rho) + omega = self.plasma.vorticity.spline.vector + self._rho = self.mass_ops.M0.dot(omega, out=self._rho) self._rho.update_ghost_regions() return self._rho - def initialize_from_params(self): + def allocate_propagators(self): """Solve initial Poisson equation. :meta private: """ # initialize fields and particles - super().initialize_from_params() + super().allocate_propagators() - if self.rank_world == 0: + if MPI.COMM_WORLD.Get_rank() == 0: print("\nINITIAL POISSON SOLVE:") - # Instantiate Poisson solver - poisson_solver = propagators_fields.Poisson( - self.pointer["phi0"], - stab_eps=self._stab_eps, - stab_mat=self._stab_mat, - rho=self._rho, - solver=self._solver, - ) - - # Solve with dt=1. and compute electric field - if self.rank_world == 0: - print("\nSolving initial Poisson problem...") - self.update_rho() - poisson_solver(1.0) + self.propagators.poisson(1.0) - if self.rank_world == 0: + if MPI.COMM_WORLD.Get_rank() == 0: print("Done.") def update_scalar_quantities(self): pass + + # default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "hw.Options" in line: + new_file += [ + "model.propagators.hw.options = model.propagators.hw.Options(phi=model.em_fields.phi)\n" + ] + elif "vorticity.add_background" in line: + new_file += ["model.plasma.density.add_background(FieldsBackground())\n"] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) diff --git a/src/struphy/models/hybrid.py b/src/struphy/models/hybrid.py index 539f4d285..6994534f9 100644 --- a/src/struphy/models/hybrid.py +++ b/src/struphy/models/hybrid.py @@ -1,9 +1,17 @@ +import cunumpy as xp +from psydac.ddm.mpi import mpi as MPI + from struphy.models.base import StruphyModel +from struphy.models.species import FieldSpecies, FluidSpecies, ParticleSpecies +from struphy.models.variables import FEECVariable, PICVariable, SPHVariable, Variable from struphy.pic.accumulation import accum_kernels, accum_kernels_gc +from struphy.pic.accumulation.particles_to_grid import AccumulatorVector +from struphy.polar.basic import PolarVector from struphy.propagators import propagators_coupling, propagators_fields, propagators_markers -from struphy.utils.arrays import xp as np from struphy.utils.pyccel import Pyccelkernel +rank = MPI.COMM_WORLD.Get_rank() + class LinearMHDVlasovCC(StruphyModel): r""" @@ -61,182 +69,109 @@ class LinearMHDVlasovCC(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} + ## species - dct["em_fields"]["b_field"] = "Hdiv" - dct["fluid"]["mhd"] = {"density": "L2", "velocity": "Hdiv", "pressure": "L2"} - dct["kinetic"]["energetic_ions"] = "Particles6D" - return dct + class EMFields(FieldSpecies): + def __init__(self): + self.b_field = FEECVariable(space="Hdiv") + self.init_variables() - @staticmethod - def bulk_species(): - return "mhd" + class MHD(FluidSpecies): + def __init__(self): + self.density = FEECVariable(space="L2") + self.velocity = FEECVariable(space="Hdiv") + self.pressure = FEECVariable(space="L2") + self.init_variables() - @staticmethod - def velocity_scale(): - return "alfvén" + class EnergeticIons(ParticleSpecies): + def __init__(self): + self.var = PICVariable(space="Particles6D") + self.init_variables() - @staticmethod - def propagators_dct(): - return { - propagators_fields.CurrentCoupling6DDensity: ["mhd_velocity"], - propagators_fields.ShearAlfven: ["mhd_velocity", "b_field"], - propagators_coupling.CurrentCoupling6DCurrent: ["energetic_ions", "mhd_velocity"], - propagators_markers.PushEta: ["energetic_ions"], - propagators_markers.PushVxB: ["energetic_ions"], - propagators_fields.Magnetosonic: ["mhd_density", "mhd_velocity", "mhd_pressure"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - # add special options - @classmethod - def options(cls): - dct = super().options() - cls.add_option( - species=["fluid", "mhd"], - key="u_space", - option="Hdiv", - dct=dct, - ) - return dct + ## propagators - def __init__(self, params, comm, clone_config=None): - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) + class Propagators: + def __init__(self): + self.couple_dens = propagators_fields.CurrentCoupling6DDensity() + self.shear_alf = propagators_fields.ShearAlfven() + self.couple_curr = propagators_coupling.CurrentCoupling6DCurrent() + self.push_eta = propagators_markers.PushEta() + self.push_vxb = propagators_markers.PushVxB() + self.mag_sonic = propagators_fields.Magnetosonic() - from struphy.polar.basic import PolarVector + ## abstract methods - # prelim - e_ions_params = self.kinetic["energetic_ions"]["params"] + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") - # extract necessary parameters - u_space = params["fluid"]["mhd"]["options"]["u_space"] - params_alfven = params["fluid"]["mhd"]["options"]["ShearAlfven"] - params_sonic = params["fluid"]["mhd"]["options"]["Magnetosonic"] - params_eta = params["kinetic"]["energetic_ions"]["options"]["PushEta"] - params_vxb = params["kinetic"]["energetic_ions"]["options"]["PushVxB"] - params_density = params["fluid"]["mhd"]["options"]["CurrentCoupling6DDensity"] - params_current = params["kinetic"]["energetic_ions"]["options"]["CurrentCoupling6DCurrent"] + # 1. instantiate all species + self.em_fields = self.EMFields() + self.mhd = self.MHD() + self.energetic_ions = self.EnergeticIons() - # compute coupling parameters - Ab = params["fluid"]["mhd"]["phys_params"]["A"] - Ah = params["kinetic"]["energetic_ions"]["phys_params"]["A"] - epsilon = self.equation_params["energetic_ions"]["epsilon"] + # 2. instantiate all propagators + self.propagators = self.Propagators() - if abs(epsilon - 1) < 1e-6: - epsilon = 1.0 + # 3. assign variables to propagators + self.propagators.couple_dens.variables.u = self.mhd.velocity - self._Ab = Ab - self._Ah = Ah + self.propagators.shear_alf.variables.u = self.mhd.velocity + self.propagators.shear_alf.variables.b = self.em_fields.b_field - # add control variate to mass_ops object - if self.pointer["energetic_ions"].control_variate: - self.mass_ops.weights["f0"] = self.pointer["energetic_ions"].f0 - - # project background magnetic field (2-form) and background pressure (3-form) - self._b_eq = self.derham.P["2"]( - [ - self.equil.b2_1, - self.equil.b2_2, - self.equil.b2_3, - ] - ) - self._p_eq = self.derham.P["3"](self.equil.p3) - self._ones = self._p_eq.space.zeros() + self.propagators.couple_curr.variables.ions = self.energetic_ions.var + self.propagators.couple_curr.variables.u = self.mhd.velocity - if isinstance(self._ones, PolarVector): - self._ones.tp[:] = 1.0 - else: - self._ones[:] = 1.0 + self.propagators.push_eta.variables.var = self.energetic_ions.var + self.propagators.push_vxb.variables.ions = self.energetic_ions.var - # set keyword arguments for propagators - if params_density["turn_off"]: - self._kwargs[propagators_fields.CurrentCoupling6DDensity] = None - else: - self._kwargs[propagators_fields.CurrentCoupling6DDensity] = { - "particles": self.pointer["energetic_ions"], - "u_space": u_space, - "b_eq": self._b_eq, - "b_tilde": self.pointer["b_field"], - "Ab": Ab, - "Ah": Ah, - "epsilon": epsilon, - "solver": params_density["solver"], - "filter": params_density["filter"], - "boundary_cut": params_density["boundary_cut"], - } - - if params_alfven["turn_off"]: - self._kwargs[propagators_fields.ShearAlfven] = None - else: - self._kwargs[propagators_fields.ShearAlfven] = { - "u_space": u_space, - "solver": params_alfven["solver"], - } + self.propagators.mag_sonic.variables.n = self.mhd.density + self.propagators.mag_sonic.variables.u = self.mhd.velocity + self.propagators.mag_sonic.variables.p = self.mhd.pressure - if params_current["turn_off"]: - self._kwargs[propagators_coupling.CurrentCoupling6DCurrent] = None - else: - self._kwargs[propagators_coupling.CurrentCoupling6DCurrent] = { - "u_space": u_space, - "b_eq": self._b_eq, - "b_tilde": self.pointer["b_field"], - "Ab": Ab, - "Ah": Ah, - "epsilon": epsilon, - "solver": params_current["solver"], - "filter": params_current["filter"], - "boundary_cut": params_current["boundary_cut"], - } - - self._kwargs[propagators_markers.PushEta] = { - "algo": params_eta["algo"], - } - - self._kwargs[propagators_markers.PushVxB] = { - "algo": params_vxb["algo"], - "kappa": 1.0 / epsilon, - "b2": self.pointer["b_field"], - "b2_add": self._b_eq, - } - - if params_sonic["turn_off"]: - self._kwargs[propagators_fields.Magnetosonic] = None - else: - self._kwargs[propagators_fields.Magnetosonic] = { - "u_space": u_space, - "b": self.pointer["b_field"], - "solver": params_sonic["solver"], - } - - # Initialize propagators used in splitting substeps - self.init_propagators() - - # Scalar variables to be saved during simulation: + # define scalars for update_scalar_quantities self.add_scalar("en_U", compute="from_field") self.add_scalar("en_p", compute="from_field") self.add_scalar("en_B", compute="from_field") - self.add_scalar("en_f", compute="from_particles", species="energetic_ions") + self.add_scalar("en_f", compute="from_particles", variable=self.energetic_ions.var) self.add_scalar("en_tot", summands=["en_U", "en_p", "en_B", "en_f"]) - self.add_scalar("n_lost_particles", compute="from_particles", species="energetic_ions") + self.add_scalar("n_lost_particles", compute="from_particles", variable=self.energetic_ions.var) + + @property + def bulk_species(self): + return self.mhd - # temporary vectors for scalar quantities: - self._tmp = np.empty(1, dtype=float) - self._n_lost_particles = np.empty(1, dtype=float) + @property + def velocity_scale(self): + return "alfvén" + + def allocate_helpers(self): + self._ones = self.projected_equil.p3.space.zeros() + if isinstance(self._ones, PolarVector): + self._ones.tp[:] = 1.0 + else: + self._ones[:] = 1.0 + + self._tmp = xp.empty(1, dtype=float) + self._n_lost_particles = xp.empty(1, dtype=float) + + # add control variate to mass_ops object + if self.energetic_ions.var.particles.control_variate: + self.mass_ops.weights["f0"] = self.energetic_ions.var.particles.f0 + + self._Ah = self.energetic_ions.mass_number + self._Ab = self.mhd.mass_number def update_scalar_quantities(self): # perturbed fields - en_U = 0.5 * self.mass_ops.M2n.dot_inner(self.pointer["mhd_velocity"], self.pointer["mhd_velocity"]) - en_B = 0.5 * self.mass_ops.M2.dot_inner(self.pointer["b_field"], self.pointer["b_field"]) - en_p = self.pointer["mhd_pressure"].inner(self._ones) / (5 / 3 - 1) + u = self.mhd.velocity.spline.vector + p = self.mhd.pressure.spline.vector + b = self.em_fields.b_field.spline.vector + particles = self.energetic_ions.var.particles + + en_U = 0.5 * self.mass_ops.M2n.dot_inner(u, u) + en_B = 0.5 * self.mass_ops.M2.dot_inner(b, b) + en_p = p.inner(self._ones) / (5 / 3 - 1) self.update_scalar("en_U", en_U) self.update_scalar("en_B", en_B) @@ -246,12 +181,10 @@ def update_scalar_quantities(self): self._tmp[0] = ( self._Ah / self._Ab - * self.pointer["energetic_ions"] - .markers_wo_holes[:, 6] - .dot( - self.pointer["energetic_ions"].markers_wo_holes[:, 3] ** 2 - + self.pointer["energetic_ions"].markers_wo_holes[:, 4] ** 2 - + self.pointer["energetic_ions"].markers_wo_holes[:, 5] ** 2, + * particles.markers_wo_holes[:, 6].dot( + particles.markers_wo_holes[:, 3] ** 2 + + particles.markers_wo_holes[:, 4] ** 2 + + particles.markers_wo_holes[:, 5] ** 2, ) / (2) ) @@ -260,16 +193,47 @@ def update_scalar_quantities(self): self.update_scalar("en_tot", en_U + en_B + en_p + self._tmp[0]) # Print number of lost ions - self._n_lost_particles[0] = self.pointer["energetic_ions"].n_lost_markers + self._n_lost_particles[0] = particles.n_lost_markers self.update_scalar("n_lost_particles", self._n_lost_particles[0]) - if self.rank_world == 0: + if rank == 0: print( "ratio of lost particles: ", - self._n_lost_particles[0] / self.pointer["energetic_ions"].Np * 100, + self._n_lost_particles[0] / particles.Np * 100, "%", ) + ## default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "mag_sonic.Options" in line: + new_file += [ + "model.propagators.mag_sonic.options = model.propagators.mag_sonic.Options(b_field=model.em_fields.b_field)\n" + ] + elif "couple_dens.Options" in line: + new_file += [ + "model.propagators.couple_dens.options = model.propagators.couple_dens.Options(energetic_ions=model.energetic_ions.var,\n" + ] + new_file += [ + " b_tilde=model.em_fields.b_field)\n" + ] + elif "couple_curr.Options" in line: + new_file += [ + "model.propagators.couple_curr.options = model.propagators.couple_curr.Options(b_tilde=model.em_fields.b_field)\n" + ] + elif "set_save_data" in line: + new_file += ["\nbinplot = BinningPlot(slice='e1', n_bins=128, ranges=(0.0, 1.0))\n"] + new_file += ["model.energetic_ions.set_save_data(binning_plots=(binplot,))\n"] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) + class LinearMHDVlasovPC(StruphyModel): r""" @@ -333,209 +297,192 @@ class LinearMHDVlasovPC(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} - - dct["em_fields"]["b_field"] = "Hdiv" - dct["fluid"]["mhd"] = { - "density": "L2", - "velocity": "Hdiv", - "pressure": "L2", - } - dct["kinetic"]["energetic_ions"] = "Particles6D" - return dct - - @staticmethod - def bulk_species(): - return "mhd" - - @staticmethod - def velocity_scale(): - return "alfvén" - - @staticmethod - def propagators_dct(): - return { - propagators_markers.PushEtaPC: ["energetic_ions"], - propagators_markers.PushVxB: ["energetic_ions"], - propagators_coupling.PressureCoupling6D: ["energetic_ions", "mhd_velocity"], - propagators_fields.ShearAlfven: ["mhd_velocity", "b_field"], - propagators_fields.Magnetosonic: ["mhd_density", "mhd_velocity", "mhd_pressure"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - # add special options - @classmethod - def options(cls): - dct = super().options() - cls.add_option( - species=["fluid", "mhd"], - key="u_space", - option="Hdiv", - dct=dct, - ) - return dct - - def __init__(self, params, comm, clone_config=None): - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) - - from struphy.polar.basic import PolarVector - - # extract necessary parameters - u_space = params["fluid"]["mhd"]["options"]["u_space"] - params_alfven = params["fluid"]["mhd"]["options"]["ShearAlfven"] - params_sonic = params["fluid"]["mhd"]["options"]["Magnetosonic"] - params_vxb = params["kinetic"]["energetic_ions"]["options"]["PushVxB"] - params_pressure = params["kinetic"]["energetic_ions"]["options"]["PressureCoupling6D"] - - # use perp model - assert ( - params["kinetic"]["energetic_ions"]["options"]["PressureCoupling6D"]["use_perp_model"] - == params["kinetic"]["energetic_ions"]["options"]["PressureCoupling6D"]["use_perp_model"] + ## species + class EnergeticIons(ParticleSpecies): + def __init__(self): + self.var = PICVariable(space="Particles6D") + self.init_variables() + + class EMFields(FieldSpecies): + def __init__(self): + self.b_field = FEECVariable(space="Hdiv") + self.init_variables() + + class MHD(FluidSpecies): + def __init__(self): + self.density = FEECVariable(space="L2") + self.pressure = FEECVariable(space="L2") + self.velocity = FEECVariable(space="Hdiv") + self.init_variables() + + ## propagators + + class Propagators: + def __init__(self, turn_off: tuple[str, ...] = (None,)): + if not "PushEtaPC" in turn_off: + self.push_eta_pc = propagators_markers.PushEtaPC() + if not "PushVxB" in turn_off: + self.push_vxb = propagators_markers.PushVxB() + if not "PressureCoupling6D" in turn_off: + self.pc6d = propagators_coupling.PressureCoupling6D() + if not "ShearAlfven" in turn_off: + self.shearalfven = propagators_fields.ShearAlfven() + if not "Magnetosonic" in turn_off: + self.magnetosonic = propagators_fields.Magnetosonic() + + def __init__(self, turn_off: tuple[str, ...] = (None,)): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + + # 1. instantiate all species + self.em_fields = self.EMFields() + self.mhd = self.MHD() + self.energetic_ions = self.EnergeticIons() + + # 2. instantiate all propagators + self.propagators = self.Propagators(turn_off) + + # 3. assign variables to propagators + if not "ShearAlfven" in turn_off: + self.propagators.shearalfven.variables.u = self.mhd.velocity + self.propagators.shearalfven.variables.b = self.em_fields.b_field + if not "Magnetosonic" in turn_off: + self.propagators.magnetosonic.variables.n = self.mhd.density + self.propagators.magnetosonic.variables.u = self.mhd.velocity + self.propagators.magnetosonic.variables.p = self.mhd.pressure + if not "PressureCoupling6D" in turn_off: + self.propagators.pc6d.variables.u = self.mhd.velocity + self.propagators.pc6d.variables.energetic_ions = self.energetic_ions.var + if not "PushEtaPC" in turn_off: + self.propagators.push_eta_pc.variables.var = self.energetic_ions.var + if not "PushVxB" in turn_off: + self.propagators.push_vxb.variables.ions = self.energetic_ions.var + + # define scalars for update_scalar_quantities + self.add_scalar("en_U") + self.add_scalar("en_p") + self.add_scalar("en_B") + self.add_scalar("en_f", compute="from_particles", variable=self.energetic_ions.var) + self.add_scalar( + "en_tot", + summands=[ + "en_U", + "en_p", + "en_B", + "en_f", + ], ) - use_perp_model = params["kinetic"]["energetic_ions"]["options"]["PressureCoupling6D"]["use_perp_model"] - # compute coupling parameters - Ab = params["fluid"]["mhd"]["phys_params"]["A"] - Ah = params["kinetic"]["energetic_ions"]["phys_params"]["A"] - epsilon = self.equation_params["energetic_ions"]["epsilon"] + @property + def bulk_species(self): + return self.mhd - if abs(epsilon - 1) < 1e-6: - epsilon = 1.0 - - self._coupling_params = {} - self._coupling_params["Ab"] = Ab - self._coupling_params["Ah"] = Ah - self._coupling_params["epsilon"] = epsilon - - # add control variate to mass_ops object - if self.pointer["energetic_ions"].control_variate: - self.mass_ops.weights["f0"] = self.pointer["energetic_ions"].f0 - - # Project magnetic field - self._b_eq = self.derham.P["2"]( - [ - self.equil.b2_1, - self.equil.b2_2, - self.equil.b2_3, - ] - ) - self._p_eq = self.derham.P["3"](self.equil.p3) - self._ones = self._p_eq.space.zeros() + @property + def velocity_scale(self): + return "alfvén" + def allocate_helpers(self): + self._ones = self.projected_equil.p3.space.zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 else: self._ones[:] = 1.0 - # set keyword arguments for propagators - self._kwargs[propagators_markers.PushEtaPC] = { - "u": self.pointer["mhd_velocity"], - "use_perp_model": use_perp_model, - "u_space": u_space, - } - - self._kwargs[propagators_markers.PushVxB] = { - "algo": params_vxb["algo"], - "kappa": epsilon, - "b2": self.pointer["b_field"], - "b2_add": self._b_eq, - } - - if params_pressure["turn_off"]: - self._kwargs[propagators_coupling.PressureCoupling6D] = None - else: - self._kwargs[propagators_coupling.PressureCoupling6D] = { - "use_perp_model": use_perp_model, - "u_space": u_space, - "solver": params_pressure["solver"], - "coupling_params": self._coupling_params, - "filter": params_pressure["filter"], - "boundary_cut": params_pressure["boundary_cut"], - } - - if params_alfven["turn_off"]: - self._kwargs[propagators_fields.ShearAlfven] = None - else: - self._kwargs[propagators_fields.ShearAlfven] = { - "u_space": u_space, - "solver": params_alfven["solver"], - } - - if params_sonic["turn_off"]: - self._kwargs[propagators_fields.Magnetosonic] = None - else: - self._kwargs[propagators_fields.Magnetosonic] = { - "b": self.pointer["b_field"], - "u_space": u_space, - "solver": params_sonic["solver"], - } - - # Initialize propagators used in splitting substeps - self.init_propagators() - - # Scalar variables to be saved during simulation: - self.add_scalar("en_U", compute="from_field") - self.add_scalar("en_p", compute="from_field") - self.add_scalar("en_B", compute="from_field") - self.add_scalar("en_f", compute="from_particles", species="energetic_ions") - self.add_scalar("en_tot", summands=["en_U", "en_p", "en_B", "en_f"]) - self.add_scalar("n_lost_particles", compute="from_particles", species="energetic_ions") - - # temporary vectors for scalar quantities - self._tmp_u = self.derham.Vh["2"].zeros() - self._tmp_b1 = self.derham.Vh["2"].zeros() - self._tmp = np.empty(1, dtype=float) - self._n_lost_particles = np.empty(1, dtype=float) + self._en_f = xp.empty(1, dtype=float) + self._n_lost_particles = xp.empty(1, dtype=float) def update_scalar_quantities(self): + # scaling factor + Ab = self.mhd.mass_number + Ah = self.energetic_ions.var.species.mass_number + # perturbed fields - if "Hdiv" == "Hdiv": - en_U = 0.5 * self.mass_ops.M2n.dot_inner(self.pointer["mhd_velocity"], self.pointer["mhd_velocity"]) - else: - en_U = 0.5 * self.mass_ops.Mvn.dot_inner(self.pointer["mhd_velocity"], self.pointer["mhd_velocity"]) - en_B = 0.5 * self.mass_ops.M2.dot_inner(self.pointer["b_field"], self.pointer["b_field"]) - en_p = self.pointer["mhd_pressure"].inner(self._ones) / (5 / 3 - 1) + en_U = 0.5 * self.mass_ops.M2n.dot_inner( + self.mhd.velocity.spline.vector, + self.mhd.velocity.spline.vector, + ) + en_B = 0.5 * self.mass_ops.M2.dot_inner( + self.em_fields.b_field.spline.vector, + self.em_fields.b_field.spline.vector, + ) + en_p = self.mhd.pressure.spline.vector.inner(self._ones) / (5 / 3 - 1) self.update_scalar("en_U", en_U) self.update_scalar("en_B", en_B) self.update_scalar("en_p", en_p) - # particles - self._tmp[0] = ( - self._coupling_params["Ah"] - / self._coupling_params["Ab"] - * self.pointer["energetic_ions"] - .markers_wo_holes[:, 6] - .dot( - self.pointer["energetic_ions"].markers_wo_holes[:, 3] ** 2 - + self.pointer["energetic_ions"].markers_wo_holes[:, 4] ** 2 - + self.pointer["energetic_ions"].markers_wo_holes[:, 5] ** 2, + # particles' energy + particles = self.energetic_ions.var.particles + + self._en_f[0] = ( + particles.markers[~particles.holes, 6].dot( + particles.markers[~particles.holes, 3] ** 2 + + particles.markers[~particles.holes, 4] ** 2 + + particles.markers[~particles.holes, 5] ** 2 ) - / (2.0) + / 2.0 + * Ah + / Ab ) - self.update_scalar("en_f", self._tmp[0]) - self.update_scalar("en_tot", en_U + en_B + en_p + self._tmp[0]) + self.update_scalar("en_f", self._en_f[0]) + self.update_scalar("en_tot") - # Print number of lost ions - self._n_lost_particles[0] = self.pointer["energetic_ions"].n_lost_markers - self.update_scalar("n_lost_particles", self._n_lost_particles[0]) - if self.rank_world == 0: + # print number of lost particles + n_lost_markers = xp.array(particles.n_lost_markers) + + if self.derham.comm is not None: + self.derham.comm.Allreduce( + MPI.IN_PLACE, + n_lost_markers, + op=MPI.SUM, + ) + + if self.clone_config is not None: + self.clone_config.inter_comm.Allreduce( + MPI.IN_PLACE, + n_lost_markers, + op=MPI.SUM, + ) + + if rank == 0: print( - "ratio of lost particles: ", - self._n_lost_particles[0] / self.pointer["energetic_ions"].Np * 100, - "%", + "Lost particle ratio: ", + n_lost_markers / particles.Np * 100, + "% \n", ) + ## default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "magnetosonic.Options" in line: + new_file += [ + """model.propagators.magnetosonic.options = model.propagators.magnetosonic.Options( + b_field=model.em_fields.b_field,)\n""" + ] + + elif "push_eta_pc.Options" in line: + new_file += [ + """model.propagators.push_eta_pc.options = model.propagators.push_eta_pc.Options( + u_tilde = model.mhd.velocity,)\n""" + ] + + elif "push_vxb.Options" in line: + new_file += [ + """model.propagators.push_vxb.options = model.propagators.push_vxb.Options( + b2_var = model.em_fields.b_field,)\n""" + ] + + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) + class LinearMHDDriftkineticCC(StruphyModel): r"""Hybrid linear ideal MHD + energetic ions (5D Driftkinetic) with **current coupling scheme**. @@ -558,13 +505,13 @@ class LinearMHDDriftkineticCC(StruphyModel): &\frac{\partial \tilde{\rho}}{\partial t}+\nabla\cdot(\rho_{0} \tilde{\mathbf{U}})=0\,, \\ \rho_{0} &\frac{\partial \tilde{\mathbf{U}}}{\partial t} - \tilde p\, \nabla - = (\nabla \times \tilde{\mathbf{B}}) \times (\mathbf{B}_0 + (\nabla \times \mathbf B_0) \times \tilde{\mathbf{B}} + = (\nabla \times \tilde{\mathbf{B}}) \times \mathbf{B} + (\nabla \times \mathbf B_0) \times \tilde{\mathbf{B}} + \frac{A_\textnormal{h}}{A_\textnormal{b}} \left[ \frac{1}{\epsilon} n_\textnormal{gc} \tilde{\mathbf{U}} - \frac{1}{\epsilon} \mathbf{J}_\textnormal{gc} - \nabla \times \mathbf{M}_\textnormal{gc} \right] \times \mathbf{B} \,, \\ &\frac{\partial \tilde p}{\partial t} + \nabla\cdot(p_0 \tilde{\mathbf{U}}) + \frac{2}{3}\,p_0\nabla\cdot \tilde{\mathbf{U}}=0\,, \\ - &\frac{\partial \tilde{\mathbf{B}}}{\partial t} - \nabla\times(\tilde{\mathbf{U}} \times \mathbf{B}_0) + &\frac{\partial \tilde{\mathbf{B}}}{\partial t} - \nabla\times(\tilde{\mathbf{U}} \times \mathbf{B}) = 0\,, \end{aligned} \right. @@ -577,7 +524,7 @@ class LinearMHDDriftkineticCC(StruphyModel): \\ & n_\textnormal{gc} = \int f_\textnormal{h} B_\parallel^* \,\textnormal dv_\parallel \textnormal d\mu \,, \\ - & \mathbf{J}_\textnormal{gc} = \int f_\textnormal{h}(v_\parallel \mathbf{B}^* - \mathbf{b}_0 \times \mathbf{E}^*) \,\textnormal dv_\parallel \textnormal d\mu \,, + & \mathbf{J}_\textnormal{gc} = \int \frac{f_\textnormal{h}}{B_\parallel^*}(v_\parallel \mathbf{B}^* - \mathbf{b}_0 \times \mathbf{E}^*) \,\textnormal dv_\parallel \textnormal d\mu \,, \\ & \mathbf{M}_\textnormal{gc} = - \int f_\textnormal{h} B_\parallel^* \mu \mathbf{b}_0 \,\textnormal dv_\parallel \textnormal d\mu \,, \end{aligned} @@ -589,9 +536,11 @@ class LinearMHDDriftkineticCC(StruphyModel): .. math:: \begin{align} - \mathbf{B}^* &= \mathbf{B} + \epsilon v_\parallel \nabla \times \mathbf{b}_0 \,,\qquad B^*_\parallel = \mathbf{b}_0 \cdot \mathbf{B}^*\,, + B^*_\parallel = \mathbf{b}_0 \cdot \mathbf{B}^*\,, + \\[2mm] + \mathbf{B}^* &= \mathbf{B} + \epsilon v_\parallel \nabla \times \mathbf{b}_0 \,, \\[2mm] - \mathbf{E}^* &= - \tilde{\mathbf{U}} \times \mathbf{B} - \epsilon \mu \nabla B_\parallel \,, + \mathbf{E}^* &= - \tilde{\mathbf{U}} \times \mathbf{B} - \epsilon \mu \nabla (\mathbf{b}_0 \cdot \mathbf{B}) \,, \end{align} with the normalization parameter @@ -608,335 +557,238 @@ class LinearMHDDriftkineticCC(StruphyModel): 4. :class:`~struphy.propagators.propagators_coupling.CurrentCoupling5DCurlb` 5. :class:`~struphy.propagators.propagators_fields.CurrentCoupling5DDensity` 6. :class:`~struphy.propagators.propagators_fields.ShearAlfvenCurrentCoupling5D` - 7. :class:`~struphy.propagators.propagators_fields.MagnetosonicCurrentCoupling5D` + 7. :class:`~struphy.propagators.propagators_fields.Magnetosonic` :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} - - dct["em_fields"]["b_field"] = "Hdiv" - dct["fluid"]["mhd"] = { - "density": "L2", - "velocity": "Hdiv", - "pressure": "L2", - } - dct["kinetic"]["energetic_ions"] = "Particles5D" - return dct - - @staticmethod - def bulk_species(): - return "mhd" - - @staticmethod - def velocity_scale(): - return "alfvén" - - @staticmethod - def propagators_dct(): - return { - propagators_markers.PushGuidingCenterBxEstar: ["energetic_ions"], - propagators_markers.PushGuidingCenterParallel: ["energetic_ions"], - propagators_coupling.CurrentCoupling5DGradB: ["energetic_ions", "mhd_velocity"], - propagators_coupling.CurrentCoupling5DCurlb: ["energetic_ions", "mhd_velocity"], - propagators_fields.CurrentCoupling5DDensity: ["mhd_velocity"], - propagators_fields.ShearAlfvenCurrentCoupling5D: ["mhd_velocity", "b_field"], - propagators_fields.MagnetosonicCurrentCoupling5D: ["mhd_density", "mhd_velocity", "mhd_pressure"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - # add special options - @classmethod - def options(cls): - dct = super().options() - cls.add_option( - species=["fluid", "mhd"], - key="u_space", - option="Hdiv", - dct=dct, - ) - return dct - - def __init__(self, params, comm, clone_config=None): - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) - - from struphy.polar.basic import PolarVector - - # extract necessary parameters - u_space = params["fluid"]["mhd"]["options"]["u_space"] - params_alfven = params["fluid"]["mhd"]["options"]["ShearAlfvenCurrentCoupling5D"] - params_sonic = params["fluid"]["mhd"]["options"]["MagnetosonicCurrentCoupling5D"] - params_density = params["fluid"]["mhd"]["options"]["CurrentCoupling5DDensity"] - - params_bxE = params["kinetic"]["energetic_ions"]["options"]["PushGuidingCenterBxEstar"] - params_parallel = params["kinetic"]["energetic_ions"]["options"]["PushGuidingCenterParallel"] - params_cc_gradB = params["kinetic"]["energetic_ions"]["options"]["CurrentCoupling5DGradB"] - params_cc_curlb = params["kinetic"]["energetic_ions"]["options"]["CurrentCoupling5DCurlb"] - params_cc_gradB = params["kinetic"]["energetic_ions"]["options"]["CurrentCoupling5DGradB"] - - # compute coupling parameters - Ab = params["fluid"]["mhd"]["phys_params"]["A"] - Ah = params["kinetic"]["energetic_ions"]["phys_params"]["A"] - epsilon = self.equation_params["energetic_ions"]["epsilon"] - - self._coupling_params = {} - self._coupling_params["Ab"] = Ab - self._coupling_params["Ah"] = Ah - - # add control variate to mass_ops object - if self.pointer["energetic_ions"].control_variate: - self.mass_ops.weights["f0"] = self.pointer["energetic_ions"].f0 - - # Project magnetic field - self._b_eq = self.derham.P["2"]( - [ - self.equil.b2_1, - self.equil.b2_2, - self.equil.b2_3, - ] - ) - - self._absB0 = self.derham.P["0"](self.equil.absB0) - - self._unit_b1 = self.derham.P["1"]( - [ - self.equil.unit_b1_1, - self.equil.unit_b1_2, - self.equil.unit_b1_3, - ] - ) - - self._unit_b2 = self.derham.P["2"]( - [ - self.equil.unit_b2_1, - self.equil.unit_b2_2, - self.equil.unit_b2_3, - ] - ) - - self._gradB1 = self.derham.P["1"]( - [ - self.equil.gradB1_1, - self.equil.gradB1_2, - self.equil.gradB1_3, - ] - ) + ## species + class EnergeticIons(ParticleSpecies): + def __init__(self): + self.var = PICVariable(space="Particles5D") + self.init_variables() + + class EMFields(FieldSpecies): + def __init__(self): + self.b_field = FEECVariable(space="Hdiv") + self.init_variables() + + class MHD(FluidSpecies): + def __init__(self): + self.density = FEECVariable(space="L2") + self.pressure = FEECVariable(space="L2") + self.velocity = FEECVariable(space="Hdiv") + self.init_variables() + + ## propagators + + class Propagators: + def __init__(self, turn_off: tuple[str, ...] = (None,)): + if not "PushGuidingCenterBxEstar" in turn_off: + self.push_bxe = propagators_markers.PushGuidingCenterBxEstar() + if not "PushGuidingCenterParallel" in turn_off: + self.push_parallel = propagators_markers.PushGuidingCenterParallel() + if not "ShearAlfvenCurrentCoupling5D" in turn_off: + self.shearalfen_cc5d = propagators_fields.ShearAlfvenCurrentCoupling5D() + if not "Magnetosonic" in turn_off: + self.magnetosonic = propagators_fields.Magnetosonic() + if not "CurrentCoupling5DDensity" in turn_off: + self.cc5d_density = propagators_fields.CurrentCoupling5DDensity() + if not "CurrentCoupling5DGradB" in turn_off: + self.cc5d_gradb = propagators_coupling.CurrentCoupling5DGradB() + if not "CurrentCoupling5DCurlb" in turn_off: + self.cc5d_curlb = propagators_coupling.CurrentCoupling5DCurlb() + + def __init__(self, turn_off: tuple[str, ...] = (None,)): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + + # 1. instantiate all species + self.em_fields = self.EMFields() + self.mhd = self.MHD() + self.energetic_ions = self.EnergeticIons() + + # 2. instantiate all propagators + self.propagators = self.Propagators(turn_off) + + # 3. assign variables to propagators + if not "ShearAlfvenCurrentCoupling5D" in turn_off: + self.propagators.shearalfen_cc5d.variables.u = self.mhd.velocity + self.propagators.shearalfen_cc5d.variables.b = self.em_fields.b_field + if not "Magnetosonic" in turn_off: + self.propagators.magnetosonic.variables.n = self.mhd.density + self.propagators.magnetosonic.variables.u = self.mhd.velocity + self.propagators.magnetosonic.variables.p = self.mhd.pressure + if not "CurrentCoupling5DDensity" in turn_off: + self.propagators.cc5d_density.variables.u = self.mhd.velocity + if not "CurrentCoupling5DGradB" in turn_off: + self.propagators.cc5d_gradb.variables.u = self.mhd.velocity + self.propagators.cc5d_gradb.variables.energetic_ions = self.energetic_ions.var + if not "CurrentCoupling5DCurlb" in turn_off: + self.propagators.cc5d_curlb.variables.u = self.mhd.velocity + self.propagators.cc5d_curlb.variables.energetic_ions = self.energetic_ions.var + if not "PushGuidingCenterBxEstar" in turn_off: + self.propagators.push_bxe.variables.ions = self.energetic_ions.var + if not "PushGuidingCenterParallel" in turn_off: + self.propagators.push_parallel.variables.ions = self.energetic_ions.var + + # define scalars for update_scalar_quantities + self.add_scalar("en_U") + self.add_scalar("en_p") + self.add_scalar("en_B") + self.add_scalar("en_fv", compute="from_particles", variable=self.energetic_ions.var) + self.add_scalar("en_fB", compute="from_particles", variable=self.energetic_ions.var) + self.add_scalar("en_tot", summands=["en_U", "en_p", "en_B", "en_fv", "en_fB"]) - self._curl_unit_b2 = self.derham.P["2"]( - [ - self.equil.curl_unit_b2_1, - self.equil.curl_unit_b2_2, - self.equil.curl_unit_b2_3, - ] - ) + @property + def bulk_species(self): + return self.mhd - self._p_eq = self.derham.P["3"](self.equil.p3) - self._ones = self._p_eq.space.zeros() + @property + def velocity_scale(self): + return "alfvén" + def allocate_helpers(self): + self._ones = self.projected_equil.p3.space.zeros() if isinstance(self._ones, PolarVector): self._ones.tp[:] = 1.0 else: self._ones[:] = 1.0 - # set keyword arguments for propagators - self._kwargs[propagators_markers.PushGuidingCenterBxEstar] = { - "b_tilde": self.pointer["b_field"], - "algo": params_bxE["algo"], - "epsilon": epsilon, - } - - self._kwargs[propagators_markers.PushGuidingCenterParallel] = { - "b_tilde": self.pointer["b_field"], - "algo": params_parallel["algo"], - "epsilon": epsilon, - } - - if params_cc_gradB["turn_off"]: - self._kwargs[propagators_coupling.CurrentCoupling5DGradB] = None - else: - self._kwargs[propagators_coupling.CurrentCoupling5DGradB] = { - "b": self.pointer["b_field"], - "b_eq": self._b_eq, - "unit_b1": self._unit_b1, - "unit_b2": self._unit_b2, - "absB0": self._absB0, - "gradB1": self._gradB1, - "curl_unit_b2": self._curl_unit_b2, - "u_space": u_space, - "solver": params_cc_gradB["solver"], - "algo": params_cc_gradB["algo"], - "filter": params_cc_gradB["filter"], - "coupling_params": self._coupling_params, - "epsilon": epsilon, - "boundary_cut": params_cc_gradB["boundary_cut"], - } - - if params_cc_curlb["turn_off"]: - self._kwargs[propagators_coupling.CurrentCoupling5DCurlb] = None - else: - self._kwargs[propagators_coupling.CurrentCoupling5DCurlb] = { - "b": self.pointer["b_field"], - "b_eq": self._b_eq, - "unit_b1": self._unit_b1, - "absB0": self._absB0, - "gradB1": self._gradB1, - "curl_unit_b2": self._curl_unit_b2, - "u_space": u_space, - "solver": params_cc_curlb["solver"], - "filter": params_cc_curlb["filter"], - "coupling_params": self._coupling_params, - "epsilon": epsilon, - "boundary_cut": params_cc_curlb["boundary_cut"], - } - - if params_density["turn_off"]: - self._kwargs[propagators_fields.CurrentCoupling5DDensity] = None - else: - self._kwargs[propagators_fields.CurrentCoupling5DDensity] = { - "particles": self.pointer["energetic_ions"], - "b": self.pointer["b_field"], - "b_eq": self._b_eq, - "unit_b1": self._unit_b1, - "curl_unit_b2": self._curl_unit_b2, - "u_space": u_space, - "solver": params_density["solver"], - "coupling_params": self._coupling_params, - "epsilon": epsilon, - "boundary_cut": params_density["boundary_cut"], - } - - if params_alfven["turn_off"]: - self._kwargs[propagators_fields.ShearAlfvenCurrentCoupling5D] = None - else: - self._kwargs[propagators_fields.ShearAlfvenCurrentCoupling5D] = { - "particles": self.pointer["energetic_ions"], - "unit_b1": self._unit_b1, - "absB0": self._absB0, - "u_space": u_space, - "solver": params_alfven["solver"], - "filter": params_alfven["filter"], - "coupling_params": self._coupling_params, - "accumulated_magnetization": self.pointer["accumulated_magnetization"], - "boundary_cut": params_alfven["boundary_cut"], - } - - if params_sonic["turn_off"]: - self._kwargs[propagators_fields.MagnetosonicCurrentCoupling5D] = None - else: - self._kwargs[propagators_fields.MagnetosonicCurrentCoupling5D] = { - "particles": self.pointer["energetic_ions"], - "b": self.pointer["b_field"], - "unit_b1": self._unit_b1, - "absB0": self._absB0, - "u_space": u_space, - "solver": params_sonic["solver"], - "filter": params_sonic["filter"], - "coupling_params": self._coupling_params, - "boundary_cut": params_sonic["boundary_cut"], - } - - # Initialize propagators used in splitting substeps - self.init_propagators() - # Scalar variables to be saved during simulation - self.add_scalar("en_U", compute="from_field") - self.add_scalar("en_p", compute="from_field") - self.add_scalar("en_B", compute="from_field") - self.add_scalar("en_fv", compute="from_particles", species="energetic_ions") - self.add_scalar("en_fB", compute="from_particles", species="energetic_ions") - # self.add_scalar('en_fv_lost', compute = 'from_particles', species='energetic_ions') - # self.add_scalar('en_fB_lost', compute = 'from_particles', species='energetic_ions') - # self.add_scalar('en_tot',summands = ['en_U','en_p','en_B','en_fv','en_fB','en_fv_lost','en_fB_lost']) - self.add_scalar("en_tot", summands=["en_U", "en_p", "en_B", "en_fv", "en_fB"]) - self.add_scalar("n_lost_particles", compute="from_particles", species="energetic_ions") - - # temporaries - self._b_full1 = self._b_eq.space.zeros() - self._PBb = self._absB0.space.zeros() + self._en_fv = xp.empty(1, dtype=float) + self._en_fB = xp.empty(1, dtype=float) + self._en_tot = xp.empty(1, dtype=float) + self._n_lost_particles = xp.empty(1, dtype=float) - self._en_fv = np.empty(1, dtype=float) - self._en_fB = np.empty(1, dtype=float) - # self._en_fv_lost = np.empty(1, dtype=float) - # self._en_fB_lost = np.empty(1, dtype=float) - self._n_lost_particles = np.empty(1, dtype=float) + self._PB = getattr(self.basis_ops, "PB") + self._PBb = self._PB.codomain.zeros() def update_scalar_quantities(self): - en_U = 0.5 * self.mass_ops.M2n.dot_inner(self.pointer["mhd_velocity"], self.pointer["mhd_velocity"]) - en_B = 0.5 * self.mass_ops.M2.dot_inner(self.pointer["b_field"], self.pointer["b_field"]) - en_p = self.pointer["mhd_pressure"].inner(self._ones) / (5 / 3 - 1) + # scaling factor + Ab = self.mhd.mass_number + Ah = self.energetic_ions.var.species.mass_number + + # perturbed fields + en_U = 0.5 * self.mass_ops.M2n.dot_inner( + self.mhd.velocity.spline.vector, + self.mhd.velocity.spline.vector, + ) + en_B = 0.5 * self.mass_ops.M2.dot_inner( + self.em_fields.b_field.spline.vector, + self.em_fields.b_field.spline.vector, + ) + en_p = self.mhd.pressure.spline.vector.inner(self._ones) / (5 / 3 - 1) self.update_scalar("en_U", en_U) - self.update_scalar("en_p", en_p) self.update_scalar("en_B", en_B) + self.update_scalar("en_p", en_p) + + # particles' energy + particles = self.energetic_ions.var.particles self._en_fv[0] = ( - self.pointer["energetic_ions"] - .markers[~self.pointer["energetic_ions"].holes, 5] - .dot( - self.pointer["energetic_ions"].markers[~self.pointer["energetic_ions"].holes, 3] ** 2, + particles.markers[~particles.holes, 5].dot( + particles.markers[~particles.holes, 3] ** 2, ) / (2.0) - * self._coupling_params["Ah"] - / self._coupling_params["Ab"] + * Ah + / Ab ) - self.update_scalar("en_fv", self._en_fv[0]) - - # self._en_fv_lost[0] = self.pointer['energetic_ions'].lost_markers[:self.pointer['energetic_ions'].n_lost_markers, 5].dot( - # self.pointer['energetic_ions'].lost_markers[:self.pointer['energetic_ions'].n_lost_markers, 3]**2) / (2.0) * self._coupling_params['Ah']/self._coupling_params['Ab'] - - # self.update_scalar('en_fv_lost', self._en_fv_lost[0]) - - # calculate particle magnetic energy - self.pointer["energetic_ions"].save_magnetic_energy( - self.pointer["b_field"], - ) + self._PBb = self._PB.dot(self.em_fields.b_field.spline.vector) + particles.save_magnetic_energy(self._PBb) self._en_fB[0] = ( - self.pointer["energetic_ions"] - .markers[~self.pointer["energetic_ions"].holes, 5] - .dot( - self.pointer["energetic_ions"].markers[~self.pointer["energetic_ions"].holes, 8], + particles.markers[~particles.holes, 5].dot( + particles.markers[~particles.holes, 8], ) - * self._coupling_params["Ah"] - / self._coupling_params["Ab"] + * Ah + / Ab ) + self.update_scalar("en_fv", self._en_fv[0]) self.update_scalar("en_fB", self._en_fB[0]) + self.update_scalar("en_tot") - # self._en_fB_lost[0] = self.pointer['energetic_ions'].lost_markers[:self.pointer['energetic_ions'].n_lost_markers, 5].dot( - # self.pointer['energetic_ions'] .lost_markers[:self.pointer['energetic_ions'].n_lost_markers, 8]) * self._coupling_params['Ah']/self._coupling_params['Ab'] + # print number of lost particles + n_lost_markers = xp.array(particles.n_lost_markers) - # self.update_scalar('en_fB_lost', self._en_fB_lost[0]) + if self.derham.comm is not None: + self.derham.comm.Allreduce( + MPI.IN_PLACE, + n_lost_markers, + op=MPI.SUM, + ) - self.update_scalar("en_tot") + if self.clone_config is not None: + self.clone_config.inter_comm.Allreduce( + MPI.IN_PLACE, + n_lost_markers, + op=MPI.SUM, + ) - # Print number of lost ions - self._n_lost_particles[0] = self.pointer["energetic_ions"].n_lost_markers - self.update_scalar("n_lost_particles", self._n_lost_particles[0]) - if self.rank_world == 0: + if rank == 0: print( - "ratio of lost particles: ", - self._n_lost_particles[0] / self.pointer["energetic_ions"].Np * 100, - "%", + "Lost particle ratio: ", + n_lost_markers / particles.Np * 100, + "% \n", ) - @staticmethod - def diagnostics_dct(): - dct = {} - - dct["accumulated_magnetization"] = "Hdiv" - return dct - - __diagnostics__ = diagnostics_dct() + ## default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "shearalfen_cc5d.Options" in line: + new_file += [ + """model.propagators.shearalfen_cc5d.options = model.propagators.shearalfen_cc5d.Options( + energetic_ions = model.energetic_ions.var,)\n""" + ] + + elif "magnetosonic.Options" in line: + new_file += [ + """model.propagators.magnetosonic.options = model.propagators.magnetosonic.Options( + b_field=model.em_fields.b_field,)\n""" + ] + + elif "cc5d_density.Options" in line: + new_file += [ + """model.propagators.cc5d_density.options = model.propagators.cc5d_density.Options( + energetic_ions = model.energetic_ions.var, + b_tilde = model.em_fields.b_field,)\n""" + ] + + elif "cc5d_curlb.Options" in line: + new_file += [ + """model.propagators.cc5d_curlb.options = model.propagators.cc5d_curlb.Options( + b_tilde = model.em_fields.b_field,)\n""" + ] + + elif "cc5d_gradb.Options" in line: + new_file += [ + """model.propagators.cc5d_gradb.options = model.propagators.cc5d_gradb.Options( + b_tilde = model.em_fields.b_field,)\n""" + ] + + elif "push_bxe.Options" in line: + new_file += [ + """model.propagators.push_bxe.options = model.propagators.push_bxe.Options( + b_tilde = model.em_fields.b_field,)\n""" + ] + + elif "push_parallel.Options" in line: + new_file += [ + """model.propagators.push_parallel.options = model.propagators.push_parallel.Options( + b_tilde = model.em_fields.b_field,)\n""" + ] + + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) class ColdPlasmaVlasov(StruphyModel): @@ -960,27 +812,22 @@ class ColdPlasmaVlasov(StruphyModel): &\frac{\partial \mathbf B}{\partial t} + \nabla\times\mathbf E = 0\,, \\[2mm] -&\frac{\partial \mathbf E}{\partial t} + \nabla\times\mathbf B = - \frac{\alpha^2}{\varepsilon_\textnormal{c}} \left( \mathbf j_\textnormal{c} + \nu \int_{\mathbb{R}^3} \mathbf{v} f \, \text{d}^3 \mathbf{v} \right) \,, + \frac{\alpha^2}{\varepsilon_\textnormal{h}} \left( \mathbf j_\textnormal{c} + \int_{\mathbb{R}^3} \mathbf{v} f \, \text{d}^3 \mathbf{v} \right) \,, where :math:`(n_0,\mathbf B_0)` denotes a (inhomogeneous) background and .. math:: - \alpha = \frac{\hat \Omega_\textnormal{p,cold}}{\hat \Omega_\textnormal{c,cold}}\,, \qquad \varepsilon_\textnormal{c} = \frac{1}{\hat \Omega_\textnormal{c,cold} \hat t}\,, \qquad \varepsilon_\textnormal{h} = \frac{1}{\hat \Omega_\textnormal{c,hot} \hat t} \,, \qquad \nu = \frac{Z_\textnormal{h}}{Z_\textnormal{c}}\,. + \alpha = \frac{\hat \Omega_\textnormal{p,cold}}{\hat \Omega_\textnormal{c,cold}}\,, \qquad \varepsilon_\textnormal{c} = \frac{1}{\hat \Omega_\textnormal{c,cold} \hat t}\,, \qquad \varepsilon_\textnormal{h} = \frac{1}{\hat \Omega_\textnormal{c,hot} \hat t} \,. At initial time the Poisson equation is solved once to weakly satisfy the Gauss law: .. math:: \begin{align} - \nabla \cdot \mathbf{E} & = \nu \frac{\alpha^2}{\varepsilon_\textnormal{c}} \int_{\mathbb{R}^3} f \, \text{d}^3 \mathbf{v}\,. + \nabla \cdot \mathbf{E} & = \nu \frac{\alpha^2}{\varepsilon_\textnormal{h}} \int_{\mathbb{R}^3} f \, \text{d}^3 \mathbf{v}\,. \end{align} - Note - ---------- - If hot and cold particles are of the same species (:math:`Z_\textnormal{c} = Z_\textnormal{h} \,, A_\textnormal{c} = A_\textnormal{h}`) then :math:`\varepsilon_\textnormal{c} = \varepsilon_\textnormal{h}` and :math:`\nu = 1`. - - :ref:`propagators` (called in sequence): 1. :class:`~struphy.propagators.propagators_fields.Maxwell` @@ -989,203 +836,177 @@ class ColdPlasmaVlasov(StruphyModel): 4. :class:`~struphy.propagators.propagators_markers.PushVxB` 5. :class:`~struphy.propagators.propagators_markers.PushEta` 6. :class:`~struphy.propagators.propagators_coupling.VlasovAmpere` - - :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} + ## species - dct["em_fields"]["e_field"] = "Hcurl" - dct["em_fields"]["b_field"] = "Hdiv" - dct["fluid"]["cold_electrons"] = {"j": "Hcurl"} - dct["kinetic"]["hot_electrons"] = "Particles6D" - return dct + class EMFields(FieldSpecies): + def __init__(self): + self.e_field = FEECVariable(space="Hcurl") + self.b_field = FEECVariable(space="Hdiv") + self.phi = FEECVariable(space="H1") + self.init_variables() - @staticmethod - def bulk_species(): - return "cold_electrons" + class ThermalElectrons(FluidSpecies): + def __init__(self): + self.current = FEECVariable(space="Hcurl") + self.init_variables() - @staticmethod - def velocity_scale(): - return "light" + class HotElectrons(ParticleSpecies): + def __init__(self): + self.var = PICVariable(space="Particles6D") + self.init_variables() - @staticmethod - def propagators_dct(): - return { - propagators_fields.Maxwell: ["e_field", "b_field"], - propagators_fields.OhmCold: ["cold_electrons_j", "e_field"], - propagators_fields.JxBCold: ["cold_electrons_j"], - propagators_markers.PushEta: ["hot_electrons"], - propagators_markers.PushVxB: ["hot_electrons"], - propagators_coupling.VlasovAmpere: ["e_field", "hot_electrons"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - # add special options - @classmethod - def options(cls): - dct = super().options() - cls.add_option( - species=["em_fields"], - option=propagators_fields.ImplicitDiffusion, - dct=dct, - ) - return dct + ## propagators - def __init__(self, params, comm, clone_config=None): - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) + class Propagators: + def __init__(self): + self.maxwell = propagators_fields.Maxwell() + self.ohm = propagators_fields.OhmCold() + self.jxb = propagators_fields.JxBCold() + self.push_eta = propagators_markers.PushEta() + self.push_vxb = propagators_markers.PushVxB() + self.coupling_va = propagators_coupling.VlasovAmpere() - # Get rank and size - self._rank = self.rank_world + ## abstract methods - # prelim - hot_params = params["kinetic"]["hot_electrons"] + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") - # model parameters - self._alpha = np.abs( - self.equation_params["cold_electrons"]["alpha"], - ) - self._epsilon_cold = self.equation_params["cold_electrons"]["epsilon"] - self._epsilon_hot = self.equation_params["hot_electrons"]["epsilon"] - - self._nu = hot_params["phys_params"]["Z"] / params["fluid"]["cold_electrons"]["phys_params"]["Z"] - - # Initialize background magnetic field from MHD equilibrium - self._b_background = self.derham.P["2"]( - [ - self.equil.b2_1, - self.equil.b2_2, - self.equil.b2_3, - ] - ) + # 1. instantiate all species + self.em_fields = self.EMFields() + self.thermal_elec = self.ThermalElectrons() + self.hot_elec = self.HotElectrons() + + # 2. instantiate all propagators + self.propagators = self.Propagators() + + # 3. assign variables to propagators + self.propagators.maxwell.variables.e = self.em_fields.e_field + self.propagators.maxwell.variables.b = self.em_fields.b_field + + self.propagators.ohm.variables.j = self.thermal_elec.current + self.propagators.ohm.variables.e = self.em_fields.e_field + + self.propagators.jxb.variables.j = self.thermal_elec.current - # propagator parameters - params_maxwell = params["em_fields"]["options"]["Maxwell"]["solver"] - params_ohmcold = params["fluid"]["cold_electrons"]["options"]["OhmCold"]["solver"] - params_jxbcold = params["fluid"]["cold_electrons"]["options"]["JxBCold"]["solver"] - algo_eta = params["kinetic"]["hot_electrons"]["options"]["PushEta"]["algo"] - algo_vxb = params["kinetic"]["hot_electrons"]["options"]["PushVxB"]["algo"] - params_coupling = params["em_fields"]["options"]["VlasovAmpere"]["solver"] - self._poisson_params = params["em_fields"]["options"]["ImplicitDiffusion"]["solver"] - - # set keyword arguments for propagators - self._kwargs[propagators_fields.Maxwell] = {"solver": params_maxwell} - - self._kwargs[propagators_fields.OhmCold] = { - "alpha": self._alpha, - "epsilon": self._epsilon_cold, - "solver": params_ohmcold, - } - - self._kwargs[propagators_fields.JxBCold] = { - "epsilon": self._epsilon_cold, - "solver": params_jxbcold, - } - - self._kwargs[propagators_markers.PushEta] = {"algo": algo_eta} - - self._kwargs[propagators_markers.PushVxB] = { - "algo": algo_vxb, - "kappa": 1.0 / self._epsilon_cold, - "b2": self.pointer["b_field"], - "b2_add": self._b_background, - } - - self._kwargs[propagators_coupling.VlasovAmpere] = { - "c1": self._nu * self._alpha**2 / self._epsilon_cold, - "c2": 1.0 / self._epsilon_hot, - "solver": params_coupling, - } - - # Initialize propagators used in splitting substeps - self.init_propagators() - - # Scalar variables to be saved during simulation + self.propagators.push_eta.variables.var = self.hot_elec.var + self.propagators.push_vxb.variables.ions = self.hot_elec.var + + self.propagators.coupling_va.variables.e = self.em_fields.e_field + self.propagators.coupling_va.variables.ions = self.hot_elec.var + + # define scalars for update_scalar_quantities self.add_scalar("en_E") self.add_scalar("en_B") self.add_scalar("en_J") - self.add_scalar("en_f", compute="from_particles", species="hot_electrons") + self.add_scalar("en_f", compute="from_particles", variable=self.hot_elec.var) self.add_scalar("en_tot") - # temporaries - self._tmp = np.empty(1, dtype=float) + # initial Poisson (not a propagator used in time stepping) + self.initial_poisson = propagators_fields.Poisson() + self.initial_poisson.variables.phi = self.em_fields.phi - def initialize_from_params(self): - """:meta private:""" - from psydac.linalg.stencil import StencilVector + @property + def bulk_species(self): + return self.thermal_elec - from struphy.pic.accumulation.particles_to_grid import AccumulatorVector + @property + def velocity_scale(self): + return "light" - # Initialize fields and particles - super().initialize_from_params() + def allocate_helpers(self): + self._tmp = xp.empty(1, dtype=float) - # Accumulate charge density - charge_accum = AccumulatorVector( - self.pointer["hot_electrons"], - "H1", - Pyccelkernel(accum_kernels.vlasov_maxwell_poisson), - self.mass_ops, - self.domain.args_domain, - ) - charge_accum() + def update_scalar_quantities(self): + # e*M1*e/2 + e = self.em_fields.e_field.spline.vector + en_E = 0.5 * self.mass_ops.M1.dot_inner(e, e) + self.update_scalar("en_E", en_E) - # Locally subtract mean charge for solvability with periodic bc - if np.all(charge_accum.vectors[0].space.periods): - charge_accum._vectors[0][:] -= np.mean( - charge_accum.vectors[0].toarray()[charge_accum.vectors[0].toarray() != 0], + # alpha^2 / 2 / N * sum_p w_p v_p^2 + particles = self.hot_elec.var.particles + alpha = self.hot_elec.equation_params.alpha + self._tmp[0] = ( + alpha**2 + / (2 * particles.Np) + * xp.dot( + particles.markers_wo_holes[:, 3] ** 2 + + particles.markers_wo_holes[:, 4] ** 2 + + particles.markers_wo_holes[:, 5] ** 2, + particles.markers_wo_holes[:, 6], ) - - # Instantiate Poisson solver - _phi = StencilVector(self.derham.Vh["0"]) - poisson_solver = propagators_fields.ImplicitDiffusion( - _phi, - sigma_1=0, - rho=self._nu * self._alpha**2 / self._epsilon_cold * charge_accum.vectors[0], - x0=self._nu * self._alpha**2 / self._epsilon_cold * charge_accum.vectors[0], - solver=self._poisson_params, ) + self.update_scalar("en_f", self._tmp[0]) - # Solve with dt=1. and compute electric field - poisson_solver(1.0) - self.derham.grad.dot(-_phi, out=self.pointer["e_field"]) + # en_tot = en_w + en_e + self.update_scalar("en_tot", en_E + self._tmp[0]) - def update_scalar_quantities(self): - en_E = 0.5 * self.mass_ops.M1.dot_inner(self.pointer["e_field"], self.pointer["e_field"]) - en_B = 0.5 * self.mass_ops.M2.dot_inner(self.pointer["b_field"], self.pointer["b_field"]) - en_J = ( - 0.5 - * self._alpha**2 - * self.mass_ops.M1ninv.dot_inner(self.pointer["cold_electrons_j"], self.pointer["cold_electrons_j"]) - ) - self.update_scalar("en_E", en_E) - self.update_scalar("en_B", en_B) - self.update_scalar("en_J", en_J) + def allocate_propagators(self): + """Solve initial Poisson equation. - # nu alpha^2 eps_h / eps_c / 2 / N * sum_p w_p v_p^2 - self._tmp[0] = ( - self._nu - * self._alpha**2 - * self._epsilon_hot - / self._epsilon_cold - / (2 * self.pointer["hot_electrons"].Np) - * np.dot( - self.pointer["hot_electrons"].markers_wo_holes[:, 3] ** 2 - + self.pointer["hot_electrons"].markers_wo_holes[:, 4] ** 2 - + self.pointer["hot_electrons"].markers_wo_holes[:, 5] ** 2, - self.pointer["hot_electrons"].markers_wo_holes[:, 6], - ) + :meta private: + """ + + # initialize fields and particles + super().allocate_propagators() + + if MPI.COMM_WORLD.Get_rank() == 0: + print("\nINITIAL POISSON SOLVE:") + + # use control variate method + particles = self.hot_elec.var.particles + particles.update_weights() + + # sanity check + # self.pointer['species1'].show_distribution_function( + # [True] + [False]*5, [xp.linspace(0, 1, 32)]) + + # accumulate charge density + charge_accum = AccumulatorVector( + particles, + "H1", + Pyccelkernel(accum_kernels.charge_density_0form), + self.mass_ops, + self.domain.args_domain, ) - self.update_scalar("en_f", self._tmp[0]) + # another sanity check: compute FE coeffs of density + # charge_accum.show_accumulated_spline_field(self.mass_ops) + + alpha = self.hot_elec.equation_params.alpha + epsilon = self.hot_elec.equation_params.epsilon - # en_tot = en_E + en_B + en_J + en_w - self.update_scalar("en_tot", en_E + en_B + en_J + self._tmp[0]) + self.initial_poisson.options.rho = charge_accum + self.initial_poisson.options.rho_coeffs = alpha**2 / epsilon + self.initial_poisson.allocate() + + # Solve with dt=1. and compute electric field + if MPI.COMM_WORLD.Get_rank() == 0: + print("\nSolving initial Poisson problem...") + self.initial_poisson(1.0) + + phi = self.initial_poisson.variables.phi.spline.vector + self.derham.grad.dot(-phi, out=self.em_fields.e_field.spline.vector) + if MPI.COMM_WORLD.Get_rank() == 0: + print("Done.") + + ## default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "coupling_va.Options" in line: + new_file += [line] + new_file += ["model.initial_poisson.options = model.initial_poisson.Options()\n"] + elif "set_save_data" in line: + new_file += ["\nbinplot = BinningPlot(slice='e1', n_bins=128, ranges=(0.0, 1.0))\n"] + new_file += ["model.hot_elec.set_save_data(binning_plots=(binplot,))\n"] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) diff --git a/src/struphy/models/kinetic.py b/src/struphy/models/kinetic.py index dbf5f523e..9a01e38fd 100644 --- a/src/struphy/models/kinetic.py +++ b/src/struphy/models/kinetic.py @@ -1,10 +1,19 @@ +import cunumpy as xp +from psydac.ddm.mpi import mpi as MPI + +from struphy.feec.projectors import L2Projector from struphy.kinetic_background.base import KineticBackground +from struphy.kinetic_background.maxwellians import Maxwellian3D from struphy.models.base import StruphyModel +from struphy.models.species import FieldSpecies, FluidSpecies, ParticleSpecies +from struphy.models.variables import FEECVariable, PICVariable, SPHVariable, Variable from struphy.pic.accumulation import accum_kernels, accum_kernels_gc +from struphy.pic.accumulation.particles_to_grid import AccumulatorVector from struphy.propagators import propagators_coupling, propagators_fields, propagators_markers -from struphy.utils.arrays import xp as np from struphy.utils.pyccel import Pyccelkernel +rank = MPI.COMM_WORLD.Get_rank() + class VlasovAmpereOneSpecies(StruphyModel): r"""Vlasov-Ampère equations for one species. @@ -73,202 +82,166 @@ class VlasovAmpereOneSpecies(StruphyModel): 1. :class:`~struphy.propagators.propagators_markers.PushEta` 2. :class:`~struphy.propagators.propagators_coupling.VlasovAmpere` 3. :class:`~struphy.propagators.propagators_markers.PushVxB` - - :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} + ## species - dct["em_fields"]["e_field"] = "Hcurl" - dct["kinetic"]["species1"] = "Particles6D" - return dct + class EMFields(FieldSpecies): + def __init__(self): + self.e_field = FEECVariable(space="Hcurl") + self.phi = FEECVariable(space="H1") + self.init_variables() - @staticmethod - def bulk_species(): - return "species1" + class KineticIons(ParticleSpecies): + def __init__(self): + self.var = PICVariable(space="Particles6D") + self.init_variables() - @staticmethod - def velocity_scale(): - return "light" + ## propagators - @staticmethod - def propagators_dct(): - return { - propagators_markers.PushEta: ["species1"], - propagators_markers.PushVxB: ["species1"], - propagators_coupling.VlasovAmpere: ["e_field", "species1"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - # add special options - @classmethod - def options(cls): - dct = super().options() - cls.add_option( - species=["em_fields"], - option=propagators_fields.ImplicitDiffusion, - dct=dct, - ) - cls.add_option( - species=["kinetic", "species1"], - key="override_eq_params", - option=[False, {"alpha": 1.0, "epsilon": -1.0}], - dct=dct, - ) - return dct + class Propagators: + def __init__(self, with_B0: bool = True): + self.push_eta = propagators_markers.PushEta() + if with_B0: + self.push_vxb = propagators_markers.PushVxB() + self.coupling_va = propagators_coupling.VlasovAmpere() - def __init__(self, params, comm, clone_config=None): - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) + ## abstract methods - # get species paramaters - species1_params = params["kinetic"]["species1"] + def __init__(self, with_B0: bool = True): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") - # Get coupling strength - if species1_params["options"]["override_eq_params"]: - self._alpha = species1_params["options"]["override_eq_params"]["alpha"] - self._epsilon = species1_params["options"]["override_eq_params"]["epsilon"] - print( - f"\n!!! Override equation parameters: {self._alpha = } and {self._epsilon = }.", - ) - else: - self._alpha = self.equation_params["species1"]["alpha"] - self._epsilon = self.equation_params["species1"]["epsilon"] - - # Check if it is control-variate method - self._control_variate = species1_params["markers"]["control_variate"] - - # check mean velocity - # TODO: assert f0.params[] == 0. - - # Initialize background magnetic field from MHD equilibrium - if self.projected_equil: - self._b_background = self.projected_equil.b2 - else: - self._b_background = None - - # propagator parameters - self._poisson_params = params["em_fields"]["options"]["ImplicitDiffusion"]["solver"] - algo_eta = params["kinetic"]["species1"]["options"]["PushEta"]["algo"] - if self._b_background is not None: - algo_vxb = params["kinetic"]["species1"]["options"]["PushVxB"]["algo"] - params_coupling = params["em_fields"]["options"]["VlasovAmpere"]["solver"] - - # set keyword arguments for propagators - self._kwargs[propagators_markers.PushEta] = { - "algo": algo_eta, - } - - # Only add PushVxB if magnetic field is not zero - self._kwargs[propagators_markers.PushVxB] = None - if self._b_background is not None: - self._kwargs[propagators_markers.PushVxB] = { - "algo": algo_vxb, - "b2": self._b_background, - "kappa": 1.0 / self._epsilon, - } - - self._kwargs[propagators_coupling.VlasovAmpere] = { - "c1": self._alpha**2 / self._epsilon, - "c2": 1.0 / self._epsilon, - "solver": params_coupling, - } - - # Initialize propagators used in splitting substeps - self.init_propagators() - - # Scalar variables to be saved during the simulation + self.with_B0 = with_B0 + + # 1. instantiate all species + self.em_fields = self.EMFields() + self.kinetic_ions = self.KineticIons() + + # 2. instantiate all propagators + self.propagators = self.Propagators(with_B0=with_B0) + + # 3. assign variables to propagators + self.propagators.push_eta.variables.var = self.kinetic_ions.var + if with_B0: + self.propagators.push_vxb.variables.ions = self.kinetic_ions.var + self.propagators.coupling_va.variables.e = self.em_fields.e_field + self.propagators.coupling_va.variables.ions = self.kinetic_ions.var + + # define scalars for update_scalar_quantities self.add_scalar("en_E") - self.add_scalar("en_f", compute="from_particles", species="species1") + self.add_scalar("en_f", compute="from_particles", variable=self.kinetic_ions.var) self.add_scalar("en_tot") - # temporaries - self._tmp = np.empty(1, dtype=float) + # initial Poisson (not a propagator used in time stepping) + self.initial_poisson = propagators_fields.Poisson() + self.initial_poisson.variables.phi = self.em_fields.phi + + @property + def bulk_species(self): + return self.kinetic_ions + + @property + def velocity_scale(self): + return "light" + + def allocate_helpers(self): + self._tmp = xp.empty(1, dtype=float) + + def update_scalar_quantities(self): + # e*M1*e/2 + e = self.em_fields.e_field.spline.vector + en_E = 0.5 * self.mass_ops.M1.dot_inner(e, e) + self.update_scalar("en_E", en_E) + + # alpha^2 / 2 / N * sum_p w_p v_p^2 + particles = self.kinetic_ions.var.particles + alpha = self.kinetic_ions.equation_params.alpha + self._tmp[0] = ( + alpha**2 + / (2 * particles.Np) + * xp.dot( + particles.markers_wo_holes[:, 3] ** 2 + + particles.markers_wo_holes[:, 4] ** 2 + + particles.markers_wo_holes[:, 5] ** 2, + particles.markers_wo_holes[:, 6], + ) + ) + self.update_scalar("en_f", self._tmp[0]) + + # en_tot = en_w + en_e + self.update_scalar("en_tot", en_E + self._tmp[0]) - def initialize_from_params(self): + def allocate_propagators(self): """Solve initial Poisson equation. :meta private: """ - from struphy.pic.accumulation.particles_to_grid import AccumulatorVector - # initialize fields and particles - super().initialize_from_params() + super().allocate_propagators() - if self.rank_world == 0: + if MPI.COMM_WORLD.Get_rank() == 0: print("\nINITIAL POISSON SOLVE:") # use control variate method - self.pointer["species1"].update_weights() + particles = self.kinetic_ions.var.particles + particles.update_weights() # sanity check # self.pointer['species1'].show_distribution_function( - # [True] + [False]*5, [np.linspace(0, 1, 32)]) + # [True] + [False]*5, [xp.linspace(0, 1, 32)]) # accumulate charge density charge_accum = AccumulatorVector( - self.pointer["species1"], + particles, "H1", Pyccelkernel(accum_kernels.charge_density_0form), self.mass_ops, self.domain.args_domain, ) - charge_accum(self.pointer["species1"].vdim) - # another sanity check: compute FE coeffs of density # charge_accum.show_accumulated_spline_field(self.mass_ops) - # Instantiate Poisson solver - _phi = self.derham.Vh["0"].zeros() - poisson_solver = propagators_fields.ImplicitDiffusion( - _phi, - sigma_1=0.0, - sigma_2=0.0, - sigma_3=1.0, - rho=self._alpha**2 / self._epsilon * charge_accum.vectors[0], - solver=self._poisson_params, - ) + alpha = self.kinetic_ions.equation_params.alpha + epsilon = self.kinetic_ions.equation_params.epsilon + + self.initial_poisson.options.rho = charge_accum + self.initial_poisson.options.rho_coeffs = alpha**2 / epsilon + self.initial_poisson.allocate() # Solve with dt=1. and compute electric field - if self.rank_world == 0: + if MPI.COMM_WORLD.Get_rank() == 0: print("\nSolving initial Poisson problem...") - poisson_solver(1.0) + self.initial_poisson(1.0) - self.derham.grad.dot(-_phi, out=self.pointer["e_field"]) - if self.rank_world == 0: + phi = self.initial_poisson.variables.phi.spline.vector + self.derham.grad.dot(-phi, out=self.em_fields.e_field.spline.vector) + if MPI.COMM_WORLD.Get_rank() == 0: print("Done.") - def update_scalar_quantities(self): - # e*M1*e/2 - en_E = 0.5 * self.mass_ops.M1.dot_inner(self.pointer["e_field"], self.pointer["e_field"]) - self.update_scalar("en_E", en_E) - - # alpha^2 / 2 / N * sum_p w_p v_p^2 - self._tmp[0] = ( - self._alpha**2 - / (2 * self.pointer["species1"].Np) - * np.dot( - self.pointer["species1"].markers_wo_holes[:, 3] ** 2 - + self.pointer["species1"].markers_wo_holes[:, 4] ** 2 - + self.pointer["species1"].markers_wo_holes[:, 5] ** 2, - self.pointer["species1"].markers_wo_holes[:, 6], - ) - ) - - self.update_scalar("en_f", self._tmp[0]) - - # en_tot = en_w + en_e - self.update_scalar("en_tot", en_E + self._tmp[0]) + ## default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "coupling_va.Options" in line: + new_file += [line] + new_file += ["model.initial_poisson.options = model.initial_poisson.Options()\n"] + elif "push_vxb.Options" in line: + new_file += ["if model.with_B0:\n"] + new_file += [" " + line] + elif "set_save_data" in line: + new_file += ["\nbinplot = BinningPlot(slice='e1', n_bins=128, ranges=(0.0, 1.0))\n"] + new_file += ["model.kinetic_ions.set_save_data(binning_plots=(binplot,))\n"] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) class VlasovMaxwellOneSpecies(StruphyModel): @@ -350,197 +323,171 @@ class VlasovMaxwellOneSpecies(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} + ## species - dct["em_fields"]["e_field"] = "Hcurl" - dct["em_fields"]["b_field"] = "Hdiv" - dct["kinetic"]["species1"] = "Particles6D" - return dct + class EMFields(FieldSpecies): + def __init__(self): + self.e_field = FEECVariable(space="Hcurl") + self.b_field = FEECVariable(space="Hdiv") + self.phi = FEECVariable(space="H1") + self.init_variables() - @staticmethod - def bulk_species(): - return "species1" + class KineticIons(ParticleSpecies): + def __init__(self): + self.var = PICVariable(space="Particles6D") + self.init_variables() - @staticmethod - def velocity_scale(): - return "light" + ## propagators - @staticmethod - def propagators_dct(): - return { - propagators_fields.Maxwell: ["e_field", "b_field"], - propagators_markers.PushEta: ["species1"], - propagators_markers.PushVxB: ["species1"], - propagators_coupling.VlasovAmpere: ["e_field", "species1"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - # add special options - @classmethod - def options(cls): - dct = super().options() - cls.add_option( - species=["em_fields"], - option=propagators_fields.ImplicitDiffusion, - dct=dct, - ) - cls.add_option( - species=["kinetic", "species1"], - key="override_eq_params", - option=[False, {"alpha": 1.0, "epsilon": -1.0}], - dct=dct, - ) - return dct + class Propagators: + def __init__(self): + self.maxwell = propagators_fields.Maxwell() + self.push_eta = propagators_markers.PushEta() + self.push_vxb = propagators_markers.PushVxB() + self.coupling_va = propagators_coupling.VlasovAmpere() - def __init__(self, params, comm, clone_config=None): - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) + ## abstract methods - # get species paramaters - species1_params = params["kinetic"]["species1"] + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") - # equation parameters - if species1_params["options"]["override_eq_params"]: - self._alpha = species1_params["options"]["override_eq_params"]["alpha"] - self._epsilon = species1_params["options"]["override_eq_params"]["epsilon"] - print( - f"\n!!! Override equation parameters: {self._alpha = } and {self._epsilon = }.", - ) - else: - self._alpha = self.equation_params["species1"]["alpha"] - self._epsilon = self.equation_params["species1"]["epsilon"] - - # set background density and mean velocity factors - self.pointer["species1"].f0.moment_factors["u"] = [ - self._epsilon / self._alpha**2, - ] * 3 - - # Initialize background magnetic field from MHD equilibrium - if self.projected_equil: - self._b_background = self.projected_equil.b2 - else: - self._b_background = None - - # propagator parameters - params_maxwell = params["em_fields"]["options"]["Maxwell"]["solver"] - algo_eta = params["kinetic"]["species1"]["options"]["PushEta"]["algo"] - algo_vxb = params["kinetic"]["species1"]["options"]["PushVxB"]["algo"] - params_coupling = params["em_fields"]["options"]["VlasovAmpere"]["solver"] - self._poisson_params = params["em_fields"]["options"]["ImplicitDiffusion"]["solver"] - - # set keyword arguments for propagators - self._kwargs[propagators_fields.Maxwell] = {"solver": params_maxwell} - - self._kwargs[propagators_markers.PushEta] = {"algo": algo_eta} - - self._kwargs[propagators_markers.PushVxB] = { - "algo": algo_vxb, - "kappa": 1.0 / self._epsilon, - "b2": self.pointer["b_field"], - "b2_add": self._b_background, - } - - self._kwargs[propagators_coupling.VlasovAmpere] = { - "c1": self._alpha**2 / self._epsilon, - "c2": 1.0 / self._epsilon, - "solver": params_coupling, - } - - # Initialize propagators used in splitting substeps - self.init_propagators() - - # Scalar variables to be saved during the simulation + # 1. instantiate all species + self.em_fields = self.EMFields() + self.kinetic_ions = self.KineticIons() + + # 2. instantiate all propagators + self.propagators = self.Propagators() + + # 3. assign variables to propagators + self.propagators.maxwell.variables.e = self.em_fields.e_field + self.propagators.maxwell.variables.b = self.em_fields.b_field + self.propagators.push_eta.variables.var = self.kinetic_ions.var + self.propagators.push_vxb.variables.ions = self.kinetic_ions.var + self.propagators.coupling_va.variables.e = self.em_fields.e_field + self.propagators.coupling_va.variables.ions = self.kinetic_ions.var + + # define scalars for update_scalar_quantities self.add_scalar("en_E") self.add_scalar("en_B") - self.add_scalar("en_f", compute="from_particles", species="species1") + self.add_scalar("en_f", compute="from_particles", variable=self.kinetic_ions.var) self.add_scalar("en_tot") - # temporaries - self._tmp = np.empty(1, dtype=float) + # initial Poisson (not a propagator used in time stepping) + self.initial_poisson = propagators_fields.Poisson() + self.initial_poisson.variables.phi = self.em_fields.phi + + @property + def bulk_species(self): + return self.kinetic_ions + + @property + def velocity_scale(self): + return "light" + + def allocate_helpers(self): + self._tmp = xp.empty(1, dtype=float) + + def update_scalar_quantities(self): + # e*M1*e/2 + e = self.em_fields.e_field.spline.vector + b = self.em_fields.b_field.spline.vector + + en_E = 0.5 * self.mass_ops.M1.dot_inner(e, e) + self.update_scalar("en_E", en_E) + + en_B = 0.5 * self.mass_ops.M2.dot_inner(b, b) + self.update_scalar("en_B", en_B) + + # alpha^2 / 2 / N * sum_p w_p v_p^2 + particles = self.kinetic_ions.var.particles + alpha = self.kinetic_ions.equation_params.alpha + self._tmp[0] = ( + alpha**2 + / (2 * particles.Np) + * xp.dot( + particles.markers_wo_holes[:, 3] ** 2 + + particles.markers_wo_holes[:, 4] ** 2 + + particles.markers_wo_holes[:, 5] ** 2, + particles.markers_wo_holes[:, 6], + ) + ) + self.update_scalar("en_f", self._tmp[0]) + + # en_tot = en_w + en_e + self.update_scalar("en_tot", en_E + self._tmp[0]) - def initialize_from_params(self): - """:meta private:""" + def allocate_propagators(self): + """Solve initial Poisson equation. - from struphy.pic.accumulation.particles_to_grid import AccumulatorVector + :meta private: + """ # initialize fields and particles - super().initialize_from_params() + super().allocate_propagators() - if self.rank_world == 0: + if MPI.COMM_WORLD.Get_rank() == 0: print("\nINITIAL POISSON SOLVE:") # use control variate method - self.pointer["species1"].update_weights() + particles = self.kinetic_ions.var.particles + particles.update_weights() # sanity check # self.pointer['species1'].show_distribution_function( - # [True] + [False]*5, [np.linspace(0, 1, 32)]) + # [True] + [False]*5, [xp.linspace(0, 1, 32)]) # accumulate charge density charge_accum = AccumulatorVector( - self.pointer["species1"], + particles, "H1", Pyccelkernel(accum_kernels.charge_density_0form), self.mass_ops, self.domain.args_domain, ) - charge_accum(self.pointer["species1"].vdim) - # another sanity check: compute FE coeffs of density # charge_accum.show_accumulated_spline_field(self.mass_ops) - # Instantiate Poisson solver - _phi = self.derham.Vh["0"].zeros() - poisson_solver = propagators_fields.ImplicitDiffusion( - _phi, - sigma_1=0.0, - sigma_2=0.0, - sigma_3=1.0, - rho=self._alpha**2 / self._epsilon * charge_accum.vectors[0], - solver=self._poisson_params, - ) + alpha = self.kinetic_ions.equation_params.alpha + epsilon = self.kinetic_ions.equation_params.epsilon + + self.initial_poisson.options.rho = charge_accum + self.initial_poisson.options.rho_coeffs = alpha**2 / epsilon + self.initial_poisson.allocate() # Solve with dt=1. and compute electric field - if self.rank_world == 0: + if MPI.COMM_WORLD.Get_rank() == 0: print("\nSolving initial Poisson problem...") - poisson_solver(1.0) + self.initial_poisson(1.0) - self.derham.grad.dot(-_phi, out=self.pointer["e_field"]) - if self.rank_world == 0: + phi = self.initial_poisson.variables.phi.spline.vector + self.derham.grad.dot(-phi, out=self.em_fields.e_field.spline.vector) + if MPI.COMM_WORLD.Get_rank() == 0: print("Done.") - def update_scalar_quantities(self): - # e*M1*e and b*M2*b - en_E = 0.5 * self.mass_ops.M1.dot_inner(self.pointer["e_field"], self.pointer["e_field"]) - en_B = 0.5 * self.mass_ops.M2.dot_inner(self.pointer["b_field"], self.pointer["b_field"]) - self.update_scalar("en_E", en_E) - self.update_scalar("en_B", en_B) - - # alpha^2 / 2 / N * sum_p w_p v_p^2 - self._tmp[0] = ( - self._alpha**2 - / (2 * self.pointer["species1"].Np) - * np.dot( - self.pointer["species1"].markers_wo_holes[:, 3] ** 2 - + self.pointer["species1"].markers_wo_holes[:, 4] ** 2 - + self.pointer["species1"].markers_wo_holes[:, 5] ** 2, - self.pointer["species1"].markers_wo_holes[:, 6], - ) - ) - - self.update_scalar("en_f", self._tmp[0]) - - # en_tot = en_w + en_e + en_b - self.update_scalar("en_tot", en_E + en_B + self._tmp[0]) + ## default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "coupling_va.Options" in line: + new_file += [line] + new_file += ["model.initial_poisson.options = model.initial_poisson.Options()\n"] + elif "push_vxb.Options" in line: + new_file += [ + "model.propagators.push_vxb.options = model.propagators.push_vxb.Options(b2_var=model.em_fields.b_field)\n" + ] + elif "set_save_data" in line: + new_file += ["\nbinplot = BinningPlot(slice='e1', n_bins=128, ranges=(0.0, 1.0))\n"] + new_file += ["model.kinetic_ions.set_save_data(binning_plots=(binplot,))\n"] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) class LinearVlasovAmpereOneSpecies(StruphyModel): @@ -610,245 +557,188 @@ class LinearVlasovAmpereOneSpecies(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} + ## species + + class EMFields(FieldSpecies): + def __init__(self): + self.e_field = FEECVariable(space="Hcurl") + self.phi = FEECVariable(space="H1") + self.init_variables() + + class KineticIons(ParticleSpecies): + def __init__(self): + self.var = PICVariable(space="DeltaFParticles6D") + self.init_variables() + + ## propagators + + class Propagators: + def __init__( + self, + with_B0: bool = True, + with_E0: bool = True, + ): + self.push_eta = propagators_markers.PushEta() + if with_E0: + self.push_vinE = propagators_markers.PushVinEfield() + self.coupling_Eweights = propagators_coupling.EfieldWeights() + if with_B0: + self.push_vxb = propagators_markers.PushVxB() + + ## abstract methods + + def __init__( + self, + with_B0: bool = True, + with_E0: bool = True, + ): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + + # 1. instantiate all species + self.em_fields = self.EMFields() + self.kinetic_ions = self.KineticIons() + + # 2. instantiate all propagators + self.propagators = self.Propagators(with_B0=with_B0, with_E0=with_E0) + + # 3. assign variables to propagators + self.propagators.push_eta.variables.var = self.kinetic_ions.var + if with_E0: + self.propagators.push_vinE.variables.var = self.kinetic_ions.var + self.propagators.coupling_Eweights.variables.e = self.em_fields.e_field + self.propagators.coupling_Eweights.variables.ions = self.kinetic_ions.var + if with_B0: + self.propagators.push_vxb.variables.ions = self.kinetic_ions.var + + # define scalars for update_scalar_quantities + self.add_scalar("en_E") + self.add_scalar("en_w", compute="from_particles", variable=self.kinetic_ions.var) + self.add_scalar("en_tot") - dct["em_fields"]["e_field"] = "Hcurl" - dct["kinetic"]["species1"] = "DeltaFParticles6D" - return dct + # initial Poisson (not a propagator used in time stepping) + self.initial_poisson = propagators_fields.Poisson() + self.initial_poisson.variables.phi = self.em_fields.phi - @staticmethod - def bulk_species(): - return "species1" + @property + def bulk_species(self): + return self.kinetic_ions - @staticmethod - def velocity_scale(): + @property + def velocity_scale(self): return "light" - @staticmethod - def propagators_dct(): - return { - propagators_markers.PushEta: ["species1"], - propagators_markers.PushVinEfield: ["species1"], - propagators_coupling.EfieldWeights: ["e_field", "species1"], - propagators_markers.PushVxB: ["species1"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - @classmethod - def options(cls): - dct = super().options() - cls.add_option( - species=["em_fields"], - option=propagators_fields.ImplicitDiffusion, - dct=dct, - ) - cls.add_option( - species=["kinetic", "species1"], - key="override_eq_params", - option=[False, {"epsilon": -1.0, "alpha": 1.0}], - dct=dct, - ) - return dct + def allocate_helpers(self): + self._tmp = xp.empty(1, dtype=float) - def __init__(self, params, comm, clone_config=None, baseclass=False): - """Initializes the model either as the full model or as a baseclass to inherit from. - In case of being a baseclass, the propagators will not be initialized in the __init__ which allows other propagators to be added. - - Parameters - ---------- - baseclass : Boolean [optional] - If this model should be used as a baseclass. Default value is False. - """ + def update_scalar_quantities(self): + # e*M1*e/2 + e = self.em_fields.e_field.spline.vector + particles = self.kinetic_ions.var.particles - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) + en_E = 0.5 * self.mass_ops.M1.dot_inner(e, e) + self.update_scalar("en_E", en_E) - from struphy.kinetic_background import maxwellians + # evaluate f0 + if not hasattr(self, "_f0"): + backgrounds = self.kinetic_ions.var.backgrounds + if isinstance(backgrounds, list): + self._f0 = backgrounds[0] + else: + self._f0 = backgrounds + self._f0_values = xp.zeros( + self.kinetic_ions.var.particles.markers.shape[0], + dtype=float, + ) + assert isinstance(self._f0, Maxwellian3D) - # if model is used as a baseclass - self._baseclass = baseclass + self._f0_values[particles.valid_mks] = self._f0(*particles.phasespace_coords.T) - # kinetic parameters - self._species_params = params["kinetic"]["species1"] + # alpha^2 * v_th^2 / (2*N) * sum_p s_0 * w_p^2 / f_{0,p} + alpha = self.kinetic_ions.equation_params.alpha + vth = self._f0.maxw_params["vth1"][0] - # Assert Maxwellian background (if list, the first entry is taken) - bckgr_params = self._species_params["background"] - li_bp = list(bckgr_params) - assert li_bp[0] == "Maxwellian3D", "The background distribution function must be a uniform Maxwellian!" - if len(li_bp) > 1: - # overwrite f0 with single Maxwellian - self._f0 = getattr(maxwellians, li_bp[0][:-2])( - maxw_params=bckgr_params[li_bp[0]], + self._tmp[0] = ( + alpha**2 + * vth**2 + / (2 * particles.Np) + * xp.dot( + particles.weights**2, # w_p^2 + particles.sampling_density / self._f0_values[particles.valid_mks], # s_{0,p} / f_{0,p} ) - else: - # keep allocated background - self._f0 = self.pointer["species1"].f0 - - # Assert uniformity of the Maxwellian background - assert self._f0.maxw_params["u1"] == 0.0, "The background Maxwellian cannot have shifts in velocity space!" - assert self._f0.maxw_params["u2"] == 0.0, "The background Maxwellian cannot have shifts in velocity space!" - assert self._f0.maxw_params["u3"] == 0.0, "The background Maxwellian cannot have shifts in velocity space!" - assert self._f0.maxw_params["vth1"] == self._f0.maxw_params["vth2"] == self._f0.maxw_params["vth3"], ( - "The background Maxwellian must be isotropic in velocity space!" - ) - self.vth = self._f0.maxw_params["vth1"] - - # Get coupling strength - if self._species_params["options"]["override_eq_params"]: - self.epsilon = self._species_params["options"]["override_eq_params"]["epsilon"] - self.alpha = self._species_params["options"]["override_eq_params"]["alpha"] - if self.rank_world == 0: - print( - f"\n!!! Override equation parameters: {self.epsilon = }, {self.alpha = }.\n", - ) - else: - self.epsilon = self.equation_params["species1"]["epsilon"] - self.alpha = self.equation_params["species1"]["alpha"] - - # allocate memory for evaluating f0 in energy computation - self._f0_values = np.zeros( - self.pointer["species1"].markers.shape[0], - dtype=float, ) - # ==================================================================================== - # Create pointers to background electric potential and field - self._has_background_e = False - if "external_E0" in self.params["em_fields"]["options"].keys(): - e0 = self.params["em_fields"]["options"]["external_E0"] - if e0 != 0.0: - self._has_background_e = True - self._e_background = self.derham.Vh["1"].zeros() - for block in self._e_background._blocks: - block._data[:, :, :] += e0 - - # Get parameters of the background magnetic field - if self.projected_equil: - self._b_background = self.projected_equil.b2 - else: - self._b_background = None - # ==================================================================================== - - # propagator parameters - self._poisson_params = params["em_fields"]["options"]["ImplicitDiffusion"]["solver"] - algo_eta = params["kinetic"]["species1"]["options"]["PushEta"]["algo"] - params_coupling = params["em_fields"]["options"]["EfieldWeights"]["solver"] - - # Initialize propagators/integrators used in splitting substeps - self._kwargs[propagators_markers.PushEta] = { - "algo": algo_eta, - } - - # Only add PushVinEfield if e-field is non-zero, otherwise it is more expensive - if self._has_background_e: - self._kwargs[propagators_markers.PushVinEfield] = { - "e_field": self._e_background, - "kappa": 1.0 / self.epsilon, - } - else: - self._kwargs[propagators_markers.PushVinEfield] = None - - self._kwargs[propagators_coupling.EfieldWeights] = { - "alpha": self.alpha, - "kappa": 1.0 / self.epsilon, - "f0": self._f0, - "solver": params_coupling, - } - - # Only add PushVxB if magnetic field is not zero - self._kwargs[propagators_markers.PushVxB] = None - if self._b_background: - self._kwargs[propagators_markers.PushVxB] = { - "kappa": 1.0 / self.epsilon, - "b2": self._b_background, - } - - # Initialize propagators used in splitting substeps - if not self._baseclass: - self.init_propagators() - - # Scalar variables to be saved during the simulation - self.add_scalar("en_E") - self.add_scalar("en_w", compute="from_particles", species="species1") - self.add_scalar("en_tot") - - # temporaries - self._tmp = np.empty(1, dtype=float) - self.en_E = 0.0 + self.update_scalar("en_w", self._tmp[0]) + self.update_scalar("en_tot", self._tmp[0] + en_E) - def initialize_from_params(self): + def allocate_propagators(self): """Solve initial Poisson equation. :meta private: """ - from struphy.pic.accumulation.particles_to_grid import AccumulatorVector - # Initialize fields and particles - super().initialize_from_params() + # initialize fields and particles + super().allocate_propagators() + + if MPI.COMM_WORLD.Get_rank() == 0: + print("\nINITIAL POISSON SOLVE:") + + # use control variate method + particles = self.kinetic_ions.var.particles + particles.update_weights() + + # sanity check + # self.pointer['species1'].show_distribution_function( + # [True] + [False]*5, [xp.linspace(0, 1, 32)]) - # Accumulate charge density + # accumulate charge density charge_accum = AccumulatorVector( - self.pointer["species1"], + particles, "H1", Pyccelkernel(accum_kernels.charge_density_0form), self.mass_ops, self.domain.args_domain, ) - charge_accum(self.pointer["species1"].vdim) - - # Instantiate Poisson solver - _phi = self.derham.Vh["0"].zeros() - poisson_solver = propagators_fields.ImplicitDiffusion( - _phi, - sigma_1=0.0, - sigma_2=0.0, - sigma_3=1.0, - rho=self.alpha**2 / self.epsilon * charge_accum.vectors[0], - solver=self._poisson_params, - ) - - # Solve with dt=1. and compute electric field - if self.rank_world == 0: - print("\nSolving initial Poisson problem...") - poisson_solver(1.0) - self.derham.grad.dot(-_phi, out=self.pointer["e_field"]) - if self.rank_world == 0: - print("Done.") + # another sanity check: compute FE coeffs of density + # charge_accum.show_accumulated_spline_field(self.mass_ops) - def update_scalar_quantities(self): - # 0.5 * e^T * M_1 * e - self.en_E = 0.5 * self.mass_ops.M1.dot_inner(self.pointer["e_field"], self.pointer["e_field"]) - self.update_scalar("en_E", self.en_E) + alpha = self.kinetic_ions.equation_params.alpha + epsilon = self.kinetic_ions.equation_params.epsilon - # evaluate f0 - self._f0_values[self.pointer["species1"].valid_mks] = self._f0(*self.pointer["species1"].phasespace_coords.T) + self.initial_poisson.options.rho = charge_accum + self.initial_poisson.options.rho_coeffs = alpha**2 / epsilon + self.initial_poisson.allocate() - # alpha^2 * v_th^2 / (2*N) * sum_p s_0 * w_p^2 / f_{0,p} - self._tmp[0] = ( - self.alpha**2 - * self.vth**2 - / (2 * self.pointer["species1"].Np) - * np.dot( - self.pointer["species1"].weights ** 2, # w_p^2 - self.pointer["species1"].sampling_density - / self._f0_values[self.pointer["species1"].valid_mks], # s_{0,p} / f_{0,p} - ) - ) + # Solve with dt=1. and compute electric field + if MPI.COMM_WORLD.Get_rank() == 0: + print("\nSolving initial Poisson problem...") + self.initial_poisson(1.0) - self.update_scalar("en_w", self._tmp[0]) + phi = self.initial_poisson.variables.phi.spline.vector + self.derham.grad.dot(-phi, out=self.em_fields.e_field.spline.vector) + if MPI.COMM_WORLD.Get_rank() == 0: + print("Done.") - # en_tot = en_w + en_e - if not self._baseclass: - self.update_scalar("en_tot", self._tmp[0] + self.en_E) + ## default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "maxwellian_1 + maxwellian_2" in line: + new_file += ["background = maxwellian_1\n"] + elif "maxwellian_1pt =" in line: + new_file += ["maxwellian_1pt = maxwellians.Maxwellian3D(n=(0.0, perturbation))\n"] + elif "set_save_data" in line: + new_file += ["\nbinplot = BinningPlot(slice='e1', n_bins=128, ranges=(0.0, 1.0))\n"] + new_file += ["model.kinetic_ions.set_save_data(binning_plots=(binplot,))\n"] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) class LinearVlasovMaxwellOneSpecies(LinearVlasovAmpereOneSpecies): @@ -921,80 +811,82 @@ class LinearVlasovMaxwellOneSpecies(LinearVlasovAmpereOneSpecies): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} - - dct["em_fields"]["e_field"] = "Hcurl" - dct["em_fields"]["b_field"] = "Hdiv" - dct["kinetic"]["species1"] = "DeltaFParticles6D" - return dct - - @staticmethod - def bulk_species(): - return "species1" - - @staticmethod - def velocity_scale(): - return "light" - - @staticmethod - def propagators_dct(): - return { - propagators_markers.PushEta: ["species1"], - propagators_markers.PushVinEfield: ["species1"], - propagators_coupling.EfieldWeights: ["e_field", "species1"], - propagators_markers.PushVxB: ["species1"], - propagators_fields.Maxwell: ["e_field", "b_field"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - @classmethod - def options(cls): - dct = super().options() - cls.add_option( - species=["em_fields"], - option=propagators_fields.ImplicitDiffusion, - dct=dct, - ) - cls.add_option( - species=["kinetic", "species1"], - key="override_eq_params", - option=[False, {"epsilon": -1.0, "alpha": 1.0}], - dct=dct, - ) - return dct - - def __init__(self, params, comm, clone_config=None): - super().__init__(params=params, comm=comm, clone_config=clone_config, baseclass=True) - - # propagator parameters - params_maxwell = params["em_fields"]["options"]["Maxwell"]["solver"] - - # set keyword arguments for propagators - self._kwargs[propagators_fields.Maxwell] = {"solver": params_maxwell} - - # Initialize propagators used in splitting substeps - self.init_propagators() - - # magnetic energy - self.add_scalar("en_b") + ## species + + class EMFields(FieldSpecies): + def __init__(self): + self.e_field = FEECVariable(space="Hcurl") + self.b_field = FEECVariable(space="Hdiv") + self.phi = FEECVariable(space="H1") + self.init_variables() + + class KineticIons(ParticleSpecies): + def __init__(self): + self.var = PICVariable(space="DeltaFParticles6D") + self.init_variables() + + ## propagators + + class Propagators: + def __init__( + self, + with_B0: bool = True, + with_E0: bool = True, + ): + self.push_eta = propagators_markers.PushEta() + if with_E0: + self.push_vinE = propagators_markers.PushVinEfield() + self.coupling_Eweights = propagators_coupling.EfieldWeights() + if with_B0: + self.push_vxb = propagators_markers.PushVxB() + self.maxwell = propagators_fields.Maxwell() + + ## abstract methods + + def __init__( + self, + with_B0: bool = True, + with_E0: bool = True, + ): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + + # 1. instantiate all species + self.em_fields = self.EMFields() + self.kinetic_ions = self.KineticIons() + + # 2. instantiate all propagators + self.propagators = self.Propagators(with_B0=with_B0, with_E0=with_E0) + + # 3. assign variables to propagators + self.propagators.push_eta.variables.var = self.kinetic_ions.var + if with_E0: + self.propagators.push_vinE.variables.var = self.kinetic_ions.var + self.propagators.coupling_Eweights.variables.e = self.em_fields.e_field + self.propagators.coupling_Eweights.variables.ions = self.kinetic_ions.var + if with_B0: + self.propagators.push_vxb.variables.ions = self.kinetic_ions.var + self.propagators.maxwell.variables.e = self.em_fields.e_field + self.propagators.maxwell.variables.b = self.em_fields.b_field + + # define scalars for update_scalar_quantities + self.add_scalar("en_E") + self.add_scalar("en_B") + self.add_scalar("en_w", compute="from_particles", variable=self.kinetic_ions.var) + self.add_scalar("en_tot") - def initialize_from_params(self): - super().initialize_from_params() + # initial Poisson (not a propagator used in time stepping) + self.initial_poisson = propagators_fields.Poisson() + self.initial_poisson.variables.phi = self.em_fields.phi def update_scalar_quantities(self): super().update_scalar_quantities() # 0.5 * b^T * M_2 * b - en_B = 0.5 * self._mass_ops.M2.dot_inner(self.pointer["b_field"], self.pointer["b_field"]) - self.update_scalar("en_tot", self._tmp[0] + self.en_E + en_B) + b = self.em_fields.b_field.spline.vector + + en_B = 0.5 * self._mass_ops.M2.dot_inner(b, b) + self.update_scalar("en_tot", self.scalar_quantities["en_tot"]["value"][0] + en_B) class DriftKineticElectrostaticAdiabatic(StruphyModel): @@ -1046,153 +938,159 @@ class DriftKineticElectrostaticAdiabatic(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} + ## species + + class EMFields(FieldSpecies): + def __init__(self): + self.phi = FEECVariable(space="H1") + self.init_variables() + + class KineticIons(ParticleSpecies): + def __init__(self): + self.var = PICVariable(space="Particles5D") + self.init_variables() + + ## propagators - dct["em_fields"]["phi"] = "H1" - dct["kinetic"]["ions"] = "Particles5D" - return dct + class Propagators: + def __init__(self): + self.gc_poisson = propagators_fields.ImplicitDiffusion() + self.push_gc_bxe = propagators_markers.PushGuidingCenterBxEstar() + self.push_gc_para = propagators_markers.PushGuidingCenterParallel() - @staticmethod - def bulk_species(): - return "ions" + ## abstract methods - @staticmethod - def velocity_scale(): + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + + # 1. instantiate all species + self.em_fields = self.EMFields() + self.kinetic_ions = self.KineticIons() + + # 2. instantiate all propagators + self.propagators = self.Propagators() + + # 3. assign variables to propagators + self.propagators.gc_poisson.variables.phi = self.em_fields.phi + self.propagators.push_gc_bxe.variables.ions = self.kinetic_ions.var + self.propagators.push_gc_para.variables.ions = self.kinetic_ions.var + + # define scalars for update_scalar_quantities + self.add_scalar("en_phi") + self.add_scalar("en_particles", compute="from_particles", variable=self.kinetic_ions.var) + self.add_scalar("en_tot") + + @property + def bulk_species(self): + return self.kinetic_ions + + @property + def velocity_scale(self): return "thermal" - @staticmethod - def propagators_dct(): - return { - propagators_fields.ImplicitDiffusion: ["phi"], - propagators_markers.PushGuidingCenterBxEstar: ["ions"], - propagators_markers.PushGuidingCenterParallel: ["ions"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - # add special options - @classmethod - def options(cls): - dct = super().options() - cls.add_option( - species=["kinetic", "ions"], - key="override_eq_params", - option=[False, {"epsilon": 1.0}], - dct=dct, - ) - return dct + def allocate_helpers(self): + self._tmp3 = xp.empty(1, dtype=float) + self._e_field = self.derham.Vh["1"].zeros() - def __init__(self, params, comm, clone_config=None): - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) + assert self.kinetic_ions.charge_number > 0, "Model written only for positive ions." - from struphy.feec.projectors import L2Projector - from struphy.pic.accumulation.particles_to_grid import AccumulatorVector + def allocate_propagators(self): + """Solve initial Poisson equation. - # prelim - solver_params = params["em_fields"]["options"]["ImplicitDiffusion"]["solver"] - ions_params = params["kinetic"]["ions"] + :meta private: + """ - Z = ions_params["phys_params"]["Z"] - assert Z > 0 # must be positive ions + # initialize fields and particles + super().allocate_propagators() # Poisson right-hand side + particles = self.kinetic_ions.var.particles + Z = self.kinetic_ions.charge_number + epsilon = self.kinetic_ions.equation_params.epsilon + charge_accum = AccumulatorVector( - self.pointer["ions"], + particles, "H1", Pyccelkernel(accum_kernels_gc.gc_density_0form), self.mass_ops, self.domain.args_domain, ) - rho = (charge_accum, self.pointer["ions"]) + rho = charge_accum # get neutralizing background density - if not self.pointer["ions"].control_variate: + if not particles.control_variate: l2_proj = L2Projector("H1", self.mass_ops) - f0e = Z * self.pointer["ions"].f0 + f0e = Z * particles.f0 assert isinstance(f0e, KineticBackground) - rho_eh = l2_proj.get_dofs(f0e.n) + rho_eh = FEECVariable(space="H1") + rho_eh.allocate(derham=self.derham, domain=self.domain) + rho_eh.spline.vector = l2_proj.get_dofs(f0e.n) rho = [rho] rho += [rho_eh] - # Get coupling strength - if ions_params["options"]["override_eq_params"]: - self.epsilon = ions_params["options"]["override_eq_params"]["epsilon"] - print( - f"\n!!! Override equation parameters: {self.epsilon = }.", - ) - else: - self.epsilon = self.equation_params["ions"]["epsilon"] - - # set keyword arguments for propagators - self._kwargs[propagators_fields.ImplicitDiffusion] = { - "sigma_1": 1.0 / self.epsilon**2 / Z, # set to zero for Landau damping test - "sigma_2": 0.0, - "sigma_3": 1.0 / self.epsilon, - "stab_mat": "M0ad", - "diffusion_mat": "M1gyro", - "rho": rho, - "solver": solver_params, - } - - self._kwargs[propagators_markers.PushGuidingCenterBxEstar] = { - "phi": self.pointer["phi"], - "evaluate_e_field": True, - "epsilon": self.epsilon / Z, - "algo": ions_params["options"]["PushGuidingCenterBxEstar"]["algo"], - } - - self._kwargs[propagators_markers.PushGuidingCenterParallel] = { - "phi": self.pointer["phi"], - "evaluate_e_field": True, - "epsilon": self.epsilon / Z, - "algo": ions_params["options"]["PushGuidingCenterParallel"]["algo"], - } - - # Initialize propagators used in splitting substeps - self.init_propagators() - - # scalar quantities - self.add_scalar("en_phi") - self.add_scalar("en_particles", compute="from_particles", species="ions") - self.add_scalar("en_tot") - - # MPI operations needed for scalar variables - self._tmp3 = np.empty(1, dtype=float) - self._e_field = self.derham.Vh["1"].zeros() + self.propagators.gc_poisson.options.sigma_1 = 1.0 / epsilon**2 / Z + self.propagators.gc_poisson.options.sigma_2 = 0.0 + self.propagators.gc_poisson.options.sigma_3 = 1.0 / epsilon + self.propagators.gc_poisson.options.stab_mat = "M0ad" + self.propagators.gc_poisson.options.diffusion_mat = "M1perp" + self.propagators.gc_poisson.options.rho = rho + self.propagators.gc_poisson.allocate() def update_scalar_quantities(self): + phi = self.em_fields.phi.spline.vector + particles = self.kinetic_ions.var.particles + epsilon = self.kinetic_ions.equation_params.epsilon + # energy from polarization - e1 = self.derham.grad.dot(-self.pointer["phi"], out=self._e_field) + e1 = self.derham.grad.dot(-phi, out=self._e_field) en_phi1 = 0.5 * self.mass_ops.M1gyro.dot_inner(e1, e1) # energy from adiabatic electrons - en_phi = 0.5 / self.epsilon**2 * self.mass_ops.M0ad.dot_inner(self.pointer["phi"], self.pointer["phi"]) + en_phi = 0.5 / epsilon**2 * self.mass_ops.M0ad.dot_inner(phi, phi) # for Landau damping test # en_phi = 0. # mu_p * |B0(eta_p)| - self.pointer["ions"].save_magnetic_background_energy() + particles.save_magnetic_background_energy() # 1/N sum_p (w_p v_p^2/2 + mu_p |B0|_p) self._tmp3[0] = ( 1 - / self.pointer["ions"].Np - * np.sum( - self.pointer["ions"].weights * self.pointer["ions"].velocities[:, 0] ** 2 / 2.0 - + self.pointer["ions"].markers_wo_holes_and_ghost[:, 8], + / particles.Np + * xp.sum( + particles.weights * particles.velocities[:, 0] ** 2 / 2.0 + particles.markers_wo_holes_and_ghost[:, 8], ) ) self.update_scalar("en_phi", en_phi + en_phi1) self.update_scalar("en_particles", self._tmp3[0]) self.update_scalar("en_tot", en_phi + en_phi1 + self._tmp3[0]) + + ## default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "BaseUnits(" in line: + new_file += ["base_units = BaseUnits(kBT=1.0)\n"] + elif "push_gc_bxe.Options" in line: + new_file += [ + "model.propagators.push_gc_bxe.options = model.propagators.push_gc_bxe.Options(phi=model.em_fields.phi)\n" + ] + elif "push_gc_para.Options" in line: + new_file += [ + "model.propagators.push_gc_para.options = model.propagators.push_gc_para.Options(phi=model.em_fields.phi)\n" + ] + elif "set_save_data" in line: + new_file += ["\nbinplot = BinningPlot(slice='e1', n_bins=128, ranges=(0.0, 1.0))\n"] + new_file += ["model.kinetic_ions.set_save_data(binning_plots=(binplot,))\n"] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) diff --git a/src/struphy/models/species.py b/src/struphy/models/species.py new file mode 100644 index 000000000..56a338ca9 --- /dev/null +++ b/src/struphy/models/species.py @@ -0,0 +1,211 @@ +import warnings +from abc import ABCMeta, abstractmethod + +import cunumpy as xp +from psydac.ddm.mpi import mpi as MPI + +from struphy.io.options import Units +from struphy.models.variables import Variable +from struphy.physics.physics import ConstantsOfNature +from struphy.pic.utilities import ( + BinningPlot, + BoundaryParameters, + KernelDensityPlot, + LoadingParameters, + WeightsParameters, +) + + +class Species(metaclass=ABCMeta): + """Single species of a StruphyModel.""" + + @abstractmethod + def __init__(self): + self.init_variables() + + # set species attribute for each variable + def init_variables(self): + self._variables = {} + for k, v in self.__dict__.items(): + if isinstance(v, Variable): + v._name = k + v._species = self + self._variables[k] = v + + @property + def variables(self) -> dict: + return self._variables + + @property + def charge_number(self) -> int: + """Charge number in units of elementary charge.""" + return self._charge_number + + @property + def mass_number(self) -> int: + """Mass number in units of proton mass.""" + return self._mass_number + + def set_phys_params( + self, + charge_number: int = 1, + mass_number: int = 1, + alpha: float = None, + epsilon: float = None, + kappa: float = None, + ): + """Set charge- and mass number. Set equation parameters (alpha, epsilon, ...) to override units.""" + self._charge_number = charge_number + self._mass_number = mass_number + self.alpha = alpha + self.epsilon = epsilon + self.kappa = kappa + + class EquationParameters: + """Normalization parameters of one species, appearing in scaled equations.""" + + def __init__( + self, + species, + units: Units = None, + alpha: float = None, + epsilon: float = None, + kappa: float = None, + verbose: bool = False, + ): + if units is None: + units = Units() + + Z = species.charge_number + A = species.mass_number + + con = ConstantsOfNature() + + # relevant frequencies + om_p = xp.sqrt(units.n * (Z * con.e) ** 2 / (con.eps0 * A * con.mH)) + om_c = Z * con.e * units.B / (A * con.mH) + + # compute equation parameters + if alpha is None: + self.alpha = om_p / om_c + else: + self.alpha = alpha + if MPI.COMM_WORLD.Get_rank() == 0: + warnings.warn(f"Override equation parameter {self.alpha = }") + + if epsilon is None: + self.epsilon = 1.0 / (om_c * units.t) + else: + self.epsilon = epsilon + if MPI.COMM_WORLD.Get_rank() == 0: + warnings.warn(f"Override equation parameter {self.epsilon = }") + + if kappa is None: + self.kappa = om_p * units.t + else: + self.kappa = kappa + if MPI.COMM_WORLD.Get_rank() == 0: + warnings.warn(f"Override equation parameter {self.kappa = }") + + if verbose and MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nSet normalization parameters for species {species.__class__.__name__}:") + for key, val in self.__dict__.items(): + print((key + ":").ljust(25), "{:4.3e}".format(val)) + + @property + def equation_params(self) -> EquationParameters: + return self._equation_params + + def setup_equation_params(self, units: Units, verbose=False): + """Set the following equation parameters: + + * alpha = plasma-frequenca / cyclotron frequency + * epsilon = 1 / (cyclotron frequency * time unit) + * kappa = plasma frequency * time unit + """ + self._equation_params = self.EquationParameters( + species=self, + units=units, + alpha=self.alpha, + epsilon=self.epsilon, + kappa=self.kappa, + verbose=verbose, + ) + + +class FieldSpecies(Species): + """Species without mass and charge (so-called 'fields').""" + + +class FluidSpecies(Species): + """Single fluid species in 3d configuration space.""" + + +class ParticleSpecies(Species): + """Single kinetic species in 3d + vdim phase space.""" + + def set_markers( + self, + loading_params: LoadingParameters = None, + weights_params: WeightsParameters = None, + boundary_params: BoundaryParameters = None, + bufsize: float = 1.0, + ): + """Set marker parameters for loading, weight calculation, kernel density reconstruction + and boundary conditions. + + Parameters + ---------- + loading_params : LoadingParameters + + weights_params : WeightsParameters + + boundary_params : BoundaryParameters + + bufsize : float + Size of buffer (as multiple of total size, default=.25) in markers array.""" + + # defaults + if loading_params is None: + loading_params = LoadingParameters() + + if weights_params is None: + weights_params = WeightsParameters() + + if boundary_params is None: + boundary_params = BoundaryParameters() + + self.loading_params = loading_params + self.weights_params = weights_params + self.boundary_params = boundary_params + self.bufsize = bufsize + + def set_sorting_boxes( + self, + do_sort: bool = False, + sorting_frequency: int = 0, + boxes_per_dim: tuple = (12, 12, 1), + box_bufsize: float = 2.0, + dims_maks: tuple = (True, True, True), + ): + """For sorting markers in memory.""" + self.do_sort = do_sort + self.sorting_fequency = sorting_frequency + self.boxes_per_dim = boxes_per_dim + self.box_bufsize = box_bufsize + self.dims_mask = dims_maks + + def set_save_data( + self, + n_markers: int | float = 3, + binning_plots: tuple[BinningPlot] = (), + kernel_density_plots: tuple[KernelDensityPlot] = (), + ): + """Saving marker orits, binned data and kernel density reconstructions.""" + self.n_markers = n_markers + self.binning_plots = binning_plots + self.kernel_density_plots = kernel_density_plots + + +class DiagnosticSpecies(Species): + """Diagnostic species (fields) without mass and charge.""" diff --git a/src/struphy/models/tests/test_fluid_models.py b/src/struphy/models/tests/test_fluid_models.py deleted file mode 100644 index 44473e163..000000000 --- a/src/struphy/models/tests/test_fluid_models.py +++ /dev/null @@ -1,28 +0,0 @@ -import inspect - -import pytest - -from struphy.models.tests.util import wrapper_for_testing - - -@pytest.mark.parametrize( - "map_and_equil", [("Cuboid", "HomogenSlab"), ("HollowTorus", "AdhocTorus"), ("Tokamak", "EQDSKequilibrium")] -) -def test_fluid( - map_and_equil: tuple | list, - fast: bool, - vrbose: bool, - verification: bool, - nclones: int, - show_plots: bool, -): - """Tests all models in models/fluid.py.""" - wrapper_for_testing( - mtype="fluid", - map_and_equil=map_and_equil, - fast=fast, - vrbose=vrbose, - verification=verification, - nclones=nclones, - show_plots=show_plots, - ) diff --git a/src/struphy/models/tests/test_hybrid_models.py b/src/struphy/models/tests/test_hybrid_models.py deleted file mode 100644 index fb056a86e..000000000 --- a/src/struphy/models/tests/test_hybrid_models.py +++ /dev/null @@ -1,28 +0,0 @@ -import inspect - -import pytest - -from struphy.models.tests.util import wrapper_for_testing - - -@pytest.mark.parametrize( - "map_and_equil", [("Cuboid", "HomogenSlab"), ("HollowTorus", "AdhocTorus"), ("Tokamak", "EQDSKequilibrium")] -) -def test_hybrid( - map_and_equil: tuple | list, - fast: bool, - vrbose: bool, - verification: bool, - nclones: int, - show_plots: bool, -): - """Tests all models in models/hybrid.py.""" - wrapper_for_testing( - mtype="hybrid", - map_and_equil=map_and_equil, - fast=fast, - vrbose=vrbose, - verification=verification, - nclones=nclones, - show_plots=show_plots, - ) diff --git a/src/struphy/models/tests/test_kinetic_models.py b/src/struphy/models/tests/test_kinetic_models.py deleted file mode 100644 index 33180b74a..000000000 --- a/src/struphy/models/tests/test_kinetic_models.py +++ /dev/null @@ -1,28 +0,0 @@ -import inspect - -import pytest - -from struphy.models.tests.util import wrapper_for_testing - - -@pytest.mark.parametrize( - "map_and_equil", [("Cuboid", "HomogenSlab"), ("HollowTorus", "AdhocTorus"), ("Tokamak", "EQDSKequilibrium")] -) -def test_kinetic( - map_and_equil: tuple | list, - fast: bool, - vrbose: bool, - verification: bool, - nclones: int, - show_plots: bool, -): - """Tests models in models/kinetic.py.""" - wrapper_for_testing( - mtype="kinetic", - map_and_equil=map_and_equil, - fast=fast, - vrbose=vrbose, - verification=verification, - nclones=nclones, - show_plots=show_plots, - ) diff --git a/src/struphy/models/tests/test_models.py b/src/struphy/models/tests/test_models.py new file mode 100644 index 000000000..36a9ea01b --- /dev/null +++ b/src/struphy/models/tests/test_models.py @@ -0,0 +1,176 @@ +import inspect +import os +from types import ModuleType + +import pytest +from psydac.ddm.mpi import mpi as MPI + +from struphy import main +from struphy.io.options import EnvironmentOptions +from struphy.io.setup import import_parameters_py +from struphy.models import fluid, hybrid, kinetic, toy +from struphy.models.base import StruphyModel + +rank = MPI.COMM_WORLD.Get_rank() + +# available models +toy_models = [] +for name, obj in inspect.getmembers(toy): + if inspect.isclass(obj) and "models.toy" in obj.__module__: + toy_models += [name] +if rank == 0: + print(f"\n{toy_models = }") + +fluid_models = [] +for name, obj in inspect.getmembers(fluid): + if inspect.isclass(obj) and "models.fluid" in obj.__module__: + fluid_models += [name] +if rank == 0: + print(f"\n{fluid_models = }") + +kinetic_models = [] +for name, obj in inspect.getmembers(kinetic): + if inspect.isclass(obj) and "models.kinetic" in obj.__module__: + kinetic_models += [name] +if rank == 0: + print(f"\n{kinetic_models = }") + +hybrid_models = [] +for name, obj in inspect.getmembers(hybrid): + if inspect.isclass(obj) and "models.hybrid" in obj.__module__: + hybrid_models += [name] +if rank == 0: + print(f"\n{hybrid_models = }") + + +# folder for test simulations +test_folder = os.path.join(os.getcwd(), "struphy_model_tests") + + +# generic function for calling model tests +def call_test(model_name: str, module: ModuleType = None, verbose=True): + if rank == 0: + print(f"\n*** Testing '{model_name}':") + + # exceptions + if model_name == "TwoFluidQuasiNeutralToy" and MPI.COMM_WORLD.Get_size() > 1: + print(f"WARNING: Model {model_name} cannot be tested for {MPI.COMM_WORLD.Get_size() = }") + return + + if module is None: + submods = [toy, fluid, kinetic, hybrid] + for submod in submods: + try: + model = getattr(submod, model_name)() + except AttributeError: + continue + + else: + model = getattr(module, model_name)() + + assert isinstance(model, StruphyModel) + + # generate paramater file for testing + path = os.path.join(test_folder, f"params_{model_name}.py") + if rank == 0: + model.generate_default_parameter_file(path=path, prompt=False) + del model + MPI.COMM_WORLD.Barrier() + + # set environment options + env = EnvironmentOptions(out_folders=test_folder, sim_folder=f"{model_name}") + + # read parameters + params_in = import_parameters_py(path) + base_units = params_in.base_units + time_opts = params_in.time_opts + domain = params_in.domain + equil = params_in.equil + grid = params_in.grid + derham_opts = params_in.derham_opts + model = params_in.model + + # test + main.run( + model, + params_path=path, + env=env, + base_units=base_units, + time_opts=time_opts, + domain=domain, + equil=equil, + grid=grid, + derham_opts=derham_opts, + verbose=verbose, + ) + + MPI.COMM_WORLD.Barrier() + if rank == 0: + path_out = os.path.join(test_folder, model_name) + main.pproc(path=path_out) + main.load_data(path=path_out) + MPI.COMM_WORLD.Barrier() + + +# specific tests +@pytest.mark.models +@pytest.mark.toy +@pytest.mark.parametrize("model", toy_models) +def test_toy( + model: str, + vrbose: bool, + nclones: int, + show_plots: bool, +): + call_test(model_name=model, module=toy, verbose=vrbose) + + +@pytest.mark.models +@pytest.mark.fluid +@pytest.mark.parametrize("model", fluid_models) +def test_fluid( + model: str, + vrbose: bool, + nclones: int, + show_plots: bool, +): + call_test(model_name=model, module=fluid, verbose=vrbose) + + +@pytest.mark.models +@pytest.mark.kinetic +@pytest.mark.parametrize("model", kinetic_models) +def test_kinetic( + model: str, + vrbose: bool, + nclones: int, + show_plots: bool, +): + call_test(model_name=model, module=kinetic, verbose=vrbose) + + +@pytest.mark.models +@pytest.mark.hybrid +@pytest.mark.parametrize("model", hybrid_models) +def test_hybrid( + model: str, + vrbose: bool, + nclones: int, + show_plots: bool, +): + call_test(model_name=model, module=hybrid, verbose=vrbose) + + +@pytest.mark.single +def test_single_model( + model_name: str, + vrbose: bool, + nclones: int, + show_plots: bool, +): + call_test(model_name=model_name, module=None, verbose=vrbose) + + +if __name__ == "__main__": + test_toy("Maxwell") + test_fluid("LinearMHD") diff --git a/src/struphy/models/tests/test_toy_models.py b/src/struphy/models/tests/test_toy_models.py deleted file mode 100644 index 8b1f03456..000000000 --- a/src/struphy/models/tests/test_toy_models.py +++ /dev/null @@ -1,28 +0,0 @@ -import inspect - -import pytest - -from struphy.models.tests.util import wrapper_for_testing - - -@pytest.mark.parametrize( - "map_and_equil", [("Cuboid", "HomogenSlab"), ("HollowTorus", "AdhocTorus"), ("Tokamak", "EQDSKequilibrium")] -) -def test_toy( - map_and_equil: tuple | list, - fast: bool, - vrbose: bool, - verification: bool, - nclones: int, - show_plots: bool, -): - """Tests models in models/toy.py.""" - wrapper_for_testing( - mtype="toy", - map_and_equil=map_and_equil, - fast=fast, - vrbose=vrbose, - verification=verification, - nclones=nclones, - show_plots=show_plots, - ) diff --git a/src/struphy/models/tests/test_verif_EulerSPH.py b/src/struphy/models/tests/test_verif_EulerSPH.py new file mode 100644 index 000000000..79a248ac9 --- /dev/null +++ b/src/struphy/models/tests/test_verif_EulerSPH.py @@ -0,0 +1,166 @@ +import os + +import cunumpy as xp +import pytest +from matplotlib import pyplot as plt +from matplotlib.ticker import FormatStrFormatter +from psydac.ddm.mpi import mpi as MPI + +from struphy import main +from struphy.fields_background import equils +from struphy.geometry import domains +from struphy.initial import perturbations +from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Time +from struphy.kinetic_background import maxwellians +from struphy.pic.utilities import ( + BinningPlot, + BoundaryParameters, + KernelDensityPlot, + LoadingParameters, + WeightsParameters, +) +from struphy.topology import grids + +test_folder = os.path.join(os.getcwd(), "struphy_verification_tests") + + +@pytest.mark.parametrize("nx", [12, 24]) +@pytest.mark.parametrize("plot_pts", [11, 32]) +def test_soundwave_1d(nx: int, plot_pts: int, do_plot: bool = False): + """Verification test for SPH discretization of isthermal Euler equations. + A standing sound wave with c_s=1 traveserses the domain once. + """ + # import model + from struphy.models.fluid import EulerSPH + + # environment options + out_folders = os.path.join(test_folder, "EulerSPH") + env = EnvironmentOptions(out_folders=out_folders, sim_folder="soundwave_1d") + + # units + base_units = BaseUnits(kBT=1.0) + + # time stepping + time_opts = Time(dt=0.03125, Tend=2.5, split_algo="Strang") + + # geometry + r1 = 2.5 + domain = domains.Cuboid(r1=r1) + + # fluid equilibrium (can be used as part of initial conditions) + equil = None + + # grid + grid = None + + # derham options + derham_opts = None + + # light-weight model instance + model = EulerSPH(with_B0=False) + + # species parameters + model.euler_fluid.set_phys_params() + + loading_params = LoadingParameters(ppb=8, loading="tesselation") + weights_params = WeightsParameters() + boundary_params = BoundaryParameters() + model.euler_fluid.set_markers( + loading_params=loading_params, + weights_params=weights_params, + boundary_params=boundary_params, + ) + model.euler_fluid.set_sorting_boxes( + boxes_per_dim=(nx, 1, 1), + dims_maks=(True, False, False), + ) + + bin_plot = BinningPlot(slice="e1", n_bins=(32,), ranges=(0.0, 1.0)) + kd_plot = KernelDensityPlot(pts_e1=plot_pts, pts_e2=1) + model.euler_fluid.set_save_data( + binning_plots=(bin_plot,), + kernel_density_plots=(kd_plot,), + ) + + # propagator options + from struphy.ode.utils import ButcherTableau + + butcher = ButcherTableau(algo="forward_euler") + model.propagators.push_eta.options = model.propagators.push_eta.Options(butcher=butcher) + if model.with_B0: + model.propagators.push_vxb.options = model.propagators.push_vxb.Options() + model.propagators.push_sph_p.options = model.propagators.push_sph_p.Options(kernel_type="gaussian_1d") + + # background, perturbations and initial conditions + background = equils.ConstantVelocity() + model.euler_fluid.var.add_background(background) + perturbation = perturbations.ModesSin(ls=(1,), amps=(1.0e-2,)) + model.euler_fluid.var.add_perturbation(del_n=perturbation) + + # start run + main.run( + model, + params_path=None, + env=env, + base_units=base_units, + time_opts=time_opts, + domain=domain, + equil=equil, + grid=grid, + derham_opts=derham_opts, + verbose=True, + ) + + # post processing + if MPI.COMM_WORLD.Get_rank() == 0: + main.pproc(env.path_out) + + # diagnostics + simdata = main.load_data(env.path_out) + + ee1, ee2, ee3 = simdata.n_sph["euler_fluid"]["view_0"]["grid_n_sph"] + n_sph = simdata.n_sph["euler_fluid"]["view_0"]["n_sph"] + + if do_plot: + ppb = 8 + dt = time_opts.dt + end_time = time_opts.Tend + Nt = int(end_time // dt) + x = ee1 * r1 + + plt.figure(figsize=(10, 8)) + interval = Nt / 10 + plot_ct = 0 + for i in range(0, Nt + 1): + if i % interval == 0: + print(f"{i = }") + plot_ct += 1 + ax = plt.gca() + + if plot_ct <= 6: + style = "-" + else: + style = "." + plt.plot(x.squeeze(), n_sph[i, :, 0, 0], style, label=f"time={i * dt:4.2f}") + plt.xlim(0, 2.5) + plt.legend() + ax.set_xticks(xp.linspace(0, 2.5, nx + 1)) + ax.xaxis.set_major_formatter(FormatStrFormatter("%.2f")) + plt.grid(c="k") + plt.xlabel("x") + plt.ylabel(r"$\rho$") + + plt.title(f"standing sound wave ($c_s = 1$) for {nx = } and {ppb = }") + if plot_ct == 11: + break + + plt.show() + + error = xp.max(xp.abs(n_sph[0] - n_sph[-1])) + print(f"SPH sound wave {error = }.") + assert error < 6e-4 + print("Assertion passed.") + + +if __name__ == "__main__": + test_soundwave_1d(nx=12, plot_pts=11, do_plot=True) diff --git a/src/struphy/models/tests/test_verif_LinearMHD.py b/src/struphy/models/tests/test_verif_LinearMHD.py new file mode 100644 index 000000000..5cbbbc9fd --- /dev/null +++ b/src/struphy/models/tests/test_verif_LinearMHD.py @@ -0,0 +1,154 @@ +import os + +import cunumpy as xp +import pytest +from psydac.ddm.mpi import mpi as MPI + +from struphy import main +from struphy.diagnostics.diagn_tools import power_spectrum_2d +from struphy.fields_background import equils +from struphy.geometry import domains +from struphy.initial import perturbations +from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Time +from struphy.kinetic_background import maxwellians +from struphy.topology import grids + +test_folder = os.path.join(os.getcwd(), "verification_tests") + + +@pytest.mark.mpi(min_size=3) +@pytest.mark.parametrize("algo", ["implicit", "explicit"]) +def test_slab_waves_1d(algo: str, do_plot: bool = False): + # import model, set verbosity + from struphy.models.fluid import LinearMHD + + verbose = True + + # environment options + out_folders = os.path.join(test_folder, "LinearMHD") + env = EnvironmentOptions(out_folders=out_folders, sim_folder="slab_waves_1d") + + # units + base_units = BaseUnits() + + # time stepping + time_opts = Time(dt=0.15, Tend=180.0) + + # geometry + domain = domains.Cuboid(r3=60.0) + + # fluid equilibrium (can be used as part of initial conditions) + B0x = 0.0 + B0y = 1.0 + B0z = 1.0 + beta = 3.0 + n0 = 0.7 + equil = equils.HomogenSlab(B0x=B0x, B0y=B0y, B0z=B0z, beta=beta, n0=n0) + + # grid + grid = grids.TensorProductGrid(Nel=(1, 1, 64)) + + # derham options + derham_opts = DerhamOptions(p=(1, 1, 3)) + + # light-weight model instance + model = LinearMHD() + + # species parameters + model.mhd.set_phys_params() + + # propagator options + model.propagators.shear_alf.options = model.propagators.shear_alf.Options(algo=algo) + model.propagators.mag_sonic.options = model.propagators.mag_sonic.Options(b_field=model.em_fields.b_field) + + # initial conditions (background + perturbation) + model.mhd.velocity.add_perturbation(perturbations.Noise(amp=0.1, comp=0, seed=123)) + model.mhd.velocity.add_perturbation(perturbations.Noise(amp=0.1, comp=1, seed=123)) + model.mhd.velocity.add_perturbation(perturbations.Noise(amp=0.1, comp=2, seed=123)) + + # start run + main.run( + model, + params_path=None, + env=env, + base_units=base_units, + time_opts=time_opts, + domain=domain, + equil=equil, + grid=grid, + derham_opts=derham_opts, + verbose=verbose, + ) + + # post processing + if MPI.COMM_WORLD.Get_rank() == 0: + main.pproc(env.path_out) + + # diagnostics + if MPI.COMM_WORLD.Get_rank() == 0: + simdata = main.load_data(env.path_out) + + # first fft + u_of_t = simdata.spline_values["mhd"]["velocity_log"] + + Bsquare = B0x**2 + B0y**2 + B0z**2 + p0 = beta * Bsquare / 2 + + disp_params = {"B0x": B0x, "B0y": B0y, "B0z": B0z, "p0": p0, "n0": n0, "gamma": 5 / 3} + + _1, _2, _3, coeffs = power_spectrum_2d( + u_of_t, + "velocity_log", + grids=simdata.grids_log, + grids_mapped=simdata.grids_phy, + component=0, + slice_at=[0, 0, None], + do_plot=do_plot, + disp_name="MHDhomogenSlab", + disp_params=disp_params, + fit_branches=1, + noise_level=0.5, + extr_order=10, + fit_degree=(1,), + ) + + # assert + vA = xp.sqrt(Bsquare / n0) + v_alfven = vA * B0z / xp.sqrt(Bsquare) + print(f"{v_alfven = }") + assert xp.abs(coeffs[0][0] - v_alfven) < 0.07 + + # second fft + p_of_t = simdata.spline_values["mhd"]["pressure_log"] + + _1, _2, _3, coeffs = power_spectrum_2d( + p_of_t, + "pressure_log", + grids=simdata.grids_log, + grids_mapped=simdata.grids_phy, + component=0, + slice_at=[0, 0, None], + do_plot=do_plot, + disp_name="MHDhomogenSlab", + disp_params=disp_params, + fit_branches=2, + noise_level=0.4, + extr_order=10, + fit_degree=(1, 1), + ) + + # assert + gamma = 5 / 3 + cS = xp.sqrt(gamma * p0 / n0) + + delta = (4 * B0z**2 * cS**2 * vA**2) / ((cS**2 + vA**2) ** 2 * Bsquare) + v_slow = xp.sqrt(1 / 2 * (cS**2 + vA**2) * (1 - xp.sqrt(1 - delta))) + v_fast = xp.sqrt(1 / 2 * (cS**2 + vA**2) * (1 + xp.sqrt(1 - delta))) + print(f"{v_slow = }") + print(f"{v_fast = }") + assert xp.abs(coeffs[0][0] - v_slow) < 0.05 + assert xp.abs(coeffs[1][0] - v_fast) < 0.19 + + +if __name__ == "__main__": + test_slab_waves_1d(algo="implicit", do_plot=True) diff --git a/src/struphy/models/tests/test_verif_Maxwell.py b/src/struphy/models/tests/test_verif_Maxwell.py new file mode 100644 index 000000000..e97675e7b --- /dev/null +++ b/src/struphy/models/tests/test_verif_Maxwell.py @@ -0,0 +1,269 @@ +import os + +import cunumpy as xp +import pytest +from matplotlib import pyplot as plt +from psydac.ddm.mpi import mpi as MPI +from scipy.special import jv, yn + +from struphy import main +from struphy.diagnostics.diagn_tools import power_spectrum_2d +from struphy.fields_background import equils +from struphy.geometry import domains +from struphy.initial import perturbations +from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Time +from struphy.kinetic_background import maxwellians +from struphy.models.toy import Maxwell +from struphy.topology import grids + +test_folder = os.path.join(os.getcwd(), "struphy_verification_tests") + + +@pytest.mark.mpi(min_size=3) +@pytest.mark.parametrize("algo", ["implicit", "explicit"]) +def test_light_wave_1d(algo: str, do_plot: bool = False): + # environment options + out_folders = os.path.join(test_folder, "Maxwell") + env = EnvironmentOptions(out_folders=out_folders, sim_folder="light_wave_1d") + + # units + base_units = BaseUnits() + + # time stepping + time_opts = Time(dt=0.05, Tend=50.0) + + # geometry + domain = domains.Cuboid(r3=20.0) + + # fluid equilibrium (can be used as part of initial conditions) + equil = None + + # grid + grid = grids.TensorProductGrid(Nel=(1, 1, 128)) + + # derham options + derham_opts = DerhamOptions(p=(1, 1, 3)) + + # light-weight model instance + model = Maxwell() + + # propagator options + model.propagators.maxwell.options = model.propagators.maxwell.Options(algo=algo) + + # initial conditions (background + perturbation) + model.em_fields.e_field.add_perturbation(perturbations.Noise(amp=0.1, comp=0, seed=123)) + model.em_fields.e_field.add_perturbation(perturbations.Noise(amp=0.1, comp=1, seed=123)) + + # start run + verbose = True + + main.run( + model, + params_path=None, + env=env, + base_units=base_units, + time_opts=time_opts, + domain=domain, + equil=equil, + grid=grid, + derham_opts=derham_opts, + verbose=verbose, + ) + + # post processing + if MPI.COMM_WORLD.Get_rank() == 0: + main.pproc(env.path_out) + + # diagnostics + if MPI.COMM_WORLD.Get_rank() == 0: + simdata = main.load_data(env.path_out) + + # fft + E_of_t = simdata.spline_values["em_fields"]["e_field_log"] + _1, _2, _3, coeffs = power_spectrum_2d( + E_of_t, + "e_field_log", + grids=simdata.grids_log, + grids_mapped=simdata.grids_phy, + component=0, + slice_at=[0, 0, None], + do_plot=do_plot, + disp_name="Maxwell1D", + fit_branches=1, + noise_level=0.5, + extr_order=10, + fit_degree=(1,), + ) + + # assert + c_light_speed = 1.0 + assert xp.abs(coeffs[0][0] - c_light_speed) < 0.02 + + +@pytest.mark.mpi(min_size=4) +def test_coaxial(do_plot: bool = False): + # import model, set verbosity + from struphy.models.toy import Maxwell + + verbose = True + + # environment options + out_folders = os.path.join(test_folder, "Maxwell") + env = EnvironmentOptions(out_folders=out_folders, sim_folder="coaxial") + + # units + base_units = BaseUnits() + + # time + time_opts = Time(dt=0.05, Tend=10.0) + + # geometry + a1 = 2.326744 + a2 = 3.686839 + Lz = 2.0 + domain = domains.HollowCylinder(a1=a1, a2=a2, Lz=Lz) + + # fluid equilibrium (can be used as part of initial conditions) + equil = equils.HomogenSlab() + + # grid + grid = grids.TensorProductGrid(Nel=(32, 64, 1)) + + # derham options + derham_opts = DerhamOptions( + p=(3, 3, 1), + spl_kind=(False, True, True), + dirichlet_bc=((True, True), (False, False), (False, False)), + ) + + # light-weight model instance + model = Maxwell() + + # propagator options + model.propagators.maxwell.options = model.propagators.maxwell.Options(algo="implicit") + + # initial conditions (background + perturbation) + m = 3 + model.em_fields.e_field.add_perturbation(perturbations.CoaxialWaveguideElectric_r(m=m, a1=a1, a2=a2)) + model.em_fields.e_field.add_perturbation(perturbations.CoaxialWaveguideElectric_theta(m=m, a1=a1, a2=a2)) + model.em_fields.b_field.add_perturbation(perturbations.CoaxialWaveguideMagnetic(m=m, a1=a1, a2=a2)) + + # start run + main.run( + model, + params_path=None, + env=env, + base_units=base_units, + time_opts=time_opts, + domain=domain, + equil=equil, + grid=grid, + derham_opts=derham_opts, + verbose=verbose, + ) + + # post processing + if MPI.COMM_WORLD.Get_rank() == 0: + main.pproc(env.path_out, physical=True) + + # diagnostics + if MPI.COMM_WORLD.Get_rank() == 0: + # get parameters + dt = time_opts.dt + split_algo = time_opts.split_algo + Nel = grid.Nel + modes = m + + # load data + simdata = main.load_data(env.path_out) + + t_grid = simdata.t_grid + grids_phy = simdata.grids_phy + e_field_phy = simdata.spline_values["em_fields"]["e_field_phy"] + b_field_phy = simdata.spline_values["em_fields"]["b_field_phy"] + + X = grids_phy[0][:, :, 0] + Y = grids_phy[1][:, :, 0] + + # define analytic solution + def B_z(X, Y, Z, m, t): + """Magnetic field in z direction of coaxial cabel""" + r = (X**2 + Y**2) ** 0.5 + theta = xp.arctan2(Y, X) + return (jv(m, r) - 0.28 * yn(m, r)) * xp.cos(m * theta - t) + + def E_r(X, Y, Z, m, t): + """Electrical field in radial direction of coaxial cabel""" + r = (X**2 + Y**2) ** 0.5 + theta = xp.arctan2(Y, X) + return -m / r * (jv(m, r) - 0.28 * yn(m, r)) * xp.cos(m * theta - t) + + def E_theta(X, Y, Z, m, t): + """Electrical field in azimuthal direction of coaxial cabel""" + r = (X**2 + Y**2) ** 0.5 + theta = xp.arctan2(Y, X) + return ((m / r * jv(m, r) - jv(m + 1, r)) - 0.28 * (m / r * yn(m, r) - yn(m + 1, r))) * xp.sin( + m * theta - t + ) + + def to_E_r(X, Y, E_x, E_y): + r = (X**2 + Y**2) ** 0.5 + theta = xp.arctan2(Y, X) + return xp.cos(theta) * E_x + xp.sin(theta) * E_y + + def to_E_theta(X, Y, E_x, E_y): + r = (X**2 + Y**2) ** 0.5 + theta = xp.arctan2(Y, X) + return -xp.sin(theta) * E_x + xp.cos(theta) * E_y + + # plot + if do_plot: + vmin = E_theta(X, Y, grids_phy[0], modes, 0).min() + vmax = E_theta(X, Y, grids_phy[0], modes, 0).max() + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4)) + plot_exac = ax1.contourf( + X, Y, E_theta(X, Y, grids_phy[0], modes, t_grid[-1]), cmap="plasma", levels=100, vmin=vmin, vmax=vmax + ) + ax2.contourf( + X, + Y, + to_E_theta(X, Y, e_field_phy[t_grid[-1]][0][:, :, 0], e_field_phy[t_grid[-1]][1][:, :, 0]), + cmap="plasma", + levels=100, + vmin=vmin, + vmax=vmax, + ) + fig.colorbar(plot_exac, ax=[ax1, ax2], orientation="vertical", shrink=0.9) + ax1.set_xlabel("Exact") + ax2.set_xlabel("Numerical") + fig.suptitle(f"Exact and Simulated $E_\\theta$ Field {dt=}, {split_algo=}, {Nel=}", fontsize=14) + plt.show() + + # assert + Ex_tend = e_field_phy[t_grid[-1]][0][:, :, 0] + Ey_tend = e_field_phy[t_grid[-1]][1][:, :, 0] + Er_exact = E_r(X, Y, grids_phy[0], modes, t_grid[-1]) + Etheta_exact = E_theta(X, Y, grids_phy[0], modes, t_grid[-1]) + Bz_tend = b_field_phy[t_grid[-1]][2][:, :, 0] + Bz_exact = B_z(X, Y, grids_phy[0], modes, t_grid[-1]) + + error_Er = xp.max(xp.abs((to_E_r(X, Y, Ex_tend, Ey_tend) - Er_exact))) + error_Etheta = xp.max(xp.abs((to_E_theta(X, Y, Ex_tend, Ey_tend) - Etheta_exact))) + error_Bz = xp.max(xp.abs((Bz_tend - Bz_exact))) + + rel_err_Er = error_Er / xp.max(xp.abs(Er_exact)) + rel_err_Etheta = error_Etheta / xp.max(xp.abs(Etheta_exact)) + rel_err_Bz = error_Bz / xp.max(xp.abs(Bz_exact)) + + print("") + assert rel_err_Bz < 0.0021, f"Assertion for magnetic field Maxwell failed: {rel_err_Bz = }" + print(f"Assertion for magnetic field Maxwell passed ({rel_err_Bz = }).") + assert rel_err_Etheta < 0.0021, f"Assertion for electric (E_theta) field Maxwell failed: {rel_err_Etheta = }" + print(f"Assertion for electric field Maxwell passed ({rel_err_Etheta = }).") + assert rel_err_Er < 0.0021, f"Assertion for electric (E_r) field Maxwell failed: {rel_err_Er = }" + print(f"Assertion for electric field Maxwell passed ({rel_err_Er = }).") + + +if __name__ == "__main__": + # test_light_wave_1d(algo="explicit", do_plot=True) + test_coaxial(do_plot=True) diff --git a/src/struphy/models/tests/test_verif_Poisson.py b/src/struphy/models/tests/test_verif_Poisson.py new file mode 100644 index 000000000..5b62d61ab --- /dev/null +++ b/src/struphy/models/tests/test_verif_Poisson.py @@ -0,0 +1,149 @@ +import os + +import cunumpy as xp +from matplotlib import pyplot as plt +from psydac.ddm.mpi import mpi as MPI + +from struphy import main +from struphy.fields_background import equils +from struphy.geometry import domains +from struphy.initial import perturbations +from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Time +from struphy.kinetic_background import maxwellians +from struphy.models.toy import Poisson +from struphy.pic.utilities import ( + BinningPlot, + BoundaryParameters, + KernelDensityPlot, + LoadingParameters, + WeightsParameters, +) +from struphy.topology import grids + +test_folder = os.path.join(os.getcwd(), "struphy_verification_tests") + + +def test_poisson_1d(do_plot=False): + # environment options + out_folders = os.path.join(test_folder, "Poisson") + env = EnvironmentOptions(out_folders=out_folders, sim_folder="time_source_1d") + + # units + base_units = BaseUnits() + + # time stepping + time_opts = Time(dt=0.1, Tend=2.0) + + # geometry + l1 = -5.0 + r1 = 5.0 + l2 = -5.0 + r2 = 5.0 + l3 = -6.0 + r3 = 6.0 + domain = domains.Cuboid( + l1=l1, + r1=r1, + ) # l2=l2, r2=r2, l3=l3, r3=r3) + + # fluid equilibrium (can be used as part of initial conditions) + equil = None + + # grid + grid = grids.TensorProductGrid(Nel=(48, 1, 1)) + + # derham options + derham_opts = DerhamOptions() + + # light-weight model instance + model = Poisson() + + # propagator options + omega = 2 * xp.pi + model.propagators.source.options = model.propagators.source.Options(omega=omega) + model.propagators.poisson.options = model.propagators.poisson.Options(rho=model.em_fields.source) + + # background, perturbations and initial conditions + l = 2 + amp = 1e-1 + pert = perturbations.ModesCos(ls=(l,), amps=(amp,)) + model.em_fields.source.add_perturbation(pert) + + # analytical solution + Lx = r1 - l1 + rhs_exact = lambda e1, e2, e3, t: amp * xp.cos(l * 2 * xp.pi / Lx * e1) * xp.cos(omega * t) + phi_exact = ( + lambda e1, e2, e3, t: amp / (l * 2 * xp.pi / Lx) ** 2 * xp.cos(l * 2 * xp.pi / Lx * e1) * xp.cos(omega * t) + ) + + # start run + verbose = True + + main.run( + model, + params_path=None, + env=env, + base_units=base_units, + time_opts=time_opts, + domain=domain, + equil=equil, + grid=grid, + derham_opts=derham_opts, + verbose=verbose, + ) + + # post processing + if MPI.COMM_WORLD.Get_rank() == 0: + main.pproc(env.path_out) + + # diagnostics + if MPI.COMM_WORLD.Get_rank() == 0: + simdata = main.load_data(env.path_out) + + phi = simdata.spline_values["em_fields"]["phi_log"] + source = simdata.spline_values["em_fields"]["source_log"] + x = simdata.grids_phy[0][:, 0, 0] + y = simdata.grids_phy[1][0, :, 0] + z = simdata.grids_phy[2][0, 0, :] + time = simdata.t_grid + + interval = 2 + c = 0 + if do_plot: + fig = plt.figure(figsize=(12, 40)) + + err = 0.0 + for i, t in enumerate(phi): + phi_h = phi[t][0][:, 0, 0] + phi_e = phi_exact(x, 0, 0, t) + new_err = xp.abs(xp.max(phi_h - phi_e)) / (amp / (l * 2 * xp.pi / Lx) ** 2) + if new_err > err: + err = new_err + + if do_plot and i % interval == 0: + plt.subplot(5, 2, 2 * c + 1) + plt.plot(x, phi_h, label="phi") + plt.plot(x, phi_e, "r--", label="exact") + plt.title(f"phi at {t = }") + plt.ylim(-amp / (l * 2 * xp.pi / Lx) ** 2, amp / (l * 2 * xp.pi / Lx) ** 2) + plt.legend() + + plt.subplot(5, 2, 2 * c + 2) + plt.plot(x, source[t][0][:, 0, 0], label="rhs") + plt.plot(x, rhs_exact(x, 0, 0, t), "r--", label="exact") + plt.title(f"source at {t = }") + plt.ylim(-amp, amp) + plt.legend() + + c += 1 + if c > 4: + break + + plt.show() + print(f"{err = }") + assert err < 0.0057 + + +if __name__ == "__main__": + # test_light_wave_1d(algo="explicit", do_plot=True) + test_poisson_1d(do_plot=False) diff --git a/src/struphy/models/tests/test_verif_VlasovAmpereOneSpecies.py b/src/struphy/models/tests/test_verif_VlasovAmpereOneSpecies.py new file mode 100644 index 000000000..585a9776a --- /dev/null +++ b/src/struphy/models/tests/test_verif_VlasovAmpereOneSpecies.py @@ -0,0 +1,167 @@ +import os + +import cunumpy as xp +import h5py +from matplotlib import pyplot as plt +from matplotlib.ticker import FormatStrFormatter +from psydac.ddm.mpi import mpi as MPI + +from struphy import main +from struphy.fields_background import equils +from struphy.geometry import domains +from struphy.initial import perturbations +from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Time +from struphy.kinetic_background import maxwellians +from struphy.pic.utilities import ( + BinningPlot, + BoundaryParameters, + KernelDensityPlot, + LoadingParameters, + WeightsParameters, +) +from struphy.topology import grids + +test_folder = os.path.join(os.getcwd(), "struphy_verification_tests") + + +def test_weak_Landau(do_plot: bool = False): + """Verification test for weak Landau damping. + The computed damping rate is compared to the analytical rate. + """ + # import model + from struphy.models.kinetic import VlasovAmpereOneSpecies + + # environment options + out_folders = os.path.join(test_folder, "VlasovAmpereOneSpecies") + env = EnvironmentOptions(out_folders=out_folders, sim_folder="weak_Landau") + + # units + base_units = BaseUnits() + + # time stepping + time_opts = Time(dt=0.05, Tend=15) + + # geometry + r1 = 12.56 + domain = domains.Cuboid(r1=r1) + + # fluid equilibrium (can be used as part of initial conditions) + equil = None + + # grid + grid = grids.TensorProductGrid(Nel=(32, 1, 1)) + + # derham options + derham_opts = DerhamOptions(p=(3, 1, 1)) + + # light-weight model instance + model = VlasovAmpereOneSpecies(with_B0=False) + + # species parameters + model.kinetic_ions.set_phys_params(alpha=1.0, epsilon=-1.0) + + ppc = 1000 + loading_params = LoadingParameters(ppc=ppc, seed=1234) + weights_params = WeightsParameters(control_variate=True) + boundary_params = BoundaryParameters() + model.kinetic_ions.set_markers( + loading_params=loading_params, + weights_params=weights_params, + boundary_params=boundary_params, + bufsize=0.4, + ) + model.kinetic_ions.set_sorting_boxes(boxes_per_dim=(16, 1, 1), do_sort=True) + + binplot = BinningPlot(slice="e1_v1", n_bins=(128, 128), ranges=((0.0, 1.0), (-5.0, 5.0))) + model.kinetic_ions.set_save_data(binning_plots=(binplot,)) + + # propagator options + model.propagators.push_eta.options = model.propagators.push_eta.Options() + if model.with_B0: + model.propagators.push_vxb.options = model.propagators.push_vxb.Options() + model.propagators.coupling_va.options = model.propagators.coupling_va.Options() + model.initial_poisson.options = model.initial_poisson.Options(stab_mat="M0") + + # background and initial conditions + background = maxwellians.Maxwellian3D(n=(1.0, None)) + model.kinetic_ions.var.add_background(background) + + # if .add_initial_condition is not called, the background is the initial condition + perturbation = perturbations.ModesCos(ls=(1,), amps=(1e-3,)) + init = maxwellians.Maxwellian3D(n=(1.0, perturbation)) + model.kinetic_ions.var.add_initial_condition(init) + + # start run + main.run( + model, + params_path=None, + env=env, + base_units=base_units, + time_opts=time_opts, + domain=domain, + equil=equil, + grid=grid, + derham_opts=derham_opts, + verbose=False, + ) + + # post processing not needed for scalar data + + # exat solution + gamma = -0.1533 + + def E_exact(t): + eps = 0.001 + k = 0.5 + r = 0.3677 + omega = 1.4156 + phi = 0.5362 + return 16 * eps**2 * r**2 * xp.exp(2 * gamma * t) * 2 * xp.pi * xp.cos(omega * t - phi) ** 2 / 2 + + # get parameters + dt = time_opts.dt + algo = time_opts.split_algo + Nel = grid.Nel + p = derham_opts.p + + # get scalar data + if MPI.COMM_WORLD.Get_rank() == 0: + pa_data = os.path.join(env.path_out, "data") + with h5py.File(os.path.join(pa_data, "data_proc0.hdf5"), "r") as f: + time = f["time"]["value"][()] + E = f["scalar"]["en_E"][()] + logE = xp.log10(E) + + # find where time derivative of E is zero + dEdt = (xp.roll(logE, -1) - xp.roll(logE, 1))[1:-1] / (2.0 * dt) + zeros = dEdt * xp.roll(dEdt, -1) < 0.0 + maxima_inds = xp.logical_and(zeros, dEdt > 0.0) + maxima = logE[1:-1][maxima_inds] + t_maxima = time[1:-1][maxima_inds] + + # plot + if do_plot: + plt.figure(figsize=(18, 12)) + plt.plot(time, logE, label="numerical") + plt.plot(time, xp.log10(E_exact(time)), label="exact") + plt.legend() + plt.title(f"{dt=}, {algo=}, {Nel=}, {p=}, {ppc=}") + plt.xlabel("time [m/c]") + plt.plot(t_maxima[:5], maxima[:5], "r") + plt.plot(t_maxima[:5], maxima[:5], "or", markersize=10) + plt.ylim([-10, -4]) + + plt.show() + + # linear fit + linfit = xp.polyfit(t_maxima[:5], maxima[:5], 1) + gamma_num = linfit[0] + + # assert + rel_error = xp.abs(gamma_num - gamma) / xp.abs(gamma) + assert rel_error < 0.22, f"Assertion for weak Landau damping failed: {gamma_num = } vs. {gamma = }." + print(f"Assertion for weak Landau damping passed ({rel_error = }).") + + +if __name__ == "__main__": + test_weak_Landau(do_plot=True) diff --git a/src/struphy/models/tests/verification.py b/src/struphy/models/tests/verification.py index fefcc0e3c..75b0a1047 100644 --- a/src/struphy/models/tests/verification.py +++ b/src/struphy/models/tests/verification.py @@ -2,6 +2,7 @@ import pickle from pathlib import Path +import cunumpy as xp import h5py import yaml from matplotlib import pyplot as plt @@ -11,7 +12,6 @@ import struphy from struphy.post_processing import pproc_struphy -from struphy.utils.arrays import xp as np def VlasovAmpereOneSpecies_weakLandau( @@ -40,7 +40,7 @@ def E_exact(t): r = 0.3677 omega = 1.4156 phi = 0.5362 - return 2 * eps**2 * np.pi / k**2 * r**2 * np.exp(2 * gamma * t) * np.cos(omega * t - phi) ** 2 + return 2 * eps**2 * xp.pi / k**2 * r**2 * xp.exp(2 * gamma * t) * xp.cos(omega * t - phi) ** 2 # get parameters with open(os.path.join(path_out, "parameters.yml")) as f: @@ -56,24 +56,24 @@ def E_exact(t): with h5py.File(os.path.join(pa_data, "data_proc0.hdf5"), "r") as f: time = f["time"]["value"][()] E = f["scalar"]["en_E"][()] - logE = np.log10(E) + logE = xp.log10(E) # find where time derivative of E is zero - dEdt = (np.roll(logE, -1) - np.roll(logE, 1))[1:-1] / (2.0 * dt) - zeros = dEdt * np.roll(dEdt, -1) < 0.0 - maxima_inds = np.logical_and(zeros, dEdt > 0.0) + dEdt = (xp.roll(logE, -1) - xp.roll(logE, 1))[1:-1] / (2.0 * dt) + zeros = dEdt * xp.roll(dEdt, -1) < 0.0 + maxima_inds = xp.logical_and(zeros, dEdt > 0.0) maxima = logE[1:-1][maxima_inds] t_maxima = time[1:-1][maxima_inds] # linear fit - linfit = np.polyfit(t_maxima[:5], maxima[:5], 1) + linfit = xp.polyfit(t_maxima[:5], maxima[:5], 1) gamma_num = linfit[0] # plot if show_plots and rank == 0: plt.figure(figsize=(18, 12)) plt.plot(time, logE, label="numerical") - plt.plot(time, np.log10(E_exact(time)), label="exact") + plt.plot(time, xp.log10(E_exact(time)), label="exact") plt.legend() plt.title(f"{dt=}, {algo=}, {Nel=}, {p=}, {ppc=}") plt.xlabel("time [m/c]") @@ -84,7 +84,7 @@ def E_exact(t): plt.show() # assert - rel_error = np.abs(gamma_num - gamma) / np.abs(gamma) + rel_error = xp.abs(gamma_num - gamma) / xp.abs(gamma) assert rel_error < 0.25, f"{rank = }: Assertion for weak Landau damping failed: {gamma_num = } vs. {gamma = }." print(f"{rank = }: Assertion for weak Landau damping passed ({rel_error = }).") @@ -115,7 +115,7 @@ def E_exact(t): r = 0.3677 omega = 1.4156 phi = 0.5362 - return 2 * eps**2 * np.pi / k**2 * r**2 * np.exp(2 * gamma * t) * np.cos(omega * t - phi) ** 2 + return 2 * eps**2 * xp.pi / k**2 * r**2 * xp.exp(2 * gamma * t) * xp.cos(omega * t - phi) ** 2 # get parameters with open(os.path.join(path_out, "parameters.yml")) as f: @@ -131,24 +131,24 @@ def E_exact(t): with h5py.File(os.path.join(pa_data, "data_proc0.hdf5"), "r") as f: time = f["time"]["value"][()] E = f["scalar"]["en_E"][()] - logE = np.log10(E) + logE = xp.log10(E) # find where time derivative of E is zero - dEdt = (np.roll(logE, -1) - np.roll(logE, 1))[1:-1] / (2.0 * dt) - zeros = dEdt * np.roll(dEdt, -1) < 0.0 - maxima_inds = np.logical_and(zeros, dEdt > 0.0) + dEdt = (xp.roll(logE, -1) - xp.roll(logE, 1))[1:-1] / (2.0 * dt) + zeros = dEdt * xp.roll(dEdt, -1) < 0.0 + maxima_inds = xp.logical_and(zeros, dEdt > 0.0) maxima = logE[1:-1][maxima_inds] t_maxima = time[1:-1][maxima_inds] # linear fit - linfit = np.polyfit(t_maxima[:5], maxima[:5], 1) + linfit = xp.polyfit(t_maxima[:5], maxima[:5], 1) gamma_num = linfit[0] # plot if show_plots and rank == 0: plt.figure(figsize=(18, 12)) plt.plot(time, logE, label="numerical") - plt.plot(time, np.log10(E_exact(time)), label="exact") + plt.plot(time, xp.log10(E_exact(time)), label="exact") plt.legend() plt.title(f"{dt=}, {algo=}, {Nel=}, {p=}, {ppc=}") plt.xlabel("time [m/c]") @@ -160,7 +160,7 @@ def E_exact(t): # plt.show() # assert - rel_error = np.abs(gamma_num - gamma) / np.abs(gamma) + rel_error = xp.abs(gamma_num - gamma) / xp.abs(gamma) assert rel_error < 0.25, f"{rank = }: Assertion for weak Landau damping failed: {gamma_num = } vs. {gamma = }." print(f"{rank = }: Assertion for weak Landau damping passed ({rel_error = }).") @@ -190,8 +190,8 @@ def IsothermalEulerSPH_soundwave( MPI.COMM_WORLD.Barrier() path_n_sph = os.path.join(path_pp, "kinetic_data/euler_fluid/n_sph/view_0/") - ee1, ee2, ee3 = np.load(os.path.join(path_n_sph, "grid_n_sph.npy")) - n_sph = np.load(os.path.join(path_n_sph, "n_sph.npy")) + ee1, ee2, ee3 = xp.load(os.path.join(path_n_sph, "grid_n_sph.npy")) + n_sph = xp.load(os.path.join(path_n_sph, "n_sph.npy")) # print(f'{ee1.shape = }, {n_sph.shape = }') if show_plots and rank == 0: @@ -218,7 +218,7 @@ def IsothermalEulerSPH_soundwave( plt.plot(x.squeeze(), n_sph[i, :, 0, 0], style, label=f"time={i * dt:4.2f}") plt.xlim(0, 2.5) plt.legend() - ax.set_xticks(np.linspace(0, 2.5, nx + 1)) + ax.set_xticks(xp.linspace(0, 2.5, nx + 1)) ax.xaxis.set_major_formatter(FormatStrFormatter("%.2f")) plt.grid(c="k") plt.xlabel("x") @@ -231,14 +231,13 @@ def IsothermalEulerSPH_soundwave( plt.show() # assert - error = np.max(np.abs(n_sph[0] - n_sph[-1])) + error = xp.max(xp.abs(n_sph[0] - n_sph[-1])) print(f"{rank = }: Assertion for SPH sound wave passed ({error = }).") assert error < 1.3e-3 def Maxwell_coaxial( path_out: str, - rank: int, show_plots: bool = False, ): """Verification test for coaxial cable with Maxwell equations. Comparison w.r.t analytic solution. @@ -251,12 +250,11 @@ def Maxwell_coaxial( path_out : str Simulation output folder (absolute path). - rank : int - MPI rank. - show_plots: bool Whether to show plots.""" + rank = MPI.COMM_WORLD.Get_rank() + if rank == 0: pproc_struphy.main(path_out, physical=True) MPI.COMM_WORLD.Barrier() @@ -264,30 +262,30 @@ def Maxwell_coaxial( def B_z(X, Y, Z, m, t): """Magnetic field in z direction of coaxial cabel""" r = (X**2 + Y**2) ** 0.5 - theta = np.arctan2(Y, X) - return (jv(m, r) - 0.28 * yn(m, r)) * np.cos(m * theta - t) + theta = xp.arctan2(Y, X) + return (jv(m, r) - 0.28 * yn(m, r)) * xp.cos(m * theta - t) def E_r(X, Y, Z, m, t): """Electrical field in radial direction of coaxial cabel""" r = (X**2 + Y**2) ** 0.5 - theta = np.arctan2(Y, X) - return -m / r * (jv(m, r) - 0.28 * yn(m, r)) * np.cos(m * theta - t) + theta = xp.arctan2(Y, X) + return -m / r * (jv(m, r) - 0.28 * yn(m, r)) * xp.cos(m * theta - t) def E_theta(X, Y, Z, m, t): """Electrical field in azimuthal direction of coaxial cabel""" r = (X**2 + Y**2) ** 0.5 - theta = np.arctan2(Y, X) - return ((m / r * jv(m, r) - jv(m + 1, r)) - 0.28 * (m / r * yn(m, r) - yn(m + 1, r))) * np.sin(m * theta - t) + theta = xp.arctan2(Y, X) + return ((m / r * jv(m, r) - jv(m + 1, r)) - 0.28 * (m / r * yn(m, r) - yn(m + 1, r))) * xp.sin(m * theta - t) def to_E_r(X, Y, E_x, E_y): r = (X**2 + Y**2) ** 0.5 - theta = np.arctan2(Y, X) - return np.cos(theta) * E_x + np.sin(theta) * E_y + theta = xp.arctan2(Y, X) + return xp.cos(theta) * E_x + xp.sin(theta) * E_y def to_E_theta(X, Y, E_x, E_y): r = (X**2 + Y**2) ** 0.5 - theta = np.arctan2(Y, X) - return -np.sin(theta) * E_x + np.cos(theta) * E_y + theta = xp.arctan2(Y, X) + return -xp.sin(theta) * E_x + xp.cos(theta) * E_y # get parameters with open(os.path.join(path_out, "parameters.yml")) as f: @@ -299,7 +297,7 @@ def to_E_theta(X, Y, E_x, E_y): pproc_path = os.path.join(path_out, "post_processing/") em_fields_path = os.path.join(pproc_path, "fields_data/em_fields/") - t_grid = np.load(os.path.join(pproc_path, "t_grid.npy")) + t_grid = xp.load(os.path.join(pproc_path, "t_grid.npy")) grids_phy = pickle.loads(Path(os.path.join(pproc_path, "fields_data/grids_phy.bin")).read_bytes()) b_field_phy = pickle.loads(Path(os.path.join(em_fields_path, "b_field_phy.bin")).read_bytes()) e_field_phy = pickle.loads(Path(os.path.join(em_fields_path, "e_field_phy.bin")).read_bytes()) @@ -338,13 +336,13 @@ def to_E_theta(X, Y, E_x, E_y): Bz_tend = b_field_phy[t_grid[-1]][2][:, :, 0] Bz_exact = B_z(X, Y, grids_phy[0], modes, t_grid[-1]) - error_Er = np.max(np.abs((to_E_r(X, Y, Ex_tend, Ey_tend) - Er_exact))) - error_Etheta = np.max(np.abs((to_E_theta(X, Y, Ex_tend, Ey_tend) - Etheta_exact))) - error_Bz = np.max(np.abs((Bz_tend - Bz_exact))) + error_Er = xp.max(xp.abs((to_E_r(X, Y, Ex_tend, Ey_tend) - Er_exact))) + error_Etheta = xp.max(xp.abs((to_E_theta(X, Y, Ex_tend, Ey_tend) - Etheta_exact))) + error_Bz = xp.max(xp.abs((Bz_tend - Bz_exact))) - rel_err_Er = error_Er / np.max(np.abs(Er_exact)) - rel_err_Etheta = error_Etheta / np.max(np.abs(Etheta_exact)) - rel_err_Bz = error_Bz / np.max(np.abs(Bz_exact)) + rel_err_Er = error_Er / xp.max(xp.abs(Er_exact)) + rel_err_Etheta = error_Etheta / xp.max(xp.abs(Etheta_exact)) + rel_err_Bz = error_Bz / xp.max(xp.abs(Bz_exact)) print(f"{rel_err_Er = }") print(f"{rel_err_Etheta = }") diff --git a/src/struphy/models/toy.py b/src/struphy/models/toy.py index e65212d6f..bad2b7916 100644 --- a/src/struphy/models/toy.py +++ b/src/struphy/models/toy.py @@ -1,6 +1,14 @@ +import cunumpy as xp +from psydac.ddm.mpi import mpi as MPI + +from struphy.feec.projectors import L2Projector +from struphy.feec.variational_utilities import InternalEnergyEvaluator from struphy.models.base import StruphyModel +from struphy.models.species import FieldSpecies, FluidSpecies, ParticleSpecies +from struphy.models.variables import FEECVariable, PICVariable, SPHVariable, Variable from struphy.propagators import propagators_coupling, propagators_fields, propagators_markers -from struphy.utils.arrays import xp as np + +rank = MPI.COMM_WORLD.Get_rank() class Maxwell(StruphyModel): @@ -23,62 +31,61 @@ class Maxwell(StruphyModel): :ref:`propagators` (called in sequence): 1. :class:`~struphy.propagators.propagators_fields.Maxwell` - - :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} - - dct["em_fields"]["e_field"] = "Hcurl" - dct["em_fields"]["b_field"] = "Hdiv" - return dct + ## species - @staticmethod - def bulk_species(): - return None + class EMFields(FieldSpecies): + def __init__(self): + self.e_field = FEECVariable(space="Hcurl") + self.b_field = FEECVariable(space="Hdiv") + self.init_variables() - @staticmethod - def velocity_scale(): - return "light" + ## propagators - @staticmethod - def propagators_dct(): - return {propagators_fields.Maxwell: ["e_field", "b_field"]} + class Propagators: + def __init__(self): + self.maxwell = propagators_fields.Maxwell() - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] + ## abstract methods - def __init__(self, params, comm, clone_config=None): - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") - # extract necessary parameters - algo = params["em_fields"]["options"]["Maxwell"]["algo"] - solver = params["em_fields"]["options"]["Maxwell"]["solver"] + # 1. instantiate all species + self.em_fields = self.EMFields() - # set keyword arguments for propagators - self._kwargs[propagators_fields.Maxwell] = { - "algo": algo, - "solver": solver, - } + # 2. instantiate all propagators + self.propagators = self.Propagators() - # Initialize propagators used in splitting substeps - self.init_propagators() + # 3. assign variables to propagators + self.propagators.maxwell.variables.e = self.em_fields.e_field + self.propagators.maxwell.variables.b = self.em_fields.b_field - # Scalar variables to be saved during simulation + # define scalars for update_scalar_quantities self.add_scalar("electric energy") self.add_scalar("magnetic energy") self.add_scalar("total energy") + @property + def bulk_species(self): + return None + + @property + def velocity_scale(self): + return "light" + + def allocate_helpers(self): + pass + def update_scalar_quantities(self): - en_E = 0.5 * self.mass_ops.M1.dot_inner(self.pointer["e_field"], self.pointer["e_field"]) - en_B = 0.5 * self.mass_ops.M2.dot_inner(self.pointer["b_field"], self.pointer["b_field"]) + en_E = 0.5 * self.mass_ops.M1.dot_inner( + self.em_fields.e_field.spline.vector, self.em_fields.e_field.spline.vector + ) + en_B = 0.5 * self.mass_ops.M2.dot_inner( + self.em_fields.b_field.spline.vector, self.em_fields.b_field.spline.vector + ) self.update_scalar("electric energy", en_E) self.update_scalar("magnetic energy", en_B) @@ -104,80 +111,59 @@ class Vlasov(StruphyModel): 1. :class:`~struphy.propagators.propagators_markers.PushVxB` 2. :class:`~struphy.propagators.propagators_markers.PushEta` - - :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} + ## species - dct["kinetic"]["ions"] = "Particles6D" - return dct + class KineticIons(ParticleSpecies): + def __init__(self): + self.var = PICVariable(space="Particles6D") + self.init_variables() - @staticmethod - def bulk_species(): - return "ions" - - @staticmethod - def velocity_scale(): - return "cyclotron" + ## propagators - @staticmethod - def propagators_dct(): - return { - propagators_markers.PushVxB: ["ions"], - propagators_markers.PushEta: ["ions"], - } + class Propagators: + def __init__(self): + self.push_vxb = propagators_markers.PushVxB() + self.push_eta = propagators_markers.PushEta() - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] + ## abstract methods - def __init__(self, params, comm, clone_config=None): - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}' ***") - # prelim - ions_params = self.kinetic["ions"]["params"] + # 1. instantiate all species + self.kinetic_ions = self.KineticIons() - # project magnetic background - self._b_eq = self.derham.P["2"]( - [ - self.equil.b2_1, - self.equil.b2_2, - self.equil.b2_3, - ] - ) + # 2. instantiate all propagators + self.propagators = self.Propagators() - # set keyword arguments for propagators - self._kwargs[propagators_markers.PushVxB] = { - "algo": ions_params["options"]["PushVxB"]["algo"], - "kappa": 1.0, - "b2": self._b_eq, - "b2_add": None, - } + # 3. assign variables to propagators + self.propagators.push_vxb.variables.ions = self.kinetic_ions.var + self.propagators.push_eta.variables.var = self.kinetic_ions.var - self._kwargs[propagators_markers.PushEta] = {"algo": ions_params["options"]["PushEta"]["algo"]} + # define scalars for update_scalar_quantities + self.add_scalar("en_f", compute="from_particles", variable=self.kinetic_ions.var) - # Initialize propagators used in splitting substeps - self.init_propagators() + @property + def bulk_species(self): + return self.kinetic_ions - # Scalar variables to be saved during simulation - self.add_scalar("en_f", compute="from_particles", species="ions") + @property + def velocity_scale(self): + return "cyclotron" - # MPI operations needed for scalar variables - self._tmp = np.empty(1, dtype=float) + def allocate_helpers(self): + self._tmp = xp.empty(1, dtype=float) def update_scalar_quantities(self): - self._tmp[0] = self.pointer["ions"].markers_wo_holes[:, 6].dot( - self.pointer["ions"].markers_wo_holes[:, 3] ** 2 - + self.pointer["ions"].markers_wo_holes[:, 4] ** 2 - + self.pointer["ions"].markers_wo_holes[:, 5] ** 2, - ) / (2 * self.pointer["ions"].Np) + particles = self.kinetic_ions.var.particles + self._tmp[0] = particles.markers_wo_holes[:, 6].dot( + particles.markers_wo_holes[:, 3] ** 2 + + particles.markers_wo_holes[:, 4] ** 2 + + particles.markers_wo_holes[:, 5] ** 2, + ) / (2 * particles.Np) self.update_scalar("en_f", self._tmp[0]) @@ -213,98 +199,85 @@ class GuidingCenter(StruphyModel): 1. :class:`~struphy.propagators.propagators_markers.PushGuidingCenterBxEstar` 2. :class:`~struphy.propagators.propagators_markers.PushGuidingCenterParallel` - - :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} + ## species - dct["kinetic"]["ions"] = "Particles5D" - return dct + class KineticIons(ParticleSpecies): + def __init__(self): + self.var = PICVariable(space="Particles5D") + self.init_variables() - @staticmethod - def bulk_species(): - return "ions" + ## propagators - @staticmethod - def velocity_scale(): - return "alfvén" + class Propagators: + def __init__(self): + self.push_bxe = propagators_markers.PushGuidingCenterBxEstar() + self.push_parallel = propagators_markers.PushGuidingCenterParallel() - @staticmethod - def propagators_dct(): - return { - propagators_markers.PushGuidingCenterBxEstar: ["ions"], - propagators_markers.PushGuidingCenterParallel: ["ions"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - def __init__(self, params, comm, clone_config=None): - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) - - # prelim - ions_params = self.kinetic["ions"]["params"] - epsilon = self.equation_params["ions"]["epsilon"] - - # set keyword arguments for propagators - self._kwargs[propagators_markers.PushGuidingCenterBxEstar] = { - "epsilon": epsilon, - "algo": ions_params["options"]["PushGuidingCenterBxEstar"]["algo"], - } - - self._kwargs[propagators_markers.PushGuidingCenterParallel] = { - "epsilon": epsilon, - "algo": ions_params["options"]["PushGuidingCenterParallel"]["algo"], - } - - # Initialize propagators used in splitting substeps - self.init_propagators() + ## abstract methods - # Scalar variables to be saved during simulation - self.add_scalar("en_fv", compute="from_particles", species="ions") - self.add_scalar("en_fB", compute="from_particles", species="ions") - self.add_scalar("en_tot", compute="from_particles", species="ions") - self.add_scalar("n_lost_particles", compute="from_particles", species="ions") + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}' ***") + + # 1. instantiate all species + self.kinetic_ions = self.KineticIons() + + # 2. instantiate all propagators + self.propagators = self.Propagators() + + # 3. assign variables to propagators + self.propagators.push_bxe.variables.ions = self.kinetic_ions.var + self.propagators.push_parallel.variables.ions = self.kinetic_ions.var + + # define scalars for update_scalar_quantities + self.add_scalar("en_fv", compute="from_particles", variable=self.kinetic_ions.var) + self.add_scalar("en_fB", compute="from_particles", variable=self.kinetic_ions.var) + self.add_scalar("en_tot", compute="from_particles", variable=self.kinetic_ions.var) + + @property + def bulk_species(self): + return self.kinetic_ions + + @property + def velocity_scale(self): + return "alfvén" - # MPI operations needed for scalar variables - self._en_fv = np.empty(1, dtype=float) - self._en_fB = np.empty(1, dtype=float) - self._en_tot = np.empty(1, dtype=float) - self._n_lost_particles = np.empty(1, dtype=float) + def allocate_helpers(self): + self._en_fv = xp.empty(1, dtype=float) + self._en_fB = xp.empty(1, dtype=float) + self._en_tot = xp.empty(1, dtype=float) + self._n_lost_particles = xp.empty(1, dtype=float) def update_scalar_quantities(self): - # particles' kinetic energy + particles = self.kinetic_ions.var.particles - self._en_fv[0] = self.pointer["ions"].markers[~self.pointer["ions"].holes, 5].dot( - self.pointer["ions"].markers[~self.pointer["ions"].holes, 3] ** 2, - ) / (2.0 * self.pointer["ions"].Np) + # particles' kinetic energy + self._en_fv[0] = particles.markers[~particles.holes, 5].dot( + particles.markers[~particles.holes, 3] ** 2, + ) / (2.0 * particles.Np) - self.pointer["ions"].save_magnetic_background_energy() + particles.save_magnetic_background_energy() self._en_tot[0] = ( - self.pointer["ions"] - .markers[~self.pointer["ions"].holes, 5] - .dot( - self.pointer["ions"].markers[~self.pointer["ions"].holes, 8], + particles.markers[~particles.holes, 5].dot( + particles.markers[~particles.holes, 8], ) - / self.pointer["ions"].Np + / particles.Np ) self._en_fB[0] = self._en_tot[0] - self._en_fv[0] - self._n_lost_particles[0] = self.pointer["ions"].n_lost_markers - self.update_scalar("en_fv", self._en_fv[0]) self.update_scalar("en_fB", self._en_fB[0]) self.update_scalar("en_tot", self._en_tot[0]) - self.update_scalar("n_lost_particles", self._n_lost_particles[0]) + + self._n_lost_particles[0] = particles.n_lost_markers + self.derham.comm.Allreduce( + MPI.IN_PLACE, + self._n_lost_particles, + op=MPI.SUM, + ) class ShearAlfven(StruphyModel): @@ -333,43 +306,30 @@ class ShearAlfven(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} - - dct["em_fields"]["b2"] = "Hdiv" - dct["fluid"]["mhd"] = {"u2": "Hdiv"} - return dct - - @staticmethod - def bulk_species(): - return "mhd" - - @staticmethod - def velocity_scale(): - return "alfvén" - - @staticmethod - def propagators_dct(): - return {propagators_fields.ShearAlfven: ["mhd_u2", "b2"]} + ## species + class EMFields(FieldSpecies): + def __init__(self): + self.b_field = FEECVariable(space="Hdiv") + self.init_variables() - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] + class MHD(FluidSpecies): + def __init__(self): + self.velocity = FEECVariable(space="Hdiv") + self.init_variables() - def __init__(self, params, comm, clone_config=None): - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) + class Propagators: + def __init__(self) -> None: + self.shear_alf = propagators_fields.ShearAlfven() - from struphy.polar.basic import PolarVector + @property + def bulk_species(self): + return self.mhd - # extract necessary parameters - alfven_solver = params["fluid"]["mhd"]["options"]["ShearAlfven"]["solver"] - alfven_algo = params["fluid"]["mhd"]["options"]["ShearAlfven"]["algo"] + @property + def velocity_scale(self): + return "alfvén" + def allocate_helpers(self): # project background magnetic field (2-form) and pressure (3-form) self._b_eq = self.derham.P["2"]( [ @@ -379,21 +339,26 @@ def __init__(self, params, comm, clone_config=None): ] ) - # set keyword arguments for propagators - self._kwargs[propagators_fields.ShearAlfven] = { - "u_space": "Hdiv", - "solver": alfven_solver, - "algo": alfven_algo, - } + # temporary vectors for scalar quantities + self._tmp_b1 = self.derham.Vh["2"].zeros() + self._tmp_b2 = self.derham.Vh["2"].zeros() + + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") - # Initialize propagators used in splitting substeps - self.init_propagators() + # 1. instantiate all species + self.em_fields = self.EMFields() + self.mhd = self.MHD() + + # 2. instantiate all propagators + self.propagators = self.Propagators() + + # 3. assign variables to propagators + self.propagators.shear_alf.variables.u = self.mhd.velocity + self.propagators.shear_alf.variables.b = self.em_fields.b_field # Scalar variables to be saved during simulation - # self.add_scalar('en_U') - # self.add_scalar('en_B') - # self.add_scalar('en_B_eq') - # self.add_scalar('en_B_tot') self.add_scalar("en_tot") self.add_scalar("en_U", compute="from_field") @@ -402,14 +367,12 @@ def __init__(self, params, comm, clone_config=None): self.add_scalar("en_B_tot", compute="from_field") self.add_scalar("en_tot2", summands=["en_U", "en_B", "en_B_eq"]) - # temporary vectors for scalar quantities - self._tmp_b1 = self.derham.Vh["2"].zeros() - self._tmp_b2 = self.derham.Vh["2"].zeros() - def update_scalar_quantities(self): # perturbed fields - en_U = 0.5 * self.mass_ops.M2n.dot_inner(self.pointer["mhd_u2"], self.pointer["mhd_u2"]) - en_B = 0.5 * self.mass_ops.M2.dot_inner(self.pointer["b2"], self.pointer["b2"]) + en_U = 0.5 * self.mass_ops.M2n.dot_inner(self.mhd.velocity.spline.vector, self.mhd.velocity.spline.vector) + en_B = 0.5 * self.mass_ops.M2.dot_inner( + self.em_fields.b_field.spline.vector, self.em_fields.b_field.spline.vector + ) self.update_scalar("en_U", en_U) self.update_scalar("en_B", en_B) @@ -422,7 +385,7 @@ def update_scalar_quantities(self): # total magnetic field self._b_eq.copy(out=self._tmp_b1) - self._tmp_b1 += self.pointer["b2"] + self._tmp_b1 += self.em_fields.b_field.spline.vector self.mass_ops.M2.dot(self._tmp_b1, apply_bc=False, out=self._tmp_b2) en_Btot = self._tmp_b1.inner(self._tmp_b2) / 2 @@ -455,77 +418,74 @@ class VariationalPressurelessFluid(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} - dct["fluid"]["fluid"] = {"rho3": "L2", "uv": "H1vec"} - return dct + ## species - @staticmethod - def bulk_species(): - return "fluid" + class Fluid(FluidSpecies): + def __init__(self): + self.density = FEECVariable(space="L2") + self.velocity = FEECVariable(space="H1vec") + self.init_variables() - @staticmethod - def velocity_scale(): - return "alfvén" + ## propagators - @staticmethod - def propagators_dct(): - return { - propagators_fields.VariationalDensityEvolve: ["fluid_rho3", "fluid_uv"], - propagators_fields.VariationalMomentumAdvection: ["fluid_uv"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - def __init__(self, params, comm, clone_config=None): - from struphy.feec.mass import WeightedMassOperator - from struphy.feec.variational_utilities import H1vecMassMatrix_density - - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) - - # Initialize mass matrix - self.WMM = H1vecMassMatrix_density(self.derham, self.mass_ops, self.domain) - - # Initialize propagators/integrators used in splitting substeps - lin_solver_momentum = params["fluid"]["fluid"]["options"]["VariationalMomentumAdvection"]["lin_solver"] - nonlin_solver_momentum = params["fluid"]["fluid"]["options"]["VariationalMomentumAdvection"]["nonlin_solver"] - lin_solver_density = params["fluid"]["fluid"]["options"]["VariationalDensityEvolve"]["lin_solver"] - nonlin_solver_density = params["fluid"]["fluid"]["options"]["VariationalDensityEvolve"]["nonlin_solver"] - - gamma = params["fluid"]["fluid"]["options"]["VariationalDensityEvolve"]["physics"]["gamma"] - - # set keyword arguments for propagators - self._kwargs[propagators_fields.VariationalDensityEvolve] = { - "model": "pressureless", - "gamma": gamma, - "mass_ops": self.WMM, - "lin_solver": lin_solver_density, - "nonlin_solver": nonlin_solver_density, - } - - self._kwargs[propagators_fields.VariationalMomentumAdvection] = { - "mass_ops": self.WMM, - "lin_solver": lin_solver_momentum, - "nonlin_solver": nonlin_solver_momentum, - } - - # Initialize propagators used in splitting substeps - self.init_propagators() + class Propagators: + def __init__(self): + self.variat_dens = propagators_fields.VariationalDensityEvolve() + self.variat_mom = propagators_fields.VariationalMomentumAdvection() - # Scalar variables to be saved during simulation + ## abstract methods + + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + + # 1. instantiate all species + self.fluid = self.Fluid() + + # 2. instantiate all propagators + self.propagators = self.Propagators() + + # 3. assign variables to propagators + self.propagators.variat_dens.variables.rho = self.fluid.density + self.propagators.variat_dens.variables.u = self.fluid.velocity + self.propagators.variat_mom.variables.u = self.fluid.velocity + + # define scalars for update_scalar_quantities self.add_scalar("en_U") + @property + def bulk_species(self): + return self.fluid + + @property + def velocity_scale(self): + return "alfvén" + + def allocate_helpers(self): + pass + def update_scalar_quantities(self): - en_U = 0.5 * self.WMM.massop.dot_inner(self.pointer["fluid_uv"], self.pointer["fluid_uv"]) + u = self.fluid.velocity.spline.vector + en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) + # default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "variat_dens.Options" in line: + new_file += [ + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='pressureless')\n" + ] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) + class VariationalBarotropicFluid(StruphyModel): r"""Barotropic fluid equations discretized with a variational method. @@ -554,84 +514,84 @@ class VariationalBarotropicFluid(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} - dct["fluid"]["fluid"] = {"rho3": "L2", "uv": "H1vec"} - return dct + ## species - @staticmethod - def bulk_species(): - return "fluid" + class Fluid(FluidSpecies): + def __init__(self): + self.density = FEECVariable(space="L2") + self.velocity = FEECVariable(space="H1vec") + self.init_variables() - @staticmethod - def velocity_scale(): - return "alfvén" + ## propagators - @staticmethod - def propagators_dct(): - return { - propagators_fields.VariationalDensityEvolve: ["fluid_rho3", "fluid_uv"], - propagators_fields.VariationalMomentumAdvection: ["fluid_uv"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - def __init__(self, params, comm, clone_config=None): - from struphy.feec.variational_utilities import H1vecMassMatrix_density - - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) - - # Initialize mass matrix - self.WMM = H1vecMassMatrix_density(self.derham, self.mass_ops, self.domain) - - # Initialize propagators/integrators used in splitting substeps - lin_solver_momentum = params["fluid"]["fluid"]["options"]["VariationalMomentumAdvection"]["lin_solver"] - nonlin_solver_momentum = params["fluid"]["fluid"]["options"]["VariationalMomentumAdvection"]["nonlin_solver"] - lin_solver_density = params["fluid"]["fluid"]["options"]["VariationalDensityEvolve"]["lin_solver"] - nonlin_solver_density = params["fluid"]["fluid"]["options"]["VariationalDensityEvolve"]["nonlin_solver"] - - gamma = params["fluid"]["fluid"]["options"]["VariationalDensityEvolve"]["physics"]["gamma"] - - # set keyword arguments for propagators - self._kwargs[propagators_fields.VariationalDensityEvolve] = { - "model": "barotropic", - "gamma": gamma, - "mass_ops": self.WMM, - "lin_solver": lin_solver_density, - "nonlin_solver": nonlin_solver_density, - } - - self._kwargs[propagators_fields.VariationalMomentumAdvection] = { - "mass_ops": self.WMM, - "lin_solver": lin_solver_momentum, - "nonlin_solver": nonlin_solver_momentum, - } - - # Initialize propagators used in splitting substeps - self.init_propagators() + class Propagators: + def __init__(self): + self.variat_dens = propagators_fields.VariationalDensityEvolve() + self.variat_mom = propagators_fields.VariationalMomentumAdvection() - # Scalar variables to be saved during simulation + ## abstract methods + + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + + # 1. instantiate all species + self.fluid = self.Fluid() + + # 2. instantiate all propagators + self.propagators = self.Propagators() + + # 3. assign variables to propagators + self.propagators.variat_dens.variables.rho = self.fluid.density + self.propagators.variat_dens.variables.u = self.fluid.velocity + self.propagators.variat_mom.variables.u = self.fluid.velocity + + # define scalars for update_scalar_quantities self.add_scalar("en_U") self.add_scalar("en_thermo") self.add_scalar("en_tot") + @property + def bulk_species(self): + return self.fluid + + @property + def velocity_scale(self): + return "alfvén" + + def allocate_helpers(self): + pass + def update_scalar_quantities(self): - en_U = 0.5 * self.WMM.massop.dot_inner(self.pointer["fluid_uv"], self.pointer["fluid_uv"]) + rho = self.fluid.density.spline.vector + u = self.fluid.velocity.spline.vector + + en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) - en_thermo = 0.5 * self.mass_ops.M3.dot_inner(self.pointer["fluid_rho3"], self.pointer["fluid_rho3"]) + en_thermo = 0.5 * self.mass_ops.M3.dot_inner(rho, rho) self.update_scalar("en_thermo", en_thermo) en_tot = en_U + en_thermo self.update_scalar("en_tot", en_tot) + # default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "variat_dens.Options" in line: + new_file += [ + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='barotropic')\n" + ] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) + class VariationalCompressibleFluid(StruphyModel): r"""Fully compressible fluid equations discretized with a variational method. @@ -663,106 +623,71 @@ class VariationalCompressibleFluid(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} - dct["fluid"]["fluid"] = {"rho3": "L2", "s3": "L2", "uv": "H1vec"} - return dct + ## species - @staticmethod - def bulk_species(): - return "fluid" + class Fluid(FluidSpecies): + def __init__(self): + self.density = FEECVariable(space="L2") + self.velocity = FEECVariable(space="H1vec") + self.entropy = FEECVariable(space="L2") + self.init_variables() - @staticmethod - def velocity_scale(): - return "alfvén" + ## propagators - @staticmethod - def propagators_dct(): - return { - propagators_fields.VariationalDensityEvolve: ["fluid_rho3", "fluid_uv"], - propagators_fields.VariationalMomentumAdvection: ["fluid_uv"], - propagators_fields.VariationalEntropyEvolve: ["fluid_s3", "fluid_uv"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - def __init__(self, params, comm, clone_config=None): - from struphy.feec.projectors import L2Projector - from struphy.feec.variational_utilities import H1vecMassMatrix_density - - # initialize base class - super().__init__(params, comm=comm, clone_config=clone_config) - - # Initialize mass matrix - self.WMM = H1vecMassMatrix_density(self.derham, self.mass_ops, self.domain) - - # Initialize propagators/integrators used in splitting substeps - lin_solver_momentum = params["fluid"]["fluid"]["options"]["VariationalMomentumAdvection"]["lin_solver"] - nonlin_solver_momentum = params["fluid"]["fluid"]["options"]["VariationalMomentumAdvection"]["nonlin_solver"] - lin_solver_density = params["fluid"]["fluid"]["options"]["VariationalDensityEvolve"]["lin_solver"] - nonlin_solver_density = params["fluid"]["fluid"]["options"]["VariationalDensityEvolve"]["nonlin_solver"] - lin_solver_entropy = params["fluid"]["fluid"]["options"]["VariationalEntropyEvolve"]["lin_solver"] - nonlin_solver_entropy = params["fluid"]["fluid"]["options"]["VariationalEntropyEvolve"]["nonlin_solver"] - - self._gamma = params["fluid"]["fluid"]["options"]["VariationalDensityEvolve"]["physics"]["gamma"] - model = "full" - - from struphy.feec.variational_utilities import InternalEnergyEvaluator - - self._energy_evaluator = InternalEnergyEvaluator(self.derham, self._gamma) - - # set keyword arguments for propagators - self._kwargs[propagators_fields.VariationalDensityEvolve] = { - "model": model, - "s": self.pointer["fluid_s3"], - "gamma": self._gamma, - "mass_ops": self.WMM, - "lin_solver": lin_solver_density, - "nonlin_solver": nonlin_solver_density, - "energy_evaluator": self._energy_evaluator, - } - - self._kwargs[propagators_fields.VariationalMomentumAdvection] = { - "mass_ops": self.WMM, - "lin_solver": lin_solver_momentum, - "nonlin_solver": nonlin_solver_momentum, - } - - self._kwargs[propagators_fields.VariationalEntropyEvolve] = { - "model": model, - "rho": self.pointer["fluid_rho3"], - "gamma": self._gamma, - "mass_ops": self.WMM, - "lin_solver": lin_solver_entropy, - "nonlin_solver": nonlin_solver_entropy, - "energy_evaluator": self._energy_evaluator, - } - - # Initialize propagators used in splitting substeps - self.init_propagators() + class Propagators: + def __init__(self): + self.variat_dens = propagators_fields.VariationalDensityEvolve() + self.variat_mom = propagators_fields.VariationalMomentumAdvection() + self.variat_ent = propagators_fields.VariationalEntropyEvolve() - # Scalar variables to be saved during simulation + ## abstract methods + + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + + # 1. instantiate all species + self.fluid = self.Fluid() + + # 2. instantiate all propagators + self.propagators = self.Propagators() + + # 3. assign variables to propagators + self.propagators.variat_dens.variables.rho = self.fluid.density + self.propagators.variat_dens.variables.u = self.fluid.velocity + self.propagators.variat_mom.variables.u = self.fluid.velocity + self.propagators.variat_ent.variables.s = self.fluid.entropy + self.propagators.variat_ent.variables.u = self.fluid.velocity + + # define scalars for update_scalar_quantities self.add_scalar("en_U") self.add_scalar("en_thermo") self.add_scalar("en_tot") - # temporary vectors for scalar quantities + @property + def bulk_species(self): + return self.fluid + + @property + def velocity_scale(self): + return "alfvén" + + def allocate_helpers(self): projV3 = L2Projector("L2", self._mass_ops) def f(e1, e2, e3): return 1 - f = np.vectorize(f) + f = xp.vectorize(f) self._integrator = projV3(f) + self._energy_evaluator = InternalEnergyEvaluator(self.derham, self.propagators.variat_ent.options.gamma) + def update_scalar_quantities(self): - en_U = 0.5 * self.WMM.massop.dot_inner(self.pointer["fluid_uv"], self.pointer["fluid_uv"]) + rho = self.fluid.density.spline.vector + u = self.fluid.velocity.spline.vector + + en_U = 0.5 * self.mass_ops.WMM.massop.dot_inner(u, u) self.update_scalar("en_U", en_U) en_thermo = self.update_thermo_energy() @@ -770,15 +695,45 @@ def update_scalar_quantities(self): en_tot = en_U + en_thermo self.update_scalar("en_tot", en_tot) + # default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "variat_dens.Options" in line: + new_file += [ + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='full',\n" + ] + new_file += [ + " s=model.fluid.entropy)\n" + ] + elif "variat_ent.Options" in line: + new_file += [ + "model.propagators.variat_ent.options = model.propagators.variat_ent.Options(model='full',\n" + ] + new_file += [ + " rho=model.fluid.density)\n" + ] + elif "entropy.add_background" in line: + new_file += ["model.fluid.density.add_background(FieldsBackground())\n"] + new_file += [line] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) + def update_thermo_energy(self): """Reuse tmp used in VariationalEntropyEvolve to compute the thermodynamical energy. :meta private: """ - en_prop = self._propagators[2] + en_prop = self.propagators.variat_ent - self._energy_evaluator.sf.vector = self.pointer["fluid_s3"] - self._energy_evaluator.rhof.vector = self.pointer["fluid_rho3"] + self._energy_evaluator.sf.vector = self.fluid.entropy.spline.vector + self._energy_evaluator.rhof.vector = self.fluid.density.spline.vector sf_values = self._energy_evaluator.sf.eval_tp_fixed_loc( self._energy_evaluator.integration_grid_spans, self._energy_evaluator.integration_grid_bd, @@ -799,7 +754,7 @@ def update_thermo_energy(self): def __ener(self, rho, s): """Themodynamical energy as a function of rho and s, usign the perfect gaz hypothesis E(rho, s) = rho^gamma*exp(s/rho)""" - return np.power(rho, self._gamma) * np.exp(s / rho) + return xp.power(rho, self.propagators.variat_ent.options.gamma) * xp.exp(s / rho) class Poisson(StruphyModel): @@ -830,65 +785,89 @@ class Poisson(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} + ## species + + class EMFields(FieldSpecies): + def __init__(self): + self.phi = FEECVariable(space="H1") + self.source = FEECVariable(space="H1") + self.init_variables() + + ## propagators + + class Propagators: + def __init__(self): + self.source = propagators_fields.TimeDependentSource() + self.poisson = propagators_fields.Poisson() + + ## abstract methods + + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") - dct["em_fields"]["phi"] = "H1" - dct["em_fields"]["source"] = "H1" - return dct + # 1. instantiate all species + self.em_fields = self.EMFields() - @staticmethod - def bulk_species(): + # 2. instantiate all propagators + self.propagators = self.Propagators() + + # 3. assign variables to propagators + self.propagators.source.variables.source = self.em_fields.source + self.propagators.poisson.variables.phi = self.em_fields.phi + + @property + def bulk_species(self): return None - @staticmethod - def velocity_scale(): + @property + def velocity_scale(self): return None - @staticmethod - def propagators_dct(): - return { - propagators_fields.TimeDependentSource: ["source"], - propagators_fields.ImplicitDiffusion: ["phi"], - } - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - def __init__(self, params, comm, clone_config=None): - super().__init__(params, comm=comm, clone_config=clone_config) - - # extract necessary parameters - model_params = params["em_fields"]["options"]["ImplicitDiffusion"]["model"] - solver_params = params["em_fields"]["options"]["ImplicitDiffusion"]["solver"] - omega = params["em_fields"]["options"]["TimeDependentSource"]["omega"] - hfun = params["em_fields"]["options"]["TimeDependentSource"]["hfun"] - - # set keyword arguments for propagators - self._kwargs[propagators_fields.TimeDependentSource] = { - "omega": omega, - "hfun": hfun, - } - - self._kwargs[propagators_fields.ImplicitDiffusion] = { - "sigma_1": model_params["sigma_1"], - "stab_mat": model_params["stab_mat"], - "diffusion_mat": model_params["diffusion_mat"], - "rho": self.pointer["source"], - "solver": solver_params, - } - - # Initialize propagators used in splitting substeps - self.init_propagators() + def allocate_helpers(self): + pass def update_scalar_quantities(self): pass + def allocate_propagators(self): + """Solve initial Poisson equation. + + :meta private: + """ + + # initialize fields and particles + super().allocate_propagators() + + # # use setter to assign source + # self.propagators.poisson.rho = self.mass_ops.M0.dot(self.em_fields.source.spline.vector) + + # Solve with dt=1. and compute electric field + if MPI.COMM_WORLD.Get_rank() == 0: + print("\nSolving initial Poisson problem...") + + self.propagators.poisson(1.0) + + if MPI.COMM_WORLD.Get_rank() == 0: + print("Done.") + + # default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "poisson.Options" in line: + new_file += [ + "model.propagators.poisson.options = model.propagators.poisson.Options(rho=model.em_fields.source)\n" + ] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) + class DeterministicParticleDiffusion(StruphyModel): r"""Diffusion equation discretized with a deterministic particle method; @@ -916,60 +895,49 @@ class DeterministicParticleDiffusion(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} + ## species - dct["kinetic"]["species1"] = "Particles3D" - return dct + class Hydrogen(ParticleSpecies): + def __init__(self): + self.var = PICVariable(space="Particles3D") + self.init_variables() - @staticmethod - def bulk_species(): - return "species1" + ## propagators - @staticmethod - def velocity_scale(): - return None + class Propagators: + def __init__(self): + self.det_diff = propagators_markers.PushDeterministicDiffusion() - @staticmethod - def propagators_dct(): - return {propagators_markers.PushDeterministicDiffusion: ["species1"]} + ## abstract methods - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") - def __init__(self, params, comm, clone_config=None): - super().__init__(params, comm=comm, clone_config=clone_config) + # 1. instantiate all species + self.hydrogen = self.Hydrogen() - # prelim - params = self.kinetic["species1"]["params"] - algo = params["options"]["PushDeterministicDiffusion"]["algo"] - diffusion_coefficient = params["options"]["PushDeterministicDiffusion"]["diffusion_coefficient"] + # 2. instantiate all propagators + self.propagators = self.Propagators() - # # project magnetic background - # self._b_eq = self.derham.P['2']([self.equil.b2_1, - # self.equil.b2_2, - # self.equil.b2_3]) + # 3. assign variables to propagators + self.propagators.det_diff.variables.var = self.hydrogen.var - # set keyword arguments for propagators - self._kwargs[propagators_markers.PushDeterministicDiffusion] = { - "algo": algo, - "bc_type": params["markers"]["bc"], - "diffusion_coefficient": diffusion_coefficient, - } + # define scalars for update_scalar_quantities + # self.add_scalar("electric energy") + # self.add_scalar("magnetic energy") + # self.add_scalar("total energy") - # Initialize propagators used in splitting substeps - self.init_propagators() + @property + def bulk_species(self): + return self.hydrogen - # Scalar variables to be saved during simulation - self.add_scalar("en_f") + @property + def velocity_scale(self): + return None - # MPI operations needed for scalar variables - self._tmp = np.empty(1, dtype=float) + def allocate_helpers(self): + pass def update_scalar_quantities(self): pass @@ -1000,60 +968,49 @@ class RandomParticleDiffusion(StruphyModel): :ref:`Model info `: """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} + ## species - dct["kinetic"]["species1"] = "Particles3D" - return dct + class Hydrogen(ParticleSpecies): + def __init__(self): + self.var = PICVariable(space="Particles3D") + self.init_variables() - @staticmethod - def bulk_species(): - return "species1" + ## propagators - @staticmethod - def velocity_scale(): - return None + class Propagators: + def __init__(self): + self.rand_diff = propagators_markers.PushRandomDiffusion() - @staticmethod - def propagators_dct(): - return {propagators_markers.PushRandomDiffusion: ["species1"]} + ## abstract methods - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") - def __init__(self, params, comm, clone_config=None): - super().__init__(params, comm=comm, clone_config=clone_config) + # 1. instantiate all species + self.hydrogen = self.Hydrogen() - # prelim - species1_params = self.kinetic["species1"]["params"] - algo = species1_params["options"]["PushRandomDiffusion"]["algo"] - diffusion_coefficient = species1_params["options"]["PushRandomDiffusion"]["diffusion_coefficient"] + # 2. instantiate all propagators + self.propagators = self.Propagators() - # # project magnetic background - # self._b_eq = self.derham.P['2']([self.equil.b2_1, - # self.equil.b2_2, - # self.equil.b2_3]) + # 3. assign variables to propagators + self.propagators.rand_diff.variables.var = self.hydrogen.var - # set keyword arguments for propagators - self._kwargs[propagators_markers.PushRandomDiffusion] = { - "algo": algo, - "bc_type": species1_params["markers"]["bc"], - "diffusion_coefficient": diffusion_coefficient, - } + # define scalars for update_scalar_quantities + # self.add_scalar("electric energy") + # self.add_scalar("magnetic energy") + # self.add_scalar("total energy") - # Initialize propagators used in splitting substeps - self.init_propagators() + @property + def bulk_species(self): + return self.hydrogen - # Scalar variables to be saved during simulation - self.add_scalar("en_f") + @property + def velocity_scale(self): + return None - # MPI operations needed for scalar variables - self._tmp = np.empty(1, dtype=float) + def allocate_helpers(self): + pass def update_scalar_quantities(self): pass @@ -1068,7 +1025,9 @@ class PressureLessSPH(StruphyModel): &\partial_t \rho + \nabla \cdot ( \rho \mathbf u ) = 0 \,, \\[4mm] - &\partial_t (\rho \mathbf u) + \nabla \cdot (\rho \mathbf u \otimes \mathbf u) = 0 \,. + &\partial_t (\rho \mathbf u) + \nabla \cdot (\rho \mathbf u \otimes \mathbf u) = - \nabla \phi_0 \,, + + where :math:`\phi_0` is a static external potential. :ref:`propagators` (called in sequence): @@ -1077,66 +1036,81 @@ class PressureLessSPH(StruphyModel): This is discretized by particles going in straight lines. """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} + ## species - dct["kinetic"]["p_fluid"] = "ParticlesSPH" - return dct + class ColdFluid(ParticleSpecies): + def __init__(self): + self.var = SPHVariable() + self.init_variables() - @staticmethod - def bulk_species(): - return "p_fluid" + ## propagators - @staticmethod - def velocity_scale(): - return None + class Propagators: + def __init__(self): + self.push_eta = propagators_markers.PushEta() + self.push_v = propagators_markers.PushVinEfield() - @staticmethod - def diagnostics_dct(): - dct = {} - dct["projected_density"] = "L2" - return dct + ## abstract methods - @staticmethod - def propagators_dct(): - return {propagators_markers.PushEta: ["p_fluid"]} + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] + # 1. instantiate all species + self.cold_fluid = self.ColdFluid() - def __init__(self, params, comm, clone_config=None): - super().__init__(params, comm=comm, clone_config=clone_config) + # 2. instantiate all propagators + self.propagators = self.Propagators() - # prelim - p_fluid_params = self.kinetic["p_fluid"]["params"] - algo_eta = params["kinetic"]["p_fluid"]["options"]["PushEta"]["algo"] + # 3. assign variables to propagators + self.propagators.push_eta.variables.var = self.cold_fluid.var + self.propagators.push_v.variables.var = self.cold_fluid.var - # set keyword arguments for propagators - self._kwargs[propagators_markers.PushEta] = { - "algo": algo_eta, - "density_field": self.pointer["projected_density"], - } + # define scalars for update_scalar_quantities + self.add_scalar("en_kin", compute="from_particles", variable=self.cold_fluid.var) - # Initialize propagators used in splitting substeps - self.init_propagators() + @property + def bulk_species(self): + return self.cold_fluid - # Scalar variables to be saved during simulation - self.add_scalar("en_kin", compute="from_particles", species="p_fluid") + @property + def velocity_scale(self): + return None + + # @staticmethod + # def diagnostics_dct(): + # dct = {} + # dct["projected_density"] = "L2" + # return dct + + def allocate_helpers(self): + pass def update_scalar_quantities(self): - en_kin = self.pointer["p_fluid"].markers_wo_holes_and_ghost[:, 6].dot( - self.pointer["p_fluid"].markers_wo_holes_and_ghost[:, 3] ** 2 - + self.pointer["p_fluid"].markers_wo_holes_and_ghost[:, 4] ** 2 - + self.pointer["p_fluid"].markers_wo_holes_and_ghost[:, 5] ** 2 - ) / (2.0 * self.pointer["p_fluid"].Np) + particles = self.cold_fluid.var.particles + valid_parts = particles.markers_wo_holes_and_ghost + en_kin = valid_parts[:, 6].dot(valid_parts[:, 3] ** 2 + valid_parts[:, 4] ** 2 + valid_parts[:, 5] ** 2) / ( + 2.0 * particles.Np + ) self.update_scalar("en_kin", en_kin) + ## default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "push_v.Options" in line: + new_file += ["phi = equil.p0\n"] + new_file += ["model.propagators.push_v.options = model.propagators.push_v.Options(phi=phi)\n"] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) + class TwoFluidQuasiNeutralToy(StruphyModel): r"""Linearized, quasi-neutral two-fluid model with zero electron inertia. @@ -1176,115 +1150,75 @@ class TwoFluidQuasiNeutralToy(StruphyModel): in plasma physics, Journal of Computational Physics 2018. """ - @staticmethod - def species(): - dct = {"em_fields": {}, "fluid": {}, "kinetic": {}} - - dct["em_fields"]["potential"] = "L2" - dct["fluid"]["ions"] = { - "u": "Hdiv", - } - dct["fluid"]["electrons"] = { - "u": "Hdiv", - } - return dct - - @staticmethod - def bulk_species(): - return "ions" - - @staticmethod - def velocity_scale(): - return "thermal" + ## species - @staticmethod - def propagators_dct(): - return {propagators_fields.TwoFluidQuasiNeutralFull: ["ions_u", "electrons_u", "potential"]} - - __em_fields__ = species()["em_fields"] - __fluid_species__ = species()["fluid"] - __kinetic_species__ = species()["kinetic"] - __bulk_species__ = bulk_species() - __velocity_scale__ = velocity_scale() - __propagators__ = [prop.__name__ for prop in propagators_dct()] - - # add special options - @classmethod - def options(cls): - dct = super().options() - cls.add_option( - species=["fluid", "electrons"], - option=propagators_fields.TwoFluidQuasiNeutralFull, - dct=dct, - ) - return dct + class EMfields(FieldSpecies): + def __init__(self): + self.phi = FEECVariable(space="L2") + self.init_variables() - def __init__(self, params, comm, clone_config=None): - super().__init__(params, comm=comm, clone_config=clone_config) + class Ions(FluidSpecies): + def __init__(self): + self.u = FEECVariable(space="Hdiv") + self.init_variables() - # get species paramaters - electrons_params = params["fluid"]["electrons"] + class Electrons(FluidSpecies): + def __init__(self): + self.u = FEECVariable(space="Hdiv") + self.init_variables() - # Get coupling strength - if electrons_params["options"]["TwoFluidQuasiNeutralFull"]["override_eq_params"]: - self._epsilon = electrons_params["options"]["TwoFluidQuasiNeutralFull"]["eps_norm"] - print( - f"\n!!! Override equation parameters: {self._epsilon = }.", - ) - else: - self._epsilon = self.equation_params["electrons"]["epsilon"] - - # extract necessary parameters - stokes_solver = params["fluid"]["electrons"]["options"]["TwoFluidQuasiNeutralFull"]["solver"] - stokes_nu = params["fluid"]["electrons"]["options"]["TwoFluidQuasiNeutralFull"]["nu"] - stokes_nu_e = params["fluid"]["electrons"]["options"]["TwoFluidQuasiNeutralFull"]["nu_e"] - stokes_a = params["fluid"]["electrons"]["options"]["TwoFluidQuasiNeutralFull"]["a"] - stokes_R0 = params["fluid"]["electrons"]["options"]["TwoFluidQuasiNeutralFull"]["R0"] - stokes_B0 = params["fluid"]["electrons"]["options"]["TwoFluidQuasiNeutralFull"]["B0"] - stokes_Bp = params["fluid"]["electrons"]["options"]["TwoFluidQuasiNeutralFull"]["Bp"] - stokes_alpha = params["fluid"]["electrons"]["options"]["TwoFluidQuasiNeutralFull"]["alpha"] - stokes_beta = params["fluid"]["electrons"]["options"]["TwoFluidQuasiNeutralFull"]["beta"] - stokes_sigma = params["fluid"]["electrons"]["options"]["TwoFluidQuasiNeutralFull"]["stab_sigma"] - stokes_variant = params["fluid"]["electrons"]["options"]["TwoFluidQuasiNeutralFull"]["variant"] - stokes_method_to_solve = params["fluid"]["electrons"]["options"]["TwoFluidQuasiNeutralFull"]["method_to_solve"] - stokes_preconditioner = params["fluid"]["electrons"]["options"]["TwoFluidQuasiNeutralFull"]["preconditioner"] - stokes_spectralanalysis = params["fluid"]["electrons"]["options"]["TwoFluidQuasiNeutralFull"][ - "spectralanalysis" - ] - stokes_lifting = params["fluid"]["electrons"]["options"]["TwoFluidQuasiNeutralFull"]["lifting"] - stokes_dimension = params["fluid"]["electrons"]["options"]["TwoFluidQuasiNeutralFull"]["dimension"] - stokes_1D_dt = params["time"]["dt"] - - # Check MPI size to ensure only one MPI process - if comm is not None and stokes_variant == "Uzawa": - if comm.Get_rank() == 0: - print(f"Error: TwoFluidQuasiNeutralToy only runs with one MPI process.") - return # Early return to stop execution for multiple MPI processes - - # set keyword arguments for propagators - self._kwargs[propagators_fields.TwoFluidQuasiNeutralFull] = { - "solver": stokes_solver, - "nu": stokes_nu, - "nu_e": stokes_nu_e, - "eps_norm": self._epsilon, - "a": stokes_a, - "R0": stokes_R0, - "B0": stokes_B0, - "Bp": stokes_Bp, - "alpha": stokes_alpha, - "beta": stokes_beta, - "stab_sigma": stokes_sigma, - "variant": stokes_variant, - "method_to_solve": stokes_method_to_solve, - "preconditioner": stokes_preconditioner, - "spectralanalysis": stokes_spectralanalysis, - "dimension": stokes_dimension, - "D1_dt": stokes_1D_dt, - "lifting": stokes_lifting, - } - - # Initialize propagators used in splitting substeps - self.init_propagators() + ## propagators + + class Propagators: + def __init__(self): + self.qn_full = propagators_fields.TwoFluidQuasiNeutralFull() + + ## abstract methods + + def __init__(self): + if rank == 0: + print(f"\n*** Creating light-weight instance of model '{self.__class__.__name__}':") + + # 1. instantiate all species + self.em_fields = self.EMfields() + self.ions = self.Ions() + self.electrons = self.Electrons() + + # 2. instantiate all propagators + self.propagators = self.Propagators() + + # 3. assign variables to propagators + self.propagators.qn_full.variables.u = self.ions.u + self.propagators.qn_full.variables.ue = self.electrons.u + self.propagators.qn_full.variables.phi = self.em_fields.phi + + # define scalars for update_scalar_quantities + + @property + def bulk_species(self): + return self.ions + + @property + def velocity_scale(self): + return "thermal" + + def allocate_helpers(self): + pass def update_scalar_quantities(self): pass + + ## default parameters + def generate_default_parameter_file(self, path=None, prompt=True): + params_path = super().generate_default_parameter_file(path=path, prompt=prompt) + new_file = [] + with open(params_path, "r") as f: + for line in f: + if "BaseUnits()" in line: + new_file += ["base_units = BaseUnits(kBT=1.0)\n"] + else: + new_file += [line] + + with open(params_path, "w") as f: + for line in new_file: + f.write(line) diff --git a/src/struphy/models/variables.py b/src/struphy/models/variables.py new file mode 100644 index 000000000..d71f4644d --- /dev/null +++ b/src/struphy/models/variables.py @@ -0,0 +1,415 @@ +# for type checking (cyclic imports) +from __future__ import annotations + +from abc import ABCMeta, abstractmethod +from typing import TYPE_CHECKING + +import cunumpy as xp +from psydac.ddm.mpi import mpi as MPI + +from struphy.feec.psydac_derham import Derham, SplineFunction +from struphy.fields_background.base import FluidEquilibrium +from struphy.fields_background.projected_equils import ProjectedFluidEquilibrium +from struphy.geometry.base import Domain +from struphy.initial.perturbations import Perturbation +from struphy.io.options import ( + FieldsBackground, + OptsFEECSpace, + OptsPICSpace, + check_option, +) +from struphy.kinetic_background.base import KineticBackground +from struphy.pic import particles +from struphy.pic.base import Particles +from struphy.pic.particles import ParticlesSPH +from struphy.utils.clone_config import CloneConfig + +if TYPE_CHECKING: + from struphy.models.species import FieldSpecies, FluidSpecies, ParticleSpecies, Species + + +class Variable(metaclass=ABCMeta): + """Single variable (unknown) of a Species.""" + + @abstractmethod + def allocate(self): + """Alocate object and memory for variable.""" + + @property + def backgrounds(self): + if not hasattr(self, "_backgrounds"): + self._backgrounds = None + return self._backgrounds + + @property + def perturbations(self): + if not hasattr(self, "_perturbations"): + self._perturbations = None + return self._perturbations + + @property + def save_data(self): + """Store variable data during simulation (default=True).""" + if not hasattr(self, "_save_data"): + self._save_data = True + return self._save_data + + @save_data.setter + def save_data(self, new): + assert isinstance(new, bool) + self._save_data = new + + @property + def species(self) -> Species: + if not hasattr(self, "_species"): + self._species = None + return self._species + + @property + def __name__(self): + if not hasattr(self, "_name"): + self._name = None + return self._name + + def add_background(self, background, verbose=True): + """Type inference of added background done in sub class.""" + if not hasattr(self, "_backgrounds") or self.backgrounds is None: + self._backgrounds = background + else: + if not isinstance(self.backgrounds, list): + self._backgrounds = [self.backgrounds] + self._backgrounds += [background] + + if verbose and MPI.COMM_WORLD.Get_rank() == 0: + print( + f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - added background '{background.__class__.__name__}' with:" + ) + for k, v in background.__dict__.items(): + print(f" {k}: {v}") + + +class FEECVariable(Variable): + def __init__(self, space: OptsFEECSpace = "H1"): + check_option(space, OptsFEECSpace) + self._space = space + + @property + def space(self): + return self._space + + @property + def spline(self) -> SplineFunction: + return self._spline + + @property + def species(self) -> FieldSpecies | FluidSpecies: + if not hasattr(self, "_species"): + self._species = None + return self._species + + def add_background(self, background: FieldsBackground, verbose=True): + super().add_background(background, verbose=verbose) + + def add_perturbation(self, perturbation: Perturbation, verbose=True): + if not hasattr(self, "_perturbations") or self.perturbations is None: + self._perturbations = perturbation + else: + if not isinstance(self.perturbations, list): + self._perturbations = [self.perturbations] + self._perturbations += [perturbation] + + if verbose and MPI.COMM_WORLD.Get_rank() == 0: + print( + f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - added perturbation '{perturbation.__class__.__name__}' with:" + ) + for k, v in perturbation.__dict__.items(): + print(f" {k}: {v}") + + def allocate( + self, + derham: Derham, + domain: Domain = None, + equil: FluidEquilibrium = None, + ): + self._spline = derham.create_spline_function( + name=self.__name__, + space_id=self.space, + backgrounds=self.backgrounds, + perturbations=self.perturbations, + domain=domain, + equil=equil, + ) + + +class PICVariable(Variable): + def __init__(self, space: OptsPICSpace = "Particles6D"): + check_option(space, OptsPICSpace) + self._space = space + + @property + def space(self): + return self._space + + @property + def particles(self) -> Particles: + return self._particles + + @property + def species(self) -> ParticleSpecies: + if not hasattr(self, "_species"): + self._species = None + return self._species + + @property + def n_as_volume_form(self) -> bool: + """Whether the number density n is given as a volume form or scalar function (=default).""" + if not hasattr(self, "_n_as_volume_form"): + self._n_as_volume_form = False + return self._n_as_volume_form + + def add_background(self, background: KineticBackground, n_as_volume_form: bool = False, verbose=True): + self._n_as_volume_form = n_as_volume_form + super().add_background(background, verbose=verbose) + + def add_initial_condition(self, init: KineticBackground, verbose=True): + self._initial_condition = init + if verbose and MPI.COMM_WORLD.Get_rank() == 0: + print( + f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - added initial condition '{init.__class__.__name__}' with:" + ) + for k, v in init.__dict__.items(): + print(f" {k}: {v}") + + @property + def initial_condition(self) -> KineticBackground: + if not hasattr(self, "_initial_condition"): + self._initial_condition = self.backgrounds + return self._initial_condition + + def allocate( + self, + clone_config: CloneConfig = None, + derham: Derham = None, + domain: Domain = None, + equil: FluidEquilibrium = None, + projected_equil: ProjectedFluidEquilibrium = None, + verbose: bool = False, + ): + # assert isinstance(self.species, KineticSpecies) + assert isinstance(self.backgrounds, KineticBackground), ( + f"List input not allowed, you can sum Kineticbackgrounds before passing them to add_background." + ) + + if derham is None: + domain_decomp = None + else: + domain_array = derham.domain_array + nprocs = derham.domain_decomposition.nprocs + domain_decomp = (domain_array, nprocs) + + kinetic_class = getattr(particles, self.space) + + comm_world = MPI.COMM_WORLD + if comm_world.Get_size() == 1: + comm_world = None + + self._particles: Particles = kinetic_class( + comm_world=comm_world, + clone_config=clone_config, + domain_decomp=domain_decomp, + mpi_dims_mask=self.species.dims_mask, + boxes_per_dim=self.species.boxes_per_dim, + box_bufsize=self.species.box_bufsize, + name=self.species.__class__.__name__, + loading_params=self.species.loading_params, + weights_params=self.species.weights_params, + boundary_params=self.species.boundary_params, + bufsize=self.species.bufsize, + domain=domain, + equil=equil, + projected_equil=projected_equil, + background=self.backgrounds, + initial_condition=self.initial_condition, + n_as_volume_form=self.n_as_volume_form, + # perturbations=self.perturbations, + equation_params=self.species.equation_params, + verbose=verbose, + ) + + if self.species.do_sort: + sort = True + else: + sort = False + self.particles.draw_markers(sort=sort, verbose=verbose) + self.particles.initialize_weights() + + # allocate array for saving markers if not present + n_markers = self.species.n_markers + if isinstance(n_markers, float): + if n_markers > 1.0: + self._n_to_save = int(n_markers) + else: + self._n_to_save = int(self.particles.n_mks_global * n_markers) + else: + self._n_to_save = n_markers + + assert self._n_to_save <= self.particles.Np, ( + f"The number of markers for which data should be stored (={self._n_to_save}) murst be <= than the total number of markers (={obj.Np})" + ) + if self._n_to_save > 0: + self._saved_markers = xp.zeros( + (self._n_to_save, self.particles.markers.shape[1]), + dtype=float, + ) + + # other data (wave-particle power exchange, etc.) + # TODO + + @property + def n_to_save(self) -> int: + return self._n_to_save + + @property + def saved_markers(self) -> xp.ndarray: + return self._saved_markers + + +class SPHVariable(Variable): + def __init__(self): + self._space = "ParticlesSPH" + self._n_as_volume_form = True + self._particle_data = {} + + @property + def space(self): + return self._space + + @property + def particles(self) -> ParticlesSPH: + return self._particles + + @property + def particle_data(self): + return self._particle_data + + @property + def species(self) -> ParticleSpecies: + if not hasattr(self, "_species"): + self._species = None + return self._species + + @property + def n_as_volume_form(self) -> bool: + """Whether the number density n is given as a volume form or scalar function (=default).""" + return self._n_as_volume_form + + def add_background(self, background: FluidEquilibrium, verbose=True): + super().add_background(background, verbose=verbose) + + def add_perturbation( + self, + del_n: Perturbation = None, + del_u1: Perturbation = None, + del_u2: Perturbation = None, + del_u3: Perturbation = None, + verbose=True, + ): + self._perturbations = {} + self._perturbations["n"] = del_n + self._perturbations["u1"] = del_u1 + self._perturbations["u2"] = del_u2 + self._perturbations["u3"] = del_u3 + + if verbose and MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - added perturbation:") + for k, v in self._perturbations.items(): + print(f" {k}: {v}") + + @property + def perturbations(self) -> dict[str, Perturbation]: + if not hasattr(self, "_perturbations"): + self._perturbations = None + return self._perturbations + + def allocate( + self, + derham: Derham = None, + domain: Domain = None, + equil: FluidEquilibrium = None, + projected_equil: ProjectedFluidEquilibrium = None, + verbose: bool = False, + ): + assert isinstance(self.backgrounds, FluidEquilibrium), ( + f"List input not allowed, you can sum Kineticbackgrounds before passing them to add_background." + ) + + self.backgrounds.domain = domain + + if derham is None: + domain_decomp = None + else: + domain_array = derham.domain_array + nprocs = derham.domain_decomposition.nprocs + domain_decomp = (domain_array, nprocs) + + comm_world = MPI.COMM_WORLD + if comm_world.Get_size() == 1: + comm_world = None + + self._particles = ParticlesSPH( + comm_world=comm_world, + domain_decomp=domain_decomp, + mpi_dims_mask=self.species.dims_mask, + boxes_per_dim=self.species.boxes_per_dim, + box_bufsize=self.species.box_bufsize, + name=self.species.__class__.__name__, + loading_params=self.species.loading_params, + weights_params=self.species.weights_params, + boundary_params=self.species.boundary_params, + bufsize=self.species.bufsize, + domain=domain, + equil=equil, + projected_equil=projected_equil, + background=self.backgrounds, + n_as_volume_form=self.n_as_volume_form, + perturbations=self.perturbations, + equation_params=self.species.equation_params, + verbose=verbose, + ) + + if self.species.do_sort: + sort = True + else: + sort = False + self.particles.draw_markers(sort=sort, verbose=verbose) + self.particles.initialize_weights() + + # allocate array for saving markers if not present + n_markers = self.species.n_markers + if isinstance(n_markers, float): + if n_markers > 1.0: + self._n_to_save = int(n_markers) + else: + self._n_to_save = int(self.particles.n_mks_global * n_markers) + else: + self._n_to_save = n_markers + + assert self._n_to_save <= self.particles.Np, ( + f"The number of markers for which data should be stored (={self._n_to_save}) murst be <= than the total number of markers (={obj.Np})" + ) + if self._n_to_save > 0: + self._saved_markers = xp.zeros( + (self._n_to_save, self.particles.markers.shape[1]), + dtype=float, + ) + + # other data (wave-particle power exchange, etc.) + # TODO + + @property + def n_to_save(self) -> int: + return self._n_to_save + + @property + def saved_markers(self) -> xp.ndarray: + return self._saved_markers diff --git a/src/struphy/ode/solvers.py b/src/struphy/ode/solvers.py index 414afd8e0..c6d6366b9 100644 --- a/src/struphy/ode/solvers.py +++ b/src/struphy/ode/solvers.py @@ -1,10 +1,10 @@ from inspect import signature +import cunumpy as xp from psydac.linalg.block import BlockVector from psydac.linalg.stencil import StencilVector from struphy.ode.utils import ButcherTableau -from struphy.utils.arrays import xp as np class ODEsolverFEEC: @@ -31,10 +31,10 @@ class ODEsolverFEEC: def __init__( self, vector_field: dict, - algo: str = "rk4", + butcher: ButcherTableau = ButcherTableau(), ): # get algorithm - self._butcher = ButcherTableau(algo=algo) + self._butcher = butcher # check arguments and allocate k for each stage self._k = {} @@ -51,7 +51,6 @@ def __init__( self._k[vec] += [vec.space.zeros()] self._vector_field = vector_field - self._algo = algo # collect unknows in list self._y = list(self.vector_field.keys()) @@ -96,11 +95,6 @@ def vector_field(self): values are callables representing the respective component of the vector field.""" return self._vector_field - @property - def algo(self): - """See :class:`~struphy.ode.utils.ButcherTableau` for available algorithms.""" - return self._algo - @property def y(self): """List of variables to be updated.""" diff --git a/src/struphy/ode/tests/test_ode_feec.py b/src/struphy/ode/tests/test_ode_feec.py index 4c3db40ae..dadd3aad3 100644 --- a/src/struphy/ode/tests/test_ode_feec.py +++ b/src/struphy/ode/tests/test_ode_feec.py @@ -1,6 +1,8 @@ +from typing import get_args + import pytest -from struphy.ode.utils import ButcherTableau +from struphy.ode.utils import OptsButcher @pytest.mark.parametrize( @@ -13,11 +15,12 @@ ("1", "0", "2"), ], ) -@pytest.mark.parametrize("algo", ButcherTableau.available_methods()) +@pytest.mark.parametrize("algo", get_args(OptsButcher)) def test_exp_growth(spaces, algo, show_plots=False): """Solve dy/dt = omega*y for different feec variables y and with all available solvers from the ButcherTableau.""" + import cunumpy as xp from matplotlib import pyplot as plt from psydac.ddm.mpi import mpi as MPI from psydac.linalg.block import BlockVector @@ -25,7 +28,7 @@ def test_exp_growth(spaces, algo, show_plots=False): from struphy.feec.psydac_derham import Derham from struphy.ode.solvers import ODEsolverFEEC - from struphy.utils.arrays import xp as np + from struphy.ode.utils import ButcherTableau comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -37,7 +40,7 @@ def test_exp_growth(spaces, algo, show_plots=False): c0 = 1.2 omega = 2.3 - y_exact = lambda t: c0 * np.exp(omega * t) + y_exact = lambda t: c0 * xp.exp(omega * t) vector_field = {} for i, space in enumerate(spaces): @@ -98,9 +101,10 @@ def f(t, y1, y2, y3, out=out): vector_field[var] = f print(f"{vector_field = }") - print(f"{algo = }") + butcher = ButcherTableau(algo=algo) + print(f"{butcher = }") - solver = ODEsolverFEEC(vector_field, algo=algo) + solver = ODEsolverFEEC(vector_field, butcher=butcher) hs = [0.1] n_hs = 6 @@ -113,7 +117,7 @@ def f(t, y1, y2, y3, out=out): errors = {} for i, h in enumerate(hs): errors[h] = {} - time = np.linspace(0, Tend, int(Tend / h) + 1) + time = xp.linspace(0, Tend, int(Tend / h) + 1) print(f"{h = }, {time.size = }") yvec = y_exact(time) ymax = {} @@ -125,16 +129,16 @@ def f(t, y1, y2, y3, out=out): for b in var.blocks: b[:] = c0 var.update_ghost_regions() - ymax[var] = c0 * np.ones_like(time) + ymax[var] = c0 * xp.ones_like(time) for n in range(time.size - 1): tn = h * n solver(tn, h) for var in vector_field: - ymax[var][n + 1] = np.max(var.toarray()) + ymax[var][n + 1] = xp.max(var.toarray()) # checks for var in vector_field: - errors[h][var] = h * np.sum(np.abs(yvec - ymax[var])) / (h * np.sum(np.abs(yvec))) + errors[h][var] = h * xp.sum(xp.abs(yvec - ymax[var])) / (h * xp.sum(xp.abs(yvec))) print(f"{errors[h][var] = }") assert errors[h][var] < 0.31 @@ -157,9 +161,9 @@ def f(t, y1, y2, y3, out=out): h_vec += [h] err_vec += [dct[var]] - m, _ = np.polyfit(np.log(h_vec), np.log(err_vec), deg=1) + m, _ = xp.polyfit(xp.log(h_vec), xp.log(err_vec), deg=1) print(f"{spaces[j]}-space, fitted convergence rate = {m} for {algo = } with {solver.butcher.conv_rate = }") - assert np.abs(m - solver.butcher.conv_rate) < 0.1 + assert xp.abs(m - solver.butcher.conv_rate) < 0.1 print(f"Convergence check passed on {rank = }.") if rank == 0: diff --git a/src/struphy/ode/utils.py b/src/struphy/ode/utils.py index bdabf4406..6748d07f1 100644 --- a/src/struphy/ode/utils.py +++ b/src/struphy/ode/utils.py @@ -1,6 +1,19 @@ -from struphy.utils.arrays import xp as np +from dataclasses import dataclass +from typing import Literal, get_args +import cunumpy as xp +OptsButcher = Literal[ + "rk4", + "forward_euler", + "heun2", + "rk2", + "heun3", + "3/8 rule", +] + + +@dataclass class ButcherTableau: r""" Butcher tableau for explicit s-stage Runge-Kutta methods. @@ -13,72 +26,62 @@ class ButcherTableau: Parameters ---------- - algo : str + algo : OptsButcher Name of the RK method. """ - @staticmethod - def available_methods(): - meth_avail = [ - "rk4", - "forward_euler", - "heun2", - "rk2", - "heun3", - "3/8 rule", - ] - return meth_avail - - def __init__(self, algo: str = "rk4"): + algo: OptsButcher = "rk4" + + def __post_init__(self): # choose algorithm - if algo == "forward_euler": + if self.algo == "forward_euler": a = () b = (1.0,) c = (0.0,) conv_rate = 1 - elif algo == "heun2": + elif self.algo == "heun2": a = ((1.0,),) b = (1 / 2, 1 / 2) c = (0.0, 1.0) conv_rate = 2 - elif algo == "rk2": + elif self.algo == "rk2": a = ((1 / 2,),) b = (0.0, 1.0) c = (0.0, 1 / 2) conv_rate = 2 - elif algo == "heun3": + elif self.algo == "heun3": a = ((1 / 3,), (0.0, 2 / 3)) b = (1 / 4, 0.0, 3 / 4) c = (0.0, 1 / 3, 2 / 3) conv_rate = 3 - elif algo == "rk4": + elif self.algo == "rk4": a = ((1 / 2,), (0.0, 1 / 2), (0.0, 0.0, 1.0)) b = (1 / 6, 1 / 3, 1 / 3, 1 / 6) c = (0.0, 1 / 2, 1 / 2, 1.0) conv_rate = 4 - elif algo == "3/8 rule": + elif self.algo == "3/8 rule": a = ((1 / 3,), (-1 / 3, 1.0), (1.0, -1.0, 1.0)) b = (1 / 8, 3 / 8, 3 / 8, 1 / 8) c = (0.0, 1 / 3, 2 / 3, 1.0) conv_rate = 4 else: - raise NotImplementedError("Chosen algorithm is not implemented.") + raise NotImplementedError(f"Chosen algorithm {self.algo} is not implemented.") - self._b = np.array(b) - self._c = np.array(c) + self._b = xp.array(b) + self._c = xp.array(c) assert self._b.size == self._c.size self._n_stages = self._b.size assert len(a) == self.n_stages - 1 - self._a = np.tri(self.n_stages, k=-1) + self._a = xp.tri(self.n_stages, k=-1) for l, st in enumerate(a): assert len(st) == l + 1 self._a[l + 1, : l + 1] = st self._conv_rate = conv_rate - __available_methods__ = available_methods() + __available_methods__ = get_args(OptsButcher) @property def a(self): diff --git a/src/struphy/physics/__init__.py b/src/struphy/physics/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/struphy/physics/physics.py b/src/struphy/physics/physics.py new file mode 100644 index 000000000..898e9a45e --- /dev/null +++ b/src/struphy/physics/physics.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass + + +@dataclass +class ConstantsOfNature: + e = 1.602176634e-19 # elementary charge (C) + mH = 1.67262192369e-27 # proton mass (kg) + mu0 = 1.25663706212e-6 # magnetic constant (N/A^2) + eps0 = 8.8541878128e-12 # vacuum permittivity (F/m) + kB = 1.380649e-23 # Boltzmann constant (J/K) + c = 299792458 # speed of light (m/s) diff --git a/src/struphy/pic/accumulation/accum_kernels.py b/src/struphy/pic/accumulation/accum_kernels.py index 8d3c2923b..2a82a9bcf 100644 --- a/src/struphy/pic/accumulation/accum_kernels.py +++ b/src/struphy/pic/accumulation/accum_kernels.py @@ -33,7 +33,6 @@ def charge_density_0form( args_derham: "DerhamArguments", args_domain: "DomainArguments", vec: "float[:,:,:]", - vdim: "int", ): r""" Kernel for :class:`~struphy.pic.accumulation.particles_to_grid.AccumulatorVector` into V0 with the filling @@ -45,6 +44,7 @@ def charge_density_0form( markers = args_markers.markers Np = args_markers.Np + weight_idx = args_markers.weight_idx # -- removed omp: #$ omp parallel private (ip, eta1, eta2, eta3, filling) # -- removed omp: #$ omp for reduction ( + :vec) @@ -59,7 +59,7 @@ def charge_density_0form( eta3 = markers[ip, 2] # filling = w_p/N - filling = markers[ip, 3 + vdim] / Np + filling = markers[ip, weight_idx] / Np particle_to_mat_kernels.vec_fill_b_v0( args_derham, @@ -487,57 +487,6 @@ def linear_vlasov_ampere( # -- removed omp: #$ omp end parallel -def vlasov_maxwell_poisson( - args_markers: "MarkerArguments", - args_derham: "DerhamArguments", - args_domain: "DomainArguments", - vec: "float[:,:,:]", -): - r""" - Accumulates the charge density in V0 - - .. math:: - - \rho_p^\mu = w_p \,. - - Parameters - ---------- - - Note - ---- - The above parameter list contains only the model specific input arguments. - """ - - markers = args_markers.markers - Np = args_markers.Np - - # -- removed omp: #$ omp parallel private (ip, eta1, eta2, eta3, filling) - # -- removed omp: #$ omp for reduction ( + :vec) - for ip in range(shape(markers)[0]): - # only do something if particle is a "true" particle (i.e. not a hole) - if markers[ip, 0] == -1.0: - continue - - # marker positions - eta1 = markers[ip, 0] - eta2 = markers[ip, 1] - eta3 = markers[ip, 2] - - # filling = w_p - filling = markers[ip, 6] / Np - - particle_to_mat_kernels.vec_fill_b_v0( - args_derham, - eta1, - eta2, - eta3, - vec, - filling, - ) - - # -- removed omp: #$ omp end parallel - - @stack_array("dfm", "df_inv", "df_inv_t", "g_inv", "v", "df_inv_times_v", "filling_m", "filling_v") def vlasov_maxwell( args_markers: "MarkerArguments", @@ -1163,9 +1112,7 @@ def pc_lin_mhd_6d_full( vec1_3: "float[:,:,:]", vec2_3: "float[:,:,:]", vec3_3: "float[:,:,:]", - scale_mat: "float", - scale_vec: "float", - boundary_cut: "float", + ep_scale: "float", ): r"""Accumulates into V1 with the filling functions @@ -1209,10 +1156,6 @@ def pc_lin_mhd_6d_full( if markers[ip, 0] == -1.0: continue - # boundary cut - if markers[ip, 0] < boundary_cut or markers[ip, 0] > 1.0 - boundary_cut: - continue - # marker positions eta1 = markers[ip, 0] eta2 = markers[ip, 1] @@ -1243,8 +1186,8 @@ def pc_lin_mhd_6d_full( weight = markers[ip, 8] - filling_m[:, :] = weight * tmp1 / Np * scale_mat - filling_v[:] = weight * tmp_v / Np * scale_vec + filling_m[:, :] = weight * tmp1 / Np * ep_scale + filling_v[:] = weight * tmp_v / Np * ep_scale # call the appropriate matvec filler particle_to_mat_kernels.m_v_fill_v1_pressure_full( @@ -1362,9 +1305,7 @@ def pc_lin_mhd_6d( vec1_3: "float[:,:,:]", vec2_3: "float[:,:,:]", vec3_3: "float[:,:,:]", - scale_mat: "float", - scale_vec: "float", - boundary_cut: "float", + ep_scale: "float", ): r"""Accumulates into V1 with the filling functions @@ -1407,10 +1348,6 @@ def pc_lin_mhd_6d( if markers[ip, 0] == -1.0: continue - # boundary cut - if markers[ip, 0] < boundary_cut or markers[ip, 0] > 1.0 - boundary_cut: - continue - # marker positions eta1 = markers[ip, 0] eta2 = markers[ip, 1] @@ -1441,8 +1378,8 @@ def pc_lin_mhd_6d( linalg_kernels.matrix_matrix(df_inv, df_inv_t, tmp1) linalg_kernels.matrix_vector(df_inv, v, tmp_v) - filling_m[:, :] = weight * tmp1 * scale_mat - filling_v[:] = weight * tmp_v * scale_vec + filling_m[:, :] = weight * tmp1 * ep_scale + filling_v[:] = weight * tmp_v * ep_scale # call the appropriate matvec filler particle_to_mat_kernels.m_v_fill_v1_pressure( diff --git a/src/struphy/pic/accumulation/accum_kernels_gc.py b/src/struphy/pic/accumulation/accum_kernels_gc.py index 628eeeab7..c5e836f81 100644 --- a/src/struphy/pic/accumulation/accum_kernels_gc.py +++ b/src/struphy/pic/accumulation/accum_kernels_gc.py @@ -8,7 +8,7 @@ These kernels are passed to :class:`struphy.pic.accumulation.particles_to_grid.Accumulator`. """ -from numpy import empty, shape, zeros +from numpy import empty, mod, shape, zeros from pyccel.decorators import stack_array import struphy.bsplines.bsplines_kernels as bsplines_kernels @@ -67,6 +67,46 @@ def gc_density_0form( # -- removed omp: #$ omp end parallel +def gc_mag_density_0form( + args_markers: "MarkerArguments", + args_derham: "DerhamArguments", + args_domain: "DomainArguments", + vec: "float[:,:,:]", + scale: "float", # model specific argument +): + r""" + Kernel for :class:`~struphy.pic.accumulation.particles_to_grid.AccumulatorVector` into V0 with the filling + + .. math:: + + B_p^\mu = \mu \frac{w_p}{N} \,. + """ + + markers = args_markers.markers + Np = args_markers.Np + + # -- removed omp: #$ omp parallel private (ip, eta1, eta2, eta3, filling) + # -- removed omp: #$ omp for reduction ( + :vec) + for ip in range(shape(markers)[0]): + # only do something if particle is a "true" particle (i.e. not a hole) + if markers[ip, 0] == -1.0: + continue + + # marker positions + eta1 = markers[ip, 0] + eta2 = markers[ip, 1] + eta3 = markers[ip, 2] + + # marker weight and magnetic moment + weight = markers[ip, 5] + mu = markers[ip, 9] + + # filling =mu*w_p/N + filling = mu * weight / Np * scale + + particle_to_mat_kernels.vec_fill_b_v0(args_derham, eta1, eta2, eta3, vec, filling) + + @stack_array("dfm", "df_inv", "df_inv_t", "g_inv", "tmp1", "tmp2", "b", "b_prod", "bstar", "norm_b1", "curl_norm_b") def cc_lin_mhd_5d_D( args_markers: "MarkerArguments", @@ -75,22 +115,19 @@ def cc_lin_mhd_5d_D( mat12: "float[:,:,:,:,:,:]", mat13: "float[:,:,:,:,:,:]", mat23: "float[:,:,:,:,:,:]", - epsilon: float, # model specific argument - b2_1: "float[:,:,:]", # model specific argument - b2_2: "float[:,:,:]", # model specific argument - b2_3: "float[:,:,:]", # model specific argument - # model specific argument + epsilon: float, + ep_scale: "float", + b2_1: "float[:,:,:]", + b2_2: "float[:,:,:]", + b2_3: "float[:,:,:]", norm_b11: "float[:,:,:]", norm_b12: "float[:,:,:]", norm_b13: "float[:,:,:]", - # model specific argument curl_norm_b1: "float[:,:,:]", curl_norm_b2: "float[:,:,:]", curl_norm_b3: "float[:,:,:]", basis_u: "int", - scale_mat: "float", - boundary_cut: float, -): # model specific argument +): r"""Accumulation kernel for the propagator :class:`~struphy.propagators.propagators_fields.CurrentCoupling5DDensity`. Accumulates :math:`\alpha`-form matrix with the filling functions (:math:`\alpha = 2`) @@ -157,9 +194,6 @@ def cc_lin_mhd_5d_D( v = markers[ip, 3] - if eta1 < boundary_cut or eta1 > 1.0 - boundary_cut: - continue - # b-field evaluation span1, span2, span3 = get_spans(eta1, eta2, eta3, args_derham) @@ -186,11 +220,9 @@ def cc_lin_mhd_5d_D( # calculate Bstar and transform to H1vec b_star[:] = b + epsilon * v * curl_norm_b - b_star /= det_df # calculate b_para and b_star_para b_para = linalg_kernels.scalar_dot(norm_b1, b) - b_para /= det_df b_star_para = linalg_kernels.scalar_dot(norm_b1, b_star) @@ -202,9 +234,9 @@ def cc_lin_mhd_5d_D( if basis_u == 0: # filling functions - filling_m12 = -weight * density_const * b_prod[0, 1] * scale_mat - filling_m13 = -weight * density_const * b_prod[0, 2] * scale_mat - filling_m23 = -weight * density_const * b_prod[1, 2] * scale_mat + filling_m12 = -weight * density_const * b_prod[0, 1] * ep_scale / epsilon + filling_m13 = -weight * density_const * b_prod[0, 2] * ep_scale / epsilon + filling_m23 = -weight * density_const * b_prod[1, 2] * ep_scale / epsilon # call the appropriate matvec filler particle_to_mat_kernels.mat_fill_v0vec_asym( @@ -219,9 +251,9 @@ def cc_lin_mhd_5d_D( linalg_kernels.matrix_matrix(g_inv, b_prod, tmp1) linalg_kernels.matrix_matrix(tmp1, g_inv, tmp2) - filling_m12 = -weight * density_const * tmp2[0, 1] * scale_mat - filling_m13 = -weight * density_const * tmp2[0, 2] * scale_mat - filling_m23 = -weight * density_const * tmp2[1, 2] * scale_mat + filling_m12 = -weight * density_const * tmp2[0, 1] * ep_scale / epsilon + filling_m13 = -weight * density_const * tmp2[0, 2] * ep_scale / epsilon + filling_m23 = -weight * density_const * tmp2[1, 2] * ep_scale / epsilon # call the appropriate matvec filler particle_to_mat_kernels.mat_fill_v1_asym( @@ -230,9 +262,9 @@ def cc_lin_mhd_5d_D( elif basis_u == 2: # filling functions - filling_m12 = -weight * density_const * b_prod[0, 1] * scale_mat / det_df**2 - filling_m13 = -weight * density_const * b_prod[0, 2] * scale_mat / det_df**2 - filling_m23 = -weight * density_const * b_prod[1, 2] * scale_mat / det_df**2 + filling_m12 = -weight * density_const * b_prod[0, 1] * ep_scale / epsilon / det_df**2 + filling_m13 = -weight * density_const * b_prod[0, 2] * ep_scale / epsilon / det_df**2 + filling_m23 = -weight * density_const * b_prod[1, 2] * ep_scale / epsilon / det_df**2 # call the appropriate matvec filler particle_to_mat_kernels.mat_fill_v2_asym( @@ -248,23 +280,23 @@ def cc_lin_mhd_5d_D( @stack_array( "dfm", - "df_inv_t", "df_inv", + "df_inv_t", "g_inv", "filling_m", "filling_v", "tmp", "tmp1", - "tmp2", "tmp_m", "tmp_v", "b", + "bfull_star", "b_prod", - "b_prod_negb_star", + "b_prod_neg", "norm_b1", "curl_norm_b", ) -def cc_lin_mhd_5d_J1( +def cc_lin_mhd_5d_curlb( args_markers: "MarkerArguments", args_derham: "DerhamArguments", args_domain: "DomainArguments", @@ -277,21 +309,19 @@ def cc_lin_mhd_5d_J1( vec1: "float[:,:,:]", vec2: "float[:,:,:]", vec3: "float[:,:,:]", - epsilon: float, # model specific argument - b1: "float[:,:,:]", # model specific argument - b2: "float[:,:,:]", # model specific argument - b3: "float[:,:,:]", # model specific argument - norm_b11: "float[:,:,:]", # model specific argument - norm_b12: "float[:,:,:]", # model specific argument - norm_b13: "float[:,:,:]", # model specific argument - curl_norm_b1: "float[:,:,:]", # model specific argument - curl_norm_b2: "float[:,:,:]", # model specific argument - curl_norm_b3: "float[:,:,:]", # model specific argument - basis_u: "int", # model specific argument - scale_mat: "float", # model specific argument - scale_vec: "float", # model specific argument - boundary_cut: "float", -): # model specific argument + epsilon: float, + ep_scale: float, + b1: "float[:,:,:]", + b2: "float[:,:,:]", + b3: "float[:,:,:]", + norm_b11: "float[:,:,:]", + norm_b12: "float[:,:,:]", + norm_b13: "float[:,:,:]", + curl_norm_b1: "float[:,:,:]", + curl_norm_b2: "float[:,:,:]", + curl_norm_b3: "float[:,:,:]", + basis_u: "int", +): r"""Accumulation kernel for the propagator :class:`~struphy.propagators.propagators_coupling.CurrentCoupling5DCurlb`. Accumulates :math:`\alpha`-form matrix and vector with the filling functions (:math:`\alpha = 2`) @@ -303,21 +333,6 @@ def cc_lin_mhd_5d_J1( B_p^\mu &= w_p \left( \frac{v^2_{\parallel,p}}{g\hat B^*_\parallel} \mathbf B^2_{\times} \right)_\mu \,, where :math:`\mathbf B^2_{\times} \mathbf a := \hat{\mathbf B}^2 \times \mathbf a` for :math:`a \in \mathbb R^3`. - - Parameters - ---------- - b1, b2, b3 : array[float] - FE coefficients c_ijk of the magnetic field as a 2-form. - - norm_b11, norm_b12, norm_b13 : array[float] - FE coefficients c_ijk of the normalized magnetic field as a 1-form. - - curl_norm_b1, curl_norm_b2, curl_norm_b3 : array[float] - FE coefficients c_ijk of the curl of normalized magnetic field as a 2-form. - - Note - ---- - The above parameter list contains only the model specific input arguments. """ markers = args_markers.markers @@ -325,7 +340,7 @@ def cc_lin_mhd_5d_J1( # allocate for magnetic field evaluation b = empty(3, dtype=float) - b_star = empty(3, dtype=float) + bfull_star = empty(3, dtype=float) b_prod = zeros((3, 3), dtype=float) b_prod_neg = zeros((3, 3), dtype=float) norm_b1 = empty(3, dtype=float) @@ -338,12 +353,11 @@ def cc_lin_mhd_5d_J1( g_inv = empty((3, 3), dtype=float) # allocate for filling - filling_m = empty((3, 3), dtype=float) - filling_v = empty(3, dtype=float) + filling_m = zeros((3, 3), dtype=float) + filling_v = zeros(3, dtype=float) tmp = empty((3, 3), dtype=float) tmp1 = empty((3, 3), dtype=float) - tmp2 = empty((3, 3), dtype=float) tmp_m = empty((3, 3), dtype=float) tmp_v = empty(3, dtype=float) @@ -351,8 +365,6 @@ def cc_lin_mhd_5d_J1( # get number of markers n_markers_loc = shape(markers)[0] - # -- removed omp: #$ omp parallel firstprivate(b_prod) private(ip, boundary_cut, eta1, eta2, eta3, v, weight, span1, span2, span3, b1, b2, b3, b, b_star, b_prod_neg, norm_b1, curl_norm_b, abs_b_star_para, dfm, df_inv, df_inv_t, g_inv, det_df, tmp, tmp1, tmp2, tmp_m, tmp_v, filling_m, filling_v) - # -- removed omp: #$ omp for reduction ( + : mat11, mat12, mat13, mat22, mat23, mat33, vec1, vec2, vec3) for ip in range(n_markers_loc): # only do something if particle is a "true" particle (i.e. not a hole) if markers[ip, 0] == -1.0: @@ -367,9 +379,6 @@ def cc_lin_mhd_5d_J1( weight = markers[ip, 5] v = markers[ip, 3] - if eta1 < boundary_cut or eta1 > 1.0 - boundary_cut: - continue - # b-field evaluation span1, span2, span3 = get_spans(eta1, eta2, eta3, args_derham) @@ -387,11 +396,11 @@ def cc_lin_mhd_5d_J1( # curl_norm_b; 2form eval_2form_spline_mpi(span1, span2, span3, args_derham, curl_norm_b1, curl_norm_b2, curl_norm_b3, curl_norm_b) - # b_star; 2form in H1vec - b_star[:] = (b + curl_norm_b * v * epsilon) / det_df + # b_star; 2form + bfull_star[:] = b + curl_norm_b * v * epsilon # calculate abs_b_star_para - abs_b_star_para = linalg_kernels.scalar_dot(norm_b1, b_star) + abs_b_star_para = linalg_kernels.scalar_dot(norm_b1, bfull_star) # calculate tensor product of two curl_norm_b linalg_kernels.outer(curl_norm_b, curl_norm_b, tmp) @@ -411,8 +420,8 @@ def cc_lin_mhd_5d_J1( linalg_kernels.matrix_matrix(tmp1, b_prod_neg, tmp_m) linalg_kernels.matrix_vector(b_prod, curl_norm_b, tmp_v) - filling_m[:, :] = weight * tmp_m * v**2 / abs_b_star_para**2 / det_df**2 * scale_mat - filling_v[:] = weight * tmp_v * v**2 / abs_b_star_para / det_df * scale_vec + filling_m[:, :] += weight * tmp_m * v**2 / abs_b_star_para**2 * ep_scale + filling_v[:] += weight * tmp_v * v**2 / abs_b_star_para * ep_scale # call the appropriate matvec filler particle_to_mat_kernels.m_v_fill_v0vec_symm( @@ -440,54 +449,13 @@ def cc_lin_mhd_5d_J1( filling_v[2], ) - elif basis_u == 1: - # needed metric coefficients - linalg_kernels.matrix_inv_with_det(dfm, det_df, df_inv) - linalg_kernels.transpose(df_inv, df_inv_t) - linalg_kernels.matrix_matrix(df_inv, df_inv_t, g_inv) - linalg_kernels.matrix_matrix(g_inv, b_prod, tmp1) - linalg_kernels.matrix_vector(tmp1, curl_norm_b, tmp_v) - - linalg_kernels.matrix_matrix(tmp1, tmp, tmp2) - linalg_kernels.matrix_matrix(tmp2, b_prod_neg, tmp1) - linalg_kernels.matrix_matrix(tmp1, g_inv, tmp_m) - - filling_m[:, :] = weight * tmp_m * v**2 / abs_b_star_para**2 / det_df**2 * scale_mat - filling_v[:] = weight * tmp_v * v**2 / abs_b_star_para / det_df * scale_vec - - # call the appropriate matvec filler - particle_to_mat_kernels.m_v_fill_v1_symm( - args_derham, - span1, - span2, - span3, - mat11, - mat12, - mat13, - mat22, - mat23, - mat33, - filling_m[0, 0], - filling_m[0, 1], - filling_m[0, 2], - filling_m[1, 1], - filling_m[1, 2], - filling_m[2, 2], - vec1, - vec2, - vec3, - filling_v[0], - filling_v[1], - filling_v[2], - ) - elif basis_u == 2: linalg_kernels.matrix_matrix(b_prod, tmp, tmp1) linalg_kernels.matrix_matrix(tmp1, b_prod_neg, tmp_m) linalg_kernels.matrix_vector(b_prod, curl_norm_b, tmp_v) - filling_m[:, :] = weight * tmp_m * v**2 / abs_b_star_para**2 / det_df**4 * scale_mat - filling_v[:] = weight * tmp_v * v**2 / abs_b_star_para / det_df**2 * scale_vec + filling_m[:, :] = weight * tmp_m * v**2 / abs_b_star_para**2 / det_df**2 * ep_scale + filling_v[:] = weight * tmp_v * v**2 / abs_b_star_para / det_df * ep_scale # call the appropriate matvec filler particle_to_mat_kernels.m_v_fill_v2_symm( @@ -526,8 +494,6 @@ def cc_lin_mhd_5d_J1( vec2 /= Np vec3 /= Np - # -- removed omp: #$ omp end parallel - @stack_array("dfm", "norm_b1", "filling_v") def cc_lin_mhd_5d_M( @@ -547,8 +513,7 @@ def cc_lin_mhd_5d_M( norm_b12: "float[:,:,:]", # model specific argument norm_b13: "float[:,:,:]", # model specific argument scale_vec: "float", # model specific argument - boundary_cut: "float", -): # model specific argument +): r"""Accumulation kernel for the propagator :class:`~struphy.propagators.propagators_fields.ShearAlfvenCurrentCoupling5D` and :class:`~struphy.propagators.propagators_fields.MagnetosonicCurrentCoupling5D`. Accumulates 2-form vector with the filling functions: @@ -600,9 +565,6 @@ def cc_lin_mhd_5d_M( weight = markers[ip, 5] mu = markers[ip, 9] - if eta1 < boundary_cut or eta1 > 1.0 - boundary_cut: - continue - # b-field evaluation span1, span2, span3 = get_spans(eta1, eta2, eta3, args_derham) @@ -633,19 +595,17 @@ def cc_lin_mhd_5d_M( "df_inv", "g_inv", "filling_v", - "tmp1", - "tmp2", + "tmp", "tmp_v", "b", "b_prod", - "norm_b2_prod", + "norm_b_prod", "b_star", "curl_norm_b", "norm_b1", - "norm_b2", "grad_PB", ) -def cc_lin_mhd_5d_J2( +def cc_lin_mhd_5d_gradB( args_markers: "MarkerArguments", args_derham: "DerhamArguments", args_domain: "DomainArguments", @@ -658,27 +618,22 @@ def cc_lin_mhd_5d_J2( vec1: "float[:,:,:]", vec2: "float[:,:,:]", vec3: "float[:,:,:]", - epsilon: float, # model specific argument - b1: "float[:,:,:]", # model specific argument - b2: "float[:,:,:]", # model specific argument - b3: "float[:,:,:]", # model specific argument - norm_b11: "float[:,:,:]", # model specific argument - norm_b12: "float[:,:,:]", # model specific argument - norm_b13: "float[:,:,:]", # model specific argument - norm_b21: "float[:,:,:]", # model specific argument - norm_b22: "float[:,:,:]", # model specific argument - norm_b23: "float[:,:,:]", # model specific argument - curl_norm_b1: "float[:,:,:]", # model specific argument - curl_norm_b2: "float[:,:,:]", # model specific argument - curl_norm_b3: "float[:,:,:]", # model specific argument - grad_PB1: "float[:,:,:]", # model specific argument - grad_PB2: "float[:,:,:]", # model specific argument - grad_PB3: "float[:,:,:]", # model specific argument + epsilon: float, + ep_scale: float, + b1: "float[:,:,:]", + b2: "float[:,:,:]", + b3: "float[:,:,:]", + norm_b11: "float[:,:,:]", + norm_b12: "float[:,:,:]", + norm_b13: "float[:,:,:]", + curl_norm_b1: "float[:,:,:]", + curl_norm_b2: "float[:,:,:]", + curl_norm_b3: "float[:,:,:]", + grad_PB1: "float[:,:,:]", + grad_PB2: "float[:,:,:]", + grad_PB3: "float[:,:,:]", basis_u: "int", - scale_mat: "float", - scale_vec: "float", - boundary_cut: float, -): # model specific argument +): r"""Accumulation kernel for the propagator :class:`~struphy.propagators.propagators_coupling.CurrentCoupling5DGradB`. Accumulates math:`\alpha` -form vector with the filling functions @@ -697,9 +652,6 @@ def cc_lin_mhd_5d_J2( norm_b11, norm_b12, norm_b13 : array[float] FE coefficients c_ijk of the normalized magnetic field as a 1-form. - norm_b21, norm_b22, norm_b23 : array[float] - FE coefficients c_ijk of the normalized magnetic field as a 2-form. - curl_norm_b1, curl_norm_b2, curl_norm_b3 : array[float] FE coefficients c_ijk of the curl of normalized magnetic field as a 2-form. @@ -718,10 +670,9 @@ def cc_lin_mhd_5d_J2( b = empty(3, dtype=float) b_star = empty(3, dtype=float) b_prod = zeros((3, 3), dtype=float) - norm_b2_prod = zeros((3, 3), dtype=float) + norm_b_prod = zeros((3, 3), dtype=float) curl_norm_b = empty(3, dtype=float) norm_b1 = empty(3, dtype=float) - norm_b2 = empty(3, dtype=float) grad_PB = empty(3, dtype=float) # allocate for metric coeffs @@ -732,17 +683,13 @@ def cc_lin_mhd_5d_J2( # allocate for filling filling_v = empty(3, dtype=float) - - tmp1 = empty((3, 3), dtype=float) - tmp2 = empty((3, 3), dtype=float) + tmp = empty((3, 3), dtype=float) tmp_v = empty(3, dtype=float) # get number of markers n_markers_loc = shape(markers)[0] - # -- removed omp: #$ omp parallel firstprivate(b_prod) private(ip, boundary_cut, eta1, eta2, eta3, v, mu, weight, span1, span2, span3, b1, b2, b3, b, b_star, norm_b1, norm_b2, norm_b2_prod, curl_norm_b, grad_PB, abs_b_star_para, dfm, df_inv, df_inv_t, g_inv, det_df, tmp1, tmp2, tmp_v, filling_v) - # -- removed omp: #$ omp for reduction ( + : mat11, mat12, mat13, mat22, mat23, mat33, vec1, vec2, vec3) for ip in range(n_markers_loc): # only do something if particle is a "true" particle (i.e. not a hole) if markers[ip, 0] == -1.0: @@ -753,9 +700,173 @@ def cc_lin_mhd_5d_J2( eta2 = markers[ip, 1] eta3 = markers[ip, 2] - if eta1 < boundary_cut or eta1 > 1.0 - boundary_cut: + # marker weight and velocity + weight = markers[ip, 5] + v = markers[ip, 3] + mu = markers[ip, 9] + + # b-field evaluation + span1, span2, span3 = get_spans(eta1, eta2, eta3, args_derham) + + # evaluate Jacobian, result in dfm + evaluation_kernels.df(eta1, eta2, eta3, args_domain, dfm) + + det_df = linalg_kernels.det(dfm) + + # needed metric coefficients + linalg_kernels.matrix_inv_with_det(dfm, det_df, df_inv) + linalg_kernels.transpose(df_inv, df_inv_t) + linalg_kernels.matrix_matrix(df_inv, df_inv_t, g_inv) + + # b; 2form + eval_2form_spline_mpi(span1, span2, span3, args_derham, b1, b2, b3, b) + + # norm_b1; 1form + eval_1form_spline_mpi(span1, span2, span3, args_derham, norm_b11, norm_b12, norm_b13, norm_b1) + + # curl_norm_b; 2form + eval_2form_spline_mpi(span1, span2, span3, args_derham, curl_norm_b1, curl_norm_b2, curl_norm_b3, curl_norm_b) + + # grad_PB; 1form + eval_1form_spline_mpi(span1, span2, span3, args_derham, grad_PB1, grad_PB2, grad_PB3, grad_PB) + + # b_star; 2form transformed into H1vec + b_star[:] = b + curl_norm_b * v * epsilon + + # calculate abs_b_star_para + abs_b_star_para = linalg_kernels.scalar_dot(norm_b1, b_star) + + # operator bx() as matrix + b_prod[0, 1] = -b[2] + b_prod[0, 2] = +b[1] + b_prod[1, 0] = +b[2] + b_prod[1, 2] = -b[0] + b_prod[2, 0] = -b[1] + b_prod[2, 1] = +b[0] + + norm_b_prod[0, 1] = -norm_b1[2] + norm_b_prod[0, 2] = +norm_b1[1] + norm_b_prod[1, 0] = +norm_b1[2] + norm_b_prod[1, 2] = -norm_b1[0] + norm_b_prod[2, 0] = -norm_b1[1] + norm_b_prod[2, 1] = +norm_b1[0] + + if basis_u == 0: + linalg_kernels.matrix_matrix(b_prod, norm_b_prod, tmp) + linalg_kernels.matrix_vector(tmp, grad_PB, tmp_v) + + filling_v[:] = weight * tmp_v * mu / abs_b_star_para * ep_scale + + # call the appropriate matvec filler + particle_to_mat_kernels.vec_fill_v0vec( + args_derham, span1, span2, span3, vec1, vec2, vec3, filling_v[0], filling_v[1], filling_v[2] + ) + + elif basis_u == 2: + linalg_kernels.matrix_matrix(b_prod, norm_b_prod, tmp) + linalg_kernels.matrix_vector(tmp, grad_PB, tmp_v) + + filling_v[:] = weight * tmp_v * mu / abs_b_star_para / det_df * ep_scale + + # call the appropriate matvec filler + particle_to_mat_kernels.vec_fill_v2( + args_derham, span1, span2, span3, vec1, vec2, vec3, filling_v[0], filling_v[1], filling_v[2] + ) + vec1 /= Np + vec2 /= Np + vec3 /= Np + + +@stack_array( + "dfm", + "df_inv_t", + "df_inv", + "g_inv", + "filling_v", + "tmp", + "tmp_v", + "b", + "b_prod", + "beq", + "beq_prod", + "norm_b_prod", + "bfull_star", + "curl_norm_b", + "norm_b1", + "grad_PB", + "grad_PBeq", +) +def cc_lin_mhd_5d_gradB_dg_init( + args_markers: "MarkerArguments", + args_derham: "DerhamArguments", + args_domain: "DomainArguments", + vec1: "float[:,:,:]", + vec2: "float[:,:,:]", + vec3: "float[:,:,:]", + epsilon: float, + ep_scale: float, + b1: "float[:,:,:]", + b2: "float[:,:,:]", + b3: "float[:,:,:]", + beq1: "float[:,:,:]", + beq2: "float[:,:,:]", + beq3: "float[:,:,:]", + norm_b11: "float[:,:,:]", + norm_b12: "float[:,:,:]", + norm_b13: "float[:,:,:]", + curl_norm_b1: "float[:,:,:]", + curl_norm_b2: "float[:,:,:]", + curl_norm_b3: "float[:,:,:]", + grad_PB1: "float[:,:,:]", + grad_PB2: "float[:,:,:]", + grad_PB3: "float[:,:,:]", + grad_PBeq1: "float[:,:,:]", + grad_PBeq2: "float[:,:,:]", + grad_PBeq3: "float[:,:,:]", + basis_u: "int", +): + r"""TODO""" + + markers = args_markers.markers + Np = args_markers.Np + + # allocate for magnetic field evaluation + b = empty(3, dtype=float) + beq = empty(3, dtype=float) + bfull_star = empty(3, dtype=float) + b_prod = zeros((3, 3), dtype=float) + beq_prod = zeros((3, 3), dtype=float) + norm_b_prod = zeros((3, 3), dtype=float) + curl_norm_b = empty(3, dtype=float) + norm_b1 = empty(3, dtype=float) + grad_PB = empty(3, dtype=float) + grad_PBeq = empty(3, dtype=float) + + # allocate for metric coeffs + dfm = empty((3, 3), dtype=float) + df_inv = empty((3, 3), dtype=float) + df_inv_t = empty((3, 3), dtype=float) + g_inv = empty((3, 3), dtype=float) + + # allocate for filling + filling_v = empty(3, dtype=float) + tmp = empty((3, 3), dtype=float) + + tmp_v = empty(3, dtype=float) + + # get number of markers + n_markers_loc = shape(markers)[0] + + for ip in range(n_markers_loc): + # only do something if particle is a "true" particle (i.e. not a hole) + if markers[ip, 0] == -1.0: continue + # marker positions + eta1 = markers[ip, 0] + eta2 = markers[ip, 1] + eta3 = markers[ip, 2] + # marker weight and velocity weight = markers[ip, 5] v = markers[ip, 3] @@ -777,23 +888,26 @@ def cc_lin_mhd_5d_J2( # b; 2form eval_2form_spline_mpi(span1, span2, span3, args_derham, b1, b2, b3, b) + # beq; 2form + eval_2form_spline_mpi(span1, span2, span3, args_derham, beq1, beq2, beq3, beq) + # norm_b1; 1form eval_1form_spline_mpi(span1, span2, span3, args_derham, norm_b11, norm_b12, norm_b13, norm_b1) - # norm_b2; 2form - eval_2form_spline_mpi(span1, span2, span3, args_derham, norm_b21, norm_b22, norm_b23, norm_b2) - # curl_norm_b; 2form eval_2form_spline_mpi(span1, span2, span3, args_derham, curl_norm_b1, curl_norm_b2, curl_norm_b3, curl_norm_b) # grad_PB; 1form eval_1form_spline_mpi(span1, span2, span3, args_derham, grad_PB1, grad_PB2, grad_PB3, grad_PB) + # grad_PBeq; 1form + eval_1form_spline_mpi(span1, span2, span3, args_derham, grad_PBeq1, grad_PBeq2, grad_PBeq3, grad_PBeq) + # b_star; 2form transformed into H1vec - b_star[:] = (b + curl_norm_b * v * epsilon) / det_df + bfull_star[:] = b + beq + curl_norm_b * v * epsilon # calculate abs_b_star_para - abs_b_star_para = linalg_kernels.scalar_dot(norm_b1, b_star) + abs_b_star_para = linalg_kernels.scalar_dot(norm_b1, bfull_star) # operator bx() as matrix b_prod[0, 1] = -b[2] @@ -803,50 +917,304 @@ def cc_lin_mhd_5d_J2( b_prod[2, 0] = -b[1] b_prod[2, 1] = +b[0] - norm_b2_prod[0, 1] = -norm_b2[2] - norm_b2_prod[0, 2] = +norm_b2[1] - norm_b2_prod[1, 0] = +norm_b2[2] - norm_b2_prod[1, 2] = -norm_b2[0] - norm_b2_prod[2, 0] = -norm_b2[1] - norm_b2_prod[2, 1] = +norm_b2[0] + beq_prod[0, 1] = -beq[2] + beq_prod[0, 2] = +beq[1] + beq_prod[1, 0] = +beq[2] + beq_prod[1, 2] = -beq[0] + beq_prod[2, 0] = -beq[1] + beq_prod[2, 1] = +beq[0] + + norm_b_prod[0, 1] = -norm_b1[2] + norm_b_prod[0, 2] = +norm_b1[1] + norm_b_prod[1, 0] = +norm_b1[2] + norm_b_prod[1, 2] = -norm_b1[0] + norm_b_prod[2, 0] = -norm_b1[1] + norm_b_prod[2, 1] = +norm_b1[0] if basis_u == 0: - linalg_kernels.matrix_matrix(b_prod, g_inv, tmp1) - linalg_kernels.matrix_matrix(tmp1, norm_b2_prod, tmp2) - linalg_kernels.matrix_matrix(tmp2, g_inv, tmp1) + # beq contribution + linalg_kernels.matrix_matrix(beq_prod, norm_b_prod, tmp) + linalg_kernels.matrix_vector(tmp, grad_PBeq, tmp_v) + + filling_v[:] = weight * tmp_v * mu / abs_b_star_para * ep_scale + + # b contribution + linalg_kernels.matrix_matrix(beq_prod, norm_b_prod, tmp) + linalg_kernels.matrix_vector(tmp, grad_PB, tmp_v) + + filling_v[:] += weight * tmp_v * mu / abs_b_star_para * ep_scale + + linalg_kernels.matrix_matrix(b_prod, norm_b_prod, tmp) + linalg_kernels.matrix_vector(tmp, grad_PBeq, tmp_v) - linalg_kernels.matrix_vector(tmp1, grad_PB, tmp_v) + filling_v[:] += weight * tmp_v * mu / abs_b_star_para * ep_scale - filling_v[:] = weight * tmp_v * mu / abs_b_star_para * scale_vec + linalg_kernels.matrix_vector(tmp, grad_PB, tmp_v) + + filling_v[:] += weight * tmp_v * mu / abs_b_star_para * ep_scale # call the appropriate matvec filler particle_to_mat_kernels.vec_fill_v0vec( args_derham, span1, span2, span3, vec1, vec2, vec3, filling_v[0], filling_v[1], filling_v[2] ) - elif basis_u == 1: - linalg_kernels.matrix_matrix(g_inv, b_prod, tmp1) - linalg_kernels.matrix_matrix(tmp1, g_inv, tmp2) - linalg_kernels.matrix_matrix(tmp2, norm_b2_prod, tmp1) - linalg_kernels.matrix_matrix(tmp1, g_inv, tmp2) + elif basis_u == 2: + # beq contribution + linalg_kernels.matrix_matrix(beq_prod, norm_b_prod, tmp) + linalg_kernels.matrix_vector(tmp, grad_PBeq, tmp_v) + + filling_v[:] = weight * tmp_v * mu / abs_b_star_para / det_df * ep_scale + + # b contribution + linalg_kernels.matrix_vector(tmp, grad_PB, tmp_v) + + filling_v[:] += weight * tmp_v * mu / abs_b_star_para / det_df * ep_scale + + linalg_kernels.matrix_matrix(b_prod, norm_b_prod, tmp) + linalg_kernels.matrix_vector(tmp, grad_PBeq, tmp_v) + + filling_v[:] += weight * tmp_v * mu / abs_b_star_para / det_df * ep_scale + + linalg_kernels.matrix_vector(tmp, grad_PB, tmp_v) + + filling_v[:] += weight * tmp_v * mu / abs_b_star_para / det_df * ep_scale + + # call the appropriate matvec filler + particle_to_mat_kernels.vec_fill_v2( + args_derham, span1, span2, span3, vec1, vec2, vec3, filling_v[0], filling_v[1], filling_v[2] + ) + + vec1 /= Np + vec2 /= Np + vec3 /= Np + + +@stack_array( + "dfm", + "df_inv_t", + "df_inv", + "g_inv", + "filling_v", + "tmp", + "tmp_v", + "b", + "b_prod", + "eta_diff", + "beq", + "beq_prod", + "norm_b_prod", + "bfull_star", + "curl_norm_b", + "norm_b1", + "grad_PB", + "grad_PBeq", + "eta_mid", + "eta_diff", +) +def cc_lin_mhd_5d_gradB_dg( + args_markers: "MarkerArguments", + args_derham: "DerhamArguments", + args_domain: "DomainArguments", + vec1: "float[:,:,:]", + vec2: "float[:,:,:]", + vec3: "float[:,:,:]", + epsilon: float, + ep_scale: float, + b1: "float[:,:,:]", + b2: "float[:,:,:]", + b3: "float[:,:,:]", + beq1: "float[:,:,:]", + beq2: "float[:,:,:]", + beq3: "float[:,:,:]", + norm_b11: "float[:,:,:]", + norm_b12: "float[:,:,:]", + norm_b13: "float[:,:,:]", + curl_norm_b1: "float[:,:,:]", + curl_norm_b2: "float[:,:,:]", + curl_norm_b3: "float[:,:,:]", + grad_PB1: "float[:,:,:]", + grad_PB2: "float[:,:,:]", + grad_PB3: "float[:,:,:]", + grad_PBeq1: "float[:,:,:]", + grad_PBeq2: "float[:,:,:]", + grad_PBeq3: "float[:,:,:]", + basis_u: "int", + const: "float", +): + r"""TODO""" + + markers = args_markers.markers + Np = args_markers.Np + + # allocate for magnetic field evaluation + eta_diff = empty(3, dtype=float) + eta_mid = empty(3, dtype=float) + b = empty(3, dtype=float) + beq = empty(3, dtype=float) + bfull_star = empty(3, dtype=float) + b_prod = zeros((3, 3), dtype=float) + beq_prod = zeros((3, 3), dtype=float) + norm_b_prod = zeros((3, 3), dtype=float) + curl_norm_b = empty(3, dtype=float) + norm_b1 = empty(3, dtype=float) + grad_PB = empty(3, dtype=float) + grad_PBeq = empty(3, dtype=float) + + # allocate for metric coeffs + dfm = empty((3, 3), dtype=float) + df_inv = empty((3, 3), dtype=float) + df_inv_t = empty((3, 3), dtype=float) + g_inv = empty((3, 3), dtype=float) + + # allocate for filling + filling_v = empty(3, dtype=float) + tmp = empty((3, 3), dtype=float) + + tmp_v = empty(3, dtype=float) + + # get number of markers + n_markers_loc = shape(markers)[0] + + for ip in range(n_markers_loc): + # only do something if particle is a "true" particle (i.e. not a hole) + if markers[ip, 0] == -1.0: + continue + + # marker positions, mid point + eta_mid[:] = (markers[ip, 0:3] + markers[ip, 11:14]) / 2.0 + eta_mid[:] = mod(eta_mid[:], 1.0) + + eta_diff[:] = markers[ip, 0:3] - markers[ip, 11:14] + + # marker weight and velocity + weight = markers[ip, 5] + v = markers[ip, 3] + mu = markers[ip, 9] + + # b-field evaluation + span1, span2, span3 = get_spans(eta_mid[0], eta_mid[1], eta_mid[2], args_derham) + + # evaluate Jacobian, result in dfm + evaluation_kernels.df(eta_mid[0], eta_mid[1], eta_mid[2], args_domain, dfm) + + det_df = linalg_kernels.det(dfm) - linalg_kernels.matrix_vector(tmp2, grad_PB, tmp_v) + # needed metric coefficients + linalg_kernels.matrix_inv_with_det(dfm, det_df, df_inv) + linalg_kernels.transpose(df_inv, df_inv_t) + linalg_kernels.matrix_matrix(df_inv, df_inv_t, g_inv) + + # b; 2form + eval_2form_spline_mpi(span1, span2, span3, args_derham, b1, b2, b3, b) + + # beq; 2form + eval_2form_spline_mpi(span1, span2, span3, args_derham, beq1, beq2, beq3, beq) + + # norm_b1; 1form + eval_1form_spline_mpi(span1, span2, span3, args_derham, norm_b11, norm_b12, norm_b13, norm_b1) + + # curl_norm_b; 2form + eval_2form_spline_mpi(span1, span2, span3, args_derham, curl_norm_b1, curl_norm_b2, curl_norm_b3, curl_norm_b) + + # grad_PB; 1form + eval_1form_spline_mpi(span1, span2, span3, args_derham, grad_PB1, grad_PB2, grad_PB3, grad_PB) - filling_v[:] = weight * tmp_v * mu / abs_b_star_para * scale_vec + # grad_PBeq; 1form + eval_1form_spline_mpi(span1, span2, span3, args_derham, grad_PBeq1, grad_PBeq2, grad_PBeq3, grad_PBeq) + + # b_star; 2form transformed into H1vec + bfull_star[:] = b + beq + curl_norm_b * v * epsilon + + # calculate abs_b_star_para + abs_b_star_para = linalg_kernels.scalar_dot(norm_b1, bfull_star) + + # operator bx() as matrix + b_prod[0, 1] = -b[2] + b_prod[0, 2] = +b[1] + b_prod[1, 0] = +b[2] + b_prod[1, 2] = -b[0] + b_prod[2, 0] = -b[1] + b_prod[2, 1] = +b[0] + + beq_prod[0, 1] = -beq[2] + beq_prod[0, 2] = +beq[1] + beq_prod[1, 0] = +beq[2] + beq_prod[1, 2] = -beq[0] + beq_prod[2, 0] = -beq[1] + beq_prod[2, 1] = +beq[0] + + norm_b_prod[0, 1] = -norm_b1[2] + norm_b_prod[0, 2] = +norm_b1[1] + norm_b_prod[1, 0] = +norm_b1[2] + norm_b_prod[1, 2] = -norm_b1[0] + norm_b_prod[2, 0] = -norm_b1[1] + norm_b_prod[2, 1] = +norm_b1[0] + + if basis_u == 0: + # beq * gradPBeq contribution + linalg_kernels.matrix_matrix(beq_prod, norm_b_prod, tmp) + linalg_kernels.matrix_vector(tmp, grad_PBeq, tmp_v) + + filling_v[:] = weight * tmp_v * mu / abs_b_star_para * ep_scale + + # beq * gradPB contribution + linalg_kernels.matrix_vector(tmp, grad_PB, tmp_v) + filling_v[:] += weight * tmp_v * mu / abs_b_star_para * ep_scale + + # beq * dg term contribution + linalg_kernels.matrix_vector(tmp, eta_diff, tmp_v) + filling_v[:] += tmp_v / abs_b_star_para * const + + # b * gradPBeq contribution + linalg_kernels.matrix_matrix(b_prod, norm_b_prod, tmp) + linalg_kernels.matrix_vector(tmp, grad_PBeq, tmp_v) + filling_v[:] += weight * tmp_v * mu / abs_b_star_para * ep_scale + + # b * gradPB contribution + linalg_kernels.matrix_vector(tmp, grad_PB, tmp_v) + filling_v[:] += weight * tmp_v * mu / abs_b_star_para * ep_scale + + # b * dg term contribution + linalg_kernels.matrix_vector(tmp, eta_diff, tmp_v) + filling_v[:] += tmp_v / abs_b_star_para * const # call the appropriate matvec filler - particle_to_mat_kernels.vec_fill_v1( + particle_to_mat_kernels.vec_fill_v0vec( args_derham, span1, span2, span3, vec1, vec2, vec3, filling_v[0], filling_v[1], filling_v[2] ) elif basis_u == 2: - linalg_kernels.matrix_matrix(b_prod, g_inv, tmp1) - linalg_kernels.matrix_matrix(tmp1, norm_b2_prod, tmp2) - linalg_kernels.matrix_matrix(tmp2, g_inv, tmp1) + # beq * gradPBeq contribution + linalg_kernels.matrix_matrix(beq_prod, norm_b_prod, tmp) + linalg_kernels.matrix_vector(tmp, grad_PBeq, tmp_v) + + filling_v[:] = weight * tmp_v * mu / abs_b_star_para / det_df * ep_scale + + # beq * gradPB contribution + linalg_kernels.matrix_vector(tmp, grad_PB, tmp_v) + + filling_v[:] += weight * tmp_v * mu / abs_b_star_para / det_df * ep_scale + + # beq * dg term contribution + linalg_kernels.matrix_vector(tmp, eta_diff, tmp_v) + + filling_v[:] += tmp_v / abs_b_star_para / det_df * const - linalg_kernels.matrix_vector(tmp1, grad_PB, tmp_v) + # b * gradPBeq contribtuion + linalg_kernels.matrix_matrix(b_prod, norm_b_prod, tmp) + linalg_kernels.matrix_vector(tmp, grad_PBeq, tmp_v) - filling_v[:] = weight * tmp_v * mu / abs_b_star_para / det_df * scale_vec + filling_v[:] += weight * tmp_v * mu / abs_b_star_para / det_df * ep_scale + + # b * gradPB contribution + linalg_kernels.matrix_vector(tmp, grad_PB, tmp_v) + + filling_v[:] += weight * tmp_v * mu / abs_b_star_para / det_df * ep_scale + + # b * dg term contribution + linalg_kernels.matrix_vector(tmp, eta_diff, tmp_v) + + filling_v[:] += tmp_v / abs_b_star_para / det_df * const # call the appropriate matvec filler particle_to_mat_kernels.vec_fill_v2( @@ -856,5 +1224,3 @@ def cc_lin_mhd_5d_J2( vec1 /= Np vec2 /= Np vec3 /= Np - - # -- removed omp: #$ omp end parallel diff --git a/src/struphy/pic/accumulation/filter.py b/src/struphy/pic/accumulation/filter.py new file mode 100644 index 000000000..2c73e3a06 --- /dev/null +++ b/src/struphy/pic/accumulation/filter.py @@ -0,0 +1,185 @@ +from dataclasses import dataclass + +import numpy as np +from scipy.fft import irfft, rfft + +from struphy.feec.psydac_derham import Derham +from struphy.io.options import OptsFilter +from struphy.pic.accumulation.filter_kernels import apply_three_point_filter_3d + + +@dataclass +class FilterParameters: + """Parameters for the AccumFilter class""" + + use_filter: OptsFilter | None = None + modes: tuple[int, ...] = (1,) + repeat: int = 1 + alpha: float = 0.5 + + +class AccumFilter: + """ + Callable filter that applies one of: + - 'fourier_in_tor' + - 'three_point' + - 'hybrid' (three_point, then fourier_in_tor) + """ + + def __init__(self, params: FilterParameters, derham: Derham, space_id: str): + self._params = params if params is not None else FilterParameters() + self._derham = derham + self._space_id = space_id + + self._form = derham.space_to_form[space_id] + self._form_int = 0 if self._form == "v" else int(self._form) + + @property + def params(self) -> FilterParameters: + return self._params + + @property + def derham(self): + """Discrete Derham complex on the logical unit cube.""" + return self._derham + + @property + def space_id(self): + """Space identifier for the matrix/vector (H1, Hcurl, Hdiv, L2 or H1vec) to be accumulated into.""" + return self._space_id + + @property + def form(self): + """p-form("0", "1", "2", "3") to be accumulated into.""" + return self._form + + @property + def form_int(self): + """Integer notation of p-form("0", "1", "2", "3") to be accumulated into.""" + return self._form_int + + def __call__(self, vec): + """ + Apply the chosen filter to `vec` in-place and return it. + + Parameters + ---------- + vec : BlockVector + Accumulated vector object. + """ + use = self.params.use_filter + if use is None: + return vec # nothing to do + + if use == "fourier_in_tor": + self._apply_toroidal_fourier_filter(vec, self._params.modes) + + elif use == "three_point": + self._apply_three_point(vec, repeat=self._params.repeat, alpha=self._params.alpha) + + elif use == "hybrid": + self._apply_three_point(vec, repeat=self._params.repeat, alpha=self._params.alpha) + self._apply_toroidal_fourier_filter(vec, self._params.modes) + + else: + raise NotImplementedError("The type of filter must be 'fourier_in_tor', 'three_point', or 'hybrid'.") + + return vec + + def _yield_dir_components(self, vec): + """ + Yields (axis, comp_vec, starts, ends) for each directions. + - For scalar accumulations ('H1','L2'): yields (0, vec, starts, ends). + - Otherwise: yields (axis, vec[axis], starts, ends) for axis=0,1,2. + """ + if self.space_id in ("H1", "L2"): + starts = self.derham.Vh[self.form].starts + ends = self.derham.Vh[self.form].ends + + yield 0, vec, starts, ends + + else: + for axis in range(3): + starts = self.derham.Vh[self.form][axis].starts + ends = self.derham.Vh[self.form][axis].ends + + yield axis, vec[axis], starts, ends + + def _apply_three_point(self, vec, repeat: int, alpha: float): + """ + Applying three point smoothing filter to the spline coefficients of the accumulated vector (``._data`` of the StencilVector): + + Parameters + ---------- + vec : BlockVector + + repeat : int + Number of repeatition. + + alpha : float + Alpha factor of the smoothing filter. + + """ + + for _ in range(repeat): + for axis, comp, starts, ends in self._yield_dir_components(vec): + apply_three_point_filter_3d( + comp._data, + axis, + self.form_int, + xp.array(self.derham.Nel), + xp.array(self.derham.spl_kind), + xp.array(self.derham.p), + xp.array(starts), + xp.array(ends), + alpha=alpha, + ) + + vec.update_ghost_regions() + + def _apply_toroidal_fourier_filter(self, vec, modes: tuple[int, ...]): + """ + Applying fourier filter to the spline coefficients of the accumulated vector (toroidal direction). + + Parameters + ---------- + vec : BlockVector + + modes : tuple[int, ...] + Mode numbers which are not filtered out. + """ + + tor_Nel = self.derham.Nel[2] + modes = xp.asarray(modes, dtype=int) + + assert tor_Nel >= 2 * int(xp.max(modes)), "Nel[2] must be at least 2*max(modes)" + assert self.derham.domain_decomposition.nprocs[2] == 1, "No domain decomposition along toroidal direction" + + pn = xp.asarray(self.derham.p, dtype=int) + ir = xp.empty(3, dtype=int) + + # rfft output length + if (tor_Nel % 2) == 0: + vec_temp = xp.zeros(int(tor_Nel / 2) + 1, dtype=complex) + else: + vec_temp = xp.zeros(int((tor_Nel - 1) / 2) + 1, dtype=complex) + + for axis, comp, starts, ends in self._yield_dir_components(vec): + for i in range(3): + ir[i] = int(ends[i] + 1 - starts[i]) + + # filter along toroidal index (k direction) + for i in range(ir[0]): + ii = pn[0] + i + for j in range(ir[1]): + jj = pn[1] + j + + # forward FFT along toroidal line + line = rfft(comp._data[ii, jj, pn[2] : pn[2] + ir[2]]) + vec_temp[:] = 0 + vec_temp[modes] = line[modes] # keep selected modes only + + # inverse FFT back to real space, write in-place + comp._data[ii, jj, pn[2] : pn[2] + ir[2]] = irfft(vec_temp, n=tor_Nel) + + vec.update_ghost_regions() diff --git a/src/struphy/pic/accumulation/filter_kernels.py b/src/struphy/pic/accumulation/filter_kernels.py index e24c7ad5d..a6c498ca8 100644 --- a/src/struphy/pic/accumulation/filter_kernels.py +++ b/src/struphy/pic/accumulation/filter_kernels.py @@ -5,8 +5,10 @@ @stack_array("vec_copy", "mask1d", "mask", "top", "i_bottom", "i_top", "fi", "ir") -def apply_three_point_filter( +def apply_three_point_filter_3d( vec: "float[:,:,:]", + dir: "int", + form: "int", Nel: "int[:]", spl_kind: "bool[:]", pn: "int[:]", @@ -47,6 +49,7 @@ def apply_three_point_filter( i_top = zeros(3, dtype=int) fi = empty(3, dtype=int) ir = empty(3, dtype=int) + isDspline = zeros(3, dtype=int) # copy vectors vec_copy[:, :, :] = vec[:, :, :] @@ -62,22 +65,33 @@ def apply_three_point_filter( mask[i, j, k] *= mask1d[i] * mask1d[j] * mask1d[k] # consider left and right boundary + if form == 1: + isDspline[dir] = 1 + elif form == 2: + isDspline[:] = 1 + isDspline[dir] = 0 + elif form == 3: + isDspline[:] = 1 + for i in range(3): if spl_kind[i]: top[i] = Nel[i] - 1 else: - top[i] = Nel[i] + pn[i] - 1 + if isDspline[i] == 1: + top[i] = Nel[i] + pn[i] - 2 + else: + top[i] = Nel[i] + pn[i] - 1 for i in range(3): if starts[i] == 0: if spl_kind[i]: - i_bottom[i] = -1 + i_bottom[i] = 0 else: i_bottom[i] = +1 if ends[i] == top[i]: if spl_kind[i]: - i_top[i] = +1 + i_top[i] = 0 else: i_top[i] = -1 diff --git a/src/struphy/pic/accumulation/particles_to_grid.py b/src/struphy/pic/accumulation/particles_to_grid.py index 02ded30e8..06d67a6df 100644 --- a/src/struphy/pic/accumulation/particles_to_grid.py +++ b/src/struphy/pic/accumulation/particles_to_grid.py @@ -1,18 +1,18 @@ "Base classes for particle deposition (accumulation) on the grid." +import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from psydac.linalg.block import BlockVector from psydac.linalg.stencil import StencilMatrix, StencilVector import struphy.pic.accumulation.accum_kernels as accums import struphy.pic.accumulation.accum_kernels_gc as accums_gc -import struphy.pic.accumulation.filter_kernels as filters from struphy.feec.mass import WeightedMassOperators from struphy.feec.psydac_derham import Derham from struphy.kernel_arguments.pusher_args_kernels import DerhamArguments, DomainArguments +from struphy.pic.accumulation.filter import AccumFilter, FilterParameters from struphy.pic.base import Particles from struphy.profiling.profiling import ProfileManager -from struphy.utils.arrays import xp as np from struphy.utils.pyccel import Pyccelkernel @@ -85,12 +85,7 @@ def __init__( *, add_vector: bool = False, symmetry: str = None, - filter_params: dict = { - "use_filter": None, - "modes": None, - "repeat": None, - "alpha": None, - }, + filter_params: FilterParameters = None, ): self._particles = particles self._space_id = space_id @@ -101,8 +96,6 @@ def __init__( self._symmetry = symmetry - self._filter_params = filter_params - self._form = self.derham.space_to_form[space_id] # initialize matrices (instances of WeightedMassOperator) @@ -179,6 +172,9 @@ def __init__( for bl in vec.blocks: self._args_data += (bl._data,) + # initialize filter + self._accfilter = AccumFilter(filter_params, self._derham, self._space_id) + def __call__(self, *optional_args, **args_control): """ Performs the accumulation into the matrix/vector by calling the chosen accumulation kernel and additional analytical contributions (control variate, optional). @@ -195,7 +191,7 @@ def __call__(self, *optional_args, **args_control): Entries must be pyccel-conform types. args_control : any - Keyword arguments for an analytical control variate correction in the accumulation step. Possible keywords are 'control_vec' for a vector correction or 'control_mat' for a matrix correction. Values are a 1d (vector) or 2d (matrix) list with callables or np.ndarrays used for the correction. + Keyword arguments for an analytical control variate correction in the accumulation step. Possible keywords are 'control_vec' for a vector correction or 'control_mat' for a matrix correction. Values are a 1d (vector) or 2d (matrix) list with callables or xp.ndarrays used for the correction. """ # flags for break @@ -217,52 +213,13 @@ def __call__(self, *optional_args, **args_control): ) # apply filter - if self.filter_params["use_filter"] is not None: + if self.accfilter.params.use_filter is not None: for vec in self._vectors: vec.exchange_assembly_data() vec.update_ghost_regions() - if self.filter_params["use_filter"] == "fourier_in_tor": - self.apply_toroidal_fourier_filter(vec, self.filter_params["modes"]) - - elif self.filter_params["use_filter"] == "three_point": - for _ in range(self.filter_params["repeat"]): - for i in range(3): - filters.apply_three_point_filter( - vec[i]._data, - np.array(self.derham.Nel), - np.array(self.derham.spl_kind), - np.array(self.derham.p), - np.array(self.derham.Vh[self.form][i].starts), - np.array(self.derham.Vh[self.form][i].ends), - alpha=self.filter_params["alpha"], - ) - - vec.update_ghost_regions() - - elif self.filter_params["use_filter"] == "hybrid": - self.apply_toroidal_fourier_filter(vec, self.filter_params["modes"]) - - for _ in range(self.filter_params["repeat"]): - for i in range(2): - filters.apply_three_point_filter( - vec[i]._data, - np.array(self.derham.Nel), - np.array(self.derham.spl_kind), - np.array(self.derham.p), - np.array(self.derham.Vh[self.form][i].starts), - np.array(self.derham.Vh[self.form][i].ends), - alpha=self.filter_params["alpha"], - ) - - vec.update_ghost_regions() - - else: - raise NotImplemented( - "The type of filter must be fourier or three_point.", - ) - - vec_finished = True + self.accfilter(vec) + vec_finished = True if self.particles.clone_config is None: num_clones = 1 @@ -396,14 +353,9 @@ def vectors(self): return out @property - def filter_params(self): - """Dict of three components for the accumulation filter parameters: use_filter(string), repeat(int) and alpha(float).""" - return self._filter_params - - @property - def filter_params(self): - """Dict of three components for the accumulation filter parameters: use_filter(string), repeat(int) and alpha(float).""" - return self._filter_params + def accfilter(self): + """Callable filters""" + return self._accfilter def init_control_variate(self, mass_ops): """Set up the use of noise reduction by control variate.""" @@ -413,55 +365,6 @@ def init_control_variate(self, mass_ops): # L2 projector for dofs self._get_L2dofs = L2Projector(self.space_id, mass_ops).get_dofs - def apply_toroidal_fourier_filter(self, vec, modes): - """ - Applying fourier filter to the spline coefficients of the accumulated vector (toroidal direction). - - Parameters - ---------- - vec : BlockVector - - modes : list - Mode numbers which are not filtered out. - """ - - from scipy.fft import irfft, rfft - - tor_Nel = self.derham.Nel[2] - - # Nel along the toroidal direction must be equal or bigger than 2*maximum mode - assert tor_Nel >= 2 * max(modes) - - pn = self.derham.p - ir = np.empty(3, dtype=int) - - if (tor_Nel % 2) == 0: - vec_temp = np.zeros(int(tor_Nel / 2) + 1, dtype=complex) - else: - vec_temp = np.zeros(int((tor_Nel - 1) / 2) + 1, dtype=complex) - - # no domain decomposition along the toroidal direction - assert self.derham.domain_decomposition.nprocs[2] == 1 - - for axis in range(3): - starts = self.derham.Vh[ſelf.form][axis].starts - ends = self.derham.Vh[self.form][axis].ends - - # index range - for i in range(3): - ir[i] = ends[i] + 1 - starts[i] - - # filtering - for i in range(ir[0]): - for j in range(ir[1]): - vec_temp[:] = 0 - vec_temp[modes] = rfft( - vec[axis]._data[pn[0] + i, pn[1] + j, pn[2] : pn[2] + ir[2]], - )[modes] - vec[axis]._data[pn[0] + i, pn[1] + j, pn[2] : pn[2] + ir[2]] = irfft(vec_temp, n=tor_Nel) - - vec.update_ghost_regions() - def show_accumulated_spline_field(self, mass_ops: WeightedMassOperators, eta_direction=0, component=0): r"""1D plot of the spline field corresponding to the accumulated vector. The latter can be viewed as the rhs of an L2-projection: @@ -485,7 +388,7 @@ def show_accumulated_spline_field(self, mass_ops: WeightedMassOperators, eta_dir field.vector = a # plot field - eta = np.linspace(0, 1, 100) + eta = xp.linspace(0, 1, 100) if eta_direction == 0: args = (eta, 0.5, 0.5) elif eta_direction == 1: @@ -534,6 +437,7 @@ def __init__( kernel: Pyccelkernel, mass_ops: WeightedMassOperators, args_domain: DomainArguments, + filter_params: FilterParameters = None, ): self._particles = particles self._space_id = space_id @@ -587,6 +491,9 @@ def __init__( for bl in vec.blocks: self._args_data += (bl._data,) + # initialize filter + self._accfilter = AccumFilter(filter_params, self._derham, self._space_id) + def __call__(self, *optional_args, **args_control): """ Performs the accumulation into the vector by calling the chosen accumulation kernel @@ -603,7 +510,7 @@ def __call__(self, *optional_args, **args_control): args_control : any Keyword arguments for an analytical control variate correction in the accumulation step. Possible keywords are 'control_vec' for a vector correction or 'control_mat' for a matrix correction. - Values are a 1d (vector) or 2d (matrix) list with callables or np.ndarrays used for the correction. + Values are a 1d (vector) or 2d (matrix) list with callables or xp.ndarrays used for the correction. """ # flags for break @@ -623,6 +530,15 @@ def __call__(self, *optional_args, **args_control): *optional_args, ) + # apply filter + if self.accfilter.params.use_filter is not None: + for vec in self._vectors: + vec.exchange_assembly_data() + vec.update_ghost_regions() + + self.accfilter(vec) + vec_finished = True + if self.particles.clone_config is None: num_clones = 1 else: @@ -692,6 +608,11 @@ def vectors(self): return out + @property + def accfilter(self): + """Callable filters""" + return self._accfilter + def init_control_variate(self, mass_ops): """Set up the use of noise reduction by control variate.""" @@ -723,7 +644,7 @@ def show_accumulated_spline_field(self, mass_ops, eta_direction=0): field.vector = a # plot field - eta = np.linspace(0, 1, 100) + eta = xp.linspace(0, 1, 100) if eta_direction == 0: args = (eta, 0.5, 0.5) elif eta_direction == 1: diff --git a/src/struphy/pic/base.py b/src/struphy/pic/base.py index d8e8cf407..0f92323ae 100644 --- a/src/struphy/pic/base.py +++ b/src/struphy/pic/base.py @@ -14,6 +14,8 @@ class Intracomm: x = None +import cunumpy as xp +from line_profiler import profile from psydac.ddm.mpi import MockComm from psydac.ddm.mpi import mpi as MPI from sympy.ntheory import factorint @@ -25,10 +27,11 @@ class Intracomm: from struphy.fields_background.projected_equils import ProjectedFluidEquilibrium from struphy.geometry.base import Domain from struphy.geometry.utilities import TransformedPformComponent -from struphy.initial import perturbations +from struphy.initial.base import Perturbation +from struphy.io.options import OptsLoading from struphy.io.output_handling import DataContainer from struphy.kernel_arguments.pusher_args_kernels import MarkerArguments -from struphy.kinetic_background import maxwellians +from struphy.kinetic_background.base import KineticBackground, Maxwellian from struphy.pic import sampling_kernels, sobol_seq from struphy.pic.pushing.pusher_utilities_kernels import reflect from struphy.pic.sorting_kernels import ( @@ -45,8 +48,12 @@ class Intracomm: naive_evaluation_flat, naive_evaluation_meshgrid, ) +from struphy.pic.utilities import ( + BoundaryParameters, + LoadingParameters, + WeightsParameters, +) from struphy.utils import utils -from struphy.utils.arrays import xp as np from struphy.utils.clone_config import CloneConfig from struphy.utils.pyccel import Pyccelkernel @@ -79,12 +86,6 @@ class Particles(metaclass=ABCMeta): clone_config : CloneConfig Manages the configuration for clone-based (copied grids) parallel processing using MPI. - Np : int - Number of particles. - - ppc : int - Particles per cell. Cells are defined from ``domain_array``. - domain_decomp : tuple The first entry is a domain_array (see :attr:`~struphy.feec.psydac_derham.Derham.domain_array`) and the second entry is the number of MPI processes in each direction. @@ -93,43 +94,26 @@ class Particles(metaclass=ABCMeta): True if the dimension is to be used in the domain decomposition (=default for each dimension). If mpi_dims_mask[i]=False, the i-th dimension will not be decomposed. - ppb : int - Particles per sorting box. Boxes are defined from ``boxes_per_dim``. - boxes_per_dim : tuple Number of boxes in each logical direction (n_eta1, n_eta2, n_eta3). box_bufsize : float Between 0 and 1, relative buffer size for box array (default = 0.25). - bc : list - Either 'remove', 'reflect', 'periodic' or 'refill' in each direction. - - bc_refill : list - Either 'inner' or 'outer'. - - bc_sph : list - Boundary condition for sph density evaluation. - Either 'periodic', 'mirror', 'static' or 'force' in each direction. - type : str Either 'full_f' (default), 'delta_f' or 'sph'. - control_variate : bool - Whether to use a control variate for noise reduction (only if type is 'full_f' or 'sph'). - name : str Name of particle species. - loading : str - Drawing of markers; either 'pseudo_random', 'sobol_standard', - 'sobol_antithetic', 'external' or 'restart'. + loading_params : LoadingParameters + Parameterts for particle loading. - loading_params : dict - Parameterts for loading, see defaults below. + weights_params : WeightsParameters + Parameters for particle weights. - weights_params : dict - Parameterts for initializing weights, see defaults below. + boundary_params : BoundaryParameters + Parameters for particle boundary conditions. bufsize : float Size of buffer (as multiple of total size, default=.25) in markers array. @@ -143,10 +127,16 @@ class Particles(metaclass=ABCMeta): projected_equil : ProjectedFluidEquilibrium Struphy fluid equilibrium projected into a discrete Derham complex. - bckgr_params : dict - Kinetic background parameters. + background : KineticBackground + Kinetic background. + + initial_condition : KineticBackground + Kinetic initial condition. + + n_as_volume_form: bool + Whether the number density n is given as a volume form or scalar function (=default). - pert_params : dict + perturbations : Perturbation | list Kinetic perturbation parameters. equation_params : dict @@ -160,28 +150,23 @@ def __init__( self, comm_world: Intracomm = None, clone_config: CloneConfig = None, - Np: int = None, - ppc: int = None, domain_decomp: tuple = None, mpi_dims_mask: tuple | list = None, - ppb: int = 10, boxes_per_dim: tuple | list = None, box_bufsize: float = 5.0, - bc: list = None, - bc_refill: str = None, - bc_sph: str = None, type: str = "full_f", - control_variate: bool = False, name: str = "some_name", - loading: str = "pseudo_random", - loading_params: dict = None, - weights_params: dict = None, + loading_params: LoadingParameters = None, + weights_params: WeightsParameters = None, + boundary_params: BoundaryParameters = None, bufsize: float = 0.25, domain: Domain = None, equil: FluidEquilibrium = None, projected_equil: ProjectedFluidEquilibrium = None, - bckgr_params: dict = None, - pert_params: dict = None, + background: KineticBackground | FluidEquilibrium = None, + initial_condition: KineticBackground = None, + perturbations: dict[str, Perturbation] = None, + n_as_volume_form: bool = False, equation_params: dict = None, verbose: bool = False, ): @@ -195,8 +180,21 @@ def __init__( self._num_clones = self.clone_config.num_clones self._clone_id = self.clone_config.clone_id + # defaults + if loading_params is None: + loading_params = LoadingParameters() + + if weights_params is None: + weights_params = WeightsParameters() + + if boundary_params is None: + boundary_params = BoundaryParameters() + # other parameters self._name = name + self._loading_params = loading_params + self._weights_params = weights_params + self._boundary_params = boundary_params self._domain = domain self._equil = equil self._projected_equil = projected_equil @@ -223,9 +221,9 @@ def __init__( self._nprocs = domain_decomp[1] # total number of cells (equal to mpi_size if no grid) - n_cells = np.sum(np.prod(self.domain_array[:, 2::3], axis=1, dtype=int)) * self.num_clones - if verbose: - print(f"{self.mpi_rank = }, {n_cells = }") + n_cells = xp.sum(xp.prod(self.domain_array[:, 2::3], axis=1, dtype=int)) * self.num_clones + # if verbose: + # print(f"\n{self.mpi_rank = }, {n_cells = }") # total number of boxes if self.boxes_per_dim is None: @@ -237,12 +235,15 @@ def __init__( assert all([nboxes % nproc == 0 for nboxes, nproc in zip(self.boxes_per_dim, self.nprocs)]), ( f"Number of boxes {self.boxes_per_dim = } must be divisible by number of processes {self.nprocs = } in each direction." ) - n_boxes = np.prod(self.boxes_per_dim, dtype=int) * self.num_clones + n_boxes = xp.prod(self.boxes_per_dim, dtype=int) * self.num_clones - if verbose: - print(f"{self.mpi_rank = }, {n_boxes = }") + # if verbose: + # print(f"\n{self.mpi_rank = }, {n_boxes = }") # total number of markers (Np) and particles per cell (ppc) + Np = self.loading_params.Np + ppc = self.loading_params.ppc + ppb = self.loading_params.ppb if Np is not None: self._Np = int(Np) self._ppc = self.Np / n_cells @@ -263,6 +264,8 @@ def __init__( self._allocate_marker_array() # boundary conditions + bc = boundary_params.bc + bc_refill = boundary_params.bc_refill if bc is None: bc = ["periodic", "periodic", "periodic"] @@ -281,87 +284,67 @@ def __init__( self._remove_axes = [axis for axis, b_c in enumerate(bc) if b_c == "remove"] self._bc_refill = bc_refill + bc_sph = boundary_params.bc_sph if bc_sph is None: bc_sph = [bci if bci == "periodic" else "mirror" for bci in self.bc] for bci in bc_sph: assert bci in ("periodic", "mirror", "fixed") - self._bc_sph = bc_sph # particle type assert type in ("full_f", "delta_f", "sph") self._type = type - self._control_variate = control_variate # initialize sorting boxes self._verbose = verbose self._initialize_sorting_boxes() # particle loading parameters - assert loading in ( - "pseudo_random", - "sobol_standard", - "sobol_antithetic", - "external", - "restart", - "tesselation", - ) - self._loading = loading - - loading_params_default = { - "seed": None, - "dir_particles": None, - "moments": None, - "spatial": "uniform", - "initial": None, - "n_quad": 1, - } - - self._loading_params = set_defaults( - loading_params, - loading_params_default, - ) - self._spatial = self.loading_params["spatial"] + self._loading = loading_params.loading + self._spatial = loading_params.spatial - # weights parameters - weights_params_default = { - "reject_weights": False, - "threshold": 0.0, - } - - self._weights_params = set_defaults( - weights_params, - weights_params_default, - ) + # weights + self._reject_weights = weights_params.reject_weights + self._threshold = weights_params.threshold + self._control_variate = weights_params.control_variate # background - if bckgr_params is None: - bckgr_params = {"Maxwellian3D": {}, "pforms": [None, None]} + if background is None: + raise ValueError("A background function must be passed to Particles.") + else: + self._background = background - # background p-form description in [eta, v] (None means 0-form, "vol" means volume form -> divide by det) - if isinstance(bckgr_params, FluidEquilibrium): - self._bckgr_params = bckgr_params - self._pforms = [None, None] + # background p-form description in [eta, v] (False means 0-form, True means volume form -> divide by det) + if isinstance(background, FluidEquilibrium): + self._is_volume_form = (n_as_volume_form, False) else: - self._bckgr_params = copy.deepcopy(bckgr_params) - self._pforms = self.bckgr_params.pop("pforms", [None, None]) + self._is_volume_form = ( + n_as_volume_form, + self.background.volume_form, + ) # set background function self._set_background_function() self._set_background_coordinates() - # perturbation parameters - self._pert_params = pert_params + # perturbation parameters (needed for fluid background) + self._perturbations = perturbations + + # initial condition + if initial_condition is None: + self._initial_condition = self.background + else: + self._initial_condition = initial_condition # for loading - if self.loading_params["moments"] is None and self.type != "sph" and isinstance(self.bckgr_params, dict): - self._auto_sampling_params() + # if self.loading_params["moments"] is None and self.type != "sph" and isinstance(self.bckgr_params, dict): + self._generate_sampling_moments() # create buffers for mpi_sort_markers - self._sorting_etas = np.zeros(self.markers.shape, dtype=float) - self._is_on_proc_domain = np.zeros((self.markers.shape[0], 3), dtype=bool) - self._can_stay = np.zeros(self.markers.shape[0], dtype=bool) + self._sorting_etas = xp.zeros(self.markers.shape, dtype=float) + self._is_on_proc_domain = xp.zeros((self.markers.shape[0], 3), dtype=bool) + self._can_stay = xp.zeros(self.markers.shape[0], dtype=bool) self._reqs = [None] * self.mpi_size self._recvbufs = [None] * self.mpi_size self._send_to_i = [None] * self.mpi_size @@ -369,8 +352,8 @@ def __init__( @classmethod @abstractmethod - def default_bckgr_params(cls): - """Dictionary holding the minimal information of the default background.""" + def default_background(cls): + """The default background (of type Maxwellian).""" pass @abstractmethod @@ -467,7 +450,7 @@ def type(self): return self._type @property - def loading(self): + def loading(self) -> OptsLoading: """Type of particle loading.""" return self._loading @@ -543,25 +526,38 @@ def clone_id(self): return self._clone_id @property - def bckgr_params(self): - """Kinetic background parameters.""" - return self._bckgr_params + def background(self) -> KineticBackground: + """Kinetic background.""" + return self._background @property - def pert_params(self): - """Kinetic perturbation parameters.""" - return self._pert_params + def perturbations(self) -> dict[str, Perturbation]: + """Kinetic perturbations, keys are the names of moments of the distribution function ("n", "u1", etc.).""" + return self._perturbations @property - def loading_params(self): - """Parameters for marker loading.""" + def loading_params(self) -> LoadingParameters: return self._loading_params @property - def weights_params(self): - """Parameters for initializing weights.""" + def weights_params(self) -> WeightsParameters: return self._weights_params + @property + def boundary_params(self) -> BoundaryParameters: + """Parameters for marker loading.""" + return self._boundary_params + + @property + def reject_weights(self): + """Whether to reect weights below threshold.""" + return self._reject_weights + + @property + def threshold(self): + """Threshold for rejecting weights.""" + return self._threshold + @property def boxes_per_dim(self): """Tuple, number of sorting boxes per dimension.""" @@ -577,6 +573,11 @@ def equation_params(self): """Parameters appearing in model equation due to Struphy normalization.""" return self._equation_params + @property + def initial_condition(self) -> KineticBackground: + """Kinetic initial condition""" + return self._initial_condition + @property def f_init(self): """Callable initial condition (background + perturbation). @@ -597,7 +598,7 @@ def u_init(self): return self._u_init @property - def f0(self): + def f0(self) -> Maxwellian: assert hasattr(self, "_f0"), AttributeError( "No background distribution available, please run self._set_background_function()", ) @@ -725,16 +726,16 @@ def index(self): def valid_mks(self): """Array of booleans stating if an entry in the markers array is a true local particle (not a hole or ghost).""" if not hasattr(self, "_valid_mks"): - self._valid_mks = ~np.logical_or(self.holes, self.ghost_particles) + self._valid_mks = ~xp.logical_or(self.holes, self.ghost_particles) return self._valid_mks def update_valid_mks(self): - self._valid_mks[:] = ~np.logical_or(self.holes, self.ghost_particles) + self._valid_mks[:] = ~xp.logical_or(self.holes, self.ghost_particles) @property def n_mks_loc(self): """Number of valid markers on process (without holes and ghosts).""" - return np.count_nonzero(self.valid_mks) + return xp.count_nonzero(self.valid_mks) @property def n_mks_on_each_proc(self): @@ -744,7 +745,7 @@ def n_mks_on_each_proc(self): @property def n_mks_on_clone(self): """Number of valid markers on current clone (without holes and ghosts).""" - return np.sum(self.n_mks_on_each_proc) + return xp.sum(self.n_mks_on_each_proc) @property def n_mks_on_each_clone(self): @@ -754,7 +755,7 @@ def n_mks_on_each_clone(self): @property def n_mks_global(self): """Number of valid markers on current clone (without holes and ghosts).""" - return np.sum(self.n_mks_on_each_clone) + return xp.sum(self.n_mks_on_each_clone) @property def positions(self): @@ -763,7 +764,7 @@ def positions(self): @positions.setter def positions(self, new): - assert isinstance(new, np.ndarray) + assert isinstance(new, xp.ndarray) assert new.shape == (self.n_mks_loc, 3) self._markers[self.valid_mks, self.index["pos"]] = new @@ -774,7 +775,7 @@ def velocities(self): @velocities.setter def velocities(self, new): - assert isinstance(new, np.ndarray) + assert isinstance(new, xp.ndarray) assert new.shape == (self.n_mks_loc, self.vdim), f"{self.n_mks_loc = } and {self.vdim = } but {new.shape = }" self._markers[self.valid_mks, self.index["vel"]] = new @@ -785,7 +786,7 @@ def phasespace_coords(self): @phasespace_coords.setter def phasespace_coords(self, new): - assert isinstance(new, np.ndarray) + assert isinstance(new, xp.ndarray) assert new.shape == (self.n_mks_loc, 3 + self.vdim) self._markers[self.valid_mks, self.index["coords"]] = new @@ -796,7 +797,7 @@ def weights(self): @weights.setter def weights(self, new): - assert isinstance(new, np.ndarray) + assert isinstance(new, xp.ndarray) assert new.shape == (self.n_mks_loc,) self._markers[self.valid_mks, self.index["weights"]] = new @@ -807,7 +808,7 @@ def sampling_density(self): @sampling_density.setter def sampling_density(self, new): - assert isinstance(new, np.ndarray) + assert isinstance(new, xp.ndarray) assert new.shape == (self.n_mks_loc,) self._markers[self.valid_mks, self.index["s0"]] = new @@ -818,7 +819,7 @@ def weights0(self): @weights0.setter def weights0(self, new): - assert isinstance(new, np.ndarray) + assert isinstance(new, xp.ndarray) assert new.shape == (self.n_mks_loc,) self._markers[self.valid_mks, self.index["w0"]] = new @@ -829,16 +830,14 @@ def marker_ids(self): @marker_ids.setter def marker_ids(self, new): - assert isinstance(new, np.ndarray) + assert isinstance(new, xp.ndarray) assert new.shape == (self.n_mks_loc,) self._markers[self.valid_mks, self.index["ids"]] = new @property - def pforms(self): - """Tuple of size 2; each entry must be either "vol" or None, defining the p-form - (space and velocity, respectively) of f_init. - """ - return self._pforms + def is_volume_form(self): + """Tuple of size 2 for (position, velocity), defining the p-form representation of f_init: True means volume-form, False means 0-form.""" + return self._is_volume_form @property def spatial(self): @@ -862,7 +861,7 @@ def f_coords(self): @f_coords.setter def f_coords(self, new): - assert isinstance(new, np.ndarray) + assert isinstance(new, xp.ndarray) self.markers[self.valid_mks, self.f_coords_index] = new @property @@ -874,16 +873,16 @@ def args_markers(self): def f_jacobian_coords(self): """Coordinates of the velocity jacobian determinant of the distribution fuction.""" if isinstance(self.f_jacobian_coords_index, list): - return self.markers[np.ix_(~self.holes, self.f_jacobian_coords_index)] + return self.markers[xp.ix_(~self.holes, self.f_jacobian_coords_index)] else: return self.markers[~self.holes, self.f_jacobian_coords_index] @f_jacobian_coords.setter def f_jacobian_coords(self, new): - assert isinstance(new, np.ndarray) + assert isinstance(new, xp.ndarray) if isinstance(self.f_jacobian_coords_index, list): self.markers[ - np.ix_( + xp.ix_( ~self.holes, self.f_jacobian_coords_index, ) @@ -930,7 +929,7 @@ def _get_domain_decomp(self, mpi_dims_mask: tuple | list = None): Returns ------- - dom_arr : np.ndarray + dom_arr : xp.ndarray A 2d array of shape (#MPI processes, 9). The row index denotes the process rank. The columns are for n=0,1,2: - arr[i, 3*n + 0] holds the LEFT domain boundary of process i in direction eta_(n+1). - arr[i, 3*n + 1] holds the RIGHT domain boundary of process i in direction eta_(n+1). @@ -942,7 +941,7 @@ def _get_domain_decomp(self, mpi_dims_mask: tuple | list = None): if mpi_dims_mask is None: mpi_dims_mask = [True, True, True] - dom_arr = np.zeros((self.mpi_size, 9), dtype=float) + dom_arr = xp.zeros((self.mpi_size, 9), dtype=float) # factorize mpi size factors = factorint(self.mpi_size) @@ -966,10 +965,10 @@ def _get_domain_decomp(self, mpi_dims_mask: tuple | list = None): mm = (mm + 1) % 3 nprocs[mm] *= fac - assert np.prod(nprocs) == self.mpi_size + assert xp.prod(nprocs) == self.mpi_size # domain decomposition - breaks = [np.linspace(0.0, 1.0, nproc + 1) for nproc in nprocs] + breaks = [xp.linspace(0.0, 1.0, nproc + 1) for nproc in nprocs] # fill domain array for n in range(self.mpi_size): @@ -992,38 +991,35 @@ def _get_domain_decomp(self, mpi_dims_mask: tuple | list = None): return dom_arr, tuple(nprocs) def _set_background_function(self): - self._f0 = None - if isinstance(self.bckgr_params, FluidEquilibrium): - self._f0 = self.bckgr_params - else: - for fi, maxw_params in self.bckgr_params.items(): - if fi[-2] == "_": - fi_type = fi[:-2] - else: - fi_type = fi - - # SPH case: f0 is set to a FluidEquilibrium - if self.type == "sph": - _eq = getattr(equils, fi_type)(**maxw_params) - if not isinstance(_eq, NumericalFluidEquilibrium): - _eq.domain = self.domain - if self._f0 is None: - self._f0 = _eq - else: - raise NotImplementedError("Summation of fluid backgrounds not yet implemented.") - # self._f0 = self._f0 + (lambda e1, e2, e3: _eq.n0(e1, e2, e3)) - # default case - else: - if self._f0 is None: - self._f0 = getattr(maxwellians, fi_type)( - maxw_params=maxw_params, - equil=self.equil, - ) - else: - self._f0 = self._f0 + getattr(maxwellians, fi_type)( - maxw_params=maxw_params, - equil=self.equil, - ) + self._f0 = self.background + + # if isinstance(self.background, FluidEquilibrium): + # self._f0 = self.background + # else: + # self._f0 = copy.deepcopy(self.background) + # self.f0.add_perturbation = False + + # self._f0 = None + # if isinstance(self.bckgr_params, FluidEquilibrium): + # self._f0 = self.bckgr_params + # else: + # for bckgr in self.backgrounds: + # # SPH case: f0 is set to a FluidEquilibrium + # if self.type == "sph": + # _eq = getattr(equils, fi_type)(**maxw_params) + # if not isinstance(_eq, NumericalFluidEquilibrium): + # _eq.domain = self.domain + # if self._f0 is None: + # self._f0 = _eq + # else: + # raise NotImplementedError("Summation of fluid backgrounds not yet implemented.") + # # self._f0 = self._f0 + (lambda e1, e2, e3: _eq.n0(e1, e2, e3)) + # # default case + # else: + # if self._f0 is None: + # self._f0 = bckgr + # else: + # self._f0 = self._f0 + bckgr def _set_background_coordinates(self): if self.type != "sph" and self.f0.coords == "constants_of_motion": @@ -1061,14 +1057,14 @@ def _n_mks_load_and_Np_per_clone(self): """Return two arrays: 1) an array of sub_comm.size where the i-th entry corresponds to the number of markers drawn on process i, and 2) an array of size num_clones where the i-th entry corresponds to the number of markers on clone i.""" # number of cells on current process - n_cells_loc = np.prod( + n_cells_loc = xp.prod( self.domain_array[self.mpi_rank, 2::3], dtype=int, ) # array of number of markers on each process at loading stage if self.clone_config is not None: - _n_cells_clone = np.sum(np.prod(self.domain_array[:, 2::3], axis=1, dtype=int)) + _n_cells_clone = xp.sum(xp.prod(self.domain_array[:, 2::3], axis=1, dtype=int)) _n_mks_load_tot = self.clone_config.get_Np_clone(self.Np) _ppc = _n_mks_load_tot / _n_cells_clone else: @@ -1078,14 +1074,14 @@ def _n_mks_load_and_Np_per_clone(self): n_mks_load = self._gather_scalar_in_subcomm_array(int(_ppc * n_cells_loc)) # add deviation from Np to rank 0 - n_mks_load[0] += _n_mks_load_tot - np.sum(n_mks_load) + n_mks_load[0] += _n_mks_load_tot - xp.sum(n_mks_load) # check if all markers are there - assert np.sum(n_mks_load) == _n_mks_load_tot + assert xp.sum(n_mks_load) == _n_mks_load_tot # Np on each clone Np_per_clone = self._gather_scalar_in_intercomm_array(_n_mks_load_tot) - assert np.sum(Np_per_clone) == self.Np + assert xp.sum(Np_per_clone) == self.Np return n_mks_load, Np_per_clone @@ -1096,23 +1092,23 @@ def _allocate_marker_array(self): # number of markers on the local process at loading stage n_mks_load_loc = self.n_mks_load[self._mpi_rank] - bufsize = self.bufsize + 1.0 / np.sqrt(n_mks_load_loc) + bufsize = self.bufsize + 1.0 / xp.sqrt(n_mks_load_loc) # allocate markers array (3 x positions, vdim x velocities, weight, s0, w0, ..., ID) with buffer self._n_rows = round(n_mks_load_loc * (1 + bufsize)) - self._markers = np.zeros((self.n_rows, self.n_cols), dtype=float) + self._markers = xp.zeros((self.n_rows, self.n_cols), dtype=float) # allocate auxiliary arrays - self._holes = np.zeros(self.n_rows, dtype=bool) - self._ghost_particles = np.zeros(self.n_rows, dtype=bool) - self._valid_mks = np.zeros(self.n_rows, dtype=bool) - self._is_outside_right = np.zeros(self.n_rows, dtype=bool) - self._is_outside_left = np.zeros(self.n_rows, dtype=bool) - self._is_outside = np.zeros(self.n_rows, dtype=bool) + self._holes = xp.zeros(self.n_rows, dtype=bool) + self._ghost_particles = xp.zeros(self.n_rows, dtype=bool) + self._valid_mks = xp.zeros(self.n_rows, dtype=bool) + self._is_outside_right = xp.zeros(self.n_rows, dtype=bool) + self._is_outside_left = xp.zeros(self.n_rows, dtype=bool) + self._is_outside = xp.zeros(self.n_rows, dtype=bool) # create array container (3 x positions, vdim x velocities, weight, s0, w0, ID) for removed markers self._n_lost_markers = 0 - self._lost_markers = np.zeros((int(self.n_rows * 0.5), 10), dtype=float) + self._lost_markers = xp.zeros((int(self.n_rows * 0.5), 10), dtype=float) # arguments for kernels self._args_markers = MarkerArguments( @@ -1171,7 +1167,7 @@ def _initialize_sorting_boxes(self): bc_sph=self.bc_sph, is_domain_boundary=is_domain_boundary, comm=self.mpi_comm, - verbose=self.verbose, + verbose=False, box_bufsize=self._box_bufsize, ) @@ -1183,102 +1179,144 @@ def _initialize_sorting_boxes(self): else: self._sorting_boxes = None - def _auto_sampling_params(self): - """Automatically determine sampling parameters from the background given""" - ns = [] - us = [] - vths = [] + def _generate_sampling_moments(self): + """Automatically determine moments for sampling distribution (Gaussian) from the given background.""" - for fi, params in self.bckgr_params.items(): - if fi[-2] == "_": - fi_type = fi[:-2] - else: - fi_type = fi + if self.loading_params.moments is None: + self.loading_params.moments = tuple([0.0] * self.vdim + [1.0] * self.vdim) - us.append([]) - vths.append([]) + # TODO: reformulate this function with KineticBackground methods - bckgr = getattr(maxwellians, fi_type) - default_maxw_params = bckgr.default_maxw_params() + # ns = [] + # us = [] + # vths = [] - for key in default_maxw_params: - if key[0] == "n": - if key in params: - ns += [params[key]] - else: - ns += [1.0] + # for fi, params in self.bckgr_params.items(): + # if fi[-2] == "_": + # fi_type = fi[:-2] + # else: + # fi_type = fi - elif key[0] == "u": - if key in params: - us[-1] += [params[key]] - else: - us[-1] += [0.0] + # us.append([]) + # vths.append([]) - elif key[0] == "v": - if key in params: - vths[-1] += [params[key]] - else: - vths[-1] += [1.0] + # bckgr = getattr(maxwellians, fi_type) + + # for key in default_maxw_params: + # if key[0] == "n": + # if key in params: + # ns += [params[key]] + # else: + # ns += [1.0] + + # elif key[0] == "u": + # if key in params: + # us[-1] += [params[key]] + # else: + # us[-1] += [0.0] - assert len(ns) == len(us) == len(vths) + # elif key[0] == "v": + # if key in params: + # vths[-1] += [params[key]] + # else: + # vths[-1] += [1.0] - ns = np.array(ns) - us = np.array(us) - vths = np.array(vths) + # assert len(ns) == len(us) == len(vths) + + # ns = xp.array(ns) + # us = xp.array(us) + # vths = xp.array(vths) # Use the mean of shifts and thermal velocity such that outermost shift+thermal is # new shift + new thermal - mean_us = np.mean(us, axis=0) - us_ext = us + vths * np.where(us >= 0, 1, -1) - us_ext_dist = us_ext - mean_us[None, :] - new_vths = np.max(np.abs(us_ext_dist), axis=0) + # mean_us = xp.mean(us, axis=0) + # us_ext = us + vths * xp.where(us >= 0, 1, -1) + # us_ext_dist = us_ext - mean_us[None, :] + # new_vths = xp.max(xp.abs(us_ext_dist), axis=0) - new_moments = [] + # new_moments = [] - new_moments += [*mean_us] - new_moments += [*new_vths] - new_moments = [float(moment) for moment in new_moments] + # new_moments += [*mean_us] + # new_moments += [*new_vths] + # new_moments = [float(moment) for moment in new_moments] - self.loading_params["moments"] = new_moments + # self.loading_params["moments"] = new_moments - def _set_initial_condition(self, bp_copy=None, pp_copy=None): - """Compute callable initial condition from background + perturbation.""" + def _set_initial_condition(self): + if self.type != "sph": + self._f_init = self.initial_condition + else: + # Get the initialization function and pass the correct arguments + assert isinstance(self.f0, FluidEquilibrium) + self._u_init = self.f0.u_cart + + if self.perturbations is not None: + for ( + moment, + pert, + ) in self.perturbations.items(): # only one perturbation is taken into account at the moment + assert isinstance(moment, str) + if pert is None: + continue + assert isinstance(pert, Perturbation) + + if moment == "n": + _fun = TransformedPformComponent( + pert, + pert.given_in_basis, + "0", + comp=pert.comp, + domain=self.domain, + ) + elif moment == "u1": + _fun = TransformedPformComponent( + pert, + pert.given_in_basis, + "v", + comp=pert.comp, + domain=self.domain, + ) + _fun_cart = lambda e1, e2, e3: self.domain.push(_fun, e1, e2, e3, kind="v") + self._u_init = lambda e1, e2, e3: self.f0.u_cart(e1, e2, e3)[0] + _fun_cart(e1, e2, e3) + # TODO: add other velocity components + else: + _fun = None - if bp_copy is None: - bp_copy = copy.deepcopy(self.bckgr_params) - if pp_copy is None: - pp_copy = copy.deepcopy(self.pert_params) + def _f_init(*etas, flat_eval=False): + if len(etas) == 1: + if _fun is None: + out = self.f0.n0(etas[0]) + else: + out = self.f0.n0(etas[0]) + _fun(*etas[0].T) + else: + assert len(etas) == 3 + E1, E2, E3, is_sparse_meshgrid = Domain.prepare_eval_pts( + etas[0], + etas[1], + etas[2], + flat_eval=flat_eval, + ) - # Get the initialization function and pass the correct arguments - self._f_init = None - for fi, maxw_params in bp_copy.items(): - if fi[-2] == "_": - fi_type = fi[:-2] - else: - fi_type = fi - - pert_params = pp_copy - if pp_copy is not None: - if fi in pp_copy: - pert_params = pp_copy[fi] - - if self._f_init is None: - self._f_init = getattr(maxwellians, fi_type)( - maxw_params=maxw_params, - pert_params=pert_params, - equil=self.equil, - ) - else: - self._f_init = self._f_init + getattr(maxwellians, fi_type)( - maxw_params=maxw_params, - pert_params=pert_params, - equil=self.equil, - ) + out0 = self.f0.n0(E1, E2, E3) + + if _fun is None: + out = out0 + else: + out1 = _fun(E1, E2, E3) + assert out0.shape == out1.shape + out = out0 + out1 + + if flat_eval: + out = xp.squeeze(out) + + return out + + self._f_init = _f_init def _load_external( self, n_mks_load_loc: int, - n_mks_load_cum_sum: np.ndarray, + n_mks_load_cum_sum: xp.ndarray, ): """Load markers from external .hdf5 file. @@ -1287,12 +1325,12 @@ def _load_external( n_mks_load_loc: int Number of markers on the local process at loading stage. - n_mks_load_cum_sum: np.ndarray + n_mks_load_cum_sum: xp.ndarray Cumulative sum of number of markers on each process at loading stage. """ if self.mpi_rank == 0: file = h5py.File( - self.loading_params["dir_external"], + self.loading_params.dir_external, "r", ) print(f"\nLoading markers from file: {file}") @@ -1311,7 +1349,7 @@ def _load_external( file.close() else: - recvbuf = np.zeros( + recvbuf = xp.zeros( (n_mks_load_loc, self.markers.shape[1]), dtype=float, ) @@ -1325,16 +1363,16 @@ def _load_restart(self): o_path = state["o_path"] - if self.loading_params["dir_particles_abs"] is None: + if self.loading_params.dir_particles_abs is None: data_path = os.path.join( o_path, - self.loading_params["dir_particles"], + self.loading_params.dir_particles, ) else: - data_path = self.loading_params["dir_particles_abs"] + data_path = self.loading_params.dir_particles_abs data = DataContainer(data_path, comm=self.mpi_comm) - self._markers[:, :] = data.file["restart/" + self.loading_params["key"]][-1, :, :] + self._markers[:, :] = data.file["restart/" + self.loading_params.restart_key][-1, :, :] def _load_tesselation(self, n_quad: int = 1): """ @@ -1453,8 +1491,8 @@ def draw_markers( self.update_ghost_particles() # cumulative sum of number of markers on each process at loading stage. - n_mks_load_cum_sum = np.cumsum(self.n_mks_load) - Np_per_clone_cum_sum = np.cumsum(self.Np_per_clone) + n_mks_load_cum_sum = xp.cumsum(self.n_mks_load) + Np_per_clone_cum_sum = xp.cumsum(self.Np_per_clone) _first_marker_id = (Np_per_clone_cum_sum - self.Np_per_clone)[self.clone_id] + ( n_mks_load_cum_sum - self.n_mks_load )[self._mpi_rank] @@ -1482,21 +1520,21 @@ def draw_markers( self._load_tesselation() if self.type == "sph": self._set_initial_condition() - self.velocities = np.array(self.u_init(self.positions)[0]).T + self.velocities = xp.array(self.u_init(self.positions)[0]).T # set markers ID in last column - self.marker_ids = _first_marker_id + np.arange(n_mks_load_loc, dtype=float) + self.marker_ids = _first_marker_id + xp.arange(n_mks_load_loc, dtype=float) else: if self.mpi_rank == 0 and verbose: print("\nLoading fresh markers:") - for key, val in self.loading_params.items(): + for key, val in self.loading_params.__dict__.items(): print((key + " :").ljust(25), val) # 1. standard random number generator (pseudo-random) if self.loading == "pseudo_random": # set seed - _seed = self.loading_params["seed"] + _seed = self.loading_params.seed if _seed is not None: - np.random.seed(_seed) + xp.random.seed(_seed) # counting integers num_loaded_particles_loc = 0 # number of particles alreday loaded (local) @@ -1507,15 +1545,15 @@ def draw_markers( while num_loaded_particles_glob < int(self.Np): # Generate a chunk of random particles num_to_add_glob = min(chunk_size, int(self.Np) - num_loaded_particles_glob) - temp = np.random.rand(num_to_add_glob, 3 + self.vdim) + temp = xp.random.rand(num_to_add_glob, 3 + self.vdim) # check which particles are on the current process domain - is_on_proc_domain = np.logical_and( + is_on_proc_domain = xp.logical_and( temp[:, :3] > self.domain_array[self.mpi_rank, 0::3], temp[:, :3] < self.domain_array[self.mpi_rank, 1::3], ) - valid_idx = np.nonzero(np.all(is_on_proc_domain, axis=1))[0] + valid_idx = xp.nonzero(xp.all(is_on_proc_domain, axis=1))[0] valid_particles = temp[valid_idx] - valid_particles = np.array_split(valid_particles, self.num_clones)[self.clone_id] + valid_particles = xp.array_split(valid_particles, self.num_clones)[self.clone_id] num_valid = valid_particles.shape[0] # Add the valid particles to the phasespace_coords array @@ -1532,7 +1570,7 @@ def draw_markers( # set new n_mks_load self._gather_scalar_in_subcomm_array(num_loaded_particles_loc, out=self.n_mks_load) n_mks_load_loc = self.n_mks_load[self.mpi_rank] - n_mks_load_cum_sum = np.cumsum(self.n_mks_load) + n_mks_load_cum_sum = xp.cumsum(self.n_mks_load) # set new holes in markers array to -1 self._markers[num_loaded_particles_loc:] = -1.0 @@ -1572,11 +1610,11 @@ def draw_markers( # initial velocities - SPH case: v(0) = u(x(0)) for given velocity u(x) if self.type == "sph": self._set_initial_condition() - self.velocities = np.array(self.u_init(self.positions)[0]).T + self.velocities = xp.array(self.u_init(self.positions)[0]).T else: # inverse transform sampling in velocity space - u_mean = np.array(self.loading_params["moments"][: self.vdim]) - v_th = np.array(self.loading_params["moments"][self.vdim :]) + u_mean = xp.array(self.loading_params.moments[: self.vdim]) + v_th = xp.array(self.loading_params.moments[self.vdim :]) # Particles6D: (1d Maxwellian, 1d Maxwellian, 1d Maxwellian) if self.vdim == 3: @@ -1584,7 +1622,7 @@ def draw_markers( sp.erfinv( 2 * self.velocities - 1, ) - * np.sqrt(2) + * xp.sqrt(2) * v_th + u_mean ) @@ -1594,16 +1632,16 @@ def draw_markers( sp.erfinv( 2 * self.velocities[:, 0] - 1, ) - * np.sqrt(2) + * xp.sqrt(2) * v_th[0] + u_mean[0] ) self._markers[:n_mks_load_loc, 4] = ( - np.sqrt( - -1 * np.log(1 - self.velocities[:, 1]), + xp.sqrt( + -1 * xp.log(1 - self.velocities[:, 1]), ) - * np.sqrt(2) + * xp.sqrt(2) * v_th[1] + u_mean[1] ) @@ -1616,17 +1654,17 @@ def draw_markers( # inversion method for drawing uniformly on the disc if self.spatial == "disc": - self._markers[:n_mks_load_loc, 0] = np.sqrt( + self._markers[:n_mks_load_loc, 0] = xp.sqrt( self._markers[:n_mks_load_loc, 0], ) else: assert self.spatial == "uniform", f'Spatial drawing must be "uniform" or "disc", is {self.spatial}.' - self.marker_ids = _first_marker_id + np.arange(n_mks_load_loc, dtype=float) + self.marker_ids = _first_marker_id + xp.arange(n_mks_load_loc, dtype=float) # set specific initial condition for some particles - if self.loading_params["initial"] is not None: - specific_markers = self.loading_params["initial"] + if self.loading_params.specific_markers is not None: + specific_markers = self.loading_params.specific_markers counter = 0 for i in range(len(specific_markers)): @@ -1643,8 +1681,8 @@ def draw_markers( # check if all particle positions are inside the unit cube [0, 1]^3 n_mks_load_loc = self.n_mks_load[self._mpi_rank] - assert np.all(~self.holes[:n_mks_load_loc]) - assert np.all(self.holes[n_mks_load_loc:]) + assert xp.all(~self.holes[:n_mks_load_loc]) + assert xp.all(self.holes[n_mks_load_loc:]) if self._initialized_sorting and sort: if self.mpi_rank == 0 and verbose: @@ -1653,6 +1691,7 @@ def draw_markers( self.mpi_sort_markers() self.do_sort() + @profile def mpi_sort_markers( self, apply_bc: bool = True, @@ -1716,8 +1755,8 @@ def mpi_sort_markers( # check if all markers are on the right process after sorting if do_test: - all_on_right_proc = np.all( - np.logical_and( + all_on_right_proc = xp.all( + xp.logical_and( self.positions > self.domain_array[self.mpi_rank, 0::3], self.positions < self.domain_array[self.mpi_rank, 1::3], ), @@ -1733,8 +1772,8 @@ def initialize_weights( *, bckgr_params: dict = None, pert_params: dict = None, - reject_weights: bool = False, - threshold: float = 1e-8, + # reject_weights: bool = False, + # threshold: float = 1e-8, ): r""" Computes the initial weights @@ -1755,20 +1794,14 @@ def initialize_weights( pert_params : dict Kinetic perturbation parameters for initial condition. - - reject_weights : bool - Whether to use ``threshold`` for rejecting weights. - - threshold : float - Minimal value of a weight; below the marker is set to a hole.les. """ if self.loading == "tesselation": - if self.pforms[0] is None: + if not self.is_volume_form[0]: fvol = TransformedPformComponent([self.f_init], "0", "3", domain=self.domain) else: fvol = self.f_init - cell_avg = self.tesselation.cell_averages(fvol, n_quad=self.loading_params["n_quad"]) + cell_avg = self.tesselation.cell_averages(fvol, n_quad=self.loading_params.n_quad) self.weights0 = cell_avg.flatten() else: assert self.domain is not None, "A domain is needed to initialize weights." @@ -1790,10 +1823,10 @@ def initialize_weights( f_init = self.f_init(*self.f_coords.T) # if f_init is vol-form, transform to 0-form - if self.pforms[0] == "vol": + if self.is_volume_form[0]: f_init /= self.domain.jacobian_det(self.positions) - if self.pforms[1] == "vol": + if self.is_volume_form[1]: f_init /= self.f_init.velocity_jacobian_det( *self.f_jacobian_coords.T, ) @@ -1804,13 +1837,13 @@ def initialize_weights( # compute w0 and save at vdim + 5 self.weights0 = f_init / self.sampling_density - if reject_weights: - reject = self.markers[:, self.index["w0"]] < threshold + if self.reject_weights: + reject = self.markers[:, self.index["w0"]] < self.threshold self._markers[reject] = -1.0 self.update_holes() self.reset_marker_ids() print( - f"\nWeights < {threshold} have been rejected, number of valid markers on process {self.mpi_rank} is {self.n_mks_loc}." + f"\nWeights < {self.threshold} have been rejected, number of valid markers on process {self.mpi_rank} is {self.n_mks_loc}." ) # compute (time-dependent) weights at vdim + 3 @@ -1819,6 +1852,7 @@ def initialize_weights( else: self.weights = self.weights0 + @profile def update_weights(self): """ Applies the control variate method, i.e. updates the time-dependent marker weights @@ -1835,10 +1869,10 @@ def update_weights(self): f0 = self.f0(*self.f_coords.T) # if f_init is vol-form, transform to 0-form - if self.pforms[0] == "vol": + if self.is_volume_form[0]: f0 /= self.domain.jacobian_det(self.positions) - if self.pforms[1] == "vol": + if self.is_volume_form[1]: f0 /= self.f0.velocity_jacobian_det(*self.f_jacobian_coords.T) self.weights = self.weights0 - f0 / self.sampling_density @@ -1846,23 +1880,29 @@ def update_weights(self): def reset_marker_ids(self): """Reset the marker ids (last column in marker array) according to the current distribution of particles. The first marker on rank 0 gets the id '0', the last marker on the last rank gets the id 'n_mks_global - 1'.""" - n_mks_proc_cumsum = np.cumsum(self.n_mks_on_each_proc) - n_mks_clone_cumsum = np.cumsum(self.n_mks_on_each_clone) + n_mks_proc_cumsum = xp.cumsum(self.n_mks_on_each_proc) + n_mks_clone_cumsum = xp.cumsum(self.n_mks_on_each_clone) first_marker_id = (n_mks_clone_cumsum - self.n_mks_on_each_clone)[self.clone_id] + ( n_mks_proc_cumsum - self.n_mks_on_each_proc )[self.mpi_rank] - self.marker_ids = first_marker_id + np.arange(self.n_mks_loc, dtype=int) + self.marker_ids = first_marker_id + xp.arange(self.n_mks_loc, dtype=int) - def binning(self, components, bin_edges, divide_by_jac=True): + @profile + def binning( + self, + components: tuple[bool], + bin_edges: tuple[xp.ndarray], + divide_by_jac: bool = True, + ): r"""Computes full-f and delta-f distribution functions via marker binning in logical space. Numpy's histogramdd is used, following the algorithm outlined in :ref:`binning`. Parameters ---------- - components : list[bool] + components : tuple[bool] List of length 3 + vdim; an entry is True if the direction in phase space is to be binned. - bin_edges : list[array] + bin_edges : tuple[array] List of bin edges (resolution) having the length of True entries in components. divide_by_jac : boll @@ -1877,7 +1917,7 @@ def binning(self, components, bin_edges, divide_by_jac=True): The reconstructed delta-f distribution function. """ - assert np.count_nonzero(components) == len(bin_edges) + assert xp.count_nonzero(components) == len(bin_edges) # volume of a bin bin_vol = 1.0 @@ -1899,13 +1939,13 @@ def binning(self, components, bin_edges, divide_by_jac=True): _weights0 /= self.domain.jacobian_det(self.positions, remove_outside=False) # _weights0 /= self.velocity_jacobian_det(*self.phasespace_coords.T) - f_slice = np.histogramdd( + f_slice = xp.histogramdd( self.markers_wo_holes_and_ghost[:, slicing], bins=bin_edges, weights=_weights0, )[0] - df_slice = np.histogramdd( + df_slice = xp.histogramdd( self.markers_wo_holes_and_ghost[:, slicing], bins=bin_edges, weights=_weights, @@ -1932,7 +1972,7 @@ def show_distribution_function(self, components, bin_edges): import matplotlib.pyplot as plt - n_dim = np.count_nonzero(components) + n_dim = xp.count_nonzero(components) assert n_dim == 1 or n_dim == 2, f"Distribution function can only be shown in 1D or 2D slices, not {n_dim}." @@ -1948,7 +1988,7 @@ def show_distribution_function(self, components, bin_edges): 4: "$v_2$", 5: "$v_3$", } - indices = np.nonzero(components)[0] + indices = xp.nonzero(components)[0] if n_dim == 1: plt.plot(bin_centers[0], f_slice) @@ -1972,16 +2012,17 @@ def _find_outside_particles(self, axis): self._is_outside_left[self.holes] = False self._is_outside_left[self.ghost_particles] = False - self._is_outside[:] = np.logical_or( + self._is_outside[:] = xp.logical_or( self._is_outside_right, self._is_outside_left, ) # indices or particles that are outside of the logical unit cube - outside_inds = np.nonzero(self._is_outside)[0] + outside_inds = xp.nonzero(self._is_outside)[0] return outside_inds + @profile def apply_kinetic_bc(self, newton=False): """ Apply boundary conditions to markers that are outside of the logical unit cube. @@ -2004,7 +2045,7 @@ def apply_kinetic_bc(self, newton=False): self.particle_refilling() self._markers[self._is_outside, :-1] = -1.0 - self._n_lost_markers += len(np.nonzero(self._is_outside)[0]) + self._n_lost_markers += len(xp.nonzero(self._is_outside)[0]) for axis in self._periodic_axes: outside_inds = self._find_outside_particles(axis) @@ -2015,8 +2056,8 @@ def apply_kinetic_bc(self, newton=False): self.markers[outside_inds, axis] = self.markers[outside_inds, axis] % 1.0 # set shift for alpha-weighted mid-point computation - outside_right_inds = np.nonzero(self._is_outside_right)[0] - outside_left_inds = np.nonzero(self._is_outside_left)[0] + outside_right_inds = xp.nonzero(self._is_outside_right)[0] + outside_left_inds = xp.nonzero(self._is_outside_left)[0] if newton: self.markers[ outside_right_inds, @@ -2084,12 +2125,12 @@ def particle_refilling(self): for kind in self.bc_refill: # sorting out particles which are out of the domain if kind == "inner": - outside_inds = np.nonzero(self._is_outside_left)[0] + outside_inds = xp.nonzero(self._is_outside_left)[0] self.markers[outside_inds, 0] = 1e-4 r_loss = self.domain.params["a1"] else: - outside_inds = np.nonzero(self._is_outside_right)[0] + outside_inds = xp.nonzero(self._is_outside_right)[0] self.markers[outside_inds, 0] = 1 - 1e-4 r_loss = 1.0 @@ -2138,12 +2179,12 @@ def gyro_transfer(self, outside_inds): Parameters ---------- - outside_inds : np.array (int) + outside_inds : xp.array (int) An array of indices of particles which are outside of the domain. Returns ------- - out : np.array (bool) + out : xp.array (bool) An array of indices of particles where its guiding centers are outside of the domain. """ @@ -2160,18 +2201,18 @@ def gyro_transfer(self, outside_inds): b_cart, xyz = self.equil.b_cart(self.markers[outside_inds, :]) # calculate magnetic field amplitude and normalized magnetic field - absB0 = np.sqrt(b_cart[0] ** 2 + b_cart[1] ** 2 + b_cart[2] ** 2) + absB0 = xp.sqrt(b_cart[0] ** 2 + b_cart[1] ** 2 + b_cart[2] ** 2) norm_b_cart = b_cart / absB0 # calculate parallel and perpendicular velocities - v_parallel = np.einsum("ij,ij->j", v, norm_b_cart) - v_perp = np.cross(norm_b_cart, np.cross(v, norm_b_cart, axis=0), axis=0) - v_perp_square = np.sqrt(v_perp[0] ** 2 + v_perp[1] ** 2 + v_perp[2] ** 2) + v_parallel = xp.einsum("ij,ij->j", v, norm_b_cart) + v_perp = xp.cross(norm_b_cart, xp.cross(v, norm_b_cart, axis=0), axis=0) + v_perp_square = xp.sqrt(v_perp[0] ** 2 + v_perp[1] ** 2 + v_perp[2] ** 2) - assert np.all(np.isclose(v_perp, v - norm_b_cart * v_parallel)) + assert xp.all(xp.isclose(v_perp, v - norm_b_cart * v_parallel)) # calculate Larmor radius - Larmor_r = np.cross(norm_b_cart, v_perp, axis=0) / absB0 * self._epsilon + Larmor_r = xp.cross(norm_b_cart, v_perp, axis=0) / absB0 * self._epsilon # transform cartesian coordinates to logical coordinates # TODO: currently only possible with the geomoetry where its inverse map is defined. @@ -2190,17 +2231,17 @@ def gyro_transfer(self, outside_inds): b_cart = self.equil.b_cart(self.markers[outside_inds, :])[0] # calculate magnetic field amplitude and normalized magnetic field - absB0 = np.sqrt(b_cart[0] ** 2 + b_cart[1] ** 2 + b_cart[2] ** 2) + absB0 = xp.sqrt(b_cart[0] ** 2 + b_cart[1] ** 2 + b_cart[2] ** 2) norm_b_cart = b_cart / absB0 Larmor_r = new_xyz - xyz - Larmor_r /= np.sqrt(Larmor_r[0] ** 2 + Larmor_r[1] ** 2 + Larmor_r[2] ** 2) + Larmor_r /= xp.sqrt(Larmor_r[0] ** 2 + Larmor_r[1] ** 2 + Larmor_r[2] ** 2) - new_v_perp = np.cross(Larmor_r, norm_b_cart, axis=0) * v_perp_square + new_v_perp = xp.cross(Larmor_r, norm_b_cart, axis=0) * v_perp_square self.markers[outside_inds, 3:6] = (norm_b_cart * v_parallel).T + new_v_perp.T - return np.logical_and(1.0 > gc_etas[0], gc_etas[0] > 0.0) + return xp.logical_and(1.0 > gc_etas[0], gc_etas[0] > 0.0) class SortingBoxes: """Boxes used for the sorting of the particles. @@ -2383,27 +2424,26 @@ def _set_boxes(self): n_particles = self._markers_shape[0] n_mkr = int(n_particles / n_box_in) + 1 n_cols = round( - n_mkr * (1 + 1 / np.sqrt(n_mkr) + self._box_bufsize), + n_mkr * (1 + 1 / xp.sqrt(n_mkr) + self._box_bufsize), ) # cartesian boxes - self._boxes = np.zeros((self._n_boxes + 1, n_cols), dtype=int) + self._boxes = xp.zeros((self._n_boxes + 1, n_cols), dtype=int) # TODO: there is still a bug here # the row number in self._boxes should not be n_boxes + 1; this is just a temporary fix to avoid an error that I dont understand. # Must be fixed soon! - self._next_index = np.zeros((self._n_boxes + 1), dtype=int) - self._cumul_next_index = np.zeros((self._n_boxes + 2), dtype=int) - self._neighbours = np.zeros((self._n_boxes, 27), dtype=int) + self._next_index = xp.zeros((self._n_boxes + 1), dtype=int) + self._cumul_next_index = xp.zeros((self._n_boxes + 2), dtype=int) + self._neighbours = xp.zeros((self._n_boxes, 27), dtype=int) # A particle on box i only sees particles in boxes that belong to neighbours[i] initialize_neighbours(self._neighbours, self.nx, self.ny, self.nz) - if self._verbose: - print(f"{self._rank = }\n{self._neighbours = }") + # print(f"{self._rank = }\n{self._neighbours = }") - self._swap_line_1 = np.zeros(self._markers_shape[1]) - self._swap_line_2 = np.zeros(self._markers_shape[1]) + self._swap_line_1 = xp.zeros(self._markers_shape[1]) + self._swap_line_2 = xp.zeros(self._markers_shape[1]) def _set_boundary_boxes(self): """Gather all the boxes that are part of a boundary""" @@ -2564,11 +2604,12 @@ def _sort_boxed_particles_numpy(self): sorting_axis = self._sorting_boxes.box_index if not hasattr(self, "_argsort_array"): - self._argsort_array = np.zeros(self.markers.shape[0], dtype=int) + self._argsort_array = xp.zeros(self.markers.shape[0], dtype=int) self._argsort_array[:] = self._markers[:, sorting_axis].argsort() self._markers[:, :] = self._markers[self._argsort_array] + @profile def put_particles_in_boxes(self): """Assign the right box to the particles and the list of the particles to each box. If sorting_boxes was instantiated with an MPI comm, then the particles in the @@ -2591,19 +2632,19 @@ def put_particles_in_boxes(self): self.check_and_assign_particles_to_boxes() self.update_ghost_particles() - if self.verbose: - valid_box_ids = np.nonzero(self._sorting_boxes._boxes[:, 0] != -1)[0] - print(f"Boxes holding at least one particle: {valid_box_ids}") - for i in valid_box_ids: - n_mks_box = np.count_nonzero(self._sorting_boxes._boxes[i] != -1) - print(f"Number of markers in box {i} is {n_mks_box}") + # if self.verbose: + # valid_box_ids = xp.nonzero(self._sorting_boxes._boxes[:, 0] != -1)[0] + # print(f"Boxes holding at least one particle: {valid_box_ids}") + # for i in valid_box_ids: + # n_mks_box = xp.count_nonzero(self._sorting_boxes._boxes[i] != -1) + # print(f"Number of markers in box {i} is {n_mks_box}") def check_and_assign_particles_to_boxes(self): """Check whether the box array has enough columns (detect load imbalance wrt to sorting boxes), and then assigne the particles to boxes.""" - bcount = np.bincount(np.int64(self.markers_wo_holes[:, -2])) - max_in_box = np.max(bcount) + bcount = xp.bincount(xp.int64(self.markers_wo_holes[:, -2])) + max_in_box = xp.max(bcount) if max_in_box > self._sorting_boxes.boxes.shape[1]: warnings.warn( f'Strong load imbalance detected in sorting boxes: \ @@ -2620,6 +2661,7 @@ def check_and_assign_particles_to_boxes(self): self._sorting_boxes._next_index, ) + @profile def do_sort(self, use_numpy_argsort=False): """Assign the particles to boxes and then sort them.""" nx = self._sorting_boxes.nx @@ -2646,7 +2688,7 @@ def do_sort(self, use_numpy_argsort=False): def remove_ghost_particles(self): self.update_ghost_particles() - new_holes = np.nonzero(self.ghost_particles) + new_holes = xp.nonzero(self.ghost_particles) self._markers[new_holes] = -1.0 self.update_holes() @@ -2660,8 +2702,8 @@ def prepare_ghost_particles(self): 4. optional: mirror position for boundary conditions """ shifts = self.sorting_boxes.bc_sph_index_shifts - if self.verbose: - print(f"{self.sorting_boxes.bc_sph_index_shifts = }") + # if self.verbose: + # print(f"{self.sorting_boxes.bc_sph_index_shifts = }") ## Faces @@ -2938,161 +2980,161 @@ def determine_markers_in_box(self, list_boxes): for i in list_boxes: indices += list(self._sorting_boxes._boxes[i][self._sorting_boxes._boxes[i] != -1]) - indices = np.array(indices, dtype=int) + indices = xp.array(indices, dtype=int) markers_in_box = self.markers[indices] return markers_in_box def get_destinations_box(self): """Find the destination proc for the particles to communicate for the box structure.""" - self._send_info_box = np.zeros(self.mpi_size, dtype=int) - self._send_list_box = [np.zeros((0, self.n_cols))] * self.mpi_size + self._send_info_box = xp.zeros(self.mpi_size, dtype=int) + self._send_list_box = [xp.zeros((0, self.n_cols))] * self.mpi_size # Faces # if self._x_m_proc is not None: self._send_info_box[self._x_m_proc] += len(self._markers_x_m) - self._send_list_box[self._x_m_proc] = np.concatenate((self._send_list_box[self._x_m_proc], self._markers_x_m)) + self._send_list_box[self._x_m_proc] = xp.concatenate((self._send_list_box[self._x_m_proc], self._markers_x_m)) # if self._x_p_proc is not None: self._send_info_box[self._x_p_proc] += len(self._markers_x_p) - self._send_list_box[self._x_p_proc] = np.concatenate((self._send_list_box[self._x_p_proc], self._markers_x_p)) + self._send_list_box[self._x_p_proc] = xp.concatenate((self._send_list_box[self._x_p_proc], self._markers_x_p)) # if self._y_m_proc is not None: self._send_info_box[self._y_m_proc] += len(self._markers_y_m) - self._send_list_box[self._y_m_proc] = np.concatenate((self._send_list_box[self._y_m_proc], self._markers_y_m)) + self._send_list_box[self._y_m_proc] = xp.concatenate((self._send_list_box[self._y_m_proc], self._markers_y_m)) # if self._y_p_proc is not None: self._send_info_box[self._y_p_proc] += len(self._markers_y_p) - self._send_list_box[self._y_p_proc] = np.concatenate((self._send_list_box[self._y_p_proc], self._markers_y_p)) + self._send_list_box[self._y_p_proc] = xp.concatenate((self._send_list_box[self._y_p_proc], self._markers_y_p)) # if self._z_m_proc is not None: self._send_info_box[self._z_m_proc] += len(self._markers_z_m) - self._send_list_box[self._z_m_proc] = np.concatenate((self._send_list_box[self._z_m_proc], self._markers_z_m)) + self._send_list_box[self._z_m_proc] = xp.concatenate((self._send_list_box[self._z_m_proc], self._markers_z_m)) # if self._z_p_proc is not None: self._send_info_box[self._z_p_proc] += len(self._markers_z_p) - self._send_list_box[self._z_p_proc] = np.concatenate((self._send_list_box[self._z_p_proc], self._markers_z_p)) + self._send_list_box[self._z_p_proc] = xp.concatenate((self._send_list_box[self._z_p_proc], self._markers_z_p)) # x-y edges # if self._x_m_y_m_proc is not None: self._send_info_box[self._x_m_y_m_proc] += len(self._markers_x_m_y_m) - self._send_list_box[self._x_m_y_m_proc] = np.concatenate( + self._send_list_box[self._x_m_y_m_proc] = xp.concatenate( (self._send_list_box[self._x_m_y_m_proc], self._markers_x_m_y_m) ) # if self._x_m_y_p_proc is not None: self._send_info_box[self._x_m_y_p_proc] += len(self._markers_x_m_y_p) - self._send_list_box[self._x_m_y_p_proc] = np.concatenate( + self._send_list_box[self._x_m_y_p_proc] = xp.concatenate( (self._send_list_box[self._x_m_y_p_proc], self._markers_x_m_y_p) ) # if self._x_p_y_m_proc is not None: self._send_info_box[self._x_p_y_m_proc] += len(self._markers_x_p_y_m) - self._send_list_box[self._x_p_y_m_proc] = np.concatenate( + self._send_list_box[self._x_p_y_m_proc] = xp.concatenate( (self._send_list_box[self._x_p_y_m_proc], self._markers_x_p_y_m) ) # if self._x_p_y_p_proc is not None: self._send_info_box[self._x_p_y_p_proc] += len(self._markers_x_p_y_p) - self._send_list_box[self._x_p_y_p_proc] = np.concatenate( + self._send_list_box[self._x_p_y_p_proc] = xp.concatenate( (self._send_list_box[self._x_p_y_p_proc], self._markers_x_p_y_p) ) # x-z edges # if self._x_m_z_m_proc is not None: self._send_info_box[self._x_m_z_m_proc] += len(self._markers_x_m_z_m) - self._send_list_box[self._x_m_z_m_proc] = np.concatenate( + self._send_list_box[self._x_m_z_m_proc] = xp.concatenate( (self._send_list_box[self._x_m_z_m_proc], self._markers_x_m_z_m) ) # if self._x_m_z_p_proc is not None: self._send_info_box[self._x_m_z_p_proc] += len(self._markers_x_m_z_p) - self._send_list_box[self._x_m_z_p_proc] = np.concatenate( + self._send_list_box[self._x_m_z_p_proc] = xp.concatenate( (self._send_list_box[self._x_m_z_p_proc], self._markers_x_m_z_p) ) # if self._x_p_z_m_proc is not None: self._send_info_box[self._x_p_z_m_proc] += len(self._markers_x_p_z_m) - self._send_list_box[self._x_p_z_m_proc] = np.concatenate( + self._send_list_box[self._x_p_z_m_proc] = xp.concatenate( (self._send_list_box[self._x_p_z_m_proc], self._markers_x_p_z_m) ) # if self._x_p_z_p_proc is not None: self._send_info_box[self._x_p_z_p_proc] += len(self._markers_x_p_z_p) - self._send_list_box[self._x_p_z_p_proc] = np.concatenate( + self._send_list_box[self._x_p_z_p_proc] = xp.concatenate( (self._send_list_box[self._x_p_z_p_proc], self._markers_x_p_z_p) ) # y-z edges # if self._y_m_z_m_proc is not None: self._send_info_box[self._y_m_z_m_proc] += len(self._markers_y_m_z_m) - self._send_list_box[self._y_m_z_m_proc] = np.concatenate( + self._send_list_box[self._y_m_z_m_proc] = xp.concatenate( (self._send_list_box[self._y_m_z_m_proc], self._markers_y_m_z_m) ) # if self._y_m_z_p_proc is not None: self._send_info_box[self._y_m_z_p_proc] += len(self._markers_y_m_z_p) - self._send_list_box[self._y_m_z_p_proc] = np.concatenate( + self._send_list_box[self._y_m_z_p_proc] = xp.concatenate( (self._send_list_box[self._y_m_z_p_proc], self._markers_y_m_z_p) ) # if self._y_p_z_m_proc is not None: self._send_info_box[self._y_p_z_m_proc] += len(self._markers_y_p_z_m) - self._send_list_box[self._y_p_z_m_proc] = np.concatenate( + self._send_list_box[self._y_p_z_m_proc] = xp.concatenate( (self._send_list_box[self._y_p_z_m_proc], self._markers_y_p_z_m) ) # if self._y_p_z_p_proc is not None: self._send_info_box[self._y_p_z_p_proc] += len(self._markers_y_p_z_p) - self._send_list_box[self._y_p_z_p_proc] = np.concatenate( + self._send_list_box[self._y_p_z_p_proc] = xp.concatenate( (self._send_list_box[self._y_p_z_p_proc], self._markers_y_p_z_p) ) # corners # if self._x_m_y_m_z_m_proc is not None: self._send_info_box[self._x_m_y_m_z_m_proc] += len(self._markers_x_m_y_m_z_m) - self._send_list_box[self._x_m_y_m_z_m_proc] = np.concatenate( + self._send_list_box[self._x_m_y_m_z_m_proc] = xp.concatenate( (self._send_list_box[self._x_m_y_m_z_m_proc], self._markers_x_m_y_m_z_m) ) # if self._x_m_y_m_z_p_proc is not None: self._send_info_box[self._x_m_y_m_z_p_proc] += len(self._markers_x_m_y_m_z_p) - self._send_list_box[self._x_m_y_m_z_p_proc] = np.concatenate( + self._send_list_box[self._x_m_y_m_z_p_proc] = xp.concatenate( (self._send_list_box[self._x_m_y_m_z_p_proc], self._markers_x_m_y_m_z_p) ) # if self._x_m_y_p_z_m_proc is not None: self._send_info_box[self._x_m_y_p_z_m_proc] += len(self._markers_x_m_y_p_z_m) - self._send_list_box[self._x_m_y_p_z_m_proc] = np.concatenate( + self._send_list_box[self._x_m_y_p_z_m_proc] = xp.concatenate( (self._send_list_box[self._x_m_y_p_z_m_proc], self._markers_x_m_y_p_z_m) ) # if self._x_m_y_p_z_p_proc is not None: self._send_info_box[self._x_m_y_p_z_p_proc] += len(self._markers_x_m_y_p_z_p) - self._send_list_box[self._x_m_y_p_z_p_proc] = np.concatenate( + self._send_list_box[self._x_m_y_p_z_p_proc] = xp.concatenate( (self._send_list_box[self._x_m_y_p_z_p_proc], self._markers_x_m_y_p_z_p) ) # if self._x_p_y_m_z_m_proc is not None: self._send_info_box[self._x_p_y_m_z_m_proc] += len(self._markers_x_p_y_m_z_m) - self._send_list_box[self._x_p_y_m_z_m_proc] = np.concatenate( + self._send_list_box[self._x_p_y_m_z_m_proc] = xp.concatenate( (self._send_list_box[self._x_p_y_m_z_m_proc], self._markers_x_p_y_m_z_m) ) # if self._x_p_y_m_z_p_proc is not None: self._send_info_box[self._x_p_y_m_z_p_proc] += len(self._markers_x_p_y_m_z_p) - self._send_list_box[self._x_p_y_m_z_p_proc] = np.concatenate( + self._send_list_box[self._x_p_y_m_z_p_proc] = xp.concatenate( (self._send_list_box[self._x_p_y_m_z_p_proc], self._markers_x_p_y_m_z_p) ) # if self._x_p_y_p_z_m_proc is not None: self._send_info_box[self._x_p_y_p_z_m_proc] += len(self._markers_x_p_y_p_z_m) - self._send_list_box[self._x_p_y_p_z_m_proc] = np.concatenate( + self._send_list_box[self._x_p_y_p_z_m_proc] = xp.concatenate( (self._send_list_box[self._x_p_y_p_z_m_proc], self._markers_x_p_y_p_z_m) ) # if self._x_p_y_p_z_p_proc is not None: self._send_info_box[self._x_p_y_p_z_p_proc] += len(self._markers_x_p_y_p_z_p) - self._send_list_box[self._x_p_y_p_z_p_proc] = np.concatenate( + self._send_list_box[self._x_p_y_p_z_p_proc] = xp.concatenate( (self._send_list_box[self._x_p_y_p_z_p_proc], self._markers_x_p_y_p_z_p) ) @@ -3102,7 +3144,7 @@ def self_communication_boxes(self): if self._send_info_box[self.mpi_rank] > 0: self.update_holes() - holes_inds = np.nonzero(self.holes)[0] + holes_inds = xp.nonzero(self.holes)[0] if holes_inds.size < self._send_info_box[self.mpi_rank]: warnings.warn( @@ -3124,16 +3166,17 @@ def self_communication_boxes(self): # self.update_holes() # self.update_ghost_particles() # self.update_valid_mks() - # holes_inds = np.nonzero(self.holes)[0] + # holes_inds = xp.nonzero(self.holes)[0] - self.markers[holes_inds[np.arange(self._send_info_box[self.mpi_rank])]] = self._send_list_box[self.mpi_rank] + self.markers[holes_inds[xp.arange(self._send_info_box[self.mpi_rank])]] = self._send_list_box[self.mpi_rank] + @profile def communicate_boxes(self, verbose=False): - if verbose: - n_valid = np.count_nonzero(self.valid_mks) - n_holes = np.count_nonzero(self.holes) - n_ghosts = np.count_nonzero(self.ghost_particles) - print(f"before communicate_boxes: {self.mpi_rank = }, {n_valid = } {n_holes = }, {n_ghosts = }") + # if verbose: + # n_valid = xp.count_nonzero(self.valid_mks) + # n_holes = xp.count_nonzero(self.holes) + # n_ghosts = xp.count_nonzero(self.ghost_particles) + # print(f"before communicate_boxes: {self.mpi_rank = }, {n_valid = } {n_holes = }, {n_ghosts = }") self.prepare_ghost_particles() self.get_destinations_box() @@ -3146,11 +3189,11 @@ def communicate_boxes(self, verbose=False): self.update_holes() self.update_ghost_particles() - if verbose: - n_valid = np.count_nonzero(self.valid_mks) - n_holes = np.count_nonzero(self.holes) - n_ghosts = np.count_nonzero(self.ghost_particles) - print(f"after communicate_boxes: {self.mpi_rank = }, {n_valid = }, {n_holes = }, {n_ghosts = }") + # if verbose: + # n_valid = xp.count_nonzero(self.valid_mks) + # n_holes = xp.count_nonzero(self.holes) + # n_ghosts = xp.count_nonzero(self.ghost_particles) + # print(f"after communicate_boxes: {self.mpi_rank = }, {n_valid = }, {n_holes = }, {n_ghosts = }") def sendrecv_all_to_all_boxes(self): """ @@ -3158,7 +3201,7 @@ def sendrecv_all_to_all_boxes(self): for the communication of particles in boundary boxes. """ - self._recv_info_box = np.zeros(self.mpi_comm.Get_size(), dtype=int) + self._recv_info_box = xp.zeros(self.mpi_comm.Get_size(), dtype=int) self.mpi_comm.Alltoall(self._send_info_box, self._recv_info_box) @@ -3169,8 +3212,8 @@ def sendrecv_markers_boxes(self): """ # i-th entry holds the number (not the index) of the first hole to be filled by data from process i - first_hole = np.cumsum(self._recv_info_box) - self._recv_info_box - hole_inds = np.nonzero(self._holes)[0] + first_hole = xp.cumsum(self._recv_info_box) - self._recv_info_box + hole_inds = xp.nonzero(self._holes)[0] # Initialize send and receive commands reqs = [] recvbufs = [] @@ -3181,7 +3224,7 @@ def sendrecv_markers_boxes(self): else: self.mpi_comm.Isend(data, dest=i, tag=self.mpi_comm.Get_rank()) - recvbufs += [np.zeros((N_recv, self._markers.shape[1]), dtype=float)] + recvbufs += [xp.zeros((N_recv, self._markers.shape[1]), dtype=float)] reqs += [self.mpi_comm.Irecv(recvbufs[-1], source=i, tag=i)] # Wait for buffer, then put markers into holes @@ -3204,7 +3247,7 @@ def sendrecv_markers_boxes(self): self.mpi_comm.Abort() # exit() - self._markers[hole_inds[first_hole[i] + np.arange(self._recv_info_box[i])]] = recvbufs[i] + self._markers[hole_inds[first_hole[i] + xp.arange(self._recv_info_box[i])]] = recvbufs[i] test_reqs.pop() reqs[i] = None @@ -3679,11 +3722,11 @@ def eval_density( def eval_sph( self, - eta1: np.ndarray, - eta2: np.ndarray, - eta3: np.ndarray, + eta1: xp.ndarray, + eta2: xp.ndarray, + eta3: xp.ndarray, index: int, - out: np.ndarray = None, + out: xp.ndarray = None, fast: bool = True, kernel_type: str = "gaussian_1d", derivative: int = "0", @@ -3729,12 +3772,12 @@ def eval_sph( h1, h2, h3 : float Radius of the smoothing kernel in each dimension. """ - _shp = np.shape(eta1) - assert _shp == np.shape(eta2) == np.shape(eta3) + _shp = xp.shape(eta1) + assert _shp == xp.shape(eta2) == xp.shape(eta3) if out is not None: - assert _shp == np.shape(out) + assert _shp == xp.shape(out) else: - out = np.zeros_like(eta1) + out = xp.zeros_like(eta1) assert derivative in {0, 1, 2, 3}, f"derivative must be 0, 1, 2 or 3, but is {derivative}." @@ -3817,7 +3860,7 @@ def update_ghost_particles(self): def sendrecv_determine_mtbs( self, - alpha: list | tuple | np.ndarray = (1.0, 1.0, 1.0), + alpha: list | tuple | xp.ndarray = (1.0, 1.0, 1.0), ): """ Determine which markers have to be sent from current process and put them in a new array. @@ -3839,34 +3882,34 @@ def sendrecv_determine_mtbs( Eta-values of shape (n_send, :) according to which the sorting is performed. """ # position that determines the sorting (including periodic shift of boundary conditions) - if not isinstance(alpha, np.ndarray): - alpha = np.array(alpha, dtype=float) + if not isinstance(alpha, xp.ndarray): + alpha = xp.array(alpha, dtype=float) assert alpha.size == 3 - assert np.all(alpha >= 0.0) and np.all(alpha <= 1.0) + assert xp.all(alpha >= 0.0) and xp.all(alpha <= 1.0) bi = self.first_pusher_idx - self._sorting_etas = np.mod( + self._sorting_etas = xp.mod( alpha * (self.markers[:, :3] + self.markers[:, bi + 3 + self.vdim : bi + 3 + self.vdim + 3]) + (1.0 - alpha) * self.markers[:, bi : bi + 3], 1.0, ) # check which particles are on the current process domain - self._is_on_proc_domain = np.logical_and( + self._is_on_proc_domain = xp.logical_and( self._sorting_etas > self.domain_array[self.mpi_rank, 0::3], self._sorting_etas < self.domain_array[self.mpi_rank, 1::3], ) # to stay on the current process, all three columns must be True - self._can_stay = np.all(self._is_on_proc_domain, axis=1) + self._can_stay = xp.all(self._is_on_proc_domain, axis=1) # holes and ghosts can stay, too self._can_stay[self.holes] = True self._can_stay[self.ghost_particles] = True # True values can stay on the process, False must be sent, already empty rows (-1) cannot be sent - send_inds = np.nonzero(~self._can_stay)[0] + send_inds = xp.nonzero(~self._can_stay)[0] - hole_inds_after_send = np.nonzero(np.logical_or(~self._can_stay, self.holes))[0] + hole_inds_after_send = xp.nonzero(xp.logical_or(~self._can_stay, self.holes))[0] return hole_inds_after_send, send_inds @@ -3885,16 +3928,16 @@ def sendrecv_get_destinations(self, send_inds): """ # One entry for each process - send_info = np.zeros(self.mpi_size, dtype=int) + send_info = xp.zeros(self.mpi_size, dtype=int) # TODO: do not loop over all processes, start with neighbours and work outwards (using while) for i in range(self.mpi_size): - conds = np.logical_and( + conds = xp.logical_and( self._sorting_etas[send_inds] > self.domain_array[i, 0::3], self._sorting_etas[send_inds] < self.domain_array[i, 1::3], ) - self._send_to_i[i] = np.nonzero(np.all(conds, axis=1))[0] + self._send_to_i[i] = xp.nonzero(xp.all(conds, axis=1))[0] send_info[i] = self._send_to_i[i].size self._send_list[i] = self.markers[send_inds][self._send_to_i[i]] @@ -3916,7 +3959,7 @@ def sendrecv_all_to_all(self, send_info): Amount of marticles to be received from i-th process. """ - recv_info = np.zeros(self.mpi_size, dtype=int) + recv_info = xp.zeros(self.mpi_size, dtype=int) self.mpi_comm.Alltoall(send_info, recv_info) @@ -3936,7 +3979,7 @@ def sendrecv_markers(self, recv_info, hole_inds_after_send): """ # i-th entry holds the number (not the index) of the first hole to be filled by data from process i - first_hole = np.cumsum(recv_info) - recv_info + first_hole = xp.cumsum(recv_info) - recv_info # Initialize send and receive commands for i, (data, N_recv) in enumerate(zip(self._send_list, list(recv_info))): @@ -3946,7 +3989,7 @@ def sendrecv_markers(self, recv_info, hole_inds_after_send): else: self.mpi_comm.Isend(data, dest=i, tag=self.mpi_rank) - self._recvbufs[i] = np.zeros((N_recv, self.markers.shape[1]), dtype=float) + self._recvbufs[i] = xp.zeros((N_recv, self.markers.shape[1]), dtype=float) self._reqs[i] = self.mpi_comm.Irecv(self._recvbufs[i], source=i, tag=i) # Wait for buffer, then put markers into holes @@ -3968,12 +4011,12 @@ def sendrecv_markers(self, recv_info, hole_inds_after_send): ) self.mpi_comm.Abort() - self.markers[hole_inds_after_send[first_hole[i] + np.arange(recv_info[i])]] = self._recvbufs[i] + self.markers[hole_inds_after_send[first_hole[i] + xp.arange(recv_info[i])]] = self._recvbufs[i] test_reqs.pop() self._reqs[i] = None - def _gather_scalar_in_subcomm_array(self, scalar: int, out: np.ndarray = None): + def _gather_scalar_in_subcomm_array(self, scalar: int, out: xp.ndarray = None): """Return an array of length sub_comm.size, where the i-th entry corresponds to the value of the scalar on process i. @@ -3982,11 +4025,11 @@ def _gather_scalar_in_subcomm_array(self, scalar: int, out: np.ndarray = None): scalar : int The scalar value on each process. - out : np.ndarray + out : xp.ndarray The returned array (optional). """ if out is None: - _tmp = np.zeros(self.mpi_size, dtype=int) + _tmp = xp.zeros(self.mpi_size, dtype=int) else: assert out.size == self.mpi_size _tmp = out @@ -4001,7 +4044,7 @@ def _gather_scalar_in_subcomm_array(self, scalar: int, out: np.ndarray = None): return _tmp - def _gather_scalar_in_intercomm_array(self, scalar: int, out: np.ndarray = None): + def _gather_scalar_in_intercomm_array(self, scalar: int, out: xp.ndarray = None): """Return an array of length inter_comm.size, where the i-th entry corresponds to the value of the scalar on clone i. @@ -4010,11 +4053,11 @@ def _gather_scalar_in_intercomm_array(self, scalar: int, out: np.ndarray = None) scalar : int The scalar value on each clone. - out : np.ndarray + out : xp.ndarray The returned array (optional). """ if out is None: - _tmp = np.zeros(self.num_clones, dtype=int) + _tmp = xp.zeros(self.num_clones, dtype=int) else: assert out.size == self.num_clones _tmp = out @@ -4043,7 +4086,7 @@ class Tesselation: comm : Intracomm MPI communicator. - domain_array : np.ndarray + domain_array : xp.ndarray A 2d array[float] of shape (comm.Get_size(), 9) holding info on the domain decomposition. sorting_boxes : Particles.SortingBoxes @@ -4055,7 +4098,7 @@ def __init__( tiles_pb: int | float, *, comm: Intracomm = None, - domain_array: np.ndarray = None, + domain_array: xp.ndarray = None, sorting_boxes: Particles.SortingBoxes = None, ): if isinstance(tiles_pb, int): @@ -4073,8 +4116,8 @@ def __init__( assert domain_array is not None if domain_array is None: - self._starts = np.zeros(3) - self._ends = np.ones(3) + self._starts = xp.zeros(3) + self._ends = xp.ones(3) else: self._starts = domain_array[self.rank, 0::3] self._ends = domain_array[self.rank, 1::3] @@ -4097,9 +4140,9 @@ def __init__( if n_boxes == 1: self._dims_mask = [True] * 3 else: - self._dims_mask = np.array(self.boxes_per_dim) > 1 + self._dims_mask = xp.array(self.boxes_per_dim) > 1 - min_tiles = 2 ** np.count_nonzero(self.dims_mask) + min_tiles = 2 ** xp.count_nonzero(self.dims_mask) assert self.tiles_pb >= min_tiles, ( f"At least {min_tiles} tiles per sorting box is enforced, but you have {self.tiles_pb}!" ) @@ -4122,19 +4165,19 @@ def get_tiles(self): # print(f'{self.dims_mask = }') # tiles in one sorting box - self._nt_per_dim = np.array([1, 1, 1]) - _ids = np.nonzero(self._dims_mask)[0] + self._nt_per_dim = xp.array([1, 1, 1]) + _ids = xp.nonzero(self._dims_mask)[0] for fac in factors_vec: _nt = self.nt_per_dim[self._dims_mask] - d = _ids[np.argmin(_nt)] + d = _ids[xp.argmin(_nt)] self._nt_per_dim[d] *= fac # print(f'{_nt = }, {d = }, {self.nt_per_dim = }') - assert np.prod(self.nt_per_dim) == self.tiles_pb + assert xp.prod(self.nt_per_dim) == self.tiles_pb # tiles between [0, box_width] in each direction - self._tile_breaks = [np.linspace(0.0, bw, nt + 1) for bw, nt in zip(self.box_widths, self.nt_per_dim)] - self._tile_midpoints = [(np.roll(tbs, -1)[:-1] + tbs[:-1]) / 2 for tbs in self.tile_breaks] + self._tile_breaks = [xp.linspace(0.0, bw, nt + 1) for bw, nt in zip(self.box_widths, self.nt_per_dim)] + self._tile_midpoints = [(xp.roll(tbs, -1)[:-1] + tbs[:-1]) / 2 for tbs in self.tile_breaks] self._tile_volume = 1.0 for tb in self.tile_breaks: self._tile_volume *= tb[1] @@ -4142,8 +4185,8 @@ def get_tiles(self): def draw_markers(self): """Draw markers on the tile midpoints.""" _, eta1 = self._tile_output_arrays() - eta2 = np.zeros_like(eta1) - eta3 = np.zeros_like(eta1) + eta2 = xp.zeros_like(eta1) + eta3 = xp.zeros_like(eta1) nt_x, nt_y, nt_z = self.nt_per_dim @@ -4154,7 +4197,7 @@ def draw_markers(self): for k in range(self.boxes_per_dim[2]): z_midpoints = self._get_midpoints(k, 2) - xx, yy, zz = np.meshgrid( + xx, yy, zz = xp.meshgrid( x_midpoints, y_midpoints, z_midpoints, @@ -4191,7 +4234,7 @@ def _get_quad_pts(self, n_quad=None): self._tile_quad_pts = [] self._tile_quad_wts = [] for nq, tb in zip(n_quad, self.tile_breaks): - pts_loc, wts_loc = np.polynomial.legendre.leggauss(nq) + pts_loc, wts_loc = xp.polynomial.legendre.leggauss(nq) pts, wts = quadrature_grid(tb[:2], pts_loc, wts_loc) self._tile_quad_pts += [pts[0]] self._tile_quad_wts += [wts[0]] @@ -4218,7 +4261,7 @@ def cell_averages(self, fun, n_quad=None): for k in range(self.boxes_per_dim[2]): z_pts = self._get_box_quad_pts(k, 2) - xx, yy, zz = np.meshgrid( + xx, yy, zz = xp.meshgrid( x_pts.flatten(), y_pts.flatten(), z_pts.flatten(), @@ -4247,9 +4290,9 @@ def _tile_output_arrays(self): * the first with one entry for each tile on one sorting box * the second with one entry for each tile on current process """ - # self._quad_pts = [np.zeros((nt, nq)).flatten() for nt, nq in zip(self.nt_per_dim, self.tile_quad_pts)] - single_box_out = np.zeros(self.nt_per_dim) - out = np.tile(single_box_out, self.boxes_per_dim) + # self._quad_pts = [xp.zeros((nt, nq)).flatten() for nt, nq in zip(self.nt_per_dim, self.tile_quad_pts)] + single_box_out = xp.zeros(self.nt_per_dim) + out = xp.tile(single_box_out, self.boxes_per_dim) return single_box_out, out def _get_midpoints(self, i: int, dim: int): @@ -4270,13 +4313,13 @@ def _get_box_quad_pts(self, i: int, dim: int): Returns ------- - x_pts : np.array + x_pts : xp.array 2d array of shape (n_tiles_pb, n_tile_quad_pts) """ xl = self.starts[dim] + i * self.box_widths[dim] x_tile_breaks = xl + self.tile_breaks[dim][:-1] x_tile_pts = self.tile_quad_pts[dim] - x_pts = np.tile(x_tile_breaks, (x_tile_pts.size, 1)).T + x_tile_pts + x_pts = xp.tile(x_tile_breaks, (x_tile_pts.size, 1)).T + x_tile_pts return x_pts @property diff --git a/src/struphy/pic/particles.py b/src/struphy/pic/particles.py index ef9cfaa6a..21e50ffda 100644 --- a/src/struphy/pic/particles.py +++ b/src/struphy/pic/particles.py @@ -1,12 +1,17 @@ import copy -from struphy.fields_background.base import FluidEquilibriumWithB +import cunumpy as xp + +from struphy.fields_background import equils +from struphy.fields_background.base import FluidEquilibrium, FluidEquilibriumWithB from struphy.fields_background.projected_equils import ProjectedFluidEquilibriumWithB from struphy.geometry.base import Domain +from struphy.geometry.utilities import TransformedPformComponent +from struphy.initial.base import Perturbation from struphy.kinetic_background import maxwellians +from struphy.kinetic_background.base import Maxwellian, SumKineticBackground from struphy.pic import utilities_kernels from struphy.pic.base import Particles -from struphy.utils.arrays import xp as np class Particles6D(Particles): @@ -23,8 +28,8 @@ class Particles6D(Particles): """ @classmethod - def default_bckgr_params(cls): - return {"Maxwellian3D": {}} + def default_background(cls): + return maxwellians.Maxwellian3D() def __init__( self, @@ -32,17 +37,19 @@ def __init__( ): kwargs["type"] = "full_f" - if "bckgr_params" not in kwargs: - kwargs["bckgr_params"] = self.default_bckgr_params() + if "background" not in kwargs: + kwargs["background"] = self.default_background() + elif kwargs["background"] is None: + kwargs["background"] = self.default_background() # default number of diagnostics and auxiliary columns self._n_cols_diagnostics = kwargs.pop("n_cols_diagn", 0) self._n_cols_aux = kwargs.pop("n_cols_aux", 5) - print(kwargs.keys()) + super().__init__(**kwargs) # call projected mhd equilibrium in case of CanonicalMaxwellian - if "CanonicalMaxwellian" in kwargs["bckgr_params"]: + if isinstance(kwargs["background"], maxwellians.CanonicalMaxwellian): assert isinstance(self.equil, FluidEquilibriumWithB), ( "CanonicalMaxwellian needs background with magnetic field." ) @@ -90,16 +97,16 @@ def svol(self, eta1, eta2, eta3, *v): """ # load sampling density svol (normalized to 1 in logical space) maxw_params = { - "n": 1.0, - "u1": self.loading_params["moments"][0], - "u2": self.loading_params["moments"][1], - "u3": self.loading_params["moments"][2], - "vth1": self.loading_params["moments"][3], - "vth2": self.loading_params["moments"][4], - "vth3": self.loading_params["moments"][5], + "n": (1.0, None), + "u1": (self.loading_params.moments[0], None), + "u2": (self.loading_params.moments[1], None), + "u3": (self.loading_params.moments[2], None), + "vth1": (self.loading_params.moments[3], None), + "vth2": (self.loading_params.moments[4], None), + "vth3": (self.loading_params.moments[5], None), } - fun = maxwellians.Maxwellian3D(maxw_params=maxw_params) + fun = maxwellians.Maxwellian3D(**maxw_params) if self.spatial == "uniform": return fun(eta1, eta2, eta3, *v) @@ -239,35 +246,45 @@ class DeltaFParticles6D(Particles6D): """ @classmethod - def default_bckgr_params(cls): - return {"Maxwellian3D": {}} + def default_background(cls): + return maxwellians.Maxwellian3D() def __init__( self, **kwargs, ): kwargs["type"] = "delta_f" - kwargs["control_variate"] = False + if "weights_params" in kwargs: + kwargs["weights_params"].control_variate = False super().__init__(**kwargs) def _set_initial_condition(self): - bp_copy = copy.deepcopy(self.bckgr_params) - pp_copy = copy.deepcopy(self.pert_params) - - # Prepare delta-f perturbation parameters - if pp_copy is not None: - for fi in bp_copy: - # Set background to zero (if "use_background_n" in perturbation params is set to false or not in keys) - if fi in pp_copy: - if "use_background_n" in pp_copy[fi]: - if not pp_copy[fi]["use_background_n"]: - bp_copy[fi]["n"] = 0.0 - else: - bp_copy[fi]["n"] = 0.0 - else: - bp_copy[fi]["n"] = 0.0 - - super()._set_initial_condition(bp_copy=bp_copy, pp_copy=pp_copy) + # bp_copy = copy.deepcopy(self.bckgr_params) + # pp_copy = copy.deepcopy(self.pert_params) + + # # Prepare delta-f perturbation parameters + # if pp_copy is not None: + # for fi in bp_copy: + # # Set background to zero (if "use_background_n" in perturbation params is set to false or not in keys) + # if fi in pp_copy: + # if "use_background_n" in pp_copy[fi]: + # if not pp_copy[fi]["use_background_n"]: + # bp_copy[fi]["n"] = 0.0 + # else: + # bp_copy[fi]["n"] = 0.0 + # else: + # bp_copy[fi]["n"] = 0.0 + self.set_n_to_zero(self.initial_condition) + + super()._set_initial_condition() + + def set_n_to_zero(self, background: Maxwellian | SumKineticBackground): + if isinstance(background, Maxwellian): + background.maxw_params["n"] = (0.0, background.maxw_params["n"][1]) + else: + assert isinstance(background, SumKineticBackground) + self.set_n_to_zero(background._f1) + self.set_n_to_zero(background._f2) class Particles5D(Particles): @@ -302,18 +319,20 @@ class Particles5D(Particles): """ @classmethod - def default_bckgr_params(cls): - return {"GyroMaxwellian2D": {}} + def default_background(cls): + return maxwellians.GyroMaxwellian2D() def __init__( self, projected_equil: ProjectedFluidEquilibriumWithB, **kwargs, ): + assert projected_equil is not None, "Particles5D needs a projected MHD equilibrium." + kwargs["type"] = "full_f" - if "bckgr_params" not in kwargs: - kwargs["bckgr_params"] = self.default_bckgr_params() + # if "bckgr_params" not in kwargs: + # kwargs["bckgr_params"] = self.default_bckgr_params() # default number of diagnostics and auxiliary columns self._n_cols_diagnostics = kwargs.pop("n_cols_diagn", 3) @@ -333,6 +352,7 @@ def __init__( self._unit_b1_h = self.projected_equil.unit_b1 self._derham = self.projected_equil.derham + self._tmp0 = self.derham.Vh["0"].zeros() self._tmp2 = self.derham.Vh["2"].zeros() @property @@ -401,14 +421,18 @@ def svol(self, eta1, eta2, eta3, *v): # load sampling density svol (normalized to 1 in logical space) maxw_params = { "n": 1.0, - "u_para": self.loading_params["moments"][0], - "u_perp": self.loading_params["moments"][1], - "vth_para": self.loading_params["moments"][2], - "vth_perp": self.loading_params["moments"][3], + "u_para": self.loading_params.moments[0], + "u_perp": self.loading_params.moments[1], + "vth_para": self.loading_params.moments[2], + "vth_perp": self.loading_params.moments[3], } self._svol = maxwellians.GyroMaxwellian2D( - maxw_params=maxw_params, + n=(1.0, None), + u_para=(self.loading_params.moments[0], None), + u_perp=(self.loading_params.moments[1], None), + vth_para=(self.loading_params.moments[2], None), + vth_perp=(self.loading_params.moments[3], None), volume_form=True, equil=self._magn_bckgr, ) @@ -538,7 +562,7 @@ def save_constants_of_motion(self): self.absB0_h._data, ) - def save_magnetic_energy(self, b2): + def save_magnetic_energy(self, PBb): r""" Calculate magnetic field energy at each particles' position and assign it into markers[:,self.first_diagnostics_idx]. @@ -549,22 +573,17 @@ def save_magnetic_energy(self, b2): Finite element coefficients of the time-dependent magnetic field. """ - E2T = self.derham.extraction_ops["2"].transpose() - b2t = E2T.dot(b2, out=self._tmp2) - b2t.update_ghost_regions() + E0T = self.derham.extraction_ops["0"].transpose() + PBbt = E0T.dot(PBb, out=self._tmp0) + PBbt.update_ghost_regions() - utilities_kernels.eval_magnetic_energy( + utilities_kernels.eval_magnetic_energy_PBb( self.markers, self.derham.args_derham, self.domain.args_domain, self.first_diagnostics_idx, self.absB0_h._data, - self.unit_b1_h[0]._data, - self.unit_b1_h[1]._data, - self.unit_b1_h[2]._data, - b2t[0]._data, - b2t[1]._data, - b2t[2]._data, + PBbt._data, ) def save_magnetic_background_energy(self): @@ -626,8 +645,8 @@ class Particles3D(Particles): """ @classmethod - def default_bckgr_params(cls): - return {"ColdPlasma": {}} + def default_background(cls): + return maxwellians.ColdPlasma() def __init__( self, @@ -635,8 +654,10 @@ def __init__( ): kwargs["type"] = "full_f" - if "bckgr_params" not in kwargs: - kwargs["bckgr_params"] = self.default_bckgr_params() + if "background" not in kwargs: + kwargs["background"] = self.default_background() + elif kwargs["background"] is None: + kwargs["background"] = self.default_background() # default number of diagnostics and auxiliary columns self._n_cols_diagnostics = kwargs.pop("n_cols_diagn", 0) @@ -749,8 +770,8 @@ class ParticlesSPH(Particles): """ @classmethod - def default_bckgr_params(cls): - return {"ConstantVelocity": {}} + def default_background(cls): + return equils.ConstantVelocity() def __init__( self, @@ -758,14 +779,20 @@ def __init__( ): kwargs["type"] = "sph" - if "bckgr_params" not in kwargs: - kwargs["bckgr_params"] = self.default_bckgr_params() + if "background" not in kwargs: + bckgr = self.default_background() + bckgr.domain = kwargs["domain"] + kwargs["background"] = bckgr + elif kwargs["background"] is None: + bckgr = self.default_background() + bckgr.domain = kwargs["domain"] + kwargs["background"] = bckgr if "boxes_per_dim" not in kwargs: - boxes_per_dim = (1, 1, 1) + kwargs["boxes_per_dim"] = (1, 1, 1) else: if kwargs["boxes_per_dim"] is None: - boxes_per_dim = (1, 1, 1) + kwargs["boxes_per_dim"] = (1, 1, 1) # TODO: maybe this needs a fix # else: @@ -861,60 +888,3 @@ def s0(self, eta1, eta2, eta3, *v, flat_eval=False, remove_holes=True): kind="3_to_0", remove_outside=remove_holes, ) - - def _set_initial_condition(self): - """Set a callable initial condition f_init as a 0-form (scalar), and u_init in Cartesian coordinates.""" - from struphy.feec.psydac_derham import transform_perturbation - from struphy.fields_background.base import FluidEquilibrium - - pp_copy = copy.deepcopy(self.pert_params) - - # Get the initialization function and pass the correct arguments - assert isinstance(self.f0, FluidEquilibrium) - self._u_init = self.f0.u_cart - - if pp_copy is not None: - if "n" in pp_copy: - for _type, _params in pp_copy["n"].items(): # only one perturbation is taken into account at the moment - _fun = transform_perturbation(_type, _params, "0", self.domain) - if "u1" in pp_copy: - for _type, _params in pp_copy[ - "u1" - ].items(): # only one perturbation is taken into account at the moment - _fun = transform_perturbation(_type, _params, "v", self.domain) - _fun_cart = lambda e1, e2, e3: self.domain.push(_fun, e1, e2, e3, kind="v") - self._u_init = lambda e1, e2, e3: self.f0.u_cart(e1, e2, e3)[0] + _fun_cart(e1, e2, e3) - # TODO: add other velocity components - else: - _fun = None - - def _f_init(*etas, flat_eval=False): - if len(etas) == 1: - if _fun is None: - out = self.f0.n0(etas[0]) - else: - out = self.f0.n0(etas[0]) + _fun(*etas[0].T) - else: - assert len(etas) == 3 - E1, E2, E3, is_sparse_meshgrid = Domain.prepare_eval_pts( - etas[0], - etas[1], - etas[2], - flat_eval=flat_eval, - ) - - out0 = self.f0.n0(E1, E2, E3) - - if _fun is None: - out = out0 - else: - out1 = _fun(E1, E2, E3) - assert out0.shape == out1.shape - out = out0 + out1 - - if flat_eval: - out = np.squeeze(out) - - return out - - self._f_init = _f_init diff --git a/src/struphy/pic/pushing/pusher.py b/src/struphy/pic/pushing/pusher.py index b18d89b86..190525de9 100644 --- a/src/struphy/pic/pushing/pusher.py +++ b/src/struphy/pic/pushing/pusher.py @@ -1,16 +1,12 @@ "Accelerated particle pushing." -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from mpi4py import MPI -else: - from psydac.ddm.mpi import mpi as MPI +import cunumpy as xp +from line_profiler import profile +from psydac.ddm.mpi import mpi as MPI from struphy.kernel_arguments.pusher_args_kernels import DerhamArguments, DomainArguments from struphy.pic.base import Particles from struphy.profiling.profiling import ProfileManager -from struphy.utils.arrays import xp as np from struphy.utils.pyccel import Pyccelkernel @@ -138,7 +134,7 @@ def __init__( comps = ker_args[2] # check marker array column number - assert isinstance(comps, np.ndarray) + assert isinstance(comps, xp.ndarray) assert column_nr + comps.size < particles.n_cols, ( f"{column_nr + comps.size} not smaller than {particles.n_cols = }; not enough columns in marker array !!" ) @@ -150,7 +146,7 @@ def __init__( comps = ker_args[3] # check marker array column number - assert isinstance(comps, np.ndarray) + assert isinstance(comps, xp.ndarray) assert column_nr + comps.size < particles.n_cols, ( f"{column_nr + comps.size} not smaller than {particles.n_cols = }; not enough columns in marker array !!" ) @@ -158,7 +154,7 @@ def __init__( self._init_kernels = init_kernels self._eval_kernels = eval_kernels - self._residuals = np.zeros(self.particles.markers.shape[0]) + self._residuals = xp.zeros(self.particles.markers.shape[0]) self._converged_loc = self._residuals == 1.0 self._not_converged_loc = self._residuals == 0.0 @@ -167,6 +163,7 @@ def __init__( else: self._box_comm = False + @profile def __call__(self, dt: float): """ Applies the chosen pusher kernel by a time step dt, @@ -212,7 +209,7 @@ def __call__(self, dt: float): add_args = ker_args[3] ker( - np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), + xp.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), column_nr, comps, self.particles.args_markers, @@ -227,7 +224,7 @@ def __call__(self, dt: float): # start stages (e.g. n_stages=4 for RK4) for stage in range(self.n_stages): # start iteration (maxiter=1 for explicit schemes) - n_not_converged = np.empty(1, dtype=int) + n_not_converged = xp.empty(1, dtype=int) n_not_converged[0] = self.particles.n_mks_loc k = 0 @@ -301,12 +298,12 @@ def __call__(self, dt: float): # compute number of non-converged particles (maxiter=1 for explicit schemes) if self.maxiter > 1: self._residuals[:] = markers[:, residual_idx] - max_res = np.max(self._residuals) + max_res = xp.max(self._residuals) if max_res < 0.0: max_res = None self._converged_loc[:] = self._residuals < self._tol self._not_converged_loc[:] = ~self._converged_loc - n_not_converged[0] = np.count_nonzero( + n_not_converged[0] = xp.count_nonzero( self._not_converged_loc, ) diff --git a/src/struphy/pic/pushing/pusher_kernels.py b/src/struphy/pic/pushing/pusher_kernels.py index 429e1c722..47b6a71ba 100644 --- a/src/struphy/pic/pushing/pusher_kernels.py +++ b/src/struphy/pic/pushing/pusher_kernels.py @@ -1615,6 +1615,15 @@ def push_bxu_Hdiv_pauli( # -- removed omp: #$ omp end parallel +@stack_array( + "dfm", + "dfinv", + "dfinv_t", + "e", + "e_cart", + "GXu", + "v", +) def push_pc_GXu_full( dt: float, stage: int, @@ -1630,7 +1639,6 @@ def push_pc_GXu_full( GXu_31: "float[:,:,:]", GXu_32: "float[:,:,:]", GXu_33: "float[:,:,:]", - boundary_cut: "float", ): r"""Updates @@ -1671,10 +1679,6 @@ def push_pc_GXu_full( if markers[ip, 0] == -1.0: continue - # boundary cut - if markers[ip, 0] < boundary_cut or markers[ip, 0] > 1.0 - boundary_cut: - continue - eta1 = markers[ip, 0] eta2 = markers[ip, 1] eta3 = markers[ip, 2] @@ -1740,6 +1744,15 @@ def push_pc_GXu_full( markers[ip, 3:6] -= dt * e_cart / 2.0 +@stack_array( + "dfm", + "dfinv", + "dfinv_t", + "e", + "e_cart", + "GXu", + "v", +) def push_pc_GXu( dt: float, stage: int, @@ -1755,7 +1768,6 @@ def push_pc_GXu( GXu_31: "float[:,:,:]", GXu_32: "float[:,:,:]", GXu_33: "float[:,:,:]", - boundary_cut: "float", ): r"""Updates @@ -1783,7 +1795,6 @@ def push_pc_GXu( e = empty(3, dtype=float) e_cart = empty(3, dtype=float) GXu = empty((3, 3), dtype=float) - GXu_t = empty((3, 3), dtype=float) # particle velocity v = empty(3, dtype=float) @@ -1797,10 +1808,6 @@ def push_pc_GXu( if markers[ip, 0] == -1.0: continue - # boundary cut - if markers[ip, 0] < boundary_cut or markers[ip, 0] > 1.0 - boundary_cut: - continue - eta1 = markers[ip, 0] eta2 = markers[ip, 1] eta3 = markers[ip, 2] @@ -1939,7 +1946,7 @@ def push_eta_stage( @stack_array("dfm", "dfinv", "dfinv_t", "ginv", "v", "u", "k", "k_v", "k_u") -def push_pc_eta_rk4_Hcurl_full( +def push_pc_eta_stage_Hcurl( dt: float, stage: int, args_markers: "MarkerArguments", @@ -1948,6 +1955,10 @@ def push_pc_eta_rk4_Hcurl_full( u_1: "float[:,:,:]", u_2: "float[:,:,:]", u_3: "float[:,:,:]", + use_perp_model: "bool", + a: "float[:]", + b: "float[:]", + c: "float[:]", ): r"""Fourth order Runge-Kutta solve of @@ -1960,14 +1971,6 @@ def push_pc_eta_rk4_Hcurl_full( .. math:: \textnormal{vec}( \hat{\mathbf U}^{1}) = G^{-1}\hat{\mathbf U}^{1}\,,\qquad \textnormal{vec}( \hat{\mathbf U}^{2}) = \frac{\hat{\mathbf U}^{2}}{\sqrt g}\,. - - Parameters - ---------- - u_1, u_2, u_3: array[float] - 3d array of FE coeffs of U-field, either as 1-form or as 2-form. - - u_basis : int - U is 1-form (u_basis=1) or a 2-form (u_basis=2). """ # allocate metric coeffs @@ -1993,22 +1996,13 @@ def push_pc_eta_rk4_Hcurl_full( first_init_idx = args_markers.first_init_idx first_free_idx = args_markers.first_free_idx - # assign factor of k for each stage - if stage == 0 or stage == 3: - nk = 1.0 - else: - nk = 2.0 + # get number of stages + n_stages = shape(b)[0] - # which stage - if stage == 3: + if stage == n_stages - 1: last = 1.0 - cont = 0.0 - elif stage == 2: - last = 0.0 - cont = 2.0 else: last = 0.0 - cont = 1.0 for ip in range(n_markers): # only do something if particle is a "true" particle (i.e. not a hole) @@ -2053,396 +2047,8 @@ def push_pc_eta_rk4_Hcurl_full( u, ) - # transform to vector field - linalg_kernels.matrix_vector(ginv, u, k_u) - - # sum contribs - k[:] = k_v + k_u - - # accum k - markers[ip, first_free_idx : first_free_idx + 3] += k * nk / 6.0 - - # update markers for the next stage - markers[ip, 0:3] = ( - markers[ip, first_init_idx : first_init_idx + 3] - + dt * k / 2 * cont - + dt * markers[ip, first_free_idx : first_free_idx + 3] * last - ) - - -@stack_array("dfm", "dfinv", "dfinv_t", "ginv", "v", "u", "k", "k_v", "k_u") -def push_pc_eta_rk4_Hdiv_full( - dt: float, - stage: int, - args_markers: "MarkerArguments", - args_domain: "DomainArguments", - args_derham: "DerhamArguments", - u_1: "float[:,:,:]", - u_2: "float[:,:,:]", - u_3: "float[:,:,:]", -): - r"""Fourth order Runge-Kutta solve of - - .. math:: - - \frac{\textnormal d \boldsymbol \eta_p(t)}{\textnormal d t} = DF^{-1}(\boldsymbol \eta_p(t)) \mathbf v + \textnormal{vec}( \hat{\mathbf U}^{1(2)}) - - for each marker :math:`p` in markers array, where :math:`\mathbf v` is constant and - - .. math:: - - \textnormal{vec}( \hat{\mathbf U}^{1}) = G^{-1}\hat{\mathbf U}^{1}\,,\qquad \textnormal{vec}( \hat{\mathbf U}^{2}) = \frac{\hat{\mathbf U}^{2}}{\sqrt g}\,. - - Parameters - ---------- - u_1, u_2, u_3: array[float] - 3d array of FE coeffs of U-field, either as 1-form or as 2-form. - - u_basis : int - U is 1-form (u_basis=1) or a 2-form (u_basis=2). - """ - - # allocate metric coeffs - dfm = empty((3, 3), dtype=float) - dfinv = empty((3, 3), dtype=float) - dfinv_t = empty((3, 3), dtype=float) - ginv = empty((3, 3), dtype=float) - - # marker velocity - v = empty(3, dtype=float) - - # U-fiels - u = empty(3, dtype=float) - - # intermediate stages in RK4 - k = empty(3, dtype=float) - k_v = empty(3, dtype=float) - k_u = empty(3, dtype=float) - - # get marker arguments - markers = args_markers.markers - n_markers = args_markers.n_markers - first_init_idx = args_markers.first_init_idx - first_free_idx = args_markers.first_free_idx - - # assign factor of k for each stage - if stage == 0 or stage == 3: - nk = 1.0 - else: - nk = 2.0 - - # is it the last stage? - if stage == 3: - last = 1.0 - cont = 0.0 - else: - last = 0.0 - cont = 1.0 - - for ip in range(n_markers): - # only do something if particle is a "true" particle (i.e. not a hole) - if markers[ip, 0] == -1.0: - continue - - e1 = markers[ip, 0] - e2 = markers[ip, 1] - e3 = markers[ip, 2] - v[:] = markers[ip, 3:6] - - # ----------------- stage n in Runge-Kutta method ------------------- - # evaluate Jacobian, result in dfm - evaluation_kernels.df( - e1, - e2, - e3, - args_domain, - dfm, - ) - - # metric coeffs - det_df = linalg_kernels.det(dfm) - linalg_kernels.matrix_inv(dfm, dfinv) - linalg_kernels.transpose(dfinv, dfinv_t) - linalg_kernels.matrix_matrix(dfinv, dfinv_t, ginv) - - # pull-back of velocity - linalg_kernels.matrix_vector(dfinv, v, k_v) - - # spline evaluation - span1, span2, span3 = get_spans(e1, e2, e3, args_derham) - - # U-field - eval_2form_spline_mpi( - span1, - span2, - span3, - args_derham, - u_1, - u_2, - u_3, - u, - ) - - # transform to vector field - k_u[:] = u / det_df - - # sum contribs - k[:] = k_v + k_u - - # accum k - markers[ip, first_free_idx : first_free_idx + 3] += k * nk / 6.0 - - # update markers for the next stage - markers[ip, 0:3] = ( - markers[ip, first_init_idx : first_init_idx + 3] - + dt * k / 2 * cont - + dt * markers[ip, first_free_idx : first_free_idx + 3] * last - ) - - -@stack_array("dfm", "dfinv", "dfinv_t", "ginv", "v", "u", "k", "k_v") -def push_pc_eta_rk4_H1vec_full( - dt: float, - stage: int, - args_markers: "MarkerArguments", - args_domain: "DomainArguments", - args_derham: "DerhamArguments", - u_1: "float[:,:,:]", - u_2: "float[:,:,:]", - u_3: "float[:,:,:]", -): - r"""Fourth order Runge-Kutta solve of - - .. math:: - - \frac{\textnormal d \boldsymbol \eta_p(t)}{\textnormal d t} = DF^{-1}(\boldsymbol \eta_p(t)) \mathbf v + \textnormal{vec}( \hat{\mathbf U}^{1(2)}) - - for each marker :math:`p` in markers array, where :math:`\mathbf v` is constant and - - .. math:: - - \textnormal{vec}( \hat{\mathbf U}^{1}) = G^{-1}\hat{\mathbf U}^{1}\,,\qquad \textnormal{vec}( \hat{\mathbf U}^{2}) = \frac{\hat{\mathbf U}^{2}}{\sqrt g}\,. - - Parameters - ---------- - u_1, u_2, u_3 : array[float] - 3d array of FE coeffs of U-field, either as 1-form or as 2-form. - - u_basis : int - U is 1-form (u_basis=1) or a 2-form (u_basis=2). - """ - - # allocate metric coeffs - dfm = empty((3, 3), dtype=float) - dfinv = empty((3, 3), dtype=float) - dfinv_t = empty((3, 3), dtype=float) - ginv = empty((3, 3), dtype=float) - - # marker and velocity - v = empty(3, dtype=float) - - # U-fiels - u = empty(3, dtype=float) - - # intermediate stages in RK4 - k = empty(3, dtype=float) - k_v = empty(3, dtype=float) - - # get marker arguments - markers = args_markers.markers - n_markers = args_markers.n_markers - first_init_idx = args_markers.first_init_idx - first_free_idx = args_markers.first_free_idx - - # assign factor of k for each stage - if stage == 0 or stage == 3: - nk = 1.0 - else: - nk = 2.0 - - # which stage - if stage == 3: - last = 1.0 - cont = 0.0 - elif stage == 2: - last = 0.0 - cont = 2.0 - else: - last = 0.0 - cont = 1.0 - - for ip in range(n_markers): - # only do something if particle is a "true" particle (i.e. not a hole) - if markers[ip, 0] == -1.0: - continue - - e1 = markers[ip, 0] - e2 = markers[ip, 1] - e3 = markers[ip, 2] - v[:] = markers[ip, 3:6] - - # ----------------- stage n in Runge-Kutta method ------------------- - # evaluate Jacobian, result in dfm - evaluation_kernels.df( - e1, - e2, - e3, - args_domain, - dfm, - ) - - # metric coeffs - linalg_kernels.matrix_inv(dfm, dfinv) - linalg_kernels.transpose(dfinv, dfinv_t) - linalg_kernels.matrix_matrix(dfinv, dfinv_t, ginv) - - # pull-back of velocity - linalg_kernels.matrix_vector(dfinv, v, k_v) - - # spline evaluation - span1, span2, span3 = get_spans(e1, e2, e3, args_derham) - - # U-field - eval_vectorfield_spline_mpi( - span1, - span2, - span3, - args_derham, - u_1, - u_2, - u_3, - u, - ) - - # sum contribs - k[:] = k_v + u - - # accum k - markers[ip, first_free_idx : first_free_idx + 3] += k * nk / 6.0 - - # update markers for the next stage - markers[ip, 0:3] = ( - markers[ip, first_init_idx : first_init_idx + 3] - + dt * k / 2 * cont - + dt * markers[ip, first_free_idx : first_free_idx + 3] * last - ) - - -@stack_array("dfm", "dfinv", "dfinv_t", "ginv", "v", "u", "k", "k_v", "k_u") -def push_pc_eta_rk4_Hcurl( - dt: float, - stage: int, - args_markers: "MarkerArguments", - args_domain: "DomainArguments", - args_derham: "DerhamArguments", - u_1: "float[:,:,:]", - u_2: "float[:,:,:]", - u_3: "float[:,:,:]", -): - r"""Fourth order Runge-Kutta solve of - - .. math:: - - \frac{\textnormal d \boldsymbol \eta_p(t)}{\textnormal d t} = DF^{-1}(\boldsymbol \eta_p(t)) \mathbf v + \textnormal{vec}( \hat{\mathbf U}^{1(2)}) - - for each marker :math:`p` in markers array, where :math:`\mathbf v` is constant and - - .. math:: - - \textnormal{vec}( \hat{\mathbf U}^{1}) = G^{-1}\hat{\mathbf U}^{1}\,,\qquad \textnormal{vec}( \hat{\mathbf U}^{2}) = \frac{\hat{\mathbf U}^{2}}{\sqrt g}\,. - - Parameters - ---------- - u_1, u_2, u_3 : array[float] - 3d array of FE coeffs of U-field, either as 1-form or as 2-form. - - u_basis : int - U is 1-form (u_basis=1) or a 2-form (u_basis=2). - """ - - # allocate metric coeffs - dfm = empty((3, 3), dtype=float) - dfinv = empty((3, 3), dtype=float) - dfinv_t = empty((3, 3), dtype=float) - ginv = empty((3, 3), dtype=float) - - # marker velocity - v = empty(3, dtype=float) - - # U-fiels - u = empty(3, dtype=float) - - # intermediate stages in RK4 - k = empty(3, dtype=float) - k_v = empty(3, dtype=float) - k_u = empty(3, dtype=float) - - # get marker arguments - markers = args_markers.markers - n_markers = args_markers.n_markers - first_init_idx = args_markers.first_init_idx - first_free_idx = args_markers.first_free_idx - - # assign factor of k for each stage - if stage == 0 or stage == 3: - nk = 1.0 - else: - nk = 2.0 - - # which stage - if stage == 3: - last = 1.0 - cont = 0.0 - elif stage == 2: - last = 0.0 - cont = 2.0 - else: - last = 0.0 - cont = 1.0 - - for ip in range(n_markers): - # only do something if particle is a "true" particle (i.e. not a hole) - if markers[ip, 0] == -1.0: - continue - - e1 = markers[ip, 0] - e2 = markers[ip, 1] - e3 = markers[ip, 2] - v[:] = markers[ip, 3:6] - - # ----------------- stage n in Runge-Kutta method ------------------- - # evaluate Jacobian, result in dfm - evaluation_kernels.df( - e1, - e2, - e3, - args_domain, - dfm, - ) - - # metric coeffs - linalg_kernels.matrix_inv(dfm, dfinv) - linalg_kernels.transpose(dfinv, dfinv_t) - linalg_kernels.matrix_matrix(dfinv, dfinv_t, ginv) - - # pull-back of velocity - linalg_kernels.matrix_vector(dfinv, v, k_v) - - # spline evaluation - span1, span2, span3 = get_spans(e1, e2, e3, args_derham) - - # U-field - eval_1form_spline_mpi( - span1, - span2, - span3, - args_derham, - u_1, - u_2, - u_3, - u, - ) - u[2] = 0.0 + if use_perp_model: + u[2] = 0.0 # transform to vector field linalg_kernels.matrix_vector(ginv, u, k_u) @@ -2451,18 +2057,18 @@ def push_pc_eta_rk4_Hcurl( k[:] = k_v + k_u # accum k - markers[ip, first_free_idx : first_free_idx + 3] += k * nk / 6.0 + markers[ip, first_free_idx : first_free_idx + 3] += dt * b[stage] * k # update markers for the next stage markers[ip, 0:3] = ( markers[ip, first_init_idx : first_init_idx + 3] - + dt * k / 2 * cont - + dt * markers[ip, first_free_idx : first_free_idx + 3] * last + + dt * k * a[stage] + + last * markers[ip, first_free_idx : first_free_idx + 3] ) @stack_array("dfm", "dfinv", "dfinv_t", "ginv", "v", "u", "k", "k_v", "k_u") -def push_pc_eta_rk4_Hdiv( +def push_pc_eta_stage_Hdiv( dt: float, stage: int, args_markers: "MarkerArguments", @@ -2471,6 +2077,10 @@ def push_pc_eta_rk4_Hdiv( u_1: "float[:,:,:]", u_2: "float[:,:,:]", u_3: "float[:,:,:]", + use_perp_model: "bool", + a: "float[:]", + b: "float[:]", + c: "float[:]", ): r"""Fourth order Runge-Kutta solve of @@ -2483,14 +2093,6 @@ def push_pc_eta_rk4_Hdiv( .. math:: \textnormal{vec}( \hat{\mathbf U}^{1}) = G^{-1}\hat{\mathbf U}^{1}\,,\qquad \textnormal{vec}( \hat{\mathbf U}^{2}) = \frac{\hat{\mathbf U}^{2}}{\sqrt g}\,. - - Parameters - ---------- - u_1, u_2, u_3 : array[float] - 3d array of FE coeffs of U-field, either as 1-form or as 2-form. - - u_basis : int - U is 1-form (u_basis=1) or a 2-form (u_basis=2). """ # allocate metric coeffs @@ -2516,19 +2118,13 @@ def push_pc_eta_rk4_Hdiv( first_init_idx = args_markers.first_init_idx first_free_idx = args_markers.first_free_idx - # assign factor of k for each stage - if stage == 0 or stage == 3: - nk = 1.0 - else: - nk = 2.0 + # get number of stages + n_stages = shape(b)[0] - # is it the last stage? - if stage == 3: + if stage == n_stages - 1: last = 1.0 - cont = 0.0 else: last = 0.0 - cont = 1.0 for ip in range(n_markers): # only do something if particle is a "true" particle (i.e. not a hole) @@ -2573,7 +2169,9 @@ def push_pc_eta_rk4_Hdiv( u_3, u, ) - u[2] = 0.0 + + if use_perp_model: + u[2] = 0.0 # transform to vector field k_u[:] = u / det_df @@ -2582,18 +2180,18 @@ def push_pc_eta_rk4_Hdiv( k[:] = k_v + k_u # accum k - markers[ip, first_free_idx : first_free_idx + 3] += k * nk / 6.0 + markers[ip, first_free_idx : first_free_idx + 3] += dt * b[stage] * k # update markers for the next stage markers[ip, 0:3] = ( markers[ip, first_init_idx : first_init_idx + 3] - + dt * k / 2 * cont - + dt * markers[ip, first_free_idx : first_free_idx + 3] * last + + dt * k * a[stage] + + last * markers[ip, first_free_idx : first_free_idx + 3] ) @stack_array("dfm", "dfinv", "dfinv_t", "ginv", "v", "u", "k", "k_v") -def push_pc_eta_rk4_H1vec( +def push_pc_eta_stage_H1vec( dt: float, stage: int, args_markers: "MarkerArguments", @@ -2602,6 +2200,10 @@ def push_pc_eta_rk4_H1vec( u_1: "float[:,:,:]", u_2: "float[:,:,:]", u_3: "float[:,:,:]", + use_perp_model: "bool", + a: "float[:]", + b: "float[:]", + c: "float[:]", ): r"""Fourth order Runge-Kutta solve of @@ -2646,22 +2248,13 @@ def push_pc_eta_rk4_H1vec( first_init_idx = args_markers.first_init_idx first_free_idx = args_markers.first_free_idx - # assign factor of k for each stage - if stage == 0 or stage == 3: - nk = 1.0 - else: - nk = 2.0 + # get number of stages + n_stages = shape(b)[0] - # which stage - if stage == 3: + if stage == n_stages - 1: last = 1.0 - cont = 0.0 - elif stage == 2: - last = 0.0 - cont = 2.0 else: last = 0.0 - cont = 1.0 for ip in range(n_markers): # only do something if particle is a "true" particle (i.e. not a hole) @@ -2705,19 +2298,21 @@ def push_pc_eta_rk4_H1vec( u_3, u, ) - u[2] = 0.0 + + if use_perp_model: + u[2] = 0.0 # sum contribs k[:] = k_v + u # accum k - markers[ip, first_free_idx : first_free_idx + 3] += k * nk / 6.0 + markers[ip, first_free_idx : first_free_idx + 3] += dt * b[stage] * k # update markers for the next stage markers[ip, 0:3] = ( markers[ip, first_init_idx : first_init_idx + 3] - + dt * k / 2 * cont - + dt * markers[ip, first_free_idx : first_free_idx + 3] * last + + dt * k * a[stage] + + last * markers[ip, first_free_idx : first_free_idx + 3] ) @@ -3035,7 +2630,7 @@ def push_v_sph_pressure( h1, h2, h3 : float Kernel width in respective dimension. - gravity: np.ndarray + gravity: xp.ndarray Constant gravitational force as 3-vector. """ # allocate arrays @@ -3266,7 +2861,7 @@ def push_v_sph_pressure_ideal_gas( h1, h2, h3 : float Kernel width in respective dimension. - gravity: np.ndarray + gravity: xp.ndarray Constant gravitational force as 3-vector. """ # allocate arrays @@ -3498,7 +3093,7 @@ def push_v_viscosity( h1, h2, h3 : float Kernel width in respective dimension. - gravity: np.ndarray + gravity: xp.ndarray Constant gravitational force as 3-vector. """ # allocate arrays diff --git a/src/struphy/pic/pushing/pusher_kernels_gc.py b/src/struphy/pic/pushing/pusher_kernels_gc.py index 0b6c9b3c7..5dfee707b 100644 --- a/src/struphy/pic/pushing/pusher_kernels_gc.py +++ b/src/struphy/pic/pushing/pusher_kernels_gc.py @@ -1896,7 +1896,7 @@ def push_gc_cc_J1_H1vec( ) # b_star; in H1vec - b_star[:] = (b + curl_norm_b * v * epsilon) / det_df + b_star[:] = b + curl_norm_b * v * epsilon # calculate abs_b_star_para abs_b_star_para = linalg_kernels.scalar_dot(norm_b1, b_star) @@ -1905,7 +1905,7 @@ def push_gc_cc_J1_H1vec( linalg_kernels.cross(b, u, e) # curl_norm_b dot electric field - temp = linalg_kernels.scalar_dot(e, curl_norm_b) / det_df + temp = linalg_kernels.scalar_dot(e, curl_norm_b) markers[ip, 3] += temp / abs_b_star_para * v * dt @@ -2077,7 +2077,6 @@ def push_gc_cc_J1_Hdiv( u1: "float[:,:,:]", u2: "float[:,:,:]", u3: "float[:,:,:]", - boundary_cut: float, ): r"""Velocity update step for the `CurrentCoupling5DCurlb `_ @@ -2105,8 +2104,6 @@ def push_gc_cc_J1_Hdiv( markers = args_markers.markers n_markers = args_markers.n_markers - # -- removed omp: #$ omp parallel private(ip, boundary_cut, eta1, eta2, eta3, v, det_df, dfm, span1, span2, span3, b, u, e, curl_norm_b, norm_b1, b_star, temp, abs_b_star_para) - # -- removed omp: #$ omp for for ip in range(n_markers): # only do something if particle is a "true" particle (i.e. not a hole) if markers[ip, 0] == -1.0: @@ -2117,9 +2114,6 @@ def push_gc_cc_J1_Hdiv( eta3 = markers[ip, 2] v = markers[ip, 3] - if eta1 < boundary_cut or eta1 > 1.0 - boundary_cut: - continue - # evaluate Jacobian, result in dfm evaluation_kernels.df( eta1, @@ -2183,10 +2177,10 @@ def push_gc_cc_J1_Hdiv( curl_norm_b, ) - # b_star; 2form in H1vec - b_star[:] = (b + curl_norm_b * v * epsilon) / det_df + # b_star; 2form + b_star[:] = b + curl_norm_b * v * epsilon - # calculate abs_b_star_para + # calculate 3form abs_b_star_para abs_b_star_para = linalg_kernels.scalar_dot(norm_b1, b_star) # transform u into H1vec @@ -2196,12 +2190,10 @@ def push_gc_cc_J1_Hdiv( linalg_kernels.cross(b, u, e) # curl_norm_b dot electric field - temp = linalg_kernels.scalar_dot(e, curl_norm_b) / det_df + temp = linalg_kernels.scalar_dot(e, curl_norm_b) markers[ip, 3] += temp / abs_b_star_para * v * dt - # -- removed omp: #$ omp end parallel - @stack_array( "dfm", @@ -2212,13 +2204,11 @@ def push_gc_cc_J1_Hdiv( "u", "bb", "b_star", - "norm_b1", - "norm_b2", + "norm_b", "curl_norm_b", - "tmp1", - "tmp2", + "tmp", "b_prod", - "norm_b2_prod", + "norm_b_prod", ) def push_gc_cc_J2_stage_H1vec( dt: float, @@ -2233,9 +2223,6 @@ def push_gc_cc_J2_stage_H1vec( norm_b11: "float[:,:,:]", norm_b12: "float[:,:,:]", norm_b13: "float[:,:,:]", - norm_b21: "float[:,:,:]", - norm_b22: "float[:,:,:]", - norm_b23: "float[:,:,:]", curl_norm_b1: "float[:,:,:]", curl_norm_b2: "float[:,:,:]", curl_norm_b3: "float[:,:,:]", @@ -2264,16 +2251,14 @@ def push_gc_cc_J2_stage_H1vec( g_inv = empty((3, 3), dtype=float) # containers for fields - tmp1 = empty((3, 3), dtype=float) - tmp2 = empty((3, 3), dtype=float) + tmp = empty((3, 3), dtype=float) b_prod = zeros((3, 3), dtype=float) - norm_b2_prod = empty((3, 3), dtype=float) + norm_b_prod = empty((3, 3), dtype=float) e = empty(3, dtype=float) u = empty(3, dtype=float) bb = empty(3, dtype=float) b_star = empty(3, dtype=float) norm_b1 = empty(3, dtype=float) - norm_b2 = empty(3, dtype=float) curl_norm_b = empty(3, dtype=float) # get marker arguments @@ -2354,18 +2339,6 @@ def push_gc_cc_J2_stage_H1vec( norm_b1, ) - # norm_b; 2form - eval_2form_spline_mpi( - span1, - span2, - span3, - args_derham, - norm_b21, - norm_b22, - norm_b23, - norm_b2, - ) - # curl_norm_b; 2form eval_2form_spline_mpi( span1, @@ -2386,24 +2359,21 @@ def push_gc_cc_J2_stage_H1vec( b_prod[2, 0] = -bb[1] b_prod[2, 1] = +bb[0] - norm_b2_prod[0, 1] = -norm_b2[2] - norm_b2_prod[0, 2] = +norm_b2[1] - norm_b2_prod[1, 0] = +norm_b2[2] - norm_b2_prod[1, 2] = -norm_b2[0] - norm_b2_prod[2, 0] = -norm_b2[1] - norm_b2_prod[2, 1] = +norm_b2[0] + norm_b_prod[0, 1] = -norm_b1[2] + norm_b_prod[0, 2] = +norm_b1[1] + norm_b_prod[1, 0] = +norm_b1[2] + norm_b_prod[1, 2] = -norm_b1[0] + norm_b_prod[2, 0] = -norm_b1[1] + norm_b_prod[2, 1] = +norm_b1[0] # b_star; 2form in H1vec - b_star[:] = (bb + curl_norm_b * v * epsilon) / det_df + b_star[:] = bb + curl_norm_b * v * epsilon - # calculate abs_b_star_para + # calculate 3form abs_b_star_para abs_b_star_para = linalg_kernels.scalar_dot(norm_b1, b_star) - linalg_kernels.matrix_matrix(g_inv, norm_b2_prod, tmp1) - linalg_kernels.matrix_matrix(tmp1, g_inv, tmp2) - linalg_kernels.matrix_matrix(tmp2, b_prod, tmp1) - - linalg_kernels.matrix_vector(tmp1, u, e) + linalg_kernels.matrix_matrix(norm_b_prod, b_prod, tmp) + linalg_kernels.matrix_vector(tmp, u, e) e /= abs_b_star_para @@ -2428,12 +2398,10 @@ def push_gc_cc_J2_stage_H1vec( "bb", "b_star", "norm_b1", - "norm_b2", "curl_norm_b", - "tmp1", - "tmp2", + "tmp", "b_prod", - "norm_b2_prod", + "norm_b_prod", ) def push_gc_cc_J2_stage_Hdiv( dt: float, @@ -2448,9 +2416,6 @@ def push_gc_cc_J2_stage_Hdiv( norm_b11: "float[:,:,:]", norm_b12: "float[:,:,:]", norm_b13: "float[:,:,:]", - norm_b21: "float[:,:,:]", - norm_b22: "float[:,:,:]", - norm_b23: "float[:,:,:]", curl_norm_b1: "float[:,:,:]", curl_norm_b2: "float[:,:,:]", curl_norm_b3: "float[:,:,:]", @@ -2460,7 +2425,6 @@ def push_gc_cc_J2_stage_Hdiv( a: "float[:]", b: "float[:]", c: "float[:]", - boundary_cut: float, ): r"""Single stage of a s-stage explicit pushing step for the `CurrentCoupling5DGradB `_ @@ -2480,16 +2444,14 @@ def push_gc_cc_J2_stage_Hdiv( g_inv = empty((3, 3), dtype=float) # containers for fields - tmp1 = zeros((3, 3), dtype=float) - tmp2 = zeros((3, 3), dtype=float) + tmp = zeros((3, 3), dtype=float) b_prod = zeros((3, 3), dtype=float) - norm_b2_prod = zeros((3, 3), dtype=float) + norm_b_prod = zeros((3, 3), dtype=float) e = empty(3, dtype=float) u = empty(3, dtype=float) bb = empty(3, dtype=float) b_star = empty(3, dtype=float) norm_b1 = empty(3, dtype=float) - norm_b2 = empty(3, dtype=float) curl_norm_b = empty(3, dtype=float) # get marker arguments @@ -2507,8 +2469,6 @@ def push_gc_cc_J2_stage_Hdiv( else: last = 0.0 - # -- removed omp: #$ omp parallel firstprivate(b_prod, norm_b2_prod) private(ip, boundary_cut, eta1, eta2, eta3, v, det_df, dfm, df_inv, df_inv_t, g_inv, span1, span2, span3, bb, u, e, curl_norm_b, norm_b1, norm_b2, b_star, tmp1, tmp2, abs_b_star_para) - # -- removed omp: #$ omp for for ip in range(n_markers): # check if marker is a hole if markers[ip, first_init_idx] == -1.0: @@ -2519,9 +2479,6 @@ def push_gc_cc_J2_stage_Hdiv( eta3 = markers[ip, 2] v = markers[ip, 3] - if eta1 < boundary_cut or eta2 > 1.0 - boundary_cut: - continue - # evaluate Jacobian, result in dfm evaluation_kernels.df( eta1, @@ -2576,18 +2533,6 @@ def push_gc_cc_J2_stage_Hdiv( norm_b1, ) - # norm_b; 2form - eval_2form_spline_mpi( - span1, - span2, - span3, - args_derham, - norm_b21, - norm_b22, - norm_b23, - norm_b2, - ) - # curl_norm_b; 2form eval_2form_spline_mpi( span1, @@ -2608,24 +2553,21 @@ def push_gc_cc_J2_stage_Hdiv( b_prod[2, 0] = -bb[1] b_prod[2, 1] = +bb[0] - norm_b2_prod[0, 1] = -norm_b2[2] - norm_b2_prod[0, 2] = +norm_b2[1] - norm_b2_prod[1, 0] = +norm_b2[2] - norm_b2_prod[1, 2] = -norm_b2[0] - norm_b2_prod[2, 0] = -norm_b2[1] - norm_b2_prod[2, 1] = +norm_b2[0] + norm_b_prod[0, 1] = -norm_b1[2] + norm_b_prod[0, 2] = +norm_b1[1] + norm_b_prod[1, 0] = +norm_b1[2] + norm_b_prod[1, 2] = -norm_b1[0] + norm_b_prod[2, 0] = -norm_b1[1] + norm_b_prod[2, 1] = +norm_b1[0] - # b_star; 2form in H1vec - b_star[:] = (bb + curl_norm_b * v * epsilon) / det_df + # b_star; 2form + b_star[:] = bb + curl_norm_b * v * epsilon # calculate abs_b_star_para abs_b_star_para = linalg_kernels.scalar_dot(norm_b1, b_star) - linalg_kernels.matrix_matrix(g_inv, norm_b2_prod, tmp1) - linalg_kernels.matrix_matrix(tmp1, g_inv, tmp2) - linalg_kernels.matrix_matrix(tmp2, b_prod, tmp1) - - linalg_kernels.matrix_vector(tmp1, u, e) + linalg_kernels.matrix_matrix(norm_b_prod, b_prod, tmp) + linalg_kernels.matrix_vector(tmp, u, e) e /= abs_b_star_para e /= det_df @@ -2640,4 +2582,367 @@ def push_gc_cc_J2_stage_Hdiv( + last * markers[ip, first_free_idx : first_free_idx + 3] ) - # -- removed omp: #$ omp end parallel + +@stack_array( + "dfm", + "df_inv", + "df_inv_t", + "g_inv", + "e", + "u", + "bb", + "b_star", + "norm_b1", + "curl_norm_b", + "tmp1", + "b_prod", + "norm_b_prod", +) +def push_gc_cc_J2_dg_init_Hdiv( + dt: float, + args_markers: "MarkerArguments", + args_domain: "DomainArguments", + args_derham: "DerhamArguments", + epsilon: float, + b1: "float[:,:,:]", + b2: "float[:,:,:]", + b3: "float[:,:,:]", + norm_b11: "float[:,:,:]", + norm_b12: "float[:,:,:]", + norm_b13: "float[:,:,:]", + curl_norm_b1: "float[:,:,:]", + curl_norm_b2: "float[:,:,:]", + curl_norm_b3: "float[:,:,:]", + u1: "float[:,:,:]", + u2: "float[:,:,:]", + u3: "float[:,:,:]", +): + r"""TODO""" + + # allocate metric coeffs + dfm = empty((3, 3), dtype=float) + df_inv = empty((3, 3), dtype=float) + df_inv_t = empty((3, 3), dtype=float) + g_inv = empty((3, 3), dtype=float) + + # containers for fields + tmp1 = zeros((3, 3), dtype=float) + b_prod = zeros((3, 3), dtype=float) + norm_b_prod = zeros((3, 3), dtype=float) + e = empty(3, dtype=float) + u = empty(3, dtype=float) + bb = empty(3, dtype=float) + b_star = empty(3, dtype=float) + norm_b1 = empty(3, dtype=float) + curl_norm_b = empty(3, dtype=float) + + # get marker arguments + markers = args_markers.markers + n_markers = args_markers.n_markers + mu_idx = args_markers.mu_idx + first_init_idx = args_markers.first_init_idx + first_free_idx = args_markers.first_free_idx + + for ip in range(n_markers): + # check if marker is a hole + if markers[ip, first_init_idx] == -1.0: + continue + + eta1 = markers[ip, 0] + eta2 = markers[ip, 1] + eta3 = markers[ip, 2] + v = markers[ip, 3] + + # evaluate Jacobian, result in dfm + evaluation_kernels.df( + eta1, + eta2, + eta3, + args_domain, + dfm, + ) + + # metric coeffs + det_df = linalg_kernels.det(dfm) + linalg_kernels.matrix_inv_with_det(dfm, det_df, df_inv) + linalg_kernels.transpose(df_inv, df_inv_t) + linalg_kernels.matrix_matrix(df_inv, df_inv_t, g_inv) + + # spline evaluation + span1, span2, span3 = get_spans(eta1, eta2, eta3, args_derham) + + # b; 2form + eval_2form_spline_mpi( + span1, + span2, + span3, + args_derham, + b1, + b2, + b3, + bb, + ) + + # u; 2form + eval_2form_spline_mpi( + span1, + span2, + span3, + args_derham, + u1, + u2, + u3, + u, + ) + + # norm_b1; 1form + eval_1form_spline_mpi( + span1, + span2, + span3, + args_derham, + norm_b11, + norm_b12, + norm_b13, + norm_b1, + ) + + # curl_norm_b; 2form + eval_2form_spline_mpi( + span1, + span2, + span3, + args_derham, + curl_norm_b1, + curl_norm_b2, + curl_norm_b3, + curl_norm_b, + ) + + # operator bx() as matrix + b_prod[0, 1] = -bb[2] + b_prod[0, 2] = +bb[1] + b_prod[1, 0] = +bb[2] + b_prod[1, 2] = -bb[0] + b_prod[2, 0] = -bb[1] + b_prod[2, 1] = +bb[0] + + norm_b_prod[0, 1] = -norm_b1[2] + norm_b_prod[0, 2] = +norm_b1[1] + norm_b_prod[1, 0] = +norm_b1[2] + norm_b_prod[1, 2] = -norm_b1[0] + norm_b_prod[2, 0] = -norm_b1[1] + norm_b_prod[2, 1] = +norm_b1[0] + + # b_star; 2form + b_star[:] = bb + curl_norm_b * v * epsilon + + # calculate 3form abs_b_star_para + abs_b_star_para = linalg_kernels.scalar_dot(norm_b1, b_star) + + linalg_kernels.matrix_matrix(norm_b_prod, b_prod, tmp1) + linalg_kernels.matrix_vector(tmp1, u, e) + + e /= abs_b_star_para + e /= det_df + + markers[ip, 0:3] -= dt * e + + +@stack_array( + "dfm", + "df_inv", + "df_inv_t", + "g_inv", + "e", + "u", + "ud", + "bb", + "b_star", + "norm_b1", + "curl_norm_b", + "tmp1", + "tmp2", + "b_prod", + "norm_b_prod", + "eta_old", + "eta_mid", +) +def push_gc_cc_J2_dg_Hdiv( + dt: float, + args_markers: "MarkerArguments", + args_domain: "DomainArguments", + args_derham: "DerhamArguments", + epsilon: float, + b1: "float[:,:,:]", + b2: "float[:,:,:]", + b3: "float[:,:,:]", + norm_b11: "float[:,:,:]", + norm_b12: "float[:,:,:]", + norm_b13: "float[:,:,:]", + curl_norm_b1: "float[:,:,:]", + curl_norm_b2: "float[:,:,:]", + curl_norm_b3: "float[:,:,:]", + u1: "float[:,:,:]", + u2: "float[:,:,:]", + u3: "float[:,:,:]", + ud1: "float[:,:,:]", + ud2: "float[:,:,:]", + ud3: "float[:,:,:]", + const: float, + alpha: float, +): + r"""TODO""" + + # allocate metric coeffs + dfm = empty((3, 3), dtype=float) + df_inv = empty((3, 3), dtype=float) + df_inv_t = empty((3, 3), dtype=float) + g_inv = empty((3, 3), dtype=float) + + # containers for fields + tmp1 = zeros((3, 3), dtype=float) + tmp2 = zeros(3, dtype=float) + b_prod = zeros((3, 3), dtype=float) + norm_b_prod = zeros((3, 3), dtype=float) + e = empty(3, dtype=float) + u = empty(3, dtype=float) + ud = empty(3, dtype=float) + bb = empty(3, dtype=float) + b_star = empty(3, dtype=float) + norm_b1 = empty(3, dtype=float) + curl_norm_b = empty(3, dtype=float) + eta_old = empty(3, dtype=float) + eta_mid = empty(3, dtype=float) + + # get marker arguments + markers = args_markers.markers + n_markers = args_markers.n_markers + mu_idx = args_markers.mu_idx + first_init_idx = args_markers.first_init_idx + first_free_idx = args_markers.first_free_idx + + for ip in range(n_markers): + # check if marker is a hole + if markers[ip, 0] == -1.0: + continue + + # marker positions, mid point + eta_old[:] = markers[ip, 0:3] + eta_mid[:] = (markers[ip, 0:3] + markers[ip, first_init_idx : first_init_idx + 3]) / 2.0 + eta_mid[:] = mod(eta_mid[:], 1.0) + + v = markers[ip, 3] + + # evaluate Jacobian, result in dfm + evaluation_kernels.df( + eta_mid[0], + eta_mid[1], + eta_mid[2], + args_domain, + dfm, + ) + + # metric coeffs + det_df = linalg_kernels.det(dfm) + linalg_kernels.matrix_inv_with_det(dfm, det_df, df_inv) + linalg_kernels.transpose(df_inv, df_inv_t) + linalg_kernels.matrix_matrix(df_inv, df_inv_t, g_inv) + + # spline evaluation + span1, span2, span3 = get_spans(eta_mid[0], eta_mid[1], eta_mid[2], args_derham) + + # b; 2form + eval_2form_spline_mpi( + span1, + span2, + span3, + args_derham, + b1, + b2, + b3, + bb, + ) + + # u; 2form + eval_2form_spline_mpi( + span1, + span2, + span3, + args_derham, + u1, + u2, + u3, + u, + ) + + # ud; 2form + eval_2form_spline_mpi( + span1, + span2, + span3, + args_derham, + ud1, + ud2, + ud3, + ud, + ) + + # norm_b1; 1form + eval_1form_spline_mpi( + span1, + span2, + span3, + args_derham, + norm_b11, + norm_b12, + norm_b13, + norm_b1, + ) + + # curl_norm_b; 2form + eval_2form_spline_mpi( + span1, + span2, + span3, + args_derham, + curl_norm_b1, + curl_norm_b2, + curl_norm_b3, + curl_norm_b, + ) + + # operator bx() as matrix + b_prod[0, 1] = -bb[2] + b_prod[0, 2] = +bb[1] + b_prod[1, 0] = +bb[2] + b_prod[1, 2] = -bb[0] + b_prod[2, 0] = -bb[1] + b_prod[2, 1] = +bb[0] + + norm_b_prod[0, 1] = -norm_b1[2] + norm_b_prod[0, 2] = +norm_b1[1] + norm_b_prod[1, 0] = +norm_b1[2] + norm_b_prod[1, 2] = -norm_b1[0] + norm_b_prod[2, 0] = -norm_b1[1] + norm_b_prod[2, 1] = +norm_b1[0] + + # b_star; 2form + b_star[:] = bb + curl_norm_b * v * epsilon + + # calculate 3form abs_b_star_para + abs_b_star_para = linalg_kernels.scalar_dot(norm_b1, b_star) + + linalg_kernels.matrix_matrix(norm_b_prod, b_prod, tmp1) + linalg_kernels.matrix_vector(tmp1, u, e) + linalg_kernels.matrix_vector(tmp1, ud, tmp2) + tmp2 *= const + + e += tmp2 + + e /= abs_b_star_para + e /= det_df + + markers[ip, 0:3] = markers[ip, first_init_idx : first_init_idx + 3] - dt * e + markers[ip, 0:3] *= alpha + markers[ip, 0:3] += eta_old * (1.0 - alpha) diff --git a/src/struphy/pic/sampling_kernels.py b/src/struphy/pic/sampling_kernels.py index 821363a97..ce68d5aff 100644 --- a/src/struphy/pic/sampling_kernels.py +++ b/src/struphy/pic/sampling_kernels.py @@ -93,13 +93,13 @@ def tile_int_kernel( Parameters ---------- - fun: np.ndarray + fun: xp.ndarray The integrand evaluated at the quadrature points (meshgrid). - x_wts, y_wts, z_wts: np.ndarray + x_wts, y_wts, z_wts: xp.ndarray Quadrature weights for tile integral. - out: np.ndarray + out: xp.ndarray The result holding all tile integrals in one sorting box.""" _shp = shape(out) diff --git a/src/struphy/pic/sobol_seq.py b/src/struphy/pic/sobol_seq.py index ff073b1b3..ce965cc8f 100644 --- a/src/struphy/pic/sobol_seq.py +++ b/src/struphy/pic/sobol_seq.py @@ -17,10 +17,9 @@ from __future__ import division +import cunumpy as xp from scipy.stats import norm -from struphy.utils.arrays import xp as np - __all__ = ["i4_bit_hi1", "i4_bit_lo0", "i4_sobol_generate", "i4_sobol", "i4_uniform", "prime_ge", "is_prime"] @@ -60,7 +59,7 @@ def i4_bit_hi1(n): Output, integer BIT, the number of bits base 2. """ - i = np.floor(n) + i = xp.floor(n) bit = 0 while i > 0: bit += 1 @@ -105,7 +104,7 @@ def i4_bit_lo0(n): Output, integer BIT, the position of the low 1 bit. """ bit = 1 - i = np.floor(n) + i = xp.floor(n) while i != 2 * (i // 2): bit += 1 i //= 2 @@ -123,7 +122,7 @@ def i4_sobol_generate(dim_num, n, skip=1): Output, real R(M,N), the points. """ - r = np.full((n, dim_num), np.nan) + r = xp.full((n, dim_num), xp.nan) for j in range(n): seed = j + skip r[j, 0:dim_num], next_seed = i4_sobol(dim_num, seed) @@ -222,8 +221,8 @@ def i4_sobol(dim_num, seed): seed_save = -1 # Initialize (part of) V. - v = np.zeros((dim_max, log_max)) - v[0:40, 0] = np.transpose( + v = xp.zeros((dim_max, log_max)) + v[0:40, 0] = xp.transpose( [ 1, 1, @@ -268,7 +267,7 @@ def i4_sobol(dim_num, seed): ] ) - v[2:40, 1] = np.transpose( + v[2:40, 1] = xp.transpose( [ 1, 3, @@ -311,7 +310,7 @@ def i4_sobol(dim_num, seed): ] ) - v[3:40, 2] = np.transpose( + v[3:40, 2] = xp.transpose( [ 7, 5, @@ -353,7 +352,7 @@ def i4_sobol(dim_num, seed): ] ) - v[5:40, 3] = np.transpose( + v[5:40, 3] = xp.transpose( [ 1, 7, @@ -393,7 +392,7 @@ def i4_sobol(dim_num, seed): ] ) - v[7:40, 4] = np.transpose( + v[7:40, 4] = xp.transpose( [ 9, 3, @@ -431,15 +430,15 @@ def i4_sobol(dim_num, seed): ] ) - v[13:40, 5] = np.transpose( + v[13:40, 5] = xp.transpose( [37, 33, 7, 5, 11, 39, 63, 27, 17, 15, 23, 29, 3, 21, 13, 31, 25, 9, 49, 33, 19, 29, 11, 19, 27, 15, 25] ) - v[19:40, 6] = np.transpose( + v[19:40, 6] = xp.transpose( [13, 33, 115, 41, 79, 17, 29, 119, 75, 73, 105, 7, 59, 65, 21, 3, 113, 61, 89, 45, 107] ) - v[37:40, 7] = np.transpose([7, 23, 39]) + v[37:40, 7] = xp.transpose([7, 23, 39]) # Set POLY. poly = [ @@ -518,7 +517,7 @@ def i4_sobol(dim_num, seed): # Expand this bit pattern to separate components of the logical array INCLUD. j = poly[i - 1] - includ = np.zeros(m) + includ = xp.zeros(m) for k in range(m, 0, -1): j2 = j // 2 includ[k - 1] = j != 2 * j2 @@ -532,7 +531,7 @@ def i4_sobol(dim_num, seed): for k in range(1, m + 1): l *= 2 if includ[k - 1]: - newv = np.bitwise_xor(int(newv), int(l * v[i - 1, j - k - 1])) + newv = xp.bitwise_xor(int(newv), int(l * v[i - 1, j - k - 1])) v[i - 1, j - 1] = newv # Multiply columns of V by appropriate power of 2. @@ -543,16 +542,16 @@ def i4_sobol(dim_num, seed): # RECIPD is 1/(common denominator of the elements in V). recipd = 1.0 / (2 * l) - lastq = np.zeros(dim_num) + lastq = xp.zeros(dim_num) - seed = int(np.floor(seed)) + seed = int(xp.floor(seed)) if seed < 0: seed = 0 l = 1 if seed == 0: - lastq = np.zeros(dim_num) + lastq = xp.zeros(dim_num) elif seed == seed_save + 1: # Find the position of the right-hand zero in SEED. @@ -560,12 +559,12 @@ def i4_sobol(dim_num, seed): elif seed <= seed_save: seed_save = 0 - lastq = np.zeros(dim_num) + lastq = xp.zeros(dim_num) for seed_temp in range(int(seed_save), int(seed)): l = i4_bit_lo0(seed_temp) for i in range(1, dim_num + 1): - lastq[i - 1] = np.bitwise_xor(int(lastq[i - 1]), int(v[i - 1, l - 1])) + lastq[i - 1] = xp.bitwise_xor(int(lastq[i - 1]), int(v[i - 1, l - 1])) l = i4_bit_lo0(seed) @@ -573,7 +572,7 @@ def i4_sobol(dim_num, seed): for seed_temp in range(int(seed_save + 1), int(seed)): l = i4_bit_lo0(seed_temp) for i in range(1, dim_num + 1): - lastq[i - 1] = np.bitwise_xor(int(lastq[i - 1]), int(v[i - 1, l - 1])) + lastq[i - 1] = xp.bitwise_xor(int(lastq[i - 1]), int(v[i - 1, l - 1])) l = i4_bit_lo0(seed) @@ -586,10 +585,10 @@ def i4_sobol(dim_num, seed): return # Calculate the new components of QUASI. - quasi = np.zeros(dim_num) + quasi = xp.zeros(dim_num) for i in range(1, dim_num + 1): quasi[i - 1] = lastq[i - 1] * recipd - lastq[i - 1] = np.bitwise_xor(int(lastq[i - 1]), int(v[i - 1, l - 1])) + lastq[i - 1] = xp.bitwise_xor(int(lastq[i - 1]), int(v[i - 1, l - 1])) seed_save = seed seed += 1 @@ -639,11 +638,11 @@ def i4_uniform(a, b, seed): print("I4_UNIFORM - Fatal error!") print(" Input SEED = 0!") - seed = np.floor(seed) + seed = xp.floor(seed) a = round(a) b = round(b) - seed = np.mod(seed, 2147483647) + seed = xp.mod(seed, 2147483647) if seed < 0: seed += 2147483647 @@ -697,7 +696,7 @@ def prime_ge(n): Output, integer P, the smallest prime number that is greater than or equal to N. """ - p = max(np.ceil(n), 2) + p = max(xp.ceil(n), 2) while not is_prime(p): p += 1 @@ -721,7 +720,7 @@ def is_prime(n): return False # All primes >3 are of the form 6n+1 or 6n+5 (6n, 6n+2, 6n+4 are 2-divisible, 6n+3 is 3-divisible) p = 5 - root = int(np.ceil(np.sqrt(n))) + root = int(xp.ceil(xp.sqrt(n))) while p <= root: if n % p == 0 or n % (p + 2) == 0: return False diff --git a/src/struphy/pic/tests/test_accum_vec_H1.py b/src/struphy/pic/tests/test_accum_vec_H1.py index f8de1b2fa..7ed52b153 100644 --- a/src/struphy/pic/tests/test_accum_vec_H1.py +++ b/src/struphy/pic/tests/test_accum_vec_H1.py @@ -47,6 +47,7 @@ def test_accum_poisson(Nel, p, spl_kind, mapping, num_clones, Np=1000): import copy + import cunumpy as xp from psydac.ddm.mpi import MockComm from psydac.ddm.mpi import mpi as MPI @@ -56,7 +57,7 @@ def test_accum_poisson(Nel, p, spl_kind, mapping, num_clones, Np=1000): from struphy.pic.accumulation import accum_kernels from struphy.pic.accumulation.particles_to_grid import AccumulatorVector from struphy.pic.particles import Particles6D - from struphy.utils.arrays import xp as np + from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters from struphy.utils.clone_config import CloneConfig if isinstance(MPI.COMM_WORLD, MockComm): @@ -75,7 +76,7 @@ def test_accum_poisson(Nel, p, spl_kind, mapping, num_clones, Np=1000): params = { "grid": {"Nel": Nel}, - "kinetic": {"test_particles": {"markers": {"Np": Np, "ppc": Np / np.prod(Nel)}}}, + "kinetic": {"test_particles": {"markers": {"Np": Np, "ppc": Np / xp.prod(Nel)}}}, } if mpi_comm is None: clone_config = None @@ -104,17 +105,16 @@ def test_accum_poisson(Nel, p, spl_kind, mapping, num_clones, Np=1000): print("Domain decomposition according to", derham.domain_array) # load distributed markers first and use Send/Receive to make global marker copies for the legacy routines - loading_params = { - "seed": 1607, - "moments": [0.0, 0.0, 0.0, 1.0, 1.0, 1.0], - "spatial": "uniform", - } + loading_params = LoadingParameters( + Np=Np, + seed=1607, + moments=(0.0, 0.0, 0.0, 1.0, 1.0, 1.0), + spatial="uniform", + ) particles = Particles6D( comm_world=mpi_comm, clone_config=clone_config, - Np=Np, - bc=["periodic"] * 3, loading_params=loading_params, domain=domain, domain_decomp=domain_decomp, @@ -129,12 +129,12 @@ def test_accum_poisson(Nel, p, spl_kind, mapping, num_clones, Np=1000): _w0 = particles.weights print("Test weights:") - print(f"rank {mpi_rank}:", _w0.shape, np.min(_w0), np.max(_w0)) + print(f"rank {mpi_rank}:", _w0.shape, xp.min(_w0), xp.max(_w0)) _sqrtg = domain.jacobian_det(0.5, 0.5, 0.5) - assert np.isclose(np.min(_w0), _sqrtg) - assert np.isclose(np.max(_w0), _sqrtg) + assert xp.isclose(xp.min(_w0), _sqrtg) + assert xp.isclose(xp.max(_w0), _sqrtg) # mass operators mass_ops = WeightedMassOperators(derham, domain) @@ -148,22 +148,22 @@ def test_accum_poisson(Nel, p, spl_kind, mapping, num_clones, Np=1000): domain.args_domain, ) - acc(particles.vdim) + acc() # sum all MC integrals - _sum_within_clone = np.empty(1, dtype=float) - _sum_within_clone[0] = np.sum(acc.vectors[0].toarray()) + _sum_within_clone = xp.empty(1, dtype=float) + _sum_within_clone[0] = xp.sum(acc.vectors[0].toarray()) if clone_config is not None: clone_config.sub_comm.Allreduce(MPI.IN_PLACE, _sum_within_clone, op=MPI.SUM) print(f"rank {mpi_rank}: {_sum_within_clone = }, {_sqrtg = }") # Check within clone - assert np.isclose(_sum_within_clone, _sqrtg) + assert xp.isclose(_sum_within_clone, _sqrtg) # Check for all clones - _sum_between_clones = np.empty(1, dtype=float) - _sum_between_clones[0] = np.sum(acc.vectors[0].toarray()) + _sum_between_clones = xp.empty(1, dtype=float) + _sum_between_clones[0] = xp.sum(acc.vectors[0].toarray()) if mpi_comm is not None: mpi_comm.Allreduce(MPI.IN_PLACE, _sum_between_clones, op=MPI.SUM) @@ -172,7 +172,7 @@ def test_accum_poisson(Nel, p, spl_kind, mapping, num_clones, Np=1000): print(f"rank {mpi_rank}: {_sum_between_clones = }, {_sqrtg = }") # Check within clone - assert np.isclose(_sum_between_clones, _sqrtg) + assert xp.isclose(_sum_between_clones, _sqrtg) if __name__ == "__main__": diff --git a/src/struphy/pic/tests/test_accumulation.py b/src/struphy/pic/tests/test_accumulation.py index f8591ca44..805c578d5 100644 --- a/src/struphy/pic/tests/test_accumulation.py +++ b/src/struphy/pic/tests/test_accumulation.py @@ -48,6 +48,7 @@ def test_accumulation(Nel, p, spl_kind, mapping, Np=40, verbose=False): def pc_lin_mhd_6d_step_ph_full(Nel, p, spl_kind, mapping, Np, verbose=False): from time import time + import cunumpy as xp from psydac.ddm.mpi import MockComm from psydac.ddm.mpi import mpi as MPI @@ -60,7 +61,7 @@ def pc_lin_mhd_6d_step_ph_full(Nel, p, spl_kind, mapping, Np, verbose=False): from struphy.pic.accumulation.particles_to_grid import Accumulator from struphy.pic.particles import Particles6D from struphy.pic.tests.test_pic_legacy_files.accumulation_kernels_3d import kernel_step_ph_full - from struphy.utils.arrays import xp as np + from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters if isinstance(MPI.COMM_WORLD, MockComm): mpi_comm = None @@ -91,12 +92,10 @@ def pc_lin_mhd_6d_step_ph_full(Nel, p, spl_kind, mapping, Np, verbose=False): print(derham.domain_array) # load distributed markers first and use Send/Receive to make global marker copies for the legacy routines - loading_params = {"seed": 1607, "moments": [0.0, 0.0, 0.0, 1.0, 2.0, 3.0], "spatial": "uniform"} + loading_params = LoadingParameters(Np=Np, seed=1607, moments=(0.0, 0.0, 0.0, 1.0, 2.0, 3.0), spatial="uniform") particles = Particles6D( comm_world=mpi_comm, - Np=Np, - bc=["periodic"] * 3, loading_params=loading_params, domain=domain, domain_decomp=domain_decomp, @@ -108,17 +107,17 @@ def pc_lin_mhd_6d_step_ph_full(Nel, p, spl_kind, mapping, Np, verbose=False): particles.markers[ ~particles.holes, 6, - ] = np.random.rand(particles.n_mks_loc) + ] = xp.random.rand(particles.n_mks_loc) # gather all particles for legacy kernel if mpi_comm is None: - marker_shapes = np.array([particles.markers.shape[0]]) + marker_shapes = xp.array([particles.markers.shape[0]]) else: - marker_shapes = np.zeros(mpi_size, dtype=int) - mpi_comm.Allgather(np.array([particles.markers.shape[0]]), marker_shapes) + marker_shapes = xp.zeros(mpi_size, dtype=int) + mpi_comm.Allgather(xp.array([particles.markers.shape[0]]), marker_shapes) print(rank, marker_shapes) - particles_leg = np.zeros( + particles_leg = xp.zeros( (sum(marker_shapes), particles.markers.shape[1]), dtype=float, ) @@ -129,7 +128,7 @@ def pc_lin_mhd_6d_step_ph_full(Nel, p, spl_kind, mapping, Np, verbose=False): cumulative_lengths = marker_shapes[0] for i in range(1, mpi_size): - arr_recv = np.zeros( + arr_recv = xp.zeros( (marker_shapes[i], particles.markers.shape[1]), dtype=float, ) @@ -162,10 +161,10 @@ def pc_lin_mhd_6d_step_ph_full(Nel, p, spl_kind, mapping, Np, verbose=False): for a in range(3): Ni = SPACES.Nbase_1form[a] - vec[a] = np.zeros((Ni[0], Ni[1], Ni[2], 3), dtype=float) + vec[a] = xp.zeros((Ni[0], Ni[1], Ni[2], 3), dtype=float) for b in range(3): - mat[a][b] = np.zeros( + mat[a][b] = xp.zeros( ( Ni[0], Ni[1], @@ -187,21 +186,21 @@ def pc_lin_mhd_6d_step_ph_full(Nel, p, spl_kind, mapping, Np, verbose=False): SPACES.T[0], SPACES.T[1], SPACES.T[2], - np.array(SPACES.p), - np.array(Nel), - np.array(SPACES.NbaseN), - np.array(SPACES.NbaseD), + xp.array(SPACES.p), + xp.array(Nel), + xp.array(SPACES.NbaseN), + xp.array(SPACES.NbaseD), particles_leg.shape[0], domain.kind_map, domain.params_numpy, domain.T[0], domain.T[1], domain.T[2], - np.array(domain.p), - np.array( + xp.array(domain.p), + xp.array( domain.Nel, ), - np.array(domain.NbaseN), + xp.array(domain.NbaseN), domain.cx, domain.cy, domain.cz, @@ -218,7 +217,7 @@ def pc_lin_mhd_6d_step_ph_full(Nel, p, spl_kind, mapping, Np, verbose=False): ) end_time = time() - tot_time = np.round(end_time - start_time, 3) + tot_time = xp.round(end_time - start_time, 3) mat[0][0] /= Np mat[0][1] /= Np @@ -248,10 +247,12 @@ def pc_lin_mhd_6d_step_ph_full(Nel, p, spl_kind, mapping, Np, verbose=False): ) start_time = time() - ACC(1.0, 1.0, 0.0) + ACC( + 1.0, + ) end_time = time() - tot_time = np.round(end_time - start_time, 3) + tot_time = xp.round(end_time - start_time, 3) if rank == 0 and verbose: print(f"Step ph New took {tot_time} seconds.") diff --git a/src/struphy/pic/tests/test_binning.py b/src/struphy/pic/tests/test_binning.py index a5457c3df..a6a1dde6e 100644 --- a/src/struphy/pic/tests/test_binning.py +++ b/src/struphy/pic/tests/test_binning.py @@ -35,13 +35,19 @@ def test_binning_6D_full_f(mapping, show_plot=False): name and specification of the mapping """ + import cunumpy as xp import matplotlib.pyplot as plt from psydac.ddm.mpi import mpi as MPI from struphy.geometry import domains + from struphy.initial import perturbations from struphy.kinetic_background.maxwellians import Maxwellian3D from struphy.pic.particles import Particles6D - from struphy.utils.arrays import xp as np + from struphy.pic.utilities import ( + BoundaryParameters, + LoadingParameters, + WeightsParameters, + ) # Set seed seed = 1234 @@ -54,19 +60,17 @@ def test_binning_6D_full_f(mapping, show_plot=False): domain = domain_class(**mapping[1]) # create particles - loading_params = { - "seed": seed, - "spatial": "uniform", - } - bc_params = ["periodic", "periodic", "periodic"] + bc_params = ("periodic", "periodic", "periodic") # =========================================== # ===== Test Maxwellian in v1 direction ===== # =========================================== + loading_params = LoadingParameters(Np=Np, seed=seed, spatial="uniform") + boundary_params = BoundaryParameters(bc=bc_params) + particles = Particles6D( - Np=Np, - bc=bc_params, loading_params=loading_params, + boundary_params=boundary_params, domain=domain, ) @@ -75,7 +79,7 @@ def test_binning_6D_full_f(mapping, show_plot=False): # test weights particles.initialize_weights() - v1_bins = np.linspace(-5.0, 5.0, 200, endpoint=True) + v1_bins = xp.linspace(-5.0, 5.0, 200, endpoint=True) dv = v1_bins[1] - v1_bins[0] binned_res, r2 = particles.binning( @@ -85,7 +89,7 @@ def test_binning_6D_full_f(mapping, show_plot=False): v1_plot = v1_bins[:-1] + dv / 2 - ana_res = 1.0 / np.sqrt(2.0 * np.pi) * np.exp(-(v1_plot**2) / 2.0) + ana_res = 1.0 / xp.sqrt(2.0 * xp.pi) * xp.exp(-(v1_plot**2) / 2.0) if show_plot: plt.plot(v1_plot, ana_res, label="Analytical result") @@ -96,7 +100,7 @@ def test_binning_6D_full_f(mapping, show_plot=False): plt.legend() plt.show() - l2_error = np.sqrt(np.sum((ana_res - binned_res) ** 2)) / np.sqrt(np.sum((ana_res) ** 2)) + l2_error = xp.sqrt(xp.sum((ana_res - binned_res) ** 2)) / xp.sqrt(xp.sum((ana_res) ** 2)) assert l2_error <= 0.02, f"Error between binned data and analytical result was {l2_error}" @@ -106,27 +110,19 @@ def test_binning_6D_full_f(mapping, show_plot=False): # test weights amp_n = 0.1 l_n = 2 - pert_params = { - "n": { - "ModesCos": { - "given_in_basis": "0", - "ls": [l_n], - "amps": [amp_n], - } - } - } + pert = perturbations.ModesCos(ls=(l_n,), amps=(amp_n,)) + maxwellian = Maxwellian3D(n=(1.0, pert)) particles = Particles6D( - Np=Np, - bc=bc_params, loading_params=loading_params, + boundary_params=boundary_params, domain=domain, - pert_params=pert_params, + background=maxwellian, ) particles.draw_markers() particles.initialize_weights() - e1_bins = np.linspace(0.0, 1.0, 200, endpoint=True) + e1_bins = xp.linspace(0.0, 1.0, 200, endpoint=True) de = e1_bins[1] - e1_bins[0] binned_res, r2 = particles.binning( @@ -136,7 +132,7 @@ def test_binning_6D_full_f(mapping, show_plot=False): e1_plot = e1_bins[:-1] + de / 2 - ana_res = 1.0 + amp_n * np.cos(2 * np.pi * l_n * e1_plot) + ana_res = 1.0 + amp_n * xp.cos(2 * xp.pi * l_n * e1_plot) if show_plot: plt.plot(e1_plot, ana_res, label="Analytical result") @@ -147,67 +143,46 @@ def test_binning_6D_full_f(mapping, show_plot=False): plt.legend() plt.show() - l2_error = np.sqrt(np.sum((ana_res - binned_res) ** 2)) / np.sqrt(np.sum((ana_res) ** 2)) + l2_error = xp.sqrt(xp.sum((ana_res - binned_res) ** 2)) / xp.sqrt(xp.sum((ana_res) ** 2)) assert l2_error <= 0.02, f"Error between binned data and analytical result was {l2_error}" # ============================================================== # ===== Test cosines for two backgrounds in eta1 direction ===== # ============================================================== - loading_params = { - "seed": seed, - "spatial": "uniform", - } n1 = 0.8 n2 = 0.2 - bckgr_params = { - "Maxwellian3D_1": { - "n": n1, - }, - "Maxwellian3D_2": { - "n": n2, - "vth1": 0.5, - "u1": 4.5, - }, - } + # test weights amp_n1 = 0.1 amp_n2 = 0.1 l_n1 = 2 l_n2 = 4 - pert_params = { - "Maxwellian3D_1": { - "n": { - "ModesCos": { - "given_in_basis": "0", - "ls": [l_n], - "amps": [amp_n], - } - } - }, - "Maxwellian3D_2": { - "n": { - "ModesCos": { - "given_in_basis": "0", - "ls": [l_n2], - "amps": [amp_n2], - } - } - }, - } - particles = Particles6D( + pert_1 = perturbations.ModesCos(ls=(l_n,), amps=(amp_n,)) + pert_2 = perturbations.ModesCos(ls=(l_n2,), amps=(amp_n2,)) + maxw_1 = Maxwellian3D(n=(n1, pert_1)) + maxw_2 = Maxwellian3D(n=(n2, pert_2), u1=(4.5, None), vth1=(0.5, None)) + background = maxw_1 + maxw_2 + + # adapt s0 for importance sampling + loading_params = LoadingParameters( Np=Np, - bc=bc_params, + seed=seed, + spatial="uniform", + moments=(2.5, 0, 0, 3, 1, 1), + ) + + particles = Particles6D( loading_params=loading_params, + boundary_params=boundary_params, domain=domain, - bckgr_params=bckgr_params, - pert_params=pert_params, + background=background, ) particles.draw_markers() particles.initialize_weights() - e1_bins = np.linspace(0.0, 1.0, 200, endpoint=True) + e1_bins = xp.linspace(0.0, 1.0, 200, endpoint=True) de = e1_bins[1] - e1_bins[0] binned_res, r2 = particles.binning( @@ -217,29 +192,28 @@ def test_binning_6D_full_f(mapping, show_plot=False): e1_plot = e1_bins[:-1] + de / 2 - ana_res = n1 + amp_n1 * np.cos(2 * np.pi * l_n1 * e1_plot) + n2 + amp_n2 * np.cos(2 * np.pi * l_n2 * e1_plot) + ana_res = n1 + amp_n1 * xp.cos(2 * xp.pi * l_n1 * e1_plot) + n2 + amp_n2 * xp.cos(2 * xp.pi * l_n2 * e1_plot) # Compare s0 and the sum of two Maxwellians if show_plot: - s0_dict = { - "n": 1.0, - "u1": particles.loading_params["moments"][0], - "u2": particles.loading_params["moments"][1], - "u3": particles.loading_params["moments"][2], - "vth1": particles.loading_params["moments"][3], - "vth2": particles.loading_params["moments"][4], - "vth3": particles.loading_params["moments"][5], - } - s0 = Maxwellian3D(maxw_params=s0_dict) + s0 = Maxwellian3D( + n=(1.0, None), + u1=(particles.loading_params.moments[0], None), + u2=(particles.loading_params.moments[1], None), + u3=(particles.loading_params.moments[2], None), + vth1=(particles.loading_params.moments[3], None), + vth2=(particles.loading_params.moments[4], None), + vth3=(particles.loading_params.moments[5], None), + ) - v1 = np.linspace(-10.0, 10.0, 400) - phase_space = np.meshgrid( - np.array([0.0]), - np.array([0.0]), - np.array([0.0]), + v1 = xp.linspace(-10.0, 10.0, 400) + phase_space = xp.meshgrid( + xp.array([0.0]), + xp.array([0.0]), + xp.array([0.0]), v1, - np.array([0.0]), - np.array([0.0]), + xp.array([0.0]), + xp.array([0.0]), ) s0_vals = s0(*phase_space).squeeze() @@ -261,7 +235,7 @@ def test_binning_6D_full_f(mapping, show_plot=False): plt.legend() plt.show() - l2_error = np.sqrt(np.sum((ana_res - binned_res) ** 2)) / np.sqrt(np.sum((ana_res) ** 2)) + l2_error = xp.sqrt(xp.sum((ana_res - binned_res) ** 2)) / xp.sqrt(xp.sum((ana_res) ** 2)) assert l2_error <= 0.04, f"Error between binned data and analytical result was {l2_error}" @@ -294,13 +268,19 @@ def test_binning_6D_delta_f(mapping, show_plot=False): name and specification of the mapping """ + import cunumpy as xp import matplotlib.pyplot as plt from psydac.ddm.mpi import mpi as MPI from struphy.geometry import domains + from struphy.initial import perturbations from struphy.kinetic_background.maxwellians import Maxwellian3D from struphy.pic.particles import DeltaFParticles6D - from struphy.utils.arrays import xp as np + from struphy.pic.utilities import ( + BoundaryParameters, + LoadingParameters, + WeightsParameters, + ) # Set seed seed = 1234 @@ -313,39 +293,30 @@ def test_binning_6D_delta_f(mapping, show_plot=False): domain = domain_class(**mapping[1]) # create particles - loading_params = { - "seed": seed, - "spatial": "uniform", - } - bc_params = ["periodic", "periodic", "periodic"] + bc_params = ("periodic", "periodic", "periodic") # ========================================= # ===== Test cosine in eta1 direction ===== # ========================================= + loading_params = LoadingParameters(Np=Np, seed=seed, spatial="uniform") + boundary_params = BoundaryParameters(bc=bc_params) + # test weights amp_n = 0.1 l_n = 2 - pert_params = { - "n": { - "ModesCos": { - "given_in_basis": "0", - "ls": [l_n], - "amps": [amp_n], - }, - } - } + pert = perturbations.ModesCos(ls=(l_n,), amps=(amp_n,)) + background = Maxwellian3D(n=(1.0, pert)) particles = DeltaFParticles6D( - Np=Np, - bc=bc_params, loading_params=loading_params, + boundary_params=boundary_params, domain=domain, - pert_params=pert_params, + background=background, ) particles.draw_markers() particles.initialize_weights() - e1_bins = np.linspace(0.0, 1.0, 200, endpoint=True) + e1_bins = xp.linspace(0.0, 1.0, 200, endpoint=True) de = e1_bins[1] - e1_bins[0] binned_res, r2 = particles.binning( @@ -355,7 +326,7 @@ def test_binning_6D_delta_f(mapping, show_plot=False): e1_plot = e1_bins[:-1] + de / 2 - ana_res = amp_n * np.cos(2 * np.pi * l_n * e1_plot) + ana_res = amp_n * xp.cos(2 * xp.pi * l_n * e1_plot) if show_plot: plt.plot(e1_plot, ana_res, label="Analytical result") @@ -366,69 +337,46 @@ def test_binning_6D_delta_f(mapping, show_plot=False): plt.legend() plt.show() - l2_error = np.sqrt(np.sum((ana_res - binned_res) ** 2)) / np.sqrt(np.sum((ana_res) ** 2)) + l2_error = xp.sqrt(xp.sum((ana_res - binned_res) ** 2)) / xp.sqrt(xp.sum((ana_res) ** 2)) assert l2_error <= 0.02, f"Error between binned data and analytical result was {l2_error}" # ============================================================== # ===== Test cosines for two backgrounds in eta1 direction ===== # ============================================================== - loading_params = { - "seed": seed, - "spatial": "uniform", - } n1 = 0.8 n2 = 0.2 - bckgr_params = { - "Maxwellian3D_1": { - "n": n1, - }, - "Maxwellian3D_2": { - "n": n2, - "vth1": 0.5, - "u1": 4.5, - }, - } + # test weights amp_n1 = 0.1 amp_n2 = 0.1 l_n1 = 2 l_n2 = 4 - pert_params = { - "Maxwellian3D_1": { - "use_background_n": False, - "n": { - "ModesCos": { - "given_in_basis": "0", - "ls": [l_n1], - "amps": [amp_n1], - } - }, - }, - "Maxwellian3D_2": { - "use_background_n": True, - "n": { - "ModesCos": { - "given_in_basis": "0", - "ls": [l_n2], - "amps": [amp_n2], - } - }, - }, - } - particles = DeltaFParticles6D( + pert_1 = perturbations.ModesCos(ls=(l_n,), amps=(amp_n,)) + pert_2 = perturbations.ModesCos(ls=(l_n2,), amps=(amp_n2,)) + maxw_1 = Maxwellian3D(n=(n1, pert_1)) + maxw_2 = Maxwellian3D(n=(n2, pert_2), u1=(4.5, None), vth1=(0.5, None)) + background = maxw_1 + maxw_2 + + # adapt s0 for importance sampling + loading_params = LoadingParameters( Np=Np, - bc=bc_params, + seed=seed, + spatial="uniform", + moments=(2.5, 0, 0, 2, 1, 1), + ) + + particles = DeltaFParticles6D( loading_params=loading_params, + boundary_params=boundary_params, domain=domain, - bckgr_params=bckgr_params, - pert_params=pert_params, + background=background, ) particles.draw_markers() particles.initialize_weights() - e1_bins = np.linspace(0.0, 1.0, 200, endpoint=True) + e1_bins = xp.linspace(0.0, 1.0, 200, endpoint=True) de = e1_bins[1] - e1_bins[0] binned_res, r2 = particles.binning( @@ -438,29 +386,28 @@ def test_binning_6D_delta_f(mapping, show_plot=False): e1_plot = e1_bins[:-1] + de / 2 - ana_res = amp_n1 * np.cos(2 * np.pi * l_n1 * e1_plot) + n2 + amp_n2 * np.cos(2 * np.pi * l_n2 * e1_plot) + ana_res = amp_n1 * xp.cos(2 * xp.pi * l_n1 * e1_plot) + amp_n2 * xp.cos(2 * xp.pi * l_n2 * e1_plot) # Compare s0 and the sum of two Maxwellians if show_plot: - s0_dict = { - "n": 1.0, - "u1": particles.loading_params["moments"][0], - "u2": particles.loading_params["moments"][1], - "u3": particles.loading_params["moments"][2], - "vth1": particles.loading_params["moments"][3], - "vth2": particles.loading_params["moments"][4], - "vth3": particles.loading_params["moments"][5], - } - s0 = Maxwellian3D(maxw_params=s0_dict) + s0 = Maxwellian3D( + n=(1.0, None), + u1=(particles.loading_params.moments[0], None), + u2=(particles.loading_params.moments[1], None), + u3=(particles.loading_params.moments[2], None), + vth1=(particles.loading_params.moments[3], None), + vth2=(particles.loading_params.moments[4], None), + vth3=(particles.loading_params.moments[5], None), + ) - v1 = np.linspace(-10.0, 10.0, 400) - phase_space = np.meshgrid( - np.array([0.0]), - np.array([0.0]), - np.array([0.0]), + v1 = xp.linspace(-10.0, 10.0, 400) + phase_space = xp.meshgrid( + xp.array([0.0]), + xp.array([0.0]), + xp.array([0.0]), v1, - np.array([0.0]), - np.array([0.0]), + xp.array([0.0]), + xp.array([0.0]), ) s0_vals = s0(*phase_space).squeeze() @@ -482,7 +429,7 @@ def test_binning_6D_delta_f(mapping, show_plot=False): plt.legend() plt.show() - l2_error = np.sqrt(np.sum((ana_res - binned_res) ** 2)) / np.sqrt(np.sum((ana_res) ** 2)) + l2_error = xp.sqrt(xp.sum((ana_res - binned_res) ** 2)) / xp.sqrt(xp.sum((ana_res) ** 2)) assert l2_error <= 0.04, f"Error between binned data and analytical result was {l2_error}" @@ -517,14 +464,20 @@ def test_binning_6D_full_f_mpi(mapping, show_plot=False): name and specification of the mapping """ + import cunumpy as xp import matplotlib.pyplot as plt from psydac.ddm.mpi import MockComm from psydac.ddm.mpi import mpi as MPI from struphy.geometry import domains + from struphy.initial import perturbations from struphy.kinetic_background.maxwellians import Maxwellian3D from struphy.pic.particles import Particles6D - from struphy.utils.arrays import xp as np + from struphy.pic.utilities import ( + BoundaryParameters, + LoadingParameters, + WeightsParameters, + ) # Set seed seed = 1234 @@ -547,19 +500,17 @@ def test_binning_6D_full_f_mpi(mapping, show_plot=False): rank = comm.Get_rank() # create particles - loading_params = { - "seed": seed, - "spatial": "uniform", - } - bc_params = ["periodic", "periodic", "periodic"] + bc_params = ("periodic", "periodic", "periodic") # =========================================== # ===== Test Maxwellian in v1 direction ===== # =========================================== + loading_params = LoadingParameters(Np=Np, seed=seed, spatial="uniform") + boundary_params = BoundaryParameters(bc=bc_params) + particles = Particles6D( - Np=Np, - bc=bc_params, loading_params=loading_params, + boundary_params=boundary_params, comm_world=comm, domain=domain, ) @@ -568,7 +519,7 @@ def test_binning_6D_full_f_mpi(mapping, show_plot=False): # test weights particles.initialize_weights() - v1_bins = np.linspace(-5.0, 5.0, 200, endpoint=True) + v1_bins = xp.linspace(-5.0, 5.0, 200, endpoint=True) dv = v1_bins[1] - v1_bins[0] binned_res, r2 = particles.binning( @@ -580,13 +531,13 @@ def test_binning_6D_full_f_mpi(mapping, show_plot=False): if comm is None: mpi_res = binned_res else: - mpi_res = np.zeros_like(binned_res) + mpi_res = xp.zeros_like(binned_res) comm.Allreduce(binned_res, mpi_res, op=MPI.SUM) comm.Barrier() v1_plot = v1_bins[:-1] + dv / 2 - ana_res = 1.0 / np.sqrt(2.0 * np.pi) * np.exp(-(v1_plot**2) / 2.0) + ana_res = 1.0 / xp.sqrt(2.0 * xp.pi) * xp.exp(-(v1_plot**2) / 2.0) if show_plot and rank == 0: plt.plot(v1_plot, ana_res, label="Analytical result") @@ -597,7 +548,7 @@ def test_binning_6D_full_f_mpi(mapping, show_plot=False): plt.legend() plt.show() - l2_error = np.sqrt(np.sum((ana_res - mpi_res) ** 2)) / np.sqrt(np.sum((ana_res) ** 2)) + l2_error = xp.sqrt(xp.sum((ana_res - mpi_res) ** 2)) / xp.sqrt(xp.sum((ana_res) ** 2)) assert l2_error <= 0.03, f"Error between binned data and analytical result was {l2_error}" @@ -607,28 +558,20 @@ def test_binning_6D_full_f_mpi(mapping, show_plot=False): # test weights amp_n = 0.1 l_n = 2 - pert_params = { - "n": { - "ModesCos": { - "given_in_basis": "0", - "ls": [l_n], - "amps": [amp_n], - } - } - } + pert = perturbations.ModesCos(ls=(l_n,), amps=(amp_n,)) + maxwellian = Maxwellian3D(n=(1.0, pert)) particles = Particles6D( - Np=Np, - bc=bc_params, loading_params=loading_params, + boundary_params=boundary_params, comm_world=comm, domain=domain, - pert_params=pert_params, + background=maxwellian, ) particles.draw_markers() particles.initialize_weights() - e1_bins = np.linspace(0.0, 1.0, 200, endpoint=True) + e1_bins = xp.linspace(0.0, 1.0, 200, endpoint=True) de = e1_bins[1] - e1_bins[0] binned_res, r2 = particles.binning( @@ -640,13 +583,13 @@ def test_binning_6D_full_f_mpi(mapping, show_plot=False): if comm is None: mpi_res = binned_res else: - mpi_res = np.zeros_like(binned_res) + mpi_res = xp.zeros_like(binned_res) comm.Allreduce(binned_res, mpi_res, op=MPI.SUM) comm.Barrier() e1_plot = e1_bins[:-1] + de / 2 - ana_res = 1.0 + amp_n * np.cos(2 * np.pi * l_n * e1_plot) + ana_res = 1.0 + amp_n * xp.cos(2 * xp.pi * l_n * e1_plot) if show_plot and rank == 0: plt.plot(e1_plot, ana_res, label="Analytical result") @@ -657,17 +600,13 @@ def test_binning_6D_full_f_mpi(mapping, show_plot=False): plt.legend() plt.show() - l2_error = np.sqrt(np.sum((ana_res - mpi_res) ** 2)) / np.sqrt(np.sum((ana_res) ** 2)) + l2_error = xp.sqrt(xp.sum((ana_res - mpi_res) ** 2)) / xp.sqrt(xp.sum((ana_res) ** 2)) assert l2_error <= 0.03, f"Error between binned data and analytical result was {l2_error}" # ============================================================== # ===== Test cosines for two backgrounds in eta1 direction ===== # ============================================================== - loading_params = { - "seed": seed, - "spatial": "uniform", - } n1 = 0.8 n2 = 0.2 bckgr_params = { @@ -705,20 +644,31 @@ def test_binning_6D_full_f_mpi(mapping, show_plot=False): } }, } + pert_1 = perturbations.ModesCos(ls=(l_n1,), amps=(amp_n1,)) + pert_2 = perturbations.ModesCos(ls=(l_n2,), amps=(amp_n2,)) + maxw_1 = Maxwellian3D(n=(n1, pert_1)) + maxw_2 = Maxwellian3D(n=(n2, pert_2), u1=(4.5, None), vth1=(0.5, None)) + background = maxw_1 + maxw_2 + + # adapt s0 for importance sampling + loading_params = LoadingParameters( + Np=Np, + seed=seed, + spatial="uniform", + moments=(2.5, 0, 0, 2, 1, 1), + ) particles = Particles6D( - Np=Np, - bc=bc_params, loading_params=loading_params, + boundary_params=boundary_params, comm_world=comm, domain=domain, - bckgr_params=bckgr_params, - pert_params=pert_params, + background=background, ) particles.draw_markers() particles.initialize_weights() - e1_bins = np.linspace(0.0, 1.0, 200, endpoint=True) + e1_bins = xp.linspace(0.0, 1.0, 200, endpoint=True) de = e1_bins[1] - e1_bins[0] binned_res, r2 = particles.binning( @@ -730,35 +680,34 @@ def test_binning_6D_full_f_mpi(mapping, show_plot=False): if comm is None: mpi_res = binned_res else: - mpi_res = np.zeros_like(binned_res) + mpi_res = xp.zeros_like(binned_res) comm.Allreduce(binned_res, mpi_res, op=MPI.SUM) comm.Barrier() e1_plot = e1_bins[:-1] + de / 2 - ana_res = n1 + amp_n1 * np.cos(2 * np.pi * l_n1 * e1_plot) + n2 + amp_n2 * np.cos(2 * np.pi * l_n2 * e1_plot) + ana_res = n1 + amp_n1 * xp.cos(2 * xp.pi * l_n1 * e1_plot) + n2 + amp_n2 * xp.cos(2 * xp.pi * l_n2 * e1_plot) # Compare s0 and the sum of two Maxwellians if show_plot and rank == 0: - s0_dict = { - "n": 1.0, - "u1": particles.loading_params["moments"][0], - "u2": particles.loading_params["moments"][1], - "u3": particles.loading_params["moments"][2], - "vth1": particles.loading_params["moments"][3], - "vth2": particles.loading_params["moments"][4], - "vth3": particles.loading_params["moments"][5], - } - s0 = Maxwellian3D(maxw_params=s0_dict) + s0 = Maxwellian3D( + n=(1.0, None), + u1=(particles.loading_params.moments[0], None), + u2=(particles.loading_params.moments[1], None), + u3=(particles.loading_params.moments[2], None), + vth1=(particles.loading_params.moments[3], None), + vth2=(particles.loading_params.moments[4], None), + vth3=(particles.loading_params.moments[5], None), + ) - v1 = np.linspace(-10.0, 10.0, 400) - phase_space = np.meshgrid( - np.array([0.0]), - np.array([0.0]), - np.array([0.0]), + v1 = xp.linspace(-10.0, 10.0, 400) + phase_space = xp.meshgrid( + xp.array([0.0]), + xp.array([0.0]), + xp.array([0.0]), v1, - np.array([0.0]), - np.array([0.0]), + xp.array([0.0]), + xp.array([0.0]), ) s0_vals = s0(*phase_space).squeeze() @@ -780,7 +729,7 @@ def test_binning_6D_full_f_mpi(mapping, show_plot=False): plt.legend() plt.show() - l2_error = np.sqrt(np.sum((ana_res - mpi_res) ** 2)) / np.sqrt(np.sum((ana_res) ** 2)) + l2_error = xp.sqrt(xp.sum((ana_res - mpi_res) ** 2)) / xp.sqrt(xp.sum((ana_res) ** 2)) assert l2_error <= 0.04, f"Error between binned data and analytical result was {l2_error}" @@ -812,14 +761,20 @@ def test_binning_6D_delta_f_mpi(mapping, show_plot=False): name and specification of the mapping """ + import cunumpy as xp import matplotlib.pyplot as plt from psydac.ddm.mpi import MockComm from psydac.ddm.mpi import mpi as MPI from struphy.geometry import domains + from struphy.initial import perturbations from struphy.kinetic_background.maxwellians import Maxwellian3D from struphy.pic.particles import DeltaFParticles6D - from struphy.utils.arrays import xp as np + from struphy.pic.utilities import ( + BoundaryParameters, + LoadingParameters, + WeightsParameters, + ) # Set seed seed = 1234 @@ -842,15 +797,14 @@ def test_binning_6D_delta_f_mpi(mapping, show_plot=False): rank = comm.Get_rank() # create particles - loading_params = { - "seed": seed, - "spatial": "uniform", - } - bc_params = ["periodic", "periodic", "periodic"] + bc_params = ("periodic", "periodic", "periodic") # ========================================= # ===== Test cosine in eta1 direction ===== # ========================================= + loading_params = LoadingParameters(Np=Np, seed=seed, spatial="uniform") + boundary_params = BoundaryParameters(bc=bc_params) + # test weights amp_n = 0.1 l_n = 2 @@ -863,19 +817,20 @@ def test_binning_6D_delta_f_mpi(mapping, show_plot=False): } } } + pert = perturbations.ModesCos(ls=(l_n,), amps=(amp_n,)) + background = Maxwellian3D(n=(1.0, pert)) particles = DeltaFParticles6D( - Np=Np, - bc=bc_params, loading_params=loading_params, + boundary_params=boundary_params, comm_world=comm, domain=domain, - pert_params=pert_params, + background=background, ) particles.draw_markers() particles.initialize_weights() - e1_bins = np.linspace(0.0, 1.0, 200, endpoint=True) + e1_bins = xp.linspace(0.0, 1.0, 200, endpoint=True) de = e1_bins[1] - e1_bins[0] binned_res, r2 = particles.binning( @@ -887,13 +842,13 @@ def test_binning_6D_delta_f_mpi(mapping, show_plot=False): if comm is None: mpi_res = binned_res else: - mpi_res = np.zeros_like(binned_res) + mpi_res = xp.zeros_like(binned_res) comm.Allreduce(binned_res, mpi_res, op=MPI.SUM) comm.Barrier() e1_plot = e1_bins[:-1] + de / 2 - ana_res = amp_n * np.cos(2 * np.pi * l_n * e1_plot) + ana_res = amp_n * xp.cos(2 * xp.pi * l_n * e1_plot) if show_plot and rank == 0: plt.plot(e1_plot, ana_res, label="Analytical result") @@ -904,17 +859,13 @@ def test_binning_6D_delta_f_mpi(mapping, show_plot=False): plt.legend() plt.show() - l2_error = np.sqrt(np.sum((ana_res - mpi_res) ** 2)) / np.sqrt(np.sum((ana_res) ** 2)) + l2_error = xp.sqrt(xp.sum((ana_res - mpi_res) ** 2)) / xp.sqrt(xp.sum((ana_res) ** 2)) assert l2_error <= 0.02, f"Error between binned data and analytical result was {l2_error}" # ============================================================== # ===== Test cosines for two backgrounds in eta1 direction ===== # ============================================================== - loading_params = { - "seed": seed, - "spatial": "uniform", - } n1 = 0.8 n2 = 0.2 bckgr_params = { @@ -954,20 +905,31 @@ def test_binning_6D_delta_f_mpi(mapping, show_plot=False): }, }, } + pert_1 = perturbations.ModesCos(ls=(l_n1,), amps=(amp_n1,)) + pert_2 = perturbations.ModesCos(ls=(l_n2,), amps=(amp_n2,)) + maxw_1 = Maxwellian3D(n=(n1, pert_1)) + maxw_2 = Maxwellian3D(n=(n2, pert_2), u1=(4.5, None), vth1=(0.5, None)) + background = maxw_1 + maxw_2 + + # adapt s0 for importance sampling + loading_params = LoadingParameters( + Np=Np, + seed=seed, + spatial="uniform", + moments=(2.5, 0, 0, 2, 1, 1), + ) particles = DeltaFParticles6D( - Np=Np, - bc=bc_params, loading_params=loading_params, + boundary_params=boundary_params, comm_world=comm, domain=domain, - bckgr_params=bckgr_params, - pert_params=pert_params, + background=background, ) particles.draw_markers() particles.initialize_weights() - e1_bins = np.linspace(0.0, 1.0, 200, endpoint=True) + e1_bins = xp.linspace(0.0, 1.0, 200, endpoint=True) de = e1_bins[1] - e1_bins[0] binned_res, r2 = particles.binning( @@ -979,35 +941,34 @@ def test_binning_6D_delta_f_mpi(mapping, show_plot=False): if comm is None: mpi_res = binned_res else: - mpi_res = np.zeros_like(binned_res) + mpi_res = xp.zeros_like(binned_res) comm.Allreduce(binned_res, mpi_res, op=MPI.SUM) comm.Barrier() e1_plot = e1_bins[:-1] + de / 2 - ana_res = amp_n1 * np.cos(2 * np.pi * l_n1 * e1_plot) + n2 + amp_n2 * np.cos(2 * np.pi * l_n2 * e1_plot) + ana_res = amp_n1 * xp.cos(2 * xp.pi * l_n1 * e1_plot) + amp_n2 * xp.cos(2 * xp.pi * l_n2 * e1_plot) # Compare s0 and the sum of two Maxwellians if show_plot and rank == 0: - s0_dict = { - "n": 1.0, - "u1": particles.loading_params["moments"][0], - "u2": particles.loading_params["moments"][1], - "u3": particles.loading_params["moments"][2], - "vth1": particles.loading_params["moments"][3], - "vth2": particles.loading_params["moments"][4], - "vth3": particles.loading_params["moments"][5], - } - s0 = Maxwellian3D(maxw_params=s0_dict) + s0 = Maxwellian3D( + n=(1.0, None), + u1=(particles.loading_params.moments[0], None), + u2=(particles.loading_params.moments[1], None), + u3=(particles.loading_params.moments[2], None), + vth1=(particles.loading_params.moments[3], None), + vth2=(particles.loading_params.moments[4], None), + vth3=(particles.loading_params.moments[5], None), + ) - v1 = np.linspace(-10.0, 10.0, 400) - phase_space = np.meshgrid( - np.array([0.0]), - np.array([0.0]), - np.array([0.0]), + v1 = xp.linspace(-10.0, 10.0, 400) + phase_space = xp.meshgrid( + xp.array([0.0]), + xp.array([0.0]), + xp.array([0.0]), v1, - np.array([0.0]), - np.array([0.0]), + xp.array([0.0]), + xp.array([0.0]), ) s0_vals = s0(*phase_space).squeeze() @@ -1029,7 +990,7 @@ def test_binning_6D_delta_f_mpi(mapping, show_plot=False): plt.legend() plt.show() - l2_error = np.sqrt(np.sum((ana_res - mpi_res) ** 2)) / np.sqrt(np.sum((ana_res) ** 2)) + l2_error = xp.sqrt(xp.sum((ana_res - mpi_res) ** 2)) / xp.sqrt(xp.sum((ana_res) ** 2)) assert l2_error <= 0.04, f"Error between binned data and analytical result was {l2_error}" diff --git a/src/struphy/pic/tests/test_draw_parallel.py b/src/struphy/pic/tests/test_draw_parallel.py index 5c1232465..cf95f4dc7 100644 --- a/src/struphy/pic/tests/test_draw_parallel.py +++ b/src/struphy/pic/tests/test_draw_parallel.py @@ -35,12 +35,13 @@ def test_draw(Nel, p, spl_kind, mapping, ppc=10): """Asserts whether all particles are on the correct process after `particles.mpi_sort_markers()`.""" + import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from struphy.feec.psydac_derham import Derham from struphy.geometry import domains from struphy.pic.particles import Particles6D - from struphy.utils.arrays import xp as np + from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -64,17 +65,16 @@ def test_draw(Nel, p, spl_kind, mapping, ppc=10): print(derham.domain_array) # create particles - loading_params = { - "seed": seed, - "moments": [0.0, 0.0, 0.0, 1.0, 1.0, 1.0], - "spatial": "uniform", - } + loading_params = LoadingParameters( + ppc=ppc, + seed=seed, + moments=(0.0, 0.0, 0.0, 1.0, 1.0, 1.0), + spatial="uniform", + ) particles = Particles6D( comm_world=comm, - ppc=ppc, domain_decomp=domain_decomp, - bc=["periodic", "periodic", "periodic"], loading_params=loading_params, domain=domain, ) @@ -85,7 +85,7 @@ def test_draw(Nel, p, spl_kind, mapping, ppc=10): particles.initialize_weights() _w0 = particles.weights print("Test weights:") - print(f"rank {rank}:", _w0.shape, np.min(_w0), np.max(_w0)) + print(f"rank {rank}:", _w0.shape, xp.min(_w0), xp.max(_w0)) comm.Barrier() print("Number of particles w/wo holes on each process before sorting : ") @@ -106,17 +106,17 @@ def test_draw(Nel, p, spl_kind, mapping, ppc=10): print("Rank", rank, ":", particles.n_mks_loc, particles.markers.shape[0]) # are all markers in the correct domain? - conds = np.logical_and( + conds = xp.logical_and( particles.markers[:, :3] > derham.domain_array[rank, 0::3], particles.markers[:, :3] < derham.domain_array[rank, 1::3], ) holes = particles.markers[:, 0] == -1.0 - stay = np.all(conds, axis=1) + stay = xp.all(conds, axis=1) - error_mks = particles.markers[np.logical_and(~stay, ~holes)] + error_mks = particles.markers[xp.logical_and(~stay, ~holes)] assert error_mks.size == 0, ( - f"rank {rank} | markers not on correct process: {np.nonzero(np.logical_and(~stay, ~holes))} \n corresponding positions:\n {error_mks[:, :3]}" + f"rank {rank} | markers not on correct process: {xp.nonzero(xp.logical_and(~stay, ~holes))} \n corresponding positions:\n {error_mks[:, :3]}" ) diff --git a/src/struphy/pic/tests/test_mat_vec_filler.py b/src/struphy/pic/tests/test_mat_vec_filler.py index 7edbf7278..491e1f20e 100644 --- a/src/struphy/pic/tests/test_mat_vec_filler.py +++ b/src/struphy/pic/tests/test_mat_vec_filler.py @@ -1,7 +1,6 @@ +import cunumpy as xp import pytest -from struphy.utils.arrays import xp as np - @pytest.mark.parametrize("Nel", [[8, 9, 10]]) @pytest.mark.parametrize("p", [[1, 2, 3]]) @@ -33,12 +32,12 @@ def test_particle_to_mat_kernels(Nel, p, spl_kind, n_markers=1): print(f"\nNel={Nel}, p={p}, spl_kind={spl_kind}\n") # DR attributes - pn = np.array(DR.p) + pn = xp.array(DR.p) tn1, tn2, tn3 = DR.Vh_fem["0"].knots starts1 = {} - starts1["v0"] = np.array(DR.Vh["0"].starts) + starts1["v0"] = xp.array(DR.Vh["0"].starts) comm.Barrier() sleep(0.02 * (rank + 1)) @@ -94,14 +93,14 @@ def test_particle_to_mat_kernels(Nel, p, spl_kind, n_markers=1): vec["v2"] += [StencilVector(DR.Vh["2"].spaces[i])._data] # Some filling for testing - fill_mat = np.reshape(np.arange(9, dtype=float), (3, 3)) + 1.0 - fill_vec = np.arange(3, dtype=float) + 1.0 + fill_mat = xp.reshape(xp.arange(9, dtype=float), (3, 3)) + 1.0 + fill_vec = xp.arange(3, dtype=float) + 1.0 # Random points in domain of process (VERY IMPORTANT to be in the right domain, otherwise NON-TRACKED errors occur in filler_kernels !!) dom = DR.domain_array[rank] - eta1s = np.random.rand(n_markers) * (dom[1] - dom[0]) + dom[0] - eta2s = np.random.rand(n_markers) * (dom[4] - dom[3]) + dom[3] - eta3s = np.random.rand(n_markers) * (dom[7] - dom[6]) + dom[6] + eta1s = xp.random.rand(n_markers) * (dom[1] - dom[0]) + dom[0] + eta2s = xp.random.rand(n_markers) * (dom[4] - dom[3]) + dom[3] + eta3s = xp.random.rand(n_markers) * (dom[7] - dom[6]) + dom[6] for eta1, eta2, eta3 in zip(eta1s, eta2s, eta3s): comm.Barrier() @@ -118,13 +117,13 @@ def test_particle_to_mat_kernels(Nel, p, spl_kind, n_markers=1): span3 = bsp.find_span(tn3, DR.p[2], eta3) # non-zero spline values at eta - bn1 = np.empty(DR.p[0] + 1, dtype=float) - bn2 = np.empty(DR.p[1] + 1, dtype=float) - bn3 = np.empty(DR.p[2] + 1, dtype=float) + bn1 = xp.empty(DR.p[0] + 1, dtype=float) + bn2 = xp.empty(DR.p[1] + 1, dtype=float) + bn3 = xp.empty(DR.p[2] + 1, dtype=float) - bd1 = np.empty(DR.p[0], dtype=float) - bd2 = np.empty(DR.p[1], dtype=float) - bd3 = np.empty(DR.p[2], dtype=float) + bd1 = xp.empty(DR.p[0], dtype=float) + bd2 = xp.empty(DR.p[1], dtype=float) + bd3 = xp.empty(DR.p[2], dtype=float) bsp.b_d_splines_slim(tn1, DR.p[0], eta1, span1, bn1, bd1) bsp.b_d_splines_slim(tn2, DR.p[1], eta2, span2, bn2, bd2) @@ -136,9 +135,9 @@ def test_particle_to_mat_kernels(Nel, p, spl_kind, n_markers=1): ie3 = span3 - pn[2] # global indices of non-vanishing B- and D-splines (no modulo) - glob_n1 = np.arange(ie1, ie1 + pn[0] + 1) - glob_n2 = np.arange(ie2, ie2 + pn[1] + 1) - glob_n3 = np.arange(ie3, ie3 + pn[2] + 1) + glob_n1 = xp.arange(ie1, ie1 + pn[0] + 1) + glob_n2 = xp.arange(ie2, ie2 + pn[1] + 1) + glob_n3 = xp.arange(ie3, ie3 + pn[2] + 1) glob_d1 = glob_n1[:-1] glob_d2 = glob_n2[:-1] @@ -164,10 +163,10 @@ def test_particle_to_mat_kernels(Nel, p, spl_kind, n_markers=1): # local column indices in _data of non-vanishing B- and D-splines, as sets for comparison cols = [{}, {}, {}] for n in range(3): - cols[n]["NN"] = set(np.arange(2 * pn[n] + 1)) - cols[n]["ND"] = set(np.arange(2 * pn[n])) - cols[n]["DN"] = set(np.arange(1, 2 * pn[n] + 1)) - cols[n]["DD"] = set(np.arange(1, 2 * pn[n])) + cols[n]["NN"] = set(xp.arange(2 * pn[n] + 1)) + cols[n]["ND"] = set(xp.arange(2 * pn[n])) + cols[n]["DN"] = set(xp.arange(1, 2 * pn[n] + 1)) + cols[n]["DD"] = set(xp.arange(1, 2 * pn[n])) # testing vector-valued spaces spaces_vector = ["v1", "v2"] @@ -337,23 +336,23 @@ def assert_mat(mat, rows, cols, row_str, col_str, rank, verbose=False): """ assert len(mat.shape) == 6 # assert non NaN - assert ~np.isnan(mat).any() + assert ~xp.isnan(mat).any() atol = 1e-14 if verbose: print(f"\n({row_str}) ({col_str})") - print(f"rank {rank} | ind_row1: {set(np.where(mat > atol)[0])}") - print(f"rank {rank} | ind_row2: {set(np.where(mat > atol)[1])}") - print(f"rank {rank} | ind_row3: {set(np.where(mat > atol)[2])}") - print(f"rank {rank} | ind_col1: {set(np.where(mat > atol)[3])}") - print(f"rank {rank} | ind_col2: {set(np.where(mat > atol)[4])}") - print(f"rank {rank} | ind_col3: {set(np.where(mat > atol)[5])}") + print(f"rank {rank} | ind_row1: {set(xp.where(mat > atol)[0])}") + print(f"rank {rank} | ind_row2: {set(xp.where(mat > atol)[1])}") + print(f"rank {rank} | ind_row3: {set(xp.where(mat > atol)[2])}") + print(f"rank {rank} | ind_col1: {set(xp.where(mat > atol)[3])}") + print(f"rank {rank} | ind_col2: {set(xp.where(mat > atol)[4])}") + print(f"rank {rank} | ind_col3: {set(xp.where(mat > atol)[5])}") # check if correct indices are non-zero for n, (r, c) in enumerate(zip(row_str, col_str)): - assert set(np.where(mat > atol)[n]) == rows[n][r] - assert set(np.where(mat > atol)[n + 3]) == cols[n][r + c] + assert set(xp.where(mat > atol)[n]) == rows[n][r] + assert set(xp.where(mat > atol)[n + 3]) == cols[n][r + c] # Set matrix back to zero mat[:, :] = 0.0 @@ -384,19 +383,19 @@ def assert_vec(vec, rows, row_str, rank, verbose=False): """ assert len(vec.shape) == 3 # assert non Nan - assert ~np.isnan(vec).any() + assert ~xp.isnan(vec).any() atol = 1e-14 if verbose: print(f"\n({row_str})") - print(f"rank {rank} | ind_row1: {set(np.where(vec > atol)[0])}") - print(f"rank {rank} | ind_row2: {set(np.where(vec > atol)[1])}") - print(f"rank {rank} | ind_row3: {set(np.where(vec > atol)[2])}") + print(f"rank {rank} | ind_row1: {set(xp.where(vec > atol)[0])}") + print(f"rank {rank} | ind_row2: {set(xp.where(vec > atol)[1])}") + print(f"rank {rank} | ind_row3: {set(xp.where(vec > atol)[2])}") # check if correct indices are non-zero for n, r in enumerate(row_str): - assert set(np.where(vec > atol)[n]) == rows[n][r] + assert set(xp.where(vec > atol)[n]) == rows[n][r] # Set vector back to zero vec[:] = 0.0 diff --git a/src/struphy/pic/tests/test_pic_legacy_files/accumulation.py b/src/struphy/pic/tests/test_pic_legacy_files/accumulation.py index 4b0cbc7ae..2ee5ba7b2 100644 --- a/src/struphy/pic/tests/test_pic_legacy_files/accumulation.py +++ b/src/struphy/pic/tests/test_pic_legacy_files/accumulation.py @@ -8,11 +8,11 @@ import time +import cunumpy as xp import scipy.sparse as spa from psydac.ddm.mpi import mpi as MPI import struphy.pic.tests.test_pic_legacy_files.accumulation_kernels_3d as pic_ker_3d -from struphy.utils.arrays import xp as np # import struphy.pic.tests.test_pic_legacy_files.accumulation_kernels_2d as pic_ker_2d @@ -69,22 +69,22 @@ def __init__(self, tensor_space_FEM, domain, basis_u, mpi_comm, use_control, cv_ else: Ni = getattr(self.space, "Nbase_" + str(self.basis_u) + "form")[a] - self.vecs_loc[a] = np.empty((Ni[0], Ni[1], Ni[2]), dtype=float) - self.vecs_glo[a] = np.empty((Ni[0], Ni[1], Ni[2]), dtype=float) + self.vecs_loc[a] = xp.empty((Ni[0], Ni[1], Ni[2]), dtype=float) + self.vecs_glo[a] = xp.empty((Ni[0], Ni[1], Ni[2]), dtype=float) for b in range(3): if self.space.dim == 2: - self.blocks_loc[a][b] = np.empty( + self.blocks_loc[a][b] = xp.empty( (Ni[0], Ni[1], Ni[2], 2 * self.space.p[0] + 1, 2 * self.space.p[1] + 1, self.space.NbaseN[2]), dtype=float, ) - self.blocks_glo[a][b] = np.empty( + self.blocks_glo[a][b] = xp.empty( (Ni[0], Ni[1], Ni[2], 2 * self.space.p[0] + 1, 2 * self.space.p[1] + 1, self.space.NbaseN[2]), dtype=float, ) else: - self.blocks_loc[a][b] = np.empty( + self.blocks_loc[a][b] = xp.empty( ( Ni[0], Ni[1], @@ -95,7 +95,7 @@ def __init__(self, tensor_space_FEM, domain, basis_u, mpi_comm, use_control, cv_ ), dtype=float, ) - self.blocks_glo[a][b] = np.empty( + self.blocks_glo[a][b] = xp.empty( ( Ni[0], Ni[1], @@ -134,16 +134,16 @@ def to_sparse_step1(self): Ni = self.space.Nbase_2form[a] Nj = self.space.Nbase_2form[b] - indices = np.indices(self.blocks_glo[a][b].shape) + indices = xp.indices(self.blocks_glo[a][b].shape) row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() - shift = [np.arange(Ni) - p for Ni, p in zip(Ni[:2], self.space.p[:2])] + shift = [xp.arange(Ni) - p for Ni, p in zip(Ni[:2], self.space.p[:2])] if self.space.dim == 2: - shift += [np.zeros(self.space.NbaseN[2], dtype=int)] + shift += [xp.zeros(self.space.NbaseN[2], dtype=int)] else: - shift += [np.arange(Ni[2]) - self.space.p[2]] + shift += [xp.arange(Ni[2]) - self.space.p[2]] col1 = (indices[3] + shift[0][:, None, None, None, None, None]) % Nj[0] col2 = (indices[4] + shift[1][None, :, None, None, None, None]) % Nj[1] @@ -201,16 +201,16 @@ def to_sparse_step3(self): Ni = self.space.Nbase_2form[a] Nj = self.space.Nbase_2form[b] - indices = np.indices(self.blocks_glo[a][b].shape) + indices = xp.indices(self.blocks_glo[a][b].shape) row = (Ni[1] * Ni[2] * indices[0] + Ni[2] * indices[1] + indices[2]).flatten() - shift = [np.arange(Ni) - p for Ni, p in zip(Ni[:2], self.space.p[:2])] + shift = [xp.arange(Ni) - p for Ni, p in zip(Ni[:2], self.space.p[:2])] if self.space.dim == 2: - shift += [np.zeros(self.space.NbaseN[2], dtype=int)] + shift += [xp.zeros(self.space.NbaseN[2], dtype=int)] else: - shift += [np.arange(Ni[2]) - self.space.p[2]] + shift += [xp.arange(Ni[2]) - self.space.p[2]] col1 = (indices[3] + shift[0][:, None, None, None, None, None]) % Nj[0] col2 = (indices[4] + shift[1][None, :, None, None, None, None]) % Nj[1] @@ -528,15 +528,15 @@ def assemble_step3(self, b2_eq, b2): # build global sparse matrix and global vector if self.basis_u == 0: return self.to_sparse_step3(), self.space.Ev_0.dot( - np.concatenate((self.vecs[0].flatten(), self.vecs[1].flatten(), self.vecs[2].flatten())) + xp.concatenate((self.vecs[0].flatten(), self.vecs[1].flatten(), self.vecs[2].flatten())) ) elif self.basis_u == 1: return self.to_sparse_step3(), self.space.E1_0.dot( - np.concatenate((self.vecs[0].flatten(), self.vecs[1].flatten(), self.vecs[2].flatten())) + xp.concatenate((self.vecs[0].flatten(), self.vecs[1].flatten(), self.vecs[2].flatten())) ) elif self.basis_u == 2: return self.to_sparse_step3(), self.space.E2_0.dot( - np.concatenate((self.vecs[0].flatten(), self.vecs[1].flatten(), self.vecs[2].flatten())) + xp.concatenate((self.vecs[0].flatten(), self.vecs[1].flatten(), self.vecs[2].flatten())) ) diff --git a/src/struphy/pic/tests/test_pic_legacy_files/pusher.py b/src/struphy/pic/tests/test_pic_legacy_files/pusher.py index 6bdb74642..518e19ee0 100644 --- a/src/struphy/pic/tests/test_pic_legacy_files/pusher.py +++ b/src/struphy/pic/tests/test_pic_legacy_files/pusher.py @@ -1,7 +1,8 @@ +import cunumpy as xp + import struphy.pic.tests.test_pic_legacy_files.pusher_pos as push_pos import struphy.pic.tests.test_pic_legacy_files.pusher_vel_2d as push_vel_2d import struphy.pic.tests.test_pic_legacy_files.pusher_vel_3d as push_vel_3d -from struphy.utils.arrays import xp as np class Pusher: diff --git a/src/struphy/pic/tests/test_pic_legacy_files/spline_evaluation_2d.py b/src/struphy/pic/tests/test_pic_legacy_files/spline_evaluation_2d.py index ba32b93bf..fdd4485b5 100644 --- a/src/struphy/pic/tests/test_pic_legacy_files/spline_evaluation_2d.py +++ b/src/struphy/pic/tests/test_pic_legacy_files/spline_evaluation_2d.py @@ -400,7 +400,7 @@ def evaluate_tensor_product( Returns: -------- - values: double[:, :] values of spline at points from np.meshgrid(eta1, eta2, indexing='ij'). + values: double[:, :] values of spline at points from xp.meshgrid(eta1, eta2, indexing='ij'). """ for i1 in range(len(eta1)): diff --git a/src/struphy/pic/tests/test_pic_legacy_files/spline_evaluation_3d.py b/src/struphy/pic/tests/test_pic_legacy_files/spline_evaluation_3d.py index 28e2b5d9c..a40d12fc9 100644 --- a/src/struphy/pic/tests/test_pic_legacy_files/spline_evaluation_3d.py +++ b/src/struphy/pic/tests/test_pic_legacy_files/spline_evaluation_3d.py @@ -806,7 +806,7 @@ def evaluate_tensor_product( Returns: -------- values: double[:, :, :] values of spline at points from - np.meshgrid(eta1, eta2, eta3, indexing='ij'). + xp.meshgrid(eta1, eta2, eta3, indexing='ij'). """ for i1 in range(len(eta1)): diff --git a/src/struphy/pic/tests/test_pushers.py b/src/struphy/pic/tests/test_pushers.py index 9dc56f127..d64076cd1 100644 --- a/src/struphy/pic/tests/test_pushers.py +++ b/src/struphy/pic/tests/test_pushers.py @@ -23,6 +23,7 @@ ], ) def test_push_vxb_analytic(Nel, p, spl_kind, mapping, show_plots=False): + import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from struphy.eigenvalue_solvers.spline_space import Spline_space_1d, Tensor_spline_space @@ -33,7 +34,7 @@ def test_push_vxb_analytic(Nel, p, spl_kind, mapping, show_plots=False): from struphy.pic.pushing import pusher_kernels from struphy.pic.pushing.pusher import Pusher as Pusher_psy from struphy.pic.tests.test_pic_legacy_files.pusher import Pusher as Pusher_str - from struphy.utils.arrays import xp as np + from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -59,14 +60,12 @@ def test_push_vxb_analytic(Nel, p, spl_kind, mapping, show_plots=False): # particle loading and sorting seed = 1234 - loader_params = {"seed": seed, "moments": [0.0, 0.0, 0.0, 1.0, 1.0, 1.0], "spatial": "uniform"} + loading_params = LoadingParameters(ppc=2, seed=seed, moments=(0.0, 0.0, 0.0, 1.0, 1.0, 1.0), spatial="uniform") particles = Particles6D( comm_world=comm, - ppc=2, domain_decomp=domain_decomp, - bc=["periodic", "periodic", "periodic"], - loading_params=loader_params, + loading_params=loading_params, ) particles.draw_markers() @@ -126,7 +125,7 @@ def test_push_vxb_analytic(Nel, p, spl_kind, mapping, show_plots=False): ) # compare if markers are the same BEFORE push - assert np.allclose(particles.markers, markers_str.T) + assert xp.allclose(particles.markers, markers_str.T) # push markers dt = 0.1 @@ -136,7 +135,7 @@ def test_push_vxb_analytic(Nel, p, spl_kind, mapping, show_plots=False): pusher_psy(dt) # compare if markers are the same AFTER push - assert np.allclose(particles.markers[:, :6], markers_str.T[:, :6]) + assert xp.allclose(particles.markers[:, :6], markers_str.T[:, :6]) @pytest.mark.parametrize("Nel", [[8, 9, 5], [7, 8, 9]]) @@ -159,6 +158,7 @@ def test_push_vxb_analytic(Nel, p, spl_kind, mapping, show_plots=False): ], ) def test_push_bxu_Hdiv(Nel, p, spl_kind, mapping, show_plots=False): + import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from struphy.eigenvalue_solvers.spline_space import Spline_space_1d, Tensor_spline_space @@ -169,7 +169,7 @@ def test_push_bxu_Hdiv(Nel, p, spl_kind, mapping, show_plots=False): from struphy.pic.pushing import pusher_kernels from struphy.pic.pushing.pusher import Pusher as Pusher_psy from struphy.pic.tests.test_pic_legacy_files.pusher import Pusher as Pusher_str - from struphy.utils.arrays import xp as np + from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -195,14 +195,12 @@ def test_push_bxu_Hdiv(Nel, p, spl_kind, mapping, show_plots=False): # particle loading and sorting seed = 1234 - loader_params = {"seed": seed, "moments": [0.0, 0.0, 0.0, 1.0, 1.0, 1.0], "spatial": "uniform"} + loading_params = LoadingParameters(ppc=2, seed=seed, moments=(0.0, 0.0, 0.0, 1.0, 1.0, 1.0), spatial="uniform") particles = Particles6D( comm_world=comm, - ppc=2, domain_decomp=domain_decomp, - bc=["periodic", "periodic", "periodic"], - loading_params=loader_params, + loading_params=loading_params, ) particles.draw_markers() @@ -252,8 +250,8 @@ def test_push_bxu_Hdiv(Nel, p, spl_kind, mapping, show_plots=False): basis_u=2, bc_pos=0, ) - mu0_str = np.zeros(markers_str.shape[1], dtype=float) - pow_str = np.zeros(markers_str.shape[1], dtype=float) + mu0_str = xp.zeros(markers_str.shape[1], dtype=float) + pow_str = xp.zeros(markers_str.shape[1], dtype=float) pusher_psy = Pusher_psy( particles, @@ -273,7 +271,7 @@ def test_push_bxu_Hdiv(Nel, p, spl_kind, mapping, show_plots=False): ) # compare if markers are the same BEFORE push - assert np.allclose(particles.markers, markers_str.T) + assert xp.allclose(particles.markers, markers_str.T) # push markers dt = 0.1 @@ -283,7 +281,7 @@ def test_push_bxu_Hdiv(Nel, p, spl_kind, mapping, show_plots=False): pusher_psy(dt) # compare if markers are the same AFTER push - assert np.allclose(particles.markers[:, :6], markers_str.T[:, :6]) + assert xp.allclose(particles.markers[:, :6], markers_str.T[:, :6]) @pytest.mark.parametrize("Nel", [[8, 9, 5], [7, 8, 9]]) @@ -306,6 +304,7 @@ def test_push_bxu_Hdiv(Nel, p, spl_kind, mapping, show_plots=False): ], ) def test_push_bxu_Hcurl(Nel, p, spl_kind, mapping, show_plots=False): + import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from struphy.eigenvalue_solvers.spline_space import Spline_space_1d, Tensor_spline_space @@ -316,7 +315,7 @@ def test_push_bxu_Hcurl(Nel, p, spl_kind, mapping, show_plots=False): from struphy.pic.pushing import pusher_kernels from struphy.pic.pushing.pusher import Pusher as Pusher_psy from struphy.pic.tests.test_pic_legacy_files.pusher import Pusher as Pusher_str - from struphy.utils.arrays import xp as np + from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -342,14 +341,12 @@ def test_push_bxu_Hcurl(Nel, p, spl_kind, mapping, show_plots=False): # particle loading and sorting seed = 1234 - loader_params = {"seed": seed, "moments": [0.0, 0.0, 0.0, 1.0, 1.0, 1.0], "spatial": "uniform"} + loading_params = LoadingParameters(ppc=2, seed=seed, moments=(0.0, 0.0, 0.0, 1.0, 1.0, 1.0), spatial="uniform") particles = Particles6D( comm_world=comm, - ppc=2, domain_decomp=domain_decomp, - bc=["periodic", "periodic", "periodic"], - loading_params=loader_params, + loading_params=loading_params, ) particles.draw_markers() @@ -399,8 +396,8 @@ def test_push_bxu_Hcurl(Nel, p, spl_kind, mapping, show_plots=False): basis_u=1, bc_pos=0, ) - mu0_str = np.zeros(markers_str.shape[1], dtype=float) - pow_str = np.zeros(markers_str.shape[1], dtype=float) + mu0_str = xp.zeros(markers_str.shape[1], dtype=float) + pow_str = xp.zeros(markers_str.shape[1], dtype=float) pusher_psy = Pusher_psy( particles, @@ -420,7 +417,7 @@ def test_push_bxu_Hcurl(Nel, p, spl_kind, mapping, show_plots=False): ) # compare if markers are the same BEFORE push - assert np.allclose(particles.markers, markers_str.T) + assert xp.allclose(particles.markers, markers_str.T) # push markers dt = 0.1 @@ -430,7 +427,7 @@ def test_push_bxu_Hcurl(Nel, p, spl_kind, mapping, show_plots=False): pusher_psy(dt) # compare if markers are the same AFTER push - assert np.allclose(particles.markers[:, :6], markers_str.T[:, :6]) + assert xp.allclose(particles.markers[:, :6], markers_str.T[:, :6]) @pytest.mark.parametrize("Nel", [[8, 9, 5], [7, 8, 9]]) @@ -453,6 +450,7 @@ def test_push_bxu_Hcurl(Nel, p, spl_kind, mapping, show_plots=False): ], ) def test_push_bxu_H1vec(Nel, p, spl_kind, mapping, show_plots=False): + import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from struphy.eigenvalue_solvers.spline_space import Spline_space_1d, Tensor_spline_space @@ -463,7 +461,7 @@ def test_push_bxu_H1vec(Nel, p, spl_kind, mapping, show_plots=False): from struphy.pic.pushing import pusher_kernels from struphy.pic.pushing.pusher import Pusher as Pusher_psy from struphy.pic.tests.test_pic_legacy_files.pusher import Pusher as Pusher_str - from struphy.utils.arrays import xp as np + from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -489,14 +487,12 @@ def test_push_bxu_H1vec(Nel, p, spl_kind, mapping, show_plots=False): # particle loading and sorting seed = 1234 - loader_params = {"seed": seed, "moments": [0.0, 0.0, 0.0, 1.0, 1.0, 1.0], "spatial": "uniform"} + loading_params = LoadingParameters(ppc=2, seed=seed, moments=(0.0, 0.0, 0.0, 1.0, 1.0, 1.0), spatial="uniform") particles = Particles6D( comm_world=comm, - ppc=2, domain_decomp=domain_decomp, - bc=["periodic", "periodic", "periodic"], - loading_params=loader_params, + loading_params=loading_params, ) particles.draw_markers() @@ -546,8 +542,8 @@ def test_push_bxu_H1vec(Nel, p, spl_kind, mapping, show_plots=False): basis_u=0, bc_pos=0, ) - mu0_str = np.zeros(markers_str.shape[1], dtype=float) - pow_str = np.zeros(markers_str.shape[1], dtype=float) + mu0_str = xp.zeros(markers_str.shape[1], dtype=float) + pow_str = xp.zeros(markers_str.shape[1], dtype=float) pusher_psy = Pusher_psy( particles, @@ -567,7 +563,7 @@ def test_push_bxu_H1vec(Nel, p, spl_kind, mapping, show_plots=False): ) # compare if markers are the same BEFORE push - assert np.allclose(particles.markers, markers_str.T) + assert xp.allclose(particles.markers, markers_str.T) # push markers dt = 0.1 @@ -577,7 +573,7 @@ def test_push_bxu_H1vec(Nel, p, spl_kind, mapping, show_plots=False): pusher_psy(dt) # compare if markers are the same AFTER push - assert np.allclose(particles.markers[:, :6], markers_str.T[:, :6]) + assert xp.allclose(particles.markers[:, :6], markers_str.T[:, :6]) @pytest.mark.parametrize("Nel", [[8, 9, 5], [7, 8, 9]]) @@ -600,6 +596,7 @@ def test_push_bxu_H1vec(Nel, p, spl_kind, mapping, show_plots=False): ], ) def test_push_bxu_Hdiv_pauli(Nel, p, spl_kind, mapping, show_plots=False): + import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from struphy.eigenvalue_solvers.spline_space import Spline_space_1d, Tensor_spline_space @@ -610,7 +607,7 @@ def test_push_bxu_Hdiv_pauli(Nel, p, spl_kind, mapping, show_plots=False): from struphy.pic.pushing import pusher_kernels from struphy.pic.pushing.pusher import Pusher as Pusher_psy from struphy.pic.tests.test_pic_legacy_files.pusher import Pusher as Pusher_str - from struphy.utils.arrays import xp as np + from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -636,14 +633,12 @@ def test_push_bxu_Hdiv_pauli(Nel, p, spl_kind, mapping, show_plots=False): # particle loading and sorting seed = 1234 - loader_params = {"seed": seed, "moments": [0.0, 0.0, 0.0, 1.0, 1.0, 1.0], "spatial": "uniform"} + loading_params = LoadingParameters(ppc=2, seed=seed, moments=(0.0, 0.0, 0.0, 1.0, 1.0, 1.0), spatial="uniform") particles = Particles6D( comm_world=comm, - ppc=2, domain_decomp=domain_decomp, - bc=["periodic", "periodic", "periodic"], - loading_params=loader_params, + loading_params=loading_params, ) particles.draw_markers() @@ -693,8 +688,8 @@ def test_push_bxu_Hdiv_pauli(Nel, p, spl_kind, mapping, show_plots=False): basis_u=2, bc_pos=0, ) - mu0_str = np.random.rand(markers_str.shape[1]) - pow_str = np.zeros(markers_str.shape[1], dtype=float) + mu0_str = xp.random.rand(markers_str.shape[1]) + pow_str = xp.zeros(markers_str.shape[1], dtype=float) pusher_psy = Pusher_psy( particles, @@ -716,7 +711,7 @@ def test_push_bxu_Hdiv_pauli(Nel, p, spl_kind, mapping, show_plots=False): ) # compare if markers are the same BEFORE push - assert np.allclose(particles.markers, markers_str.T) + assert xp.allclose(particles.markers, markers_str.T) # push markers dt = 0.1 @@ -726,7 +721,7 @@ def test_push_bxu_Hdiv_pauli(Nel, p, spl_kind, mapping, show_plots=False): pusher_psy(dt) # compare if markers are the same AFTER push - assert np.allclose(particles.markers[:, :6], markers_str.T[:, :6]) + assert xp.allclose(particles.markers[:, :6], markers_str.T[:, :6]) @pytest.mark.parametrize("Nel", [[8, 9, 5], [7, 8, 9]]) @@ -749,6 +744,7 @@ def test_push_bxu_Hdiv_pauli(Nel, p, spl_kind, mapping, show_plots=False): ], ) def test_push_eta_rk4(Nel, p, spl_kind, mapping, show_plots=False): + import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from struphy.eigenvalue_solvers.spline_space import Spline_space_1d, Tensor_spline_space @@ -760,7 +756,7 @@ def test_push_eta_rk4(Nel, p, spl_kind, mapping, show_plots=False): from struphy.pic.pushing import pusher_kernels from struphy.pic.pushing.pusher import Pusher as Pusher_psy from struphy.pic.tests.test_pic_legacy_files.pusher import Pusher as Pusher_str - from struphy.utils.arrays import xp as np + from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -787,14 +783,12 @@ def test_push_eta_rk4(Nel, p, spl_kind, mapping, show_plots=False): # particle loading and sorting seed = 1234 - loader_params = {"seed": seed, "moments": [0.0, 0.0, 0.0, 1.0, 1.0, 1.0], "spatial": "uniform"} + loading_params = LoadingParameters(ppc=2, seed=seed, moments=(0.0, 0.0, 0.0, 1.0, 1.0, 1.0), spatial="uniform") particles = Particles6D( comm_world=comm, - ppc=2, domain_decomp=domain_decomp, - bc=["periodic", "periodic", "periodic"], - loading_params=loader_params, + loading_params=loading_params, ) particles.draw_markers() @@ -836,8 +830,8 @@ def test_push_eta_rk4(Nel, p, spl_kind, mapping, show_plots=False): butcher = ButcherTableau("rk4") # temp fix due to refactoring of ButcherTableau: - butcher._a = np.diag(butcher.a, k=-1) - butcher._a = np.array(list(butcher._a) + [0.0]) + butcher._a = xp.diag(butcher.a, k=-1) + butcher._a = xp.array(list(butcher._a) + [0.0]) pusher_psy = Pusher_psy( particles, @@ -849,7 +843,7 @@ def test_push_eta_rk4(Nel, p, spl_kind, mapping, show_plots=False): ) # compare if markers are the same BEFORE push - assert np.allclose(particles.markers, markers_str.T) + assert xp.allclose(particles.markers, markers_str.T) # push markers dt = 0.1 @@ -857,12 +851,12 @@ def test_push_eta_rk4(Nel, p, spl_kind, mapping, show_plots=False): pusher_str.push_step4(markers_str, dt) pusher_psy(dt) - n_mks_load = np.zeros(size, dtype=int) + n_mks_load = xp.zeros(size, dtype=int) - comm.Allgather(np.array(np.shape(particles.markers)[0]), n_mks_load) + comm.Allgather(xp.array(xp.shape(particles.markers)[0]), n_mks_load) - sendcounts = np.zeros(size, dtype=int) - displacements = np.zeros(size, dtype=int) + sendcounts = xp.zeros(size, dtype=int) + displacements = xp.zeros(size, dtype=int) accum_sendcounts = 0.0 for i in range(size): @@ -870,18 +864,18 @@ def test_push_eta_rk4(Nel, p, spl_kind, mapping, show_plots=False): displacements[i] = accum_sendcounts accum_sendcounts += sendcounts[i] - all_particles_psy = np.zeros((int(accum_sendcounts) * 3,), dtype=float) - all_particles_str = np.zeros((int(accum_sendcounts) * 3,), dtype=float) + all_particles_psy = xp.zeros((int(accum_sendcounts) * 3,), dtype=float) + all_particles_str = xp.zeros((int(accum_sendcounts) * 3,), dtype=float) comm.Barrier() - comm.Allgatherv(np.array(particles.markers[:, :3]), [all_particles_psy, sendcounts, displacements, MPI.DOUBLE]) - comm.Allgatherv(np.array(markers_str.T[:, :3]), [all_particles_str, sendcounts, displacements, MPI.DOUBLE]) + comm.Allgatherv(xp.array(particles.markers[:, :3]), [all_particles_psy, sendcounts, displacements, MPI.DOUBLE]) + comm.Allgatherv(xp.array(markers_str.T[:, :3]), [all_particles_str, sendcounts, displacements, MPI.DOUBLE]) comm.Barrier() - unique_psy = np.unique(all_particles_psy) - unique_str = np.unique(all_particles_str) + unique_psy = xp.unique(all_particles_psy) + unique_str = xp.unique(all_particles_str) - assert np.allclose(unique_psy, unique_str) + assert xp.allclose(unique_psy, unique_str) if __name__ == "__main__": diff --git a/src/struphy/pic/tests/test_sorting.py b/src/struphy/pic/tests/test_sorting.py index 48b7cad6f..337dfaede 100644 --- a/src/struphy/pic/tests/test_sorting.py +++ b/src/struphy/pic/tests/test_sorting.py @@ -1,12 +1,13 @@ from time import time +import cunumpy as xp import pytest from psydac.ddm.mpi import mpi as MPI from struphy.feec.psydac_derham import Derham from struphy.geometry import domains from struphy.pic.particles import Particles6D -from struphy.utils.arrays import xp as np +from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters @pytest.mark.parametrize("nx", [8, 70]) @@ -16,9 +17,49 @@ def test_flattening(nx, ny, nz, algo): from struphy.pic.sorting_kernels import flatten_index, unflatten_index - n1s = np.array(np.random.rand(10) * (nx + 1), dtype=int) - n2s = np.array(np.random.rand(10) * (ny + 1), dtype=int) - n3s = np.array(np.random.rand(10) * (nz + 1), dtype=int) + n1s = xp.array(xp.random.rand(10) * (nx + 1), dtype=int) + n2s = xp.array(xp.random.rand(10) * (ny + 1), dtype=int) + n3s = xp.array(xp.random.rand(10) * (nz + 1), dtype=int) + for n1 in n1s: + for n2 in n2s: + for n3 in n3s: + n_glob = flatten_index(int(n1), int(n2), int(n3), nx, ny, nz, algo) + n1n, n2n, n3n = unflatten_index(n_glob, nx, ny, nz, algo) + assert n1n == n1 + assert n2n == n2 + assert n3n == n3 + + +@pytest.mark.parametrize("nx", [8, 70]) +@pytest.mark.parametrize("ny", [16, 80]) +@pytest.mark.parametrize("nz", [32, 90]) +@pytest.mark.parametrize("algo", ["fortran_ordering", "c_ordering"]) +def test_flattening(nx, ny, nz, algo): + from struphy.pic.sorting_kernels import flatten_index, unflatten_index + + n1s = xp.array(xp.random.rand(10) * (nx + 1), dtype=int) + n2s = xp.array(xp.random.rand(10) * (ny + 1), dtype=int) + n3s = xp.array(xp.random.rand(10) * (nz + 1), dtype=int) + for n1 in n1s: + for n2 in n2s: + for n3 in n3s: + n_glob = flatten_index(int(n1), int(n2), int(n3), nx, ny, nz, algo) + n1n, n2n, n3n = unflatten_index(n_glob, nx, ny, nz, algo) + assert n1n == n1 + assert n2n == n2 + assert n3n == n3 + + +@pytest.mark.parametrize("nx", [8, 70]) +@pytest.mark.parametrize("ny", [16, 80]) +@pytest.mark.parametrize("nz", [32, 90]) +@pytest.mark.parametrize("algo", ["fortran_ordering", "c_ordering"]) +def test_flattening(nx, ny, nz, algo): + from struphy.pic.sorting_kernels import flatten_index, unflatten_index + + n1s = xp.array(xp.random.rand(10) * (nx + 1), dtype=int) + n2s = xp.array(xp.random.rand(10) * (ny + 1), dtype=int) + n3s = xp.array(xp.random.rand(10) * (nz + 1), dtype=int) for n1 in n1s: for n2 in n2s: for n3 in n3s: @@ -69,13 +110,11 @@ def test_sorting(Nel, p, spl_kind, mapping, Np, verbose=False): nprocs = derham.domain_decomposition.nprocs domain_decomp = (domain_array, nprocs) - loading_params = {"seed": 1607, "moments": [0.0, 0.0, 0.0, 1.0, 2.0, 3.0], "spatial": "uniform"} + loading_params = LoadingParameters(Np=Np, seed=1607, moments=(0.0, 0.0, 0.0, 1.0, 2.0, 3.0), spatial="uniform") boxes_per_dim = (3, 3, 6) particles = Particles6D( comm_world=mpi_comm, - Np=Np, - bc=["periodic", "periodic", "periodic"], loading_params=loading_params, domain_decomp=domain_decomp, boxes_per_dim=boxes_per_dim, diff --git a/src/struphy/pic/tests/test_sph.py b/src/struphy/pic/tests/test_sph.py index 889941b88..64b7b0cc3 100644 --- a/src/struphy/pic/tests/test_sph.py +++ b/src/struphy/pic/tests/test_sph.py @@ -1,11 +1,14 @@ +import cunumpy as xp import pytest from matplotlib import pyplot as plt from psydac.ddm.mpi import MockComm from psydac.ddm.mpi import mpi as MPI +from struphy.fields_background.equils import ConstantVelocity from struphy.geometry import domains +from struphy.initial import perturbations from struphy.pic.particles import ParticlesSPH -from struphy.utils.arrays import xp as np +from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters @pytest.mark.parametrize("boxes_per_dim", [(24, 1, 1)]) @@ -37,56 +40,48 @@ def test_sph_evaluation_1d( domain = domain_class(**dom_params) if tesselation: - loading = "tesselation" - loading_params = {"n_quad": 1} if kernel == "trigonometric_1d" and derivative == 1: ppb = 100 else: ppb = 4 + loading_params = LoadingParameters(ppb=ppb, seed=1607, loading="tesselation") else: - loading = "pseudo_random" - loading_params = {"seed": 223} if derivative == 0: ppb = 1000 else: ppb = 20000 + loading_params = LoadingParameters(ppb=ppb, seed=223) # background - cst_vel = {"density_profile": "constant", "n": 1.5} - bckgr_params = {"ConstantVelocity": cst_vel, "pforms": ["vol", None]} + background = ConstantVelocity(n=1.5, density_profile="constant") + background.domain = domain - mode_params = {"given_in_basis": "0", "ls": [1], "amps": [1e-0]} - modes = {"ModesCos": mode_params} - pert_params = {"n": modes} + pert = {"n": perturbations.ModesCos(ls=(1,), amps=(1e-0,))} if derivative == 0: - fun_exact = lambda e1, e2, e3: 1.5 + np.cos(2 * np.pi * e1) + fun_exact = lambda e1, e2, e3: 1.5 + xp.cos(2 * xp.pi * e1) else: - fun_exact = lambda e1, e2, e3: -2 * np.pi * np.sin(2 * np.pi * e1) + fun_exact = lambda e1, e2, e3: -2 * xp.pi * xp.sin(2 * xp.pi * e1) - # boundary conditions - bc_sph = [bc_x, "periodic", "periodic"] - - # eval points - eta1 = np.linspace(0, 1.0, eval_pts) - eta2 = np.array([0.0]) - eta3 = np.array([0.0]) + boundary_params = BoundaryParameters(bc_sph=(bc_x, "periodic", "periodic")) - # particles object particles = ParticlesSPH( comm_world=comm, - ppb=ppb, + loading_params=loading_params, + boundary_params=boundary_params, boxes_per_dim=boxes_per_dim, - bc_sph=bc_sph, bufsize=1.0, - loading=loading, - loading_params=loading_params, domain=domain, - bckgr_params=bckgr_params, - pert_params=pert_params, - verbose=False, + background=background, + perturbations=pert, + n_as_volume_form=True, ) + # eval points + eta1 = xp.linspace(0, 1.0, eval_pts) + eta2 = xp.array([0.0]) + eta3 = xp.array([0.0]) + particles.draw_markers(sort=False, verbose=False) if comm is not None: particles.mpi_sort_markers() @@ -94,7 +89,7 @@ def test_sph_evaluation_1d( h1 = 1 / boxes_per_dim[0] h2 = 1 / boxes_per_dim[1] h3 = 1 / boxes_per_dim[2] - ee1, ee2, ee3 = np.meshgrid(eta1, eta2, eta3, indexing="ij") + ee1, ee2, ee3 = xp.meshgrid(eta1, eta2, eta3, indexing="ij") test_eval = particles.eval_density( ee1, ee2, @@ -109,11 +104,11 @@ def test_sph_evaluation_1d( if comm is None: all_eval = test_eval else: - all_eval = np.zeros_like(test_eval) + all_eval = xp.zeros_like(test_eval) comm.Allreduce(test_eval, all_eval, op=MPI.SUM) exact_eval = fun_exact(ee1, ee2, ee3) - err_max_norm = np.max(np.abs(all_eval - exact_eval)) / np.max(np.abs(exact_eval)) + err_max_norm = xp.max(xp.abs(all_eval - exact_eval)) / xp.max(xp.abs(exact_eval)) if rank == 0: print(f"\n{boxes_per_dim = }") @@ -169,48 +164,45 @@ def test_sph_evaluation_2d( domain_class = getattr(domains, dom_type) domain = domain_class(**dom_params) - loading = "tesselation" - loading_params = {"n_quad": 1} if kernel == "trigonometric_2d" and derivative != 0: ppb = 100 else: ppb = 16 + loading_params = LoadingParameters(ppb=ppb, loading="tesselation") + # background - cst_vel = {"density_profile": "constant", "n": 1.5} - bckgr_params = {"ConstantVelocity": cst_vel, "pforms": ["vol", None]} + background = ConstantVelocity(n=1.5, density_profile="constant") + background.domain = domain - mode_params = {"given_in_basis": "0", "ls": [1], "ms": [1], "amps": [1.0]} - modes = {"ModesCosCos": mode_params} - pert_params = {"n": modes} + pert = {"n": perturbations.ModesCosCos(ls=(1,), ms=(1,), amps=(1e-0,))} if derivative == 0: - fun_exact = lambda e1, e2, e3: 1.5 + np.cos(2 * np.pi * e1) * np.cos(2 * np.pi * e2) + fun_exact = lambda e1, e2, e3: 1.5 + xp.cos(2 * xp.pi * e1) * xp.cos(2 * xp.pi * e2) elif derivative == 1: - fun_exact = lambda e1, e2, e3: -2 * np.pi * np.sin(2 * np.pi * e1) * np.cos(2 * np.pi * e2) + fun_exact = lambda e1, e2, e3: -2 * xp.pi * xp.sin(2 * xp.pi * e1) * xp.cos(2 * xp.pi * e2) else: - fun_exact = lambda e1, e2, e3: -2 * np.pi * np.cos(2 * np.pi * e1) * np.sin(2 * np.pi * e2) + fun_exact = lambda e1, e2, e3: -2 * xp.pi * xp.cos(2 * xp.pi * e1) * xp.sin(2 * xp.pi * e2) # boundary conditions - bc_sph = [bc_x, bc_y, "periodic"] + boundary_params = BoundaryParameters(bc_sph=(bc_x, bc_y, "periodic")) # eval points - eta1 = np.linspace(0, 1.0, eval_pts) - eta2 = np.linspace(0, 1.0, eval_pts) - eta3 = np.array([0.0]) + eta1 = xp.linspace(0, 1.0, eval_pts) + eta2 = xp.linspace(0, 1.0, eval_pts) + eta3 = xp.array([0.0]) # particles object particles = ParticlesSPH( comm_world=comm, - ppb=ppb, + loading_params=loading_params, + boundary_params=boundary_params, boxes_per_dim=boxes_per_dim, - bc_sph=bc_sph, bufsize=1.0, - loading=loading, - loading_params=loading_params, domain=domain, - bckgr_params=bckgr_params, - pert_params=pert_params, + background=background, + perturbations=pert, + n_as_volume_form=True, verbose=False, ) @@ -221,7 +213,7 @@ def test_sph_evaluation_2d( h1 = 1 / boxes_per_dim[0] h2 = 1 / boxes_per_dim[1] h3 = 1 / boxes_per_dim[2] - ee1, ee2, ee3 = np.meshgrid(eta1, eta2, eta3, indexing="ij") + ee1, ee2, ee3 = xp.meshgrid(eta1, eta2, eta3, indexing="ij") test_eval = particles.eval_density( ee1, ee2, @@ -236,11 +228,11 @@ def test_sph_evaluation_2d( if comm is None: all_eval = test_eval else: - all_eval = np.zeros_like(test_eval) + all_eval = xp.zeros_like(test_eval) comm.Allreduce(test_eval, all_eval, op=MPI.SUM) exact_eval = fun_exact(ee1, ee2, ee3) - err_max_norm = np.max(np.abs(all_eval - exact_eval)) / np.max(np.abs(exact_eval)) + err_max_norm = xp.max(xp.abs(all_eval - exact_eval)) / xp.max(xp.abs(exact_eval)) if rank == 0: print(f"\n{boxes_per_dim = }") @@ -296,16 +288,16 @@ def test_sph_evaluation_3d( domain_class = getattr(domains, dom_type) domain = domain_class(**dom_params) - loading = "tesselation" - loading_params = {"n_quad": 1} if kernel in ("trigonometric_3d", "linear_isotropic_3d") and derivative != 0: ppb = 100 else: ppb = 64 + loading_params = LoadingParameters(ppb=ppb, loading="tesselation") + # background - cst_vel = {"density_profile": "constant", "n": 1.5} - bckgr_params = {"ConstantVelocity": cst_vel, "pforms": ["vol", None]} + background = ConstantVelocity(n=1.5, density_profile="constant") + background.domain = domain if derivative == 0: fun_exact = lambda e1, e2, e3: 1.5 + 0.0 * e1 @@ -313,25 +305,23 @@ def test_sph_evaluation_3d( fun_exact = lambda e1, e2, e3: 0.0 * e1 # boundary conditions - bc_sph = [bc_x, bc_y, bc_z] + boundary_params = BoundaryParameters(bc_sph=(bc_x, bc_y, bc_z)) # eval points - eta1 = np.linspace(0, 1.0, eval_pts) - eta2 = np.linspace(0, 1.0, eval_pts) - eta3 = np.linspace(0, 1.0, eval_pts) + eta1 = xp.linspace(0, 1.0, eval_pts) + eta2 = xp.linspace(0, 1.0, eval_pts) + eta3 = xp.linspace(0, 1.0, eval_pts) # particles object particles = ParticlesSPH( comm_world=comm, - ppb=ppb, + loading_params=loading_params, + boundary_params=boundary_params, boxes_per_dim=boxes_per_dim, - bc_sph=bc_sph, bufsize=2.0, - loading=loading, - loading_params=loading_params, domain=domain, - bckgr_params=bckgr_params, - # pert_params=pert_params, + background=background, + n_as_volume_form=True, verbose=False, ) @@ -342,7 +332,7 @@ def test_sph_evaluation_3d( h1 = 1 / boxes_per_dim[0] h2 = 1 / boxes_per_dim[1] h3 = 1 / boxes_per_dim[2] - ee1, ee2, ee3 = np.meshgrid(eta1, eta2, eta3, indexing="ij") + ee1, ee2, ee3 = xp.meshgrid(eta1, eta2, eta3, indexing="ij") test_eval = particles.eval_density( ee1, ee2, @@ -357,11 +347,11 @@ def test_sph_evaluation_3d( if comm is None: all_eval = test_eval else: - all_eval = np.zeros_like(test_eval) + all_eval = xp.zeros_like(test_eval) comm.Allreduce(test_eval, all_eval, op=MPI.SUM) exact_eval = fun_exact(ee1, ee2, ee3) - err_max_norm = np.max(np.abs(all_eval - exact_eval)) + err_max_norm = xp.max(xp.abs(all_eval - exact_eval)) if rank == 0: print(f"\n{boxes_per_dim = }") @@ -419,53 +409,52 @@ def test_evaluation_SPH_Np_convergence_1d(boxes_per_dim, bc_x, eval_pts, tessela domain = domain_class(**dom_params) if tesselation: - loading = "tesselation" - loading_params = {"n_quad": 1} - # ppbs = [5000, 10000, 15000, 20000, 25000] ppbs = [4, 8, 16, 32, 64] Nps = [None] * len(ppbs) else: - loading = "pseudo_random" - loading_params = {"seed": 1607} Nps = [(2**k) * 10**3 for k in range(-2, 9)] ppbs = [None] * len(Nps) # background - cst_vel = {"density_profile": "constant", "n": 1.5} - bckgr_params = {"ConstantVelocity": cst_vel, "pforms": ["vol", None]} + background = ConstantVelocity(n=1.5, density_profile="constant") + background.domain = domain - # perturbation - mode_params = {"given_in_basis": "0", "ls": [1], "amps": [-1e-0]} + # perturbation]} if bc_x in ("periodic", "fixed"): - fun_exact = lambda e1, e2, e3: 1.5 - np.sin(2 * np.pi * e1) - modes = {"ModesSin": mode_params} + fun_exact = lambda e1, e2, e3: 1.5 - xp.sin(2 * xp.pi * e1) + pert = {"n": perturbations.ModesSin(ls=(1,), amps=(-1e-0,))} elif bc_x == "mirror": - fun_exact = lambda e1, e2, e3: 1.5 - np.cos(2 * np.pi * e1) - modes = {"ModesCos": mode_params} - pert_params = {"n": modes} + fun_exact = lambda e1, e2, e3: 1.5 - xp.cos(2 * xp.pi * e1) + pert = {"n": perturbations.ModesCos(ls=(1,), amps=(-1e-0,))} # exact solution - eta1 = np.linspace(0, 1.0, eval_pts) # add offset for non-periodic boundary conditions, TODO: implement Neumann - eta2 = np.array([0.0]) - eta3 = np.array([0.0]) - ee1, ee2, ee3 = np.meshgrid(eta1, eta2, eta3, indexing="ij") + eta1 = xp.linspace(0, 1.0, eval_pts) # add offset for non-periodic boundary conditions, TODO: implement Neumann + eta2 = xp.array([0.0]) + eta3 = xp.array([0.0]) + ee1, ee2, ee3 = xp.meshgrid(eta1, eta2, eta3, indexing="ij") exact_eval = fun_exact(ee1, ee2, ee3) + # boundary conditions + boundary_params = BoundaryParameters(bc_sph=(bc_x, "periodic", "periodic")) + # loop err_vec = [] for Np, ppb in zip(Nps, ppbs): + if tesselation: + loading_params = LoadingParameters(ppb=ppb, loading="tesselation") + else: + loading_params = LoadingParameters(Np=Np, seed=1607) + particles = ParticlesSPH( comm_world=comm, - Np=Np, - ppb=ppb, + loading_params=loading_params, + boundary_params=boundary_params, boxes_per_dim=boxes_per_dim, - bc_sph=[bc_x, "periodic", "periodic"], bufsize=1.0, - loading=loading, - loading_params=loading_params, domain=domain, - bckgr_params=bckgr_params, - pert_params=pert_params, + background=background, + perturbations=pert, + n_as_volume_form=True, verbose=False, ) @@ -482,7 +471,7 @@ def test_evaluation_SPH_Np_convergence_1d(boxes_per_dim, bc_x, eval_pts, tessela if comm is None: all_eval = test_eval else: - all_eval = np.zeros_like(test_eval) + all_eval = xp.zeros_like(test_eval) comm.Allreduce(test_eval, all_eval, op=MPI.SUM) if show_plot and rank == 0: @@ -492,21 +481,21 @@ def test_evaluation_SPH_Np_convergence_1d(boxes_per_dim, bc_x, eval_pts, tessela plt.title(f"{Np = }, {ppb = }") # plt.savefig(f"fun_{Np}_{ppb}.png") - diff = np.max(np.abs(all_eval - exact_eval)) / np.max(np.abs(exact_eval)) + diff = xp.max(xp.abs(all_eval - exact_eval)) / xp.max(xp.abs(exact_eval)) err_vec += [diff] print(f"{Np = }, {ppb = }, {diff = }") if tesselation: - fit = np.polyfit(np.log(ppbs), np.log(err_vec), 1) + fit = xp.polyfit(xp.log(ppbs), xp.log(err_vec), 1) xvec = ppbs else: - fit = np.polyfit(np.log(Nps), np.log(err_vec), 1) + fit = xp.polyfit(xp.log(Nps), xp.log(err_vec), 1) xvec = Nps if show_plot and rank == 0: plt.figure(figsize=(12, 8)) plt.loglog(xvec, err_vec, label="Convergence") - plt.loglog(xvec, np.exp(fit[1]) * np.array(xvec) ** (fit[0]), "--", label=f"fit with slope {fit[0]}") + plt.loglog(xvec, xp.exp(fit[1]) * xp.array(xvec) ** (fit[0]), "--", label=f"fit with slope {fit[0]}") plt.legend() plt.show() # plt.savefig(f"Convergence_SPH_{tesselation=}") @@ -517,7 +506,7 @@ def test_evaluation_SPH_Np_convergence_1d(boxes_per_dim, bc_x, eval_pts, tessela if tesselation: assert fit[0] < 2e-3 else: - assert np.abs(fit[0] + 0.5) < 0.1 # Monte Carlo rate + assert xp.abs(fit[0] + 0.5) < 0.1 # Monte Carlo rate @pytest.mark.parametrize("boxes_per_dim", [(12, 1, 1)]) @@ -539,52 +528,50 @@ def test_evaluation_SPH_h_convergence_1d(boxes_per_dim, bc_x, eval_pts, tesselat domain = domain_class(**dom_params) if tesselation: - loading = "tesselation" - loading_params = {"seed": 1607} Np = None ppb = 160 + loading_params = LoadingParameters(ppb=ppb, loading="tesselation") else: - loading = "pseudo_random" - loading_params = {"seed": 1607} Np = 160000 ppb = None + loading_params = LoadingParameters(Np=Np, ppb=ppb, seed=1607) - cst_vel = {"density_profile": "constant", "n": 1.5} - bckgr_params = {"ConstantVelocity": cst_vel, "pforms": ["vol", None]} + # background + background = ConstantVelocity(n=1.5, density_profile="constant") + background.domain = domain # perturbation - mode_params = {"given_in_basis": "0", "ls": [1], "amps": [-1e-0]} if bc_x in ("periodic", "fixed"): - fun_exact = lambda e1, e2, e3: 1.5 - np.sin(2 * np.pi * e1) - modes = {"ModesSin": mode_params} + fun_exact = lambda e1, e2, e3: 1.5 - xp.sin(2 * xp.pi * e1) + pert = {"n": perturbations.ModesSin(ls=(1,), amps=(-1e-0,))} elif bc_x == "mirror": - fun_exact = lambda e1, e2, e3: 1.5 - np.cos(2 * np.pi * e1) - modes = {"ModesCos": mode_params} - pert_params = {"n": modes} + fun_exact = lambda e1, e2, e3: 1.5 - xp.cos(2 * xp.pi * e1) + pert = {"n": perturbations.ModesCos(ls=(1,), amps=(-1e-0,))} # exact solution - eta1 = np.linspace(0, 1.0, eval_pts) # add offset for non-periodic boundary conditions, TODO: implement Neumann - eta2 = np.array([0.0]) - eta3 = np.array([0.0]) - ee1, ee2, ee3 = np.meshgrid(eta1, eta2, eta3, indexing="ij") + eta1 = xp.linspace(0, 1.0, eval_pts) # add offset for non-periodic boundary conditions, TODO: implement Neumann + eta2 = xp.array([0.0]) + eta3 = xp.array([0.0]) + ee1, ee2, ee3 = xp.meshgrid(eta1, eta2, eta3, indexing="ij") exact_eval = fun_exact(ee1, ee2, ee3) - # parameters + # boundary conditions + boundary_params = BoundaryParameters(bc_sph=(bc_x, "periodic", "periodic")) + + # loop h_vec = [((2**k) * 10**-3 * 0.25) for k in range(2, 12)] err_vec = [] for h1 in h_vec: particles = ParticlesSPH( comm_world=comm, - Np=Np, - ppb=ppb, + loading_params=loading_params, + boundary_params=boundary_params, boxes_per_dim=boxes_per_dim, - bc_sph=[bc_x, "periodic", "periodic"], bufsize=1.0, - loading=loading, - loading_params=loading_params, domain=domain, - bckgr_params=bckgr_params, - pert_params=pert_params, + background=background, + perturbations=pert, + n_as_volume_form=True, verbose=False, ) @@ -600,7 +587,7 @@ def test_evaluation_SPH_h_convergence_1d(boxes_per_dim, bc_x, eval_pts, tesselat if comm is None: all_eval = test_eval else: - all_eval = np.zeros_like(test_eval) + all_eval = xp.zeros_like(test_eval) comm.Allreduce(test_eval, all_eval, op=MPI.SUM) if show_plot and rank == 0: @@ -611,7 +598,7 @@ def test_evaluation_SPH_h_convergence_1d(boxes_per_dim, bc_x, eval_pts, tesselat # plt.savefig(f"fun_{h1}.png") # error in max-norm - diff = np.max(np.abs(all_eval - exact_eval)) / np.max(np.abs(exact_eval)) + diff = xp.max(xp.abs(all_eval - exact_eval)) / xp.max(xp.abs(exact_eval)) print(f"{h1 = }, {diff = }") @@ -621,14 +608,14 @@ def test_evaluation_SPH_h_convergence_1d(boxes_per_dim, bc_x, eval_pts, tesselat err_vec += [diff] if tesselation: - fit = np.polyfit(np.log(h_vec[1:5]), np.log(err_vec[1:5]), 1) + fit = xp.polyfit(xp.log(h_vec[1:5]), xp.log(err_vec[1:5]), 1) else: - fit = np.polyfit(np.log(h_vec[:-2]), np.log(err_vec[:-2]), 1) + fit = xp.polyfit(xp.log(h_vec[:-2]), xp.log(err_vec[:-2]), 1) if show_plot and rank == 0: plt.figure(figsize=(12, 8)) plt.loglog(h_vec, err_vec, label="Convergence") - plt.loglog(h_vec, np.exp(fit[1]) * np.array(h_vec) ** (fit[0]), "--", label=f"fit with slope {fit[0]}") + plt.loglog(h_vec, xp.exp(fit[1]) * xp.array(h_vec) ** (fit[0]), "--", label=f"fit with slope {fit[0]}") plt.legend() plt.show() # plt.savefig("Convergence_SPH") @@ -637,7 +624,7 @@ def test_evaluation_SPH_h_convergence_1d(boxes_per_dim, bc_x, eval_pts, tesselat print(f"\n{bc_x = }, {eval_pts = }, {tesselation = }, {fit[0] = }") if not tesselation: - assert np.abs(fit[0] + 0.5) < 0.1 # Monte Carlo rate + assert xp.abs(fit[0] + 0.5) < 0.1 # Monte Carlo rate @pytest.mark.parametrize("boxes_per_dim", [(12, 1, 1)]) @@ -659,55 +646,54 @@ def test_evaluation_mc_Np_and_h_convergence_1d(boxes_per_dim, bc_x, eval_pts, te domain = domain_class(**dom_params) if tesselation: - loading = "tesselation" - loading_params = {"n_quad": 1} - # ppbs = [5000, 10000, 15000, 20000, 25000] ppbs = [4, 8, 16, 32, 64] Nps = [None] * len(ppbs) - else: - loading = "pseudo_random" - loading_params = {"seed": 1607} Nps = [(2**k) * 10**3 for k in range(-2, 9)] ppbs = [None] * len(Nps) - cst_vel = {"density_profile": "constant", "n": 1.5} - bckgr_params = {"ConstantVelocity": cst_vel, "pforms": ["vol", None]} + # background + background = ConstantVelocity(n=1.5, density_profile="constant") + background.domain = domain # perturbation - mode_params = {"given_in_basis": "0", "ls": [1], "amps": [-1e-0]} if bc_x in ("periodic", "fixed"): - fun_exact = lambda e1, e2, e3: 1.5 - np.sin(2 * np.pi * e1) - modes = {"ModesSin": mode_params} + fun_exact = lambda e1, e2, e3: 1.5 - xp.sin(2 * xp.pi * e1) + pert = {"n": perturbations.ModesSin(ls=(1,), amps=(-1e-0,))} elif bc_x == "mirror": - fun_exact = lambda e1, e2, e3: 1.5 - np.cos(2 * np.pi * e1) - modes = {"ModesCos": mode_params} - pert_params = {"n": modes} + fun_exact = lambda e1, e2, e3: 1.5 - xp.cos(2 * xp.pi * e1) + pert = {"n": perturbations.ModesCos(ls=(1,), amps=(-1e-0,))} # exact solution - eta1 = np.linspace(0, 1.0, eval_pts) - eta2 = np.array([0.0]) - eta3 = np.array([0.0]) - ee1, ee2, ee3 = np.meshgrid(eta1, eta2, eta3, indexing="ij") + eta1 = xp.linspace(0, 1.0, eval_pts) + eta2 = xp.array([0.0]) + eta3 = xp.array([0.0]) + ee1, ee2, ee3 = xp.meshgrid(eta1, eta2, eta3, indexing="ij") exact_eval = fun_exact(ee1, ee2, ee3) + # boundary conditions + boundary_params = BoundaryParameters(bc_sph=(bc_x, "periodic", "periodic")) + h_arr = [((2**k) * 10**-3 * 0.25) for k in range(2, 12)] err_vec = [] for h in h_arr: err_vec += [[]] for Np, ppb in zip(Nps, ppbs): + if tesselation: + loading_params = LoadingParameters(ppb=ppb, loading="tesselation") + else: + loading_params = LoadingParameters(Np=Np, seed=1607) + particles = ParticlesSPH( comm_world=comm, - Np=Np, - ppb=ppb, + loading_params=loading_params, + boundary_params=boundary_params, boxes_per_dim=boxes_per_dim, - bc_sph=[bc_x, "periodic", "periodic"], bufsize=1.0, - loading=loading, - loading_params=loading_params, domain=domain, - bckgr_params=bckgr_params, - pert_params=pert_params, + background=background, + perturbations=pert, + n_as_volume_form=True, verbose=False, ) @@ -724,11 +710,11 @@ def test_evaluation_mc_Np_and_h_convergence_1d(boxes_per_dim, bc_x, eval_pts, te if comm is None: all_eval = test_eval else: - all_eval = np.zeros_like(test_eval) + all_eval = xp.zeros_like(test_eval) comm.Allreduce(test_eval, all_eval, op=MPI.SUM) # error in max-norm - diff = np.max(np.abs(all_eval - exact_eval)) / np.max(np.abs(exact_eval)) + diff = xp.max(xp.abs(all_eval - exact_eval)) / xp.max(xp.abs(exact_eval)) err_vec[-1] += [diff] if rank == 0: @@ -740,29 +726,29 @@ def test_evaluation_mc_Np_and_h_convergence_1d(boxes_per_dim, bc_x, eval_pts, te # plt.title(f"{h = }, {Np = }") # # plt.savefig(f"fun_h{h}_N{Np}_ppb{ppb}.png") - err_vec = np.array(err_vec) - err_min = np.min(err_vec) + err_vec = xp.array(err_vec) + err_min = xp.min(err_vec) if show_plot and rank == 0: if tesselation: - h_mesh, n_mesh = np.meshgrid(np.log10(h_arr), np.log10(ppbs), indexing="ij") + h_mesh, n_mesh = xp.meshgrid(xp.log10(h_arr), xp.log10(ppbs), indexing="ij") if not tesselation: - h_mesh, n_mesh = np.meshgrid(np.log10(h_arr), np.log10(Nps), indexing="ij") + h_mesh, n_mesh = xp.meshgrid(xp.log10(h_arr), xp.log10(Nps), indexing="ij") plt.figure(figsize=(6, 6)) - plt.pcolor(h_mesh, n_mesh, np.log10(err_vec), shading="auto") + plt.pcolor(h_mesh, n_mesh, xp.log10(err_vec), shading="auto") plt.title("Error") plt.colorbar(label="log10(error)") plt.xlabel("log10(h)") plt.ylabel("log10(particles)") - min_indices = np.argmin(err_vec, axis=0) + min_indices = xp.argmin(err_vec, axis=0) min_h_values = [] for mi in min_indices: - min_h_values += [np.log10(h_arr[mi])] + min_h_values += [xp.log10(h_arr[mi])] if tesselation: - log_particles = np.log10(ppbs) + log_particles = xp.log10(ppbs) else: - log_particles = np.log10(Nps) + log_particles = xp.log10(Nps) plt.plot(min_h_values, log_particles, "r-", label="Min error h for each Np", linewidth=2) plt.legend() # plt.savefig("SPH_conv_in_h_and_N.png") @@ -774,7 +760,7 @@ def test_evaluation_mc_Np_and_h_convergence_1d(boxes_per_dim, bc_x, eval_pts, te if tesselation: if bc_x == "periodic": - assert np.min(err_vec) < 7.7e-5 + assert xp.min(err_vec) < 7.7e-5 elif bc_x == "fixed": assert err_min < 7.7e-5 else: @@ -808,65 +794,63 @@ def test_evaluation_SPH_Np_convergence_2d(boxes_per_dim, bc_x, bc_y, tesselation domain = domain_class(**dom_params) if tesselation: - loading = "tesselation" - loading_params = {"n_quad": 1} ppbs = [4, 8, 16, 32, 64, 200] Nps = [None] * len(ppbs) else: - loading = "pseudo_random" - loading_params = {"seed": 1607} Nps = [(2**k) * 10**3 for k in range(-2, 9)] ppbs = [None] * len(Nps) - cst_vel = {"density_profile": "constant", "n": 1.5} - bckgr_params = {"ConstantVelocity": cst_vel, "pforms": ["vol", None]} + # background + background = ConstantVelocity(n=1.5, density_profile="constant") + background.domain = domain # perturbation - mode_params = {"given_in_basis": "0", "ls": [1], "ms": [1], "amps": [-1e-0]} - if bc_x in ("periodic", "fixed"): if bc_y in ("periodic", "fixed"): - fun_exact = lambda x, y, z: 1.5 - np.sin(2 * np.pi / Lx * x) * np.sin(2 * np.pi / Ly * y) - modes = {"ModesSinSin": mode_params} + fun_exact = lambda x, y, z: 1.5 - xp.sin(2 * xp.pi / Lx * x) * xp.sin(2 * xp.pi / Ly * y) + pert = {"n": perturbations.ModesSinSin(ls=(1,), ms=(1,), amps=(-1e-0,))} elif bc_y == "mirror": - fun_exact = lambda x, y, z: 1.5 - np.sin(2 * np.pi / Lx * x) * np.cos(2 * np.pi / Ly * y) - modes = {"ModesSinCos": mode_params} + fun_exact = lambda x, y, z: 1.5 - xp.sin(2 * xp.pi / Lx * x) * xp.cos(2 * xp.pi / Ly * y) + pert = {"n": perturbations.ModesSinCos(ls=(1,), ms=(1,), amps=(-1e-0,))} elif bc_x == "mirror": if bc_y in ("periodic", "fixed"): - fun_exact = lambda x, y, z: 1.5 - np.cos(2 * np.pi / Lx * x) * np.sin(2 * np.pi / Ly * y) - modes = {"ModesCosSin": mode_params} + fun_exact = lambda x, y, z: 1.5 - xp.cos(2 * xp.pi / Lx * x) * xp.sin(2 * xp.pi / Ly * y) + pert = {"n": perturbations.ModesCosSin(ls=(1,), ms=(1,), amps=(-1e-0,))} elif bc_y == "mirror": - fun_exact = lambda x, y, z: 1.5 - np.cos(2 * np.pi / Lx * x) * np.cos(2 * np.pi / Ly * y) - modes = {"ModesCosCos": mode_params} - - pert_params = {"n": modes} + fun_exact = lambda x, y, z: 1.5 - xp.cos(2 * xp.pi / Lx * x) * xp.cos(2 * xp.pi / Ly * y) + pert = {"n": perturbations.ModesCosCos(ls=(1,), ms=(1,), amps=(-1e-0,))} # exact solution - eta1 = np.linspace(0, 1.0, 41) - eta2 = np.linspace(0, 1.0, 86) - eta3 = np.array([0.0]) - ee1, ee2, ee3 = np.meshgrid(eta1, eta2, eta3, indexing="ij") + eta1 = xp.linspace(0, 1.0, 41) + eta2 = xp.linspace(0, 1.0, 86) + eta3 = xp.array([0.0]) + ee1, ee2, ee3 = xp.meshgrid(eta1, eta2, eta3, indexing="ij") x, y, z = domain(eta1, eta2, eta3) exact_eval = fun_exact(x, y, z) + # boundary conditions + boundary_params = BoundaryParameters(bc_sph=(bc_x, bc_y, "periodic")) + err_vec = [] for Np, ppb in zip(Nps, ppbs): + if tesselation: + loading_params = LoadingParameters(ppb=ppb, loading="tesselation") + else: + loading_params = LoadingParameters(Np=Np, seed=1607) + particles = ParticlesSPH( comm_world=comm, - Np=Np, - ppb=ppb, + loading_params=loading_params, + boundary_params=boundary_params, boxes_per_dim=boxes_per_dim, - bc_sph=[bc_x, bc_y, "periodic"], bufsize=1.0, box_bufsize=4.0, - loading=loading, - loading_params=loading_params, domain=domain, - bckgr_params=bckgr_params, - pert_params=pert_params, + background=background, + perturbations=pert, + n_as_volume_form=True, verbose=False, - mpi_dims_mask=[True, False, False], ) if rank == 0: print(f"{particles.domain_array}") @@ -884,16 +868,11 @@ def test_evaluation_SPH_Np_convergence_2d(boxes_per_dim, bc_x, bc_y, tesselation if comm is None: all_eval = test_eval else: - all_eval = np.zeros_like(test_eval) + all_eval = xp.zeros_like(test_eval) comm.Allreduce(test_eval, all_eval, op=MPI.SUM) - # if rank == 0: - # print(f"{all_eval.squeeze().shape}") - # print(f"{all_eval.squeeze()[0]}") - # print(f"{all_eval.squeeze().T[0]}") - # error in max-norm - diff = np.max(np.abs(all_eval - exact_eval)) / np.max(np.abs(exact_eval)) + diff = xp.max(xp.abs(all_eval - exact_eval)) / xp.max(xp.abs(exact_eval)) err_vec += [diff] if tesselation: @@ -911,16 +890,16 @@ def test_evaluation_SPH_Np_convergence_2d(boxes_per_dim, bc_x, bc_y, tesselation # fig.savefig(f"2d_sph_{Np}_{ppb}.png") if tesselation: - fit = np.polyfit(np.log(ppbs), np.log(err_vec), 1) + fit = xp.polyfit(xp.log(ppbs), xp.log(err_vec), 1) xvec = ppbs else: - fit = np.polyfit(np.log(Nps), np.log(err_vec), 1) + fit = xp.polyfit(xp.log(Nps), xp.log(err_vec), 1) xvec = Nps if show_plot and rank == 0: plt.figure(figsize=(12, 8)) plt.loglog(xvec, err_vec, label="Convergence") - plt.loglog(xvec, np.exp(fit[1]) * np.array(xvec) ** (fit[0]), "--", label=f"fit with slope {fit[0]}") + plt.loglog(xvec, xp.exp(fit[1]) * xp.array(xvec) ** (fit[0]), "--", label=f"fit with slope {fit[0]}") plt.legend() plt.show() # plt.savefig(f"Convergence_SPH_{tesselation=}") @@ -929,21 +908,21 @@ def test_evaluation_SPH_Np_convergence_2d(boxes_per_dim, bc_x, bc_y, tesselation print(f"\n{bc_x = }, {tesselation = }, {fit[0] = }") if not tesselation: - assert np.abs(fit[0] + 0.5) < 0.1 # Monte Carlo rate + assert xp.abs(fit[0] + 0.5) < 0.1 # Monte Carlo rate if __name__ == "__main__": - # test_sph_evaluation_1d( - # (24, 1, 1), - # "trigonometric_1d", - # # "gaussian_1d", - # 1, - # "periodic", - # # "mirror", - # 10, - # tesselation=True, - # show_plot=True - # ) + test_sph_evaluation_1d( + (24, 1, 1), + "trigonometric_1d", + # "gaussian_1d", + 1, + # "periodic", + "mirror", + 16, + tesselation=False, + show_plot=True, + ) # test_sph_evaluation_2d( # (12, 12, 1), @@ -974,7 +953,7 @@ def test_evaluation_SPH_Np_convergence_2d(boxes_per_dim, bc_x, bc_y, tesselation # test_evaluation_SPH_h_convergence_1d((12,1,1), "periodic", eval_pts=16, tesselation=True, show_plot=True) # test_evaluation_mc_Np_and_h_convergence_1d((12,1,1),"mirror", eval_pts=16, tesselation = False, show_plot=True) # test_evaluation_SPH_Np_convergence_2d((24, 24, 1), "periodic", "periodic", tesselation=True, show_plot=True) - test_evaluation_SPH_Np_convergence_2d((24, 24, 1), "periodic", "fixed", tesselation=True, show_plot=True) + # test_evaluation_SPH_Np_convergence_2d((24, 24, 1), "periodic", "fixed", tesselation=True, show_plot=True) # test_evaluation_SPH_Np_convergence_2d((32, 32, 1), "fixed", "periodic", tesselation=True, show_plot=True) # test_evaluation_SPH_Np_convergence_2d((32, 32, 1), "fixed", "fixed", tesselation=True, show_plot=True) # test_evaluation_SPH_Np_convergence_2d((32, 32, 1), "mirror", "mirror", tesselation=True, show_plot=True) diff --git a/src/struphy/pic/tests/test_tesselation.py b/src/struphy/pic/tests/test_tesselation.py index 7215bf76b..cf6ed922e 100644 --- a/src/struphy/pic/tests/test_tesselation.py +++ b/src/struphy/pic/tests/test_tesselation.py @@ -1,13 +1,16 @@ from time import time +import cunumpy as xp import pytest from matplotlib import pyplot as plt from psydac.ddm.mpi import mpi as MPI from struphy.feec.psydac_derham import Derham +from struphy.fields_background.equils import ConstantVelocity from struphy.geometry import domains +from struphy.initial import perturbations from struphy.pic.particles import ParticlesSPH -from struphy.utils.arrays import xp as np +from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters @pytest.mark.parametrize("ppb", [8, 12]) @@ -24,17 +27,14 @@ def test_draw(ppb, nx, ny, nz): domain = domain_class(**dom_params) boxes_per_dim = (nx, ny, nz) - bc = ["periodic"] * 3 - loading = "tesselation" bufsize = 0.5 + loading_params = LoadingParameters(ppb=ppb, loading="tesselation") # instantiate Particle object particles = ParticlesSPH( comm_world=comm, - ppb=ppb, + loading_params=loading_params, boxes_per_dim=boxes_per_dim, - bc=bc, - loading=loading, domain=domain, verbose=False, bufsize=bufsize, @@ -56,20 +56,20 @@ def test_draw(ppb, nx, ny, nz): zl = particles.domain_array[rank, 6] zr = particles.domain_array[rank, 7] - eta1 = np.linspace(xl, xr, tiles_x + 1)[:-1] + (xr - xl) / (2 * tiles_x) - eta2 = np.linspace(yl, yr, tiles_y + 1)[:-1] + (yr - yl) / (2 * tiles_y) - eta3 = np.linspace(zl, zr, tiles_z + 1)[:-1] + (zr - zl) / (2 * tiles_z) + eta1 = xp.linspace(xl, xr, tiles_x + 1)[:-1] + (xr - xl) / (2 * tiles_x) + eta2 = xp.linspace(yl, yr, tiles_y + 1)[:-1] + (yr - yl) / (2 * tiles_y) + eta3 = xp.linspace(zl, zr, tiles_z + 1)[:-1] + (zr - zl) / (2 * tiles_z) - ee1, ee2, ee3 = np.meshgrid(eta1, eta2, eta3, indexing="ij") + ee1, ee2, ee3 = xp.meshgrid(eta1, eta2, eta3, indexing="ij") e1 = ee1.flatten() e2 = ee2.flatten() e3 = ee3.flatten() # print(f'\n{rank = }, {e1 = }') - assert np.allclose(particles.positions[:, 0], e1) - assert np.allclose(particles.positions[:, 1], e2) - assert np.allclose(particles.positions[:, 2], e3) + assert xp.allclose(particles.positions[:, 0], e1) + assert xp.allclose(particles.positions[:, 1], e2) + assert xp.allclose(particles.positions[:, 2], e3) @pytest.mark.parametrize("ppb", [8, 12]) @@ -87,31 +87,24 @@ def test_cell_average(ppb, nx, ny, nz, n_quad, show_plot=False): domain = domain_class(**dom_params) boxes_per_dim = (nx, ny, nz) - bc = ["periodic"] * 3 - loading = "tesselation" - loading_params = {"n_quad": n_quad} + loading_params = LoadingParameters(ppb=ppb, loading="tesselation", n_quad=n_quad) bufsize = 0.5 - cst_vel = {"ux": 0.0, "uy": 0.0, "uz": 0.0, "density_profile": "constant"} - bckgr_params = {"ConstantVelocity": cst_vel} + background = ConstantVelocity(n=1.0, ux=0.0, uy=0.0, uz=0.0, density_profile="constant") + background.domain = domain - mode_params = {"given_in_basis": "0", "ls": [1], "amps": [1e-0]} - modes = {"ModesSin": mode_params} - pert_params = {"n": modes} + pert = {"n": perturbations.ModesSin(ls=(1,), amps=(1e-0,))} # instantiate Particle object particles = ParticlesSPH( comm_world=comm, - ppb=ppb, boxes_per_dim=boxes_per_dim, - bc=bc, - loading=loading, loading_params=loading_params, domain=domain, verbose=False, bufsize=bufsize, - bckgr_params=bckgr_params, - pert_params=pert_params, + background=background, + perturbations=pert, ) particles.draw_markers(sort=False) @@ -126,20 +119,20 @@ def test_cell_average(ppb, nx, ny, nz, n_quad, show_plot=False): yl = particles.domain_array[rank, 3] yr = particles.domain_array[rank, 4] - eta1 = np.linspace(xl, xr, tiles_x + 1) - eta2 = np.linspace(yl, yr, tiles_y + 1) + eta1 = xp.linspace(xl, xr, tiles_x + 1) + eta2 = xp.linspace(yl, yr, tiles_y + 1) if ny == nz == 1: plt.figure(figsize=(15, 10)) - plt.plot(particles.positions[:, 0], np.zeros_like(particles.weights), "o", label="markers") + plt.plot(particles.positions[:, 0], xp.zeros_like(particles.weights), "o", label="markers") plt.plot(particles.positions[:, 0], particles.weights, "-o", label="weights") plt.plot( - np.linspace(xl, xr, 100), - particles.f_init(np.linspace(xl, xr, 100), 0.5, 0.5).squeeze(), + xp.linspace(xl, xr, 100), + particles.f_init(xp.linspace(xl, xr, 100), 0.5, 0.5).squeeze(), "--", label="f_init", ) - plt.vlines(np.linspace(xl, xr, nx + 1), 0, 2, label="sorting boxes", color="k") + plt.vlines(xp.linspace(xl, xr, nx + 1), 0, 2, label="sorting boxes", color="k") ax = plt.gca() ax.set_xticks(eta1) ax.set_yticks(eta2) @@ -153,8 +146,8 @@ def test_cell_average(ppb, nx, ny, nz, n_quad, show_plot=False): plt.subplot(1, 2, 1) ax = plt.gca() - ax.set_xticks(np.linspace(0, 1, nx + 1)) - ax.set_yticks(np.linspace(0, 1, ny + 1)) + ax.set_xticks(xp.linspace(0, 1, nx + 1)) + ax.set_yticks(xp.linspace(0, 1, ny + 1)) coloring = particles.weights plt.scatter(particles.positions[:, 0], particles.positions[:, 1], c=coloring, s=40) plt.grid(c="k") @@ -166,12 +159,12 @@ def test_cell_average(ppb, nx, ny, nz, n_quad, show_plot=False): plt.subplot(1, 2, 2) ax = plt.gca() - ax.set_xticks(np.linspace(0, 1, nx + 1)) - ax.set_yticks(np.linspace(0, 1, ny + 1)) + ax.set_xticks(xp.linspace(0, 1, nx + 1)) + ax.set_yticks(xp.linspace(0, 1, ny + 1)) coloring = particles.weights - pos1 = np.linspace(xl, xr, 100) - pos2 = np.linspace(yl, yr, 100) - pp1, pp2 = np.meshgrid(pos1, pos2, indexing="ij") + pos1 = xp.linspace(xl, xr, 100) + pos2 = xp.linspace(yl, yr, 100) + pp1, pp2 = xp.meshgrid(pos1, pos2, indexing="ij") plt.pcolor(pp1, pp2, particles.f_init(pp1, pp2, 0.5).squeeze()) plt.grid(c="k") plt.axis("square") @@ -183,10 +176,10 @@ def test_cell_average(ppb, nx, ny, nz, n_quad, show_plot=False): plt.show() # test - print(f"\n{rank = }, {np.max(np.abs(particles.weights - particles.f_init(particles.positions))) = }") - assert np.max(np.abs(particles.weights - particles.f_init(particles.positions))) < 0.012 + print(f"\n{rank = }, {xp.max(xp.abs(particles.weights - particles.f_init(particles.positions))) = }") + assert xp.max(xp.abs(particles.weights - particles.f_init(particles.positions))) < 0.012 if __name__ == "__main__": - # test_draw(8, 16, 1, 1) + test_draw(8, 16, 1, 1) test_cell_average(8, 6, 16, 14, n_quad=2, show_plot=True) diff --git a/src/struphy/pic/utilities.py b/src/struphy/pic/utilities.py index 5507526a5..3ae645557 100644 --- a/src/struphy/pic/utilities.py +++ b/src/struphy/pic/utilities.py @@ -1,5 +1,239 @@ +import cunumpy as xp + import struphy.pic.utilities_kernels as utils -from struphy.utils.arrays import xp as np +from struphy.io.options import ( + OptsLoading, + OptsMarkerBC, + OptsRecontructBC, + OptsSpatialLoading, +) + + +class LoadingParameters: + """Parameters for particle loading. + + Parameters + ---------- + Np : int + Total number of particles to load. + + ppc : int + Particles to load per cell if a grid is defined. Cells are defined from ``domain_array``. + + ppb : int + Particles to load per sorting box. Sorting boxes are defined from ``boxes_per_dim``. + + loading : OptsLoading + How to load markers: multiple options for Monte-Carlo, or "tesselation" for positioning them on a regular grid. + + seed : int + Seed for random generator. If None, no seed is taken. + + moments : tuple + Mean velocities and temperatures for the Gaussian sampling distribution. + If None, these are auto-calculated form the given background. + + spatial : OptsSpatialLoading + Draw uniformly in eta, or draw uniformly on the "disc" image of (eta1, eta2). + + specific_markers : tuple[tuple] + Each entry is a tuple of phase space coordinates (floats) of a specific marker to be initialized. + + n_quad : int + Number of quadrature points for tesselation. + + dir_external : str + Load markers from external .hdf5 file (absolute path). + + dir_particles_abs : str + Load markers from restart .hdf5 file (absolute path). + + dir_particles : str + Load markers from restart .hdf5 file (relative path to output folder). + + restart_key : str + Key in .hdf5 file's restart/ folder where marker array is stored. + """ + + def __init__( + self, + Np: int = None, + ppc: int = None, + ppb: int = 10, + loading: OptsLoading = "pseudo_random", + seed: int = None, + moments: tuple = None, + spatial: OptsSpatialLoading = "uniform", + specific_markers: tuple[tuple] = None, + n_quad: int = 1, + dir_exrernal: str = None, + dir_particles: str = None, + dir_particles_abs: str = None, + restart_key: str = None, + ): + self.Np = Np + self.ppc = ppc + self.ppb = ppb + self.loading = loading + self.seed = seed + self.moments = moments + self.spatial = spatial + self.specific_markers = specific_markers + self.n_quad = n_quad + self.dir_external = dir_exrernal + self.dir_particles = dir_particles + self.dir_particles_abs = dir_particles_abs + self.restart_key = restart_key + + +class WeightsParameters: + """Paramters for particle weights. + + Parameters + ---------- + control_variate : bool + Whether to use a control variate for noise reduction. + + reject_weights : bool + Whether to reject weights below threshold. + + threshold : float + Threshold for rejecting weights. + """ + + def __init__( + self, + control_variate: bool = False, + reject_weights: bool = False, + threshold: float = 0.0, + ): + self.control_variate = control_variate + self.reject_weights = reject_weights + self.threshold = threshold + + +class BoundaryParameters: + """Parameters for particle boundary and sph reconstruction boundary conditions. + + Parameters + ---------- + bc : tuple[OptsMarkerBC] + Boundary conditions for particle movement. + Either 'remove', 'reflect', 'periodic' or 'refill' in each direction. + + bc_refill : list + Either 'inner' or 'outer'. + + bc_sph : tuple[OptsRecontructBC] + Boundary conditions for sph kernel reconstruction. + """ + + def __init__( + self, + bc: tuple[OptsMarkerBC] = ("periodic", "periodic", "periodic"), + bc_refill=None, + bc_sph: tuple[OptsRecontructBC] = ("periodic", "periodic", "periodic"), + ): + self.bc = bc + self.bc_refill = bc_refill + self.bc_sph = bc_sph + + +class BinningPlot: + """Binning plot of marker distribution in phase space. + + Parameters + ---------- + slice : str + Coordinate-slice in phase space to bin. A combination of "e1", "e2", "e3", "v1", etc., separated by an underscore "_". + For example, "e1" showas a 1D binning plot over eta1, whereas "e1_v1" shows a 2D binning plot over eta1 and v1. + + n_bins : int | tuple[int] + Number of bins for each coordinate. + + ranges : tuple[int] | tuple[tuple[int]] = (0.0, 1.0) + Binning range (as an interval in R) for each coordinate. + + divide_by_jac : bool + Whether to divide by the Jacobian determinant (volume-to-0-form). + """ + + def __init__( + self, + slice: str = "e1", + n_bins: int | tuple[int] = 128, + ranges: tuple[float] | tuple[tuple[float]] = (0.0, 1.0), + divide_by_jac: bool = True, + ): + if isinstance(n_bins, int): + n_bins = (n_bins,) + + if not isinstance(ranges[0], tuple): + ranges = (ranges,) + + assert ((len(slice) - 2) / 3).is_integer(), f"Binning coordinates must be separated by '_', but reads {slice}." + assert len(slice.split("_")) == len(ranges) == len(n_bins), ( + f"Number of slices names ({len(slice.split('_'))}), number of bins ({len(n_bins)}), and number of ranges ({len(ranges)}) are inconsistent with each other!\n\n" + ) + self.slice = slice + self.n_bins = n_bins + self.ranges = ranges + self.divide_by_jac = divide_by_jac + + # computations and allocations + self._bin_edges = [] + for nb, rng in zip(n_bins, ranges): + self._bin_edges += [xp.linspace(rng[0], rng[1], nb + 1)] + self._bin_edges = tuple(self.bin_edges) + + self._f = xp.zeros(n_bins, dtype=float) + self._df = xp.zeros(n_bins, dtype=float) + + @property + def bin_edges(self) -> tuple: + return self._bin_edges + + @property + def f(self) -> xp.ndarray: + """The binned distribution function (full-f).""" + return self._f + + @property + def df(self) -> xp.ndarray: + """The binned distribution function minus the background (delta-f).""" + return self._df + + +class KernelDensityPlot: + """SPH density plot in configuration space. + + Parameters + ---------- + pts_e1, pts_e2, pts_e3 : int + Number of evaluation points in each direction. + """ + + def __init__( + self, + pts_e1: int = 16, + pts_e2: int = 16, + pts_e3: int = 1, + ): + e1 = xp.linspace(0.0, 1.0, pts_e1) + e2 = xp.linspace(0.0, 1.0, pts_e2) + e3 = xp.linspace(0.0, 1.0, pts_e3) + ee1, ee2, ee3 = xp.meshgrid(e1, e2, e3, indexing="ij") + self._plot_pts = (ee1, ee2, ee3) + self._n_sph = xp.zeros(ee1.shape, dtype=float) + + @property + def plot_pts(self) -> tuple: + return self._plot_pts + + @property + def n_sph(self) -> xp.ndarray: + """The evaluated density.""" + return self._n_sph def get_kinetic_energy_particles(fe_coeffs, derham, domain, particles): @@ -18,15 +252,15 @@ def get_kinetic_energy_particles(fe_coeffs, derham, domain, particles): Particles object. """ - res = np.empty(1, dtype=float) + res = xp.empty(1, dtype=float) utils.canonical_kinetic_particles( res, particles.markers, - np.array(derham.p), + xp.array(derham.p), derham.Vh_fem["0"].knots[0], derham.Vh_fem["0"].knots[1], derham.Vh_fem["0"].knots[2], - np.array( + xp.array( derham.V0.coeff_space.starts, ), *domain.args_map, @@ -51,7 +285,7 @@ def get_electron_thermal_energy(density_0_form, derham, domain, nel1, nel2, nel3 Discrete Derham complex. """ - res = np.empty(1, dtype=float) + res = xp.empty(1, dtype=float) utils.thermal_energy( res, density_0_form._operators[0].matrix._data, diff --git a/src/struphy/pic/utilities_kernels.py b/src/struphy/pic/utilities_kernels.py index d0f3c4e92..cb25cc05f 100644 --- a/src/struphy/pic/utilities_kernels.py +++ b/src/struphy/pic/utilities_kernels.py @@ -1,4 +1,4 @@ -from numpy import abs, empty, log, pi, shape, sign, sqrt, zeros +from numpy import abs, empty, log, mod, pi, shape, sign, sqrt, zeros from pyccel.decorators import stack_array import struphy.bsplines.bsplines_kernels as bsplines_kernels @@ -14,7 +14,7 @@ eval_vectorfield_spline_mpi, get_spans, ) -from struphy.kernel_arguments.pusher_args_kernels import DerhamArguments, DomainArguments +from struphy.kernel_arguments.pusher_args_kernels import DerhamArguments, DomainArguments, MarkerArguments def eval_magnetic_moment_5d( @@ -331,6 +331,71 @@ def eval_magnetic_energy( # -- removed omp: #$ omp end parallel +@stack_array("dfm", "eta") +def eval_magnetic_energy_PBb( + markers: "float[:,:]", + args_derham: "DerhamArguments", + args_domain: "DomainArguments", + first_diagnostics_idx: int, + abs_B0: "float[:,:,:]", + PBb: "float[:,:,:]", +): + r""" + Evaluate :math:`mu_p |B(\boldsymbol \eta_p)_\parallel|` for each marker. + The result is stored at markers[:, first_diagnostics_idx]. + """ + eta = empty(3, dtype=float) + + dfm = empty((3, 3), dtype=float) + + # get number of markers + n_markers = shape(markers)[0] + + for ip in range(n_markers): + # only do something if particle is a "true" particle (i.e. not a hole) + if markers[ip, 0] == -1.0: + continue + + eta[:] = mod(markers[ip, 0:3], 1.0) + + weight = markers[ip, 7] + dweight = markers[ip, 5] + + mu = markers[ip, first_diagnostics_idx + 1] + + # spline evaluation + span1, span2, span3 = get_spans(eta[0], eta[1], eta[2], args_derham) + + # evaluate Jacobian, result in dfm + evaluation_kernels.df( + eta[0], + eta[1], + eta[2], + args_domain, + dfm, + ) + + # abs_B0; 0form + abs_B = eval_0form_spline_mpi( + span1, + span2, + span3, + args_derham, + abs_B0, + ) + + # PBb; 0form + PB_b = eval_0form_spline_mpi( + span1, + span2, + span3, + args_derham, + PBb, + ) + + markers[ip, first_diagnostics_idx] = mu * (abs_B + PB_b) + + @stack_array("v", "dfm", "b2", "norm_b_cart", "temp", "v_perp", "Larmor_r") def eval_guiding_center_from_6d( markers: "float[:,:]", @@ -441,189 +506,101 @@ def eval_guiding_center_from_6d( markers[ip, first_diagnostics_idx + 2] = z - Larmor_r[2] -@stack_array("grad_PB", "tmp") -def accum_gradI_const( - markers: "float[:,:]", - Np: "int", +@stack_array("dfm", "df_t", "g", "g_inv", "gradB, grad_PB_b", "tmp", "eta_mid", "eta_diff") +def eval_gradB_ediff( + args_markers: "MarkerArguments", + args_domain: "DomainArguments", args_derham: "DerhamArguments", - grad_PB1: "float[:,:,:]", - grad_PB2: "float[:,:,:]", - grad_PB3: "float[:,:,:]", - scale: "float", + gradB1: "float[:,:,:]", + gradB2: "float[:,:,:]", + gradB3: "float[:,:,:]", + grad_PB_b1: "float[:,:,:]", + grad_PB_b2: "float[:,:,:]", + grad_PB_b3: "float[:,:,:]", + idx: int, ): r"""TODO""" + + # allocate metric coeffs + dfm = empty((3, 3), dtype=float) + df_t = empty((3, 3), dtype=float) + g = empty((3, 3), dtype=float) + g_inv = empty((3, 3), dtype=float) + # allocate for magnetic field evaluation - grad_PB = empty(3, dtype=float) + gradB = empty(3, dtype=float) + grad_PB_b = empty(3, dtype=float) tmp = empty(3, dtype=float) + eta_mid = empty(3, dtype=float) + eta_diff = empty(3, dtype=float) - # allocate for filling - res = zeros(1, dtype=float) - - # get number of markers - n_markers_loc = shape(markers)[0] + # get marker arguments + markers = args_markers.markers + n_markers = args_markers.n_markers + mu_idx = args_markers.mu_idx + first_init_idx = args_markers.first_init_idx + first_free_idx = args_markers.first_free_idx - for ip in range(n_markers_loc): + for ip in range(n_markers): # only do something if particle is a "true" particle (i.e. not a hole) if markers[ip, 0] == -1.0: continue - # marker positions - eta1 = markers[ip, 0] # mid - eta2 = markers[ip, 1] # mid - eta3 = markers[ip, 2] # mid + # marker positions, mid point + eta_mid[:] = (markers[ip, 0:3] + markers[ip, first_init_idx : first_init_idx + 3]) / 2.0 + eta_mid[:] = mod(eta_mid[:], 1.0) + + eta_diff = markers[ip, 0:3] - markers[ip, first_init_idx : first_init_idx + 3] # marker weight and velocity weight = markers[ip, 5] - mu = markers[ip, 9] + mu = markers[ip, mu_idx] # b-field evaluation - span1, span2, span3 = get_spans(eta1, eta2, eta3, args_derham) + span1, span2, span3 = get_spans(eta_mid[0], eta_mid[1], eta_mid[2], args_derham) + # print(span1, span2, span3) - # grad_PB; 1form + # evaluate Jacobian, result in dfm + evaluation_kernels.df( + eta_mid[0], + eta_mid[1], + eta_mid[2], + args_domain, + dfm, + ) + + linalg_kernels.transpose(dfm, df_t) + linalg_kernels.matrix_matrix(df_t, dfm, g) + linalg_kernels.matrix_inv(g, g_inv) + + # gradB; 1form eval_1form_spline_mpi( span1, span2, span3, args_derham, - grad_PB1, - grad_PB2, - grad_PB3, - grad_PB, + gradB1, + gradB2, + gradB3, + gradB, ) - tmp[:] = markers[ip, 15:18] - res += linalg_kernels.scalar_dot(tmp, grad_PB) * weight * mu * scale - - return res / Np - - -def accum_en_fB( - markers: "float[:,:]", - Np: "int", - args_derham: "DerhamArguments", - PB: "float[:,:,:]", -): - r"""TODO""" - - # allocate for filling - res = zeros(1, dtype=float) - - # get number of markers - n_markers_loc = shape(markers)[0] - - for ip in range(n_markers_loc): - # only do something if particle is a "true" particle (i.e. not a hole) - if markers[ip, 0] == -1.0: - continue - - # marker positions - eta1 = markers[ip, 0] - eta2 = markers[ip, 1] - eta3 = markers[ip, 2] - - # marker weight and velocity - mu = markers[ip, 9] - weight = markers[ip, 5] - - # b-field evaluation - span1, span2, span3 = get_spans(eta1, eta2, eta3, args_derham) - - B0 = eval_0form_spline_mpi( + # grad_PB_b; 1form + eval_1form_spline_mpi( span1, span2, span3, args_derham, - PB, + grad_PB_b1, + grad_PB_b2, + grad_PB_b3, + grad_PB_b, ) - res += abs(B0) * mu * weight - - return res / Np - - -@stack_array("e", "e_diff") -def check_eta_diff(markers: "float[:,:]"): - r"""TODO""" - # marker position e - e = empty(3, dtype=float) - e_diff = empty(3, dtype=float) - - # get number of markers - n_markers_loc = shape(markers)[0] - - for ip in range(n_markers_loc): - # only do something if particle is a "true" particle (i.e. not a hole) - if markers[ip, 0] == -1.0: - continue - - e[:] = markers[ip, 0:3] - e_diff[:] = e[:] - markers[ip, 9:12] - - for axis in range(3): - if e_diff[axis] > 0.5: - e_diff[axis] -= 1.0 - elif e_diff[axis] < -0.5: - e_diff[axis] += 1.0 - - markers[ip, 15:18] = e_diff[:] - - -@stack_array("e", "e_diff") -def check_eta_diff2(markers: "float[:,:]"): - r"""TODO""" - # marker position e - e = empty(3, dtype=float) - e_diff = empty(3, dtype=float) - - # get number of markers - n_markers_loc = shape(markers)[0] - - for ip in range(n_markers_loc): - # only do something if particle is a "true" particle (i.e. not a hole) - if markers[ip, 0] == -1.0: - continue - - e[:] = markers[ip, 0:3] - e_diff[:] = e[:] - markers[ip, 12:15] - - for axis in range(3): - if e_diff[axis] > 0.5: - e_diff[axis] -= 1.0 - elif e_diff[axis] < -0.5: - e_diff[axis] += 1.0 - - markers[ip, 15:18] = e_diff[:] - - -@stack_array("e", "e_diff", "e_mid") -def check_eta_mid(markers: "float[:,:]"): - r"""TODO""" - # marker position e - e = empty(3, dtype=float) - e_diff = empty(3, dtype=float) - e_mid = empty(3, dtype=float) - - # get number of markers - n_markers_loc = shape(markers)[0] - - for ip in range(n_markers_loc): - # only do something if particle is a "true" particle (i.e. not a hole) - if markers[ip, 0] == -1.0: - continue - - e[:] = markers[ip, 0:3] - markers[ip, 12:15] = e[:] - - e_diff[:] = e[:] - markers[ip, 9:12] - e_mid[:] = (e[:] + markers[ip, 9:12]) / 2.0 - - for axis in range(3): - if e_diff[axis] > 0.5: - e_mid[axis] += 0.5 - elif e_diff[axis] < -0.5: - e_mid[axis] += 0.5 + tmp = gradB + grad_PB_b - markers[ip, 0:3] = e_mid[:] + markers[ip, idx] = linalg_kernels.scalar_dot(eta_diff, tmp) + markers[ip, idx] *= mu @stack_array("dfm", "dfinv", "dfinv_t", "v", "a_form", "dfta_form") diff --git a/src/struphy/polar/basic.py b/src/struphy/polar/basic.py index 78b81d4ff..6a1c2c2f2 100644 --- a/src/struphy/polar/basic.py +++ b/src/struphy/polar/basic.py @@ -1,10 +1,9 @@ +import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from psydac.linalg.basic import Vector, VectorSpace from psydac.linalg.block import BlockVector from psydac.linalg.stencil import StencilVector -from struphy.utils.arrays import xp as np - class PolarDerhamSpace(VectorSpace): """ @@ -209,7 +208,7 @@ class PolarVector(Vector): Element of a PolarDerhamSpace. An instance of a PolarVector consists of two parts: - 1. a list of np.arrays of the polar coeffs (not distributed) + 1. a list of xp.arrays of the polar coeffs (not distributed) 2. a tensor product StencilVector/BlockVector of the parent space with inner rings set to zero (distributed). Parameters @@ -224,7 +223,7 @@ def __init__(self, V): self._dtype = V.dtype # initialize polar coeffs - self._pol = [np.zeros((m, n)) for m, n in zip(V.n_polar, V.n3)] + self._pol = [xp.zeros((m, n)) for m, n in zip(V.n_polar, V.n3)] # full tensor product vector self._tp = V.parent_space.zeros() @@ -241,7 +240,7 @@ def dtype(self): @property def pol(self): - """Polar coefficients as np.array.""" + """Polar coefficients as xp.array.""" return self._pol @pol.setter @@ -327,7 +326,7 @@ def toarray(self, allreduce=False): if self.space.comm is not None and allreduce: self.space.comm.Allreduce(MPI.IN_PLACE, out, op=MPI.SUM) - out = np.concatenate((self.pol[0].flatten(), out)) + out = xp.concatenate((self.pol[0].flatten(), out)) else: out1 = self.tp[0].toarray()[self.space.n_rings[0] * self.space.n[1] * self.space.n3[0] :] @@ -340,7 +339,7 @@ def toarray(self, allreduce=False): self.space.comm.Allreduce(MPI.IN_PLACE, out2, op=MPI.SUM) self.space.comm.Allreduce(MPI.IN_PLACE, out3, op=MPI.SUM) - out = np.concatenate( + out = xp.concatenate( ( self.pol[0].flatten(), out1, @@ -366,7 +365,7 @@ def copy(self, out=None): self._tp.copy(out=w.tp) # copy polar part for n, pl in enumerate(self._pol): - np.copyto(w._pol[n], pl, casting="no") + xp.copyto(w._pol[n], pl, casting="no") return w def __neg__(self): diff --git a/src/struphy/polar/extraction_operators.py b/src/struphy/polar/extraction_operators.py index 9afb9d237..f58c88f59 100644 --- a/src/struphy/polar/extraction_operators.py +++ b/src/struphy/polar/extraction_operators.py @@ -1,4 +1,4 @@ -from struphy.utils.arrays import xp as np +import cunumpy as xp # ============================= 2D polar splines (C1) =================================== @@ -47,8 +47,8 @@ def __init__(self, domain, derham): self._pole = (cx[0, 0], cy[0, 0]) - assert np.all(cx[0] == self.pole[0]) - assert np.all(cy[0] == self.pole[1]) + assert xp.all(cx[0] == self.pole[0]) + assert xp.all(cy[0] == self.pole[1]) self._n0 = cx.shape[0] self._n1 = cx.shape[1] @@ -70,14 +70,14 @@ def __init__(self, domain, derham): self._tau = max( [ ((self.cx[1] - self.pole[0]) * (-2)).max(), - ((self.cx[1] - self.pole[0]) - np.sqrt(3) * (self.cy[1] - self.pole[1])).max(), - ((self.cx[1] - self.pole[0]) + np.sqrt(3) * (self.cy[1] - self.pole[1])).max(), + ((self.cx[1] - self.pole[0]) - xp.sqrt(3) * (self.cy[1] - self.pole[1])).max(), + ((self.cx[1] - self.pole[0]) + xp.sqrt(3) * (self.cy[1] - self.pole[1])).max(), ] ) # barycentric coordinates - self._xi_0 = np.zeros((3, self.n1), dtype=float) - self._xi_1 = np.zeros((3, self.n1), dtype=float) + self._xi_0 = xp.zeros((3, self.n1), dtype=float) + self._xi_1 = xp.zeros((3, self.n1), dtype=float) self._xi_0[:, :] = 1 / 3 @@ -85,12 +85,12 @@ def __init__(self, domain, derham): self._xi_1[1, :] = ( 1 / 3 - 1 / (3 * self.tau) * (self.cx[1] - self.pole[0]) - + np.sqrt(3) / (3 * self.tau) * (self.cy[1] - self.pole[1]) + + xp.sqrt(3) / (3 * self.tau) * (self.cy[1] - self.pole[1]) ) self._xi_1[2, :] = ( 1 / 3 - 1 / (3 * self.tau) * (self.cx[1] - self.pole[0]) - - np.sqrt(3) / (3 * self.tau) * (self.cy[1] - self.pole[1]) + - xp.sqrt(3) / (3 * self.tau) * (self.cy[1] - self.pole[1]) ) # remove small values @@ -102,17 +102,17 @@ def __init__(self, domain, derham): # ============= basis extraction operator for discrete 0-forms ================ # first n_rings tp rings --> "polar coeffs" - e0_blocks_ten_to_pol = np.block([self.xi_0, self.xi_1]) + e0_blocks_ten_to_pol = xp.block([self.xi_0, self.xi_1]) self._e_ten_to_pol["0"] = [[csr(e0_blocks_ten_to_pol)]] # ============ basis extraction operator for discrete 1-forms (Hcurl) ========= # first n_rings tp rings --> "polar coeffs" - e1_11_blocks_ten_to_pol = np.zeros((self.n_polar[1][0], self.n_rings[1][0] * self.n1), dtype=float) - e1_12_blocks_ten_to_pol = np.zeros((self.n_polar[1][0], self.n_rings[1][1] * self.d1), dtype=float) + e1_11_blocks_ten_to_pol = xp.zeros((self.n_polar[1][0], self.n_rings[1][0] * self.n1), dtype=float) + e1_12_blocks_ten_to_pol = xp.zeros((self.n_polar[1][0], self.n_rings[1][1] * self.d1), dtype=float) - e1_21_blocks_ten_to_pol = np.zeros((self.n_polar[1][1], self.n_rings[1][0] * self.n1), dtype=float) - e1_22_blocks_ten_to_pol = np.zeros((self.n_polar[1][1], self.n_rings[1][1] * self.d1), dtype=float) + e1_21_blocks_ten_to_pol = xp.zeros((self.n_polar[1][1], self.n_rings[1][0] * self.n1), dtype=float) + e1_22_blocks_ten_to_pol = xp.zeros((self.n_polar[1][1], self.n_rings[1][1] * self.d1), dtype=float) # 1st component for l in range(2): @@ -135,7 +135,7 @@ def __init__(self, domain, derham): # =============== basis extraction operator for discrete 1-forms (Hdiv) ========= # first n_rings tp rings --> "polar coeffs" - e3_blocks_ten_to_pol = np.zeros((self.n_polar[3][0], self.n_rings[3][0] * self.d1), dtype=float) + e3_blocks_ten_to_pol = xp.zeros((self.n_polar[3][0], self.n_rings[3][0] * self.d1), dtype=float) self._e_ten_to_pol["2"] = [ [csr(e1_22_blocks_ten_to_pol), csr(-e1_21_blocks_ten_to_pol), None], @@ -161,7 +161,7 @@ def __init__(self, domain, derham): self._p_ten_to_ten = {} # first n_rings tp rings --> "polar coeffs" - p0_blocks_ten_to_pol = np.zeros((self.n_polar[0][0], self.n_rings[0][0] * self.n1), dtype=float) + p0_blocks_ten_to_pol = xp.zeros((self.n_polar[0][0], self.n_rings[0][0] * self.n1), dtype=float) # !! NOTE: for odd spline degrees and periodic splines the first Greville point sometimes does NOT start at zero!! if domain.p[1] % 2 != 0 and not (abs(derham.Vh_fem["0"].spaces[1].interpolation_grid[0]) < 1e-14): @@ -176,15 +176,15 @@ def __init__(self, domain, derham): self._p_ten_to_pol["0"] = [[csr(p0_blocks_ten_to_pol)]] # first n_rings + 1 tp rings --> "first tp ring" - p0_blocks_ten_to_ten = np.block([0 * np.identity(self.n1)] * self.n_rings[0][0] + [np.identity(self.n1)]) + p0_blocks_ten_to_ten = xp.block([0 * xp.identity(self.n1)] * self.n_rings[0][0] + [xp.identity(self.n1)]) self._p_ten_to_ten["0"] = [[csr(p0_blocks_ten_to_ten)]] # =========== projection extraction operator for discrete 1-forms (Hcurl) ======== # first n_rings tp rings --> "polar coeffs" - p1_11_blocks_ten_to_pol = np.zeros((self.n_polar[1][0], self.n_rings[1][0] * self.n1), dtype=float) - p1_22_blocks_ten_to_pol = np.zeros((self.n_polar[1][1], self.n_rings[1][1] * self.d1), dtype=float) + p1_11_blocks_ten_to_pol = xp.zeros((self.n_polar[1][0], self.n_rings[1][0] * self.n1), dtype=float) + p1_22_blocks_ten_to_pol = xp.zeros((self.n_polar[1][1], self.n_rings[1][1] * self.d1), dtype=float) # !! NOTE: PSYDAC's first integration interval sometimes start at < 0 !! if derham.Vh_fem["3"].spaces[1].histopolation_grid[0] < -1e-14: @@ -196,8 +196,8 @@ def __init__(self, domain, derham): p1_22_blocks_ten_to_pol[1, (self.d1 + 0 * self.d1 // 3) : (self.d1 + 1 * self.d1 // 3)] = 1.0 p1_22_blocks_ten_to_pol[1, (self.d1 + 1 * self.d1 // 3) : (self.d1 + 2 * self.d1 // 3)] = 1.0 - p1_12_blocks_ten_to_pol = np.zeros((self.n_polar[1][0], self.n_rings[1][1] * self.d1), dtype=float) - p1_21_blocks_ten_to_pol = np.zeros((self.n_polar[1][1], self.n_rings[1][0] * self.d1), dtype=float) + p1_12_blocks_ten_to_pol = xp.zeros((self.n_polar[1][0], self.n_rings[1][1] * self.d1), dtype=float) + p1_21_blocks_ten_to_pol = xp.zeros((self.n_polar[1][1], self.n_rings[1][0] * self.d1), dtype=float) self._p_ten_to_pol["1"] = [ [csr(p1_11_blocks_ten_to_pol), csr(p1_12_blocks_ten_to_pol), None], @@ -206,26 +206,26 @@ def __init__(self, domain, derham): ] # first n_rings + 1 tp rings --> "first tp ring" - p1_11_blocks_ten_to_ten = np.zeros((self.n1, self.n1), dtype=float) + p1_11_blocks_ten_to_ten = xp.zeros((self.n1, self.n1), dtype=float) # !! NOTE: for odd spline degrees and periodic splines the first Greville point sometimes does NOT start at zero!! if domain.p[1] % 2 != 0 and not (abs(derham.Vh_fem["0"].spaces[1].interpolation_grid[0]) < 1e-14): - p1_11_blocks_ten_to_ten[:, 3 * self.n1 // 3 - 1] = -np.roll(self.xi_1[0], -1) - p1_11_blocks_ten_to_ten[:, 1 * self.n1 // 3 - 1] = -np.roll(self.xi_1[1], -1) - p1_11_blocks_ten_to_ten[:, 2 * self.n1 // 3 - 1] = -np.roll(self.xi_1[2], -1) + p1_11_blocks_ten_to_ten[:, 3 * self.n1 // 3 - 1] = -xp.roll(self.xi_1[0], -1) + p1_11_blocks_ten_to_ten[:, 1 * self.n1 // 3 - 1] = -xp.roll(self.xi_1[1], -1) + p1_11_blocks_ten_to_ten[:, 2 * self.n1 // 3 - 1] = -xp.roll(self.xi_1[2], -1) else: p1_11_blocks_ten_to_ten[:, 0 * self.n1 // 3] = -self.xi_1[0] p1_11_blocks_ten_to_ten[:, 1 * self.n1 // 3] = -self.xi_1[1] p1_11_blocks_ten_to_ten[:, 2 * self.n1 // 3] = -self.xi_1[2] - p1_11_blocks_ten_to_ten += np.identity(self.n1) + p1_11_blocks_ten_to_ten += xp.identity(self.n1) - p1_11_blocks_ten_to_ten = np.block([p1_11_blocks_ten_to_ten, np.identity(self.n1)]) + p1_11_blocks_ten_to_ten = xp.block([p1_11_blocks_ten_to_ten, xp.identity(self.n1)]) - p1_22_blocks_ten_to_ten = np.block([0 * np.identity(self.d1)] * self.n_rings[1][1] + [np.identity(self.d1)]) + p1_22_blocks_ten_to_ten = xp.block([0 * xp.identity(self.d1)] * self.n_rings[1][1] + [xp.identity(self.d1)]) - p1_12_blocks_ten_to_ten = np.zeros((self.d1, (self.n_rings[1][1] + 1) * self.d1), dtype=float) - p1_21_blocks_ten_to_ten = np.zeros((self.n1, (self.n_rings[1][0] + 1) * self.n1), dtype=float) + p1_12_blocks_ten_to_ten = xp.zeros((self.d1, (self.n_rings[1][1] + 1) * self.d1), dtype=float) + p1_21_blocks_ten_to_ten = xp.zeros((self.n1, (self.n_rings[1][0] + 1) * self.n1), dtype=float) self._p_ten_to_ten["1"] = [ [csr(p1_11_blocks_ten_to_ten), csr(p1_12_blocks_ten_to_ten), None], @@ -236,7 +236,7 @@ def __init__(self, domain, derham): # ========== projection extraction operator for discrete 1-forms (Hdiv) ========== # first n_rings tp rings --> "polar coeffs" - p3_blocks_ten_to_pol = np.zeros((self.n_polar[3][0], self.n_rings[3][0] * self.d1), dtype=float) + p3_blocks_ten_to_pol = xp.zeros((self.n_polar[3][0], self.n_rings[3][0] * self.d1), dtype=float) self._p_ten_to_pol["2"] = [ [csr(p1_22_blocks_ten_to_pol), csr(p1_21_blocks_ten_to_pol), None], @@ -245,24 +245,24 @@ def __init__(self, domain, derham): ] # first n_rings + 1 tp rings --> "first tp ring" - p3_blocks_ten_to_ten = np.zeros((self.d1, self.d1), dtype=float) + p3_blocks_ten_to_ten = xp.zeros((self.d1, self.d1), dtype=float) - a0 = np.diff(self.xi_1[1], append=self.xi_1[1, 0]) - a1 = np.diff(self.xi_1[2], append=self.xi_1[2, 0]) + a0 = xp.diff(self.xi_1[1], append=self.xi_1[1, 0]) + a1 = xp.diff(self.xi_1[2], append=self.xi_1[2, 0]) # !! NOTE: PSYDAC's first integration interval sometimes start at < 0 !! if derham.Vh_fem["3"].spaces[1].histopolation_grid[0] < -1e-14: p3_blocks_ten_to_ten[:, (0 * self.n1 // 3 + 1) : (1 * self.n1 // 3 + 1)] = ( - -np.roll(a0, +1)[:, None] - np.roll(a1, +1)[:, None] + -xp.roll(a0, +1)[:, None] - xp.roll(a1, +1)[:, None] ) - p3_blocks_ten_to_ten[:, (1 * self.n1 // 3 + 1) : (2 * self.n1 // 3 + 1)] = -np.roll(a1, +1)[:, None] + p3_blocks_ten_to_ten[:, (1 * self.n1 // 3 + 1) : (2 * self.n1 // 3 + 1)] = -xp.roll(a1, +1)[:, None] else: p3_blocks_ten_to_ten[:, 0 * self.n1 // 3 : 1 * self.n1 // 3] = -a0[:, None] - a1[:, None] p3_blocks_ten_to_ten[:, 1 * self.n1 // 3 : 2 * self.n1 // 3] = -a1[:, None] - p3_blocks_ten_to_ten += np.identity(self.d1) + p3_blocks_ten_to_ten += xp.identity(self.d1) - p3_blocks_ten_to_ten = np.block([p3_blocks_ten_to_ten, np.identity(self.d1)]) + p3_blocks_ten_to_ten = xp.block([p3_blocks_ten_to_ten, xp.identity(self.d1)]) self._p_ten_to_ten["2"] = [ [csr(p1_22_blocks_ten_to_ten), csr(p1_21_blocks_ten_to_ten), None], @@ -295,24 +295,24 @@ def __init__(self, domain, derham): # ======================= discrete gradient ====================================== # "polar coeffs" to "polar coeffs" - grad_pol_to_pol_1 = np.zeros((self.n_polar[1][0], self.n_polar[0][0]), dtype=float) - grad_pol_to_pol_2 = np.array([[-1.0, 1.0, 0.0], [-1.0, 0.0, 1.0]]) - grad_pol_to_pol_3 = np.identity(self.n_polar[0][0], dtype=float) + grad_pol_to_pol_1 = xp.zeros((self.n_polar[1][0], self.n_polar[0][0]), dtype=float) + grad_pol_to_pol_2 = xp.array([[-1.0, 1.0, 0.0], [-1.0, 0.0, 1.0]]) + grad_pol_to_pol_3 = xp.identity(self.n_polar[0][0], dtype=float) self._grad_pol_to_pol = [[csr(grad_pol_to_pol_1)], [csr(grad_pol_to_pol_2)], [csr(grad_pol_to_pol_3)]] # "polar coeffs" to "first tp ring" - grad_pol_to_ten_1 = np.zeros(((self.n_rings[1][0] + 1) * self.n1, self.n_polar[0][0])) - grad_pol_to_ten_2 = np.zeros(((self.n_rings[1][1] + 1) * self.d1, self.n_polar[0][0])) - grad_pol_to_ten_3 = np.zeros(((self.n_rings[0][0] + 1) * self.n1, self.n_polar[0][0])) + grad_pol_to_ten_1 = xp.zeros(((self.n_rings[1][0] + 1) * self.n1, self.n_polar[0][0])) + grad_pol_to_ten_2 = xp.zeros(((self.n_rings[1][1] + 1) * self.d1, self.n_polar[0][0])) + grad_pol_to_ten_3 = xp.zeros(((self.n_rings[0][0] + 1) * self.n1, self.n_polar[0][0])) grad_pol_to_ten_1[-self.n1 :, :] = -self.xi_1.T self._grad_pol_to_ten = [[csr(grad_pol_to_ten_1)], [csr(grad_pol_to_ten_2)], [csr(grad_pol_to_ten_3)]] # eta_3 direction - grad_e3_1 = np.identity(self.n2, dtype=float) - grad_e3_2 = np.identity(self.n2, dtype=float) + grad_e3_1 = xp.identity(self.n2, dtype=float) + grad_e3_2 = xp.identity(self.n2, dtype=float) grad_e3_3 = grad_1d_matrix(derham.spl_kind[2], self.n2) self._grad_e3 = [[csr(grad_e3_1)], [csr(grad_e3_2)], [csr(grad_e3_3)]] @@ -320,14 +320,14 @@ def __init__(self, domain, derham): # =========================== discrete curl ====================================== # "polar coeffs" to "polar coeffs" - curl_pol_to_pol_12 = np.identity(self.n_polar[1][1], dtype=float) - curl_pol_to_pol_13 = np.array([[-1.0, 1.0, 0.0], [-1.0, 0.0, 1.0]]) + curl_pol_to_pol_12 = xp.identity(self.n_polar[1][1], dtype=float) + curl_pol_to_pol_13 = xp.array([[-1.0, 1.0, 0.0], [-1.0, 0.0, 1.0]]) - curl_pol_to_pol_21 = np.identity(self.n_polar[1][0], dtype=float) - curl_pol_to_pol_23 = np.zeros((self.n_polar[2][1], self.n_polar[0][0]), dtype=float) + curl_pol_to_pol_21 = xp.identity(self.n_polar[1][0], dtype=float) + curl_pol_to_pol_23 = xp.zeros((self.n_polar[2][1], self.n_polar[0][0]), dtype=float) - curl_pol_to_pol_31 = np.zeros((self.n_polar[3][0], self.n_polar[1][0]), dtype=float) - curl_pol_to_pol_32 = np.zeros((self.n_polar[3][0], self.n_polar[1][1]), dtype=float) + curl_pol_to_pol_31 = xp.zeros((self.n_polar[3][0], self.n_polar[1][0]), dtype=float) + curl_pol_to_pol_32 = xp.zeros((self.n_polar[3][0], self.n_polar[1][1]), dtype=float) self._curl_pol_to_pol = [ [None, csr(-curl_pol_to_pol_12), csr(curl_pol_to_pol_13)], @@ -336,14 +336,14 @@ def __init__(self, domain, derham): ] # "polar coeffs" to "first tp ring" - curl_pol_to_ten_12 = np.zeros(((self.n_rings[2][0] + 1) * self.d1, self.n_polar[1][1])) - curl_pol_to_ten_13 = np.zeros(((self.n_rings[2][0] + 1) * self.d1, self.n_polar[0][0])) + curl_pol_to_ten_12 = xp.zeros(((self.n_rings[2][0] + 1) * self.d1, self.n_polar[1][1])) + curl_pol_to_ten_13 = xp.zeros(((self.n_rings[2][0] + 1) * self.d1, self.n_polar[0][0])) - curl_pol_to_ten_21 = np.zeros(((self.n_rings[2][1] + 1) * self.n1, self.n_polar[1][0])) - curl_pol_to_ten_23 = np.zeros(((self.n_rings[2][1] + 1) * self.n1, self.n_polar[0][0])) + curl_pol_to_ten_21 = xp.zeros(((self.n_rings[2][1] + 1) * self.n1, self.n_polar[1][0])) + curl_pol_to_ten_23 = xp.zeros(((self.n_rings[2][1] + 1) * self.n1, self.n_polar[0][0])) - curl_pol_to_ten_31 = np.zeros(((self.n_rings[3][0] + 1) * self.n1, self.n_polar[1][0])) - curl_pol_to_ten_32 = np.zeros(((self.n_rings[3][0] + 1) * self.d1, self.n_polar[1][1])) + curl_pol_to_ten_31 = xp.zeros(((self.n_rings[3][0] + 1) * self.n1, self.n_polar[1][0])) + curl_pol_to_ten_32 = xp.zeros(((self.n_rings[3][0] + 1) * self.d1, self.n_polar[1][1])) curl_pol_to_ten_23[-self.n1 :, :] = -self.xi_1.T @@ -361,13 +361,13 @@ def __init__(self, domain, derham): # eta_3 direction curl_e3_12 = grad_1d_matrix(derham.spl_kind[2], self.n2) - curl_e3_13 = np.identity(self.d2) + curl_e3_13 = xp.identity(self.d2) curl_e3_21 = grad_1d_matrix(derham.spl_kind[2], self.n2) - curl_e3_23 = np.identity(self.d2) + curl_e3_23 = xp.identity(self.d2) - curl_e3_31 = np.identity(self.n2) - curl_e3_32 = np.identity(self.n2) + curl_e3_31 = xp.identity(self.n2) + curl_e3_32 = xp.identity(self.n2) self._curl_e3 = [ [None, csr(curl_e3_12), csr(curl_e3_13)], @@ -378,16 +378,16 @@ def __init__(self, domain, derham): # =========================== discrete div ====================================== # "polar coeffs" to "polar coeffs" - div_pol_to_pol_1 = np.zeros((self.n_polar[3][0], self.n_polar[2][0]), dtype=float) - div_pol_to_pol_2 = np.zeros((self.n_polar[3][0], self.n_polar[2][1]), dtype=float) - div_pol_to_pol_3 = np.identity(self.n_polar[3][0], dtype=float) + div_pol_to_pol_1 = xp.zeros((self.n_polar[3][0], self.n_polar[2][0]), dtype=float) + div_pol_to_pol_2 = xp.zeros((self.n_polar[3][0], self.n_polar[2][1]), dtype=float) + div_pol_to_pol_3 = xp.identity(self.n_polar[3][0], dtype=float) self._div_pol_to_pol = [[csr(div_pol_to_pol_1), csr(div_pol_to_pol_2), csr(div_pol_to_pol_3)]] # "polar coeffs" to "first tp ring" - div_pol_to_ten_1 = np.zeros(((self.n_rings[3][0] + 1) * self.d1, self.n_polar[2][0])) - div_pol_to_ten_2 = np.zeros(((self.n_rings[3][0] + 1) * self.d1, self.n_polar[2][1])) - div_pol_to_ten_3 = np.zeros(((self.n_rings[3][0] + 1) * self.d1, self.n_polar[3][0])) + div_pol_to_ten_1 = xp.zeros(((self.n_rings[3][0] + 1) * self.d1, self.n_polar[2][0])) + div_pol_to_ten_2 = xp.zeros(((self.n_rings[3][0] + 1) * self.d1, self.n_polar[2][1])) + div_pol_to_ten_3 = xp.zeros(((self.n_rings[3][0] + 1) * self.d1, self.n_polar[3][0])) for l in range(2): for j in range(self.d1, 2 * self.d1): @@ -398,8 +398,8 @@ def __init__(self, domain, derham): self._div_pol_to_ten = [[csr(div_pol_to_ten_1), csr(div_pol_to_ten_2), csr(div_pol_to_ten_3)]] # eta_3 direction - div_e3_1 = np.identity(self.d2, dtype=float) - div_e3_2 = np.identity(self.d2, dtype=float) + div_e3_1 = xp.identity(self.d2, dtype=float) + div_e3_2 = xp.identity(self.d2, dtype=float) div_e3_3 = grad_1d_matrix(derham.spl_kind[2], self.n2) self._div_e3 = [[csr(div_e3_1), csr(div_e3_2), csr(div_e3_3)]] @@ -539,13 +539,13 @@ def __init__(self, n0, n1): # =========== extraction operators for discrete 0-forms ================== # extraction operator for basis functions - self.E0_11 = spa.csr_matrix(np.ones((1, n1), dtype=float)) + self.E0_11 = spa.csr_matrix(xp.ones((1, n1), dtype=float)) self.E0_22 = spa.identity((n0 - 1) * n1, format="csr") self.E0 = spa.bmat([[self.E0_11, None], [None, self.E0_22]], format="csr") # global projection extraction operator for interpolation points - self.P0_11 = np.zeros((1, n1), dtype=float) + self.P0_11 = xp.zeros((1, n1), dtype=float) self.P0_11[0, 0] = 1.0 @@ -598,7 +598,7 @@ def __init__(self, n0, n1): # ========= discrete polar gradient matrix =============================== # radial dofs (DN) - G11 = np.zeros(((d0 - 0) * n1, 1), dtype=float) + G11 = xp.zeros(((d0 - 0) * n1, 1), dtype=float) G11[:n1, 0] = -1.0 G12 = spa.kron(grad_1d_1[:, 1:], spa.identity(n1)) @@ -606,7 +606,7 @@ def __init__(self, n0, n1): self.G1 = spa.bmat([[G11, G12]], format="csr") # angular dofs (ND) - G21 = np.zeros(((n0 - 1) * d1, 1), dtype=float) + G21 = xp.zeros(((n0 - 1) * d1, 1), dtype=float) G22 = spa.kron(spa.identity(n0 - 1), grad_1d_2, format="csr") self.G2 = spa.bmat([[G21, G22]], format="csr") @@ -619,13 +619,13 @@ def __init__(self, n0, n1): # 2D vector curl (NN --> ND DN) # angular dofs (ND) - VC11 = np.zeros(((n0 - 1) * d1, 1), dtype=float) + VC11 = xp.zeros(((n0 - 1) * d1, 1), dtype=float) VC12 = spa.kron(spa.identity(n0 - 1), grad_1d_2, format="csr") self.VC1 = spa.bmat([[VC11, VC12]], format="csr") # radial dofs (DN) - VC21 = np.zeros(((d0 - 0) * n1, 1), dtype=float) + VC21 = xp.zeros(((d0 - 0) * n1, 1), dtype=float) VC21[:n1, 0] = 1.0 VC22 = -spa.kron(grad_1d_1[:, 1:], spa.identity(n1)) @@ -687,26 +687,26 @@ def __init__(self, cx, cy): self.Nbase2 = (d0 - 1) * d1 # size of control triangle - self.tau = np.array( + self.tau = xp.array( [ (-2 * (cx[1] - self.x0)).max(), - ((cx[1] - self.x0) - np.sqrt(3) * (cy[1] - self.y0)).max(), - ((cx[1] - self.x0) + np.sqrt(3) * (cy[1] - self.y0)).max(), + ((cx[1] - self.x0) - xp.sqrt(3) * (cy[1] - self.y0)).max(), + ((cx[1] - self.x0) + xp.sqrt(3) * (cy[1] - self.y0)).max(), ] ).max() - self.Xi_0 = np.zeros((3, n1), dtype=float) - self.Xi_1 = np.zeros((3, n1), dtype=float) + self.Xi_0 = xp.zeros((3, n1), dtype=float) + self.Xi_1 = xp.zeros((3, n1), dtype=float) # barycentric coordinates self.Xi_0[:, :] = 1 / 3 self.Xi_1[0, :] = 1 / 3 + 2 / (3 * self.tau) * (cx[1] - self.x0) self.Xi_1[1, :] = ( - 1 / 3 - 1 / (3 * self.tau) * (cx[1] - self.x0) + np.sqrt(3) / (3 * self.tau) * (cy[1] - self.y0) + 1 / 3 - 1 / (3 * self.tau) * (cx[1] - self.x0) + xp.sqrt(3) / (3 * self.tau) * (cy[1] - self.y0) ) self.Xi_1[2, :] = ( - 1 / 3 - 1 / (3 * self.tau) * (cx[1] - self.x0) - np.sqrt(3) / (3 * self.tau) * (cy[1] - self.y0) + 1 / 3 - 1 / (3 * self.tau) * (cx[1] - self.x0) - xp.sqrt(3) / (3 * self.tau) * (cy[1] - self.y0) ) # remove small values @@ -714,13 +714,13 @@ def __init__(self, cx, cy): # =========== extraction operators for discrete 0-forms ================== # extraction operator for basis functions - self.E0_11 = spa.csr_matrix(np.hstack((self.Xi_0, self.Xi_1))) + self.E0_11 = spa.csr_matrix(xp.hstack((self.Xi_0, self.Xi_1))) self.E0_22 = spa.identity((n0 - 2) * n1, format="csr") self.E0 = spa.bmat([[self.E0_11, None], [None, self.E0_22]], format="csr") # global projection extraction operator for interpolation points - self.P0_11 = np.zeros((3, 2 * n1), dtype=float) + self.P0_11 = xp.zeros((3, 2 * n1), dtype=float) self.P0_11[0, n1 + 0 * n1 // 3] = 1.0 self.P0_11[1, n1 + 1 * n1 // 3] = 1.0 @@ -737,8 +737,8 @@ def __init__(self, cx, cy): self.E1C_12 = spa.identity((d0 - 1) * n1) self.E1C_34 = spa.identity((n0 - 2) * d1) - self.E1C_21 = np.zeros((2, 1 * n1), dtype=float) - self.E1C_23 = np.zeros((2, 2 * d1), dtype=float) + self.E1C_21 = xp.zeros((2, 1 * n1), dtype=float) + self.E1C_23 = xp.zeros((2, 2 * d1), dtype=float) # 1st component for s in range(2): @@ -760,22 +760,22 @@ def __init__(self, cx, cy): # extraction operator for interpolation/histopolation in global projector # 1st component - self.P1C_11 = np.zeros((n1, n1), dtype=float) + self.P1C_11 = xp.zeros((n1, n1), dtype=float) self.P1C_12 = spa.identity(n1) self.P1C_23 = spa.identity((d0 - 2) * n1) self.P1C_11[:, 0 * n1 // 3] = -self.Xi_1[0] self.P1C_11[:, 1 * n1 // 3] = -self.Xi_1[1] self.P1C_11[:, 2 * n1 // 3] = -self.Xi_1[2] - self.P1C_11 += np.identity(n1) + self.P1C_11 += xp.identity(n1) # 2nd component - self.P1C_34 = np.zeros((2, 2 * d1), dtype=float) + self.P1C_34 = xp.zeros((2, 2 * d1), dtype=float) self.P1C_45 = spa.identity((n0 - 2) * d1) - self.P1C_34[0, (d1 + 0 * d1 // 3) : (d1 + 1 * d1 // 3)] = np.ones(d1 // 3, dtype=float) - self.P1C_34[1, (d1 + 0 * d1 // 3) : (d1 + 1 * d1 // 3)] = np.ones(d1 // 3, dtype=float) - self.P1C_34[1, (d1 + 1 * d1 // 3) : (d1 + 2 * d1 // 3)] = np.ones(d1 // 3, dtype=float) + self.P1C_34[0, (d1 + 0 * d1 // 3) : (d1 + 1 * d1 // 3)] = xp.ones(d1 // 3, dtype=float) + self.P1C_34[1, (d1 + 0 * d1 // 3) : (d1 + 1 * d1 // 3)] = xp.ones(d1 // 3, dtype=float) + self.P1C_34[1, (d1 + 1 * d1 // 3) : (d1 + 2 * d1 // 3)] = xp.ones(d1 // 3, dtype=float) # combined first and second component self.P1C = spa.bmat( @@ -790,8 +790,8 @@ def __init__(self, cx, cy): # ========================================================================= # ========= extraction operators for discrete 1-forms (H_div) ============= - self.E1D_11 = np.zeros((2, 2 * d1), dtype=float) - self.E1D_13 = np.zeros((2, 1 * n1), dtype=float) + self.E1D_11 = xp.zeros((2, 2 * d1), dtype=float) + self.E1D_13 = xp.zeros((2, 1 * n1), dtype=float) self.E1D_22 = spa.identity((n0 - 2) * d1) self.E1D_34 = spa.identity((d0 - 1) * n1) @@ -834,13 +834,13 @@ def __init__(self, cx, cy): # ========================================================================= # =========== extraction operators for discrete 2-forms =================== - self.E2_1 = np.zeros(((d0 - 1) * d1, d1), dtype=float) + self.E2_1 = xp.zeros(((d0 - 1) * d1, d1), dtype=float) self.E2_2 = spa.identity((d0 - 1) * d1) self.E2 = spa.bmat([[self.E2_1, self.E2_2]], format="csr") # extraction operator for histopolation in global projector - self.P2_11 = np.zeros((d1, d1), dtype=float) + self.P2_11 = xp.zeros((d1, d1), dtype=float) self.P2_12 = spa.identity(d1) self.P2_23 = spa.identity((d0 - 2) * d1) @@ -853,7 +853,7 @@ def __init__(self, cx, cy): # block B self.P2_11[i, 1 * n1 // 3 : 2 * n1 // 3] = -(self.Xi_1[2, (i + 1) % n1] - self.Xi_1[2, i]) - self.P2_11 += np.identity(d1) + self.P2_11 += xp.identity(d1) self.P2 = spa.bmat([[self.P2_11, self.P2_12, None], [None, None, self.P2_23]], format="csr") # ========================================================================= @@ -864,14 +864,14 @@ def __init__(self, cx, cy): # ========================================================================= # ========= discrete polar gradient matrix ================================ - self.G1_1 = np.zeros(((d0 - 1) * n1, 3), dtype=float) + self.G1_1 = xp.zeros(((d0 - 1) * n1, 3), dtype=float) self.G1_1[:n1, :] = -self.Xi_1.T self.G1_2 = spa.kron(grad_1d_1[1:, 2:], spa.identity(n1)) self.G1 = spa.bmat([[self.G1_1, self.G1_2]], format="csr") - self.G2_11 = np.zeros((2, 3), dtype=float) + self.G2_11 = xp.zeros((2, 3), dtype=float) self.G2_11[0, 0] = -1.0 self.G2_11[0, 1] = 1.0 @@ -888,7 +888,7 @@ def __init__(self, cx, cy): # ========= discrete polar curl matrix =================================== # 2D vector curl - self.VC1_11 = np.zeros((2, 3), dtype=float) + self.VC1_11 = xp.zeros((2, 3), dtype=float) self.VC1_11[0, 0] = -1.0 self.VC1_11[0, 1] = 1.0 @@ -900,7 +900,7 @@ def __init__(self, cx, cy): self.VC1 = spa.bmat([[self.VC1_11, None], [None, self.VC1_22]], format="csr") - self.VC2_11 = np.zeros(((d0 - 1) * n1, 3), dtype=float) + self.VC2_11 = xp.zeros(((d0 - 1) * n1, 3), dtype=float) self.VC2_11[:n1, :] = -self.Xi_1.T self.VC2_22 = spa.kron(grad_1d_1[1:, 2:], spa.identity(n1)) @@ -912,7 +912,7 @@ def __init__(self, cx, cy): # 2D scalar curl self.SC1 = -spa.kron(spa.identity(d0 - 1), grad_1d_2) - self.SC2_1 = np.zeros(((d0 - 1) * d1, 2), dtype=float) + self.SC2_1 = xp.zeros(((d0 - 1) * d1, 2), dtype=float) for s in range(2): for j in range(d1): @@ -926,7 +926,7 @@ def __init__(self, cx, cy): # ========================================================================= # ========= discrete polar div matrix ===================================== - self.D1_1 = np.zeros(((d0 - 1) * d1, 2), dtype=float) + self.D1_1 = xp.zeros(((d0 - 1) * d1, 2), dtype=float) for s in range(2): for j in range(d1): @@ -965,24 +965,24 @@ def __init__(self, tensor_space, cx, cy): self.Nbase3_pol = (d0 - 1) * d1 # size of control triangle - self.tau = np.array( - [(-2 * cx[1]).max(), (cx[1] - np.sqrt(3) * cy[1]).max(), (cx[1] + np.sqrt(3) * cy[1]).max()] + self.tau = xp.array( + [(-2 * cx[1]).max(), (cx[1] - xp.sqrt(3) * cy[1]).max(), (cx[1] + xp.sqrt(3) * cy[1]).max()] ).max() - self.Xi_0 = np.zeros((3, n1), dtype=float) - self.Xi_1 = np.zeros((3, n1), dtype=float) + self.Xi_0 = xp.zeros((3, n1), dtype=float) + self.Xi_1 = xp.zeros((3, n1), dtype=float) # barycentric coordinates self.Xi_0[:, :] = 1 / 3 self.Xi_1[0, :] = 1 / 3 + 2 / (3 * self.tau) * cx[1, :, 0] - self.Xi_1[1, :] = 1 / 3 - 1 / (3 * self.tau) * cx[1, :, 0] + np.sqrt(3) / (3 * self.tau) * cy[1, :, 0] - self.Xi_1[2, :] = 1 / 3 - 1 / (3 * self.tau) * cx[1, :, 0] - np.sqrt(3) / (3 * self.tau) * cy[1, :, 0] + self.Xi_1[1, :] = 1 / 3 - 1 / (3 * self.tau) * cx[1, :, 0] + xp.sqrt(3) / (3 * self.tau) * cy[1, :, 0] + self.Xi_1[2, :] = 1 / 3 - 1 / (3 * self.tau) * cx[1, :, 0] - xp.sqrt(3) / (3 * self.tau) * cy[1, :, 0] # =========== extraction operators for discrete 0-forms ================== # extraction operator for basis functions self.E0_pol = spa.bmat( - [[np.hstack((self.Xi_0, self.Xi_1)), None], [None, spa.identity((n0 - 2) * n1)]], format="csr" + [[xp.hstack((self.Xi_0, self.Xi_1)), None], [None, spa.identity((n0 - 2) * n1)]], format="csr" ) self.E0 = spa.kron(self.E0_pol, spa.identity(n2), format="csr") @@ -1005,7 +1005,7 @@ def __init__(self, tensor_space, cx, cy): for j in range(n1): self.E1_1_pol[(d0 - 1) * n1 + s, j] = self.Xi_1[s + 1, j] - self.Xi_0[s + 1, j] - self.E1_1_pol[: (d0 - 1) * n1, n1:] = np.identity((d0 - 1) * n1) + self.E1_1_pol[: (d0 - 1) * n1, n1:] = xp.identity((d0 - 1) * n1) self.E1_1_pol = self.E1_1_pol.tocsr() # 2nd component @@ -1014,7 +1014,7 @@ def __init__(self, tensor_space, cx, cy): self.E1_2_pol[(d0 - 1) * n1 + s, j] = 0.0 self.E1_2_pol[(d0 - 1) * n1 + s, n1 + j] = self.Xi_1[s + 1, (j + 1) % n1] - self.Xi_1[s + 1, j] - self.E1_2_pol[((d0 - 1) * n1 + 2) :, 2 * d1 :] = np.identity((n0 - 2) * d1) + self.E1_2_pol[((d0 - 1) * n1 + 2) :, 2 * d1 :] = xp.identity((n0 - 2) * d1) self.E1_2_pol = self.E1_2_pol.tocsr() # 3rd component @@ -1043,9 +1043,9 @@ def __init__(self, tensor_space, cx, cy): self.P1_1_pol = self.P1_1_pol.tocsr() # 2nd component - self.P1_2_pol[0, (n1 + 0 * n1 // 3) : (n1 + 1 * n1 // 3)] = np.ones((1, n1 // 3), dtype=float) - self.P1_2_pol[1, (n1 + 0 * n1 // 3) : (n1 + 1 * n1 // 3)] = np.ones((1, n1 // 3), dtype=float) - self.P1_2_pol[1, (n1 + 1 * n1 // 3) : (n1 + 2 * n1 // 3)] = np.ones((1, n1 // 3), dtype=float) + self.P1_2_pol[0, (n1 + 0 * n1 // 3) : (n1 + 1 * n1 // 3)] = xp.ones((1, n1 // 3), dtype=float) + self.P1_2_pol[1, (n1 + 0 * n1 // 3) : (n1 + 1 * n1 // 3)] = xp.ones((1, n1 // 3), dtype=float) + self.P1_2_pol[1, (n1 + 1 * n1 // 3) : (n1 + 2 * n1 // 3)] = xp.ones((1, n1 // 3), dtype=float) self.P1_2_pol[2:, 2 * n1 :] = spa.identity((n0 - 2) * d1) self.P1_2_pol = self.P1_2_pol.tocsr() @@ -1073,7 +1073,7 @@ def __init__(self, tensor_space, cx, cy): self.E2_1_pol[s, j] = 0.0 self.E2_1_pol[s, n1 + j] = self.Xi_1[s + 1, (j + 1) % n1] - self.Xi_1[s + 1, j] - self.E2_1_pol[2 : (2 + (n0 - 2) * d1), 2 * n1 :] = np.identity((n0 - 2) * d1) + self.E2_1_pol[2 : (2 + (n0 - 2) * d1), 2 * n1 :] = xp.identity((n0 - 2) * d1) self.E2_1_pol = self.E2_1_pol.tocsr() # 2nd component @@ -1081,11 +1081,11 @@ def __init__(self, tensor_space, cx, cy): for j in range(n1): self.E2_2_pol[s, j] = -(self.Xi_1[s + 1, j] - self.Xi_0[s + 1, j]) - self.E2_2_pol[(2 + (n0 - 2) * d1) :, 1 * n1 :] = np.identity((d0 - 1) * n1) + self.E2_2_pol[(2 + (n0 - 2) * d1) :, 1 * n1 :] = xp.identity((d0 - 1) * n1) self.E2_2_pol = self.E2_2_pol.tocsr() # 3rd component - self.E2_3_pol[:, 1 * d1 :] = np.identity((d0 - 1) * d1) + self.E2_3_pol[:, 1 * d1 :] = xp.identity((d0 - 1) * d1) self.E2_3_pol = self.E2_3_pol.tocsr() # combined first and second component diff --git a/src/struphy/polar/linear_operators.py b/src/struphy/polar/linear_operators.py index 019b7aae0..03a3e504a 100644 --- a/src/struphy/polar/linear_operators.py +++ b/src/struphy/polar/linear_operators.py @@ -1,3 +1,4 @@ +import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from psydac.linalg.block import BlockVector, BlockVectorSpace from psydac.linalg.stencil import StencilVector, StencilVectorSpace @@ -6,7 +7,6 @@ from struphy.feec.linear_operators import LinOpWithTransp from struphy.linear_algebra.linalg_kron import kron_matvec_2d from struphy.polar.basic import PolarDerhamSpace, PolarVector -from struphy.utils.arrays import xp as np class PolarExtractionOperator(LinOpWithTransp): @@ -668,7 +668,7 @@ def dot_inner_tp_rings(blocks_e1_e2, blocks_e3, v, out): # loop over codomain components for m, (row_e1_e2, row_e3) in enumerate(zip(blocks_e1_e2, blocks_e3)): - res = np.zeros((n_rows[m], n3_out[m]), dtype=float) + res = xp.zeros((n_rows[m], n3_out[m]), dtype=float) # loop over domain components for n, (block_e1_e2, block_e3) in enumerate(zip(row_e1_e2, row_e3)): @@ -677,7 +677,7 @@ def dot_inner_tp_rings(blocks_e1_e2, blocks_e3, v, out): e1, e2, e3 = in_ends[n] if block_e1_e2 is not None: - tmp = np.zeros((n_rings_in[n], n2, n3_in[n]), dtype=float) + tmp = xp.zeros((n_rings_in[n], n2, n3_in[n]), dtype=float) tmp[:, s2 : e2 + 1, s3 : e3 + 1] = in_vec[n][0 : n_rings_in[n], s2 : e2 + 1, s3 : e3 + 1] res += kron_matvec_2d([block_e1_e2, block_e3], tmp.reshape(n_rings_in[n] * n2, n3_in[n])) @@ -785,7 +785,7 @@ def dot_parts_of_polar(blocks_e1_e2, blocks_e3, v, out): # loop over codomain components for m, (row_e1_e2, row_e3) in enumerate(zip(blocks_e1_e2, blocks_e3)): - res = np.zeros((n_rings_out[m], n2, n3_out[m]), dtype=float) + res = xp.zeros((n_rings_out[m], n2, n3_out[m]), dtype=float) # loop over domain components for n, (block_e1_e2, block_e3) in enumerate(zip(row_e1_e2, row_e3)): @@ -794,7 +794,7 @@ def dot_parts_of_polar(blocks_e1_e2, blocks_e3, v, out): if in_starts[n][0] == 0: s1, s2, s3 = in_starts[n] e1, e2, e3 = in_ends[n] - tmp = np.zeros((n2, n3_in[n]), dtype=float) + tmp = xp.zeros((n2, n3_in[n]), dtype=float) tmp[s2 : e2 + 1, s3 : e3 + 1] = in_tp[n][n_rings_in[n], s2 : e2 + 1, s3 : e3 + 1] res += kron_matvec_2d([block_e1_e2, block_e3], tmp).reshape(n_rings_out[m], n2, n3_out[m]) else: diff --git a/src/struphy/polar/tests/test_legacy_polar_splines.py b/src/struphy/polar/tests/test_legacy_polar_splines.py index b9665ae0b..be2bfb654 100644 --- a/src/struphy/polar/tests/test_legacy_polar_splines.py +++ b/src/struphy/polar/tests/test_legacy_polar_splines.py @@ -7,12 +7,12 @@ def test_polar_splines_2D(plot=False): sys.path.append("..") + import cunumpy as xp import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D from struphy.eigenvalue_solvers.spline_space import Spline_space_1d, Tensor_spline_space from struphy.geometry import domains - from struphy.utils.arrays import xp as np # parameters # number of elements (number of elements in angular direction must be a multiple of 3) @@ -42,8 +42,8 @@ def test_polar_splines_2D(plot=False): fig.set_figheight(10) fig.set_figwidth(10) - el_b_1 = np.linspace(0.0, 1.0, Nel[0] + 1) - el_b_2 = np.linspace(0.0, 1.0, Nel[1] + 1) + el_b_1 = xp.linspace(0.0, 1.0, Nel[0] + 1) + el_b_2 = xp.linspace(0.0, 1.0, Nel[1] + 1) grid_x = domain(el_b_1, el_b_2, 0.0, squeeze_out=True)[0] grid_y = domain(el_b_1, el_b_2, 0.0, squeeze_out=True)[1] @@ -108,7 +108,7 @@ def test_polar_splines_2D(plot=False): ) # plot three new polar splines in V0 - etaplot = [np.linspace(0.0, 1.0, 200), np.linspace(0.0, 1.0, 200)] + etaplot = [xp.linspace(0.0, 1.0, 200), xp.linspace(0.0, 1.0, 200)] xplot = [ domain(etaplot[0], etaplot[1], 0.0, squeeze_out=True)[0], domain(etaplot[0], etaplot[1], 0.0, squeeze_out=True)[1], @@ -123,9 +123,9 @@ def test_polar_splines_2D(plot=False): ax3 = fig.add_subplot(133, projection="3d") # coeffs in polar basis - c0_pol1 = np.zeros(space_2d.E0.shape[0], dtype=float) - c0_pol2 = np.zeros(space_2d.E0.shape[0], dtype=float) - c0_pol3 = np.zeros(space_2d.E0.shape[0], dtype=float) + c0_pol1 = xp.zeros(space_2d.E0.shape[0], dtype=float) + c0_pol2 = xp.zeros(space_2d.E0.shape[0], dtype=float) + c0_pol3 = xp.zeros(space_2d.E0.shape[0], dtype=float) c0_pol1[0] = 1.0 c0_pol2[1] = 1.0 @@ -134,7 +134,7 @@ def test_polar_splines_2D(plot=False): ax1.plot_surface( xplot[0], xplot[1], - space_2d.evaluate_NN(etaplot[0], etaplot[1], np.array([0.0]), c0_pol1, "V0")[:, :, 0], + space_2d.evaluate_NN(etaplot[0], etaplot[1], xp.array([0.0]), c0_pol1, "V0")[:, :, 0], cmap="jet", ) ax1.set_xlabel("R [m]", labelpad=5) @@ -144,7 +144,7 @@ def test_polar_splines_2D(plot=False): ax2.plot_surface( xplot[0], xplot[1], - space_2d.evaluate_NN(etaplot[0], etaplot[1], np.array([0.0]), c0_pol2, "V0")[:, :, 0], + space_2d.evaluate_NN(etaplot[0], etaplot[1], xp.array([0.0]), c0_pol2, "V0")[:, :, 0], cmap="jet", ) ax2.set_xlabel("R [m]", labelpad=5) @@ -154,7 +154,7 @@ def test_polar_splines_2D(plot=False): ax3.plot_surface( xplot[0], xplot[1], - space_2d.evaluate_NN(etaplot[0], etaplot[1], np.array([0.0]), c0_pol3, "V0")[:, :, 0], + space_2d.evaluate_NN(etaplot[0], etaplot[1], xp.array([0.0]), c0_pol3, "V0")[:, :, 0], cmap="jet", ) ax3.set_xlabel("R [m]", labelpad=5) diff --git a/src/struphy/polar/tests/test_polar.py b/src/struphy/polar/tests/test_polar.py index b2b5c8326..ac0113c4f 100644 --- a/src/struphy/polar/tests/test_polar.py +++ b/src/struphy/polar/tests/test_polar.py @@ -167,6 +167,7 @@ def test_spaces(Nel, p, spl_kind): @pytest.mark.parametrize("p", [[3, 2, 2]]) @pytest.mark.parametrize("spl_kind", [[False, True, True], [False, True, False]]) def test_extraction_ops_and_derivatives(Nel, p, spl_kind): + import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from struphy.eigenvalue_solvers.spline_space import Spline_space_1d, Tensor_spline_space @@ -176,7 +177,6 @@ def test_extraction_ops_and_derivatives(Nel, p, spl_kind): from struphy.polar.basic import PolarDerhamSpace, PolarVector from struphy.polar.extraction_operators import PolarExtractionBlocksC1 from struphy.polar.linear_operators import PolarExtractionOperator, PolarLinearOperator - from struphy.utils.arrays import xp as np comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -222,11 +222,11 @@ def test_extraction_ops_and_derivatives(Nel, p, spl_kind): b2_pol.tp = b2_tp p3_pol.tp = p3_tp - np.random.seed(1607) - f0_pol.pol = [np.random.rand(f0_pol.pol[0].shape[0], f0_pol.pol[0].shape[1])] - e1_pol.pol = [np.random.rand(e1_pol.pol[n].shape[0], e1_pol.pol[n].shape[1]) for n in range(3)] - b2_pol.pol = [np.random.rand(b2_pol.pol[n].shape[0], b2_pol.pol[n].shape[1]) for n in range(3)] - p3_pol.pol = [np.random.rand(p3_pol.pol[0].shape[0], p3_pol.pol[0].shape[1])] + xp.random.seed(1607) + f0_pol.pol = [xp.random.rand(f0_pol.pol[0].shape[0], f0_pol.pol[0].shape[1])] + e1_pol.pol = [xp.random.rand(e1_pol.pol[n].shape[0], e1_pol.pol[n].shape[1]) for n in range(3)] + b2_pol.pol = [xp.random.rand(b2_pol.pol[n].shape[0], b2_pol.pol[n].shape[1]) for n in range(3)] + p3_pol.pol = [xp.random.rand(p3_pol.pol[0].shape[0], p3_pol.pol[0].shape[1])] f0_pol_leg = f0_pol.toarray(True) e1_pol_leg = e1_pol.toarray(True) @@ -243,10 +243,10 @@ def test_extraction_ops_and_derivatives(Nel, p, spl_kind): r2_pol = derham.extraction_ops["2"].dot(b2_tp) r3_pol = derham.extraction_ops["3"].dot(p3_tp) - assert np.allclose(r0_pol.toarray(True), space.E0.dot(f0_tp_leg)) - assert np.allclose(r1_pol.toarray(True), space.E1.dot(e1_tp_leg)) - assert np.allclose(r2_pol.toarray(True), space.E2.dot(b2_tp_leg)) - assert np.allclose(r3_pol.toarray(True), space.E3.dot(p3_tp_leg)) + assert xp.allclose(r0_pol.toarray(True), space.E0.dot(f0_tp_leg)) + assert xp.allclose(r1_pol.toarray(True), space.E1.dot(e1_tp_leg)) + assert xp.allclose(r2_pol.toarray(True), space.E2.dot(b2_tp_leg)) + assert xp.allclose(r3_pol.toarray(True), space.E3.dot(p3_tp_leg)) # test transposed extraction operators E0T = derham.extraction_ops["0"].transpose() @@ -277,9 +277,9 @@ def test_extraction_ops_and_derivatives(Nel, p, spl_kind): r2_pol = derham.curl.dot(e1_pol) r3_pol = derham.div.dot(b2_pol) - assert np.allclose(r1_pol.toarray(True), space.G.dot(f0_pol_leg)) - assert np.allclose(r2_pol.toarray(True), space.C.dot(e1_pol_leg)) - assert np.allclose(r3_pol.toarray(True), space.D.dot(b2_pol_leg)) + assert xp.allclose(r1_pol.toarray(True), space.G.dot(f0_pol_leg)) + assert xp.allclose(r2_pol.toarray(True), space.C.dot(e1_pol_leg)) + assert xp.allclose(r3_pol.toarray(True), space.D.dot(b2_pol_leg)) # test transposed derivatives GT = derham.grad.transpose() @@ -290,9 +290,9 @@ def test_extraction_ops_and_derivatives(Nel, p, spl_kind): r1_pol = CT.dot(b2_pol) r2_pol = DT.dot(p3_pol) - assert np.allclose(r0_pol.toarray(True), space.G.T.dot(e1_pol_leg)) - assert np.allclose(r1_pol.toarray(True), space.C.T.dot(b2_pol_leg)) - assert np.allclose(r2_pol.toarray(True), space.D.T.dot(p3_pol_leg)) + assert xp.allclose(r0_pol.toarray(True), space.G.T.dot(e1_pol_leg)) + assert xp.allclose(r1_pol.toarray(True), space.C.T.dot(b2_pol_leg)) + assert xp.allclose(r2_pol.toarray(True), space.D.T.dot(p3_pol_leg)) if rank == 0: print("------------- Test passed ---------------------------") @@ -302,12 +302,12 @@ def test_extraction_ops_and_derivatives(Nel, p, spl_kind): @pytest.mark.parametrize("p", [[4, 3, 2]]) @pytest.mark.parametrize("spl_kind", [[False, True, True], [False, True, False]]) def test_projectors(Nel, p, spl_kind): + import cunumpy as xp from psydac.ddm.mpi import mpi as MPI from struphy.eigenvalue_solvers.spline_space import Spline_space_1d, Tensor_spline_space from struphy.feec.psydac_derham import Derham from struphy.geometry.domains import IGAPolarCylinder - from struphy.utils.arrays import xp as np comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -338,7 +338,7 @@ def test_projectors(Nel, p, spl_kind): # function to project on physical domain def fun_scalar(x, y, z): - return np.sin(2 * np.pi * (x)) * np.cos(2 * np.pi * y) * np.sin(2 * np.pi * z) + return xp.sin(2 * xp.pi * (x)) * xp.cos(2 * xp.pi * y) * xp.sin(2 * xp.pi * z) fun_vector = [fun_scalar, fun_scalar, fun_scalar] @@ -369,7 +369,7 @@ def fun3(e1, e2, e3): r0_pol_leg = space.projectors.pi_0(fun0) - assert np.allclose(r0_pol.toarray(True), r0_pol_leg) + assert xp.allclose(r0_pol.toarray(True), r0_pol_leg) if rank == 0: print("Test passed for PI_0 polar projector") @@ -385,7 +385,7 @@ def fun3(e1, e2, e3): r1_pol_leg = space.projectors.pi_1(fun1, with_subs=False) - assert np.allclose(r1_pol.toarray(True), r1_pol_leg) + assert xp.allclose(r1_pol.toarray(True), r1_pol_leg) if rank == 0: print("Test passed for PI_1 polar projector") @@ -401,7 +401,7 @@ def fun3(e1, e2, e3): r2_pol_leg = space.projectors.pi_2(fun2, with_subs=False) - assert np.allclose(r2_pol.toarray(True), r2_pol_leg) + assert xp.allclose(r2_pol.toarray(True), r2_pol_leg) if rank == 0: print("Test passed for PI_2 polar projector") @@ -417,7 +417,7 @@ def fun3(e1, e2, e3): r3_pol_leg = space.projectors.pi_3(fun3, with_subs=False) - assert np.allclose(r3_pol.toarray(True), r3_pol_leg) + assert xp.allclose(r3_pol.toarray(True), r3_pol_leg) if rank == 0: print("Test passed for PI_3 polar projector") diff --git a/src/struphy/post_processing/likwid/plot_likwidproject.py b/src/struphy/post_processing/likwid/plot_likwidproject.py index 33b0426af..feda5d3b6 100644 --- a/src/struphy/post_processing/likwid/plot_likwidproject.py +++ b/src/struphy/post_processing/likwid/plot_likwidproject.py @@ -7,6 +7,7 @@ import re import sys +import cunumpy as xp import matplotlib.pyplot as plt import pandas as pd import plotly.express as px @@ -16,7 +17,6 @@ import struphy.post_processing.likwid.likwid_parser as lp import struphy.post_processing.likwid.maxplotlylib as mply import struphy.post_processing.likwid.roofline_plotter as rp -from struphy.utils.arrays import xp as np def clean_string(string_in): @@ -196,16 +196,16 @@ def plot_roofline( fig.update_xaxes( type="log", # Ensure the x-axis is logarithmic - range=[np.log10(xmin), np.log10(xmax)], + range=[xp.log10(xmin), xp.log10(xmax)], title="Operational intensity (FLOP/Byte)", tickvals=xtick_values, # Set where ticks appear ticktext=[str(t) for t in xtick_values], - # ticktext=[f'$10^{{{int(np.log10(t))}}}$' for t in xtick_values] # Set tick labels + # ticktext=[f'$10^{{{int(xp.log10(t))}}}$' for t in xtick_values] # Set tick labels ) fig.update_yaxes( type="log", # Ensure the x-axis is logarithmic - range=[np.log10(ymin), np.log10(ymax)], + range=[xp.log10(ymin), xp.log10(ymax)], title="Performance [GFLOP/s]", tickvals=ytick_values, # Set where ticks appear ticktext=[str(t) for t in ytick_values], diff --git a/src/struphy/post_processing/likwid/plot_time_traces.py b/src/struphy/post_processing/likwid/plot_time_traces.py index 4f3f4eeb8..ed0a34010 100644 --- a/src/struphy/post_processing/likwid/plot_time_traces.py +++ b/src/struphy/post_processing/likwid/plot_time_traces.py @@ -2,12 +2,12 @@ import pickle import re +import cunumpy as xp import matplotlib.pyplot as plt import plotly.io as pio # pio.kaleido.scope.mathjax = None import struphy.post_processing.likwid.maxplotlylib as mply -from struphy.utils.arrays import xp as np def glob_to_regex(pat: str) -> str: @@ -121,9 +121,9 @@ def plot_avg_duration_bar_chart( # Compute statistics per region regions = sorted(region_durations.keys()) - avg_durations = [np.mean(region_durations[r]) for r in regions] - min_durations = [np.min(region_durations[r]) for r in regions] - max_durations = [np.max(region_durations[r]) for r in regions] + avg_durations = [xp.mean(region_durations[r]) for r in regions] + min_durations = [xp.min(region_durations[r]) for r in regions] + max_durations = [xp.max(region_durations[r]) for r in regions] yerr = [ [avg - min_ for avg, min_ in zip(avg_durations, min_durations)], [max_ - avg for avg, max_ in zip(avg_durations, max_durations)], @@ -131,7 +131,7 @@ def plot_avg_duration_bar_chart( # Plot bar chart with error bars (min-max spans) plt.figure(figsize=(12, 6)) - x = np.arange(len(regions)) + x = xp.arange(len(regions)) plt.bar(x, avg_durations, yerr=yerr, capsize=5, color="skyblue", edgecolor="k") plt.yscale("log") plt.xticks(x, regions, rotation=45, ha="right") @@ -175,7 +175,7 @@ def plot_gantt_chart_plotly( region_start_times = {} for rank_data in profiling_data["rank_data"].values(): for region_name, info in rank_data.items(): - first_start_time = np.min(info["start_times"]) + first_start_time = xp.min(info["start_times"]) if region_name not in region_start_times or first_start_time < region_start_times[region_name]: region_start_times[region_name] = first_start_time @@ -291,7 +291,7 @@ def plot_gantt_chart( region_start_times = {} for rank_data in profiling_data["rank_data"].values(): for region_name, info in rank_data.items(): - first_start_time = np.min(info["start_times"]) + first_start_time = xp.min(info["start_times"]) if region_name not in region_start_times or first_start_time < region_start_times[region_name]: region_start_times[region_name] = first_start_time diff --git a/src/struphy/post_processing/likwid/roofline_plotter.py b/src/struphy/post_processing/likwid/roofline_plotter.py index 6a49f9c34..3a4808bdc 100644 --- a/src/struphy/post_processing/likwid/roofline_plotter.py +++ b/src/struphy/post_processing/likwid/roofline_plotter.py @@ -1,11 +1,10 @@ import glob import pickle +import cunumpy as xp import pandas as pd import yaml -from struphy.utils.arrays import xp as np - def sort_by_num_threads(bm): sorted_arrays = {} @@ -143,14 +142,14 @@ def add_plot_diagonal( bandwidth_GBps, label="", ymax=1e4, - operational_intensity_FLOPpMB=np.arange(0, 1000, 1), + operational_intensity_FLOPpMB=xp.arange(0, 1000, 1), ): max_performance_GFLOP = operational_intensity_FLOPpMB * bandwidth_GBps (line,) = mfig.axs.plot(operational_intensity_FLOPpMB, max_performance_GFLOP) # Specify the y-value where you want to place the text specific_y = ymax # Interpolate to find the corresponding x-value - specific_x = np.interp( + specific_x = xp.interp( specific_y, max_performance_GFLOP, operational_intensity_FLOPpMB, @@ -210,10 +209,10 @@ def get_average_val( xvec.append(x) yvec.append(y) # print('xvec', xvec, 'yvec', yvec) - xvec = np.array(xvec) - yvec = np.array(yvec) + xvec = xp.array(xvec) + yvec = xp.array(yvec) # print('xvec', xvec, 'yvec', yvec) - return np.average(xvec), np.average(yvec), np.std(xvec), np.std(yvec) + return xp.average(xvec), xp.average(yvec), xp.std(xvec), xp.std(yvec) def get_maximum(path, df_index=-1, metric="DP [MFLOP/s] STAT", column_name="Sum"): diff --git a/src/struphy/post_processing/orbits/orbits_tools.py b/src/struphy/post_processing/orbits/orbits_tools.py index eb72ebdfb..97eee89af 100644 --- a/src/struphy/post_processing/orbits/orbits_tools.py +++ b/src/struphy/post_processing/orbits/orbits_tools.py @@ -1,13 +1,12 @@ import os import shutil +import cunumpy as xp import h5py import yaml from tqdm import tqdm -from struphy.io.setup import setup_domain_and_equil from struphy.post_processing.orbits.orbits_kernels import calculate_guiding_center_from_6d -from struphy.utils.arrays import xp as np def post_process_orbit_guiding_center(path_in, path_kinetics_species, species): @@ -62,7 +61,7 @@ def post_process_orbit_guiding_center(path_in, path_kinetics_species, species): if file.endswith(".npy") ] pproc_nt = len(npy_files_list) - n_markers = np.load(os.path.join(path_orbits, npy_files_list[0])).shape[0] + n_markers = xp.load(os.path.join(path_orbits, npy_files_list[0])).shape[0] # re-ordering npy_files npy_files_list = sorted(npy_files_list) @@ -77,10 +76,10 @@ def post_process_orbit_guiding_center(path_in, path_kinetics_species, species): os.mkdir(path_gc) # temporary marker array - temp = np.empty((n_markers, 7), dtype=float) - etas = np.empty((n_markers, 3), dtype=float) - B_cart = np.empty((n_markers, 3), dtype=float) - lost_particles_mask = np.empty(n_markers, dtype=bool) + temp = xp.empty((n_markers, 7), dtype=float) + etas = xp.empty((n_markers, 3), dtype=float) + B_cart = xp.empty((n_markers, 3), dtype=float) + lost_particles_mask = xp.empty(n_markers, dtype=bool) print("Evaluation of guiding center for " + str(species)) @@ -95,13 +94,13 @@ def post_process_orbit_guiding_center(path_in, path_kinetics_species, species): file_txt = os.path.join(path_gc, npy_files_list[n][:-4] + ".txt") # call .npy file - temp[:, :] = np.load(os.path.join(path_orbits, npy_files_list[n])) + temp[:, :] = xp.load(os.path.join(path_orbits, npy_files_list[n])) # move ids to last column and save - temp = np.roll(temp, -1, axis=1) + temp = xp.roll(temp, -1, axis=1) # sorting out lost particles - lost_particles_mask = np.all(temp[:, :-1] == 0, axis=1) + lost_particles_mask = xp.all(temp[:, :-1] == 0, axis=1) # domain inverse map etas[~lost_particles_mask, :] = domain.inverse_map( @@ -111,7 +110,7 @@ def post_process_orbit_guiding_center(path_in, path_kinetics_species, species): # eval cartesian magnetic filed at marker positions B_cart[~lost_particles_mask, :] = equil.b_cart( - *np.concatenate( + *xp.concatenate( ( etas[:, 0][:, None], etas[:, 1][:, None], @@ -124,10 +123,10 @@ def post_process_orbit_guiding_center(path_in, path_kinetics_species, species): calculate_guiding_center_from_6d(temp, B_cart) # move ids to first column and save - temp = np.roll(temp, 1, axis=1) + temp = xp.roll(temp, 1, axis=1) - np.save(file_npy, temp) - np.savetxt(file_txt, temp[:, :4], fmt="%12.6f", delimiter=", ") + xp.save(file_npy, temp) + xp.savetxt(file_txt, temp[:, :4], fmt="%12.6f", delimiter=", ") def post_process_orbit_classification(path_kinetics_species, species): @@ -169,16 +168,16 @@ def post_process_orbit_classification(path_kinetics_species, species): if file.endswith(".npy") ] pproc_nt = len(npy_files_list) - n_markers = np.load(os.path.join(path_gc, npy_files_list[0])).shape[0] + n_markers = xp.load(os.path.join(path_gc, npy_files_list[0])).shape[0] # re-ordering npy_files npy_files_list = sorted(npy_files_list) # temporary marker array - temp = np.empty((n_markers, 8), dtype=float) - v_parallel = np.empty(n_markers, dtype=float) - trapped_particle_mask = np.empty(n_markers, dtype=bool) - lost_particle_mask = np.empty(n_markers, dtype=bool) + temp = xp.empty((n_markers, 8), dtype=float) + v_parallel = xp.empty(n_markers, dtype=float) + trapped_particle_mask = xp.empty(n_markers, dtype=bool) + lost_particle_mask = xp.empty(n_markers, dtype=bool) print("Classifying guiding center orbits for " + str(species)) @@ -189,16 +188,16 @@ def post_process_orbit_classification(path_kinetics_species, species): # load .npy files file_npy = os.path.join(path_gc, npy_files_list[n]) - temp[:, :-1] = np.load(file_npy) + temp[:, :-1] = xp.load(file_npy) # initial time step if n == 0: v_init = temp[:, 4] - np.save(file_npy, temp) + xp.save(file_npy, temp) continue # synchronizing with former time step - temp[:, -1] = np.load( + temp[:, -1] = xp.load( os.path.join( path_gc, npy_files_list[n - 1], @@ -206,10 +205,10 @@ def post_process_orbit_classification(path_kinetics_species, species): )[:, -1] # call parallel velocity data from .npy file - v_parallel = np.load(os.path.join(path_gc, npy_files_list[n]))[:, 4] + v_parallel = xp.load(os.path.join(path_gc, npy_files_list[n]))[:, 4] # sorting out lost particles - lost_particle_mask = np.all(temp[:, 1:-1] == 0, axis=1) + lost_particle_mask = xp.all(temp[:, 1:-1] == 0, axis=1) # check reverse of parallel velocity trapped_particle_mask[:] = False @@ -222,4 +221,4 @@ def post_process_orbit_classification(path_kinetics_species, species): # assign "-1" at the last index of lost particles temp[lost_particle_mask, -1] = -1 - np.save(file_npy, temp) + xp.save(file_npy, temp) diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 28919c6d3..74a6288f6 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -1,20 +1,122 @@ import os +import pickle import shutil +import cunumpy as xp import h5py -import matplotlib.pyplot as plt import yaml from tqdm import tqdm -from struphy.feec.psydac_derham import Derham -from struphy.io.setup import setup_domain_and_equil +from struphy.feec.psydac_derham import SplineFunction +from struphy.fields_background import equils +from struphy.fields_background.base import FluidEquilibrium +from struphy.geometry import domains +from struphy.geometry.base import Domain +from struphy.io.options import BaseUnits, EnvironmentOptions, Time +from struphy.io.setup import import_parameters_py from struphy.kinetic_background import maxwellians -from struphy.models import fluid, hybrid, kinetic, toy -from struphy.utils.arrays import xp as np +from struphy.kinetic_background.base import KineticBackground +from struphy.models.base import StruphyModel, setup_derham +from struphy.models.species import ParticleSpecies +from struphy.models.variables import PICVariable +from struphy.topology.grids import TensorProductGrid + + +class ParamsIn: + """Holds the input parameters of a Struphy simulation as attributes.""" + + def __init__( + self, + env: EnvironmentOptions = None, + base_units: BaseUnits = None, + time_opts: Time = None, + domain=None, + equil=None, + grid: TensorProductGrid = None, + derham_opts=None, + model: StruphyModel = None, + ): + self.env = env + self.units = base_units + self.time_opts = time_opts + self.domain = domain + self.equil = equil + self.grid = grid + self.derham_opts = derham_opts + self.model = model + + +def get_params_of_run(path: str) -> ParamsIn: + """Retrieve parameters of finished Struphy run. + + Parameters + ---------- + path : str + Absolute path of simulation output folder. + """ + + print(f"\nReading in paramters from {path} ... ") + + params_path = os.path.join(path, "parameters.py") + bin_path = os.path.join(path, "env.bin") + + if os.path.exists(params_path): + params_in = import_parameters_py(params_path) + env = params_in.env + base_units = params_in.base_units + time_opts = params_in.time_opts + domain = params_in.domain + equil = params_in.equil + grid = params_in.grid + derham_opts = params_in.derham_opts + model = params_in.model + + elif os.path.exists(bin_path): + with open(os.path.join(path, "env.bin"), "rb") as f: + env = pickle.load(f) + with open(os.path.join(path, "base_units.bin"), "rb") as f: + base_units = pickle.load(f) + with open(os.path.join(path, "time_opts.bin"), "rb") as f: + time_opts = pickle.load(f) + with open(os.path.join(path, "domain.bin"), "rb") as f: + # WORKAROUND: cannot pickle pyccelized classes at the moment + domain_dct = pickle.load(f) + domain: Domain = getattr(domains, domain_dct["name"])(**domain_dct["params"]) + with open(os.path.join(path, "equil.bin"), "rb") as f: + # WORKAROUND: cannot pickle pyccelized classes at the moment + equil_dct = pickle.load(f) + if equil_dct: + equil: FluidEquilibrium = getattr(equils, equil_dct["name"])(**equil_dct["params"]) + else: + equil = None + with open(os.path.join(path, "grid.bin"), "rb") as f: + grid = pickle.load(f) + with open(os.path.join(path, "derham_opts.bin"), "rb") as f: + derham_opts = pickle.load(f) + with open(os.path.join(path, "model_class.bin"), "rb") as f: + model_class: StruphyModel = pickle.load(f) + model = model_class() + + else: + raise FileNotFoundError(f"Neither of the paths {params_path} or {bin_path} exists.") + + print("done.") + + return ParamsIn( + env=env, + base_units=base_units, + time_opts=time_opts, + domain=domain, + equil=equil, + grid=grid, + derham_opts=derham_opts, + model=model, + ) def create_femfields( path: str, + params_in: ParamsIn, *, step: int = 1, ): @@ -25,6 +127,9 @@ def create_femfields( path : str Absolute path of simulation output folder. + params_in : ParamsIn + Simulation parameters. + step : int Whether to create FEM fields at every time step (step=1, default), every second time step (step=2), etc. @@ -33,50 +138,50 @@ def create_femfields( fields : dict Nested dictionary holding :class:`~struphy.feec.psydac_derham.SplineFunction`: fields[t][name] contains the Field with the name "name" in the hdf5 file at time t. - space_ids : dict - The space IDs of the fields (H1, Hcurl, Hdiv, L2 or H1vec). space_ids[name] contains the space ID of the field with the name "name". - - model : str - From which model in struphy/models the data has been obtained. + t_grid : xp.ndarray + Time grid. """ - # get model name and # of MPI processes from meta.txt file - with open(os.path.join(path, "meta.txt"), "r") as f: - lines = f.readlines() - - model = lines[3].split()[-1] - nproc = lines[4].split()[-1] + with open(os.path.join(path, "meta.yml"), "r") as f: + meta = yaml.load(f, Loader=yaml.FullLoader) + nproc = meta["MPI processes"] - # create Derham sequence from grid parameters - with open(os.path.join(path, "parameters.yml"), "r") as f: - params = yaml.load(f, Loader=yaml.FullLoader) - - derham = Derham( - params["grid"]["Nel"], - params["grid"]["p"], - params["grid"]["spl_kind"], + derham = setup_derham( + params_in.grid, + params_in.derham_opts, + comm=None, + domain=params_in.domain, ) # get fields names, space IDs and time grid from 0-th rank hdf5 file file = h5py.File(os.path.join(path, "data/", "data_proc0.hdf5"), "r") - space_ids = {} - - for field_name, dset in file["feec"].items(): - space_ids[field_name] = dset.attrs["space_id"] + print(f"\nReading hdf5 data of following species:") + for species, dset in file["feec"].items(): + space_ids[species] = {} + print(f"{species}:") + for var, ddset in dset.items(): + space_ids[species][var] = ddset.attrs["space_id"] + print(f" {var}:", ddset) t_grid = file["time/value"][::step].copy() - file.close() # create one FemField for each snapshot fields = {} for t in t_grid: fields[t] = {} - for field_name, ID in space_ids.items(): - fields[t][field_name] = derham.create_spline_function(field_name, ID) + for species, vars in space_ids.items(): + fields[t][species] = {} + for var, id in vars.items(): + fields[t][species][var] = derham.create_spline_function( + var, + id, + verbose=False, + ) # get hdf5 data + print("") for rank in range(int(nproc)): # open hdf5 file file = h5py.File( @@ -88,67 +193,66 @@ def create_femfields( "r", ) - for field_name, dset in tqdm(file["feec"].items()): - # get global start indices, end indices and pads - gl_s = dset.attrs["starts"] - gl_e = dset.attrs["ends"] - pads = dset.attrs["pads"] - - assert gl_s.shape == (3,) or gl_s.shape == (3, 3) - assert gl_e.shape == (3,) or gl_e.shape == (3, 3) - assert pads.shape == (3,) or pads.shape == (3, 3) - - # loop over time - for n, t in enumerate(t_grid): - # scalar field - if gl_s.shape == (3,): - s1, s2, s3 = gl_s - e1, e2, e3 = gl_e - p1, p2, p3 = pads - - data = dset[n * step, p1:-p1, p2:-p2, p3:-p3].copy() - - fields[t][field_name].vector[ - s1 : e1 + 1, - s2 : e2 + 1, - s3 : e3 + 1, - ] = data - # update after each data addition, can be made more efficient - fields[t][field_name].vector.update_ghost_regions() - - # vector-valued field - else: - for comp in range(3): - s1, s2, s3 = gl_s[comp] - e1, e2, e3 = gl_e[comp] - p1, p2, p3 = pads[comp] - - data = dset[str(comp + 1)][ - n * step, - p1:-p1, - p2:-p2, - p3:-p3, - ].copy() - - fields[t][field_name].vector[comp][ + for species, dset in file["feec"].items(): + for var, ddset in tqdm(dset.items()): + # get global start indices, end indices and pads + gl_s = ddset.attrs["starts"] + gl_e = ddset.attrs["ends"] + pads = ddset.attrs["pads"] + + assert gl_s.shape == (3,) or gl_s.shape == (3, 3) + assert gl_e.shape == (3,) or gl_e.shape == (3, 3) + assert pads.shape == (3,) or pads.shape == (3, 3) + + # loop over time + for n, t in enumerate(t_grid): + # scalar field + if gl_s.shape == (3,): + s1, s2, s3 = gl_s + e1, e2, e3 = gl_e + p1, p2, p3 = pads + + data = ddset[n * step, p1:-p1, p2:-p2, p3:-p3].copy() + + fields[t][species][var].vector[ s1 : e1 + 1, s2 : e2 + 1, s3 : e3 + 1, ] = data - # update after each data addition, can be made more efficient - fields[t][field_name].vector.update_ghost_regions() + # update after each data addition, can be made more efficient + fields[t][species][var].vector.update_ghost_regions() + # vector-valued field + else: + for comp in range(3): + s1, s2, s3 = gl_s[comp] + e1, e2, e3 = gl_e[comp] + p1, p2, p3 = pads[comp] + + data = ddset[str(comp + 1)][ + n * step, + p1:-p1, + p2:-p2, + p3:-p3, + ].copy() + + fields[t][species][var].vector[comp][ + s1 : e1 + 1, + s2 : e2 + 1, + s3 : e3 + 1, + ] = data + # update after each data addition, can be made more efficient + fields[t][species][var].vector.update_ghost_regions() file.close() print("Creation of Struphy Fields done.") - return fields, space_ids, model + return fields, t_grid def eval_femfields( - path: str, + params_in: ParamsIn, fields: dict, - space_ids: dict, *, celldivide: list = [1, 1, 1], physical: bool = False, @@ -157,15 +261,12 @@ def eval_femfields( Parameters ---------- - path : str - Absolute path of simulation output folder. + params_in : ParamsIn + Simulation parameters. fields : dict Obtained from struphy.diagnostics.post_processing.create_femfields. - space_ids : dict - Obtained from struphy.diagnostics.post_processing.create_femfields. - celldivide : list of ints Grid refinement in each eta direction. @@ -175,7 +276,7 @@ def eval_femfields( Returns ------- point_data : dict - Nested dictionary holding values of FemFields on the grid as list of 3d np.arrays: + Nested dictionary holding values of FemFields on the grid as list of 3d xp.arrays: point_data[name][t] contains the values of the field with name "name" in fields[t].keys() at time t. If physical is True, physical components of fields are saved. @@ -188,22 +289,17 @@ def eval_femfields( Mapped (physical) grids obtained by domain(*grids_log). """ - assert isinstance(fields, dict) - assert isinstance(space_ids, dict) - - # domain object according to parameter file and grids - with open(os.path.join(path, "parameters.yml"), "r") as f: - params = yaml.load(f, Loader=yaml.FullLoader) - - domain = setup_domain_and_equil(params)[0] + # get domain + domain = params_in.domain # create logical and physical grids + assert isinstance(fields, dict) assert isinstance(celldivide, list) assert len(celldivide) == 3 - Nel = params["grid"]["Nel"] + Nel = params_in.grid.Nel - grids_log = [np.linspace(0.0, 1.0, Nel_i * n_i + 1) for Nel_i, n_i in zip(Nel, celldivide)] + grids_log = [xp.linspace(0.0, 1.0, Nel_i * n_i + 1) for Nel_i, n_i in zip(Nel, celldivide)] grids_phy = [ domain(*grids_log)[0], domain(*grids_log)[1], @@ -212,86 +308,86 @@ def eval_femfields( # evaluate fields at evaluation grid and push-forward point_data = {} + for species, vars in fields[list(fields.keys())[0]].items(): + point_data[species] = {} + for name, field in vars.items(): + point_data[species][name] = {} - # one dict for each field - for name in space_ids: - point_data[name] = {} - - # time loop - print("Evaluating fields ...") + print("\nEvaluating fields ...") for t in tqdm(fields): - # field loop - for name, field in fields[t].items(): - # space ID - space_id = space_ids[name] - - # field evaluation - temp_val = field(*grids_log) - - point_data[name][t] = [] - - # scalar spaces - if isinstance(temp_val, np.ndarray): - if physical: - # push-forward - if space_id == "H1": - point_data[name][t].append( - domain.push( - temp_val, - *grids_log, - kind="0", - ), - ) - elif space_id == "L2": - point_data[name][t].append( - domain.push( - temp_val, - *grids_log, - kind="3", - ), - ) + for species, vars in fields[t].items(): + for name, field in vars.items(): + assert isinstance(field, SplineFunction) + space_id = field.space_id - else: - point_data[name][t].append(temp_val) + # field evaluation + temp_val = field(*grids_log) - # vector-valued spaces - else: - for j in range(3): + point_data[species][name][t] = [] + + # scalar spaces + if isinstance(temp_val, xp.ndarray): if physical: # push-forward - if space_id == "Hcurl": - point_data[name][t].append( - domain.push( - temp_val, - *grids_log, - kind="1", - )[j], - ) - elif space_id == "Hdiv": - point_data[name][t].append( + if space_id == "H1": + point_data[species][name][t].append( domain.push( temp_val, *grids_log, - kind="2", - )[j], + kind="0", + ), ) - elif space_id == "H1vec": - point_data[name][t].append( + elif space_id == "L2": + point_data[species][name][t].append( domain.push( temp_val, *grids_log, - kind="v", - )[j], + kind="3", + ), ) else: - point_data[name][t].append(temp_val[j]) + point_data[species][name][t].append(temp_val) + + # vector-valued spaces + else: + for j in range(3): + if physical: + # push-forward + if space_id == "Hcurl": + point_data[species][name][t].append( + domain.push( + temp_val, + *grids_log, + kind="1", + )[j], + ) + elif space_id == "Hdiv": + point_data[species][name][t].append( + domain.push( + temp_val, + *grids_log, + kind="2", + )[j], + ) + elif space_id == "H1vec": + point_data[species][name][t].append( + domain.push( + temp_val, + *grids_log, + kind="v", + )[j], + ) + + else: + point_data[species][name][t].append(temp_val[j]) return point_data, grids_log, grids_phy def create_vtk( path: str, + t_grid: xp.ndarray, grids_phy: list, point_data: dict, *, @@ -304,6 +400,9 @@ def create_vtk( path : str Absolute path of where to store the .vts files. Will then be in path/vtk/step_.vts. + t_grid : xp.ndarray + Time grid. + grids_phy : 3-list Mapped (physical) grids obtained from struphy.diagnostics.post_processing.eval_femfields. @@ -316,48 +415,52 @@ def create_vtk( from pyevtk.hl import gridToVTK - # directory for vtk files - path_vtk = os.path.join(path, "vtk" + physical * "_phy") - - try: - os.mkdir(path_vtk) - except: - shutil.rmtree(path_vtk) - os.mkdir(path_vtk) - - # field names - names = list(point_data.keys()) + for species, vars in point_data.items(): + species_path = os.path.join(path, species, "vtk" + physical * "_phy") + try: + os.mkdir(species_path) + except: + shutil.rmtree(species_path) + os.mkdir(species_path) # time loop - tgrid = list(point_data[names[0]].keys()) - - nt = len(tgrid) - 1 - log_nt = int(np.log10(nt)) + 1 + nt = len(t_grid) - 1 + log_nt = int(xp.log10(nt)) + 1 - print("Creating vtk ...") - for n, t in enumerate(tqdm(tgrid)): + print(f"\nCreating vtk in {path} ...") + for n, t in enumerate(tqdm(t_grid)): point_data_n = {} - for name in names: - points_list = point_data[name][t] + for species, vars in point_data.items(): + species_path = os.path.join(path, species, "vtk" + physical * "_phy") + point_data_n[species] = {} + for name, data in vars.items(): + points_list = data[t] - # scalar - if len(points_list) == 1: - point_data_n[name] = points_list[0] + # scalar + if len(points_list) == 1: + point_data_n[species][name] = points_list[0] - # vector - else: - for j in range(3): - point_data_n[name + f"_{j + 1}"] = points_list[j] + # vectorpoint_data[name] + else: + for j in range(3): + point_data_n[species][name + f"_{j + 1}"] = points_list[j] - gridToVTK( - os.path.join(path_vtk, "step_{0:0{1}d}".format(n, log_nt)), - *grids_phy, - pointData=point_data_n, - ) + gridToVTK( + os.path.join(species_path, "step_{0:0{1}d}".format(n, log_nt)), + *grids_phy, + pointData=point_data_n[species], + ) -def post_process_markers(path_in, path_out, species, kind, step=1): +def post_process_markers( + path_in: str, + path_out: str, + species: str, + domain: Domain, + kind: str = "Particles6D", + step: int = 1, +): """Computes the Cartesian (x, y, z) coordinates of saved markers during a simulation and writes them to a .npy files and to .txt files. Also saves the weights. @@ -409,24 +512,19 @@ def post_process_markers(path_in, path_out, species, kind, step=1): species : str Name of the species for which the post processing should be performed. + domain : Domain + Domain object. + kind : str Name of the kinetic kind (Particles6D, Particles5D or Particles3D). step : int, optional Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. """ - # get # of MPI processes from meta.txt file - with open(os.path.join(path_in, "meta.txt"), "r") as f: - lines = f.readlines() - - nproc = lines[4].split()[-1] - - with open(os.path.join(path_in, "parameters.yml"), "r") as f: - params = yaml.load(f, Loader=yaml.FullLoader) - - # create domain for calculating markers' physical coordinates - domain = setup_domain_and_equil(params)[0] + with open(os.path.join(path_in, "meta.yml"), "r") as f: + meta = yaml.load(f, Loader=yaml.FullLoader) + nproc = meta["MPI processes"] # open hdf5 files and get names and number of saved markers of kinetic species files = [ @@ -444,17 +542,16 @@ def post_process_markers(path_in, path_out, species, kind, step=1): # get number of time steps and markers nt, n_markers, n_cols = files[0]["kinetic/" + species + "/markers"].shape - log_nt = int(np.log10(int(((nt - 1) / step)))) + 1 + log_nt = int(xp.log10(int(((nt - 1) / step)))) + 1 # directory for .txt files and marker index which will be saved + path_orbits = os.path.join(path_out, "orbits") + if "5D" in kind: - path_orbits = os.path.join(path_out, "guiding_center") save_index = list(range(0, 6)) + [10] + [-1] elif "6D" in kind or "SPH" in kind: - path_orbits = os.path.join(path_out, "orbits") save_index = list(range(0, 7)) + [-1] else: - path_orbits = os.path.join(path_out, "orbits") save_index = list(range(0, 4)) + [-1] try: @@ -464,15 +561,15 @@ def post_process_markers(path_in, path_out, species, kind, step=1): os.mkdir(path_orbits) # temporary array - temp = np.empty((n_markers, len(save_index)), order="C") - lost_particles_mask = np.empty(n_markers, dtype=bool) + temp = xp.empty((n_markers, len(save_index)), order="C") + lost_particles_mask = xp.empty(n_markers, dtype=bool) print(f"Evaluation of {n_markers} marker orbits for {species}") # loop over time grid for n in tqdm(range(int((nt - 1) / step) + 1)): # clear buffer - temp[:, :] = 0 + temp[:, :] = 0.0 # create text file for this time step and this species file_npy = os.path.join( @@ -492,35 +589,42 @@ def post_process_markers(path_in, path_out, species, kind, step=1): # sorting out lost particles ids = temp[:, -1].astype("int") - ids_lost_particles = np.setdiff1d(np.arange(n_markers), ids) + ids_lost_particles = xp.setdiff1d(xp.arange(n_markers), ids) + ids_removed_particles = xp.nonzero(temp[:, 0] == -1.0)[0] + ids_lost_particles = xp.array(list(set(ids_lost_particles) | set(ids_removed_particles)), dtype=int) lost_particles_mask[:] = False lost_particles_mask[ids_lost_particles] = True if len(ids_lost_particles) > 0: # lost markers are saved as [0, ..., 0, ids] temp[lost_particles_mask, -1] = ids_lost_particles - ids = np.unique(np.append(ids, ids_lost_particles)) + ids = xp.unique(xp.append(ids, ids_lost_particles)) - assert np.all(sorted(ids) == np.arange(n_markers)) + assert xp.all(sorted(ids) == xp.arange(n_markers)) # compute physical positions (x, y, z) - temp[~lost_particles_mask, :3] = domain( - np.array(temp[~lost_particles_mask, :3]), - change_out_order=True, - ) - - # move ids to first column and save - temp = np.roll(temp, 1, axis=1) + pos_phys = domain(xp.array(temp[~lost_particles_mask, :3]), change_out_order=True) + temp[~lost_particles_mask, :3] = pos_phys - np.save(file_npy, temp) - np.savetxt(file_txt, temp[:, (0, 1, 2, 3, -1)], fmt="%12.6f", delimiter=", ") + # save numpy + xp.save(file_npy, temp) + # move ids to first column and save txt + temp = xp.roll(temp, 1, axis=1) + xp.savetxt(file_txt, temp[:, (0, 1, 2, 3, -1)], fmt="%12.6f", delimiter=", ") # close hdf5 files for file in files: file.close() -def post_process_f(path_in, path_out, species, step=1, compute_bckgr=False): +def post_process_f( + path_in, + params_in: ParamsIn, + path_out, + species, + step=1, + compute_bckgr=False, +): """Computes and saves distribution functions of saved binning data during a simulation. Parameters @@ -528,6 +632,9 @@ def post_process_f(path_in, path_out, species, step=1, compute_bckgr=False): path_in : str Absolute path of simulation output folder. + params_in : ParamsIn + Simulation parameters. + path_out : str Absolute path of where to store the .txt files. Will be in path_out/orbits. @@ -538,21 +645,15 @@ def post_process_f(path_in, path_out, species, step=1, compute_bckgr=False): Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. compute_bckgr : bool - Whehter to compute the kinetic background values and add them to the binning data. + Whether to compute the kinetic background values and add them to the binning data. This is used if non-standard weights are binned. """ + # get # of MPI processes from meta file + with open(os.path.join(path_in, "meta.yml"), "r") as f: + meta = yaml.load(f, Loader=yaml.FullLoader) + nproc = meta["MPI processes"] - # get model name and # of MPI processes from meta.txt file - with open(os.path.join(path_in, "meta.txt"), "r") as f: - lines = f.readlines() - - nproc = lines[4].split()[-1] - - # load parameters - with open(os.path.join(path_in, "parameters.yml"), "r") as f: - params = yaml.load(f, Loader=yaml.FullLoader) - - # open hdf5 files + # open hdf5 files and get names and number of saved markers of kinetic species files = [ h5py.File( os.path.join( @@ -591,7 +692,7 @@ def post_process_f(path_in, path_out, species, step=1, compute_bckgr=False): path_slice, "grid_" + slice_names[n_gr] + ".npy", ) - np.save(grid_path, grid[:]) + xp.save(grid_path, grid[:]) # compute distribution function for slice_name in tqdm(files[0]["kinetic/" + species + "/f"]): @@ -612,27 +713,31 @@ def post_process_f(path_in, path_out, species, step=1, compute_bckgr=False): data_df += files[rank]["kinetic/" + species + "/df/" + slice_name][::step] # save distribution functions - np.save(os.path.join(path_slice, "f_binned.npy"), data) - np.save(os.path.join(path_slice, "delta_f_binned.npy"), data_df) + xp.save(os.path.join(path_slice, "f_binned.npy"), data) + xp.save(os.path.join(path_slice, "delta_f_binned.npy"), data_df) if compute_bckgr: - bckgr_params = params["kinetic"][species]["background"] - - f_bckgr = None - for fi, maxw_params in bckgr_params.items(): - if fi[-2] == "_": - fi_type = fi[:-2] - else: - fi_type = fi - - if f_bckgr is None: - f_bckgr = getattr(maxwellians, fi_type)( - maxw_params=maxw_params, - ) - else: - f_bckgr = f_bckgr + getattr(maxwellians, fi_type)( - maxw_params=maxw_params, - ) + # bckgr_params = params["kinetic"][species]["background"] + + # f_bckgr = None + # for fi, maxw_params in bckgr_params.items(): + # if fi[-2] == "_": + # fi_type = fi[:-2] + # else: + # fi_type = fi + + # if f_bckgr is None: + # f_bckgr = getattr(maxwellians, fi_type)( + # maxw_params=maxw_params, + # ) + # else: + # f_bckgr = f_bckgr + getattr(maxwellians, fi_type)( + # maxw_params=maxw_params, + # ) + + spec: ParticleSpecies = getattr(params_in.model, species) + var: PICVariable = spec.var + f_bckgr: KineticBackground = var.backgrounds # load all grids of the variables of f grid_tot = [] @@ -648,11 +753,11 @@ def post_process_f(path_in, path_out, species, step=1, compute_bckgr=False): # check if file exists and is in slice_name if os.path.exists(filename) and current_slice in slice_names: - grid_tot += [np.load(filename)] + grid_tot += [xp.load(filename)] # otherwise evaluate at zero else: - grid_tot += [np.zeros(1)] + grid_tot += [xp.zeros(1)] # v-grid for comp in range(1, f_bckgr.vdim + 1): @@ -664,15 +769,15 @@ def post_process_f(path_in, path_out, species, step=1, compute_bckgr=False): # check if file exists and is in slice_name if os.path.exists(filename) and current_slice in slice_names: - grid_tot += [np.load(filename)] + grid_tot += [xp.load(filename)] # otherwise evaluate at zero else: - grid_tot += [np.zeros(1)] + grid_tot += [xp.zeros(1)] # correct integrating out in v-direction, TODO: check for 5D Maxwellians - factor *= np.sqrt(2 * np.pi) + factor *= xp.sqrt(2 * xp.pi) - grid_eval = np.meshgrid(*grid_tot, indexing="ij") + grid_eval = xp.meshgrid(*grid_tot, indexing="ij") data_bckgr = f_bckgr(*grid_eval).squeeze() @@ -683,9 +788,9 @@ def post_process_f(path_in, path_out, species, step=1, compute_bckgr=False): data_delta_f = data_df # save distribution function - np.save(os.path.join(path_slice, "delta_f_binned.npy"), data_delta_f) + xp.save(os.path.join(path_slice, "delta_f_binned.npy"), data_delta_f) # add extra axis for data_bckgr since data_delta_f has axis for time series - np.save( + xp.save( os.path.join(path_slice, "f_binned.npy"), data_delta_f + data_bckgr[tuple([None])], ) @@ -695,7 +800,13 @@ def post_process_f(path_in, path_out, species, step=1, compute_bckgr=False): file.close() -def post_process_n_sph(path_in, path_out, species, step=1, compute_bckgr=False): +def post_process_n_sph( + path_in, + params_in: ParamsIn, + path_out, + species, + step=1, +): """Computes and saves the density n of saved sph data during a simulation. Parameters @@ -703,6 +814,9 @@ def post_process_n_sph(path_in, path_out, species, step=1, compute_bckgr=False): path_in : str Absolute path of simulation output folder. + params_in : ParamsIn + Simulation parameters. + path_out : str Absolute path of where to store the .txt files. Will be in path_out/orbits. @@ -711,21 +825,11 @@ def post_process_n_sph(path_in, path_out, species, step=1, compute_bckgr=False): step : int, optional Whether to do post-processing at every time step (step=1, default), every second time step (step=2), etc. - - compute_bckgr : bool - Whehter to compute the kinetic background values and add them to the binning data. - This is used if non-standard weights are binned. """ - - # get model name and # of MPI processes from meta.txt file - with open(os.path.join(path_in, "meta.txt"), "r") as f: - lines = f.readlines() - - nproc = lines[4].split()[-1] - - # load parameters - with open(os.path.join(path_in, "parameters.yml"), "r") as f: - params = yaml.load(f, Loader=yaml.FullLoader) + # get model name and # of MPI processes from meta file + with open(os.path.join(path_in, "meta.yml"), "r") as f: + meta = yaml.load(f, Loader=yaml.FullLoader) + nproc = meta["MPI processes"] # open hdf5 files files = [ @@ -762,7 +866,7 @@ def post_process_n_sph(path_in, path_out, species, step=1, compute_bckgr=False): eta2 = files[0]["kinetic/" + species + "/n_sph/" + view].attrs["eta2"] eta3 = files[0]["kinetic/" + species + "/n_sph/" + view].attrs["eta3"] - ee1, ee2, ee3 = np.meshgrid( + ee1, ee2, ee3 = xp.meshgrid( eta1, eta2, eta3, @@ -773,7 +877,7 @@ def post_process_n_sph(path_in, path_out, species, step=1, compute_bckgr=False): path_view, "grid_n_sph.npy", ) - np.save(grid_path, (ee1, ee2, ee3)) + xp.save(grid_path, (ee1, ee2, ee3)) # load n_sph data data = files[0]["kinetic/" + species + "/n_sph/" + view][::step].copy() @@ -781,4 +885,4 @@ def post_process_n_sph(path_in, path_out, species, step=1, compute_bckgr=False): data += files[rank]["kinetic/" + species + "/n_sph/" + view][::step] # save distribution functions - np.save(os.path.join(path_view, "n_sph.npy"), data) + xp.save(os.path.join(path_view, "n_sph.npy"), data) diff --git a/src/struphy/post_processing/pproc_struphy.py b/src/struphy/post_processing/pproc_struphy.py index 29199d065..044ec0de8 100644 --- a/src/struphy/post_processing/pproc_struphy.py +++ b/src/struphy/post_processing/pproc_struphy.py @@ -1,3 +1,16 @@ +import os +import pickle +import shutil + +import cunumpy as xp +import h5py +import yaml + +import struphy.post_processing.orbits.orbits_tools as orbits_pproc +import struphy.post_processing.post_processing_tools as pproc +from struphy.io.setup import import_parameters_py + + def main( path: str, *, @@ -36,19 +49,6 @@ def main( time_trace : bool whether to plot the time trace of each measured region """ - - import os - import pickle - import shutil - - import h5py - import yaml - - import struphy.post_processing.orbits.orbits_tools as orbits_pproc - import struphy.post_processing.post_processing_tools as pproc - from struphy.models import fluid, hybrid, kinetic, toy - from struphy.utils.arrays import xp as np - print("") # create post-processing folder @@ -64,24 +64,7 @@ def main( file = h5py.File(os.path.join(path, "data/", "data_proc0.hdf5"), "r") # save time grid at which post-processing data is created - np.save(os.path.join(path_pproc, "t_grid.npy"), file["time/value"][::step].copy()) - - # load parameters.yml - with open(os.path.join(path, "parameters.yml"), "r") as f: - params = yaml.load(f, Loader=yaml.FullLoader) - - # get model class from meta.txt file - with open(os.path.join(path, "meta.txt"), "r") as f: - lines = f.readlines() - model_name = lines[3].split()[-1] - - objs = [fluid, kinetic, hybrid, toy] - - for obj in objs: - try: - model_class = getattr(obj, model_name) - except AttributeError: - pass + xp.save(os.path.join(path_pproc, "t_grid.npy"), file["time/value"][::step].copy()) if "feec" in file.keys(): exist_fields = True @@ -90,42 +73,35 @@ def main( if "kinetic" in file.keys(): exist_kinetic = {"markers": False, "f": False, "n_sph": False} - - kinetic_species = [] - kinetic_kinds = [] - for name in file["kinetic"].keys(): - kinetic_species += [name] - kinetic_kinds += [model_class.species()["kinetic"][name]] - # check for saved markers if "markers" in file["kinetic"][name]: exist_kinetic["markers"] = True - # check for saved distribution function if "f" in file["kinetic"][name]: exist_kinetic["f"] = True - # check for saved sph density if "n_sph" in file["kinetic"][name]: exist_kinetic["n_sph"] = True - else: exist_kinetic = None file.close() + # import parameters + params_in = import_parameters_py(os.path.join(path, "parameters.py")) + # field post-processing if exist_fields: - fields, space_ids, _ = pproc.create_femfields(path, step=step) + fields, t_grid = pproc.create_femfields(path, params_in, step=step) point_data, grids_log, grids_phy = pproc.eval_femfields( - path, fields, space_ids, celldivide=[celldivide, celldivide, celldivide] + params_in, fields, celldivide=[celldivide, celldivide, celldivide] ) if physical: point_data_phy, grids_log, grids_phy = pproc.eval_femfields( - path, fields, space_ids, celldivide=[celldivide, celldivide, celldivide], physical=True + params_in, fields, celldivide=[celldivide, celldivide, celldivide], physical=True ) # directory for field data @@ -138,38 +114,19 @@ def main( os.mkdir(path_fields) # save data dicts for each field - for name, val in point_data.items(): - aux = name.split("_") - # is em field - if len(aux) == 1 or "field" in name: - subfolder = "em_fields" - new_name = name + for species, vars in point_data.items(): + for name, val in vars.items(): try: - os.mkdir(os.path.join(path_fields, subfolder)) + os.mkdir(os.path.join(path_fields, species)) except: pass - # is fluid species - else: - subfolder = aux[0] - for au in aux[1:-1]: - subfolder += "_" + au - new_name = aux[-1] - try: - os.mkdir(os.path.join(path_fields, subfolder)) - except: - pass + with open(os.path.join(path_fields, species, name + "_log.bin"), "wb") as handle: + pickle.dump(val, handle, protocol=pickle.HIGHEST_PROTOCOL) - print(f"{name = }") - print(f"{subfolder = }") - print(f"{new_name = }") - - with open(os.path.join(path_fields, subfolder, new_name + "_log.bin"), "wb") as handle: - pickle.dump(val, handle, protocol=pickle.HIGHEST_PROTOCOL) - - if physical: - with open(os.path.join(path_fields, subfolder, new_name + "_phy.bin"), "wb") as handle: - pickle.dump(point_data_phy[name], handle, protocol=pickle.HIGHEST_PROTOCOL) + if physical: + with open(os.path.join(path_fields, species, name + "_phy.bin"), "wb") as handle: + pickle.dump(point_data_phy[species][name], handle, protocol=pickle.HIGHEST_PROTOCOL) # save grids with open(os.path.join(path_fields, "grids_log.bin"), "wb") as handle: @@ -180,9 +137,9 @@ def main( # create vtk files if not no_vtk: - pproc.create_vtk(path_fields, grids_phy, point_data) + pproc.create_vtk(path_fields, t_grid, grids_phy, point_data) if physical: - pproc.create_vtk(path_fields, grids_phy, point_data_phy, physical=True) + pproc.create_vtk(path_fields, t_grid, grids_phy, point_data_phy, physical=True) # kinetic post-processing if exist_kinetic is not None: diff --git a/src/struphy/post_processing/profile_struphy.py b/src/struphy/post_processing/profile_struphy.py index 8c738a9a7..da4632555 100644 --- a/src/struphy/post_processing/profile_struphy.py +++ b/src/struphy/post_processing/profile_struphy.py @@ -1,11 +1,11 @@ import pickle import sys +import cunumpy as xp import yaml from matplotlib import pyplot as plt from struphy.post_processing.cprofile_analyser import get_cprofile_data, replace_keys -from struphy.utils.arrays import xp as np def main(): @@ -150,17 +150,17 @@ def main(): plt.ylabel("time [s]") plt.title("Strong scaling for Nel=" + str(val["Nel"][0]) + " cells") plt.legend(loc="lower left") - plt.loglog(val["mpi_size"], val["time"][0] / 2 ** np.arange(len(val["time"])), "k--", alpha=0.3) + plt.loglog(val["mpi_size"], val["time"][0] / 2 ** xp.arange(len(val["time"])), "k--", alpha=0.3) # weak scaling plot else: plt.plot(val["mpi_size"], val["time"], label=key) plt.xlabel("mpi_size") plt.ylabel("time [s]") plt.title( - "Weak scaling for cells/mpi_size=" + str(np.prod(val["Nel"][0]) / val["mpi_size"][0]) + "=const." + "Weak scaling for cells/mpi_size=" + str(xp.prod(val["Nel"][0]) / val["mpi_size"][0]) + "=const." ) plt.legend(loc="upper left") - # plt.loglog(val['mpi_size'], val['time'][0]*np.ones_like(val['time']), 'k--', alpha=0.3) + # plt.loglog(val['mpi_size'], val['time'][0]*xp.ones_like(val['time']), 'k--', alpha=0.3) plt.xscale("log") plt.show() diff --git a/src/struphy/profiling/profiling.py b/src/struphy/profiling/profiling.py index 568478e40..e96749614 100644 --- a/src/struphy/profiling/profiling.py +++ b/src/struphy/profiling/profiling.py @@ -17,10 +17,9 @@ # Import the profiling configuration class and context manager from functools import lru_cache +import cunumpy as xp from psydac.ddm.mpi import mpi as MPI -from struphy.utils.arrays import xp as np - @lru_cache(maxsize=None) # Cache the import result to avoid repeated imports def _import_pylikwid(): @@ -171,9 +170,9 @@ def save_to_pickle(cls, file_path): for name, region in cls._regions.items(): local_data[name] = { "ncalls": region.ncalls, - "durations": np.array(region.durations, dtype=np.float64), - "start_times": np.array(region.start_times, dtype=np.float64), - "end_times": np.array(region.end_times, dtype=np.float64), + "durations": xp.array(region.durations, dtype=xp.float64), + "start_times": xp.array(region.start_times, dtype=xp.float64), + "end_times": xp.array(region.end_times, dtype=xp.float64), "config": { "likwid": region.config.likwid, "simulation_label": region.config.simulation_label, @@ -247,7 +246,7 @@ def print_summary(cls): average_duration = total_duration / region.ncalls min_duration = min(region.durations) max_duration = max(region.durations) - std_duration = np.std(region.durations) + std_duration = xp.std(region.durations) else: total_duration = average_duration = min_duration = max_duration = std_duration = 0 @@ -271,16 +270,16 @@ def __init__(self, region_name, time_trace=False): self._region_name = self.config.simulation_label + region_name self._time_trace = time_trace self._ncalls = 0 - self._start_times = np.empty(1, dtype=float) - self._end_times = np.empty(1, dtype=float) - self._durations = np.empty(1, dtype=float) + self._start_times = xp.empty(1, dtype=float) + self._end_times = xp.empty(1, dtype=float) + self._durations = xp.empty(1, dtype=float) self._started = False def __enter__(self): if self._ncalls == len(self._start_times): - self._start_times = np.append(self._start_times, np.zeros_like(self._start_times)) - self._end_times = np.append(self._end_times, np.zeros_like(self._end_times)) - self._durations = np.append(self._durations, np.zeros_like(self._durations)) + self._start_times = xp.append(self._start_times, xp.zeros_like(self._start_times)) + self._end_times = xp.append(self._end_times, xp.zeros_like(self._end_times)) + self._durations = xp.append(self._durations, xp.zeros_like(self._durations)) if self.config.likwid: self._pylikwid().markerstartregion(self.region_name) diff --git a/src/struphy/propagators/__init__.py b/src/struphy/propagators/__init__.py index 9d6a018ee..04418745c 100644 --- a/src/struphy/propagators/__init__.py +++ b/src/struphy/propagators/__init__.py @@ -1,95 +1,96 @@ -from struphy.propagators.propagators_coupling import ( - CurrentCoupling5DCurlb, - CurrentCoupling5DGradB, - CurrentCoupling6DCurrent, - EfieldWeights, - PressureCoupling6D, - VlasovAmpere, -) -from struphy.propagators.propagators_fields import ( - AdiabaticPhi, - CurrentCoupling5DDensity, - CurrentCoupling6DDensity, - FaradayExtended, - Hall, - HasegawaWakatani, - ImplicitDiffusion, - JxBCold, - Magnetosonic, - MagnetosonicCurrentCoupling5D, - MagnetosonicUniform, - Maxwell, - OhmCold, - Poisson, - ShearAlfven, - ShearAlfvenB1, - ShearAlfvenCurrentCoupling5D, - TimeDependentSource, - TwoFluidQuasiNeutralFull, - VariationalDensityEvolve, - VariationalEntropyEvolve, - VariationalMagFieldEvolve, - VariationalMomentumAdvection, - VariationalPBEvolve, - VariationalQBEvolve, - VariationalResistivity, - VariationalViscosity, -) -from struphy.propagators.propagators_markers import ( - PushDeterministicDiffusion, - PushEta, - PushEtaPC, - PushGuidingCenterBxEstar, - PushGuidingCenterParallel, - PushRandomDiffusion, - PushVinEfield, - PushVinSPHpressure, - PushVinViscousPotential, - PushVxB, -) +# from struphy.propagators.propagators_coupling import ( +# CurrentCoupling5DCurlb, +# CurrentCoupling5DGradB, +# CurrentCoupling6DCurrent, +# EfieldWeights, +# PressureCoupling6D, +# VlasovAmpere, +# ) +# from struphy.propagators.propagators_fields import ( +# AdiabaticPhi, +# CurrentCoupling5DDensity, +# CurrentCoupling6DDensity, +# FaradayExtended, +# Hall, +# HasegawaWakatani, +# ImplicitDiffusion, +# JxBCold, +# Magnetosonic, +# MagnetosonicCurrentCoupling5D, +# MagnetosonicUniform, +# Maxwell, +# OhmCold, +# Poisson, +# ShearAlfven, +# ShearAlfvenB1, +# ShearAlfvenCurrentCoupling5D, +# TimeDependentSource, +# TwoFluidQuasiNeutralFull, +# VariationalDensityEvolve, +# VariationalEntropyEvolve, +# VariationalMagFieldEvolve, +# VariationalMomentumAdvection, +# VariationalPBEvolve, +# VariationalQBEvolve, +# VariationalResistivity, +# VariationalViscosity, +# ) +# from struphy.propagators.propagators_markers import ( +# PushDeterministicDiffusion, +# PushEta, +# PushEtaPC, +# PushGuidingCenterBxEstar, +# PushGuidingCenterParallel, +# PushRandomDiffusion, +# PushVinEfield, +# PushVinSPHpressure, +# PushVinViscousPotential, +# PushVxB, +# StepStaticEfield, +# ) -__all__ = [ - "VlasovAmpere", - "EfieldWeights", - "PressureCoupling6D", - "CurrentCoupling6DCurrent", - "CurrentCoupling5DCurlb", - "CurrentCoupling5DGradB", - "Maxwell", - "OhmCold", - "JxBCold", - "ShearAlfven", - "ShearAlfvenB1", - "Hall", - "Magnetosonic", - "MagnetosonicUniform", - "FaradayExtended", - "CurrentCoupling6DDensity", - "ShearAlfvenCurrentCoupling5D", - "MagnetosonicCurrentCoupling5D", - "CurrentCoupling5DDensity", - "ImplicitDiffusion", - "Poisson", - "VariationalMomentumAdvection", - "VariationalDensityEvolve", - "VariationalEntropyEvolve", - "VariationalMagFieldEvolve", - "VariationalPBEvolve", - "VariationalQBEvolve", - "VariationalViscosity", - "VariationalResistivity", - "TimeDependentSource", - "AdiabaticPhi", - "HasegawaWakatani", - "TwoFluidQuasiNeutralFull", - "PushEta", - "PushVxB", - "PushVinEfield", - "PushEtaPC", - "PushGuidingCenterBxEstar", - "PushGuidingCenterParallel", - "PushDeterministicDiffusion", - "PushRandomDiffusion", - "PushVinSPHpressure", - "PushVinViscousPotential", -] +# __all__ = [ +# "VlasovAmpere", +# "EfieldWeights", +# "PressureCoupling6D", +# "CurrentCoupling6DCurrent", +# "CurrentCoupling5DCurlb", +# "CurrentCoupling5DGradB", +# "Maxwell", +# "OhmCold", +# "JxBCold", +# "ShearAlfven", +# "ShearAlfvenB1", +# "Hall", +# "Magnetosonic", +# "MagnetosonicUniform", +# "FaradayExtended", +# "CurrentCoupling6DDensity", +# "ShearAlfvenCurrentCoupling5D", +# "CurrentCoupling5DDensity", +# "ImplicitDiffusion", +# "Poisson", +# "VariationalMomentumAdvection", +# "VariationalDensityEvolve", +# "VariationalEntropyEvolve", +# "VariationalMagFieldEvolve", +# "VariationalPBEvolve", +# "VariationalQBEvolve", +# "VariationalViscosity", +# "VariationalResistivity", +# "TimeDependentSource", +# "AdiabaticPhi", +# "HasegawaWakatani", +# "TwoFluidQuasiNeutralFull", +# "PushEta", +# "PushVxB", +# "PushVinEfield", +# "PushEtaPC", +# "PushGuidingCenterBxEstar", +# "PushGuidingCenterParallel", +# "StepStaticEfield", +# "PushDeterministicDiffusion", +# "PushRandomDiffusion", +# "PushVinSPHpressure", +# "PushVinViscousPotential", +# ] diff --git a/src/struphy/propagators/base.py b/src/struphy/propagators/base.py index 8e05b1b17..f69d4c7fe 100644 --- a/src/struphy/propagators/base.py +++ b/src/struphy/propagators/base.py @@ -1,16 +1,24 @@ "Propagator base class." from abc import ABCMeta, abstractmethod +from dataclasses import dataclass +from typing import Literal + +import cunumpy as xp +from psydac.linalg.block import BlockVector +from psydac.linalg.stencil import StencilVector from struphy.feec.basis_projection_ops import BasisProjectionOperators from struphy.feec.mass import WeightedMassOperators from struphy.feec.psydac_derham import Derham +from struphy.fields_background.projected_equils import ProjectedFluidEquilibriumWithB from struphy.geometry.base import Domain -from struphy.utils.arrays import xp as np +from struphy.io.options import check_option +from struphy.models.variables import FEECVariable, PICVariable, SPHVariable, Variable class Propagator(metaclass=ABCMeta): - """Base class for Struphy propagators used in Struphy models. + """Base class for propagators used in StruphyModels. Note ---- @@ -19,55 +27,99 @@ class Propagator(metaclass=ABCMeta): Only propagators that update both a FEEC and a PIC species go into ``propagators_coupling.py``. """ - def __init__(self, *vars): - """Create an instance of a Propagator. + @abstractmethod + class Variables: + """Define variable names and types to be updated by the propagator.""" + + def __init__(self): + self._var1 = None + + @property + def var1(self): + return self._var1 + + @var1.setter + def var1(self, new): + assert isinstance(new, PICVariable) + assert new.space == "Particles6D" + self._var1 = new + + @abstractmethod + def __init__(self): + self.variables = self.Variables() + + @abstractmethod + @dataclass + class Options: + # specific literals + OptsTemplate = Literal["implicit", "explicit"] + # propagator options + opt1: str = ("implicit",) + + def __post_init__(self): + # checks + check_option(self.opt1, self.OptsTemplate) + + @property + @abstractmethod + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + @abstractmethod + def options(self, new): + assert isinstance(new, self.Options) + if True: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @abstractmethod + def allocate(self): + """Allocate all data/objects of the instance.""" + + @abstractmethod + def __call__(self, dt: float): + """Update variables from t -> t + dt. + Use ``Propagators.feec_vars_update`` to write to FEEC variables to ``Propagator.feec_vars``. Parameters ---------- - vars : Vector or Particles - :attr:`struphy.models.base.StruphyModel.pointer` of variables to be updated. + dt : float + Time step size. """ - from psydac.linalg.basic import Vector - - from struphy.pic.particles import Particles - - self._feec_vars = [] - self._particles = [] - - for var in vars: - if isinstance(var, Vector): - self._feec_vars += [var] - elif isinstance(var, Particles): - self._particles += [var] - else: - ValueError( - f'Variable {var} must be of type "Vector" or "Particles".', - ) - - # for iterative particle push - self._init_kernels = [] - self._eval_kernels = [] - - # mpi comm - if self.particles: - comm = self.particles[0].mpi_comm - else: - comm = self.derham.comm - self._rank = comm.Get_rank() if comm is not None else 0 - @property - def feec_vars(self): - """List of FEEC variables (not particles) to be updated by the propagator. - Contains FE coefficients from :attr:`struphy.feec.SplineFunction.vector`. - """ - return self._feec_vars + def update_feec_variables(self, **new_coeffs): + r"""Return max_diff = max(abs(new - old)) for each new_coeffs, + update feec coefficients and update ghost regions. - @property - def particles(self): - """List of kinetic variables (not FEEC) to be updated by the propagator. - Contains :class:`struphy.pic.particles.Particles`. + Returns + ------- + diffs : dict + max_diff for all feec variables. """ - return self._particles + diffs = {} + for var, new in new_coeffs.items(): + assert "_" + var in self.variables.__dict__, f"{var} not in {self.variables.__dict__}." + assert isinstance(new, (StencilVector, BlockVector)) + old_var = getattr(self.variables, var) + assert isinstance(old_var, FEECVariable) + old = old_var.spline.vector + assert new.space == old.space + + # calculate maximum of difference abs(new - old) + diffs[var] = xp.max(xp.abs(new.toarray() - old.toarray())) + + # copy new coeffs into old + new.copy(out=old) + + # important: sync processes! + old.update_ghost_regions() + + return diffs @property def init_kernels(self): @@ -91,24 +143,6 @@ def rank(self): """MPI rank, is 0 if no communicator.""" return self._rank - @abstractmethod - def __call__(self, dt): - """Update from t -> t + dt. - Use ``Propagators.feec_vars_update`` to write to FEEC variables to ``Propagator.feec_vars``. - - Parameters - ---------- - dt : float - Time step size. - """ - pass - - @staticmethod - @abstractmethod - def options(): - """Dictionary of available propagator options, as appearing under species/options in the parameter file.""" - pass - @property def derham(self): """Derham spaces and projectors.""" @@ -157,7 +191,7 @@ def basis_ops(self, basis_ops): self._basis_ops = basis_ops @property - def projected_equil(self): + def projected_equil(self) -> ProjectedFluidEquilibriumWithB: """Fluid equilibrium projected on 3d Derham sequence with commuting projectors.""" assert hasattr( self, @@ -166,8 +200,9 @@ def projected_equil(self): return self._projected_equil @projected_equil.setter - def projected_equil(self, projected_equil): - self._projected_equil = projected_equil + def projected_equil(self, new): + assert isinstance(new, ProjectedFluidEquilibriumWithB) + self._projected_equil = new @property def time_state(self): @@ -185,40 +220,6 @@ def add_time_state(self, time_state): assert time_state.size == 1 self._time_state = time_state - def feec_vars_update(self, *variables_new): - r"""Return :math:`\textrm{max}_i |x_i(t + \Delta t) - x_i(t)|` for each unknown in list, - update :method:`~struphy.propagators.base.Propagator.feec_vars` - and update ghost regions. - - Parameters - ---------- - variables_new : list[StencilVector | BlockVector] - Same sequence as in :method:`~struphy.propagators.base.Propagator.feec_vars` - but with the updated variables, - i.e. for feec_vars = [e, b] we must have variables_new = [e_updated, b_updated]. - - Returns - ------- - diffs : list - A list [max(abs(self.feec_vars - variables_new)), ...] for all variables in self.feec_vars and variables_new. - """ - - diffs = [] - - for i, new in enumerate(variables_new): - assert type(new) is type(self.feec_vars[i]) - - # calculate maximum of difference abs(old - new) - diffs += [np.max(np.abs(self.feec_vars[i].toarray() - new.toarray()))] - - # copy new variables into self.feec_vars - new.copy(out=self.feec_vars[i]) - - # important: sync processes! - self.feec_vars[i].update_ghost_regions() - - return diffs - def add_init_kernel( self, kernel, @@ -245,9 +246,12 @@ def add_init_kernel( The arguments for the kernel function. """ if comps is None: - comps = np.array([0]) # case for scalar evaluation + comps = xp.array([0]) # case for scalar evaluation else: - comps = np.array(comps, dtype=int) + comps = xp.array(comps, dtype=int) + + if not hasattr(self, "_init_kernels"): + self._init_kernels = [] self._init_kernels += [ ( @@ -293,12 +297,15 @@ def add_eval_kernel( """ if isinstance(alpha, int) or isinstance(alpha, float): alpha = [alpha] * 6 - alpha = np.array(alpha) + alpha = xp.array(alpha) if comps is None: - comps = np.array([0]) # case for scalar evaluation + comps = xp.array([0]) # case for scalar evaluation else: - comps = np.array(comps, dtype=int) + comps = xp.array(comps, dtype=int) + + if not hasattr(self, "_eval_kernels"): + self._eval_kernels = [] self._eval_kernels += [ ( diff --git a/src/struphy/propagators/propagators_coupling.py b/src/struphy/propagators/propagators_coupling.py index 88472a177..80d483568 100644 --- a/src/struphy/propagators/propagators_coupling.py +++ b/src/struphy/propagators/propagators_coupling.py @@ -1,22 +1,34 @@ "Particle and FEEC variables are updated." +from dataclasses import dataclass +from typing import Literal + +import cunumpy as xp +from line_profiler import profile +from psydac.ddm.mpi import mpi as MPI from psydac.linalg.block import BlockVector +from psydac.linalg.solvers import inverse from psydac.linalg.stencil import StencilVector from struphy.feec import preconditioner from struphy.feec.linear_operators import LinOpWithTransp +from struphy.io.options import OptsGenSolver, OptsMassPrecond, OptsSymmSolver, OptsVecSpace, check_option from struphy.io.setup import descend_options_dict from struphy.kinetic_background.base import Maxwellian from struphy.kinetic_background.maxwellians import Maxwellian3D from struphy.linear_algebra.schur_solver import SchurSolver +from struphy.linear_algebra.solver import DiscreteGradientSolverParameters, SolverParameters +from struphy.models.variables import FEECVariable, PICVariable +from struphy.ode.utils import ButcherTableau +from struphy.pic import utilities_kernels from struphy.pic.accumulation import accum_kernels, accum_kernels_gc -from struphy.pic.accumulation.particles_to_grid import Accumulator +from struphy.pic.accumulation.filter import FilterParameters +from struphy.pic.accumulation.particles_to_grid import Accumulator, AccumulatorVector from struphy.pic.particles import Particles5D, Particles6D from struphy.pic.pushing import pusher_kernels, pusher_kernels_gc from struphy.pic.pushing.pusher import Pusher from struphy.polar.basic import PolarVector from struphy.propagators.base import Propagator -from struphy.utils.arrays import xp as np from struphy.utils.pyccel import Pyccelkernel @@ -27,9 +39,9 @@ class VlasovAmpere(Propagator): .. math:: -& \int_\Omega \frac{\partial \mathbf E}{\partial t} \cdot \mathbf F\,\textrm d \mathbf x = - c_1 \int_\Omega \int_{\mathbb{R}^3} f \mathbf{v} \cdot \mathbf F \, \text{d}^3 \mathbf{v} \,\textrm d \mathbf x \qquad \forall \, \mathbf F \in H(\textnormal{curl}) \,, + \frac{\alpha^2}{\varepsilon} \int_\Omega \int_{\mathbb{R}^3} f \mathbf{v} \cdot \mathbf F \, \text{d}^3 \mathbf{v} \,\textrm d \mathbf x \qquad \forall \, \mathbf F \in H(\textnormal{curl}) \,, \\[2mm] - &\frac{\partial f}{\partial t} + c_2\, \mathbf{E} + &\frac{\partial f}{\partial t} + \frac{1}{\varepsilon}\, \mathbf{E} \cdot \frac{\partial f}{\partial \mathbf{v}} = 0 \,. :ref:`time_discret`: Crank-Nicolson (implicit mid-point). System size reduction via :class:`~struphy.linear_algebra.schur_solver.SchurSolver`, such that @@ -43,8 +55,8 @@ class VlasovAmpere(Propagator): = \frac{\Delta t}{2} \begin{bmatrix} - 0 & - c_1 \mathbb L^1 \bar{DF^{-1}} \bar{\mathbf w} \\ - c_2 \bar{DF^{-\top}} \left(\mathbb L^1\right)^\top & 0 + 0 & - \frac{\alpha^2}{\varepsilon} \mathbb L^1 \bar{DF^{-1}} \bar{\mathbf w} \\ + \frac{1}{\varepsilon} \bar{DF^{-\top}} \left(\mathbb L^1\right)^\top & 0 \end{bmatrix} \begin{bmatrix} \mathbf{e}^{n+1} + \mathbf{e}^n \\ @@ -55,59 +67,90 @@ class VlasovAmpere(Propagator): .. math:: - A = \mathbb M^1\,,\qquad B = \frac{c_1}{2} \mathbb L^1 \bar{DF^{-1}} \bar{\mathbf w}\,,\qquad C = - \frac{c_2}{2} \bar{DF^{-\top}} \left(\mathbb L^1\right)^\top \,. + A = \mathbb M^1\,,\qquad B = \frac{\alpha^2}{2\varepsilon} \mathbb L^1 \bar{DF^{-1}} \bar{\mathbf w}\,,\qquad C = - \frac{1}{2\varepsilon} \bar{DF^{-\top}} \left(\mathbb L^1\right)^\top \,. The accumulation matrix and vector assembled in :class:`~struphy.pic.accumulation.particles_to_grid.Accumulator` are .. math:: M = BC \,,\qquad V = B \mathbf V \,. - - Note - ---------- - * For :class:`~struphy.models.kinetic.VlasovAmpereOneSpecies`: :math:`c_1 = \kappa^2 \,, \, c_2 = 1` - * For :class:`~struphy.models.kinetic.VlasovMaxwellOneSpecies`: :math:`c_1 = \alpha^2/\varepsilon \,, \, c_2 = 1/\varepsilon` - * For :class:`~struphy.models.hybrid.ColdPlasmaVlasov`: :math:`c_1 = \nu\alpha^2/\varepsilon_\textrm{c} \,, \, c_2 = 1/\varepsilon_\textrm{h}` """ - @staticmethod - def options(default=False): - dct = {} - dct["solver"] = { - "type": [ - ("pcg", "MassMatrixPreconditioner"), - ("cg", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - e: BlockVector, - particles: Particles6D, - *, - c1: float = 1.0, - c2: float = 1.0, - solver=options(default=True)["solver"], - ): - super().__init__(e, particles) - - self._c1 = c1 - self._c2 = c2 - self._info = solver["info"] + class Variables: + def __init__(self): + self._e: FEECVariable = None + self._ions: PICVariable = None + + @property + def e(self) -> FEECVariable: + return self._e + + @e.setter + def e(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "Hcurl" + self._e = new + + @property + def ions(self) -> PICVariable: + return self._ions + + @ions.setter + def ions(self, new): + assert isinstance(new, PICVariable) + assert new.space == "Particles6D" + self._ions = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + + def __post_init__(self): + # checks + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + # scaling factors + alpha = self.variables.ions.species.equation_params.alpha + epsilon = self.variables.ions.species.equation_params.epsilon + + self._c1 = alpha**2 / epsilon + self._c2 = 1.0 / epsilon + + self._info = self.options.solver_params.info # get accumulation kernel accum_kernel = Pyccelkernel(accum_kernels.vlasov_maxwell) # Initialize Accumulator object + particles = self.variables.ions.particles + self._accum = Accumulator( particles, "Hcurl", @@ -119,19 +162,19 @@ def __init__( ) # Create buffers to store temporarily e and its sum with old e - self._e_tmp = e.space.zeros() - self._e_scale = e.space.zeros() - self._e_sum = e.space.zeros() + self._e_tmp = self.derham.Vh["1"].zeros() + self._e_scale = self.derham.Vh["1"].zeros() + self._e_sum = self.derham.Vh["1"].zeros() # ================================ # ========= Schur Solver ========= # ================================ # Preconditioner - if solver["type"][1] == None: + if self.options.precond is None: pc = None else: - pc_class = getattr(preconditioner, solver["type"][1]) + pc_class = getattr(preconditioner, self.options.precond) pc = pc_class(self.mass_ops.M1) # Define block matrix [[A B], [C I]] (without time step size dt in the diagonals) @@ -142,11 +185,9 @@ def __init__( self._schur_solver = SchurSolver( _A, _BC, - solver["type"][0], - pc=pc, - tol=solver["tol"], - maxiter=solver["maxiter"], - verbose=solver["verbose"], + self.options.solver, + precond=pc, + solver_params=self.options.solver_params, ) # Instantiate particle pusher @@ -166,6 +207,7 @@ def __init__( alpha_in_kernel=1.0, ) + @profile def __call__(self, dt): # accumulate self._accum() @@ -174,14 +216,14 @@ def __call__(self, dt): self._schur_solver.BC = self._accum.operators[0] self._schur_solver.BC *= -self._c1 * self._c2 / 4.0 - # Vector for schur solver + # Vector for Schur solver self._e_scale *= 0.0 self._e_scale += self._accum.vectors[0] self._e_scale *= self._c1 / 2.0 # new e coeffs self._e_tmp, info = self._schur_solver( - self.feec_vars[0], + self.variables.e.spline.vector, self._e_scale, dt, out=self._e_tmp, @@ -189,7 +231,7 @@ def __call__(self, dt): # mid-point e-field (no tmps created here) self._e_sum *= 0.0 - self._e_sum += self.feec_vars[0] + self._e_sum += self.variables.e.spline.vector self._e_sum += self._e_tmp self._e_sum *= 0.5 @@ -197,29 +239,30 @@ def __call__(self, dt): self._pusher(dt) # update_weights - if self.particles[0].control_variate: - self.particles[0].update_weights() + if self.variables.ions.species.weights_params.control_variate: + self.variables.ions.particles.update_weights() # write new coeffs into self.variables - (max_de,) = self.feec_vars_update(self._e_tmp) + (max_de,) = self.update_feec_variables(e=self._e_tmp) # Print out max differences for weights and e-field if self._info: print("Status for VlasovMaxwell:", info["success"]) print("Iterations for VlasovMaxwell:", info["niter"]) print("Maxdiff e1 for VlasovMaxwell:", max_de) - buffer_idx = self.particles[0].bufferindex - max_diff = np.max( - np.abs( - np.sqrt( - self.particles[0].markers_wo_holes[:, 3] ** 2 - + self.particles[0].markers_wo_holes[:, 4] ** 2 - + self.particles[0].markers_wo_holes[:, 5] ** 2, + particles = self.variables.ions.particles + buffer_idx = particles.bufferindex + max_diff = xp.max( + xp.abs( + xp.sqrt( + particles.markers_wo_holes[:, 3] ** 2 + + particles.markers_wo_holes[:, 4] ** 2 + + particles.markers_wo_holes[:, 5] ** 2, ) - - np.sqrt( - self.particles[0].markers_wo_holes[:, buffer_idx + 3] ** 2 - + self.particles[0].markers_wo_holes[:, buffer_idx + 4] ** 2 - + self.particles[0].markers_wo_holes[:, buffer_idx + 5] ** 2, + - xp.sqrt( + particles.markers_wo_holes[:, buffer_idx + 3] ** 2 + + particles.markers_wo_holes[:, buffer_idx + 4] ** 2 + + particles.markers_wo_holes[:, buffer_idx + 5] ** 2, ), ), ) @@ -283,50 +326,85 @@ class EfieldWeights(Propagator): """ - @staticmethod - def options(default=False): - dct = {} - dct["solver"] = { - "type": [ - ("pcg", "MassMatrixPreconditioner"), - ("cg", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - e: BlockVector, - particles: Particles6D, - *, - alpha: float = 1.0, - kappa: float = 1.0, - f0: Maxwellian = None, - solver=options(default=True)["solver"], - ): - super().__init__(e, particles) - - if f0 is None: - f0 = Maxwellian3D() - assert isinstance(f0, Maxwellian3D) - - self._alpha = alpha - self._kappa = kappa - self._f0 = f0 - assert self._f0.maxw_params["vth1"] == self._f0.maxw_params["vth2"] == self._f0.maxw_params["vth3"] - self._vth = self._f0.maxw_params["vth1"] - - self._info = solver["info"] + class Variables: + def __init__(self): + self._e: FEECVariable = None + self._ions: PICVariable = None + + @property + def e(self) -> FEECVariable: + return self._e + + @e.setter + def e(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "Hcurl" + self._e = new + + @property + def ions(self) -> PICVariable: + return self._ions + + @ions.setter + def ions(self, new): + assert isinstance(new, PICVariable) + assert new.space in ("Particles6D", "DeltaFParticles6D") + self._ions = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + alpha: float = 1.0 + kappa: float = 1.0 + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + + def __post_init__(self): + # checks + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + + if self.solver_params is None: + self.solver_params = SolverParameters() + + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + self._alpha = self.options.alpha + self._kappa = self.options.kappa + + backgrounds = self.variables.ions.backgrounds + # use single Maxwellian + if isinstance(backgrounds, list): + self._f0 = backgrounds[0] + else: + self._f0 = backgrounds + assert isinstance(self._f0, Maxwellian3D), "The background distribution function must be a uniform Maxwellian!" + self._vth = self._f0.maxw_params["vth1"][0] + + self._info = self.options.solver_params.info # Initialize Accumulator object + e = self.variables.e.spline.vector + particles = self.variables.ions.particles + self._accum = Accumulator( particles, "Hcurl", @@ -343,18 +421,18 @@ def __init__( self._e_sum = e.space.zeros() # marker storage - self._f0_values = np.zeros(particles.markers.shape[0], dtype=float) - self._old_weights = np.empty(particles.markers.shape[0], dtype=float) + self._f0_values = xp.zeros(particles.markers.shape[0], dtype=float) + self._old_weights = xp.empty(particles.markers.shape[0], dtype=float) # ================================ # ========= Schur Solver ========= # ================================ # Preconditioner - if solver["type"][1] == None: + if self.options.precond == None: pc = None else: - pc_class = getattr(preconditioner, solver["type"][1]) + pc_class = getattr(preconditioner, self.options.precond) pc = pc_class(self.mass_ops.M1) # Define block matrix [[A B], [C I]] (without time step size dt in the diagonals) @@ -365,11 +443,9 @@ def __init__( self._schur_solver = SchurSolver( _A, _BC, - solver["type"][0], - pc=pc, - tol=solver["tol"], - maxiter=solver["maxiter"], - verbose=solver["verbose"], + self.options.solver, + precond=pc, + solver_params=self.options.solver_params, ) # Instantiate particle pusher @@ -392,14 +468,17 @@ def __init__( ) def __call__(self, dt): + en = self.variables.e.spline.vector + particles = self.variables.ions.particles + # evaluate f0 and accumulate self._f0_values[:] = self._f0( - self.particles[0].markers[:, 0], - self.particles[0].markers[:, 1], - self.particles[0].markers[:, 2], - self.particles[0].markers[:, 3], - self.particles[0].markers[:, 4], - self.particles[0].markers[:, 5], + particles.markers[:, 0], + particles.markers[:, 1], + particles.markers[:, 2], + particles.markers[:, 3], + particles.markers[:, 4], + particles.markers[:, 5], ) self._accum(self._f0_values) @@ -415,35 +494,34 @@ def __call__(self, dt): # new e-field (no tmps created here) self._e_tmp, info = self._schur_solver( - xn=self.feec_vars[0], + xn=en, Byn=self._e_scale, dt=dt, out=self._e_tmp, ) # Store old weights - self._old_weights[~self.particles[0].holes] = self.particles[0].markers_wo_holes[:, 6] + self._old_weights[~particles.holes] = particles.markers_wo_holes[:, 6] # Compute (e^{n+1} + e^n) (no tmps created here) self._e_sum *= 0.0 - self._e_sum += self.feec_vars[0] + self._e_sum += en self._e_sum += self._e_tmp # Update weights self._pusher(dt) # write new coeffs into self.variables - (max_de,) = self.feec_vars_update(self._e_tmp) + max_de = self.update_feec_variables(e=self._e_tmp) # Print out max differences for weights and e-field if self._info: print("Status for StepEfieldWeights:", info["success"]) print("Iterations for StepEfieldWeights:", info["niter"]) print("Maxdiff e1 for StepEfieldWeights:", max_de) - max_diff = np.max( - np.abs( - self._old_weights[~self.particles[0].holes] - - self.particles[0].markers[~self.particles[0].holes, 6], + max_diff = xp.max( + xp.abs( + self._old_weights[~particles.holes] - particles.markers[~particles.holes, 6], ), ) print("Maxdiff weights for StepEfieldWeights:", max_diff) @@ -473,119 +551,133 @@ class PressureCoupling6D(Propagator): \begin{bmatrix} {\mathbb M^n}(u^{n+1} + u^n) \\ \bar W (V^{n+1} + V^{n} \end{bmatrix} \,. """ - @staticmethod - def options(default=False): - dct = {} - dct["use_perp_model"] = [True, False] - dct["solver"] = { - "type": [ - ("pcg", "MassMatrixPreconditioner"), - ("cg", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - dct["filter"] = { - "use_filter": None, - "modes": (1), - "repeat": 1, - "alpha": 0.5, - } - dct["boundary_cut"] = { - "e1": 0.0, - "e2": 0.0, - "e3": 0.0, - } - dct["turn_off"] = False - - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - particles: Particles5D, - u: BlockVector | PolarVector, - *, - use_perp_model: bool = options(default=True)["use_perp_model"], - u_space: str, - solver: dict = options(default=True)["solver"], - coupling_params: dict, - filter: dict = options(default=True)["filter"], - boundary_cut: dict = options(default=True)["boundary_cut"], - ): - super().__init__(particles, u) - - self._G = self.derham.grad - self._GT = self.derham.grad.transpose() - - self._info = solver["info"] - if self.derham.comm is None: - self._rank = 0 - else: - self._rank = self.derham.comm.Get_rank() + class Variables: + def __init__(self): + self._u: FEECVariable = None + self._energetic_ions: PICVariable = None - assert u_space in {"Hcurl", "Hdiv", "H1vec"} + @property + def u(self) -> FEECVariable: + return self._u + + @u.setter + def u(self, new): + assert isinstance(new, FEECVariable) + assert new.space in ("Hcurl", "Hdiv", "H1vec") + self._u = new + + @property + def energetic_ions(self) -> PICVariable: + return self._energetic_ions + + @energetic_ions.setter + def energetic_ions(self, new): + assert isinstance(new, PICVariable) + assert new.space == "Particles6D" + self._energetic_ions = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # propagator options + ep_scale: float = 1.0 + u_space: OptsVecSpace = "Hdiv" + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + filter_params: FilterParameters = None + use_perp_model: bool = True + + def __post_init__(self): + # checks + check_option(self.u_space, OptsVecSpace) + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + assert isinstance(self.ep_scale, float) + assert isinstance(self.use_perp_model, bool) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + if self.filter_params is None: + self.filter_params = FilterParameters() + + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + if self.options.u_space == "H1vec": + self._u_form_int = 0 + else: + self._u_form_int = int(self.derham.space_to_form[self.options.u_space]) - if u_space == "Hcurl": + if self.options.u_space == "Hcurl": id_Mn = "M1n" id_X = "X1" - elif u_space == "Hdiv": + elif self.options.u_space == "Hdiv": id_Mn = "M2n" id_X = "X2" - elif u_space == "H1vec": + elif self.options.u_space == "H1vec": id_Mn = "Mvn" id_X = "Xv" - if u_space == "H1vec": - self._space_key_int = 0 - else: - self._space_key_int = int( - self.derham.space_to_form[u_space], - ) + # call operatros + id_M = "M" + self.derham.space_to_form[self.options.u_space] + "n" + _A = getattr(self.mass_ops, id_M) + self._X = getattr(self.basis_ops, id_X) + self._XT = self._X.transpose() + grad = self.derham.grad + gradT = grad.transpose() # Preconditioner - if solver["type"][1] is None: + if self.options.precond is None: pc = None else: - pc_class = getattr(preconditioner, solver["type"][1]) - pc = pc_class(getattr(self.mass_ops, id_Mn)) + pc_class = getattr(preconditioner, self.options.precond) + pc = pc_class(getattr(self.mass_ops, id_M)) # Call the accumulation and Pusher class - if use_perp_model: + if self.options.use_perp_model: accum_ker = Pyccelkernel(accum_kernels.pc_lin_mhd_6d) pusher_ker = Pyccelkernel(pusher_kernels.push_pc_GXu) else: accum_ker = Pyccelkernel(accum_kernels.pc_lin_mhd_6d_full) pusher_ker = Pyccelkernel(pusher_kernels.push_pc_GXu_full) - self._coupling_mat = coupling_params["Ah"] / coupling_params["Ab"] - self._coupling_vec = coupling_params["Ah"] / coupling_params["Ab"] - self._scale_push = 1 - - self._boundary_cut_e1 = boundary_cut["e1"] - + # define Accumulator and arguments self._ACC = Accumulator( - particles, - "Hcurl", + self.variables.energetic_ions.particles, + "Hcurl", # TODO:check accum_ker, self.mass_ops, self.domain.args_domain, add_vector=True, symmetry="pressure", - filter_params=filter, + filter_params=self.options.filter_params, ) - self._tmp_g1 = self._G.codomain.zeros() - self._tmp_g2 = self._G.codomain.zeros() - self._tmp_g3 = self._G.codomain.zeros() + self._tmp_g1 = grad.codomain.zeros() + self._tmp_g2 = grad.codomain.zeros() + self._tmp_g3 = grad.codomain.zeros() # instantiate Pusher - args_kernel = ( + args_pusher_kernel = ( self.derham.args_derham, self._tmp_g1[0]._data, self._tmp_g1[1]._data, @@ -596,38 +688,20 @@ def __init__( self._tmp_g3[0]._data, self._tmp_g3[1]._data, self._tmp_g3[2]._data, - self._boundary_cut_e1, ) self._pusher = Pusher( - particles, + self.variables.energetic_ions.particles, pusher_ker, - args_kernel, + args_pusher_kernel, self.domain.args_domain, alpha_in_kernel=1.0, ) - # Define operators - self._A = getattr(self.mass_ops, id_Mn) - self._X = getattr(self.basis_ops, id_X) - self._XT = self._X.transpose() - - # Instantiate schur solver with dummy BC - self._schur_solver = SchurSolver( - self._A, - self._XT @ self._X, - solver["type"][0], - pc=pc, - tol=solver["tol"], - maxiter=solver["maxiter"], - verbose=solver["verbose"], - recycle=solver["recycle"], - ) - - self.u_temp = u.space.zeros() - self.u_temp2 = u.space.zeros() + self.u_temp = self.variables.u.spline.vector.space.zeros() + self.u_temp2 = self.variables.u.spline.vector.space.zeros() self._tmp = self._X.codomain.zeros() - self._BV = u.space.zeros() + self._BV = self.variables.u.spline.vector.space.zeros() self._MAT = [ [self._ACC.operators[0], self._ACC.operators[1], self._ACC.operators[2]], @@ -637,20 +711,32 @@ def __init__( self._GT_VEC = BlockVector(self.derham.Vh["v"]) + _BC = -1 / 4 * self._XT @ self.GT_MAT_G(self.derham, self._MAT) @ self._X + + self._schur_solver = SchurSolver( + _A, + _BC, + self.options.solver, + precond=pc, + solver_params=self.options.solver_params, + ) + def __call__(self, dt): - # current u - un = self.feec_vars[0] - un.update_ghost_regions() + # current FE coeffs + un = self.variables.u.spline.vector + + # operators + grad = self.derham.grad + gradT = grad.transpose() # acuumulate MAT and VEC - self._ACC(self._coupling_mat, self._coupling_vec, self._boundary_cut_e1) + self._ACC( + self.options.ep_scale, + ) # update GT_VEC for i in range(3): - self._GT_VEC[i] = self._GT.dot(self._ACC.vectors[i]) - - # define BC and B dot V of the Schur block matrix [[A, B], [C, I]] - self._schur_solver.BC = -1 / 4 * self._XT @ self.GT_MAT_G(self.derham, self._MAT) @ self._X + self._GT_VEC[i] = gradT.dot(self._ACC.vectors[i]) self._BV = self._XT.dot(self._GT_VEC) * (-1 / 2) @@ -663,9 +749,9 @@ def __call__(self, dt): # calculate GXu Xu = self._X.dot(_u, out=self._tmp) - GXu_1 = self._G.dot(Xu[0], out=self._tmp_g1) - GXu_2 = self._G.dot(Xu[1], out=self._tmp_g2) - GXu_3 = self._G.dot(Xu[2], out=self._tmp_g3) + GXu_1 = grad.dot(Xu[0], out=self._tmp_g1) + GXu_2 = grad.dot(Xu[1], out=self._tmp_g2) + GXu_3 = grad.dot(Xu[2], out=self._tmp_g3) GXu_1.update_ghost_regions() GXu_2.update_ghost_regions() @@ -675,16 +761,16 @@ def __call__(self, dt): self._pusher(dt) # write new coeffs into Propagator.variables - (max_du,) = self.feec_vars_update(un1) + diffs = self.update_feec_variables(u=un1) # update weights in case of control variate - if self.particles[0].control_variate: - self.particles[0].update_weights() + if self.variables.energetic_ions.species.weights_params.control_variate: + self.variables.energetic_ions.particles.update_weights() - if self._info and self._rank == 0: + if self.options.solver_params.info and MPI.COMM_WORLD.Get_rank() == 0: print("Status for StepPressurecoupling:", info["success"]) print("Iterations for StepPressurecoupling:", info["niter"]) - print("Maxdiff u1 for StepPressurecoupling:", max_du) + print("Maxdiff u1 for StepPressurecoupling:", diffs["u"]) print() class GT_MAT_G(LinOpWithTransp): @@ -703,8 +789,8 @@ class GT_MAT_G(LinOpWithTransp): def __init__(self, derham, MAT, transposed=False): self._derham = derham - self._G = derham.grad - self._GT = derham.grad.transpose() + self._grad = derham.grad + self._gradT = derham.grad.transpose() self._domain = derham.Vh["v"] self._codomain = derham.Vh["v"] @@ -758,9 +844,9 @@ def dot(self, v, out=None): for i in range(3): for j in range(3): - self._temp += self._MAT[i][j].dot(self._G.dot(v[j])) + self._temp += self._MAT[i][j].dot(self._grad.dot(v[j])) - self._vector[i] = self._GT.dot(self._temp) + self._vector[i] = self._gradT.dot(self._temp) self._temp *= 0.0 self._vector.update_ghost_regions() @@ -790,88 +876,107 @@ class CurrentCoupling6DCurrent(Propagator): :ref:`time_discret`: Crank-Nicolson (implicit mid-point). System size reduction via :class:`~struphy.linear_algebra.schur_solver.SchurSolver`. """ - @staticmethod - def options(default=False): - dct = {} - dct["solver"] = { - "type": [ - ("pcg", "MassMatrixPreconditioner"), - ("cg", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - dct["filter"] = { - "use_filter": None, - "modes": (1), - "repeat": 1, - "alpha": 0.5, - } - dct["boundary_cut"] = { - "e1": 0.0, - "e2": 0.0, - "e3": 0.0, - } - dct["turn_off"] = False - - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - particles: Particles6D, - u: BlockVector, - *, - u_space: str, - b_eq: BlockVector | PolarVector, - b_tilde: BlockVector | PolarVector, - Ab: int = 1, - Ah: int = 1, - epsilon: float = 1.0, - solver: dict = options(default=True)["solver"], - filter: dict = options(default=True)["filter"], - boundary_cut: dict = options(default=True)["boundary_cut"], - ): - super().__init__(particles, u) - - if u_space == "H1vec": - self._space_key_int = 0 - else: - self._space_key_int = int( - self.derham.space_to_form[u_space], - ) + class Variables: + def __init__(self): + self._ions: PICVariable = None + self._u: FEECVariable = None + + @property + def ions(self) -> PICVariable: + return self._ions - self._b_eq = b_eq - self._b_tilde = b_tilde + @ions.setter + def ions(self, new): + assert isinstance(new, PICVariable) + assert new.space in ("Particles6D") + self._ions = new - self._info = solver["info"] + @property + def u(self) -> FEECVariable: + return self._u + + @u.setter + def u(self, new): + assert isinstance(new, FEECVariable) + assert new.space in ("Hcurl", "Hdiv", "H1vec") + self._u = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # propagator options + b_tilde: FEECVariable = None + u_space: OptsVecSpace = "Hdiv" + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + filter_params: FilterParameters = None + boundary_cut: tuple = (0.0, 0.0, 0.0) + + def __post_init__(self): + # checks + check_option(self.u_space, OptsVecSpace) + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + assert self.b_tilde.space == "Hdiv" + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + self._space_key_int = int(self.derham.space_to_form[self.options.u_space]) + + particles = self.variables.ions.particles + u = self.variables.u.spline.vector + self._b_eq = self.projected_equil.b2 + self._b_tilde = self.options.b_tilde.spline.vector + + self._info = self.options.solver_params.info if self.derham.comm is None: self._rank = 0 else: self._rank = self.derham.comm.Get_rank() + Ah = self.variables.ions.species.mass_number + Ab = self.variables.u.species.mass_number + epsilon = self.variables.ions.species.equation_params.epsilon + self._coupling_mat = Ah / Ab / epsilon**2 self._coupling_vec = Ah / Ab / epsilon self._scale_push = 1.0 / epsilon - self._boundary_cut_e1 = boundary_cut["e1"] + self._boundary_cut_e1 = self.options.boundary_cut[0] # load accumulator self._accumulator = Accumulator( particles, - u_space, + self.options.u_space, Pyccelkernel(accum_kernels.cc_lin_mhd_6d_2), self.mass_ops, self.domain.args_domain, add_vector=True, symmetry="symm", - filter_params=filter, + filter_params=self.options.filter_params, ) # if self.particles[0].control_variate: @@ -900,17 +1005,17 @@ def __init__( # self.particles[0].f0.n, *quad_pts, kind='0', squeeze_out=False, coordinates='logical') # # memory allocation for magnetic field at quadrature points - # self._b_quad1 = np.zeros_like(self._nuh0_at_quad[0]) - # self._b_quad2 = np.zeros_like(self._nuh0_at_quad[0]) - # self._b_quad3 = np.zeros_like(self._nuh0_at_quad[0]) + # self._b_quad1 = xp.zeros_like(self._nuh0_at_quad[0]) + # self._b_quad2 = xp.zeros_like(self._nuh0_at_quad[0]) + # self._b_quad3 = xp.zeros_like(self._nuh0_at_quad[0]) # # memory allocation for (self._b_quad x self._nuh0_at_quad) * self._coupling_vec - # self._vec1 = np.zeros_like(self._nuh0_at_quad[0]) - # self._vec2 = np.zeros_like(self._nuh0_at_quad[0]) - # self._vec3 = np.zeros_like(self._nuh0_at_quad[0]) + # self._vec1 = xp.zeros_like(self._nuh0_at_quad[0]) + # self._vec2 = xp.zeros_like(self._nuh0_at_quad[0]) + # self._vec3 = xp.zeros_like(self._nuh0_at_quad[0]) # FEM spaces and basis extraction operators for u and b - u_id = self.derham.space_to_form[u_space] + u_id = self.derham.space_to_form[self.options.u_space] self._EuT = self.derham.extraction_ops[u_id].transpose() self._EbT = self.derham.extraction_ops["2"].transpose() @@ -924,15 +1029,15 @@ def __init__( self._u_avg2 = self._EuT.codomain.zeros() # load particle pusher kernel - if u_space == "Hcurl": + if self.options.u_space == "Hcurl": kernel = Pyccelkernel(pusher_kernels.push_bxu_Hcurl) - elif u_space == "Hdiv": + elif self.options.u_space == "Hdiv": kernel = Pyccelkernel(pusher_kernels.push_bxu_Hdiv) - elif u_space == "H1vec": + elif self.options.u_space == "H1vec": kernel = Pyccelkernel(pusher_kernels.push_bxu_H1vec) else: raise ValueError( - f'{u_space = } not valid, choose from "Hcurl", "Hdiv" or "H1vec.', + f'{self.options.u_space = } not valid, choose from "Hcurl", "Hdiv" or "H1vec.', ) # instantiate Pusher @@ -959,10 +1064,10 @@ def __init__( _A = getattr(self.mass_ops, "M" + u_id + "n") # preconditioner - if solver["type"][1] is None: + if self.options.precond is None: pc = None else: - pc_class = getattr(preconditioner, solver["type"][1]) + pc_class = getattr(preconditioner, self.options.precond) pc = pc_class(_A) _BC = -1 / 4 * self._accumulator.operators[0] @@ -970,17 +1075,15 @@ def __init__( self._schur_solver = SchurSolver( _A, _BC, - solver["type"][0], - pc=pc, - tol=solver["tol"], - maxiter=solver["maxiter"], - verbose=solver["verbose"], - recycle=solver["recycle"], + self.options.solver, + precond=pc, + solver_params=self.options.solver_params, ) def __call__(self, dt): # pointer to old coefficients - un = self.feec_vars[0] + particles = self.variables.ions.particles + un = self.variables.u.spline.vector # sum up total magnetic field b_full1 = b_eq + b_tilde (in-place) self._b_eq.copy(out=self._b_full1) @@ -1050,11 +1153,11 @@ def __call__(self, dt): self._pusher(self._scale_push * dt) # write new coeffs into Propagator.variables - max_du = self.feec_vars_update(un1) + max_du = self.update_feec_variables(u=un1) # update weights in case of control variate - if self.particles[0].control_variate: - self.particles[0].update_weights() + if particles.control_variate: + particles.update_weights() if self._info and self._rank == 0: print("Status for CurrentCoupling6DCurrent:", info["success"]) @@ -1099,292 +1202,198 @@ class CurrentCoupling5DCurlb(Propagator): For the detail explanation of the notations, see `2022_DriftKineticCurrentCoupling `_. """ - @staticmethod - def options(default=False): - dct = {} - dct["solver"] = { - "type": [ - ("pcg", "MassMatrixPreconditioner"), - ("cg", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - dct["filter"] = { - "use_filter": None, - "modes": (1), - "repeat": 1, - "alpha": 0.5, - } - dct["boundary_cut"] = { - "e1": 0.0, - "e2": 0.0, - "e3": 0.0, - } - dct["turn_off"] = False - - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - particles: Particles5D, - u: BlockVector, - *, - b: BlockVector, - b_eq: BlockVector, - unit_b1: BlockVector, - absB0: StencilVector, - gradB1: BlockVector, - curl_unit_b2: BlockVector, - u_space: str, - solver: dict = options(default=True)["solver"], - filter: dict = options(default=True)["filter"], - coupling_params: dict, - epsilon: float = 1.0, - boundary_cut: dict = options(default=True)["boundary_cut"], - ): - super().__init__(particles, u) - - assert u_space in {"Hcurl", "Hdiv", "H1vec"} - - if u_space == "H1vec": - self._space_key_int = 0 - else: - self._space_key_int = int( - self.derham.space_to_form[u_space], - ) + class Variables: + def __init__(self): + self._u: FEECVariable = None + self._energetic_ions: PICVariable = None - self._epsilon = epsilon - self._b = b - self._b_eq = b_eq - self._unit_b1 = unit_b1 - self._absB0 = absB0 - self._gradB1 = gradB1 - self._curl_norm_b = curl_unit_b2 + @property + def u(self) -> FEECVariable: + return self._u - self._info = solver["info"] + @u.setter + def u(self, new): + assert isinstance(new, FEECVariable) + assert new.space in ("Hcurl", "Hdiv", "H1vec") + self._u = new - if self.derham.comm is None: - self._rank = 0 + @property + def energetic_ions(self) -> PICVariable: + return self._energetic_ions + + @energetic_ions.setter + def energetic_ions(self, new): + assert isinstance(new, PICVariable) + assert new.space == "Particles5D" + self._energetic_ions = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # propagator options + b_tilde: FEECVariable = None + ep_scale: float = 1.0 + u_space: OptsVecSpace = "Hdiv" + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + filter_params: FilterParameters = None + + def __post_init__(self): + # checks + check_option(self.u_space, OptsVecSpace) + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + assert isinstance(self.b_tilde, FEECVariable) + assert isinstance(self.ep_scale, float) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + if self.filter_params is None: + self.filter_params = FilterParameters() + + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + if self.options.u_space == "H1vec": + self._u_form_int = 0 else: - self._rank = self.derham.comm.Get_rank() - - self._coupling_mat = coupling_params["Ah"] / coupling_params["Ab"] - self._coupling_vec = coupling_params["Ah"] / coupling_params["Ab"] - self._scale_push = 1 + self._u_form_int = int(self.derham.space_to_form[self.options.u_space]) - self._boundary_cut_e1 = boundary_cut["e1"] + # call operatros + id_M = "M" + self.derham.space_to_form[self.options.u_space] + "n" + _A = getattr(self.mass_ops, id_M) - u_id = self.derham.space_to_form[u_space] - self._E0T = self.derham.extraction_ops["0"].transpose() - self._EuT = self.derham.extraction_ops[u_id].transpose() - self._E2T = self.derham.extraction_ops["2"].transpose() - self._E1T = self.derham.extraction_ops["1"].transpose() + # Preconditioner + if self.options.precond is None: + pc = None + else: + pc_class = getattr(preconditioner, self.options.precond) + pc = pc_class(getattr(self.mass_ops, id_M)) - self._unit_b1 = self._E1T.dot(self._unit_b1) - self._curl_norm_b = self._E2T.dot(self._curl_norm_b) - self._curl_norm_b.update_ghost_regions() - self._absB0 = self._E0T.dot(self._absB0) + # magnetic equilibrium field + unit_b1 = self.projected_equil.unit_b1 + curl_unit_b1 = self.projected_equil.curl_unit_b1 + self._b2 = self.projected_equil.b2 - # define system [[A B], [C I]] [u_new, v_new] = [[A -B], [-C I]] [u_old, v_old] (without time step size dt) - _A = getattr(self.mass_ops, "M" + u_id + "n") + # magnetic field + self._b_tilde = self.options.b_tilde.spline.vector - # preconditioner - if solver["type"][1] is None: - pc = None - else: - pc_class = getattr(preconditioner, solver["type"][1]) - pc = pc_class(_A) + # scaling factor + epsilon = self.variables.energetic_ions.species.equation_params.epsilon # temporary vectors to avoid memory allocation - self._b_full1 = self._b_eq.space.zeros() - self._b_full2 = self._E2T.codomain.zeros() - self._u_new = u.space.zeros() - self._u_avg1 = u.space.zeros() - self._u_avg2 = self._EuT.codomain.zeros() + self._b_full = self._b2.space.zeros() + self._u_new = self.variables.u.spline.vector.space.zeros() + self._u_avg = self.variables.u.spline.vector.space.zeros() - # Call the accumulation and Pusher class + # define Accumulator and arguments self._ACC = Accumulator( - particles, - u_space, - Pyccelkernel(accum_kernels_gc.cc_lin_mhd_5d_J1), + self.variables.energetic_ions.particles, + self.options.u_space, + Pyccelkernel(accum_kernels_gc.cc_lin_mhd_5d_curlb), self.mass_ops, self.domain.args_domain, add_vector=True, symmetry="symm", - filter_params=filter, + filter_params=self.options.filter_params, ) - if u_space == "Hcurl": - kernel = Pyccelkernel(pusher_kernels_gc.push_gc_cc_J1_Hcurl) - elif u_space == "Hdiv": - kernel = Pyccelkernel(pusher_kernels_gc.push_gc_cc_J1_Hdiv) - elif u_space == "H1vec": - kernel = Pyccelkernel(pusher_kernels_gc.push_gc_cc_J1_H1vec) + self._args_accum_kernel = ( + epsilon, + self.options.ep_scale, + self._b_full[0]._data, + self._b_full[1]._data, + self._b_full[2]._data, + unit_b1[0]._data, + unit_b1[1]._data, + unit_b1[2]._data, + curl_unit_b1[0]._data, + curl_unit_b1[1]._data, + curl_unit_b1[2]._data, + self._u_form_int, + ) + + # define Pusher + if self.options.u_space == "Hcurl": + pusher_kernel = Pyccelkernel(pusher_kernels_gc.push_gc_cc_J1_Hcurl) + elif self.options.u_space == "Hdiv": + pusher_kernel = Pyccelkernel(pusher_kernels_gc.push_gc_cc_J1_Hdiv) + elif self.options.u_space == "H1vec": + pusher_kernel = Pyccelkernel(pusher_kernels_gc.push_gc_cc_J1_H1vec) else: raise ValueError( - f'{u_space = } not valid, choose from "Hcurl", "Hdiv" or "H1vec.', + f'{self.options.u_space = } not valid, choose from "Hcurl", "Hdiv" or "H1vec.', ) - # instantiate Pusher - args_kernel = ( + args_pusher_kernel = ( self.derham.args_derham, - self._epsilon, - self._b_full2[0]._data, - self._b_full2[1]._data, - self._b_full2[2]._data, - self._unit_b1[0]._data, - self._unit_b1[1]._data, - self._unit_b1[2]._data, - self._curl_norm_b[0]._data, - self._curl_norm_b[1]._data, - self._curl_norm_b[2]._data, - self._u_avg2[0]._data, - self._u_avg2[1]._data, - self._u_avg2[2]._data, - 0.0, + epsilon, + self._b_full[0]._data, + self._b_full[1]._data, + self._b_full[2]._data, + unit_b1[0]._data, + unit_b1[1]._data, + unit_b1[2]._data, + curl_unit_b1[0]._data, + curl_unit_b1[1]._data, + curl_unit_b1[2]._data, + self._u_avg[0]._data, + self._u_avg[1]._data, + self._u_avg[2]._data, ) self._pusher = Pusher( - particles, - kernel, - args_kernel, + self.variables.energetic_ions.particles, + pusher_kernel, + args_pusher_kernel, self.domain.args_domain, alpha_in_kernel=1.0, ) - # define BC and B dot V of the Schur block matrix [[A, B], [C, I]] _BC = -1 / 4 * self._ACC.operators[0] - # call SchurSolver class self._schur_solver = SchurSolver( _A, _BC, - solver["type"][0], - pc=pc, - tol=solver["tol"], - maxiter=solver["maxiter"], - verbose=solver["verbose"], - recycle=solver["recycle"], + self.options.solver, + precond=pc, + solver_params=self.options.solver_params, ) def __call__(self, dt): - un = self.feec_vars[0] + # current FE coeffs + un = self.variables.u.spline.vector # sum up total magnetic field b_full1 = b_eq + b_tilde (in-place) - b_full = self._b_eq.copy(out=self._b_full1) - - if self._b is not None: - self._b_full1 += self._b - - # extract coefficients to tensor product space (in-place) - Eb_full = self._E2T.dot(b_full, out=self._b_full2) - - # update ghost regions because of non-local access in accumulation kernel! - Eb_full.update_ghost_regions() - - # perform accumulation (either with or without control variate) - # if self.particles[0].control_variate: + b_full = self._b2.copy(out=self._b_full) - # # evaluate magnetic field at quadrature points (in-place) - # WeightedMassOperator.eval_quad(self.derham.Vh_fem['2'], self._b_full2, - # out=[self._b_at_quad[0], self._b_at_quad[1], self._b_at_quad[2]]) - - # # evaluate B_parallel - # self._B_para_at_quad = np.sum( - # p * q for p, q in zip(self._unit_b1_at_quad, self._b_at_quad)) - # self._B_para_at_quad += self._unit_b1_dot_curl_norm_b_at_quad - - # # assemble (B x)(curl norm_b)(curl norm_b)(B x) / B_star_para² / det_df³ * (f0.u_para² + f0.vth_para²) * f0.n - # self._mat11[:, :, :] = (self._b_at_quad[1]*self._curl_norm_b_at_quad[2] - - # self._b_at_quad[2]*self._curl_norm_b_at_quad[1])**2 * \ - # self._control_const * self._coupling_mat / \ - # self._det_df_at_quad**3 / self._B_para_at_quad**2 - # self._mat12[:, :, :] = (self._b_at_quad[1]*self._curl_norm_b_at_quad[2] - - # self._b_at_quad[2]*self._curl_norm_b_at_quad[1]) * \ - # (self._b_at_quad[2]*self._curl_norm_b_at_quad[0] - - # self._b_at_quad[0]*self._curl_norm_b_at_quad[2]) * \ - # self._control_const * self._coupling_mat / \ - # self._det_df_at_quad**3 / self._B_para_at_quad**2 - # self._mat13[:, :, :] = (self._b_at_quad[1]*self._curl_norm_b_at_quad[2] - - # self._b_at_quad[2]*self._curl_norm_b_at_quad[1]) * \ - # (self._b_at_quad[0]*self._curl_norm_b_at_quad[1] - - # self._b_at_quad[1]*self._curl_norm_b_at_quad[0]) * \ - # self._control_const * self._coupling_mat / \ - # self._det_df_at_quad**3 / self._B_para_at_quad**2 - # self._mat22[:, :, :] = (self._b_at_quad[2]*self._curl_norm_b_at_quad[0] - - # self._b_at_quad[0]*self._curl_norm_b_at_quad[2])**2 * \ - # self._control_const * self._coupling_mat / \ - # self._det_df_at_quad**3 / self._B_para_at_quad**2 - # self._mat23[:, :, :] = (self._b_at_quad[2]*self._curl_norm_b_at_quad[0] - - # self._b_at_quad[0]*self._curl_norm_b_at_quad[2]) * \ - # (self._b_at_quad[0]*self._curl_norm_b_at_quad[1] - - # self._b_at_quad[1]*self._curl_norm_b_at_quad[0]) * \ - # self._control_const * self._coupling_mat / \ - # self._det_df_at_quad**3 / self._B_para_at_quad**2 - # self._mat33[:, :, :] = (self._b_at_quad[0]*self._curl_norm_b_at_quad[1] - - # self._b_at_quad[1]*self._curl_norm_b_at_quad[0])**2 * \ - # self._control_const * self._coupling_mat / \ - # self._det_df_at_quad**3 / self._B_para_at_quad**2 - - # self._mat21[:, :, :] = -self._mat12 - # self._mat31[:, :, :] = -self._mat13 - # self._mat32[:, :, :] = -self._mat23 - - # # assemble (B x)(curl norm_b) / B_star_para / det_df * (f0.u_para² + f0.vth_para²) * f0.n - # self._vec1[:, :, :] = (self._b_at_quad[1]*self._curl_norm_b_at_quad[2] - - # self._b_at_quad[2]*self._curl_norm_b_at_quad[1]) * \ - # self._control_const * self._coupling_vec / \ - # self._det_df_at_quad / self._B_para_at_quad - # self._vec2[:, :, :] = (self._b_at_quad[2]*self._curl_norm_b_at_quad[0] - - # self._b_at_quad[0]*self._curl_norm_b_at_quad[2]) * \ - # self._control_const * self._coupling_vec / \ - # self._det_df_at_quad / self._B_para_at_quad - # self._vec3[:, :, :] = (self._b_at_quad[0]*self._curl_norm_b_at_quad[1] - - # self._b_at_quad[1]*self._curl_norm_b_at_quad[0]) * \ - # self._control_const * self._coupling_vec / \ - # self._det_df_at_quad / self._B_para_at_quad - - # self._ACC.accumulate(self.particles[0], self._epsilon, - # Eb_full[0]._data, Eb_full[1]._data, Eb_full[2]._data, - # self._unit_b1[0]._data, self._unit_b1[1]._data, self._unit_b1[2]._data, - # self._curl_norm_b[0]._data, self._curl_norm_b[1]._data, self._curl_norm_b[2]._data, - # self._space_key_int, self._coupling_mat, self._coupling_vec, 0.1, - # control_mat=[[None, self._mat12, self._mat13], - # [self._mat21, None, self._mat23], - # [self._mat31, self._mat32, None]], - # control_vec=[self._vec1, self._vec2, self._vec3]) - # else: - # self._ACC.accumulate(self.particles[0], self._epsilon, - # Eb_full[0]._data, Eb_full[1]._data, Eb_full[2]._data, - # self._unit_b1[0]._data, self._unit_b1[1]._data, self._unit_b1[2]._data, - # self._curl_norm_b[0]._data, self._curl_norm_b[1]._data, self._curl_norm_b[2]._data, - # self._space_key_int, self._coupling_mat, self._coupling_vec, 0.1) + b_full += self._b_tilde + b_full.update_ghost_regions() self._ACC( - self._epsilon, - Eb_full[0]._data, - Eb_full[1]._data, - Eb_full[2]._data, - self._unit_b1[0]._data, - self._unit_b1[1]._data, - self._unit_b1[2]._data, - self._curl_norm_b[0]._data, - self._curl_norm_b[1]._data, - self._curl_norm_b[2]._data, - self._space_key_int, - self._coupling_mat, - self._coupling_vec, - self._boundary_cut_e1, + *self._args_accum_kernel, ) - # update u coefficients + # solve un1, info = self._schur_solver( un, -self._ACC.vectors[0] / 2, @@ -1393,27 +1402,25 @@ def __call__(self, dt): ) # call pusher kernel with average field (u_new + u_old)/2 and update ghost regions because of non-local access in kernel - _u = un.copy(out=self._u_avg1) + _u = un.copy(out=self._u_avg) _u += un1 _u *= 0.5 - _Eu = self._EuT.dot(_u, out=self._u_avg2) - - _Eu.update_ghost_regions() + _u.update_ghost_regions() - self._pusher(self._scale_push * dt) + self._pusher(dt) - # write new coeffs into Propagator.variables - (max_du,) = self.feec_vars_update(un1) + # update u coefficients + diffs = self.update_feec_variables(u=un1) # update_weights - if self.particles[0].control_variate: - self.particles[0].update_weights() + if self.variables.energetic_ions.species.weights_params.control_variate: + self.variables.energetic_ions.particles.update_weights() - if self._info and self._rank == 0: + if self.options.solver_params.info and MPI.COMM_WORLD.Get_rank() == 0: print("Status for CurrentCoupling5DCurlb:", info["success"]) print("Iterations for CurrentCoupling5DCurlb:", info["niter"]) - print("Maxdiff up for CurrentCoupling5DCurlb:", max_du) + print("Maxdiff up for CurrentCoupling5DCurlb:", diffs["u"]) print() @@ -1453,440 +1460,722 @@ class CurrentCoupling5DGradB(Propagator): For the detail explanation of the notations, see `2022_DriftKineticCurrentCoupling `_. """ - @staticmethod - def options(default=False): - dct = {} - dct["solver"] = { - "type": [ - ("pcg", "MassMatrixPreconditioner"), - ("cg", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - dct["algo"] = ["rk4", "forward_euler", "heun2", "rk2", "heun3"] - dct["filter"] = { - "use_filter": None, - "modes": (1), - "repeat": 1, - "alpha": 0.5, - } - dct["boundary_cut"] = { - "e1": 0.0, - "e2": 0.0, - "e3": 0.0, - } - dct["turn_off"] = False - - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - particles: Particles5D, - u: BlockVector, - *, - b: BlockVector, - b_eq: BlockVector, - unit_b1: BlockVector, - unit_b2: BlockVector, - absB0: StencilVector, - gradB1: BlockVector, - curl_unit_b2: BlockVector, - u_space: str, - solver: dict = options(default=True)["solver"], - algo: dict = options(default=True)["algo"], - filter: dict = options(default=True)["filter"], - coupling_params: dict, - epsilon: float = 1.0, - boundary_cut: dict = options(default=True)["boundary_cut"], - ): - from psydac.linalg.solvers import inverse - - from struphy.ode.utils import ButcherTableau - - super().__init__(particles, u) - - assert u_space in {"Hcurl", "Hdiv", "H1vec"} - - if u_space == "H1vec": - self._space_key_int = 0 - else: - self._space_key_int = int( - self.derham.space_to_form[u_space], - ) + class Variables: + def __init__(self): + self._u: FEECVariable = None + self._energetic_ions: PICVariable = None - self._epsilon = epsilon - self._b = b - self._b_eq = b_eq - self._unit_b1 = unit_b1 - self._unit_b2 = unit_b2 - self._absB0 = absB0 - self._gradB1 = gradB1 - self._curl_norm_b = curl_unit_b2 + @property + def u(self) -> FEECVariable: + return self._u - self._info = solver["info"] + @u.setter + def u(self, new): + assert isinstance(new, FEECVariable) + assert new.space in ("Hcurl", "Hdiv", "H1vec") + self._u = new - if self.derham.comm is None: - self._rank = 0 + @property + def energetic_ions(self) -> PICVariable: + return self._energetic_ions + + @energetic_ions.setter + def energetic_ions(self, new): + assert isinstance(new, PICVariable) + assert new.space == "Particles5D" + self._energetic_ions = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # specific literals + OptsAlgo = Literal[ + "discrete_gradient", + "explicit", + ] + # propagator options + b_tilde: FEECVariable = None + ep_scale: float = 1.0 + algo: OptsAlgo = "explicit" + butcher: ButcherTableau = None + u_space: OptsVecSpace = "Hdiv" + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + filter_params: FilterParameters = None + dg_solver_params: DiscreteGradientSolverParameters = None + + def __post_init__(self): + # checks + check_option(self.algo, self.OptsAlgo) + check_option(self.u_space, OptsVecSpace) + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + assert isinstance(self.b_tilde, FEECVariable) + assert isinstance(self.ep_scale, float) + + # defaults + if self.algo == "explicit" and self.butcher is None: + self.butcher = ButcherTableau() + + if self.algo == "discrete_gradient" and self.dg_solver_params is None: + self.dg_solver_params = DiscreteGradientSolverParameters() + + if self.solver_params is None: + self.solver_params = SolverParameters() + + if self.filter_params is None: + self.filter_params = FilterParameters() + + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + if self.options.u_space == "H1vec": + self._u_form_int = 0 else: - self._rank = self.derham.comm.Get_rank() + self._u_form_int = int(self.derham.space_to_form[self.options.u_space]) - self._coupling_mat = coupling_params["Ah"] / coupling_params["Ab"] - self._coupling_vec = coupling_params["Ah"] / coupling_params["Ab"] - self._scale_push = 1 + # call operatros + id_M = "M" + self.derham.space_to_form[self.options.u_space] + "n" + self._A = getattr(self.mass_ops, id_M) + self._PB = getattr(self.basis_ops, "PB") - self._boundary_cut_e1 = boundary_cut["e1"] + # Preconditioner + if self.options.precond is None: + pc = None + else: + pc_class = getattr(preconditioner, self.options.precond) + pc = pc_class(getattr(self.mass_ops, id_M)) - u_id = self.derham.space_to_form[u_space] - self._E0T = self.derham.extraction_ops["0"].transpose() - self._EuT = self.derham.extraction_ops[u_id].transpose() - self._E1T = self.derham.extraction_ops["1"].transpose() - self._E2T = self.derham.extraction_ops["2"].transpose() + # linear solver + self._A_inv = inverse( + self._A, + self.options.solver, + pc=pc, + tol=self.options.solver_params.tol, + maxiter=self.options.solver_params.maxiter, + verbose=self.options.solver_params.verbose, + ) + # magnetic equilibrium field + unit_b1 = self.projected_equil.unit_b1 + curl_unit_b1 = self.projected_equil.curl_unit_b1 + self._b2 = self.projected_equil.b2 + gradB1 = self.projected_equil.gradB1 + absB0 = self.projected_equil.absB0 + + # magnetic field + self._b_tilde = self.options.b_tilde.spline.vector + + # scaling factor + epsilon = self.variables.energetic_ions.species.equation_params.epsilon + + if self.options.algo == "explicit": + # temporary vectors to avoid memory allocation + self._b_full = self._b2.space.zeros() + self._u_new = self.variables.u.spline.vector.space.zeros() + self._u_temp = self.variables.u.spline.vector.space.zeros() + self._ku = self.variables.u.spline.vector.space.zeros() + self._PB_b = self._PB.codomain.zeros() + self._grad_PB_b = self.derham.grad.codomain.zeros() + + # define Accumulator and arguments + self._ACC = Accumulator( + self.variables.energetic_ions.particles, + self.options.u_space, + Pyccelkernel(accum_kernels_gc.cc_lin_mhd_5d_gradB), + self.mass_ops, + self.domain.args_domain, + add_vector=True, + symmetry="symm", + filter_params=self.options.filter_params, + ) - self._PB = getattr(self.basis_ops, "PB") + self._args_accum_kernel = ( + epsilon, + self.options.ep_scale, + self._b_full[0]._data, + self._b_full[1]._data, + self._b_full[2]._data, + unit_b1[0]._data, + unit_b1[1]._data, + unit_b1[2]._data, + curl_unit_b1[0]._data, + curl_unit_b1[1]._data, + curl_unit_b1[2]._data, + self._grad_PB_b[0]._data, + self._grad_PB_b[1]._data, + self._grad_PB_b[2]._data, + self._u_form_int, + ) - self._unit_b1 = self._E1T.dot(self._unit_b1) - self._unit_b2 = self._E2T.dot(self._unit_b2) - self._curl_norm_b = self._E2T.dot(self._curl_norm_b) - self._absB0 = self._E0T.dot(self._absB0) + # define Pusher + if self.options.u_space == "Hdiv": + self._pusher_kernel = pusher_kernels_gc.push_gc_cc_J2_stage_Hdiv + elif self.options.u_space == "H1vec": + self._pusher_kernel = pusher_kernels_gc.push_gc_cc_J2_stage_H1vec + else: + raise ValueError( + f'{self.options.u_space = } not valid, choose from "Hdiv" or "H1vec.', + ) - _A = getattr(self.mass_ops, "M" + u_id + "n") + # temp fix due to refactoring of ButcherTableau: + butcher = self.options.butcher + import numpy as np + + butcher._a = xp.diag(butcher.a, k=-1) + butcher._a = xp.array(list(butcher.a) + [0.0]) + + self._args_pusher_kernel = ( + self.domain.args_domain, + self.derham.args_derham, + epsilon, + self._b_full[0]._data, + self._b_full[1]._data, + self._b_full[2]._data, + unit_b1[0]._data, + unit_b1[1]._data, + unit_b1[2]._data, + curl_unit_b1[0]._data, + curl_unit_b1[1]._data, + curl_unit_b1[2]._data, + self._u_temp[0]._data, + self._u_temp[1]._data, + self._u_temp[2]._data, + self.options.butcher.a, + self.options.butcher.b, + self.options.butcher.c, + ) - # preconditioner - if solver["type"][1] is None: - pc = None else: - pc_class = getattr(preconditioner, solver["type"][1]) - pc = pc_class(_A) + # temporary vectors to avoid memory allocation + self._b_full = self._b2.space.zeros() + self._PB_b = self._PB.codomain.zeros() + self._grad_PB_b = self.derham.grad.codomain.zeros() + self._u_old = self.variables.u.spline.vector.space.zeros() + self._u_new = self.variables.u.spline.vector.space.zeros() + self._u_diff = self.variables.u.spline.vector.space.zeros() + self._u_mid = self.variables.u.spline.vector.space.zeros() + self._M2n_dot_u = self.variables.u.spline.vector.space.zeros() + self._ku = self.variables.u.spline.vector.space.zeros() + self._u_temp = self.variables.u.spline.vector.space.zeros() + + # Call the accumulation and Pusher class + accum_kernel_init = accum_kernels_gc.cc_lin_mhd_5d_gradB_dg_init + accum_kernel = accum_kernels_gc.cc_lin_mhd_5d_gradB_dg + self._accum_kernel_en_fB_mid = utilities_kernels.eval_gradB_ediff + + self._args_accum_kernel = ( + epsilon, + self.options.ep_scale, + self._b_tilde[0]._data, + self._b_tilde[1]._data, + self._b_tilde[2]._data, + self._b2[0]._data, + self._b2[1]._data, + self._b2[2]._data, + unit_b1[0]._data, + unit_b1[1]._data, + unit_b1[2]._data, + curl_unit_b1[0]._data, + curl_unit_b1[1]._data, + curl_unit_b1[2]._data, + self._grad_PB_b[0]._data, + self._grad_PB_b[1]._data, + self._grad_PB_b[2]._data, + gradB1[0]._data, + gradB1[1]._data, + gradB1[2]._data, + self._u_form_int, + ) - self._solver = inverse( - _A, - solver["type"][0], - pc=pc, - tol=solver["tol"], - maxiter=solver["maxiter"], - verbose=solver["verbose"], - recycle=solver["recycle"], - ) + self._args_accum_kernel_en_fB_mid = ( + self.domain.args_domain, + self.derham.args_derham, + gradB1[0]._data, + gradB1[1]._data, + gradB1[2]._data, + self._grad_PB_b[0]._data, + self._grad_PB_b[1]._data, + self._grad_PB_b[2]._data, + ) - # Call the accumulation and Pusher class - self._ACC = Accumulator( - particles, - u_space, - Pyccelkernel(accum_kernels_gc.cc_lin_mhd_5d_J2), - self.mass_ops, - self.domain.args_domain, - add_vector=True, - symmetry="symm", - filter_params=filter, - ) + self._ACC_init = AccumulatorVector( + self.variables.energetic_ions.particles, + self.options.u_space, + accum_kernel_init, + self.mass_ops, + self.domain.args_domain, + filter_params=self.options.filter_params, + ) - # if self.particles[0].control_variate: + self._ACC = AccumulatorVector( + self.variables.energetic_ions.particles, + self.options.u_space, + accum_kernel, + self.mass_ops, + self.domain.args_domain, + filter_params=self.options.filter_params, + ) - # # control variate method is only valid with Maxwellian distributions - # assert isinstance(self.particles[0].f0, Maxwellian) - # assert params['u_space'] == 'Hdiv' + self._args_pusher_kernel_init = ( + self.domain.args_domain, + self.derham.args_derham, + epsilon, + self._b_full[0]._data, + self._b_full[1]._data, + self._b_full[2]._data, + unit_b1[0]._data, + unit_b1[1]._data, + unit_b1[2]._data, + curl_unit_b1[0]._data, + curl_unit_b1[1]._data, + curl_unit_b1[2]._data, + self.variables.u.spline.vector[0]._data, + self.variables.u.spline.vector[1]._data, + self.variables.u.spline.vector[2]._data, + ) - # self._ACC.init_control_variate(self.mass_ops) + self._args_pusher_kernel = ( + self.domain.args_domain, + self.derham.args_derham, + epsilon, + self._b_full[0]._data, + self._b_full[1]._data, + self._b_full[2]._data, + unit_b1[0]._data, + unit_b1[1]._data, + unit_b1[2]._data, + curl_unit_b1[0]._data, + curl_unit_b1[1]._data, + curl_unit_b1[2]._data, + self._u_mid[0]._data, + self._u_mid[1]._data, + self._u_mid[2]._data, + self._u_temp[0]._data, + self._u_temp[1]._data, + self._u_temp[2]._data, + ) - # # evaluate and save n0 at quadrature points - # quad_pts = [quad_grid[nquad].points.flatten() - # for quad_grid, nquad in zip(self.derham.get_quad_grids(self.derham.Vh_fem['0']), self.derham.nquads)] + self._pusher_kernel_init = pusher_kernels_gc.push_gc_cc_J2_dg_init_Hdiv + self._pusher_kernel = pusher_kernels_gc.push_gc_cc_J2_dg_Hdiv - # self._n0_at_quad = self.domain.push( - # self.particles[0].f0.n, *quad_pts, kind='0', squeeze_out=False) + def __call__(self, dt): + # current FE coeffs + un = self.variables.u.spline.vector - # # evaluate unit_b1 (1form) dot epsilon * u0_parallel * curl_norm_b/|det(DF)| at quadrature points - # quad_pts_array = self.domain.prepare_eval_pts(*quad_pts)[:3] + # particle markers and idx + particles = self.variables.energetic_ions.particles + holes = particles.holes + args_markers = particles.args_markers + markers = args_markers.markers + first_init_idx = args_markers.first_init_idx + first_free_idx = args_markers.first_free_idx - # u0_parallel_at_quad = self.particles[0].f0.u( - # *quad_pts_array)[0] + # clear buffer + markers[:, first_init_idx:-2] = 0.0 - # vth_perp = self.particles[0].f0.vth(*quad_pts_array)[1] + # save old marker positions + markers[:, first_init_idx : first_init_idx + 3] = markers[:, :3] - # absB0_at_quad = WeightedMassOperator.eval_quad( - # self.derham.Vh_fem['0'], self._absB0) + # sum up total magnetic field b_full1 = b_eq + b_tilde (in-place) + b_full = self._b2.copy(out=self._b_full) - # self._det_df_at_quad = self.domain.jacobian_det( - # *quad_pts, squeeze_out=False) + b_full += self._b_tilde + b_full.update_ghost_regions() - # self._unit_b1_at_quad = WeightedMassOperator.eval_quad( - # self.derham.Vh_fem['1'], self._unit_b1) + if self.options.algo == "explicit": + PB_b = self._PB.dot(b_full, out=self._PB_b) + grad_PB_b = self.derham.grad.dot(PB_b, out=self._grad_PB_b) + grad_PB_b.update_ghost_regions() - # curl_norm_b_at_quad = WeightedMassOperator.eval_quad( - # self.derham.Vh_fem['2'], self._curl_norm_b) + # save old u + u_new = un.copy(out=self._u_new) - # self._unit_b1_dot_curl_norm_b_at_quad = np.sum( - # p * q for p, q in zip(self._unit_b1_at_quad, curl_norm_b_at_quad)) + for stage in range(self.options.butcher.n_stages): + # accumulate + self._ACC( + *self._args_accum_kernel, + ) - # self._unit_b1_dot_curl_norm_b_at_quad /= self._det_df_at_quad - # self._unit_b1_dot_curl_norm_b_at_quad *= self._epsilon - # self._unit_b1_dot_curl_norm_b_at_quad *= u0_parallel_at_quad + # push particles + self._pusher_kernel( + dt, + stage, + args_markers, + *self._args_pusher_kernel, + ) - # # precalculate constant 2 * f0.vth_perp² / B0 * f0.n for control MAT and VEC - # self._control_const = vth_perp**2 / absB0_at_quad * self._n0_at_quad + if particles.mpi_comm is not None: + particles.mpi_sort_markers() + else: + particles.apply_kinetic_bc() - # # assemble the matrix (G_inv)(unit_b1 x)(G_inv) - # G_inv_at_quad = self.domain.metric_inv( - # *quad_pts, squeeze_out=False) + # solve linear system for updating u coefficients + ku = self._A_inv.dot(self._ACC.vectors[0], out=self._ku) + info = self._A_inv._info - # self._G_inv_bx_G_inv_at_quad = [[np.zeros_like(self._n0_at_quad), np.zeros_like(self._n0_at_quad), np.zeros_like(self._n0_at_quad)], - # [np.zeros_like(self._n0_at_quad), np.zeros_like( - # self._n0_at_quad), np.zeros_like(self._n0_at_quad)], - # [np.zeros_like(self._n0_at_quad), np.zeros_like(self._n0_at_quad), np.zeros_like(self._n0_at_quad)]] + # calculate u^{n+1}_k + u_temp = un.copy(out=self._u_temp) + u_temp += ku * dt * self.options.butcher.a[stage] - # for j in range(3): - # temp = (-self._unit_b1_at_quad[2]*G_inv_at_quad[1, j] + self._unit_b1_at_quad[1]*G_inv_at_quad[2, j], - # self._unit_b1_at_quad[2]*G_inv_at_quad[0, j] - - # self._unit_b1_at_quad[0]*G_inv_at_quad[2, j], - # -self._unit_b1_at_quad[1]*G_inv_at_quad[0, j] + self._unit_b1_at_quad[0]*G_inv_at_quad[1, j]) + u_temp.update_ghost_regions() - # for i in range(3): - # self._G_inv_bx_G_inv_at_quad[i][j] = np.sum( - # p * q for p, q in zip(G_inv_at_quad[i], temp[:])) + # calculate u^{n+1} + u_new += ku * dt * self.options.butcher.b[stage] - # # memory allocation of magnetic field at quadrature points - # self._b_at_quad = [np.zeros_like(self._n0_at_quad), - # np.zeros_like(self._n0_at_quad), - # np.zeros_like(self._n0_at_quad)] + if self.options.solver_params.info and MPI.COMM_WORLD.Get_rank() == 0: + print("Stage: ", stage) + print("Status for CurrentCoupling5DGradB:", info["success"]) + print("Iterations for CurrentCoupling5DGradB:", info["niter"]) + print() - # # memory allocation of parallel magnetic field at quadrature points - # self._B_para_at_quad = np.zeros_like(self._n0_at_quad) + # update u coefficients + diffs = self.update_feec_variables(u=u_new) - # # memory allocation of gradient of parallel magnetic field at quadrature points - # self._grad_PBb_at_quad = (np.zeros_like(self._n0_at_quad), - # np.zeros_like(self._n0_at_quad), - # np.zeros_like(self._n0_at_quad)) - # # memory allocation for temporary matrix - # self._temp = [[np.zeros_like(self._n0_at_quad), np.zeros_like(self._n0_at_quad), np.zeros_like(self._n0_at_quad)], - # [np.zeros_like(self._n0_at_quad), np.zeros_like( - # self._n0_at_quad), np.zeros_like(self._n0_at_quad)], - # [np.zeros_like(self._n0_at_quad), np.zeros_like(self._n0_at_quad), np.zeros_like(self._n0_at_quad)]] + # clear the buffer + markers[:, first_init_idx:-2] = 0.0 - # # memory allocation for control VEC - # self._vec1 = np.zeros_like(self._n0_at_quad) - # self._vec2 = np.zeros_like(self._n0_at_quad) - # self._vec3 = np.zeros_like(self._n0_at_quad) + # update_weights + if self.variables.energetic_ions.species.weights_params.control_variate: + particles.update_weights() - # choose algorithm - self._butcher = ButcherTableau(algo) - # temp fix due to refactoring of ButcherTableau: - self._butcher._a = np.diag(self._butcher.a, k=-1) - self._butcher._a = np.array(list(self._butcher.a) + [0.0]) + if self.options.solver_params.info and MPI.COMM_WORLD.Get_rank() == 0: + print("Maxdiff up for CurrentCoupling5DGradB:", diffs["u"]) + print() - # instantiate Pusher - if u_space == "Hdiv": - kernel = Pyccelkernel(pusher_kernels_gc.push_gc_cc_J2_stage_Hdiv) - elif u_space == "H1vec": - kernel = Pyccelkernel(pusher_kernels_gc.push_gc_cc_J2_stage_H1vec) else: - raise ValueError( - f'{u_space = } not valid, choose from "Hdiv" or "H1vec.', + # total number of markers + n_mks_tot = particles.Np + + # relaxation factor + alpha = self.options.dg_solver_params.relaxation_factor + + # eval parallel tilde b and its gradient + PB_b = self._PB.dot(self._b_tilde, out=self._PB_b) + PB_b.update_ghost_regions() + grad_PB_b = self.derham.grad.dot(PB_b, out=self._grad_PB_b) + grad_PB_b.update_ghost_regions() + + # save old u + u_old = un.copy(out=self._u_old) + u_new = un.copy(out=self._u_new) + + # save en_U_old + self._A.dot(un, out=self._M2n_dot_u) + en_U_old = un.inner(self._M2n_dot_u) / 2.0 + + # save en_fB_old + particles.save_magnetic_energy(PB_b) + en_fB_old = xp.sum(markers[~holes, 8].dot(markers[~holes, 5])) * self.options.ep_scale + en_fB_old /= n_mks_tot + + buffer_array = xp.array([en_fB_old]) + + if particles.mpi_comm is not None: + particles.mpi_comm.Allreduce( + MPI.IN_PLACE, + buffer_array, + op=MPI.SUM, + ) + + if particles.clone_config is not None: + particles.clone_config.inter_comm.Allreduce( + MPI.IN_PLACE, + buffer_array, + op=MPI.SUM, + ) + + en_fB_old = buffer_array[0] + en_tot_old = en_U_old + en_fB_old + + # initial guess + self._ACC_init(*self._args_accum_kernel) + + ku = self._A_inv.dot(self._ACC_init.vectors[0], out=self._ku) + u_new += ku * dt + + u_new.update_ghost_regions() + + # save en_U_new + self._A.dot(u_new, out=self._M2n_dot_u) + en_U_new = u_new.inner(self._M2n_dot_u) / 2.0 + + # push eta + self._pusher_kernel_init( + dt, + args_markers, + *self._args_pusher_kernel_init, ) - args_kernel = (self.derham.args_derham,) + if particles.mpi_comm is not None: + particles.mpi_sort_markers(apply_bc=False) - self._pusher = Pusher( - particles, - kernel, - args_kernel, - self.domain.args_domain, - alpha_in_kernel=1.0, - ) + # save en_fB_new + particles.save_magnetic_energy(PB_b) + en_fB_new = xp.sum(markers[~holes, 8].dot(markers[~holes, 5])) * self.options.ep_scale + en_fB_new /= n_mks_tot - # temporary vectors to avoid memory allocation - self._b_full1 = self._b_eq.space.zeros() - self._b_full2 = self._E2T.codomain.zeros() - self._u_new = u.space.zeros() - self._Eu_new = self._EuT.codomain.zeros() - self._u_temp1 = u.space.zeros() - self._u_temp2 = u.space.zeros() - self._Eu_temp = self._EuT.codomain.zeros() - self._tmp1 = self._E0T.codomain.zeros() - self._tmp2 = self._gradB1.space.zeros() - self._tmp3 = self._E1T.codomain.zeros() + buffer_array = xp.array([en_fB_new]) - def __call__(self, dt): - un = self.feec_vars[0] + if particles.mpi_comm is not None: + particles.mpi_comm.Allreduce( + MPI.IN_PLACE, + buffer_array, + op=MPI.SUM, + ) - # sum up total magnetic field b_full1 = b_eq + b_tilde (in-place) - b_full = self._b_eq.copy(out=self._b_full1) + if particles.clone_config is not None: + particles.clone_config.inter_comm.Allreduce( + MPI.IN_PLACE, + buffer_array, + op=MPI.SUM, + ) - if self._b is not None: - self._b_full1 += self._b + en_fB_new = buffer_array[0] - PBb = self._PB.dot(self._b, out=self._tmp1) - grad_PBb = self.derham.grad.dot(PBb, out=self._tmp2) - grad_PBb += self._gradB1 + # fixed-point iterations + iter_num = 0 - Eb_full = self._E2T.dot(b_full, out=self._b_full2) - Eb_full.update_ghost_regions() + while True: + iter_num += 1 - Egrad_PBb = self._E1T.dot(grad_PBb, out=self._tmp3) - Egrad_PBb.update_ghost_regions() + if self.options.dg_solver_params.verbose and MPI.COMM_WORLD.Get_rank() == 0: + print("# of iteration: ", iter_num) - # perform accumulation (either with or without control variate) - # if self.particles[0].control_variate: + # calculate discrete gradient + # save u^{n+1, k} + u_old = u_new.copy(out=self._u_old) - # # evaluate magnetic field at quadrature points (in-place) - # WeightedMassOperator.eval_quad(self.derham.Vh_fem['2'], self._b_full2, - # out=[self._b_at_quad[0], self._b_at_quad[1], self._b_at_quad[2]]) - - # # evaluate B_parallel - # self._B_para_at_quad = np.sum( - # p * q for p, q in zip(self._unit_b1_at_quad, self._b_at_quad)) - # self._B_para_at_quad += self._unit_b1_dot_curl_norm_b_at_quad - - # # evaluate grad B_parallel - # WeightedMassOperator.eval_quad(self.derham.Vh_fem['1'], self._tmp3, - # out=[self._grad_PBb_at_quad[0], self._grad_PBb_at_quad[1], self._grad_PBb_at_quad[2]]) - - # # assemble temp = (B x)(G_inv)(unit_b1 x)(G_inv) - # for i in range(3): - # self._temp[0][i] = -self._b_at_quad[2]*self._G_inv_bx_G_inv_at_quad[1][i] + \ - # self._b_at_quad[1]*self._G_inv_bx_G_inv_at_quad[2][i] - # self._temp[1][i] = +self._b_at_quad[2]*self._G_inv_bx_G_inv_at_quad[0][i] - \ - # self._b_at_quad[0]*self._G_inv_bx_G_inv_at_quad[2][i] - # self._temp[2][i] = -self._b_at_quad[1]*self._G_inv_bx_G_inv_at_quad[0][i] + \ - # self._b_at_quad[0]*self._G_inv_bx_G_inv_at_quad[1][i] - - # # assemble (temp)(grad B_parallel) / B_star_para * 2 * f0.vth_perp² / B0 * f0.n - # self._vec1[:, :, :] = np.sum(p * q for p, q in zip(self._temp[0][:], self._grad_PBb_at_quad)) * \ - # self._control_const * self._coupling_vec / self._B_para_at_quad - # self._vec2[:, :, :] = np.sum(p * q for p, q in zip(self._temp[1][:], self._grad_PBb_at_quad)) * \ - # self._control_const * self._coupling_vec / self._B_para_at_quad - # self._vec3[:, :, :] = np.sum(p * q for p, q in zip(self._temp[2][:], self._grad_PBb_at_quad)) * \ - # self._control_const * self._coupling_vec / self._B_para_at_quad - - # save old u - _u_new = un.copy(out=self._u_new) - _u_temp = un.copy(out=self._u_temp1) + u_diff = u_old.copy(out=self._u_diff) + u_diff -= un + u_diff.update_ghost_regions() - # save old marker positions - self.particles[0].markers[ - ~self.particles[0].holes, - 11:14, - ] = self.particles[0].markers[~self.particles[0].holes, 0:3] - - for stage in range(self._butcher.n_stages): - # accumulate RHS - # if self.particles[0].control_variate: - # self._ACC.accumulate(self.particles[0], self._epsilon, - # Eb_full[0]._data, Eb_full[1]._data, Eb_full[2]._data, - # self._unit_b1[0]._data, self._unit_b1[1]._data, self._unit_b1[2]._data, - # self._unit_b2[0]._data, self._unit_b2[1]._data, self._unit_b2[2]._data, - # self._curl_norm_b[0]._data, self._curl_norm_b[1]._data, self._curl_norm_b[2]._data, - # Egrad_PBb[0]._data, Egrad_PBb[1]._data, Egrad_PBb[2]._data, - # self._space_key_int, self._coupling_mat, self._coupling_vec, 0., - # control_vec=[self._vec1, self._vec2, self._vec3]) - # else: - # self._ACC.accumulate(self.particles[0], self._epsilon, - # Eb_full[0]._data, Eb_full[1]._data, Eb_full[2]._data, - # self._unit_b1[0]._data, self._unit_b1[1]._data, self._unit_b1[2]._data, - # self._unit_b2[0]._data, self._unit_b2[1]._data, self._unit_b2[2]._data, - # self._curl_norm_b[0]._data, self._curl_norm_b[1]._data, self._curl_norm_b[2]._data, - # Egrad_PBb[0]._data, Egrad_PBb[1]._data, Egrad_PBb[2]._data, - # self._space_key_int, self._coupling_mat, self._coupling_vec, 0.) - - self._ACC( - self._epsilon, - Eb_full[0]._data, - Eb_full[1]._data, - Eb_full[2]._data, - self._unit_b1[0]._data, - self._unit_b1[1]._data, - self._unit_b1[2]._data, - self._unit_b2[0]._data, - self._unit_b2[1]._data, - self._unit_b2[2]._data, - self._curl_norm_b[0]._data, - self._curl_norm_b[1]._data, - self._curl_norm_b[2]._data, - Egrad_PBb[0]._data, - Egrad_PBb[1]._data, - Egrad_PBb[2]._data, - self._space_key_int, - self._coupling_mat, - self._coupling_vec, - self._boundary_cut_e1, - ) + u_mid = u_old.copy(out=self._u_mid) + u_mid += un + u_mid /= 2.0 + u_mid.update_ghost_regions() - # push particles - Eu = self._EuT.dot(_u_temp, out=self._Eu_temp) - Eu.update_ghost_regions() + # save H^{n+1, k} + markers[~holes, first_free_idx : first_free_idx + 3] = markers[~holes, 0:3] - self._pusher.kernel( - dt, - stage, - self.particles[0].args_markers, - self.domain.args_domain, - self.derham.args_derham, - self._epsilon, - Eb_full[0]._data, - Eb_full[1]._data, - Eb_full[2]._data, - self._unit_b1[0]._data, - self._unit_b1[1]._data, - self._unit_b1[2]._data, - self._unit_b2[0]._data, - self._unit_b2[1]._data, - self._unit_b2[2]._data, - self._curl_norm_b[0]._data, - self._curl_norm_b[1]._data, - self._curl_norm_b[2]._data, - Eu[0]._data, - Eu[1]._data, - Eu[2]._data, - self._butcher.a, - self._butcher.b, - self._butcher.c, - self._boundary_cut_e1, - ) + # calculate denominator ||z^{n+1, k} - z^n||^2 + sum_u_diff_loc = xp.sum((u_diff.toarray() ** 2)) + + sum_H_diff_loc = xp.sum( + (markers[~holes, :3] - markers[~holes, first_init_idx : first_init_idx + 3]) ** 2 + ) + + buffer_array = xp.array([sum_u_diff_loc]) + + if particles.mpi_comm is not None: + particles.mpi_comm.Allreduce( + MPI.IN_PLACE, + buffer_array, + op=MPI.SUM, + ) + + denominator = buffer_array[0] + + buffer_array = xp.array([sum_H_diff_loc]) + + if particles.mpi_comm is not None: + particles.mpi_comm.Allreduce( + MPI.IN_PLACE, + buffer_array, + op=MPI.SUM, + ) + + if particles.clone_config is not None: + particles.clone_config.inter_comm.Allreduce( + MPI.IN_PLACE, + buffer_array, + op=MPI.SUM, + ) + + denominator += buffer_array[0] + + # sorting markers at mid-point + if particles.mpi_comm is not None: + particles.mpi_sort_markers(apply_bc=False, alpha=0.5) + + self._accum_kernel_en_fB_mid( + args_markers, + *self._args_accum_kernel_en_fB_mid, + first_free_idx + 3, + ) + en_fB_mid = xp.sum(markers[~holes, first_free_idx + 3].dot(markers[~holes, 5])) * self.options.ep_scale - if self.particles[0].mpi_comm is not None: - self.particles[0].mpi_sort_markers() + en_fB_mid /= n_mks_tot - # solve linear system for updated u coefficients - _ku = self._solver.dot(self._ACC.vectors[0], out=self._u_temp2) + buffer_array = xp.array([en_fB_mid]) - # calculate u^{n+1}_k - _u_temp = un.copy(out=self._u_temp1) - _u_temp += _ku * dt * self._butcher.a[stage] + if particles.mpi_comm is not None: + particles.mpi_comm.Allreduce( + MPI.IN_PLACE, + buffer_array, + op=MPI.SUM, + ) + + if particles.clone_config is not None: + particles.clone_config.inter_comm.Allreduce( + MPI.IN_PLACE, + buffer_array, + op=MPI.SUM, + ) + + en_fB_mid = buffer_array[0] + + if denominator == 0.0: + const = 0.0 + else: + const = (en_fB_new - en_fB_old - en_fB_mid) / denominator + + # update u^{n+1, k} + self._ACC(*self._args_accum_kernel, const) + + ku = self._A_inv.dot(self._ACC.vectors[0], out=self._ku) + + u_new = un.copy(out=self._u_new) + u_new += ku * dt + u_new *= alpha + u_new += u_old * (1.0 - alpha) + + u_new.update_ghost_regions() - # calculate u^{n+1} - _u_new += _ku * dt * self._butcher.b[stage] + # update en_U_new + self._A.dot(u_new, out=self._M2n_dot_u) + en_U_new = u_new.inner(self._M2n_dot_u) / 2.0 - if self._info and self._rank == 0: - print("Stage:", stage) - print( - "Status for CurrentCoupling5DGradB:", - self._solver._info["success"], + # update H^{n+1, k} + self._pusher_kernel( + dt, + args_markers, + *self._args_pusher_kernel, + const, + alpha, ) - print( - "Iterations for CurrentCoupling5DGradB:", - self._solver._info["niter"], + + sum_H_diff_loc = xp.sum( + xp.abs(markers[~holes, 0:3] - markers[~holes, first_free_idx : first_free_idx + 3]) ) - # clear the buffer - if stage == self._butcher.n_stages - 1: - self.particles[0].markers[ - ~self.particles[0].holes, - 11:-1, - ] = 0.0 + if particles.mpi_comm is not None: + particles.mpi_sort_markers(apply_bc=False) - # write new coeffs into Propagator.variables - (max_du,) = self.feec_vars_update(_u_new) + # update en_fB_new + particles.save_magnetic_energy(PB_b) + en_fB_new = xp.sum(markers[~holes, 8].dot(markers[~holes, 5])) * self.options.ep_scale + en_fB_new /= n_mks_tot - # update_weights - if self.particles[0].control_variate: - self.particles[0].update_weights() + buffer_array = xp.array([en_fB_new]) - if self._info and self._rank == 0: - print("Maxdiff up for CurrentCoupling5DGradB:", max_du) - print() + if particles.mpi_comm is not None: + particles.mpi_comm.Allreduce( + MPI.IN_PLACE, + buffer_array, + op=MPI.SUM, + ) + + if particles.clone_config is not None: + particles.clone_config.inter_comm.Allreduce( + MPI.IN_PLACE, + buffer_array, + op=MPI.SUM, + ) + + en_fB_new = buffer_array[0] + + # calculate total energy difference + e_diff = xp.abs(en_U_new + en_fB_new - en_tot_old) + + # calculate ||z^{n+1, k} - z^{n+1, k-1|| + sum_u_diff_loc = xp.sum(xp.abs(u_new.toarray() - u_old.toarray())) + + buffer_array = xp.array([sum_u_diff_loc]) + + if particles.mpi_comm is not None: + particles.mpi_comm.Allreduce( + MPI.IN_PLACE, + buffer_array, + op=MPI.SUM, + ) + + diff = buffer_array[0] + + buffer_array = xp.array([sum_H_diff_loc]) + + if particles.mpi_comm is not None: + particles.mpi_comm.Allreduce( + MPI.IN_PLACE, + buffer_array, + op=MPI.SUM, + ) + + if particles.clone_config is not None: + particles.clone_config.inter_comm.Allreduce( + MPI.IN_PLACE, + buffer_array, + op=MPI.SUM, + ) + + diff += buffer_array[0] + + # check convergence + if diff < self.options.dg_solver_params.tol: + if self.options.dg_solver_params.verbose and MPI.COMM_WORLD.Get_rank() == 0: + print("converged diff: ", diff) + print("converged e_diff: ", e_diff) + + if particles.mpi_comm is not None: + particles.mpi_comm.Barrier() + break + + else: + if self.options.dg_solver_params.verbose and MPI.COMM_WORLD.Get_rank() == 0: + print("not converged diff: ", diff) + print("not converged e_diff: ", e_diff) + + if iter_num == self.options.dg_solver_params.maxiter: + if self.options.dg_solver_params.info and MPI.COMM_WORLD.Get_rank() == 0: + print( + f"{iter_num = }, maxiter={self.options.dg_solver_params.maxiter} reached! diff: {diff}, e_diff: {e_diff}", + ) + if particles.mpi_comm is not None: + particles.mpi_comm.Barrier() + break + + # sorting markers + if particles.mpi_comm is not None: + particles.mpi_sort_markers() + else: + particles.apply_kinetic_bc() + + # update u coefficients + diffs = self.update_feec_variables(u=u_new) + + # clear the buffer + markers[:, first_init_idx:-2] = 0.0 + + # update_weights + if self.variables.energetic_ions.species.weights_params.control_variate: + particles.update_weights() + + if self.options.dg_solver_params.info and MPI.COMM_WORLD.Get_rank() == 0: + print("Maxdiff up for CurrentCoupling5DGradB:", diffs["u"]) + print() diff --git a/src/struphy/propagators/propagators_fields.py b/src/struphy/propagators/propagators_fields.py index 848706ecd..f7ac3a3c3 100644 --- a/src/struphy/propagators/propagators_fields.py +++ b/src/struphy/propagators/propagators_fields.py @@ -1,9 +1,13 @@ "Only FEEC variables are updated." -from collections.abc import Callable +import copy from copy import deepcopy +from dataclasses import dataclass +from typing import Callable, Literal, get_args +import cunumpy as xp import scipy as sc +from line_profiler import profile from matplotlib import pyplot as plt from numpy import zeros from psydac.api.essential_bc import apply_essential_bc_stencil @@ -24,32 +28,46 @@ ) from struphy.feec.linear_operators import BoundaryOperator from struphy.feec.mass import WeightedMassOperator, WeightedMassOperators -from struphy.feec.preconditioner import MassMatrixPreconditioner +from struphy.feec.preconditioner import MassMatrixDiagonalPreconditioner, MassMatrixPreconditioner from struphy.feec.projectors import L2Projector from struphy.feec.psydac_derham import Derham, SplineFunction from struphy.feec.variational_utilities import ( BracketOperator, - H1vecMassMatrix_density, + Hdiv0_transport_operator, InternalEnergyEvaluator, KineticEnergyEvaluator, + Pressure_transport_operator, ) from struphy.fields_background.equils import set_defaults from struphy.geometry.utilities import TransformedPformComponent from struphy.initial import perturbations +from struphy.io.options import ( + OptsDirectSolver, + OptsGenSolver, + OptsMassPrecond, + OptsNonlinearSolver, + OptsSaddlePointSolver, + OptsSymmSolver, + OptsVecSpace, + check_option, +) from struphy.io.setup import descend_options_dict from struphy.kinetic_background.base import Maxwellian from struphy.kinetic_background.maxwellians import GyroMaxwellian2D, Maxwellian3D from struphy.linear_algebra.saddle_point import SaddlePointSolver -from struphy.linear_algebra.schur_solver import SchurSolver +from struphy.linear_algebra.schur_solver import SchurSolver, SchurSolverFull +from struphy.linear_algebra.solver import NonlinearSolverParameters, SolverParameters +from struphy.models.species import Species +from struphy.models.variables import FEECVariable, PICVariable, SPHVariable, Variable from struphy.ode.solvers import ODEsolverFEEC -from struphy.ode.utils import ButcherTableau +from struphy.ode.utils import ButcherTableau, OptsButcher from struphy.pic.accumulation import accum_kernels, accum_kernels_gc +from struphy.pic.accumulation.filter import FilterParameters from struphy.pic.accumulation.particles_to_grid import Accumulator, AccumulatorVector from struphy.pic.base import Particles from struphy.pic.particles import Particles5D, Particles6D from struphy.polar.basic import PolarVector from struphy.propagators.base import Propagator -from struphy.utils.arrays import xp as np from struphy.utils.pyccel import Pyccelkernel @@ -66,52 +84,89 @@ class Maxwell(Propagator): :ref:`time_discret`: Crank-Nicolson (implicit mid-point). System size reduction via :class:`~struphy.linear_algebra.schur_solver.SchurSolver`. """ - @staticmethod - def options(default=False): - dct = {} - dct["algo"] = ["implicit"] + ButcherTableau.available_methods() - dct["solver"] = { - "type": [ - ("pcg", "MassMatrixPreconditioner"), - ("cg", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - if default: - dct = descend_options_dict(dct, [], verbose=False) - - return dct - - def __init__( - self, - e: BlockVector, - b: BlockVector, - *, - algo: dict = options(default=True)["algo"], - solver: dict = options(default=True)["solver"], - ): - super().__init__(e, b) - - self._algo = algo + class Variables: + def __init__(self): + self._e: FEECVariable = None + self._b: FEECVariable = None + + @property + def e(self) -> FEECVariable: + return self._e + + @e.setter + def e(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "Hcurl" + self._e = new + + @property + def b(self) -> FEECVariable: + return self._b + + @b.setter + def b(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "Hdiv" + self._b = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # specific literals + OptsAlgo = Literal["implicit", "explicit"] + # propagator options + algo: OptsAlgo = "implicit" + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + butcher: ButcherTableau = None + + def __post_init__(self): + # checks + check_option(self.algo, self.OptsAlgo) + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + if self.algo == "explicit" and self.butcher is None: + self.butcher = ButcherTableau() + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): # obtain needed matrices M1 = self.mass_ops.M1 M2 = self.mass_ops.M2 curl = self.derham.curl # Preconditioner for M1 + ... - if solver["type"][1] is None: + if self.options.precond is None: pc = None else: - pc_class = getattr(preconditioner, solver["type"][1]) - pc = pc_class(self.mass_ops.M1) + pc_class = getattr(preconditioner, self.options.precond) + pc = pc_class(M1) - if self._algo == "implicit": - self._info = solver["info"] + if self.options.algo == "implicit": + self._info = self.options.solver_params.info # Define block matrix [[A B], [C I]] (without time step size dt in the diagonals) _A = M1 @@ -125,11 +180,9 @@ def __init__( self._schur_solver = SchurSolver( _A, _BC, - solver["type"][0], - pc=pc, - tol=solver["tol"], - maxiter=solver["maxiter"], - verbose=solver["verbose"], + self.options.solver, + precond=pc, + solver_params=self.options.solver_params, ) # pre-allocate arrays @@ -140,43 +193,44 @@ def __init__( # define vector field M1_inv = inverse( M1, - solver["type"][0], + self.options.solver, pc=pc, - tol=solver["tol"], - maxiter=solver["maxiter"], - verbose=solver["verbose"], + tol=self.options.solver_params.tol, + maxiter=self.options.solver_params.maxiter, + verbose=self.options.solver_params.verbose, ) weak_curl = M1_inv @ curl.T @ M2 # allocate output of vector field - out1 = e.space.zeros() - out2 = b.space.zeros() + out1 = self.variables.e.spline.vector.space.zeros() + out2 = self.variables.b.spline.vector.space.zeros() - def f1(t, y1, y2, out=out1): + def f1(t, y1, y2, out: BlockVector = out1): weak_curl.dot(y2, out=out) out.update_ghost_regions() return out - def f2(t, y1, y2, out=out2): + def f2(t, y1, y2, out: BlockVector = out2): curl.dot(y1, out=out) out *= -1.0 out.update_ghost_regions() return out - vector_field = {e: f1, b: f2} - self._ode_solver = ODEsolverFEEC(vector_field, algo=algo) + vector_field = {self.variables.e.spline.vector: f1, self.variables.b.spline.vector: f2} + self._ode_solver = ODEsolverFEEC(vector_field, butcher=self.options.butcher) # allocate place-holder vectors to avoid temporary array allocations in __call__ - self._e_tmp1 = e.space.zeros() - self._e_tmp2 = e.space.zeros() - self._b_tmp1 = b.space.zeros() + self._e_tmp1 = self.variables.e.spline.vector.space.zeros() + self._e_tmp2 = self.variables.e.spline.vector.space.zeros() + self._b_tmp1 = self.variables.b.spline.vector.space.zeros() + @profile def __call__(self, dt): - # current variables - en = self.feec_vars[0] - bn = self.feec_vars[1] + # current FE coeffs + en = self.variables.e.spline.vector + bn = self.variables.b.spline.vector - if self._algo == "implicit": + if self.options.algo == "implicit": # solve for new e coeffs self._B.dot(bn, out=self._byn) @@ -189,17 +243,16 @@ def __call__(self, dt): bn1 *= -dt bn1 += bn - # write new coeffs into self.feec_vars - max_de, max_db = self.feec_vars_update(en1, bn1) + diffs = self.update_feec_variables(e=en1, b=bn1) else: self._ode_solver(0.0, dt) if self._info and MPI.COMM_WORLD.Get_rank() == 0: - if self._algo == "implicit": + if self.options.algo == "implicit": print("Status for Maxwell:", info["success"]) print("Iterations for Maxwell:", info["niter"]) - print("Maxdiff e1 for Maxwell:", max_de) - print("Maxdiff b2 for Maxwell:", max_db) + print("Maxdiff e for Maxwell:", diffs["e"]) + print("Maxdiff b for Maxwell:", diffs["b"]) print() @@ -231,39 +284,71 @@ class OhmCold(Propagator): \end{bmatrix} \,. """ - @staticmethod - def options(default=False): - dct = {} - dct["solver"] = { - "type": [ - ("pcg", "MassMatrixPreconditioner"), - ("cg", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - j: BlockVector, - e: BlockVector, - *, - alpha: float = 1.0, - epsilon: float = 1.0, - solver: dict = options(default=True)["solver"], - ): - super().__init__(e, j) + class Variables: + def __init__(self): + self._j: FEECVariable = None + self._e: FEECVariable = None + + @property + def j(self) -> FEECVariable: + return self._j + + @j.setter + def j(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "Hcurl" + self._j = new + + @property + def e(self) -> FEECVariable: + return self._e + + @e.setter + def e(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "Hcurl" + self._e = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # propagator options + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + + def __post_init__(self): + # checks + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() - self._info = solver["info"] - self._alpha = alpha - self._epsilon = epsilon + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + self._info = self.options.solver_params.info + + self._alpha = self.variables.j.species.equation_params.alpha + self._epsilon = self.variables.j.species.equation_params.epsilon # Define block matrix [[A B], [C I]] (without time step size dt in the diagonals) _A = self.mass_ops.M1ninv @@ -271,10 +356,10 @@ def __init__( self._B = -1 / 2 * 1 / self._epsilon * self.mass_ops.M1 # no dt # Preconditioner - if solver["type"][1] is None: + if self.options.precond is None: pc = None else: - pc_class = getattr(preconditioner, solver["type"][1]) + pc_class = getattr(preconditioner, self.options.precond) pc = pc_class(self.mass_ops.M1ninv) # Instantiate Schur solver (constant in this case) @@ -283,13 +368,14 @@ def __init__( self._schur_solver = SchurSolver( _A, _BC, - solver["type"][0], - pc=pc, - tol=solver["tol"], - maxiter=solver["maxiter"], - verbose=solver["verbose"], + self.options.solver, + precond=pc, + solver_params=self.options.solver_params, ) + j = self.variables.j.spline.vector + e = self.variables.e.spline.vector + self._tmp_j1 = j.space.zeros() self._tmp_j2 = j.space.zeros() self._tmp_e1 = e.space.zeros() @@ -297,8 +383,8 @@ def __init__( def __call__(self, dt): # current variables - en = self.feec_vars[0] - jn = self.feec_vars[1] + jn = self.variables.j.spline.vector + en = self.variables.e.spline.vector # in-place solution (no tmps created here) Ben = self._B.dot(en, out=self._tmp_e1) @@ -312,13 +398,13 @@ def __call__(self, dt): en1 += en # write new coeffs into Propagator.variables - max_de, max_dj = self.feec_vars_update(en1, jn1) + diffs = self.update_feec_variables(e=en1, j=jn1) if self._info: print("Status for OhmCold:", info["success"]) print("Iterations for OhmCold:", info["niter"]) - print("Maxdiff e1 for OhmCold:", max_de) - print("Maxdiff j1 for OhmCold:", max_dj) + print("Maxdiff e1 for OhmCold:", diffs["e"]) + print("Maxdiff j1 for OhmCold:", diffs["j"]) print() @@ -338,65 +424,90 @@ class JxBCold(Propagator): \mathbb M_{1/n_0} \left( \mathbf j^{n+1} - \mathbf j^n \right) = \frac{\Delta t}{2} \frac{1}{\varepsilon} \mathbb M_{B_0/n_0} \left( \mathbf j^{n+1} - \mathbf j^n \right)\,. """ - @staticmethod - def options(default=False): - dct = {} - dct["solver"] = { - "type": [ - ("pcg", "MassMatrixPreconditioner"), - ("cg", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - if default: - dct = descend_options_dict(dct, []) + class Variables: + def __init__(self): + self._j: FEECVariable = None - return dct + @property + def j(self) -> FEECVariable: + return self._j - def __init__( - self, - j: BlockVector, - *, - epsilon: float = 1.0, - solver: dict = options(default=True)["solver"], - ): - super().__init__(j) + @j.setter + def j(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "Hcurl" + self._j = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # propagator options + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None - self._info = solver["info"] + def __post_init__(self): + # checks + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + self._info = self.options.solver_params.info + + epsilon = self.variables.j.species.equation_params.epsilon # mass matrix in system (M - dt/2 * A)*j^(n + 1) = (M + dt/2 * A)*j^n self._M = self.mass_ops.M1ninv self._A = -1 / epsilon * self.mass_ops.M1Bninv # no dt # Preconditioner - if solver["type"][1] is None: + if self.options.precond is None: pc = None else: - pc_class = getattr(preconditioner, solver["type"][1]) + pc_class = getattr(preconditioner, self.options.precond) pc = pc_class(self.mass_ops.M1ninv) # Instantiate linear solver self._solver = inverse( self._M, - solver["type"][0], + self.options.solver, pc=pc, - x0=self.feec_vars[0], - tol=solver["tol"], - maxiter=solver["maxiter"], - verbose=solver["verbose"], + x0=self.variables.j.spline.vector, + tol=self.options.solver_params.tol, + maxiter=self.options.solver_params.maxiter, + verbose=self.options.solver_params.verbose, ) # allocate dummy vectors to avoid temporary array allocations self._rhs_j = self._M.codomain.zeros() - self._j_new = j.space.zeros() + self._j_new = self.variables.j.spline.vector.space.zeros() + @profile def __call__(self, dt): # current variables - jn = self.feec_vars[0] + jn = self.variables.j.spline.vector # define system (M - dt/2 * A)*b^(n + 1) = (M + dt/2 * A)*b^n lhs = self._M - dt / 2.0 * self._A @@ -411,7 +522,7 @@ def __call__(self, dt): info = self._solver._info # write new coeffs into Propagator.variables - max_dj = self.feec_vars_update(jn1)[0] + max_dj = self.update_feec_variables(j=jn1) if self._info: print("Status for FluidCold:", info["success"]) @@ -444,42 +555,78 @@ class ShearAlfven(Propagator): the MHD equilibirum density. The solution of the above system is based on the :ref:`Schur complement `. """ - @staticmethod - def options(default=False): - dct = {} - dct["algo"] = ["implicit", "rk4", "forward_euler", "heun2", "rk2", "heun3"] - dct["solver"] = { - "type": [ - ("pcg", "MassMatrixDiagonalPreconditioner"), - ("cg", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - dct["turn_off"] = False - - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - u: BlockVector, - b: BlockVector, - *, - u_space: str, - algo: dict = options(default=True)["algo"], - solver: dict = options(default=True)["solver"], - ): - super().__init__(u, b) - - assert u_space in {"Hcurl", "Hdiv", "H1vec"} + class Variables: + def __init__(self): + self._u: FEECVariable = None + self._b: FEECVariable = None + + @property + def u(self) -> FEECVariable: + return self._u + + @u.setter + def u(self, new): + assert isinstance(new, FEECVariable) + assert new.space in ("Hcurl", "Hdiv", "H1vec") + self._u = new + + @property + def b(self) -> FEECVariable: + return self._b + + @b.setter + def b(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "Hdiv" + self._b = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # specific literals + OptsAlgo = Literal["implicit", "explicit"] + # propagator options + u_space: OptsVecSpace = "Hdiv" + algo: OptsAlgo = "implicit" + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + butcher: ButcherTableau = None + + def __post_init__(self): + # checks + check_option(self.u_space, OptsVecSpace) + check_option(self.algo, self.OptsAlgo) + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + if self.algo == "explicit" and self.butcher is None: + self.butcher = ButcherTableau() - self._algo = algo + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + u_space = self.options.u_space # define block matrix [[A B], [C I]] (without time step size dt in the diagonals) id_M = "M" + self.derham.space_to_form[u_space] + "n" @@ -492,14 +639,14 @@ def __init__( curl = self.derham.curl # Preconditioner - if solver["type"][1] is None: + if self.options.precond is None: pc = None else: - pc_class = getattr(preconditioner, solver["type"][1]) + pc_class = getattr(preconditioner, self.options.precond) pc = pc_class(getattr(self.mass_ops, id_M)) - if self._algo == "implicit": - self._info = solver["info"] + if self.options.algo == "implicit": + self._info = self.options.solver_params.info self._B = -1 / 2 * _T.T @ curl.T @ _M2 self._C = 1 / 2 * curl @ _T @@ -510,12 +657,9 @@ def __init__( self._schur_solver = SchurSolver( _A, _BC, - solver["type"][0], - pc=pc, - tol=solver["tol"], - maxiter=solver["maxiter"], - verbose=solver["verbose"], - recycle=solver["recycle"], + self.options.solver, + precond=pc, + solver_params=self.options.solver_params, ) # pre-allocate arrays @@ -527,44 +671,45 @@ def __init__( # define vector field A_inv = inverse( _A, - solver["type"][0], + self.options.solver, pc=pc, - tol=solver["tol"], - maxiter=solver["maxiter"], - verbose=solver["verbose"], + tol=self.options.solver_params.tol, + maxiter=self.options.solver_params.maxiter, + verbose=self.options.solver_params.verbose, ) _f1 = A_inv @ _T.T @ curl.T @ _M2 _f2 = curl @ _T # allocate output of vector field - out1 = u.space.zeros() - out2 = b.space.zeros() + out1 = self.variables.u.spline.vector.space.zeros() + out2 = self.variables.b.spline.vector.space.zeros() - def f1(t, y1, y2, out=out1): + def f1(t, y1, y2, out: BlockVector = out1): _f1.dot(y2, out=out) out.update_ghost_regions() return out - def f2(t, y1, y2, out=out2): + def f2(t, y1, y2, out: BlockVector = out2): _f2.dot(y1, out=out) out *= -1.0 out.update_ghost_regions() return out - vector_field = {u: f1, b: f2} - self._ode_solver = ODEsolverFEEC(vector_field, algo=algo) + vector_field = {self.variables.u.spline.vector: f1, self.variables.b.spline.vector: f2} + self._ode_solver = ODEsolverFEEC(vector_field, butcher=self.options.butcher) # allocate dummy vectors to avoid temporary array allocations - self._u_tmp1 = u.space.zeros() - self._u_tmp2 = u.space.zeros() - self._b_tmp1 = b.space.zeros() + self._u_tmp1 = self.variables.u.spline.vector.space.zeros() + self._u_tmp2 = self.variables.u.spline.vector.space.zeros() + self._b_tmp1 = self.variables.b.spline.vector.space.zeros() + @profile def __call__(self, dt): - # current variables - un = self.feec_vars[0] - bn = self.feec_vars[1] + # current FE coeffs + un = self.variables.u.spline.vector + bn = self.variables.b.spline.vector - if self._algo == "implicit": + if self.options.algo == "implicit": # solve for new u coeffs byn = self._B.dot(bn, out=self._byn) @@ -577,18 +722,16 @@ def __call__(self, dt): bn1 *= -dt bn1 += bn - # write new coeffs into self.feec_vars - max_du, max_db = self.feec_vars_update(un1, bn1) - + diffs = self.update_feec_variables(u=un1, b=bn1) else: self._ode_solver(0.0, dt) if self._info and MPI.COMM_WORLD.Get_rank() == 0: - if self._algo == "implicit": + if self.options.algo == "implicit": print("Status for ShearAlfven:", info["success"]) print("Iterations for ShearAlfven:", info["niter"]) - print("Maxdiff up for ShearAlfven:", max_du) - print("Maxdiff b2 for ShearAlfven:", max_db) + print("Maxdiff up for ShearAlfven:", diffs["u"]) + print("Maxdiff b2 for ShearAlfven:", diffs["b"]) print() @@ -615,62 +758,91 @@ class ShearAlfvenB1(Propagator): the MHD equilibirum density. """ - @staticmethod - def options(default=False): - dct = {} - dct["solver"] = { - "type": [ - ("pcg", "MassMatrixPreconditioner"), - ("cg", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - dct["solver_M1"] = { - "type": [ - ("pcg", "MassMatrixPreconditioner"), - ("cg", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - u: BlockVector, - b: BlockVector, - *, - solver: dict = options(default=True)["solver"], - solver_M1: dict = options(default=True)["solver_M1"], - ): - super().__init__(u, b) + class Variables: + def __init__(self): + self._u: FEECVariable = None + self._b: FEECVariable = None + + @property + def u(self) -> FEECVariable: + return self._u + + @u.setter + def u(self, new): + assert isinstance(new, FEECVariable) + assert new.space in ("Hdiv") + self._u = new + + @property + def b(self) -> FEECVariable: + return self._b + + @b.setter + def b(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "Hcurl" + self._b = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # propagator options + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + solver_M1: OptsSymmSolver = "pcg" + precond_M1: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params_M1: SolverParameters = None + + def __post_init__(self): + # checks + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + check_option(self.solver_M1, OptsSymmSolver) + check_option(self.precond_M1, OptsMassPrecond) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + if self.solver_params_M1 is None: + self.solver_params_M1 = SolverParameters() - self._info = solver["info"] + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + self._info = self.options.solver_params.info # define inverse of M1 - if solver_M1["type"][1] is None: + if self.options.precond_M1 is None: pc = None else: - pc_class = getattr(preconditioner, solver_M1["type"][1]) + pc_class = getattr(preconditioner, self.options.precond_M1) pc = pc_class(self.mass_ops.M1) M1_inv = inverse( self.mass_ops.M1, - solver_M1["type"][0], + self.options.solver_M1, pc=pc, - tol=solver_M1["tol"], - maxiter=solver_M1["maxiter"], - verbose=solver_M1["verbose"], + tol=self.options.solver_params_M1.tol, + maxiter=self.options.solver_params_M1.maxiter, + verbose=self.options.solver_params_M1.verbose, ) # define block matrix [[A B], [C I]] (without time step size dt in the diagonals) @@ -680,10 +852,10 @@ def __init__( self._C = 1 / 2 * M1_inv @ self.derham.curl.T @ self.mass_ops.M2B # Preconditioner - if solver["type"][1] is None: + if self.options.precond is None: pc = None else: - pc_class = getattr(preconditioner, solver["type"][1]) + pc_class = getattr(preconditioner, self.options.precond) pc = pc_class(getattr(self.mass_ops, "M2n")) # instantiate Schur solver (constant in this case) @@ -692,24 +864,26 @@ def __init__( self._schur_solver = SchurSolver( _A, _BC, - solver["type"][0], - pc=pc, - tol=solver["tol"], - maxiter=solver["maxiter"], - verbose=solver["verbose"], + self.options.solver, + precond=pc, + solver_params=self.options.solver_params, ) # allocate dummy vectors to avoid temporary array allocations + u = self.variables.u.spline.vector + b = self.variables.b.spline.vector + self._u_tmp1 = u.space.zeros() self._u_tmp2 = u.space.zeros() self._b_tmp1 = b.space.zeros() self._byn = self._B.codomain.zeros() + @profile def __call__(self, dt): # current variables - un = self.feec_vars[0] - bn = self.feec_vars[1] + un = self.variables.u.spline.vector + bn = self.variables.b.spline.vector # solve for new u coeffs byn = self._B.dot(bn, out=self._byn) @@ -724,13 +898,13 @@ def __call__(self, dt): bn1 += bn # write new coeffs into self.feec_vars - max_du, max_db = self.feec_vars_update(un1, bn1) + max_diffs = self.update_feec_variables(u=un1, b=bn1) if self._info and MPI.COMM_WORLD.Get_rank() == 0: print("Status for ShearAlfvenB1:", info["success"]) print("Iterations for ShearAlfvenB1:", info["niter"]) - print("Maxdiff up for ShearAlfvenB1:", max_du) - print("Maxdiff b2 for ShearAlfvenB1:", max_db) + print("Maxdiff up for ShearAlfvenB1:", max_diffs["u"]) + print("Maxdiff b2 for ShearAlfvenB1:", max_diffs["b"]) print() @@ -754,38 +928,66 @@ class Hall(Propagator): The solution of the above system is based on the Pre-conditioned Biconjugate Gradient Stabilized algortihm (PBiConjugateGradientStab). """ - @staticmethod - def options(default=False): - dct = {} - dct["solver"] = { - "type": [ - ("pbicgstab", "MassMatrixPreconditioner"), - ("bicgstab", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - if default: - dct = descend_options_dict(dct, []) + class Variables: + def __init__(self): + self._b: FEECVariable = None - return dct + @property + def b(self) -> FEECVariable: + return self._b - def __init__( - self, - b: BlockVector, - *, - epsilon: float = 1.0, - solver: dict = options(default=True)["solver"], - ): - super().__init__(b) + @b.setter + def b(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "Hcurl" + self._b = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # propagator options + solver: OptsGenSolver = "pbicgstab" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + epsilon_from: Species = None + + def __post_init__(self): + # checks + check_option(self.solver, OptsGenSolver) + check_option(self.precond, OptsMassPrecond) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + if self.options.epsilon_from is None: + epsilon = 1.0 + else: + epsilon = self.options.epsilon_from.equation_params.epsilon - self._info = solver["info"] - self._tol = solver["tol"] - self._maxiter = solver["maxiter"] - self._verbose = solver["verbose"] + self._info = self.options.solver_params.info + self._tol = self.options.solver_params.tol + self._maxiter = self.options.solver_params.maxiter + self._verbose = self.options.solver_params.verbose # mass matrix in system (M - dt/2 * A)*b^(n + 1) = (M + dt/2 * A)*b^n id_M = "M1" @@ -795,18 +997,18 @@ def __init__( self._A = 1.0 / epsilon * self.derham.curl.T @ self._M2Bn @ self.derham.curl # Preconditioner - if solver["type"][1] is None: + if self.options.precond is None: pc = None else: - pc_class = getattr(preconditioner, solver["type"][1]) + pc_class = getattr(preconditioner, self.options.precond) pc = pc_class(getattr(self.mass_ops, id_M)) # Instantiate linear solver self._solver = inverse( self._M, - solver["type"][0], + self.options.solver, pc=pc, - x0=self.feec_vars[0], + x0=self.variables.b.spline.vector, tol=self._tol, maxiter=self._maxiter, verbose=self._verbose, @@ -814,11 +1016,11 @@ def __init__( # allocate dummy vectors to avoid temporary array allocations self._rhs_b = self._M.codomain.zeros() - self._b_new = b.space.zeros() + self._b_new = self.variables.b.spline.vector.space.zeros() def __call__(self, dt): # current variables - bn = self.feec_vars[0] + bn = self.variables.b.spline.vector # define system (M - dt/2 * A)*b^(n + 1) = (M + dt/2 * A)*b^n lhs = self._M - dt / 2.0 * self._A @@ -832,12 +1034,12 @@ def __call__(self, dt): info = self._solver._info # write new coeffs into self.feec_vars - max_db = self.feec_vars_update(bn1) + max_db = self.update_feec_variables(b=bn1) if self._info and MPI.COMM_WORLD.Get_rank() == 0: print("Status for Hall:", info["success"]) print("Iterations for Hall:", info["niter"]) - print("Maxdiff b1 for Hall:", max_db) + print("Maxdiff b1 for Hall:", max_db["b"]) print() @@ -875,42 +1077,85 @@ class Magnetosonic(Propagator): \boldsymbol{\rho}^{n+1} = \boldsymbol{\rho}^n - \frac{\Delta t}{2} \mathbb D \mathcal Q^\alpha (\mathbf u^{n+1} + \mathbf u^n) \,. """ - @staticmethod - def options(default=False): - dct = {} - dct["solver"] = { - "type": [ - ("pbicgstab", "MassMatrixPreconditioner"), - ("bicgstab", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - dct["turn_off"] = False - - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - n: StencilVector, - u: BlockVector, - p: StencilVector, - *, - u_space: str, - b: BlockVector, - solver: dict = options(default=True)["solver"], - ): - super().__init__(n, u, p) - - assert u_space in {"Hcurl", "Hdiv", "H1vec"} + class Variables: + def __init__(self): + self._n: FEECVariable = None + self._u: FEECVariable = None + self._p: FEECVariable = None + + @property + def n(self) -> FEECVariable: + return self._n + + @n.setter + def n(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "L2" + self._n = new + + @property + def u(self) -> FEECVariable: + return self._u + + @u.setter + def u(self, new): + assert isinstance(new, FEECVariable) + assert new.space in ("Hcurl", "Hdiv", "H1vec") + self._u = new + + @property + def p(self) -> FEECVariable: + return self._p + + @p.setter + def p(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "L2" + self._p = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + b_field: FEECVariable = None + u_space: OptsVecSpace = "Hdiv" + solver: OptsGenSolver = "pbicgstab" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + + def __post_init__(self): + # checks + check_option(self.u_space, OptsVecSpace) + check_option(self.solver, OptsGenSolver) + check_option(self.precond, OptsMassPrecond) + + # defaults + if self.b_field is None: + self.b_field = FEECVariable(space="Hdiv") + if self.solver_params is None: + self.solver_params = SolverParameters() - self._info = solver["info"] + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + u_space = self.options.u_space + + self._info = self.options.solver_params.info self._bc = self.derham.dirichlet_bc # define block matrix [[A B], [C I]] (without time step size dt in the diagonals) @@ -929,7 +1174,8 @@ def __init__( _K = getattr(self.basis_ops, id_K) if id_U is None: - _U, _UT = IdentityOperator(u.space), IdentityOperator(u.space) + _U = IdentityOperator(self.variables.u.spline.vector.space) + _UT = IdentityOperator(self.variables.u.spline.vector.space) else: _U = getattr(self.basis_ops, id_U) _UT = _U.T @@ -940,13 +1186,14 @@ def __init__( self._MJ = getattr(self.mass_ops, id_MJ) self._DQ = self.derham.div @ getattr(self.basis_ops, id_Q) - self._b = b + self.options.b_field.allocate(self.derham, self.domain) + self._b = self.options.b_field.spline.vector # preconditioner - if solver["type"][1] is None: + if self.options.precond is None: pc = None else: - pc_class = getattr(preconditioner, solver["type"][1]) + pc_class = getattr(preconditioner, self.options.precond) pc = pc_class(getattr(self.mass_ops, id_Mn)) # instantiate Schur solver (constant in this case) @@ -955,29 +1202,27 @@ def __init__( self._schur_solver = SchurSolver( _A, _BC, - solver["type"][0], - pc=pc, - tol=solver["tol"], - maxiter=solver["maxiter"], - verbose=solver["verbose"], - recycle=solver["recycle"], + self.options.solver, + precond=pc, + solver_params=self.options.solver_params, ) # allocate dummy vectors to avoid temporary array allocations - self._u_tmp1 = u.space.zeros() - self._u_tmp2 = u.space.zeros() - self._p_tmp1 = p.space.zeros() - self._n_tmp1 = n.space.zeros() + self._u_tmp1 = self.variables.u.spline.vector.space.zeros() + self._u_tmp2 = self.variables.u.spline.vector.space.zeros() + self._p_tmp1 = self.variables.p.spline.vector.space.zeros() + self._n_tmp1 = self.variables.n.spline.vector.space.zeros() self._b_tmp1 = self._b.space.zeros() self._byn1 = self._B.codomain.zeros() self._byn2 = self._B.codomain.zeros() + @profile def __call__(self, dt): - # current variables - nn = self.feec_vars[0] - un = self.feec_vars[1] - pn = self.feec_vars[2] + # current FE coeffs + nn = self.variables.n.spline.vector + un = self.variables.u.spline.vector + pn = self.variables.p.spline.vector # solve for new u coeffs (no tmps created here) byn1 = self._B.dot(pn, out=self._byn1) @@ -998,19 +1243,14 @@ def __call__(self, dt): nn1 *= -dt / 2 nn1 += nn - # write new coeffs into self.feec_vars - max_dn, max_du, max_dp = self.feec_vars_update( - nn1, - un1, - pn1, - ) + diffs = self.update_feec_variables(n=nn1, u=un1, p=pn1) if self._info and MPI.COMM_WORLD.Get_rank() == 0: print("Status for Magnetosonic:", info["success"]) print("Iterations for Magnetosonic:", info["niter"]) - print("Maxdiff n3 for Magnetosonic:", max_dn) - print("Maxdiff up for Magnetosonic:", max_du) - print("Maxdiff p3 for Magnetosonic:", max_dp) + print("Maxdiff n3 for Magnetosonic:", diffs["n"]) + print("Maxdiff up for Magnetosonic:", diffs["u"]) + print("Maxdiff p3 for Magnetosonic:", diffs["p"]) print() @@ -1065,36 +1305,78 @@ class MagnetosonicUniform(Propagator): Solver- and/or other parameters for this splitting step. """ - @staticmethod - def options(default=False): - dct = {} - dct["solver"] = { - "type": [ - ("pbicgstab", "MassMatrixPreconditioner"), - ("bicgstab", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - n: StencilVector, - u: BlockVector, - p: StencilVector, - *, - solver: dict = options(default=True)["solver"], - ): - super().__init__(n, u, p) + class Variables: + def __init__(self): + self._n: FEECVariable = None + self._u: FEECVariable = None + self._p: FEECVariable = None + + @property + def n(self) -> FEECVariable: + return self._n + + @n.setter + def n(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "L2" + self._n = new + + @property + def u(self) -> FEECVariable: + return self._u + + @u.setter + def u(self, new): + assert isinstance(new, FEECVariable) + assert new.space in ("Hcurl", "Hdiv", "H1vec") + self._u = new + + @property + def p(self) -> FEECVariable: + return self._p + + @p.setter + def p(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "L2" + self._p = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + solver: OptsGenSolver = "pbicgstab" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + + def __post_init__(self): + # checks + check_option(self.solver, OptsGenSolver) + check_option(self.precond, OptsMassPrecond) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() - self._info = solver["info"] + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + self._info = self.options.solver_params.info self._bc = self.derham.dirichlet_bc # define block matrix [[A B], [C I]] (without time step size dt in the diagonals) @@ -1110,10 +1392,10 @@ def __init__( self._QD = getattr(self.basis_ops, id_Q) @ self.derham.div # preconditioner - if solver["type"][1] is None: + if self.options.precond is None: pc = None else: - pc_class = getattr(preconditioner, solver["type"][1]) + pc_class = getattr(preconditioner, self.options.precond) pc = pc_class(getattr(self.mass_ops, id_Mn)) # instantiate Schur solver (constant in this case) @@ -1122,14 +1404,16 @@ def __init__( self._schur_solver = SchurSolver( _A, _BC, - solver["type"][0], - pc=pc, - tol=solver["tol"], - maxiter=solver["maxiter"], - verbose=solver["verbose"], + self.options.solver, + precond=pc, + solver_params=self.options.solver_params, ) # allocate dummy vectors to avoid temporary array allocations + n = self.variables.n.spline.vector + u = self.variables.u.spline.vector + p = self.variables.p.spline.vector + self._u_tmp1 = u.space.zeros() self._u_tmp2 = u.space.zeros() self._p_tmp1 = p.space.zeros() @@ -1137,11 +1421,12 @@ def __init__( self._byn1 = self._B.codomain.zeros() + @profile def __call__(self, dt): # current variables - nn = self.feec_vars[0] - un = self.feec_vars[1] - pn = self.feec_vars[2] + nn = self.variables.n.spline.vector + un = self.variables.u.spline.vector + pn = self.variables.p.spline.vector # solve for new u coeffs byn1 = self._B.dot(pn, out=self._byn1) @@ -1160,18 +1445,14 @@ def __call__(self, dt): nn1 += nn # write new coeffs into self.feec_vars - max_dn, max_du, max_dp = self.feec_vars_update( - nn1, - un1, - pn1, - ) + diffs = self.update_feec_variables(n=nn1, u=un1, p=pn1) if self._info and MPI.COMM_WORLD.Get_rank() == 0: print("Status for Magnetosonic:", info["success"]) print("Iterations for Magnetosonic:", info["niter"]) - print("Maxdiff n3 for Magnetosonic:", max_dn) - print("Maxdiff up for Magnetosonic:", max_du) - print("Maxdiff p3 for Magnetosonic:", max_dp) + print("Maxdiff n3 for Magnetosonic:", diffs["n"]) + print("Maxdiff up for Magnetosonic:", diffs["u"]) + print("Maxdiff p3 for Magnetosonic:", diffs["p"]) print() @@ -1259,13 +1540,13 @@ def __init__(self, a, **params): ] # Initialize Accumulator object for getting density from particles - self._pts_x = 1.0 / (2.0 * self.derham.Nel[0]) * np.polynomial.legendre.leggauss( + self._pts_x = 1.0 / (2.0 * self.derham.Nel[0]) * xp.polynomial.legendre.leggauss( self._nqs[0], )[0] + 1.0 / (2.0 * self.derham.Nel[0]) - self._pts_y = 1.0 / (2.0 * self.derham.Nel[1]) * np.polynomial.legendre.leggauss( + self._pts_y = 1.0 / (2.0 * self.derham.Nel[1]) * xp.polynomial.legendre.leggauss( self._nqs[1], )[0] + 1.0 / (2.0 * self.derham.Nel[1]) - self._pts_z = 1.0 / (2.0 * self.derham.Nel[2]) * np.polynomial.legendre.leggauss( + self._pts_z = 1.0 / (2.0 * self.derham.Nel[2]) * xp.polynomial.legendre.leggauss( self._nqs[2], )[0] + 1.0 / (2.0 * self.derham.Nel[2]) @@ -1305,15 +1586,15 @@ def __call__(self, dt): self._accum_density.accumulate( self._particles, - np.array(self.derham.Nel), - np.array(self._nqs), - np.array( + xp.array(self.derham.Nel), + xp.array(self._nqs), + xp.array( self._pts_x, ), - np.array(self._pts_y), - np.array(self._pts_z), - np.array(self._p_shape), - np.array(self._p_size), + xp.array(self._pts_y), + xp.array(self._pts_z), + xp.array(self._p_shape), + xp.array(self._p_size), ) self._accum_potential.accumulate(self._particles) @@ -1406,65 +1687,70 @@ class CurrentCoupling6DDensity(Propagator): :ref:`time_discret`: Crank-Nicolson (implicit mid-point). """ - @staticmethod - def options(default=False): - dct = {} - dct["solver"] = { - "type": [ - ("pbicgstab", "MassMatrixPreconditioner"), - ("bicgstab", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - dct["filter"] = { - "use_filter": None, - "modes": (1), - "repeat": 1, - "alpha": 0.5, - } - dct["boundary_cut"] = { - "e1": 0.0, - "e2": 0.0, - "e3": 0.0, - } - dct["turn_off"] = False - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - u: BlockVector, - *, - particles: Particles6D, - u_space: str, - b_eq: BlockVector | PolarVector, - b_tilde: BlockVector | PolarVector, - Ab: int = 1, - Ah: int = 1, - epsilon: float = 1.0, - solver: dict = options(default=True)["solver"], - filter: dict = options(default=True)["filter"], - boundary_cut: dict = options(default=True)["boundary_cut"], - ): - super().__init__(u) - - # assert parameters and expose some quantities to self - if u_space == "H1vec": - self._space_key_int = 0 - else: - self._space_key_int = int( - self.derham.space_to_form[u_space], - ) + class Variables: + def __init__(self): + self._u: FEECVariable = None + + @property + def u(self) -> FEECVariable: + return self._u + + @u.setter + def u(self, new): + assert isinstance(new, FEECVariable) + assert new.space in ("Hcurl", "Hdiv", "H1vec") + self._u = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # propagator options + energetic_ions: PICVariable = None + b_tilde: FEECVariable = None + u_space: OptsVecSpace = "Hdiv" + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + filter_params: FilterParameters = None + boundary_cut: tuple = (0.0, 0.0, 0.0) + + def __post_init__(self): + # checks + check_option(self.u_space, OptsVecSpace) + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + assert self.energetic_ions.space == "Particles6D" + assert self.b_tilde.space == "Hdiv" + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() - self._particles = particles - self._b_eq = b_eq - self._b_tilde = b_tilde + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + self._space_key_int = int(self.derham.space_to_form[self.options.u_space]) + + particles = self.options.energetic_ions.particles + u = self.variables.u.spline.vector + self._b_eq = self.projected_equil.b2 + self._b_tilde = self.options.b_tilde.spline.vector # if self._particles.control_variate: @@ -1480,65 +1766,70 @@ def __init__( # self._particles.f0.n, *quad_pts, kind='3', squeeze_out=False) # # memory allocation of magnetic field at quadrature points - # self._b_quad1 = np.zeros_like(self._nh0_at_quad) - # self._b_quad2 = np.zeros_like(self._nh0_at_quad) - # self._b_quad3 = np.zeros_like(self._nh0_at_quad) + # self._b_quad1 = xp.zeros_like(self._nh0_at_quad) + # self._b_quad2 = xp.zeros_like(self._nh0_at_quad) + # self._b_quad3 = xp.zeros_like(self._nh0_at_quad) # # memory allocation for self._b_quad x self._nh0_at_quad * self._coupling_const - # self._mat12 = np.zeros_like(self._nh0_at_quad) - # self._mat13 = np.zeros_like(self._nh0_at_quad) - # self._mat23 = np.zeros_like(self._nh0_at_quad) + # self._mat12 = xp.zeros_like(self._nh0_at_quad) + # self._mat13 = xp.zeros_like(self._nh0_at_quad) + # self._mat23 = xp.zeros_like(self._nh0_at_quad) + + # self._mat21 = xp.zeros_like(self._nh0_at_quad) + # self._mat31 = xp.zeros_like(self._nh0_at_quad) + # self._mat32 = xp.zeros_like(self._nh0_at_quad) - # self._mat21 = np.zeros_like(self._nh0_at_quad) - # self._mat31 = np.zeros_like(self._nh0_at_quad) - # self._mat32 = np.zeros_like(self._nh0_at_quad) + self._type = self.options.solver + self._tol = self.options.solver_params.tol + self._maxiter = self.options.solver_params.maxiter + self._info = self.options.solver_params.info + self._verbose = self.options.solver_params.verbose + self._recycle = self.options.solver_params.recycle - self._type = solver["type"][0] - self._tol = solver["tol"] - self._maxiter = solver["maxiter"] - self._info = solver["info"] - self._verbose = solver["verbose"] + Ah = self.options.energetic_ions.species.mass_number + Ab = self.variables.u.species.mass_number + epsilon = self.options.energetic_ions.species.equation_params.epsilon self._coupling_const = Ah / Ab / epsilon - self._boundary_cut_e1 = boundary_cut["e1"] + self._boundary_cut_e1 = self.options.boundary_cut[0] # load accumulator self._accumulator = Accumulator( particles, - u_space, + self.options.u_space, Pyccelkernel(accum_kernels.cc_lin_mhd_6d_1), self.mass_ops, self.domain.args_domain, add_vector=False, symmetry="asym", - filter_params=filter, + filter_params=self.options.filter_params, ) # transposed extraction operator PolarVector --> BlockVector (identity map in case of no polar splines) self._E2T = self.derham.extraction_ops["2"].transpose() # mass matrix in system (M - dt/2 * A)*u^(n + 1) = (M + dt/2 * A)*u^n - u_id = self.derham.space_to_form[u_space] + u_id = self.derham.space_to_form[self.options.u_space] self._M = getattr(self.mass_ops, "M" + u_id + "n") # preconditioner - if solver["type"][1] is None: + if self.options.precond is None: pc = None else: - pc_class = getattr(preconditioner, solver["type"][1]) + pc_class = getattr(preconditioner, self.options.precond) pc = pc_class(self._M) # linear solver self._solver = inverse( self._M, - solver["type"][0], + self.options.solver, pc=pc, - x0=self.feec_vars[0], + x0=self.variables.u.spline.vector, tol=self._tol, maxiter=self._maxiter, verbose=self._verbose, - recycle=solver["recycle"], + recycle=self._recycle, ) # temporary vectors to avoid memory allocation @@ -1550,7 +1841,7 @@ def __init__( def __call__(self, dt): # pointer to old coefficients - un = self.feec_vars[0] + un = self.variables.u.spline.vector # sum up total magnetic field b_full1 = b_eq + b_tilde (in-place) self._b_eq.copy(out=self._b_full1) @@ -1613,7 +1904,7 @@ def __call__(self, dt): info = self._solver._info # write new coeffs into Propagator.variables - max_du = self.feec_vars_update(un1) + max_du = self.update_feec_variables(u=un1) if self._info and MPI.COMM_WORLD.Get_rank() == 0: print("Status for CurrentCoupling6DDensity:", info["success"]) @@ -1630,9 +1921,9 @@ class ShearAlfvenCurrentCoupling5D(Propagator): \left\{ \begin{aligned} - \int \rho_0 &\frac{\partial \tilde{\mathbf U}}{\partial t} \cdot \mathbf V \, \textnormal{d} \mathbf{x} = \int \left(\tilde{\mathbf B} - \frac{A_\textnormal{h}}{A_b} \iint f^\text{vol} \mu \mathbf{b}_0\textnormal{d} \mu \textnormal{d} v_\parallel \right) \cdot \nabla \times (\mathbf B_0 \times \mathbf V) \, \textnormal{d} \mathbf{x} \quad \forall \, \mathbf V \in \{H(\textnormal{curl}), H(\textnormal{div}), (H^1)^3\}\,, \,, + \int \rho_0 &\frac{\partial \tilde{\mathbf U}}{\partial t} \cdot \mathbf V \, \textnormal{d} \mathbf{x} = \int \left(\tilde{\mathbf B} - \frac{A_\textnormal{h}}{A_b} \iint f^\text{vol} \mu \mathbf{b}_0\textnormal{d} \mu \textnormal{d} v_\parallel \right) \cdot \nabla \times (\tilde{\mathbf B} \times \mathbf V) \, \textnormal{d} \mathbf{x} \quad \forall \, \mathbf V \in \{H(\textnormal{curl}), H(\textnormal{div}), (H^1)^3\}\,, \,, \\ - &\frac{\partial \tilde{\mathbf B}}{\partial t} = - \nabla \times (\mathbf B_0 \times \tilde{\mathbf U}) \,. + &\frac{\partial \tilde{\mathbf B}}{\partial t} = - \nabla \times (\tilde{\mathbf B} \times \tilde{\mathbf U}) \,. \end{aligned} \right. @@ -1645,499 +1936,242 @@ class ShearAlfvenCurrentCoupling5D(Propagator): \end{bmatrix} = \frac{\Delta t}{2} \,. \begin{bmatrix} - 0 & (\mathbb M^{\alpha,n})^{-1} \mathcal {T^\alpha}^\top \mathbb C^\top \\ - \mathbb C \mathcal {T^\alpha} (\mathbb M^{\alpha,n})^{-1} & 0 + 0 & (\mathbb M^{2,n})^{-1} \mathcal {T^2}^\top \mathbb C^\top \\ - \mathbb C \mathcal {T^2} (\mathbb M^{2,n})^{-1} & 0 \end{bmatrix} \begin{bmatrix} - {\mathbb M^{\alpha,n}}(\mathbf u^{n+1} + \mathbf u^n) \\ \mathbb M_2(\mathbf b^{n+1} + \mathbf b^n) + \sum_k^{N_p} \omega_k \mu_k \hat{\mathbf b}¹_0 (\boldsymbol \eta_k) \cdot \left(\frac{1}{\sqrt{g(\boldsymbol \eta_k)}} \vec \Lambda² (\boldsymbol \eta_k) \right) + {\mathbb M^{2,n}}(\mathbf u^{n+1} + \mathbf u^n) \\ \mathbb M_2(\mathbf b^{n+1} + \mathbf b^n) + \sum_k^{N_p} \omega_k \mu_k \hat{\mathbf b}¹_0 (\boldsymbol \eta_k) \cdot \left(\frac{1}{\sqrt{g(\boldsymbol \eta_k)}} \vec \Lambda² (\boldsymbol \eta_k) \right) \end{bmatrix} \,, where - :math:`\mathcal{T}^\alpha` is a :class:`~struphy.feec.basis_projection_ops.BasisProjectionOperators` and - :math:`\mathbb M^{\alpha,n}` is a :class:`~struphy.feec.mass.WeightedMassOperators` being weighted with :math:`\rho_\text{eq}`, the MHD equilibirum density. - :math:`\alpha \in \{1, 2, v\}` denotes the :math:`\alpha`-form space where the operators correspond to. - Moreover, :math:`\sum_k^{N_p} \omega_k \mu_k \hat{\mathbf b}¹_0 (\boldsymbol \eta_k) \cdot \left(\frac{1}{\sqrt{g(\boldsymbol \eta_k)}} \vec \Lambda² (\boldsymbol \eta_k)\right)` is accumulated by the kernel :class:`~struphy.pic.accumulation.accum_kernels_gc.cc_lin_mhd_5d_M`. + :math:`\mathcal{T}^2 = \hat \Pi \left[\frac{\tilde{\mathbf B}^2}{\sqrt{g} \times \vec \Lambda^2\right]` and + :math:`\mathbb M^{2,n}` is a :class:`~struphy.feec.mass.WeightedMassOperators` being weighted with :math:`\rho_\text{eq}`, the MHD equilibirum density. """ - @staticmethod - def options(default=False): - dct = {} - dct["solver"] = { - "type": [ - ("pcg", "MassMatrixDiagonalPreconditioner"), - ("cg", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - dct["filter"] = { - "use_filter": None, - "modes": (1), - "repeat": 1, - "alpha": 0.5, - } - dct["boundary_cut"] = { - "e1": 0.0, - "e2": 0.0, - "e3": 0.0, - } - dct["turn_off"] = False - - if default: - dct = descend_options_dict(dct, []) + class Variables: + def __init__(self): + self._u: FEECVariable = None + self._b: FEECVariable = None + + @property + def u(self) -> FEECVariable: + return self._u + + @u.setter + def u(self, new): + assert isinstance(new, FEECVariable) + assert new.space in ("Hcurl", "Hdiv", "H1vec") + self._u = new + + @property + def b(self) -> FEECVariable: + return self._b + + @b.setter + def b(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "Hdiv" + self._b = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # specific literals + OptsAlgo = Literal["implicit", "explicit"] + # propagator options + energetic_ions: PICVariable = None + ep_scale: float = 1.0 + u_space: OptsVecSpace = "Hdiv" + algo: OptsAlgo = "implicit" + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixDiagonalPreconditioner" + solver_params: SolverParameters = None + filter_params: FilterParameters = None + butcher: ButcherTableau = None + nonlinear: bool = True + + def __post_init__(self): + # checks + check_option(self.u_space, OptsVecSpace) + check_option(self.algo, self.OptsAlgo) + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + assert isinstance(self.energetic_ions, PICVariable) + assert self.energetic_ions.space == "Particles5D" + assert isinstance(self.ep_scale, float) + assert isinstance(self.nonlinear, bool) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + if self.filter_params is None: + self.filter_params = FilterParameters() + + if self.algo == "explicit" and self.butcher is None: + self.butcher = ButcherTableau() - return dct - - def __init__( - self, - u: BlockVector, - b: BlockVector, - *, - particles: Particles5D, - absB0: StencilVector, - unit_b1: BlockVector, - u_space: str, - solver: dict = options(default=True)["solver"], - filter: dict = options(default=True)["filter"], - coupling_params: dict, - accumulated_magnetization: BlockVector, - boundary_cut: dict = options(default=True)["boundary_cut"], - ): - super().__init__(u, b) - - self._particles = particles - self._unit_b1 = unit_b1 - self._absB0 = absB0 + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + self._u_form = self.derham.space_to_form[self.options.u_space] + + # call operatros + id_M = "M" + self._u_form + "n" + id_T = "T" + self._u_form - self._info = solver["info"] + _A = getattr(self.mass_ops, id_M) + _T = getattr(self.basis_ops, id_T) + M2 = self.mass_ops.M2 + curl = self.derham.curl + PB = getattr(self.basis_ops, "PB") - self._scale_vec = coupling_params["Ah"] / coupling_params["Ab"] - - self._E1T = self.derham.extraction_ops["1"].transpose() - self._unit_b1 = self._E1T.dot(self._unit_b1) - - self._accumulated_magnetization = accumulated_magnetization - - self._boundary_cut_e1 = boundary_cut["e1"] - - self._ACC = Accumulator( - particles, - u_space, - Pyccelkernel(accum_kernels_gc.cc_lin_mhd_5d_M), - self.mass_ops, - self.domain.args_domain, - add_vector=True, - symmetry="symm", - filter_params=filter, - ) - - # if self._particles.control_variate: - - # # control variate method is only valid with Maxwellian distributions with "zero perp mean velocity". - # assert isinstance(self._particles.f0, Maxwellian) - - # self._ACC.init_control_variate(self.mass_ops) - - # # evaluate and save f0.n at quadrature points - # quad_pts = [quad_grid[nquad].points.flatten() - # for quad_grid, nquad in zip(self.derham.get_quad_grids(self.derham.Vh_fem['0']), self.derham.nquads)] - - # n0_at_quad = self.domain.push( - # self._particles.f0.n, *quad_pts, kind='0', squeeze_out=False) - - # # evaluate M0 = unit_b1 (1form) / absB0 (0form) * 2 * vth_perp² at quadrature points - # quad_pts_array = self.domain.prepare_eval_pts(*quad_pts)[:3] - - # vth_perp = self.particles.f0.vth(*quad_pts_array)[1] - - # absB0_at_quad = WeightedMassOperator.eval_quad(self.derham.Vh_fem['0'], self._absB0) - - # unit_b1_at_quad = WeightedMassOperator.eval_quad(self.derham.Vh_fem['1'], self._unit_b1) - - # self._M0_at_quad = unit_b1_at_quad / absB0_at_quad * vth_perp**2 * n0_at_quad * self._scale_vec - - # define block matrix [[A B], [C I]] (without time step size dt in the diagonals) - id_M = "M" + self.derham.space_to_form[u_space] + "n" - id_T = "T" + self.derham.space_to_form[u_space] - - _A = getattr(self.mass_ops, id_M) - _T = getattr(self.basis_ops, id_T) - - self._B = -1 / 2 * _T.T @ self.derham.curl.T @ self.mass_ops.M2 - self._C = 1 / 2 * self.derham.curl @ _T - self._B2 = -1 / 2 * _T.T @ self.derham.curl.T + # define Accumulator and arguments + self._ACC = AccumulatorVector( + self.options.energetic_ions.particles, + "H1", + Pyccelkernel(accum_kernels_gc.gc_mag_density_0form), + self.mass_ops, + self.domain.args_domain, + filter_params=self.options.filter_params, + ) # Preconditioner - if solver["type"][1] is None: + if self.options.precond is None: pc = None else: - pc_class = getattr(preconditioner, solver["type"][1]) + pc_class = getattr(preconditioner, self.options.precond) pc = pc_class(getattr(self.mass_ops, id_M)) - # Instantiate Schur solver (constant in this case) - _BC = self._B @ self._C - - self._schur_solver = SchurSolver( - _A, - _BC, - solver["type"][0], - pc=pc, - tol=solver["tol"], - maxiter=solver["maxiter"], - verbose=solver["verbose"], - recycle=solver["recycle"], - ) - - # allocate dummy vectors to avoid temporary array allocations - self._u_tmp1 = u.space.zeros() - self._u_tmp2 = u.space.zeros() - self._b_tmp1 = b.space.zeros() - - self._byn = self._B.codomain.zeros() - self._tmp_acc = self._B2.codomain.zeros() - - def __call__(self, dt): - # current variables - un = self.feec_vars[0] - bn = self.feec_vars[1] - - # perform accumulation (either with or without control variate) - # if self._particles.control_variate: - - # self._ACC.accumulate(self._particles, - # self._unit_b1[0]._data, self._unit_b1[1]._data, self._unit_b1[2]._data, - # self._scale_vec, 0., - # control_vec=[self._M0_at_quad[0], self._M0_at_quad[1], self._M0_at_quad[2]]) - # else: - # self._ACC.accumulate(self._particles, - # self._unit_b1[0]._data, self._unit_b1[1]._data, self._unit_b1[2]._data, - # self._scale_vec, 0.) - - self._ACC( - self._unit_b1[0]._data, - self._unit_b1[1]._data, - self._unit_b1[2]._data, - self._scale_vec, - self._boundary_cut_e1, - ) - - self._ACC.vectors[0].copy(out=self._accumulated_magnetization) - - # solve for new u coeffs (no tmps created here) - byn = self._B.dot(bn, out=self._byn) - b2acc = self._B2.dot(self._ACC.vectors[0], out=self._tmp_acc) - byn += b2acc - - # b2acc.copy(out=self._accumulated_magnetization) - - un1, info = self._schur_solver(un, byn, dt, out=self._u_tmp1) - - # new b coeffs (no tmps created here) - _u = un.copy(out=self._u_tmp2) - _u += un1 - bn1 = self._C.dot(_u, out=self._b_tmp1) - bn1 *= -dt - bn1 += bn - - # write new coeffs into self.feec_vars - max_du, max_db = self.feec_vars_update(un1, bn1) - - if self._info and MPI.COMM_WORLD.Get_rank() == 0: - print("Status for ShearAlfven:", info["success"]) - print("Iterations for ShearAlfven:", info["niter"]) - print("Maxdiff up for ShearAlfven:", max_du) - print("Maxdiff b2 for ShearAlfven:", max_db) - print() - - -class MagnetosonicCurrentCoupling5D(Propagator): - r""" - :ref:`FEEC ` discretization of the following equations: - find :math:`\tilde \rho \in L^2, \tilde{\mathbf U} \in \{H(\textnormal{curl}), H(\textnormal{div}), (H^1)^3\}, \tilde p \in L^2` such that - - .. math:: - - \left\{ - \begin{aligned} - &\frac{\partial \tilde{\rho}}{\partial t} = - \nabla \cdot (\rho_0 \tilde{\mathbf U}) \,, - \\ - \int \rho_0 &\frac{\partial \tilde{\mathbf U}}{\partial t} \cdot \mathbf V \, \textnormal{d} \mathbf{x} = \int (\nabla \times \mathbf B_0) \times \tilde{\mathbf B} \cdot \mathbf V \, \textnormal{d} \mathbf x + \frac{A_\textnormal{h}}{A_b}\iint f^\text{vol} \mu \mathbf b_0 \cdot \nabla \times (\tilde{\mathbf B} \times \mathbf V) \, \textnormal{d} \mathbf x \textnormal{d} v_\parallel \textnormal{d} \mu + \int \tilde p \nabla \cdot \mathbf V \, \textnormal{d} \mathbf x \qquad \forall \, \mathbf V \in \{H(\textnormal{curl}), H(\textnormal{div}), (H^1)^3\}\,, - \\ - &\frac{\partial \tilde p}{\partial t} = - \nabla \cdot (p_0 \tilde{\mathbf U}) - (\gamma - 1) p_0 \nabla \cdot \tilde{\mathbf U} \,. - \end{aligned} - \right. - - :ref:`time_discret`: Crank-Nicolson (implicit mid-point). System size reduction via :class:`~struphy.linear_algebra.schur_solver.SchurSolver`: - - .. math:: - - \boldsymbol{\rho}^{n+1} - \boldsymbol{\rho}^n = - \frac{\Delta t}{2} \mathbb D \mathcal Q^\alpha (\mathbf u^{n+1} + \mathbf u^n) \,, - - .. math:: - - \begin{bmatrix} - \mathbf u^{n+1} - \mathbf u^n \\ \mathbf p^{n+1} - \mathbf p^n - \end{bmatrix} - = \frac{\Delta t}{2} - \begin{bmatrix} - 0 & (\mathbb M^{\alpha,n})^{-1} {\mathcal U^\alpha}^\top \mathbb D^\top \mathbb M_3 \\ - \mathbb D \mathcal S^\alpha - (\gamma - 1) \mathcal K^\alpha \mathbb D \mathcal U^\alpha & 0 - \end{bmatrix} - \begin{bmatrix} - (\mathbf u^{n+1} + \mathbf u^n) \\ (\mathbf p^{n+1} + \mathbf p^n) - \end{bmatrix} + - \begin{bmatrix} - \Delta t (\mathbb M^{\alpha,n})^{-1}\left[\mathbb M^{\alpha,J} \mathbf b^n + \frac{A_\textnormal{h}}{A_b}{\mathcal{T}^B}^\top \mathbb{C}^\top \sum_k^{N_p} \omega_k \mu_k \hat{\mathbf b}¹_0 (\boldsymbol \eta_k) \cdot \left(\frac{1}{\sqrt{g(\boldsymbol \eta_k)}} \vec \Lambda² (\boldsymbol \eta_k) \right)\right] \\ 0 - \end{bmatrix} \,, - - where - :math:`\mathcal U^\alpha`, :math:`\mathcal S^\alpha`, :math:`\mathcal K^\alpha` and :math:`\mathcal Q^\alpha` are :class:`~struphy.feec.basis_projection_ops.BasisProjectionOperators` and - :math:`\mathbb M^{\alpha,n}` and :math:`\mathbb M^{\alpha,J}` are :class:`~struphy.feec.mass.WeightedMassOperators` being weighted with :math:`\rho_0` the MHD equilibrium density. - :math:`\alpha \in \{1, 2, v\}` denotes the :math:`\alpha`-form space where the operators correspond to. - Moreover, :math:`\sum_k^{N_p} \omega_k \mu_k \hat{\mathbf b}¹_0 (\boldsymbol \eta_k) \cdot \left(\frac{1}{\sqrt{g(\boldsymbol \eta_k)}} \vec \Lambda² (\boldsymbol \eta_k)\right)` is accumulated by the kernel :class:`~struphy.pic.accumulation.accum_kernels_gc.cc_lin_mhd_5d_M` and - the time-varying projection operator :math:`\mathcal{T}^B` is defined as - - .. math:: - - \mathcal{T}^B_{(\mu,ijk),(\nu,mno)} := \hat \Pi¹_{(\mu,ijk)} \left[ \epsilon_{\mu \alpha \nu} \frac{\tilde{B}^2_\alpha}{\sqrt{g}} \Lambda²_{\nu,mno} \right] \,. - """ - - @staticmethod - def options(default=False): - dct = {} - dct["solver"] = { - "type": [ - ("pbicgstab", "MassMatrixPreconditioner"), - ("bicgstab", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - dct["filter"] = { - "use_filter": None, - "modes": (0, 1), - "repeat": 3, - "alpha": 0.5, - } - dct["boundary_cut"] = { - "e1": 0.0, - "e2": 0.0, - "e3": 0.0, - } - dct["turn_off"] = False - - if default: - dct = descend_options_dict(dct, []) + if self.options.nonlinear: + # initialize operator TB + self._initialize_projection_operator_TB() - return dct + _T = _T + self._TB + _TT = _T.T + self._TBT - def __init__( - self, - n: StencilVector, - u: BlockVector, - p: StencilVector, - *, - particles: Particles5D, - b: BlockVector, - absB0: StencilVector, - unit_b1: BlockVector, - u_space: str, - solver: dict = options(default=True)["solver"], - filter: dict = options(default=True)["filter"], - coupling_params: dict, - boundary_cut: dict = options(default=True)["boundary_cut"], - ): - super().__init__(n, u, p) - - self._particles = particles - self._b = b - self._unit_b1 = unit_b1 - self._absB0 = absB0 - - self._info = solver["info"] - - self._scale_vec = coupling_params["Ah"] / coupling_params["Ab"] - - self._E1T = self.derham.extraction_ops["1"].transpose() - self._unit_b1 = self._E1T.dot(self._unit_b1) - - self._u_id = self.derham.space_to_form[u_space] - if self._u_id == "v": - self._space_key_int = 0 else: - self._space_key_int = int(self._u_id) - - self._boundary_cut_e1 = boundary_cut["e1"] - - self._ACC = Accumulator( - particles, - u_space, - Pyccelkernel(accum_kernels_gc.cc_lin_mhd_5d_M), - self.mass_ops, - self.domain.args_domain, - add_vector=True, - symmetry="symm", - filter_params=filter, - ) - - # if self._particles.control_variate: - - # # control variate method is only valid with Maxwellian distributions with "zero perp mean velocity". - # assert isinstance(self._particles.f0, Maxwellian) - - # self._ACC.init_control_variate(self.mass_ops) - - # # evaluate and save f0.n at quadrature points - # quad_pts = [quad_grid[nquad].points.flatten() - # for quad_grid, nquad in zip(self.derham.get_quad_grids(self.derham.Vh_fem['0']), self.derham.nquads)] - - # n0_at_quad = self.domain.push( - # self._particles.f0.n, *quad_pts, kind='0', squeeze_out=False) + _TT = _T.T - # # evaluate M0 = unit_b1 (1form) / absB0 (0form) * 2 * vth_perp² at quadrature points - # quad_pts_array = self.domain.prepare_eval_pts(*quad_pts)[:3] + if self.options.algo == "implicit": + self._info = self.options.solver_params.info - # vth_perp = self.particles.f0.vth(*quad_pts_array)[1] + # define block matrix [[A B], [C I]] (without time step size dt in the diagonals) + self._B = -1 / 2 * _TT @ curl.T @ M2 + self._B2 = -1 / 2 * _TT @ curl.T @ PB.T - # absB0_at_quad = WeightedMassOperator.eval_quad(self.derham.Vh_fem['0'], self._absB0) - - # unit_b1_at_quad = WeightedMassOperator.eval_quad(self.derham.Vh_fem['1'], self._unit_b1) - - # self._M0_at_quad = unit_b1_at_quad / absB0_at_quad * vth_perp**2 * n0_at_quad * self._scale_vec - - # define block matrix [[A B], [C I]] (without time step size dt in the diagonals) - id_Mn = "M" + self._u_id + "n" - id_MJ = "M" + self._u_id + "J" + self._C = 1 / 2 * curl @ _T - if self._u_id == "1": - id_S, id_U, id_K, id_Q = "S1", "U1", "K3", "Q1" - elif self._u_id == "2": - id_S, id_U, id_K, id_Q = "S2", None, "K3", "Q2" - elif self._u_id == "v": - id_S, id_U, id_K, id_Q = "Sv", "Uv", "K3", "Qv" + # Instantiate Schur solver (constant in this case) + _BC = self._B @ self._C - self._E2T = self.derham.extraction_ops["2"].transpose() + self._schur_solver = SchurSolver( + _A, + _BC, + self.options.solver, + precond=pc, + solver_params=self.options.solver_params, + ) - _A = getattr(self.mass_ops, id_Mn) - _S = getattr(self.basis_ops, id_S) - _K = getattr(self.basis_ops, id_K) + # allocate dummy vectors to avoid temporary array allocations + self._u_tmp1 = self.variables.u.spline.vector.space.zeros() + self._u_tmp2 = self.variables.u.spline.vector.space.zeros() + self._b_tmp1 = self.variables.b.spline.vector.space.zeros() - # initialize projection operator TB - self._initialize_projection_operator_TB() + self._byn = self._B.codomain.zeros() + self._tmp_acc = self._B2.codomain.zeros() - if id_U is None: - _U, _UT = IdentityOperator(u.space), IdentityOperator(u.space) else: - _U = getattr(self.basis_ops, id_U) - _UT = _U.T - - self._B = -1 / 2.0 * _UT @ self.derham.div.T @ self.mass_ops.M3 - self._C = 1 / 2.0 * (self.derham.div @ _S + 2 / 3.0 * _K @ self.derham.div @ _U) - - self._MJ = getattr(self.mass_ops, id_MJ) - self._DQ = self.derham.div @ getattr(self.basis_ops, id_Q) + self._info = False - self._TC = self._TB.T @ self.derham.curl.T + # define vector field + A_inv = inverse( + _A, + self.options.solver, + pc=pc, + tol=self.options.solver_params.tol, + maxiter=self.options.solver_params.maxiter, + verbose=self.options.solver_params.verbose, + ) + _f1 = A_inv @ _TT @ curl.T @ M2 + _f1_acc = A_inv @ _TT @ curl.T @ PB.T + _f2 = curl @ _T - # preconditioner - if solver["type"][1] is None: - pc = None - else: - pc_class = getattr(preconditioner, solver["type"][1]) - pc = pc_class(getattr(self.mass_ops, id_Mn)) + # allocate output of vector field + out_acc = self.variables.u.spline.vector.space.zeros() + out1 = self.variables.u.spline.vector.space.zeros() + out2 = self.variables.b.spline.vector.space.zeros() - # instantiate Schur solver (constant in this case) - _BC = self._B @ self._C + def f1(t, y1, y2, out: BlockVector = out1): + _f1.dot(y2, out=out) + _f1_acc.dot(self._ACC.vectors[0], out=out_acc) + out += out_acc + out.update_ghost_regions() + return out - self._schur_solver = SchurSolver( - _A, - _BC, - solver["type"][0], - pc=pc, - tol=solver["tol"], - maxiter=solver["maxiter"], - verbose=solver["verbose"], - recycle=solver["recycle"], - ) + def f2(t, y1, y2, out: BlockVector = out2): + _f2.dot(y1, out=out) + out *= -1.0 + out.update_ghost_regions() + return out - # allocate dummy vectors to avoid temporary array allocations - self._u_tmp1 = u.space.zeros() - self._u_tmp2 = u.space.zeros() - self._p_tmp1 = p.space.zeros() - self._n_tmp1 = n.space.zeros() - self._byn1 = self._B.codomain.zeros() - self._byn2 = self._B.codomain.zeros() - self._tmp_acc = self._TC.codomain.zeros() + vector_field = {self.variables.u.spline.vector: f1, self.variables.b.spline.vector: f2} + self._ode_solver = ODEsolverFEEC(vector_field, butcher=self.options.butcher) def __call__(self, dt): - # current variables - nn = self.feec_vars[0] - un = self.feec_vars[1] - pn = self.feec_vars[2] - - # perform accumulation (either with or without control variate) - # if self._particles.control_variate: + # update time-dependent operator TB + if self.options.nonlinear: + self._update_weights_TB() - # self._ACC.accumulate(self._particles, - # self._unit_b1[0]._data, self._unit_b1[1]._data, self._unit_b1[2]._data, - # self._scale_vec, 0., - # control_vec=[self._M0_at_quad[0], self._M0_at_quad[1], self._M0_at_quad[2]]) - # else: - # self._ACC.accumulate(self._particles, - # self._unit_b1[0]._data, self._unit_b1[1]._data, self._unit_b1[2]._data, - # self._scale_vec, 0.) + # current FE coeffs + un = self.variables.u.spline.vector + bn = self.variables.b.spline.vector - self._ACC( - self._unit_b1[0]._data, - self._unit_b1[1]._data, - self._unit_b1[2]._data, - self._scale_vec, - self._boundary_cut_e1, - ) + # accumulate + self._ACC(self.options.ep_scale) - # update time-dependent operator - self._b.update_ghost_regions() - self._update_weights_TB() - - # solve for new u coeffs (no tmps created here) - byn1 = self._B.dot(pn, out=self._byn1) - byn2 = self._MJ.dot(self._b, out=self._byn2) - b2acc = self._TC.dot(self._ACC.vectors[0], out=self._tmp_acc) - byn2 += b2acc - byn2 *= 1 / 2 - byn1 -= byn2 + if self.options.algo == "implicit": + # solve for new u coeffs (no tmps created here) + byn = self._B.dot(bn, out=self._byn) + b2acc = self._B2.dot(self._ACC.vectors[0], out=self._tmp_acc) + byn += b2acc - un1, info = self._schur_solver(un, byn1, dt, out=self._u_tmp1) + un1, info = self._schur_solver(un, byn, dt, out=self._u_tmp1) - # new p, n, b coeffs (no tmps created here) - _u = un.copy(out=self._u_tmp2) - _u += un1 - pn1 = self._C.dot(_u, out=self._p_tmp1) - pn1 *= -dt - pn1 += pn + # new b coeffs (no tmps created here) + _u = un.copy(out=self._u_tmp2) + _u += un1 + bn1 = self._C.dot(_u, out=self._b_tmp1) + bn1 *= -dt + bn1 += bn - nn1 = self._DQ.dot(_u, out=self._n_tmp1) - nn1 *= -dt / 2 - nn1 += nn + diffs = self.update_feec_variables(u=un1, b=bn1) - # write new coeffs into self.feec_vars - max_dn, max_du, max_dp = self.feec_vars_update( - nn1, - un1, - pn1, - ) + else: + self._ode_solver(0.0, dt) if self._info and MPI.COMM_WORLD.Get_rank() == 0: - print("Status for Magnetosonic:", info["success"]) - print("Iterations for Magnetosonic:", info["niter"]) - print("Maxdiff n3 for Magnetosonic:", max_dn) - print("Maxdiff up for Magnetosonic:", max_du) - print("Maxdiff p3 for Magnetosonic:", max_dp) - print() + if self.options.algo == "implicit": + print("Status for ShearAlfvenCurrentCoupling5D:", info["success"]) + print("Iterations for ShearAlfvenCurrentCoupling5D:", info["niter"]) + print("Maxdiff up for ShearAlfvenCurrentCoupling5D:", diffs["u"]) + print("Maxdiff b2 for ShearAlfvenCurrentCoupling5D:", diffs["b"]) + print() def _initialize_projection_operator_TB(self): r"""Initialize BasisProjectionOperator TB with the time-varying weight. @@ -2150,27 +2184,80 @@ def _initialize_projection_operator_TB(self): # Call the projector and the space P1 = self.derham.P["1"] - Vh = self.derham.Vh_fem[self._u_id] + Vh = self.derham.Vh_fem[self._u_form] # Femfield for the field evaluation self._bf = self.derham.create_spline_function("bf", "Hdiv") - # define temp callable - def tmp(x, y, z): - return 0 * x - # Initialize BasisProjectionOperator - if self.derham._with_local_projectors: - self._TB = BasisProjectionOperatorLocal(P1, Vh, [[tmp, tmp, tmp]]) + if self.derham._with_local_projectors == True: + self._TB = BasisProjectionOperatorLocal( + P1, + Vh, + [ + [None, None, None], + [None, None, None], + [None, None, None], + ], + transposed=False, + use_cache=True, + polar_shift=True, + V_extraction_op=self.derham.extraction_ops[self._u_form], + V_boundary_op=self.derham.boundary_ops[self._u_form], + P_boundary_op=self.derham.boundary_ops["1"], + ) + self._TBT = BasisProjectionOperatorLocal( + P1, + Vh, + [ + [None, None, None], + [None, None, None], + [None, None, None], + ], + transposed=True, + use_cache=True, + polar_shift=True, + V_extraction_op=self.derham.extraction_ops[self._u_form], + V_boundary_op=self.derham.boundary_ops[self._u_form], + P_boundary_op=self.derham.boundary_ops["1"], + ) else: - self._TB = BasisProjectionOperator(P1, Vh, [[tmp, tmp, tmp]]) + self._TB = BasisProjectionOperator( + P1, + Vh, + [ + [None, None, None], + [None, None, None], + [None, None, None], + ], + transposed=False, + use_cache=True, + polar_shift=True, + V_extraction_op=self.derham.extraction_ops[self._u_form], + V_boundary_op=self.derham.boundary_ops[self._u_form], + P_boundary_op=self.derham.boundary_ops["1"], + ) + self._TBT = BasisProjectionOperator( + P1, + Vh, + [ + [None, None, None], + [None, None, None], + [None, None, None], + ], + transposed=True, + use_cache=True, + polar_shift=True, + V_extraction_op=self.derham.extraction_ops[self._u_form], + V_boundary_op=self.derham.boundary_ops[self._u_form], + P_boundary_op=self.derham.boundary_ops["1"], + ) def _update_weights_TB(self): """Updats time-dependent weights of the BasisProjectionOperator TB""" # Update Femfield - self._bf.vector = self._b - self._bf.vector.update_ghost_regions() + self.variables.b.spline.vector.copy(out=self._bf.vector) # define callable weights def bf1(x, y, z): @@ -2188,7 +2275,7 @@ def bf3(x, y, z): fun = [] - if self._u_id == "v": + if self._u_form == "v": for m in range(3): fun += [[]] for n in range(3): @@ -2196,7 +2283,7 @@ def bf3(x, y, z): lambda e1, e2, e3, m=m, n=n: rot_B(e1, e2, e3)[:, :, :, m, n], ] - elif self._u_id == "1": + elif self._u_form == "1": for m in range(3): fun += [[]] for n in range(3): @@ -2222,8 +2309,9 @@ def bf3(x, y, z): / abs(self.domain.jacobian_det(e1, e2, e3, squeeze_out=False)), ] - # Initialize BasisProjectionOperator + # update BasisProjectionOperator self._TB.update_weights(fun) + self._TBT.update_weights(fun) class CurrentCoupling5DDensity(Propagator): @@ -2243,268 +2331,166 @@ class CurrentCoupling5DDensity(Propagator): For the detail explanation of the notations, see `2022_DriftKineticCurrentCoupling `_. """ - @staticmethod - def options(default=False): - dct = {} - dct["solver"] = { - "type": [ - ("pbicgstab", "MassMatrixPreconditioner"), - ("bicgstab", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - dct["filter"] = { - "use_filter": None, - "modes": (1), - "repeat": 1, - "alpha": 0.5, - } - dct["boundary_cut"] = { - "e1": 0.0, - "e2": 0.0, - "e3": 0.0, - } - dct["turn_off"] = False - - if default: - dct = descend_options_dict(dct, []) - - return dct + class Variables: + def __init__(self): + self._u: FEECVariable = None + + @property + def u(self) -> FEECVariable: + return self._u + + @u.setter + def u(self, new): + assert isinstance(new, FEECVariable) + assert new.space in ("Hcurl", "Hdiv", "H1vec") + self._u = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # propagator options + energetic_ions: PICVariable = None + b_tilde: FEECVariable = None + ep_scale: float = 1.0 + u_space: OptsVecSpace = "Hdiv" + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + filter_params: FilterParameters = None + + def __post_init__(self): + # checks + check_option(self.u_space, OptsVecSpace) + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + assert isinstance(self.energetic_ions, PICVariable) + assert self.energetic_ions.space == "Particles5D" + assert isinstance(self.b_tilde, FEECVariable) + assert isinstance(self.ep_scale, float) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + if self.filter_params is None: + self.filter_params = FilterParameters() - def __init__( - self, - u: BlockVector, - *, - particles: Particles5D, - b: BlockVector, - b_eq: BlockVector, - unit_b1: BlockVector, - curl_unit_b2: BlockVector, - u_space: str, - solver: dict = options(default=True)["solver"], - coupling_params: dict, - epsilon: float = 1.0, - filter: dict = options(default=True)["filter"], - boundary_cut: dict = options(default=True)["boundary_cut"], - ): - super().__init__(u) - - # assert parameters and expose some quantities to self - assert isinstance(particles, (Particles5D)) - - assert u_space in {"Hcurl", "Hdiv", "H1vec"} - - if u_space == "H1vec": - self._space_key_int = 0 + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + if self.options.u_space == "H1vec": + self._u_form_int = 0 else: - self._space_key_int = int( - self.derham.space_to_form[u_space], - ) + self._u_form_int = int(self.derham.space_to_form[self.options.u_space]) - self._epsilon = epsilon - self._particles = particles - self._b = b - self._b_eq = b_eq - self._unit_b1 = unit_b1 - self._curl_norm_b = curl_unit_b2 + # call operatros + id_M = "M" + self.derham.space_to_form[self.options.u_space] + "n" + self._A = getattr(self.mass_ops, id_M) - self._info = solver["info"] + # magnetic equilibrium field + unit_b1 = self.projected_equil.unit_b1 + curl_unit_b1 = self.projected_equil.curl_unit_b1 + self._b2 = self.projected_equil.b2 - self._scale_mat = coupling_params["Ah"] / coupling_params["Ab"] / self._epsilon + # scaling factor + epsilon = self.options.energetic_ions.species.equation_params.epsilon - self._boundary_cut_e1 = boundary_cut["e1"] + # temporary vectors to avoid memory allocation + self._b_full = self._b2.space.zeros() + self._rhs_v = self.variables.u.spline.vector.space.zeros() + self._u_new = self.variables.u.spline.vector.space.zeros() - self._accumulator = Accumulator( - particles, - u_space, + # define Accumulator and arguments + self._ACC = Accumulator( + self.options.energetic_ions.particles, + self.options.u_space, Pyccelkernel(accum_kernels_gc.cc_lin_mhd_5d_D), self.mass_ops, self.domain.args_domain, add_vector=False, symmetry="asym", - filter_params=filter, + filter_params=self.options.filter_params, ) - # if self._particles.control_variate: - - # # control variate method is only valid with Maxwellian distributions - # assert isinstance(self._particles.f0, Maxwellian) - # assert params['u_space'] == 'Hdiv' - - # # evaluate and save f0.n / |det(DF)| at quadrature points - # quad_pts = [quad_grid[nquad].points.flatten() - # for quad_grid, nquad in zip(self.derham.get_quad_grids(self.derham.Vh_fem['0']), self.derham.nquads)] - - # self._n0_at_quad = self.domain.push( - # self._particles.f0.n, *quad_pts, kind='3', squeeze_out=False) - - # # prepare field evaluation - # quad_pts_array = self.domain.prepare_eval_pts(*quad_pts)[:3] - - # u0_parallel = self._particles.f0.u(*quad_pts_array)[0] - - # det_df_at_quad = self.domain.jacobian_det(*quad_pts, squeeze_out=False) - - # # evaluate unit_b1 / |det(DF)| at quadrature points - # self._unit_b1_at_quad = WeightedMassOperator.eval_quad(self.derham.Vh_fem['1'], self._unit_b1) - # self._unit_b1_at_quad /= det_df_at_quad - - # # evaluate unit_b1 (1form) dot epsilon * f0.u * curl_norm_b (2form) / |det(DF)| at quadrature points - # curl_norm_b_at_quad = WeightedMassOperator.eval_quad(self.derham.Vh_fem['2'], self._curl_norm_b) - - # self._unit_b1_dot_curl_norm_b_at_quad = np.sum(p * q for p, q in zip(self._unit_b1_at_quad, curl_norm_b_at_quad)) - - # self._unit_b1_dot_curl_norm_b_at_quad /= det_df_at_quad - # self._unit_b1_dot_curl_norm_b_at_quad *= self._epsilon - # self._unit_b1_dot_curl_norm_b_at_quad *= u0_parallel - - # # memory allocation for magnetic field at quadrature points - # self._b_quad1 = np.zeros_like(self._n0_at_quad) - # self._b_quad2 = np.zeros_like(self._n0_at_quad) - # self._b_quad3 = np.zeros_like(self._n0_at_quad) - - # # memory allocation for parallel magnetic field at quadrature points - # self._B_para = np.zeros_like(self._n0_at_quad) - - # # memory allocation for control_const at quadrature points - # self._control_const = np.zeros_like(self._n0_at_quad) - - # # memory allocation for self._b_quad x self._nh0_at_quad * self._coupling_const - # self._mat12 = np.zeros_like(self._n0_at_quad) - # self._mat13 = np.zeros_like(self._n0_at_quad) - # self._mat23 = np.zeros_like(self._n0_at_quad) - - # self._mat21 = np.zeros_like(self._n0_at_quad) - # self._mat31 = np.zeros_like(self._n0_at_quad) - # self._mat32 = np.zeros_like(self._n0_at_quad) - - u_id = self.derham.space_to_form[u_space] - self._M = getattr(self.mass_ops, "M" + u_id + "n") - - self._E0T = self.derham.extraction_ops["0"].transpose() - self._EuT = self.derham.extraction_ops[u_id].transpose() - self._E1T = self.derham.extraction_ops["1"].transpose() - self._E2T = self.derham.extraction_ops["2"].transpose() - - self._PB = getattr(self.basis_ops, "PB") - self._unit_b1 = self._E1T.dot(self._unit_b1) + self._args_accum_kernel = ( + epsilon, + self.options.ep_scale, + self._b_full[0]._data, + self._b_full[1]._data, + self._b_full[2]._data, + unit_b1[0]._data, + unit_b1[1]._data, + unit_b1[2]._data, + curl_unit_b1[0]._data, + curl_unit_b1[1]._data, + curl_unit_b1[2]._data, + self._u_form_int, + ) - # preconditioner - if solver["type"][1] is None: - self._pc = None + # Preconditioner + if self.options.precond is None: + pc = None else: - pc_class = getattr(preconditioner, solver["type"][1]) - self._pc = pc_class(self._M) + pc_class = getattr(preconditioner, self.options.precond) + pc = pc_class(getattr(self.mass_ops, id_M)) # linear solver - self._solver = inverse( - self._M, - solver["type"][0], - pc=self._pc, - x0=self.feec_vars[0], - tol=solver["tol"], - maxiter=solver["maxiter"], - verbose=solver["verbose"], - recycle=solver["recycle"], + self._A_inv = inverse( + self._A, + self.options.solver, + pc=pc, + tol=self.options.solver_params.tol, + maxiter=self.options.solver_params.maxiter, + verbose=self.options.solver_params.verbose, ) - # temporary vectors to avoid memory allocation - self._b_full1 = self._b_eq.space.zeros() - self._b_full2 = self._E2T.codomain.zeros() - self._rhs_v = u.space.zeros() - self._u_new = u.space.zeros() - def __call__(self, dt): - # pointer to old coefficients - un = self.feec_vars[0] + # current FE coeffs + un = self.variables.u.spline.vector # sum up total magnetic field b_full1 = b_eq + b_tilde (in-place) - b_full = self._b_eq.copy(out=self._b_full1) - - if self._b is not None: - b_full += self._b - - Eb_full = self._E2T.dot(b_full, out=self._b_full2) - Eb_full.update_ghost_regions() - - # perform accumulation (either with or without control variate) - # if self._particles.control_variate: - - # # evaluate magnetic field at quadrature points (in-place) - # WeightedMassOperator.eval_quad(self.derham.Vh_fem['2'], self._b_full2, - # out=[self._b_quad1, self._b_quad2, self._b_quad3]) - - # # evaluate B_parallel - # self._B_para = np.sum(p * q for p, q in zip(self._unit_b1_at_quad, [self._b_quad1, self._b_quad2, self._b_quad3])) - - # # evaluate coupling_const 1 - B_parallel / B^star_parallel - # self._control_const = 1 - (self._B_para / (self._B_para + self._unit_b1_dot_curl_norm_b_at_quad)) - - # # assemble (B x) - # self._mat12[:, :, :] = self._scale_mat * \ - # self._b_quad3 * self._n0_at_quad * self._control_const - # self._mat13[:, :, :] = -self._scale_mat * \ - # self._b_quad2 * self._n0_at_quad * self._control_const - # self._mat23[:, :, :] = self._scale_mat * \ - # self._b_quad1 * self._n0_at_quad * self._control_const + b_full = self._b2.copy(out=self._b_full) - # self._mat21[:, :, :] = -self._mat12 - # self._mat31[:, :, :] = -self._mat13 - # self._mat32[:, :, :] = -self._mat23 - - # self._accumulator.accumulate(self._particles, self._epsilon, - # Eb_full[0]._data, Eb_full[1]._data, Eb_full[2]._data, - # self._unit_b1[0]._data, self._unit_b1[1]._data, self._unit_b1[2]._data, - # self._curl_norm_b[0]._data, self._curl_norm_b[1]._data, self._curl_norm_b[2]._data, - # self._space_key_int, self._scale_mat, 0.1, - # control_mat=[[None, self._mat12, self._mat13], - # [self._mat21, None, self._mat23], - # [self._mat31, self._mat32, None]]) - # else: - # self._accumulator.accumulate(self._particles, self._epsilon, - # Eb_full[0]._data, Eb_full[1]._data, Eb_full[2]._data, - # self._unit_b1[0]._data, self._unit_b1[1]._data, self._unit_b1[2]._data, - # self._curl_norm_b[0]._data, self._curl_norm_b[1]._data, self._curl_norm_b[2]._data, - # self._space_key_int, self._scale_mat, 0.) + b_full += self.options.b_tilde.spline.vector + b_full.update_ghost_regions() - self._accumulator( - self._epsilon, - Eb_full[0]._data, - Eb_full[1]._data, - Eb_full[2]._data, - self._unit_b1[0]._data, - self._unit_b1[1]._data, - self._unit_b1[2]._data, - self._curl_norm_b[0]._data, - self._curl_norm_b[1]._data, - self._curl_norm_b[2]._data, - self._space_key_int, - self._scale_mat, - self._boundary_cut_e1, + self._ACC( + *self._args_accum_kernel, ) # define system (M - dt/2 * A)*u^(n + 1) = (M + dt/2 * A)*u^n - lhs = self._M - dt / 2 * self._accumulator.operators[0] - rhs = self._M + dt / 2 * self._accumulator.operators[0] + lhs = self._A - dt / 2 * self._ACC.operators[0] + rhs = self._A + dt / 2 * self._ACC.operators[0] # solve linear system for updated u coefficients (in-place) rhs = rhs.dot(un, out=self._rhs_v) - self._solver.linop = lhs + self._A_inv.linop = lhs - un1 = self._solver.solve(rhs, out=self._u_new) - info = self._solver._info + _u = self._A_inv.solve(rhs, out=self._u_new) + info = self._A_inv._info - # write new coeffs into Propagator.variables - max_du = self.feec_vars_update(un1) + diffs = self.update_feec_variables(u=_u) - if self._info and MPI.COMM_WORLD.Get_rank() == 0: + if self.options.solver_params.info and MPI.COMM_WORLD.Get_rank() == 0: print("Status for CurrentCoupling5DDensity:", info["success"]) print("Iterations for CurrentCoupling5DDensity:", info["niter"]) - print("Maxdiff up for CurrentCoupling5DDensity:", max_du) + print("Maxdiff up for CurrentCoupling5DDensity:", diffs["u"]) print() @@ -2538,129 +2524,134 @@ class ImplicitDiffusion(Propagator): * :math:`\sigma_1=\sigma_2=0` and :math:`\sigma_3 = \Delta t`: **Poisson solver** with a given charge density :math:`\sum_i\rho_i`. * :math:`\sigma_2=0` and :math:`\sigma_1 = \sigma_3 = \Delta t` : Poisson with **adiabatic electrons**. * :math:`\sigma_1=\sigma_2=1` and :math:`\sigma_3 = 0`: **Implicit heat equation**. - - Parameters - ---------- - phi : StencilVector - FE coefficients of the solution as a discrete 0-form. - - sigma_1, sigma_2, sigma_3 : float | int - Equation parameters. - - divide_by_dt : bool - Whether to divide the sigmas by dt during __call__. - - stab_mat : str - Name of the matrix :math:`M^0_{n_0}`. - - diffusion_mat : str - Name of the matrix :math:`M^1_{D_0}`. - - rho : StencilVector or tuple or list - (List of) right-hand side FE coefficients of a 0-form (optional, can be set with a setter later). - Can be either a) StencilVector or b) 2-tuple, or a list of those. - In case b) the first tuple entry must be :class:`~struphy.pic.accumulation.particles_to_grid.AccumulatorVector`, - and the second entry must be :class:`~struphy.pic.base.Particles`. - - x0 : StencilVector - Initial guess for the iterative solver (optional, can be set with a setter later). - - solver : dict - Parameters for the iterative solver (see ``__init__`` for details). """ - @staticmethod - def options(default=False): - dct = {} - dct["model"] = { - "sigma_1": 1.0, - "sigma_2": 0.0, - "sigma_3": 1.0, - "stab_mat": ["M0", "M0ad", "Id"], - "diffusion_mat": ["M1", "M1perp"], - } - dct["solver"] = { - "type": [ - ("pcg", "MassMatrixPreconditioner"), - ("cg", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - phi: StencilVector, - *, - sigma_1: float = options()["model"]["sigma_1"], - sigma_2: float = options()["model"]["sigma_2"], - sigma_3: float = options()["model"]["sigma_3"], - divide_by_dt: bool = False, - stab_mat: str = options(default=True)["model"]["stab_mat"], - diffusion_mat: str = options(default=True)["model"]["diffusion_mat"], - rho: StencilVector | tuple | list | Callable = None, - x0: StencilVector = None, - solver: dict = options(default=True)["solver"], - ): - assert phi.space == self.derham.Vh["0"] - - super().__init__(phi) + class Variables: + def __init__(self): + self._phi: FEECVariable = None + + @property + def phi(self) -> FEECVariable: + return self._phi + + @phi.setter + def phi(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "H1" + self._phi = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # specific literals + OptsStabMat = Literal["M0", "M0ad", "Id"] + OptsDiffusionMat = Literal["M1", "M1perp"] + # propagator options + sigma_1: float = 1.0 + sigma_2: float = 0.0 + sigma_3: float = 1.0 + divide_by_dt: bool = False + stab_mat: OptsStabMat = "M0" + diffusion_mat: OptsDiffusionMat = "M1" + rho: FEECVariable | Callable | tuple[AccumulatorVector, Particles] | list = None + rho_coeffs: float | list = None + x0: StencilVector = None + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + + def __post_init__(self): + # checks + check_option(self.stab_mat, self.OptsStabMat) + check_option(self.diffusion_mat, self.OptsDiffusionMat) + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): # always stabilize - if np.abs(sigma_1) < 1e-14: - sigma_1 = 1e-14 - print(f"Stabilizing Poisson solve with {sigma_1 = }") + if xp.abs(self.options.sigma_1) < 1e-14: + self.options.sigma_1 = 1e-14 + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"Stabilizing Poisson solve with {self.options.sigma_1 = }") # model parameters - self._sigma_1 = sigma_1 - self._sigma_2 = sigma_2 - self._sigma_3 = sigma_3 - self._divide_by_dt = divide_by_dt + self._sigma_1 = self.options.sigma_1 + self._sigma_2 = self.options.sigma_2 + self._sigma_3 = self.options.sigma_3 + self._divide_by_dt = self.options.divide_by_dt + + phi = self.variables.phi.spline.vector # collect rhs - if rho is None: - self._rho = [phi.space.zeros()] - else: - if isinstance(rho, list): - for r in rho: - if isinstance(r, tuple): - assert isinstance(r[0], AccumulatorVector) - assert isinstance(r[1], Particles) - # assert r.space_id == 'H1' - else: - assert r.space == phi.space - elif isinstance(rho, tuple): - assert isinstance(rho[0], AccumulatorVector) - assert isinstance(rho[1], Particles) - # assert rho[0].space_id == 'H1' - rho = [rho] + def verify_rhs(rho) -> StencilVector | FEECVariable | AccumulatorVector: + """Perform preliminary operations on rho to comute the rhs and return the result.""" + if rho is None: + rhs = phi.space.zeros() + elif isinstance(rho, FEECVariable): + assert rho.space == "H1" + rhs = rho + elif isinstance(rho, AccumulatorVector): + rhs = rho elif isinstance(rho, Callable): - rho = [rho()] + rhs = L2Projector("H1", self.mass_ops).get_dofs(rho, apply_bc=True) else: - assert rho.space == phi.space - rho = [rho] - self._rho = rho + raise TypeError(f"{type(rho) = } is not accepted.") + + return rhs + + rho = self.options.rho + if isinstance(rho, list): + self._sources = [] + for r in rho: + self._sources += [verify_rhs(r)] + else: + self._sources = [verify_rhs(rho)] + + # coeffs of rhs + if self.options.rho_coeffs is not None: + if isinstance(self.options.rho_coeffs, (list, tuple)): + self._coeffs = self.options.rho_coeffs + else: + self._coeffs = [self.options.rho_coeffs] + assert len(self._coeffs) == len(self._sources) + else: + self._coeffs = [1.0 for src in self.sources] # initial guess and solver params - self._x0 = x0 - self._info = solver["info"] + self._x0 = self.options.x0 + self._info = self.options.solver_params.info - if stab_mat == "Id": + if self.options.stab_mat == "Id": stab_mat = IdentityOperator(phi.space) else: - stab_mat = getattr(self.mass_ops, stab_mat) + stab_mat = getattr(self.mass_ops, self.options.stab_mat) - print(f"{diffusion_mat = }") - if isinstance(diffusion_mat, str): - diffusion_mat = getattr(self.mass_ops, diffusion_mat) + if isinstance(self.options.diffusion_mat, str): + diffusion_mat = getattr(self.mass_ops, self.options.diffusion_mat) else: + diffusion_mat = self.options.diffusion_mat assert isinstance(diffusion_mat, WeightedMassOperator) assert diffusion_mat.domain == self.derham.grad.codomain assert diffusion_mat.codomain == self.derham.grad.codomain @@ -2670,7 +2661,7 @@ def __init__( self._diffusion_op = self.derham.grad.T @ diffusion_mat @ self.derham.grad # preconditioner and solver for Ax=b - if solver["type"][1] is None: + if self.options.precond is None: pc = None else: # TODO: waiting for multigrid preconditioner @@ -2679,79 +2670,56 @@ def __init__( # solver just with A_2, but will be set during call with dt self._solver = inverse( self._diffusion_op, - solver["type"][0], + self.options.solver, pc=pc, x0=self.x0, - tol=solver["tol"], - maxiter=solver["maxiter"], - verbose=solver["verbose"], - recycle=solver["recycle"], + tol=self.options.solver_params.tol, + maxiter=self.options.solver_params.maxiter, + verbose=self.options.solver_params.verbose, + recycle=self.options.solver_params.recycle, ) # allocate memory for solution self._tmp = phi.space.zeros() self._rhs = phi.space.zeros() self._rhs2 = phi.space.zeros() + self._tmp_src = phi.space.zeros() @property - def rho(self): + def sources(self) -> list[StencilVector | FEECVariable | AccumulatorVector]: """ - (List of) right-hand side FE coefficients of a 0-form. - The list entries can be either a) StencilVectors or b) 2-tuples; - in the latter case, the first tuple entry must be :class:`~struphy.pic.accumulation.particles_to_grid.AccumulatorVector`, - and the second entry must be :class:`~struphy.pic.base.Particles`. + Right-hand side of the equation (sources). """ - return self._rho + return self._sources - @rho.setter - def rho(self, value): - """In-place setter for StencilVector/PolarVector. - If rho is a list, len(value) msut be len(rho) and value can contain None. + @property + def coeffs(self) -> list[float]: """ - if isinstance(value, list): - assert len(value) == len(self.rho) - for i, (val, r) in enumerate(zip(value, self.rho)): - if val is None: - continue - elif isinstance(val, tuple): - assert isinstance(val[0], AccumulatorVector) - assert isinstance(val[1], Particles) - assert isinstance(r, tuple) - self._rho[i] = val - else: - assert val.space == r.space - r[:] = val[:] - elif isinstance(ValueError, tuple): - assert isinstance(value[0], AccumulatorVector) - assert isinstance(value[1], Particles) - assert len(self.rho) == 1 - # assert rho[0].space_id == 'H1' - self._rho[0] = value - else: - assert value.space == self.derham.Vh["0"] - assert len(self.rho) == 1 - self._rho[0][:] = value[:] + Same length as self.sources. Coefficients multiplied with sources before solve (default is 1.0). + """ + return self._coeffs @property def x0(self): """ psydac.linalg.stencil.StencilVector or struphy.polar.basic.PolarVector. First guess of the iterative solver. """ - return self._x0 + return self.options.x0 @x0.setter - def x0(self, value): + def x0(self, value: StencilVector): """In-place setter for StencilVector/PolarVector. First guess of the iterative solver.""" assert value.space == self.derham.Vh["0"] assert value.space.symbolic_space == "H1", ( f"Right-hand side must be in H1, but is in {value.space.symbolic_space}." ) - if self._x0 is None: - self._x0 = value + if self.options.x0 is None: + self.options.x0 = value else: - self._x0[:] = value[:] + self.options.x0[:] = value[:] + @profile def __call__(self, dt): # set parameters if self._divide_by_dt: @@ -2764,17 +2732,20 @@ def __call__(self, dt): sig_3 = self._sigma_3 # compute rhs - phin = self.feec_vars[0] + phin = self.variables.phi.spline.vector rhs = self._stab_mat.dot(phin, out=self._rhs) rhs *= sig_2 self._rhs2 *= 0.0 - for rho in self._rho: - if isinstance(rho, tuple): - rho[0]() # accumulate - self._rhs2 += sig_3 * rho[0].vectors[0] - else: - self._rhs2 += sig_3 * rho + for src, coeff in zip(self.sources, self.coeffs): + if isinstance(src, StencilVector): + self._rhs2 += sig_3 * coeff * src + elif isinstance(src, FEECVariable): + v = src.spline.vector + self._rhs2 += sig_3 * coeff * self.mass_ops.M0.dot(v, out=self._tmp_src) + elif isinstance(src, AccumulatorVector): + src() # accumulate + self._rhs2 += sig_3 * coeff * src.vectors[0] rhs += self._rhs2 @@ -2788,7 +2759,7 @@ def __call__(self, dt): if self._info: print(info) - self.feec_vars_update(out) + self.update_feec_variables(phi=out) class Poisson(ImplicitDiffusion): @@ -2836,51 +2807,52 @@ class Poisson(ImplicitDiffusion): Parameters for the iterative solver (see ``__init__`` for details). """ - @staticmethod - def options(default=False): - dct = {} - dct["stabilization"] = { - "stab_eps": 0.0, - "stab_mat": ["Id", "M0", "M0ad"], - } - dct["solver"] = { - "type": [ - ("pcg", "MassMatrixPreconditioner"), - ("cg", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - if default: - dct = descend_options_dict(dct, []) + @dataclass + class Options: + # specific literals + OptsStabMat = Literal["M0", "M0ad", "Id"] + # propagator options + stab_eps: float = 0.0 + stab_mat: OptsStabMat = "Id" + rho: FEECVariable | Callable | tuple[AccumulatorVector, Particles] | list = None + rho_coeffs: float | list = None + x0: StencilVector = None + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + + def __post_init__(self): + # checks + check_option(self.stab_mat, self.OptsStabMat) + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + # Poisson solve (-> set some params of parent class) + self.sigma_1 = self.stab_eps + self.sigma_2 = 0.0 + self.sigma_3 = 1.0 + self.divide_by_dt = False + self.diffusion_mat = "M1" - return dct - - def __init__( - self, - phi: StencilVector, - *, - stab_eps: float = 0.0, - stab_mat: str = options(default=True)["stabilization"]["stab_mat"], - rho: StencilVector | tuple | list | Callable = None, - x0: StencilVector = None, - solver: dict = options(default=True)["solver"], - ): - super().__init__( - phi, - sigma_1=stab_eps, - sigma_2=0.0, - sigma_3=1.0, - divide_by_dt=False, - stab_mat=stab_mat, - diffusion_mat="M1", - rho=rho, - x0=x0, - solver=solver, - ) + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + if "sigma" not in k and k not in ("divide_by_dt", "diffusion_mat"): + print(f" {k}: {v}") + self._options = new class VariationalMomentumAdvection(Propagator): @@ -2911,52 +2883,75 @@ class VariationalMomentumAdvection(Propagator): .. math:: \hat{\mathbf{u}}_h^{n+1/2} = (\mathbf{u}^{n+1/2})^\top \vec{\boldsymbol \Lambda}^v \in (V_h^0)^3 \,, \qquad \hat{\mathbf A}^1_{\mu,h} = \nabla P_\mu((\mathbf u^{n+1/2})^\top \vec{\boldsymbol \Lambda}^v)] \in V_h^1\,, \qquad \hat{\rho}_h^{n} = (\rho^{n})^\top \vec{\boldsymbol \Lambda}^3 \in V_h^3 \,. - """ - - @staticmethod - def options(default=False): - dct = {} - dct["lin_solver"] = { - "tol": 1e-12, - "maxiter": 500, - "type": [ - ("pcg", "MassMatrixDiagonalPreconditioner"), - ("cg", None), - ], - "verbose": False, - } - dct["nonlin_solver"] = { - "tol": 1e-8, - "maxiter": 100, - "type": ["Newton", "Picard"], - "info": False, - } - if default: - dct = descend_options_dict(dct, []) - return dct + """ - def __init__( - self, - u: BlockVector, - *, - mass_ops: H1vecMassMatrix_density, - lin_solver: dict = options(default=True)["lin_solver"], - nonlin_solver: dict = options(default=True)["nonlin_solver"], - ): - super().__init__(u) + class Variables: + def __init__(self): + self._u: FEECVariable = None + + @property + def u(self) -> FEECVariable: + return self._u + + @u.setter + def u(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "H1vec" + self._u = new + + def __init__(self): + self.variables = self.Variables() - assert mass_ops is not None + @dataclass + class Options: + # propagator options + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + nonlin_solver: NonlinearSolverParameters = None - self._lin_solver = lin_solver - self._nonlin_solver = nonlin_solver + def __post_init__(self): + # checks + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) - self._info = self._nonlin_solver["info"] and (MPI.COMM_WORLD.Get_rank() == 0) + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() - self._Mrho = mass_ops + if self.nonlin_solver is None: + self.nonlin_solver = NonlinearSolverParameters() + + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + self._lin_solver = self.options.solver_params + self._nonlin_solver = self.options.nonlin_solver + + self._info = self._nonlin_solver.info and (MPI.COMM_WORLD.Get_rank() == 0) + + self._Mrho = self.mass_ops.WMM + self._Mrho.inv._options["pc"] = MassMatrixDiagonalPreconditioner(self._Mrho.massop) self._initialize_mass() # bunch of temporaries to avoid allocating in the loop + u = self.variables.u.spline.vector + self._tmp_un1 = u.space.zeros() self._tmp_un12 = u.space.zeros() self._tmp_diff = u.space.zeros() @@ -2972,25 +2967,25 @@ def __init__( self.inv_derivative = inverse( self._Mrho.inv @ self.derivative, "gmres", - tol=self._lin_solver["tol"], - maxiter=self._lin_solver["maxiter"], - verbose=self._lin_solver["verbose"], + tol=self._lin_solver.tol, + maxiter=self._lin_solver.maxiter, + verbose=self._lin_solver.verbose, recycle=True, ) def __call__(self, dt): - if self._nonlin_solver["type"] == "Newton": + if self._nonlin_solver.type == "Newton": self.__call_newton(dt) - elif self._nonlin_solver["type"] == "Picard": + elif self._nonlin_solver.type == "Picard": self.__call_picard(dt) def __call_newton(self, dt): # Initialize variable for Newton iteration - un = self.feec_vars[0] + un = self.variables.u.spline.vector mn = self._Mrho.massop.dot(un, out=self._tmp_mn) mn1 = mn.copy(out=self._tmp_mn1) un1 = un.copy(out=self._tmp_un1) - tol = self._nonlin_solver["tol"] + tol = self.options.nonlin_solver.tol err = tol + 1 # Jacobian matrix for Newton solve self._dt2_brack._scalar = dt / 2 @@ -2998,7 +2993,7 @@ def __call_newton(self, dt): print() print("Newton iteration in VariationalMomentumAdvection") - for it in range(self._nonlin_solver["maxiter"]): + for it in range(self.options.nonlin_solver.maxiter): un12 = un.copy(out=self._tmp_un12) un12 += un1 un12 *= 0.5 @@ -3018,7 +3013,7 @@ def __call_newton(self, dt): if self._info: print("iteration : ", it, " error : ", err) - if err < tol**2 or np.isnan(err): + if err < tol**2 or xp.isnan(err): break # Newton step @@ -3032,26 +3027,26 @@ def __call_newton(self, dt): un1 -= update mn1 = self._Mrho.massop.dot(un1, out=self._tmp_mn1) - if it == self._nonlin_solver["maxiter"] - 1 or np.isnan(err): + if it == self.options.nonlin_solver.maxiter - 1 or xp.isnan(err): print( f"!!!WARNING: Maximum iteration in VariationalMomentumAdvection reached - not converged \n {err = } \n {tol**2 = }", ) - self.feec_vars_update(un1) + self.update_feec_variables(u=un1) def __call_picard(self, dt): # Initialize variable for Picard iteration - un = self.feec_vars[0] + un = self.variables.u.spline.vector mn = self._Mrho.massop.dot(un, out=self._tmp_mn) mn1 = mn.copy(out=self._tmp_mn1) un1 = un.copy(out=self._tmp_un1) - tol = self._nonlin_solver["tol"] + tol = self.options.nonlin_solver.tol err = tol + 1 # Jacobian matrix for Newton solve - for it in range(self._nonlin_solver["maxiter"]): + for it in range(self.options.nonlin_solver.maxiter): # Picard iteration - if err < tol**2 or np.isnan(err): + if err < tol**2 or xp.isnan(err): break # half time step approximation un12 = un.copy(out=self._tmp_un12) @@ -3078,12 +3073,12 @@ def __call_picard(self, dt): # Inverse the mass matrix to get the velocity un1 = self._Mrho.inv.dot(mn1, out=self._tmp_un1) - if it == self._nonlin_solver["maxiter"] - 1 or np.isnan(err): + if it == self.options.nonlin_solver.maxiter - 1 or xp.isnan(err): print( f"!!!WARNING: Maximum iteration in VariationalMomentumAdvection reached - not converged \n {err = } \n {tol**2 = }", ) - self.feec_vars_update(un1) + self.update_feec_variables(u=un1) def _initialize_mass(self): """Initialization of the mass matrix solver""" @@ -3156,48 +3151,38 @@ class VariationalDensityEvolve(Propagator): \hat{\mathbf{u}}_h^{k} = (\mathbf{u}^{k})^\top \vec{\boldsymbol \Lambda}^v \in (V_h^0)^3 \, \text{for k in} \{n, n+1/2, n+1\}, \qquad \hat{\rho}_h^{k} = (\rho^{k})^\top \vec{\boldsymbol \Lambda}^3 \in V_h^3 \, \text{for k in} \{n, n+1/2, n+1\} . """ - @staticmethod - def options(default=False): - dct = {} - dct["lin_solver"] = { - "tol": 1e-12, - "maxiter": 500, - "type": [ - ("pcg", "MassMatrixDiagonalPreconditioner"), - ("cg", None), - ], - "verbose": False, - "recycle": True, - } - dct["nonlin_solver"] = { - "tol": 1e-8, - "maxiter": 100, - "info": False, - "linearize": False, - } - dct["physics"] = {"gamma": 5 / 3} - - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - rho: StencilVector, - u: BlockVector, - *, - model: str = "barotropic", - gamma: float = options()["physics"]["gamma"], - s: StencilVector = None, - mass_ops: H1vecMassMatrix_density, - lin_solver: dict = options(default=True)["lin_solver"], - nonlin_solver: dict = options(default=True)["nonlin_solver"], - energy_evaluator: InternalEnergyEvaluator = None, - ): - super().__init__(rho, u) - - assert model in [ + class Variables: + def __init__(self): + self._rho: FEECVariable = None + self._u: FEECVariable = None + + @property + def rho(self) -> FEECVariable: + return self._rho + + @rho.setter + def rho(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "L2" + self._rho = new + + @property + def u(self) -> FEECVariable: + return self._u + + @u.setter + def u(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "H1vec" + self._u = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # specific literals + OptsModel = Literal[ "pressureless", "barotropic", "full", @@ -3208,27 +3193,69 @@ def __init__( "linear_q", "deltaf_q", ] - if model == "full": - assert s is not None - assert mass_ops is not None - - self._model = model - self._gamma = gamma - self._s = s - self._lin_solver = lin_solver - self._nonlin_solver = nonlin_solver - self._linearize = self._nonlin_solver["linearize"] - - self._info = self._nonlin_solver["info"] and (MPI.COMM_WORLD.Get_rank() == 0) + # propagator options + model: OptsModel = "barotropic" + gamma: float = 5.0 / 3.0 + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + nonlin_solver: NonlinearSolverParameters = None + s: FEECVariable = None + + def __post_init__(self): + # checks + check_option(self.model, self.OptsModel) + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + if self.nonlin_solver is None: + self.nonlin_solver = NonlinearSolverParameters() - self._Mrho = mass_ops + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + if self.options.model == "full": + assert self.options.s is not None + + self._model = self.options.model + self._gamma = self.options.gamma + self._s = self.options.s + self._lin_solver = self.options.solver_params + self._nonlin_solver = self.options.nonlin_solver + self._linearize = self.options.nonlin_solver.linearize + + self._info = self.options.nonlin_solver.info and (MPI.COMM_WORLD.Get_rank() == 0) + + self._Mrho = self.mass_ops.WMM + self._Mrho.inv._options["pc"] = MassMatrixDiagonalPreconditioner(self._Mrho.massop) # Femfields for the projector self.rhof = self.derham.create_spline_function("rhof", "L2") self.rhof1 = self.derham.create_spline_function("rhof1", "L2") + rho = self.variables.rho.spline.vector + u = self.variables.u.spline.vector + # Projector - self._energy_evaluator = energy_evaluator + self._energy_evaluator = InternalEnergyEvaluator(self.derham, self._gamma) self._kinetic_evaluator = KineticEnergyEvaluator(self.derham, self.domain, self.mass_ops) self._initialize_projectors_and_mass() if self._model in ["linear", "linear_q"]: @@ -3264,6 +3291,7 @@ def __init__( if self._model in ["linear", "linear_q"]: self._update_Pirho(self.projected_equil.n3) + @profile def __call__(self, dt): self.__call_newton(dt) @@ -3275,15 +3303,15 @@ def __call_newton(self, dt): print("Newton iteration in VariationalDensityEvolve") # Initial variables - rhon = self.feec_vars[0] - un = self.feec_vars[1] + rhon = self.variables.rho.spline.vector + un = self.variables.u.spline.vector if self._model in ["linear", "linear_q"]: advection = self.divPirho.dot(un, out=self._tmp_rho_advection) advection *= dt rhon1 = rhon.copy(out=self._tmp_rhon1) rhon1 -= advection - self.feec_vars_update(rhon1, un) + self.update_feec_variables(rho=rhon1, u=un) return if self._model in ["deltaf", "deltaf_q"]: @@ -3296,7 +3324,7 @@ def __call_newton(self, dt): # Initialize variable for Newton iteration if self._model == "full": - s = self._s + s = self._s.spline.vector else: s = None @@ -3318,10 +3346,10 @@ def __call_newton(self, dt): un1 = un.copy(out=self._tmp_un1) un1 += self._tmp_un_diff mn1 = self._Mrho.massop.dot(un1, out=self._tmp_mn1) - tol = self._nonlin_solver["tol"] + tol = self._nonlin_solver.tol err = tol + 1 - for it in range(self._nonlin_solver["maxiter"]): + for it in range(self._nonlin_solver.maxiter): # Newton iteration un12 = un.copy(out=self._tmp_un12) @@ -3367,7 +3395,7 @@ def __call_newton(self, dt): if self._info: print("iteration : ", it, " error : ", err) - if err < tol**2 or np.isnan(err): + if err < tol**2 or xp.isnan(err): break # Derivative for Newton @@ -3397,14 +3425,14 @@ def __call_newton(self, dt): mn1 = self._Mrho.massop.dot(un1, out=self._tmp_mn1) - if it == self._nonlin_solver["maxiter"] - 1 or np.isnan(err): + if it == self._nonlin_solver.maxiter - 1 or xp.isnan(err): print( f"!!!Warning: Maximum iteration in VariationalDensityEvolve reached - not converged:\n {err = } \n {tol**2 = }", ) self._tmp_un_diff = un1 - un self._tmp_rhon_diff = rhon1 - rhon - self.feec_vars_update(rhon1, un1) + self.update_feec_variables(rho=rhon1, u=un1) def _initialize_projectors_and_mass(self): """Initialization of all the `BasisProjectionOperator` and `CoordinateProjector` needed to compute the bracket term""" @@ -3440,7 +3468,7 @@ def _initialize_projectors_and_mass(self): # tmps grid_shape = tuple([len(loc_grid) for loc_grid in integration_grid]) - self._rhof_values = np.zeros(grid_shape, dtype=float) + self._rhof_values = xp.zeros(grid_shape, dtype=float) # Other mass matrices for newton solve self._M_drho = self.mass_ops.create_weighted_mass("L2", "L2") @@ -3473,17 +3501,17 @@ def _initialize_projectors_and_mass(self): self._Jacobian, "pbicgstab", pc=self._Mrho.inv, - tol=self._lin_solver["tol"], - maxiter=self._lin_solver["maxiter"], - verbose=self._lin_solver["verbose"], + tol=self._lin_solver.tol, + maxiter=self._lin_solver.maxiter, + verbose=self._lin_solver.verbose, recycle=True, ) # self._inv_Jacobian = inverse(self._Jacobian, # 'gmres', - # tol=self._lin_solver['tol'], - # maxiter=self._lin_solver['maxiter'], - # verbose=self._lin_solver['verbose'], + # tol=self._lin_solver.tol, + # maxiter=self._lin_solver.maxiter, + # verbose=self._lin_solver.verbose, # recycle=True) # L2-projector for V3 @@ -3492,20 +3520,20 @@ def _initialize_projectors_and_mass(self): grid_shape = tuple([len(loc_grid) for loc_grid in integration_grid]) # tmps - self._eval_dl_drho = np.zeros(grid_shape, dtype=float) + self._eval_dl_drho = xp.zeros(grid_shape, dtype=float) - self._uf_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] - self._uf1_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] + self._uf_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] + self._uf1_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] - self._tmp_int_grid = np.zeros(grid_shape, dtype=float) - self._tmp_int_grid2 = np.zeros(grid_shape, dtype=float) - self._rhof_values = np.zeros(grid_shape, dtype=float) - self._rhof1_values = np.zeros(grid_shape, dtype=float) + self._tmp_int_grid = xp.zeros(grid_shape, dtype=float) + self._tmp_int_grid2 = xp.zeros(grid_shape, dtype=float) + self._rhof_values = xp.zeros(grid_shape, dtype=float) + self._rhof1_values = xp.zeros(grid_shape, dtype=float) if self._model == "full": - self._tmp_de_drho = np.zeros(grid_shape, dtype=float) + self._tmp_de_drho = xp.zeros(grid_shape, dtype=float) gam = self._gamma - metric = np.power( + metric = xp.power( self.domain.jacobian_det( *integration_grid, ), @@ -3513,7 +3541,7 @@ def _initialize_projectors_and_mass(self): ) self._proj_rho2_metric_term = deepcopy(metric) - metric = np.power( + metric = xp.power( self.domain.jacobian_det( *integration_grid, ), @@ -3522,7 +3550,7 @@ def _initialize_projectors_and_mass(self): self._proj_drho_metric_term = deepcopy(metric) if self._linearize: - self._init_dener_drho = np.zeros(grid_shape, dtype=float) + self._init_dener_drho = xp.zeros(grid_shape, dtype=float) def _update_Pirho(self, rho): """Update the weights of the `BasisProjectionOperator` Pirho""" @@ -3536,7 +3564,7 @@ def _update_weighted_MM(self, rho): self._Mrho.update_weight(rho) def _update_linear_form_dl_drho(self, rhon, rhon1, un, un1, sn): - """Update the linearform representing integration in V3 against kynetic energy""" + """Update the linearform representing integration in V3 against kinetic energy""" self._kinetic_evaluator.get_u2_grid(un, un1, self._eval_dl_drho) @@ -3662,67 +3690,100 @@ class VariationalEntropyEvolve(Propagator): \hat{\mathbf{u}}_h^{k} = (\mathbf{u}^{k})^\top \vec{\boldsymbol \Lambda}^v \in (V_h^0)^3 \, \text{for k in} \{n, n+1/2, n+1\}, \qquad \hat{s}_h^{k} = (s^{k})^\top \vec{\boldsymbol \Lambda}^3 \in V_h^3 \, \text{for k in} \{n, n+1/2, n+1\} \qquad \hat{\rho}_h^{n} = (\rho^{n})^\top \vec{\boldsymbol \Lambda}^3 \in V_h^3 \. """ - @staticmethod - def options(default=False): - dct = {} - dct["lin_solver"] = { - "tol": 1e-12, - "maxiter": 500, - "type": [ - ("pcg", "MassMatrixDiagonalPreconditioner"), - ("cg", None), - ], - "verbose": False, - } - dct["nonlin_solver"] = { - "tol": 1e-8, - "maxiter": 100, - "info": False, - "linearize": "False", - } - dct["physics"] = {"gamma": 5 / 3} - - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - s: StencilVector, - u: BlockVector, - *, - model: str = "full", - gamma: float = options()["physics"]["gamma"], - rho: StencilVector, - mass_ops: H1vecMassMatrix_density, - lin_solver: dict = options(default=True)["lin_solver"], - nonlin_solver: dict = options(default=True)["nonlin_solver"], - energy_evaluator: InternalEnergyEvaluator = None, - ): - super().__init__(s, u) - - assert model in ["full"] - if model == "full": - assert rho is not None - assert mass_ops is not None - - self._model = model - self._gamma = gamma - self._rho = rho - self._lin_solver = lin_solver - self._nonlin_solver = nonlin_solver - self._linearize = self._nonlin_solver["linearize"] - - self._info = self._nonlin_solver["info"] and (MPI.COMM_WORLD.Get_rank() == 0) + class Variables: + def __init__(self): + self._s: FEECVariable = None + self._u: FEECVariable = None + + @property + def s(self) -> FEECVariable: + return self._s + + @s.setter + def s(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "L2" + self._s = new + + @property + def u(self) -> FEECVariable: + return self._u + + @u.setter + def u(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "H1vec" + self._u = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # specific literals + OptsModel = Literal["full"] + # propagator options + model: OptsModel = "full" + gamma: float = 5.0 / 3.0 + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + nonlin_solver: NonlinearSolverParameters = None + rho: FEECVariable = None + + def __post_init__(self): + # checks + check_option(self.model, self.OptsModel) + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + if self.nonlin_solver is None: + self.nonlin_solver = NonlinearSolverParameters() - self._Mrho = mass_ops + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + if self.options.model == "full": + assert self.options.rho is not None + + self._model = self.options.model + self._gamma = self.options.gamma + self._rho = self.options.rho + self._lin_solver = self.options.solver_params + self._nonlin_solver = self.options.nonlin_solver + self._linearize = self.options.nonlin_solver.linearize + + self._info = self._nonlin_solver.info and (MPI.COMM_WORLD.Get_rank() == 0) + + self._Mrho = self.mass_ops.WMM + self._Mrho.inv._options["pc"] = MassMatrixDiagonalPreconditioner(self._Mrho.massop) # Projector - self._energy_evaluator = energy_evaluator + self._energy_evaluator = InternalEnergyEvaluator(self.derham, self._gamma) self._initialize_projectors_and_mass() # bunch of temporaries to avoid allocating in the loop + s = self.variables.s.spline.vector + u = self.variables.u.spline.vector + self._tmp_un1 = u.space.zeros() self._tmp_un2 = u.space.zeros() self._tmp_un12 = u.space.zeros() @@ -3750,12 +3811,12 @@ def __call_newton(self, dt): if self._info: print() print("Newton iteration in VariationalEntropyEvolve") - sn = self.feec_vars[0] - un = self.feec_vars[1] + sn = self.variables.s.spline.vector + un = self.variables.u.spline.vector sn1 = sn.copy(out=self._tmp_sn1) # Initialize variable for Newton iteration - rho = self._rho + rho = self._rho.spline.vector self._update_Pis(sn) mn = self._Mrho.massop.dot(un, out=self._tmp_mn) @@ -3764,10 +3825,10 @@ def __call_newton(self, dt): un1 = un.copy(out=self._tmp_un1) un1 += self._tmp_un_diff mn1 = self._Mrho.massop.dot(un1, out=self._tmp_mn1) - tol = self._nonlin_solver["tol"] + tol = self._nonlin_solver.tol err = tol + 1 - for it in range(self._nonlin_solver["maxiter"]): + for it in range(self._nonlin_solver.maxiter): # Newton iteration un12 = un.copy(out=self._tmp_un12) @@ -3805,7 +3866,7 @@ def __call_newton(self, dt): if self._info: print("iteration : ", it, " error : ", err) - if err < tol**2 or np.isnan(err): + if err < tol**2 or xp.isnan(err): break # Derivative for Newton @@ -3827,13 +3888,13 @@ def __call_newton(self, dt): # Multiply by the mass matrix to get the momentum mn1 = self._Mrho.massop.dot(un1, out=self._tmp_mn1) - if it == self._nonlin_solver["maxiter"] - 1 or np.isnan(err): + if it == self._nonlin_solver.maxiter - 1 or xp.isnan(err): print( f"!!!Warning: Maximum iteration in VariationalEntropyEvolve reached - not converged:\n {err = } \n {tol**2 = }", ) self._tmp_sn_diff = sn1 - sn self._tmp_un_diff = un1 - un - self.feec_vars_update(sn1, un1) + self.update_feec_variables(s=sn1, u=un1) def _initialize_projectors_and_mass(self): """Initialization of all the `BasisProjectionOperator` and `CoordinateProjector` needed to compute the bracket term""" @@ -3886,19 +3947,19 @@ def _initialize_projectors_and_mass(self): self._inv_Jacobian = SchurSolverFull( self._Jacobian, - self._lin_solver["type"][0], + self.options.solver, pc=self._Mrho.inv, - tol=self._lin_solver["tol"], - maxiter=self._lin_solver["maxiter"], - verbose=self._lin_solver["verbose"], + tol=self._lin_solver.tol, + maxiter=self._lin_solver.maxiter, + verbose=self._lin_solver.verbose, recycle=True, ) # self._inv_Jacobian = inverse(self._Jacobian, # 'gmres', - # tol=self._lin_solver['tol'], - # maxiter=self._lin_solver['maxiter'], - # verbose=self._lin_solver['verbose'], + # tol=self._lin_solver.tol, + # maxiter=self._lin_solver.maxiter, + # verbose=self._lin_solver.verbose, # recycle=True) # prepare for integration of linear form @@ -3914,15 +3975,15 @@ def _initialize_projectors_and_mass(self): ) grid_shape = tuple([len(loc_grid) for loc_grid in integration_grid]) - self._tmp_int_grid = np.zeros(grid_shape, dtype=float) + self._tmp_int_grid = xp.zeros(grid_shape, dtype=float) if self._model == "full": - self._tmp_de_ds = np.zeros(grid_shape, dtype=float) + self._tmp_de_ds = xp.zeros(grid_shape, dtype=float) if self._linearize: - self._init_dener_ds = np.zeros(grid_shape, dtype=float) + self._init_dener_ds = xp.zeros(grid_shape, dtype=float) gam = self._gamma - metric = np.power( + metric = xp.power( self.domain.jacobian_det( *integration_grid, ), @@ -3930,7 +3991,7 @@ def _initialize_projectors_and_mass(self): ) self._proj_rho2_metric_term = deepcopy(metric) - metric = np.power( + metric = xp.power( self.domain.jacobian_det( *integration_grid, ), @@ -4032,58 +4093,91 @@ class VariationalMagFieldEvolve(Propagator): """ - @staticmethod - def options(default=False): - dct = {} - dct["lin_solver"] = { - "tol": 1e-12, - "maxiter": 500, - "non_linear_maxiter": 100, - "type": [ - ("pcg", "MassMatrixDiagonalPreconditioner"), - ("cg", None), - ], - "verbose": False, - } - dct["nonlin_solver"] = { - "tol": 1e-8, - "maxiter": 100, - "info": False, - "linearize": False, - } - - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - b: BlockVector, - u: BlockVector, - *, - model: str = "full", - mass_ops: H1vecMassMatrix_density, - lin_solver: dict = options(default=True)["lin_solver"], - nonlin_solver: dict = options(default=True)["nonlin_solver"], - ): - super().__init__(b, u) - - assert model in ["full", "full_p", "linear"] - self._model = model - self._mass_ops = mass_ops - self._lin_solver = lin_solver - self._nonlin_solver = nonlin_solver - self._linearize = self._nonlin_solver["linearize"] + class Variables: + def __init__(self): + self._u: FEECVariable = None + self._b: FEECVariable = None + + @property + def u(self) -> FEECVariable: + return self._u + + @u.setter + def u(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "H1vec" + self._u = new + + @property + def b(self) -> FEECVariable: + return self._b + + @b.setter + def b(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "Hdiv" + self._b = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + OptsModel = Literal["full", "full_p", "linear"] + # propagator options + model: OptsModel = "full" + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + nonlin_solver: NonlinearSolverParameters = None + + def __post_init__(self): + # checks + check_option(self.model, self.OptsModel) + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + if self.nonlin_solver is None: + self.nonlin_solver = NonlinearSolverParameters(type="Newton") - self._info = self._nonlin_solver["info"] and (MPI.COMM_WORLD.Get_rank() == 0) - - self._Mrho = mass_ops + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + self._model = self.options.model + self._lin_solver = self.options.solver_params + self._nonlin_solver = self.options.nonlin_solver + self._linearize = self._nonlin_solver.linearize + + self._info = self._nonlin_solver.info and (MPI.COMM_WORLD.Get_rank() == 0) + + self._Mrho = self.mass_ops.WMM + self._Mrho.inv._options["pc"] = MassMatrixDiagonalPreconditioner(self._Mrho.massop) # Projector self._initialize_projectors_and_mass() # bunch of temporaries to avoid allocating in the loop + u = self.variables.u.spline.vector + b = self.variables.b.spline.vector + self._tmp_un1 = u.space.zeros() self._tmp_un2 = u.space.zeros() self._tmp_un12 = u.space.zeros() @@ -4114,8 +4208,8 @@ def __call_newton(self, dt): print() print("Newton iteration in VariationalMagFieldEvolve") # Compute implicit approximation of s^{n+1} - bn = self.feec_vars[0] - un = self.feec_vars[1] + un = self.variables.u.spline.vector + bn = self.variables.b.spline.vector bn1 = bn.copy(out=self._tmp_bn1) # Initialize variable for Newton iteration @@ -4128,10 +4222,10 @@ def __call_newton(self, dt): un1 = un.copy(out=self._tmp_un1) un1 += self._tmp_un_diff mn1 = self._Mrho.massop.dot(un1, out=self._tmp_mn1) - tol = self._nonlin_solver["tol"] + tol = self._nonlin_solver.tol err = tol + 1 - for it in range(self._nonlin_solver["maxiter"]): + for it in range(self._nonlin_solver.maxiter): # Newton iteration # half time step approximation bn12 = bn.copy(out=self._tmp_bn12) @@ -4192,7 +4286,7 @@ def __call_newton(self, dt): if self._info: print("iteration : ", it, " error : ", err) - if err < tol**2 or np.isnan(err): + if err < tol**2 or xp.isnan(err): break # Derivative for Newton @@ -4214,20 +4308,18 @@ def __call_newton(self, dt): # Multiply by the mass matrix to get the momentum mn1 = self._Mrho.massop.dot(un1, out=self._tmp_mn1) - if it == self._nonlin_solver["maxiter"] - 1 or np.isnan(err): + if it == self._nonlin_solver.maxiter - 1 or xp.isnan(err): print( f"!!!Warning: Maximum iteration in VariationalMagFieldEvolve reached - not converged:\n {err = } \n {tol**2 = }", ) self._tmp_un_diff = un1 - un self._tmp_bn_diff = bn1 - bn - self.feec_vars_update(bn1, un1) + self.update_feec_variables(b=bn1, u=un1) def _initialize_projectors_and_mass(self): """Initialization of all the `BasisProjectionOperator` and needed to compute the bracket term""" - from struphy.feec.variational_utilities import Hdiv0_transport_operator - self.curlPib = Hdiv0_transport_operator(self.derham) self.curlPibT = self.curlPib.T @@ -4276,15 +4368,13 @@ def _initialize_projectors_and_mass(self): self._Jacobian[1, 0] = self._dt2_curlPib self._Jacobian[1, 1] = self._I2 - from struphy.linear_algebra.schur_solver import SchurSolverFull - self._inv_Jacobian = SchurSolverFull( self._Jacobian, - self._lin_solver["type"][0], + self.options.solver, pc=self._Mrho.inv, - tol=self._lin_solver["tol"], - maxiter=self._lin_solver["maxiter"], - verbose=self._lin_solver["verbose"], + tol=self._lin_solver.tol, + maxiter=self._lin_solver.maxiter, + verbose=self._lin_solver.verbose, recycle=True, ) @@ -4302,8 +4392,6 @@ def _update_Pib(self, b): self.curlPibT.update_coeffs(b) def _create_Pib0(self): - from struphy.feec.variational_utilities import Hdiv0_transport_operator - self.curlPib0 = Hdiv0_transport_operator(self.derham) self.curlPibT0 = self.curlPib0.T @@ -4389,72 +4477,130 @@ class VariationalPBEvolve(Propagator): and :math:`\mathcal{U}^v` is :class:`~struphy.feec.basis_projection_ops.BasisProjectionOperators`. """ - @staticmethod - def options(default=False): - dct = {} - dct["lin_solver"] = { - "tol": 1e-12, - "maxiter": 500, - "non_linear_maxiter": 100, - "type": [ - ("pcg", "MassMatrixDiagonalPreconditioner"), - ("cg", None), - ], - "verbose": False, - } - dct["nonlin_solver"] = { - "tol": 1e-8, - "maxiter": 100, - "type": ["Picard"], - "info": False, - "linearize": False, - } - dct["physics"] = {"gamma": 5 / 3} - - if default: - dct = descend_options_dict(dct, []) + class Variables: + def __init__(self): + self._p: FEECVariable = None + self._u: FEECVariable = None + self._b: FEECVariable = None + + @property + def p(self) -> FEECVariable: + return self._p + + @p.setter + def p(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "L2" + self._p = new + + @property + def u(self) -> FEECVariable: + return self._u + + @u.setter + def u(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "H1vec" + self._u = new + + @property + def b(self) -> FEECVariable: + return self._b + + @b.setter + def b(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "Hdiv" + self._b = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # specific literals + OptsModel = Literal["full_p", "linear", "deltaf"] + # propagator options + model: OptsModel = "full_p" + gamma: float = 5.0 / 3.0 + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + nonlin_solver: NonlinearSolverParameters = None + div_u: FEECVariable = None + u2: FEECVariable = None + pt3: FEECVariable = None + bt2: FEECVariable = None + + def __post_init__(self): + # checks + check_option(self.model, self.OptsModel) + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + if self.nonlin_solver is None: + self.nonlin_solver = NonlinearSolverParameters() - return dct + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + self._model = self.options.model + self._lin_solver = self.options.solver_params + self._nonlin_solver = self.options.nonlin_solver + self._linearize = self.options.nonlin_solver.linearize + self._gamma = self.options.gamma + + if self.options.div_u is None: + self._divu = None + else: + self._divu = self.options.div_u.spline.vector - def __init__( - self, - p: StencilVector, - b: BlockVector, - u: BlockVector, - *, - model: str = "full", - gamma: float = options()["physics"]["gamma"], - mass_ops: H1vecMassMatrix_density, - lin_solver: dict = options(default=True)["lin_solver"], - nonlin_solver: dict = options(default=True)["nonlin_solver"], - div_u: StencilVector | None = None, - u2: BlockVector | None = None, - pt3: StencilVector | None = None, - bt2: BlockVector | None = None, - ): - super().__init__(p, b, u) + if self.options.u2 is None: + self._u2 = None + else: + self._u2 = self.options.u2.spline.vector - assert model in ["full_p", "linear", "deltaf"] - self._model = model - self._mass_ops = mass_ops - self._lin_solver = lin_solver - self._nonlin_solver = nonlin_solver - self._linearize = self._nonlin_solver["linearize"] - self._gamma = gamma + if self.options.pt3 is None: + self._pt3 = None + else: + self._pt3 = self.options.pt3.spline.vector - self._divu = div_u - self._u2 = u2 - self._pt3 = pt3 - self._bt2 = bt2 + if self.options.bt2 is None: + self._bt2 = None + else: + self._bt2 = self.options.bt2.spline.vector - self._info = self._nonlin_solver["info"] and (MPI.COMM_WORLD.Get_rank() == 0) + self._info = self._nonlin_solver.info and (MPI.COMM_WORLD.Get_rank() == 0) - self._Mrho = mass_ops + self._Mrho = self.mass_ops.WMM + self._Mrho.inv._options["pc"] = MassMatrixDiagonalPreconditioner(self._Mrho.massop) # Projector self._initialize_projectors_and_mass() # bunch of temporaries to avoid allocating in the loop + u = self.variables.u.spline.vector + p = self.variables.p.spline.vector + b = self.variables.b.spline.vector + self._tmp_un1 = u.space.zeros() self._tmp_un2 = u.space.zeros() self._tmp_un12 = u.space.zeros() @@ -4488,7 +4634,7 @@ def __init__( self._extracted_b2 = self.derham.extraction_ops["2"].dot(self.projected_equil.b2) def __call__(self, dt): - if self._nonlin_solver["type"] == "Picard": + if self._nonlin_solver.type == "Picard": self.__call_picard(dt) else: raise ValueError("Only Picard solver is implemented for VariationalPBEvolve") @@ -4500,9 +4646,9 @@ def __call_picard(self, dt): print() print("Newton iteration in VariationalPBEvolve") - pn = self.feec_vars[0] - bn = self.feec_vars[1] - un = self.feec_vars[2] + un = self.variables.u.spline.vector + pn = self.variables.p.spline.vector + bn = self.variables.b.spline.vector self._update_Pib(bn) self._update_Projp(pn) @@ -4515,10 +4661,10 @@ def __call_picard(self, dt): un1 = un.copy(out=self._tmp_un1) un1 += self._tmp_un_diff mn1 = self._Mrho.massop.dot(un1, out=self._tmp_mn1) - tol = self._nonlin_solver["tol"] + tol = self._nonlin_solver.tol err = tol + 1 - for it in range(self._nonlin_solver["maxiter"]): + for it in range(self._nonlin_solver.maxiter): # Picard iteration # half time step approximation @@ -4643,7 +4789,7 @@ def __call_picard(self, dt): if self._info: print("iteration : ", it, " error : ", err) - if err < tol**2 or np.isnan(err): + if err < tol**2 or xp.isnan(err): break # Derivative for Newton @@ -4665,7 +4811,7 @@ def __call_picard(self, dt): # Multiply by the mass matrix to get the momentum mn1 = self._Mrho.massop.dot(un1, out=self._tmp_mn1) - if it == self._nonlin_solver["maxiter"] - 1 or np.isnan(err): + if it == self._nonlin_solver.maxiter - 1 or xp.isnan(err): print( f"!!!Warning: Maximum iteration in VariationalPBEvolve reached - not converged:\n {err = } \n {tol**2 = }", ) @@ -4673,7 +4819,7 @@ def __call_picard(self, dt): self._tmp_un_diff = un1 - un self._tmp_bn_diff = bn1 - bn self._tmp_pn_diff = pn1 - pn - self.feec_vars_update(pn1, bn1, un1) + self.update_feec_variables(p=pn1, b=bn1, u=un1) self._transop_p.div.dot(un12, out=self._divu) self._transop_p._Uv.dot(un1, out=self._u2) @@ -4699,9 +4845,6 @@ def __call_picard(self, dt): def _initialize_projectors_and_mass(self): """Initialization of all the `BasisProjectionOperator` and needed to compute the bracket term""" - from struphy.feec.projectors import L2Projector - from struphy.feec.variational_utilities import Hdiv0_transport_operator, Pressure_transport_operator - self.curlPib = Hdiv0_transport_operator(self.derham) self.curlPibT = self.curlPib.T self._transop_p = Pressure_transport_operator(self.derham, self.domain, self.basis_ops.Uv, self._gamma) @@ -4717,7 +4860,7 @@ def _initialize_projectors_and_mass(self): grid_shape = tuple([len(loc_grid) for loc_grid in integration_grid]) - self._tmp_int_grid = np.zeros(grid_shape, dtype=float) + self._tmp_int_grid = xp.zeros(grid_shape, dtype=float) # Inverse mass matrix needed to compute the error self.pc_Mv = preconditioner.MassMatrixDiagonalPreconditioner( @@ -4789,11 +4932,11 @@ def _initialize_projectors_and_mass(self): self._inv_Jacobian = SchurSolverFull( self._Jacobian, - self._lin_solver["type"][0], + self.options.solver, pc=self._Mrho.inv, - tol=self._lin_solver["tol"], - maxiter=self._lin_solver["maxiter"], - verbose=self._lin_solver["verbose"], + tol=self._lin_solver.tol, + maxiter=self._lin_solver.maxiter, + verbose=self._lin_solver.verbose, recycle=True, ) @@ -4812,8 +4955,6 @@ def _update_Pib(self, b): self.curlPibT.update_coeffs(b) def _create_Pib0(self): - from struphy.feec.variational_utilities import Hdiv0_transport_operator - self.curlPib0 = Hdiv0_transport_operator(self.derham) self.curlPibT0 = self.curlPib.T self.curlPib0.update_coeffs(self.projected_equil.b2) @@ -4826,7 +4967,6 @@ def _update_Projp(self, p): def _create_transop0(self): """Update the weights of the `BasisProjectionOperator`""" - from struphy.feec.variational_utilities import Pressure_transport_operator self._transop_p0 = Pressure_transport_operator(self.derham, self.domain, self.basis_ops.Uv, self._gamma) self._transop_p0T = self._transop_p0.T @@ -4935,72 +5075,130 @@ class VariationalQBEvolve(Propagator): and :math:`\mathcal{U}^v` is :class:`~struphy.feec.basis_projection_ops.BasisProjectionOperators`. """ - @staticmethod - def options(default=False): - dct = {} - dct["lin_solver"] = { - "tol": 1e-12, - "maxiter": 500, - "non_linear_maxiter": 100, - "type": [ - ("pcg", "MassMatrixDiagonalPreconditioner"), - ("cg", None), - ], - "verbose": False, - } - dct["nonlin_solver"] = { - "tol": 1e-8, - "maxiter": 100, - "type": ["Picard"], - "info": False, - "linearize": False, - } - dct["physics"] = {"gamma": 5 / 3} - - if default: - dct = descend_options_dict(dct, []) + class Variables: + def __init__(self): + self._q: FEECVariable = None + self._u: FEECVariable = None + self._b: FEECVariable = None + + @property + def q(self) -> FEECVariable: + return self._q + + @q.setter + def q(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "L2" + self._q = new + + @property + def u(self) -> FEECVariable: + return self._u + + @u.setter + def u(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "H1vec" + self._u = new + + @property + def b(self) -> FEECVariable: + return self._b + + @b.setter + def b(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "Hdiv" + self._b = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # specific literals + OptsModel = Literal["full_q", "linear_q", "deltaf_q"] + # propagator options + model: OptsModel = "full_q" + gamma: float = 5.0 / 3.0 + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + nonlin_solver: NonlinearSolverParameters = None + div_u: FEECVariable = None + u2: FEECVariable = None + qt3: FEECVariable = None + bt2: FEECVariable = None + + def __post_init__(self): + # checks + check_option(self.model, self.OptsModel) + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + if self.nonlin_solver is None: + self.nonlin_solver = NonlinearSolverParameters() - return dct + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + self._model = self.options.model + self._lin_solver = self.options.solver_params + self._nonlin_solver = self.options.nonlin_solver + self._linearize = self.options.nonlin_solver.linearize + self._gamma = self.options.gamma + + if self.options.div_u is None: + self._divu = None + else: + self._divu = self.options.div_u.spline.vector - def __init__( - self, - q: StencilVector, - b: BlockVector, - u: BlockVector, - *, - model: str = "full", - gamma: float = options()["physics"]["gamma"], - mass_ops: H1vecMassMatrix_density, - lin_solver: dict = options(default=True)["lin_solver"], - nonlin_solver: dict = options(default=True)["nonlin_solver"], - div_u: StencilVector | None = None, - u2: BlockVector | None = None, - qt3: StencilVector | None = None, - bt2: BlockVector | None = None, - ): - super().__init__(q, b, u) + if self.options.u2 is None: + self._u2 = None + else: + self._u2 = self.options.u2.spline.vector - assert model in ["full_q", "linear_q", "deltaf_q"] - self._model = model - self._mass_ops = mass_ops - self._lin_solver = lin_solver - self._nonlin_solver = nonlin_solver - self._linearize = self._nonlin_solver["linearize"] - self._gamma = gamma + if self.options.qt3 is None: + self._qt3 = None + else: + self._qt3 = self.options.qt3.spline.vector - self._divu = div_u - self._u2 = u2 - self._qt3 = qt3 - self._bt2 = bt2 + if self.options.bt2 is None: + self._bt2 = None + else: + self._bt2 = self.options.bt2.spline.vector - self._info = self._nonlin_solver["info"] and (self.rank == 0) + self._info = self._nonlin_solver.info and (self.rank == 0) - self._Mrho = mass_ops + self._Mrho = self.mass_ops.WMM + self._Mrho.inv._options["pc"] = MassMatrixDiagonalPreconditioner(self._Mrho.massop) # Projector self._initialize_projectors_and_mass() # bunch of temporaries to avoid allocating in the loop + u = self.variables.u.spline.vector + q = self.variables.q.spline.vector + b = self.variables.b.spline.vector + self._tmp_un1 = u.space.zeros() self._tmp_un12 = u.space.zeros() self._tmp_bn1 = b.space.zeros() @@ -5032,7 +5230,7 @@ def __init__( self._extracted_q3 = self.derham.extraction_ops["3"].dot(self.projected_equil.q3) def __call__(self, dt): - if self._nonlin_solver["type"] == "Picard": + if self._nonlin_solver.type == "Picard": self.__call_picard(dt) else: raise ValueError("Only Picard solver is implemented for VariationalQBEvolve") @@ -5044,9 +5242,9 @@ def __call_picard(self, dt): print() print("Newton iteration in VariationalQBEvolve") - qn = self.feec_vars[0] - bn = self.feec_vars[1] - un = self.feec_vars[2] + un = self.variables.u.spline.vector + qn = self.variables.q.spline.vector + bn = self.variables.b.spline.vector self._update_Pib(bn) self._update_Projq(qn) @@ -5059,10 +5257,10 @@ def __call_picard(self, dt): un1 = un.copy(out=self._tmp_un1) un1 += self._tmp_un_diff mn1 = self._Mrho.massop.dot(un1, out=self._tmp_mn1) - tol = self._nonlin_solver["tol"] + tol = self._nonlin_solver.tol err = tol + 1 - for it in range(self._nonlin_solver["maxiter"]): + for it in range(self._nonlin_solver.maxiter): # Picard iteration # half time step approximation @@ -5180,7 +5378,7 @@ def __call_picard(self, dt): if self._info: print("iteration : ", it, " error : ", err) - if err < tol**2 or np.isnan(err): + if err < tol**2 or xp.isnan(err): break # Derivative for Newton @@ -5204,7 +5402,7 @@ def __call_picard(self, dt): # Multiply by the mass matrix to get the momentum mn1 = self._Mrho.massop.dot(un1, out=self._tmp_mn1) - if it == self._nonlin_solver["maxiter"] - 1 or np.isnan(err): + if it == self._nonlin_solver.maxiter - 1 or xp.isnan(err): print( f"!!!Warning: Maximum iteration in VariationalPBEvolve reached - not converged:\n {err = } \n {tol**2 = }", ) @@ -5212,7 +5410,7 @@ def __call_picard(self, dt): self._tmp_un_diff = un1 - un self._tmp_bn_diff = bn1 - bn self._tmp_qn_diff = qn1 - qn - self.feec_vars_update(qn1, bn1, un1) + self.update_feec_variables(q=qn1, b=bn1, u=un1) self._transop_q.div.dot(un12, out=self._divu) self._transop_q._Uv.dot(un1, out=self._u2) @@ -5238,9 +5436,6 @@ def __call_picard(self, dt): def _initialize_projectors_and_mass(self): """Initialization of all the `BasisProjectionOperator` and needed to compute the bracket term""" - from struphy.feec.projectors import L2Projector - from struphy.feec.variational_utilities import Hdiv0_transport_operator, Pressure_transport_operator - self.curlPib = Hdiv0_transport_operator(self.derham) self.curlPibT = self.curlPib.T self._transop_q = Pressure_transport_operator(self.derham, self.domain, self.basis_ops.Uv, self._gamma / 2.0) @@ -5256,7 +5451,7 @@ def _initialize_projectors_and_mass(self): grid_shape = tuple([len(loc_grid) for loc_grid in integration_grid]) - self._tmp_int_grid = np.zeros(grid_shape, dtype=float) + self._tmp_int_grid = xp.zeros(grid_shape, dtype=float) # Inverse mass matrix needed to compute the error self.pc_Mv = preconditioner.MassMatrixDiagonalPreconditioner( @@ -5347,11 +5542,11 @@ def _initialize_projectors_and_mass(self): self._inv_Jacobian = SchurSolverFull3( self._Jacobian, - self._lin_solver["type"][0], + self.options.solver, pc=self._Mrho.inv, - tol=self._lin_solver["tol"], - maxiter=self._lin_solver["maxiter"], - verbose=self._lin_solver["verbose"], + tol=self._lin_solver.tol, + maxiter=self._lin_solver.maxiter, + verbose=self._lin_solver.verbose, recycle=True, ) @@ -5370,8 +5565,6 @@ def _update_Pib(self, b): self.curlPibT.update_coeffs(b) def _create_Pib0(self): - from struphy.feec.variational_utilities import Hdiv0_transport_operator - self.curlPib0 = Hdiv0_transport_operator(self.derham) self.curlPibT0 = self.curlPib.T self.curlPib0.update_coeffs(self.projected_equil.b2) @@ -5384,7 +5577,6 @@ def _update_Projq(self, q): def _create_transop0(self): """Update the weights of the `BasisProjectionOperator`""" - from struphy.feec.variational_utilities import Pressure_transport_operator self._transop_q0 = Pressure_transport_operator(self.derham, self.domain, self.basis_ops.Uv, self._gamma / 2.0) self._transop_q0T = self._transop_q0.T @@ -5489,72 +5681,95 @@ class VariationalViscosity(Propagator): """ - @staticmethod - def options(default=False): - dct = {} - dct["lin_solver"] = { - "tol": 1e-12, - "maxiter": 500, - "type": [ - ("pcg", "MassMatrixDiagonalPreconditioner"), - ("cg", None), - ], - "verbose": False, - } - dct["nonlin_solver"] = { - "tol": 1e-8, - "maxiter": 100, - "type": ["Newton"], - "info": False, - "fast": False, - } - dct["physics"] = { - "gamma": 1.66666666667, - "mu": 0.0, - "mu_a": 0.0, - "alpha": 0.0, - } - - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - s: StencilVector, - u: BlockVector, - *, - model: str = "barotropic", - gamma: float = options()["physics"]["gamma"], - rho: StencilVector, - mu: float = options()["physics"]["mu"], - mu_a: float = options()["physics"]["mu_a"], - alpha: float = options()["physics"]["alpha"], - mass_ops: H1vecMassMatrix_density, - lin_solver: dict = options(default=True)["lin_solver"], - nonlin_solver: dict = options(default=True)["nonlin_solver"], - energy_evaluator: InternalEnergyEvaluator = None, - pt3: StencilVector | None = None, - ): - super().__init__(s, u) - - assert model in ["full", "full_p", "full_q", "linear_p", "linear_q", "deltaf_q"] - - self._model = model - self._gamma = gamma - self._lin_solver = lin_solver - self._nonlin_solver = nonlin_solver - self._mu_a = mu_a - self._alpha = alpha - self._mu = mu - self._rho = rho - self._pt3 = pt3 - self._energy_evaluator = energy_evaluator - - self._info = self._nonlin_solver["info"] and (MPI.COMM_WORLD.Get_rank() == 0) + class Variables: + def __init__(self): + self._s: FEECVariable = None + self._u: FEECVariable = None + + @property + def s(self) -> FEECVariable: + return self._s + + @s.setter + def s(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "L2" + self._s = new + + @property + def u(self) -> FEECVariable: + return self._u + + @u.setter + def u(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "H1vec" + self._u = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # specific literals + OptsModel = Literal["full", "full_p", "full_q", "linear_p", "linear_q", "deltaf_q"] + # propagator options + model: OptsModel = "full" + gamma: float = 5.0 / 3.0 + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixDiagonalPreconditioner" + solver_params: SolverParameters = None + nonlin_solver: NonlinearSolverParameters = None + rho: FEECVariable = None + pt3: FEECVariable = None + mu: float = 0.0 + mu_a: float = 0.0 + alpha: float = 0.0 + + def __post_init__(self): + # checks + check_option(self.model, self.OptsModel) + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + if self.nonlin_solver is None: + self.nonlin_solver = NonlinearSolverParameters(type="Newton") - self._Mrho = mass_ops + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + self._model = self.options.model + self._gamma = self.options.gamma + self._lin_solver = self.options.solver_params + self._nonlin_solver = self.options.nonlin_solver + self._mu_a = self.options.mu_a + self._alpha = self.options.alpha + self._mu = self.options.mu + self._rho = self.options.rho + self._pt3 = self.options.pt3 + + self._info = self._nonlin_solver.info and (MPI.COMM_WORLD.Get_rank() == 0) + + self._Mrho = self.mass_ops.WMM + self._Mrho.inv._options["pc"] = MassMatrixDiagonalPreconditioner(self._Mrho.massop) # Femfields for the projector self.sf = self.derham.create_spline_function("sf", "L2") @@ -5569,9 +5784,13 @@ def __init__( self.gu122f = self.derham.create_spline_function("gu122", "Hcurl") # Projector + self._energy_evaluator = InternalEnergyEvaluator(self.derham, self._gamma) self._initialize_projectors_and_mass() # bunch of temporaries to avoid allocating in the loop + u = self.variables.u.spline.vector + s = self.variables.s.spline.vector + self._tmp_un1 = u.space.zeros() self._tmp_un12 = u.space.zeros() self._tmp_sn1 = s.space.zeros() @@ -5588,7 +5807,7 @@ def __init__( self.tot_rhs = s.space.zeros() def __call__(self, dt): - if self._nonlin_solver["type"] == "Newton": + if self._nonlin_solver.type == "Newton": self.__call_newton(dt) else: raise ValueError( @@ -5598,10 +5817,11 @@ def __call__(self, dt): def __call_newton(self, dt): """Solve the non linear system for updating the variables using Newton iteration method""" # Compute dissipation implicitely - sn = self.feec_vars[0] - un = self.feec_vars[1] + sn = self.variables.s.spline.vector + un = self.variables.u.spline.vector + if self._mu < 1.0e-15 and self._mu_a < 1.0e-15 and self._alpha < 1.0e-15: - self.feec_vars_update(sn, un) + self.update_feec_variables(s=sn, u=un) return if self._info: @@ -5619,7 +5839,7 @@ def __call_newton(self, dt): print("information on the linear solver : ", self.inv_lop._info) if self._model == "linear_p" or (self._model == "linear_q" and self._nonlin_solver["fast"]): - self.feec_vars_update(sn, un1) + self.update_feec_variables(s=sn, u=un1) return # Energy balance term @@ -5628,7 +5848,7 @@ def __call_newton(self, dt): # 2) Initial energy and linear form rho = self._rho if self._model in ["deltaf_q", "linear_q"]: - self.sf.vector = self._pt3 + self.sf.vector = self._pt3.spline.vector else: self.sf.vector = sn @@ -5684,7 +5904,7 @@ def __call_newton(self, dt): for it in range(self._nonlin_solver["maxiter"]): if self._model in ["deltaf_q", "linear_q"]: - self.sf1.vector = self._pt3 + self.sf1.vector = self._pt3.spline.vector else: self.sf1.vector = sn1 @@ -5736,7 +5956,7 @@ def __call_newton(self, dt): if self._info: print("iteration : ", it, " error : ", err) - if (err < tol**2 and it > 0) or np.isnan(err): + if (err < tol**2 and it > 0) or xp.isnan(err): # force at least one iteration break @@ -5774,18 +5994,16 @@ def __call_newton(self, dt): else: sn1 += incr - if it == self._nonlin_solver["maxiter"] - 1 or np.isnan(err): + if it == self._nonlin_solver["maxiter"] - 1 or xp.isnan(err): print( f"!!!Warning: Maximum iteration in VariationalViscosity reached - not converged:\n {err = } \n {tol**2 = }", ) - self.feec_vars_update(sn1, un1) + self.update_feec_variables(s=sn1, u=un1) def _initialize_projectors_and_mass(self): """Initialization of all the `BasisProjectionOperator` and needed to compute the bracket term""" - from struphy.feec.projectors import L2Projector - Xv = getattr(self.basis_ops, "Xv") Pcoord0 = CoordinateProjector( 0, @@ -5820,12 +6038,12 @@ def _initialize_projectors_and_mass(self): self.M_de_ds = self.mass_ops.create_weighted_mass("L2", "L2") - if self._lin_solver["type"][1] is None: + if self.options.precond is None: self.pc_jac = None else: pc_class = getattr( preconditioner, - self._lin_solver["type"][1], + self.options.precond, ) self.pc_jac = pc_class(self.M_de_ds) @@ -5833,8 +6051,8 @@ def _initialize_projectors_and_mass(self): self.M_de_ds, "pcg", pc=self.pc_jac, - tol=self._lin_solver["tol"], - maxiter=self._lin_solver["maxiter"], + tol=self._lin_solver.tol, + maxiter=self._lin_solver.maxiter, verbose=False, recycle=True, ) @@ -5875,8 +6093,8 @@ def _initialize_projectors_and_mass(self): self.l_op, "pcg", pc=self._Mrho.inv, - tol=self._lin_solver["tol"], - maxiter=self._lin_solver["maxiter"], + tol=self._lin_solver.tol, + maxiter=self._lin_solver.maxiter, verbose=False, recycle=True, ) @@ -5912,35 +6130,35 @@ def _initialize_projectors_and_mass(self): grid_shape = tuple([len(loc_grid) for loc_grid in integration_grid]) - self._guf0_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] - self._guf1_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] - self._guf2_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] + self._guf0_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] + self._guf1_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] + self._guf2_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] - self._guf120_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] - self._guf121_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] - self._guf122_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] + self._guf120_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] + self._guf121_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] + self._guf122_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] - self._uf1_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] - self._uf12_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] + self._uf1_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] + self._uf12_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] - self._gu_sq_values = np.zeros(grid_shape, dtype=float) - self._u_sq_values = np.zeros(grid_shape, dtype=float) - self._gu_init_values = np.zeros(grid_shape, dtype=float) + self._gu_sq_values = xp.zeros(grid_shape, dtype=float) + self._u_sq_values = xp.zeros(grid_shape, dtype=float) + self._gu_init_values = xp.zeros(grid_shape, dtype=float) - self._sf_values = np.zeros(grid_shape, dtype=float) - self._sf1_values = np.zeros(grid_shape, dtype=float) - self._rhof_values = np.zeros(grid_shape, dtype=float) + self._sf_values = xp.zeros(grid_shape, dtype=float) + self._sf1_values = xp.zeros(grid_shape, dtype=float) + self._rhof_values = xp.zeros(grid_shape, dtype=float) - self._e_n1 = np.zeros(grid_shape, dtype=float) - self._e_n = np.zeros(grid_shape, dtype=float) + self._e_n1 = xp.zeros(grid_shape, dtype=float) + self._e_n = xp.zeros(grid_shape, dtype=float) - self._de_s1_values = np.zeros(grid_shape, dtype=float) + self._de_s1_values = xp.zeros(grid_shape, dtype=float) - self._tmp_int_grid = np.zeros(grid_shape, dtype=float) + self._tmp_int_grid = xp.zeros(grid_shape, dtype=float) gam = self._gamma if self._model == "full": - metric = np.power( + metric = xp.power( self.domain.jacobian_det( *integration_grid, ), @@ -5948,7 +6166,7 @@ def _initialize_projectors_and_mass(self): ) self._mass_metric_term = deepcopy(metric) - metric = np.power( + metric = xp.power( self.domain.jacobian_det( *integration_grid, ), @@ -5981,7 +6199,7 @@ def _initialize_projectors_and_mass(self): self.pc_jac.update_mass_operator(self.M_de_ds) elif self._model in ["full_q", "linear_q", "deltaf_q"]: - metric = np.power( + metric = xp.power( self.domain.jacobian_det( *integration_grid, ), @@ -5989,7 +6207,7 @@ def _initialize_projectors_and_mass(self): ) self._mass_metric_term = deepcopy(metric) - metric = np.power( + metric = xp.power( self.domain.jacobian_det( *integration_grid, ), @@ -5997,7 +6215,7 @@ def _initialize_projectors_and_mass(self): ) self._energy_metric = deepcopy(metric) - metric = np.power( + metric = xp.power( self.domain.jacobian_det( *integration_grid, ), @@ -6063,7 +6281,7 @@ def _update_artificial_viscosity(self, un, dt): gu_sq_v += gu1_v[i] gu_sq_v += gu2_v[i] - np.sqrt(gu_sq_v, out=gu_sq_v) + xp.sqrt(gu_sq_v, out=gu_sq_v) gu_sq_v *= dt * self._mu_a # /2 @@ -6221,63 +6439,92 @@ class VariationalResistivity(Propagator): """ - @staticmethod - def options(default=False): - dct = {} - dct["lin_solver"] = { - "tol": 1e-12, - "maxiter": 500, - "type": [ - ("pcg", "MassMatrixDiagonalPreconditioner"), - ("cg", None), - ], - "verbose": False, - } - dct["nonlin_solver"] = {"tol": 1e-8, "maxiter": 100, "type": ["Newton"], "info": False, "fast": False} - dct["physics"] = { - "eta": 0.0, - "eta_a": 0.0, - "gamma": 5 / 3, - } - dct["linearize_current"] = False - - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - s: StencilVector, - b: BlockVector, - *, - model: str = "full", - gamma: float = options()["physics"]["gamma"], - rho: StencilVector, - eta: float = options()["physics"]["eta"], - eta_a: float = options()["physics"]["eta_a"], - lin_solver: dict = options(default=True)["lin_solver"], - nonlin_solver: dict = options(default=True)["nonlin_solver"], - linearize_current: dict = options(default=True)["linearize_current"], - energy_evaluator: InternalEnergyEvaluator = None, - pt3: StencilVector | None = None, - ): - super().__init__(s, b) - - assert model in ["full", "full_p", "full_q", "linear_p", "delta_p", "linear_q", "deltaf_q"] - - self._energy_evaluator = energy_evaluator - self._model = model - self._gamma = gamma - self._eta = eta - self._eta_a = eta_a - self._lin_solver = lin_solver - self._nonlin_solver = nonlin_solver - self._rho = rho - self._linearize_current = linearize_current - self._pt3 = pt3 + class Variables: + def __init__(self): + self._s: FEECVariable = None + self._b: FEECVariable = None + + @property + def s(self) -> FEECVariable: + return self._s + + @s.setter + def s(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "L2" + self._s = new + + @property + def b(self) -> FEECVariable: + return self._b + + @b.setter + def b(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "Hdiv" + self._b = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # specific literals + OptsModel = Literal["full", "full_p", "full_q", "linear_p", "linear_q", "deltaf_q"] + # propagator options + model: OptsModel = "full" + gamma: float = 5.0 / 3.0 + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixDiagonalPreconditioner" + solver_params: SolverParameters = None + nonlin_solver: NonlinearSolverParameters = None + linearize_current: bool = False + rho: FEECVariable = None + pt3: FEECVariable = None + eta: float = 0.0 + eta_a: float = 0.0 + + def __post_init__(self): + # checks + check_option(self.model, self.OptsModel) + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + if self.nonlin_solver is None: + self.nonlin_solver = NonlinearSolverParameters(type="Newton") - self._info = self._nonlin_solver["info"] and (MPI.COMM_WORLD.Get_rank() == 0) + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + self._model = self.options.model + self._gamma = self.options.gamma + self._eta = self.options.eta + self._eta_a = self.options.eta_a + self._lin_solver = self.options.solver_params + self._nonlin_solver = self.options.nonlin_solver + self._linearize_current = self.options.linearize_current + self._rho = self.options.rho + self._pt3 = self.options.pt3 + + self._info = self._nonlin_solver.info and (MPI.COMM_WORLD.Get_rank() == 0) # Femfields for the projector self.rhof = self.derham.create_spline_function("rhof", "L2") @@ -6289,9 +6536,13 @@ def __init__( self.cbf12 = self.derham.create_spline_function("cBf", "Hcurl") # Projector + self._energy_evaluator = InternalEnergyEvaluator(self.derham, self._gamma) self._initialize_projectors_and_mass() # bunch of temporaries to avoid allocating in the loop + s = self.variables.s.spline.vector + b = self.variables.b.spline.vector + self._tmp_bn1 = b.space.zeros() self._tmp_bn12 = b.space.zeros() self._tmp_sn1 = s.space.zeros() @@ -6308,7 +6559,7 @@ def __init__( ) def __call__(self, dt): - if self._nonlin_solver["type"] == "Newton": + if self._nonlin_solver.type == "Newton": self.__call_newton(dt) else: raise ValueError( @@ -6318,10 +6569,11 @@ def __call__(self, dt): def __call_newton(self, dt): """Solve the non linear system for updating the variables using Newton iteration method""" # Compute dissipation implicitely - sn = self.feec_vars[0] - bn = self.feec_vars[1] + sn = self.variables.s.spline.vector + bn = self.variables.b.spline.vector + if self._eta < 1.0e-15 and self._eta_a < 1.0e-15: - self.feec_vars_update(sn, bn) + self.update_feec_variables(s=sn, b=bn) return if self._info: @@ -6348,17 +6600,17 @@ def __call_newton(self, dt): print("information on the linear solver : ", self.inv_lop._info) if self._model == "linear_p" or (self._model == "linear_q" and self._nonlin_solver["fast"]): - self.feec_vars_update(sn, bn1) + self.update_feec_variables(s=sn, b=bn1) return # Energy balance term # 1) Pointwize energy change energy_change = self._get_energy_change(bn, bn1, total_resistivity) # 2) Initial energy and linear form - rho = self._rho + rho = self._rho.spline.vector self.rhof.vector = rho if self._model in ["deltaf_q", "linear_q"]: - self.sf.vector = self._pt3 + self.sf.vector = self._pt3.spline.vector else: self.sf.vector = sn @@ -6418,7 +6670,7 @@ def __call_newton(self, dt): for it in range(self._nonlin_solver["maxiter"]): if self._model in ["deltaf_q", "linear_q"]: - self.sf1.vector = self._pt3 + self.sf1.vector = self._pt3.spline.vector else: self.sf1.vector = sn1 @@ -6470,7 +6722,7 @@ def __call_newton(self, dt): if self._info: print("iteration : ", it, " error : ", err) - if (err < tol**2 and it > 0) or np.isnan(err): + if (err < tol**2 and it > 0) or xp.isnan(err): break if self._model == "full": @@ -6506,12 +6758,12 @@ def __call_newton(self, dt): else: sn1 += incr - if it == self._nonlin_solver["maxiter"] - 1 or np.isnan(err): + if it == self._nonlin_solver["maxiter"] - 1 or xp.isnan(err): print( f"!!!Warning: Maximum iteration in VariationalResistivity reached - not converged:\n {err = } \n {tol**2 = }", ) - self.feec_vars_update(sn1, bn1) + self.update_feec_variables(s=sn1, b=bn1) # if self._pt3 is not None: # bn12 = bn.copy(out=self._tmp_bn12) @@ -6589,7 +6841,7 @@ def __call_newton(self, dt): # if self._info: # print("iteration : ", it, " error : ", err) - # if (err < tol**2 and it > 0) or np.isnan(err): + # if (err < tol**2 and it > 0) or xp.isnan(err): # break # incr = self.inv_jac.dot(self.tot_rhs, out=self._tmp_sn_incr) @@ -6602,8 +6854,6 @@ def __call_newton(self, dt): def _initialize_projectors_and_mass(self): """Initialization of all the `BasisProjectionOperator` and needed to compute the bracket term""" - from struphy.feec.projectors import L2Projector - pc_M1 = preconditioner.MassMatrixDiagonalPreconditioner( self.mass_ops.M1, ) @@ -6634,12 +6884,12 @@ def _initialize_projectors_and_mass(self): D = [[1, 0, 0], [0, 1, 0], [0, 0, 1]] self.M1_cb = self.mass_ops.create_weighted_mass("Hcurl", "Hcurl", weights=[D, "sqrt_g"]) - if self._lin_solver["type"][1] is None: + if self.options.precond is None: self.pc = None else: pc_class = getattr( preconditioner, - self._lin_solver["type"][1], + self.options.precond, ) self.pc_jac = pc_class(self.M_de_ds) @@ -6647,8 +6897,8 @@ def _initialize_projectors_and_mass(self): self.M_de_ds, "pcg", pc=self.pc_jac, - tol=self._lin_solver["tol"], - maxiter=self._lin_solver["maxiter"], + tol=self._lin_solver.tol, + maxiter=self._lin_solver.maxiter, verbose=False, recycle=True, ) @@ -6664,12 +6914,12 @@ def _initialize_projectors_and_mass(self): self.r_op = M2 # - self._scaled_stiffness self.l_op = M2 + self._scaled_stiffness + self.phy_cb_stiffness - if self._lin_solver["type"][1] is None: + if self.options.precond is None: self.pc = None else: pc_class = getattr( preconditioner, - self._lin_solver["type"][1], + self.options.precond, ) self.pc = pc_class(M2) @@ -6677,8 +6927,8 @@ def _initialize_projectors_and_mass(self): self.l_op, "pcg", pc=self.pc, - tol=self._lin_solver["tol"], - maxiter=self._lin_solver["maxiter"], + tol=self._lin_solver.tol, + maxiter=self._lin_solver.maxiter, verbose=False, recycle=True, ) @@ -6704,26 +6954,26 @@ def _initialize_projectors_and_mass(self): grid_shape = tuple([len(loc_grid) for loc_grid in integration_grid]) - self._cb12_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] - self._cb1_values = [np.zeros(grid_shape, dtype=float) for i in range(3)] + self._cb12_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] + self._cb1_values = [xp.zeros(grid_shape, dtype=float) for i in range(3)] - self._cb_sq_values = np.zeros(grid_shape, dtype=float) - self._cb_sq_values_init = np.zeros(grid_shape, dtype=float) + self._cb_sq_values = xp.zeros(grid_shape, dtype=float) + self._cb_sq_values_init = xp.zeros(grid_shape, dtype=float) - self._sf_values = np.zeros(grid_shape, dtype=float) - self._sf1_values = np.zeros(grid_shape, dtype=float) - self._rhof_values = np.zeros(grid_shape, dtype=float) + self._sf_values = xp.zeros(grid_shape, dtype=float) + self._sf1_values = xp.zeros(grid_shape, dtype=float) + self._rhof_values = xp.zeros(grid_shape, dtype=float) - self._e_n1 = np.zeros(grid_shape, dtype=float) - self._e_n = np.zeros(grid_shape, dtype=float) + self._e_n1 = xp.zeros(grid_shape, dtype=float) + self._e_n = xp.zeros(grid_shape, dtype=float) - self._de_s1_values = np.zeros(grid_shape, dtype=float) + self._de_s1_values = xp.zeros(grid_shape, dtype=float) - self._tmp_int_grid = np.zeros(grid_shape, dtype=float) + self._tmp_int_grid = xp.zeros(grid_shape, dtype=float) gam = self._gamma if self._model == "full": - metric = np.power( + metric = xp.power( self.domain.jacobian_det( *integration_grid, ), @@ -6731,7 +6981,7 @@ def _initialize_projectors_and_mass(self): ) self._mass_metric_term = deepcopy(metric) - metric = np.power( + metric = xp.power( self.domain.jacobian_det( *integration_grid, ), @@ -6764,7 +7014,7 @@ def _initialize_projectors_and_mass(self): self.pc_jac.update_mass_operator(self.M_de_ds) elif self._model in ["full_q", "linear_q", "deltaf_q"]: - metric = np.power( + metric = xp.power( self.domain.jacobian_det( *integration_grid, ), @@ -6772,7 +7022,7 @@ def _initialize_projectors_and_mass(self): ) self._mass_metric_term = deepcopy(metric) - metric = np.power( + metric = xp.power( self.domain.jacobian_det( *integration_grid, ), @@ -6819,7 +7069,7 @@ def _update_artificial_resistivity(self, bn, dt): for j in range(3): cb_sq_v += cb_v[i] * self._sq_term_metric_no_jac[i, j] * cb_v[j] - np.sqrt(cb_sq_v, out=cb_sq_v) + xp.sqrt(cb_sq_v, out=cb_sq_v) cb_sq_v *= dt * self._eta_a @@ -6917,48 +7167,74 @@ class TimeDependentSource(Propagator): * :math:`h(\omega t) = \sin(\omega t)` """ - @staticmethod - def options(default=False): - dct = {} - dct["omega"] = 1.0 - dct["hfun"] = ["cos", "sin"] - if default: - dct = descend_options_dict(dct, []) - return dct + class Variables: + def __init__(self): + self._source: FEECVariable = None - def __init__( - self, - c: StencilVector, - *, - omega: float = options()["omega"], - hfun: str = options(default=True)["hfun"], - ): - super().__init__(c) + @property + def source(self) -> FEECVariable: + return self._source - if hfun == "cos": + @source.setter + def source(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "H1" + self._source = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # specific literals + OptsTimeSource = Literal["cos", "sin"] + # propagator options + omega: float = 2.0 * xp.pi + hfun: OptsTimeSource = "cos" + + def __post_init__(self): + # checks + check_option(self.hfun, self.OptsTimeSource) + + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + if self.options.hfun == "cos": def hfun(t): - return np.cos(omega * t) - elif hfun == "sin": + return xp.cos(self.options.omega * t) + elif self.options.hfun == "sin": def hfun(t): - return np.sin(omega * t) + return xp.sin(self.options.omega * t) else: - raise NotImplementedError(f"{hfun = } not implemented.") + raise NotImplementedError(f"{self.options.hfun = } not implemented.") self._hfun = hfun + self._c0 = self.variables.source.spline.vector.copy() + @profile def __call__(self, dt): - print(f"{self.time_state[0] = }") - if self.time_state[0] == 0.0: - self._c0 = self.feec_vars[0].copy() - print("Initial source coeffs set.") - # new coeffs cn1 = self._c0 * self._hfun(self.time_state[0]) # write new coeffs into self.feec_vars - max_dc = self.feec_vars_update(cn1) + # max_dc = self.feec_vars_update(cn1) + self.update_feec_variables(source=cn1) class AdiabaticPhi(Propagator): @@ -7196,63 +7472,100 @@ class HasegawaWakatani(Propagator): Solver parameters for M0 inversion. """ - @staticmethod - def options(default=False): - dct = {} - dct["c_fun"] = ["const"] - dct["kappa"] = 1.0 - dct["nu"] = 0.01 - dct["algo"] = ButcherTableau.available_methods() - dct["M0_solver"] = { - "type": [ - ("pcg", "MassMatrixPreconditioner"), - ("cg", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - if default: - dct = descend_options_dict(dct, []) - return dct - - def __init__( - self, - n0: StencilVector, - omega0: StencilVector, - *, - phi: SplineFunction = None, - c_fun: str = options(default=True)["c_fun"], - kappa: float = options(default=True)["kappa"], - nu: float = options(default=True)["nu"], - algo: str = options(default=True)["algo"], - M0_solver: dict = options(default=True)["M0_solver"], - ): - super().__init__(n0, omega0) + class Variables: + def __init__(self): + self._n: FEECVariable = None + self._omega: FEECVariable = None + + @property + def n(self) -> FEECVariable: + return self._n + + @n.setter + def n(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "H1" + self._n = new + + @property + def omega(self) -> FEECVariable: + return self._omega + + @omega.setter + def omega(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "H1" + self._omega = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # specific literals + OptsCfun = Literal["const"] + # propagator options + phi: FEECVariable = None + c_fun: OptsCfun = "const" + kappa: float = 1.0 + nu: float = 0.01 + butcher: ButcherTableau = None + solver: OptsSymmSolver = "pcg" + precond: OptsMassPrecond = "MassMatrixPreconditioner" + solver_params: SolverParameters = None + + def __post_init__(self): + # checks + check_option(self.c_fun, self.OptsCfun) + check_option(self.solver, OptsSymmSolver) + check_option(self.precond, OptsMassPrecond) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() + + if self.butcher is None: + self.butcher = ButcherTableau() + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): # default phi - if phi is None: - self._phi = self.derham.create_spline_function("phi", "H1") - self._phi.vector[:] = 1.0 - self._phi.vector.update_ghost_regions() - else: - self._phi = phi + if self.options.phi is None: + self.options.phi = FEECVariable(space="H1") + self.options.phi.allocate(derham=self.derham, domain=self.domain) + + self._phi = self.options.phi.spline + self._phi.vector[:] = 1.0 + self._phi.vector.update_ghost_regions() # default c-function - if c_fun == "const": + if self.options.c_fun == "const": c_fun = lambda e1, e2, e3: 0.0 + 0.0 * e1 else: - raise NotImplementedError(f"{c_fun = } is not available.") + raise NotImplementedError(f"{self.options.c_fun = } is not available.") # expose equation parameters - self._kappa = kappa - self._nu = nu + self._kappa = self.options.kappa + self._nu = self.options.nu # get quadrature grid of V0 pts = [grid.flatten() for grid in self.derham.quad_grid_pts["0"]] - mesh_pts = np.meshgrid(*pts, indexing="ij") + mesh_pts = xp.meshgrid(*pts, indexing="ij") # evaluate c(x, y) and metric coeff at local quadrature grid and multiply self._weights = c_fun(*mesh_pts) @@ -7285,13 +7598,13 @@ def __init__( for m in range(3): self._M1hw_weights += [[None, None, None]] - self._phi_5d = np.zeros((*self._phi_at_pts.shape, 3, 3), dtype=float) - self._tmp_5d = np.zeros((*self._phi_at_pts.shape, 3, 3), dtype=float) - self._tmp_5dT = np.zeros((3, 3, *self._phi_at_pts.shape), dtype=float) + self._phi_5d = xp.zeros((*self._phi_at_pts.shape, 3, 3), dtype=float) + self._tmp_5d = xp.zeros((*self._phi_at_pts.shape, 3, 3), dtype=float) + self._tmp_5dT = xp.zeros((3, 3, *self._phi_at_pts.shape), dtype=float) self._phi_5d[:, :, :, 0, 1] = self._phi_at_pts * self._jac_det self._phi_5d[:, :, :, 1, 0] = -self._phi_at_pts * self._jac_det self._tmp_5d[:] = self._jac_inv @ self._phi_5d @ self._jac_invT - self._tmp_5dT[:] = np.transpose(self._tmp_5d, axes=(3, 4, 0, 1, 2)) + self._tmp_5dT[:] = xp.transpose(self._tmp_5d, axes=(3, 4, 0, 1, 2)) self._M1hw_weights[0][1] = self._tmp_5dT[0, 1, :, :, :] self._M1hw_weights[1][0] = self._tmp_5dT[1, 0, :, :, :] @@ -7306,16 +7619,24 @@ def __init__( ) # inverse M0 mass matrix - solver = M0_solver["type"][0] - if M0_solver["type"][1] is None: + solver = self.options.solver + if self.options.precond is None: pc = None else: - pc_class = getattr(preconditioner, M0_solver["type"][1]) + pc_class = getattr(preconditioner, self.options.precond) pc = pc_class(self.mass_ops.M0) - solver_params = deepcopy(M0_solver) # need a copy to pop, otherwise testing fails - solver_params.pop("type") - self._info = solver_params.pop("info") - M0_inv = inverse(M0, solver, pc=pc, **solver_params) + # solver_params = deepcopy(M0_solver) # need a copy to pop, otherwise testing fails + # solver_params.pop("type") + self._info = self.options.solver_params.info + M0_inv = inverse( + M0, + solver, + pc=pc, + tol=self.options.solver_params.tol, + maxiter=self.options.solver_params.maxiter, + verbose=self.options.solver_params.verbose, + recycle=self.options.solver_params.recycle, + ) # basis projection operator df_12 = lambda e1, e2, e3: self.domain.jacobian_inv(e1, e2, e3)[0, 1, :, :, :] @@ -7333,6 +7654,9 @@ def __init__( # print(f"{self._BPO._dof_mat.blocks = }") # pre-allocated helper arrays + n0 = self.variables.n.spline.vector + omega0 = self.variables.omega.spline.vector + self._tmp1 = n0.space.zeros() tmp2 = n0.space.zeros() self._tmp3 = n0.space.zeros() @@ -7340,11 +7664,11 @@ def __init__( tmp5 = n0.space.zeros() # rhs-callables for explicit ode solve - terms1_n = -M0c + grad.T @ self._M1hw @ grad - nu * grad.T @ M1 @ grad + terms1_n = -M0c + grad.T @ self._M1hw @ grad - self.options.nu * grad.T @ M1 @ grad terms1_phi = M0c - terms1_phi_strong = -kappa * self._BPO @ grad + terms1_phi_strong = -self.options.kappa * self._BPO @ grad - terms2_omega = grad.T @ self._M1hw @ grad - nu * grad.T @ M1 @ grad + terms2_omega = grad.T @ self._M1hw @ grad - self.options.nu * grad.T @ M1 @ grad terms2_n = -M0c terms2_phi = M0c @@ -7372,7 +7696,7 @@ def f2(t, n, omega, out=out2): return out vector_field = {n0: f1, omega0: f2} - self._ode_solver = ODEsolverFEEC(vector_field, algo=algo) + self._ode_solver = ODEsolverFEEC(vector_field, butcher=self.options.butcher) def __call__(self, dt): # update time-dependent mass operator @@ -7381,7 +7705,7 @@ def __call__(self, dt): self._phi_5d[:, :, :, 0, 1] = self._phi_at_pts * self._jac_det self._phi_5d[:, :, :, 1, 0] = -self._phi_at_pts * self._jac_det self._tmp_5d[:] = self._jac_inv @ self._phi_5d @ self._jac_invT - self._tmp_5dT[:] = np.transpose(self._tmp_5d, axes=(3, 4, 0, 1, 2)) + self._tmp_5dT[:] = xp.transpose(self._tmp_5d, axes=(3, 4, 0, 1, 2)) self._M1hw_weights[0][1] = self._tmp_5dT[0, 1, :, :, :] self._M1hw_weights[1][0] = self._tmp_5dT[1, 0, :, :, :] @@ -7410,91 +7734,122 @@ class TwoFluidQuasiNeutralFull(Propagator): :ref:`time_discret`: fully implicit. """ - @staticmethod - def options(default=False): - dct = {} - dct["solver"] = { - "type": [ - ("gmres", None), - ], - "tol": 1.0e-8, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": True, - } - dct["nu"] = 1.0 - dct["nu_e"] = 0.01 - dct["override_eq_params"] = [False, {"epsilon": 1.0}] - dct["eps_norm"] = 1.0 - dct["a"] = 1.0 - dct["R0"] = 1.0 - dct["B0"] = 10.0 - dct["Bp"] = 12.5 - dct["alpha"] = 0.1 - dct["beta"] = 1.0 - dct["stab_sigma"] = 0.00001 - dct["variant"] = "GMRES" - dct["method_to_solve"] = "DirectNPInverse" - dct["preconditioner"] = False - dct["spectralanalysis"] = False - dct["lifting"] = False - dct["dimension"] = "2D" - dct["1D_dt"] = 0.001 - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - u: BlockVector, - ue: BlockVector, - phi: BlockVector, - *, - nu: float = options(default=True)["nu"], - nu_e: float = options(default=True)["nu_e"], - eps_norm: float = options(default=True)["eps_norm"], - solver: dict = options(default=True)["solver"], - a: float = options(default=True)["a"], - R0: float = options(default=True)["R0"], - B0: float = options(default=True)["B0"], - Bp: float = options(default=True)["Bp"], - alpha: float = options(default=True)["alpha"], - beta: float = options(default=True)["beta"], - stab_sigma: float = options(default=True)["stab_sigma"], - variant: str = options(default=True)["variant"], - method_to_solve: str = options(default=True)["method_to_solve"], - preconditioner: bool = options(default=True)["preconditioner"], - spectralanalysis: bool = options(default=True)["spectralanalysis"], - lifting: bool = options(default=False)["lifting"], - dimension: str = options(default=True)["dimension"], - D1_dt: float = options(default=True)["1D_dt"], - ): - super().__init__(u, ue, phi) + class Variables: + def __init__(self): + self._u: FEECVariable = None + self._ue: FEECVariable = None + self._phi: FEECVariable = None + + @property + def u(self) -> FEECVariable: + return self._u + + @u.setter + def u(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "Hdiv" + self._u = new + + @property + def ue(self) -> FEECVariable: + return self._ue + + @ue.setter + def ue(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "Hdiv" + self._ue = new + + @property + def phi(self) -> FEECVariable: + return self._phi + + @phi.setter + def phi(self, new): + assert isinstance(new, FEECVariable) + assert new.space == "L2" + self._phi = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # specific literals + OptsDimension = Literal["1D", "2D", "Restelli", "Tokamak"] + # propagator options + nu: float = 1.0 + nu_e: float = 0.01 + eps_norm: float = 1.0 + solver: OptsGenSolver = "GMRES" + solver_params: SolverParameters = None + a: float = 1.0 + R0: float = 1.0 + B0: float = 10.0 + Bp: float = 12.0 + alpha: float = 0.1 + beta: float = 1.0 + stab_sigma: float = 1e-5 + variant: OptsSaddlePointSolver = "Uzawa" + method_to_solve: OptsDirectSolver = "DirectNPInverse" + preconditioner: bool = False + spectralanalysis: bool = False + lifting: bool = False + dimension: OptsDimension = "2D" + D1_dt: float = 1e-3 + + def __post_init__(self): + # checks + check_option(self.solver, OptsGenSolver) + check_option(self.variant, OptsSaddlePointSolver) + check_option(self.method_to_solve, OptsDirectSolver) + check_option(self.dimension, self.OptsDimension) + + # defaults + if self.solver_params is None: + self.solver_params = SolverParameters() - self._info = solver["info"] + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + self._info = self.options.solver_params.info if self.derham.comm is not None: self._rank = self.derham.comm.Get_rank() else: self._rank = 0 - self._nu = nu - self._nu_e = nu_e - self._eps_norm = eps_norm - self._a = a - self._R0 = R0 - self._B0 = B0 - self._Bp = Bp - self._alpha = alpha - self._beta = beta - self._stab_sigma = stab_sigma - self._variant = variant - self._method_to_solve = method_to_solve - self._preconditioner = preconditioner - self._dimension = dimension - self._spectralanalysis = spectralanalysis - self._lifting = lifting + self._nu = self.options.nu + self._nu_e = self.options.nu_e + self._eps_norm = self.options.eps_norm + self._a = self.options.a + self._R0 = self.options.R0 + self._B0 = self.options.B0 + self._Bp = self.options.Bp + self._alpha = self.options.alpha + self._beta = self.options.beta + self._stab_sigma = self.options.stab_sigma + self._variant = self.options.variant + self._method_to_solve = self.options.method_to_solve + self._preconditioner = self.options.preconditioner + self._dimension = self.options.dimension + self._spectralanalysis = self.options.spectralanalysis + self._lifting = self.options.lifting + + solver_params = self.options.solver_params # Lifting for nontrivial boundary conditions # derham had boundary conditions in eta1 direction, the following is in space Hdiv_0 @@ -7510,13 +7865,13 @@ def __init__( self._mass_opsv0 = WeightedMassOperators( self.derhamv0, self.domain, - verbose=solver["verbose"], + verbose=solver_params.verbose, eq_mhd=self.mass_ops.weights["eq_mhd"], ) self._basis_opsv0 = BasisProjectionOperators( self.derhamv0, self.domain, - verbose=solver["verbose"], + verbose=solver_params.verbose, eq_mhd=self.basis_ops.weights["eq_mhd"], ) else: @@ -7545,7 +7900,7 @@ def __init__( dimension=self._dimension, stab_sigma=self._stab_sigma, eps=self._eps_norm, - dt=D1_dt, + dt=self.options.D1_dt, ) _funy = getattr(callables, "ManufacturedSolutionForceterm")( species="Ions", @@ -7555,7 +7910,7 @@ def __init__( dimension=self._dimension, stab_sigma=self._stab_sigma, eps=self._eps_norm, - dt=D1_dt, + dt=self.options.D1_dt, ) _funelectronsx = getattr(callables, "ManufacturedSolutionForceterm")( species="Electrons", @@ -7565,7 +7920,7 @@ def __init__( dimension=self._dimension, stab_sigma=self._stab_sigma, eps=self._eps_norm, - dt=D1_dt, + dt=self.options.D1_dt, ) _funelectronsy = getattr(callables, "ManufacturedSolutionForceterm")( species="Electrons", @@ -7575,7 +7930,7 @@ def __init__( dimension=self._dimension, stab_sigma=self._stab_sigma, eps=self._eps_norm, - dt=D1_dt, + dt=self.options.D1_dt, ) # get callable(s) for specified init type @@ -7584,16 +7939,16 @@ def __init__( # pullback callable funx = TransformedPformComponent( - forceterm_class, fun_basis="physical", out_form="2", comp=0, domain=self.domain + forceterm_class, given_in_basis="physical", out_form="2", comp=0, domain=self.domain ) funy = TransformedPformComponent( - forceterm_class, fun_basis="physical", out_form="2", comp=1, domain=self.domain + forceterm_class, given_in_basis="physical", out_form="2", comp=1, domain=self.domain ) fun_electronsx = TransformedPformComponent( - forcetermelectrons_class, fun_basis="physical", out_form="2", comp=0, domain=self.domain + forcetermelectrons_class, given_in_basis="physical", out_form="2", comp=0, domain=self.domain ) fun_electronsy = TransformedPformComponent( - forcetermelectrons_class, fun_basis="physical", out_form="2", comp=1, domain=self.domain + forcetermelectrons_class, given_in_basis="physical", out_form="2", comp=1, domain=self.domain ) l2_proj = L2Projector(space_id="Hdiv", mass_ops=self.mass_ops) self._F1 = l2_proj([funx, funy, _forceterm_logical]) @@ -7628,22 +7983,22 @@ def __init__( # pullback callable fun_pb_1 = TransformedPformComponent( - forceterm_class, fun_basis="physical", out_form="2", comp=0, domain=self.domain + forceterm_class, given_in_basis="physical", out_form="2", comp=0, domain=self.domain ) fun_pb_2 = TransformedPformComponent( - forceterm_class, fun_basis="physical", out_form="2", comp=1, domain=self.domain + forceterm_class, given_in_basis="physical", out_form="2", comp=1, domain=self.domain ) fun_pb_3 = TransformedPformComponent( - forceterm_class, fun_basis="physical", out_form="2", comp=2, domain=self.domain + forceterm_class, given_in_basis="physical", out_form="2", comp=2, domain=self.domain ) fun_electrons_pb_1 = TransformedPformComponent( - forcetermelectrons_class, fun_basis="physical", out_form="2", comp=0, domain=self.domain + forcetermelectrons_class, given_in_basis="physical", out_form="2", comp=0, domain=self.domain ) fun_electrons_pb_2 = TransformedPformComponent( - forcetermelectrons_class, fun_basis="physical", out_form="2", comp=1, domain=self.domain + forcetermelectrons_class, given_in_basis="physical", out_form="2", comp=1, domain=self.domain ) fun_electrons_pb_3 = TransformedPformComponent( - forcetermelectrons_class, fun_basis="physical", out_form="2", comp=2, domain=self.domain + forcetermelectrons_class, given_in_basis="physical", out_form="2", comp=2, domain=self.domain ) if self._lifting: l2_proj = L2Projector(space_id="Hdiv", mass_ops=self._mass_opsv0) @@ -7666,7 +8021,7 @@ def __init__( dimension=self._dimension, stab_sigma=self._stab_sigma, eps=self._eps_norm, - dt=D1_dt, + dt=self.options.D1_dt, a=self._a, Bp=self._Bp, alpha=self._alpha, @@ -7680,7 +8035,7 @@ def __init__( dimension=self._dimension, stab_sigma=self._stab_sigma, eps=self._eps_norm, - dt=D1_dt, + dt=self.options.D1_dt, a=self._a, Bp=self._Bp, alpha=self._alpha, @@ -7694,7 +8049,7 @@ def __init__( dimension=self._dimension, stab_sigma=self._stab_sigma, eps=self._eps_norm, - dt=D1_dt, + dt=self.options.D1_dt, a=self._a, Bp=self._Bp, alpha=self._alpha, @@ -7708,7 +8063,7 @@ def __init__( dimension=self._dimension, stab_sigma=self._stab_sigma, eps=self._eps_norm, - dt=D1_dt, + dt=self.options.D1_dt, a=self._a, Bp=self._Bp, alpha=self._alpha, @@ -7722,7 +8077,7 @@ def __init__( dimension=self._dimension, stab_sigma=self._stab_sigma, eps=self._eps_norm, - dt=D1_dt, + dt=self.options.D1_dt, a=self._a, Bp=self._Bp, alpha=self._alpha, @@ -7736,7 +8091,7 @@ def __init__( dimension=self._dimension, stab_sigma=self._stab_sigma, eps=self._eps_norm, - dt=D1_dt, + dt=self.options.D1_dt, a=self._a, Bp=self._Bp, alpha=self._alpha, @@ -7749,22 +8104,22 @@ def __init__( # pullback callable fun_pb_1 = TransformedPformComponent( - forceterm_class, fun_basis="physical", out_form="2", comp=0, domain=self.domain + forceterm_class, given_in_basis="physical", out_form="2", comp=0, domain=self.domain ) fun_pb_2 = TransformedPformComponent( - forceterm_class, fun_basis="physical", out_form="2", comp=1, domain=self.domain + forceterm_class, given_in_basis="physical", out_form="2", comp=1, domain=self.domain ) fun_pb_3 = TransformedPformComponent( - forceterm_class, fun_basis="physical", out_form="2", comp=2, domain=self.domain + forceterm_class, given_in_basis="physical", out_form="2", comp=2, domain=self.domain ) fun_electrons_pb_1 = TransformedPformComponent( - forcetermelectrons_class, fun_basis="physical", out_form="2", comp=0, domain=self.domain + forcetermelectrons_class, given_in_basis="physical", out_form="2", comp=0, domain=self.domain ) fun_electrons_pb_2 = TransformedPformComponent( - forcetermelectrons_class, fun_basis="physical", out_form="2", comp=1, domain=self.domain + forcetermelectrons_class, given_in_basis="physical", out_form="2", comp=1, domain=self.domain ) fun_electrons_pb_3 = TransformedPformComponent( - forcetermelectrons_class, fun_basis="physical", out_form="2", comp=2, domain=self.domain + forcetermelectrons_class, given_in_basis="physical", out_form="2", comp=2, domain=self.domain ) if self._lifting: l2_proj = L2Projector(space_id="Hdiv", mass_ops=self._mass_opsv0) @@ -8001,9 +8356,9 @@ def __init__( A11np = self._M2np + self._A11np_notimedependency if self._method_to_solve in ("DirectNPInverse", "InexactNPInverse"): - A11np += self._stab_sigma * np.identity(A11np.shape[0]) + A11np += self._stab_sigma * xp.identity(A11np.shape[0]) self.A22np = ( - self._stab_sigma * np.identity(A11np.shape[0]) + self._stab_sigma * xp.identity(A11np.shape[0]) + self._nu_e * ( self._Dnp.T @ self._M3np @ self._Dnp @@ -8012,7 +8367,7 @@ def __init__( + self._M2Bnp / self._eps_norm ) self._A22prenp = ( - np.identity(self.A22np.shape[0]) * self._stab_sigma + xp.identity(self.A22np.shape[0]) * self._stab_sigma ) # + self._nu_e * (self._Dnp.T @ self._M3np @ self._Dnp) elif self._method_to_solve in ("SparseSolver", "ScipySparse"): A11np += self._stab_sigma * sc.sparse.eye(A11np.shape[0], format="csr") @@ -8045,10 +8400,10 @@ def __init__( A=_A, B=_B, F=_F, - solver_name=solver["type"][0], - tol=solver["tol"], - max_iter=solver["maxiter"], - verbose=solver["verbose"], + solver_name=self.options.solver, + tol=self.options.solver_params.tol, + max_iter=self.options.solver_params.maxiter, + verbose=self.options.solver_params.verbose, pc=None, ) # Allocate memory for call @@ -8062,17 +8417,17 @@ def __init__( F=_Fnp, method_to_solve=self._method_to_solve, preconditioner=self._preconditioner, - spectralanalysis=spectralanalysis, - tol=solver["tol"], - max_iter=solver["maxiter"], - verbose=solver["verbose"], + spectralanalysis=self.options.spectralanalysis, + tol=self.options.solver_params.tol, + max_iter=self.options.solver_params.maxiter, + verbose=self.options.solver_params.verbose, ) def __call__(self, dt): # current variables - unfeec = self.feec_vars[0] - uenfeec = self.feec_vars[1] - phinfeec = self.feec_vars[2] + unfeec = self.variables.u.spline.vector + uenfeec = self.variables.ue.spline.vector + phinfeec = self.variables.phi.spline.vector if self._variant == "GMRES": if self._lifting: @@ -8189,13 +8544,13 @@ def __call__(self, dt): uen = _sol1[1] phin = _sol2 # write new coeffs into self.feec_vars - max_du, max_due, max_dphi = self.feec_vars_update(un, uen, phin) + max_du, max_due, max_dphi = self.update_feec_variables(u=un, ue=uen, phi=phin) elif self._variant == "Uzawa": # Numpy A11np = self._M2np / dt + self._A11np_notimedependency if self._method_to_solve in ("DirectNPInverse", "InexactNPInverse"): - A11np += self._stab_sigma * np.identity(A11np.shape[0]) + A11np += self._stab_sigma * xp.identity(A11np.shape[0]) _A22prenp = self._A22prenp A22np = self.A22np elif self._method_to_solve in ("SparseSolver", "ScipySparse"): @@ -8288,7 +8643,7 @@ def __call__(self, dt): print(f"TwoFluidQuasiNeutralFull is only running on one MPI.") # write new coeffs into self.feec_vars - max_du, max_due, max_dphi = self.feec_vars_update(u_temp, ue_temp, phi_temp) + max_du, max_due, max_dphi = self.update_feec_variables(u=u_temp, ue=ue_temp, phi=phi_temp) if self._info and self._rank == 0: print("Status for TwoFluidQuasiNeutralFull:", info["success"]) diff --git a/src/struphy/propagators/propagators_markers.py b/src/struphy/propagators/propagators_markers.py index fff58d514..0563c9dd1 100644 --- a/src/struphy/propagators/propagators_markers.py +++ b/src/struphy/propagators/propagators_markers.py @@ -1,22 +1,37 @@ "Only particle variables are updated." +import copy +from dataclasses import dataclass +from typing import Callable, Literal, get_args + +import cunumpy as xp +from line_profiler import profile from numpy import array, polynomial, random +from psydac.ddm.mpi import mpi as MPI +from psydac.linalg.basic import LinearOperator from psydac.linalg.block import BlockVector from psydac.linalg.stencil import StencilVector from struphy.feec.mass import WeightedMassOperators from struphy.fields_background.base import MHDequilibrium from struphy.fields_background.equils import set_defaults +from struphy.io.options import ( + OptsKernel, + OptsMPIsort, + OptsVecSpace, + check_option, +) from struphy.io.setup import descend_options_dict +from struphy.models.variables import FEECVariable, PICVariable, SPHVariable from struphy.ode.utils import ButcherTableau from struphy.pic.accumulation import accum_kernels, accum_kernels_gc +from struphy.pic.accumulation.particles_to_grid import AccumulatorVector from struphy.pic.base import Particles from struphy.pic.particles import Particles3D, Particles5D, Particles6D, ParticlesSPH from struphy.pic.pushing import eval_kernels_gc, pusher_kernels, pusher_kernels_gc from struphy.pic.pushing.pusher import Pusher from struphy.polar.basic import PolarVector from struphy.propagators.base import Propagator -from struphy.utils.arrays import xp as np from struphy.utils.pyccel import Pyccelkernel @@ -36,47 +51,60 @@ class PushEta(Propagator): Available algorithms: * Explicit from :class:`~struphy.ode.utils.ButcherTableau` - - Parameters - ---------- - particles : Particles6D | ParticlesSPH - Particles object. - - algo : str - Algorithm for solving the ODE (see options below). - - density_field: StencilVector - Storage for density evaluation at each __call__. """ - @staticmethod - def options(default=False): - dct = {} - dct["algo"] = ["rk4", "forward_euler", "heun2", "rk2", "heun3"] - if default: - dct = descend_options_dict(dct, []) - return dct - - def __init__( - self, - particles: Particles6D | ParticlesSPH, - *, - algo: str = options(default=True)["algo"], - density_field: StencilVector | None = None, - ): - # base class constructor call - super().__init__(particles) - + class Variables: + def __init__(self): + self._var: PICVariable | SPHVariable = None + + @property + def var(self) -> PICVariable | SPHVariable: + return self._var + + @var.setter + def var(self, new): + assert isinstance(new, PICVariable | SPHVariable) + self._var = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + butcher: ButcherTableau = None + + def __post_init__(self): + # defaults + if self.butcher is None: + self.butcher = ButcherTableau() + + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): # get kernel kernel = Pyccelkernel(pusher_kernels.push_eta_stage) # define algorithm - butcher = ButcherTableau(algo) + butcher = self.options.butcher # temp fix due to refactoring of ButcherTableau: - from struphy.utils.arrays import xp as np + import cunumpy as xp - butcher._a = np.diag(butcher.a, k=-1) - butcher._a = np.array(list(butcher.a) + [0.0]) + butcher._a = xp.diag(butcher.a, k=-1) + butcher._a = xp.array(list(butcher.a) + [0.0]) args_kernel = ( butcher.a, @@ -85,7 +113,7 @@ def __init__( ) self._pusher = Pusher( - particles, + self.variables.var.particles, kernel, args_kernel, self.domain.args_domain, @@ -94,28 +122,13 @@ def __init__( mpi_sort="each", ) - self._eval_density = False - if density_field is not None: - self._eval_density = True - self._density_field = density_field - + @profile def __call__(self, dt): self._pusher(dt) # update_weights - if self.particles[0].control_variate: - self.particles[0].update_weights() - - if self._eval_density: - eval_density = lambda eta1, eta2, eta3: self.particles[0].eval_density( - eta1, - eta2, - eta3, - h1=0.1, - h2=0.1, - h3=0.1, - ) - self.derham.P["3"](eval_density, out=self._density_field) + if self.variables.var.particles.control_variate: + self.variables.var.particles.update_weights() class PushVxB(Propagator): @@ -123,9 +136,9 @@ class PushVxB(Propagator): .. math:: - \frac{\textnormal d \mathbf v_p(t)}{\textnormal d t} = \kappa \, \mathbf v_p(t) \times (\mathbf B + \mathbf B_{\text{add}}) \,, + \frac{\textnormal d \mathbf v_p(t)}{\textnormal d t} = \frac{1}{\varepsilon} \, \mathbf v_p(t) \times (\mathbf B + \mathbf B_{\text{add}}) \,, - where :math:`\kappa \in \mathbb R` is a constant scaling factor, and for rotation vector :math:`\mathbf B` and optional, additional fixed rotation + where :math:`\varepsilon = 1/(\hat\Omega_c \hat t)` is a constant scaling factor, and for rotation vector :math:`\mathbf B` and optional, additional fixed rotation vector :math:`\mathbf B_{\text{add}}`, both given as a 2-form: .. math:: @@ -135,45 +148,80 @@ class PushVxB(Propagator): Available algorithms: ``analytic``, ``implicit``. """ - @staticmethod - def options(default=False): - dct = {} - dct["algo"] = ["analytic", "implicit"] - if default: - dct = descend_options_dict(dct, []) - return dct + class Variables: + def __init__(self): + self._ions: PICVariable | SPHVariable = None + + @property + def ions(self) -> PICVariable | SPHVariable: + return self._ions + + @ions.setter + def ions(self, new): + assert isinstance(new, PICVariable | SPHVariable) + assert new.space in ("Particles6D", "DeltaFParticles6D", "ParticlesSPH") + self._ions = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # specific literals + OptsAlgo = Literal["analytic", "implicit"] + # propagator options + algo: OptsAlgo = "analytic" + b2_var: FEECVariable = None + + def __post_init__(self): + # checks + check_option(self.algo, self.OptsAlgo) + + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + # scaling factor + self._epsilon = self.variables.ions.species.equation_params.epsilon + assert self.derham is not None, f"{self.__class__.__name__} needs a Derham object." - def __init__( - self, - particles: Particles6D, - *, - algo: str = options(default=True)["algo"], - kappa: float = 1.0, - b2: BlockVector | PolarVector, - b2_add: BlockVector | PolarVector = None, - ): # TODO: treat PolarVector as well, but polar splines are being reworked at the moment - assert b2.space == self.derham.Vh["2"] - if b2_add is not None: - assert b2_add.space == self.derham.Vh["2"] + if self.projected_equil is not None: + self._b2 = self.projected_equil.b2 + assert self._b2.space == self.derham.Vh["2"] + else: + self._b2 = self.derham.Vh["2"].zeros() - # base class constructor call - super().__init__(particles) + if self.options.b2_var is None: + self._b2_var = None + else: + assert self.options.b2_var.spline.vector.space == self.derham.Vh["2"] + self._b2_var = self.options.b2_var.spline.vector - # parameters that need to be exposed - self._kappa = kappa - self._b2 = b2 - self._b2_add = b2_add + # allocate dummy vectors to avoid temporary array allocations self._tmp = self.derham.Vh["2"].zeros() self._b_full = self.derham.Vh["2"].zeros() # define pusher kernel - if algo == "analytic": + if self.options.algo == "analytic": kernel = Pyccelkernel(pusher_kernels.push_vxb_analytic) - elif algo == "implicit": + elif self.options.algo == "implicit": kernel = Pyccelkernel(pusher_kernels.push_vxb_implicit) else: - raise ValueError(f"{algo = } not supported.") + raise ValueError(f"{self.options.algo = } not supported.") # instantiate Pusher args_kernel = ( @@ -184,7 +232,7 @@ def __init__( ) self._pusher = Pusher( - particles, + self.variables.ions.particles, kernel, args_kernel, self.domain.args_domain, @@ -192,24 +240,26 @@ def __init__( ) # transposed extraction operator PolarVector --> BlockVector (identity map in case of no polar splines) - self._E2T = self.derham.extraction_ops["2"].transpose() + self._E2T: LinearOperator = self.derham.extraction_ops["2"].transpose() + @profile def __call__(self, dt): # sum up total magnetic field tmp = self._b2.copy(out=self._tmp) - if self._b2_add is not None: - tmp += self._b2_add + if self._b2_var is not None: + tmp += self._b2_var # extract coefficients to tensor product space - b_full = self._E2T.dot(tmp, out=self._b_full) + b_full: BlockVector = self._E2T.dot(tmp, out=self._b_full) b_full.update_ghost_regions() + b_full /= self._epsilon # call pusher kernel - self._pusher(self._kappa * dt) + self._pusher(dt) # update_weights - if self.particles[0].control_variate: - self.particles[0].update_weights() + if self.variables.ions.particles.control_variate: + self.variables.ions.particles.update_weights() class PushVinEfield(Propagator): @@ -217,57 +267,107 @@ class PushVinEfield(Propagator): .. math:: - \frac{\text{d} \mathbf{v}_p}{\text{d} t} = \kappa \, \mathbf{E}(\mathbf{x}_p) \,, + \frac{\text{d} \mathbf{v}_p}{\text{d} t} = \frac{1}{\varepsilon} \, \mathbf{E}(\mathbf{x}_p) \,, - where :math:`\kappa \in \mathbb R` is a constant and in logical coordinates, given by :math:`\mathbf x = F(\boldsymbol \eta)`: + where :math:`\varepsilon \in \mathbb R` is a constant. In logical coordinates, given by :math:`\mathbf x = F(\boldsymbol \eta)`: .. math:: - \frac{\text{d} \mathbf{v}_p}{\text{d} t} = \kappa \, DF^{-\top} \hat{\mathbf E}^1(\boldsymbol \eta_p) \,, + \frac{\text{d} \mathbf{v}_p}{\text{d} t} = \frac{1}{\varepsilon} \, DF^{-\top} \hat{\mathbf E}^1(\boldsymbol \eta_p) \,, - which is solved analytically. + which is solved analytically. :math:`\mathbf E` can optionally be defined + through a potential, :math:`\mathbf E = - \nabla \phi`. """ - @staticmethod - def options(): - pass - - def __init__( - self, - particles: Particles6D, - *, - e_field: BlockVector | PolarVector, - kappa: float = 1.0, - ): - super().__init__(particles) - - self.kappa = kappa + class Variables: + def __init__(self): + self._var: PICVariable | SPHVariable = None + + @property + def var(self) -> PICVariable | SPHVariable: + return self._var + + @var.setter + def var(self, new): + assert isinstance(new, PICVariable | SPHVariable) + assert new.space in ("Particles6D", "DeltaFParticles6D", "ParticlesSPH") + self._var = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # propagator options + e_field: FEECVariable | tuple[Callable] = None + phi: FEECVariable | Callable = None + + def __post_init__(self): + # checks + if self.e_field is not None: + assert isinstance(self.e_field, tuple[Callable]) or self.e_field.space == "Hcurl" + else: + if self.phi is not None: + assert isinstance(self.phi, Callable) or self.phi.space == "H1" + + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + # scaling factor + self._epsilon = self.variables.var.species.equation_params.epsilon + + self._e_field = None + + if self.options.e_field is not None: + if isinstance(self.options.e_field, tuple[Callable]): + self._e_field = self.derham.P["1"](self.options.e_field) + else: + self._e_field = self.options.e_field.spline.vector - assert isinstance(e_field, (BlockVector, PolarVector)) - self._e_field = e_field + if self.options.phi is not None: + if isinstance(self.options.phi, Callable): + _phi = self.derham.P["0"](self.options.phi) + else: + _phi = self.options.phi.spline.vector + self._e_field = self.derham.grad.dot(_phi) + self._e_field.update_ghost_regions() # very important, we will move it inside grad + self._e_field *= -1.0 - # instantiate Pusher - args_kernel = ( - self.derham.args_derham, - self._e_field[0]._data, - self._e_field[1]._data, - self._e_field[2]._data, - self.kappa, - ) + if self._e_field is not None: + # instantiate Pusher + args_kernel = ( + self.derham.args_derham, + self._e_field[0]._data, + self._e_field[1]._data, + self._e_field[2]._data, + 1.0 / self._epsilon, + ) - self._pusher = Pusher( - particles, - Pyccelkernel(pusher_kernels.push_v_with_efield), - args_kernel, - self.domain.args_domain, - alpha_in_kernel=1.0, - ) + self._pusher = Pusher( + self.variables.var.particles, + Pyccelkernel(pusher_kernels.push_v_with_efield), + args_kernel, + self.domain.args_domain, + alpha_in_kernel=1.0, + ) def __call__(self, dt): - """ - TODO - """ - self._pusher(dt) + if self._e_field is not None: + self._pusher(dt) class PushEtaPC(Propagator): @@ -298,85 +398,106 @@ class PushEtaPC(Propagator): * ``heun3`` (3rd order) """ - @staticmethod - def options(default=False): - dct = {} - dct["use_perp_model"] = [True, False] - - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - particles: Particles, - *, - u: BlockVector | PolarVector, - use_perp_model: bool = options(default=True)["use_perp_model"], - u_space: str, - ): - super().__init__(particles) - - assert isinstance(u, (BlockVector, PolarVector)) + class Variables: + def __init__(self): + self._var: PICVariable | SPHVariable = None + + @property + def var(self) -> PICVariable | SPHVariable: + return self._var + + @var.setter + def var(self, new): + assert isinstance(new, PICVariable | SPHVariable) + self._var = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + butcher: ButcherTableau = None + use_perp_model: bool = True + u_tilde: FEECVariable = None + u_space: OptsVecSpace = "Hdiv" + + def __post_init__(self): + # checks + check_option(self.u_space, OptsVecSpace) + assert isinstance(self.u_tilde, FEECVariable) + + # defaults + if self.butcher is None: + self.butcher = ButcherTableau() + + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + self._u_tilde = self.options.u_tilde.spline.vector + + # get kernell: + if self.options.u_space == "Hcurl": + kernel = Pyccelkernel(pusher_kernels.push_pc_eta_stage_Hcurl) + elif self.options.u_space == "Hdiv": + kernel = Pyccelkernel(pusher_kernels.push_pc_eta_stage_Hdiv) + elif self.options.u_space == "H1vec": + kernel = Pyccelkernel(pusher_kernels.push_pc_eta_stage_H1vec) + else: + raise ValueError( + f'{self.options.u_space = } not valid, choose from "Hcurl", "Hdiv" or "H1vec.', + ) - self._u = u + # define algorithm + butcher = self.options.butcher + # temp fix due to refactoring of ButcherTableau: + import cunumpy as xp - # call Pusher class - if use_perp_model: - if u_space == "Hcurl": - kernel = Pyccelkernel(pusher_kernels.push_pc_eta_rk4_Hcurl) - elif u_space == "Hdiv": - kernel = Pyccelkernel(pusher_kernels.push_pc_eta_rk4_Hdiv) - elif u_space == "H1vec": - kernel = Pyccelkernel(pusher_kernels.push_pc_eta_rk4_H1vec) - else: - raise ValueError( - f'{u_space = } not valid, choose from "Hcurl", "Hdiv" or "H1vec.', - ) - else: - if u_space == "Hcurl": - kernel = Pyccelkernel(pusher_kernels.push_pc_eta_rk4_Hcurl_full) - elif u_space == "Hdiv": - kernel = Pyccelkernel(pusher_kernels.push_pc_eta_rk4_Hdiv_full) - elif u_space == "H1vec": - kernel = Pyccelkernel(pusher_kernels.push_pc_eta_rk4_H1vec_full) - else: - raise ValueError( - f'{u_space = } not valid, choose from "Hcurl", "Hdiv" or "H1vec.', - ) + butcher._a = xp.diag(butcher.a, k=-1) + butcher._a = xp.array(list(butcher.a) + [0.0]) args_kernel = ( self.derham.args_derham, - self._u[0]._data, - self._u[1]._data, - self._u[2]._data, + self._u_tilde[0]._data, + self._u_tilde[1]._data, + self._u_tilde[2]._data, + self.options.use_perp_model, + butcher.a, + butcher.b, + butcher.c, ) self._pusher = Pusher( - particles, + self.variables.var.particles, kernel, args_kernel, self.domain.args_domain, alpha_in_kernel=1.0, - n_stages=4, + n_stages=butcher.n_stages, mpi_sort="each", ) def __call__(self, dt): - # check if ghost regions are synchronized - if not self._u[0].ghost_regions_in_sync: - self._u[0].update_ghost_regions() - if not self._u[1].ghost_regions_in_sync: - self._u[1].update_ghost_regions() - if not self._u[2].ghost_regions_in_sync: - self._u[2].update_ghost_regions() + self._u_tilde.update_ghost_regions() self._pusher(dt) # update_weights - if self.particles[0].control_variate: - self.particles[0].update_weights() + if self.variables.var.particles.control_variate: + self.variables.var.particles.update_weights() class PushGuidingCenterBxEstar(Propagator): @@ -409,41 +530,74 @@ class PushGuidingCenterBxEstar(Propagator): * :func:`~struphy.pic.pushing.pusher_kernels_gc.push_gc_bxEstar_discrete_gradient_2nd_order` """ - @staticmethod - def options(default=False): - dct = {} - dct["algo"] = { - "method": [ - "discrete_gradient_2nd_order", - "discrete_gradient_1st_order", - "discrete_gradient_1st_order_newton", - "rk4", - "forward_euler", - "heun2", - "rk2", - "heun3", - ], - "maxiter": 20, - "tol": 1e-7, - "mpi_sort": "each", - "verbose": False, - } - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - particles: Particles5D, - *, - phi: StencilVector = None, - evaluate_e_field: bool = False, - b_tilde: BlockVector = None, - epsilon: float = 1.0, - algo: dict = options(default=True)["algo"], - ): - super().__init__(particles) + class Variables: + def __init__(self): + self._ions: PICVariable = None + + @property + def ions(self) -> PICVariable: + return self._ions + + @ions.setter + def ions(self, new): + assert isinstance(new, PICVariable) + assert new.space == "Particles5D" + self._ions = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # specific literals + OptsAlgo = Literal[ + "discrete_gradient_2nd_order", + "discrete_gradient_1st_order", + "discrete_gradient_1st_order_newton", + "explicit", + ] + # propagator options + phi: FEECVariable = None + evaluate_e_field: bool = False + b_tilde: FEECVariable = None + algo: OptsAlgo = "discrete_gradient_1st_order" + butcher: ButcherTableau = None + maxiter: int = 20 + tol: float = 1e-7 + mpi_sort: OptsMPIsort = "each" + verbose: bool = False + + def __post_init__(self): + # checks + check_option(self.algo, self.OptsAlgo) + check_option(self.mpi_sort, OptsMPIsort) + + # defaults + if self.phi is None: + self.phi = FEECVariable(space="H1") + + if self.algo == "explicit" and self.butcher is None: + self.butcher = ButcherTableau() + + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + # scaling factor + self._epsilon = self.variables.ions.species.equation_params.epsilon # magnetic equilibrium field unit_b1 = self.projected_equil.unit_b1 @@ -452,14 +606,13 @@ def __init__( curl_unit_b_dot_b0 = self.projected_equil.curl_unit_b_dot_b0 # magnetic perturbation - self._b_tilde = b_tilde - if self._b_tilde is not None: + if self.options.b_tilde is not None: self._B_dot_b = self.derham.Vh["0"].zeros() self._grad_b_full = self.derham.Vh["1"].zeros() self._PB = getattr(self.basis_ops, "PB") - B_dot_b = self._PB.dot(self._b_tilde, out=self._B_dot_b) + B_dot_b = self._PB.dot(self.options.b_tilde.spline.vector, out=self._B_dot_b) B_dot_b.update_ghost_regions() grad_b_full = self.derham.grad.dot(B_dot_b, out=self._grad_b_full) @@ -472,20 +625,19 @@ def __init__( self._B_dot_b = self._absB0 # allocate electric field - if phi is None: - phi = self.derham.Vh["0"].zeros() - self._evaluate_e_field = False - self._phi = phi - self._evaluate_e_field = evaluate_e_field + self.options.phi.allocate(self.derham, self.domain) + self._phi = self.options.phi.spline.vector + self._evaluate_e_field = self.options.evaluate_e_field self._e_field = self.derham.Vh["1"].zeros() - self._epsilon = epsilon # choose method - if "discrete_gradient" in algo["method"]: + particles = self.variables.ions.particles + + if "discrete_gradient" in self.options.algo: # place for storing data during iteration first_free_idx = particles.args_markers.first_free_idx - if "1st_order" in algo["method"]: + if "1st_order" in self.options.algo: # init kernels self.add_init_kernel( eval_kernels_gc.driftkinetic_hamiltonian, @@ -524,7 +676,7 @@ def __init__( ), ) - if "newton" in algo["method"]: + if "newton" in self.options.algo: # eval kernels self.add_eval_kernel( eval_kernels_gc.driftkinetic_hamiltonian, @@ -640,7 +792,7 @@ def __init__( self._evaluate_e_field, ) - elif "2nd_order" in algo["method"]: + elif "2nd_order" in self.options.algo: # init kernels (evaluate at eta^n and save) self.add_init_kernel( eval_kernels_gc.driftkinetic_hamiltonian, @@ -691,11 +843,6 @@ def __init__( self._evaluate_e_field, ) - else: - raise NotImplementedError( - f"Chosen method {algo['method']} is not implemented.", - ) - # Pusher instance self._pusher = Pusher( particles, @@ -705,19 +852,22 @@ def __init__( alpha_in_kernel=alpha_in_kernel, init_kernels=self.init_kernels, eval_kernels=self.eval_kernels, - maxiter=algo["maxiter"], - tol=algo["tol"], - mpi_sort=algo["mpi_sort"], - verbose=algo["verbose"], + maxiter=self.options.maxiter, + tol=self.options.tol, + mpi_sort=self.options.mpi_sort, + verbose=self.options.verbose, ) else: - butcher = ButcherTableau(algo["method"]) + if self.options.butcher is None: + butcher = ButcherTableau() + else: + butcher = self.options.butcher # temp fix due to refactoring of ButcherTableau: - from struphy.utils.arrays import xp as np + import cunumpy as xp - butcher._a = np.diag(butcher.a, k=-1) - butcher._a = np.array(list(butcher.a) + [0.0]) + butcher._a = xp.diag(butcher.a, k=-1) + butcher._a = xp.array(list(butcher.a) + [0.0]) kernel = Pyccelkernel(pusher_kernels_gc.push_gc_bxEstar_explicit_multistage) @@ -748,10 +898,11 @@ def __init__( self.domain.args_domain, alpha_in_kernel=1.0, n_stages=butcher.n_stages, - mpi_sort=algo["mpi_sort"], - verbose=algo["verbose"], + mpi_sort=self.options.mpi_sort, + verbose=self.options.verbose, ) + @profile def __call__(self, dt): # electric field # TODO: add out to __neg__ of StencilVector @@ -760,8 +911,8 @@ def __call__(self, dt): e_field.update_ghost_regions() # magnetic perturbation - if self._b_tilde is not None: - B_dot_b = self._PB.dot(self._b_tilde, out=self._B_dot_b) + if self.options.b_tilde is not None: + B_dot_b = self._PB.dot(self.options.b_tilde.spline.vector, out=self._B_dot_b) B_dot_b.update_ghost_regions() grad_b_full = self.derham.grad.dot(B_dot_b, out=self._grad_b_full) @@ -774,8 +925,8 @@ def __call__(self, dt): self._pusher(dt) # update_weights - if self.particles[0].control_variate: - self.particles[0].update_weights() + if self.variables.ions.species.weights_params.control_variate: + self.variables.ions.particles.update_weights() class PushGuidingCenterParallel(Propagator): @@ -820,43 +971,74 @@ class PushGuidingCenterParallel(Propagator): * :func:`~struphy.pic.pushing.pusher_kernels_gc.push_gc_Bstar_discrete_gradient_2nd_order` """ - @staticmethod - def options(default=False): - dct = {} - dct["algo"] = { - "method": [ - "discrete_gradient_2nd_order", - "discrete_gradient_1st_order", - "discrete_gradient_1st_order_newton", - "rk4", - "forward_euler", - "heun2", - "rk2", - "heun3", - ], - "maxiter": 20, - "tol": 1e-7, - "mpi_sort": "each", - "verbose": False, - } - if default: - dct = descend_options_dict(dct, []) - - return dct - - def __init__( - self, - particles: Particles5D, - *, - phi: StencilVector = None, - evaluate_e_field: bool = False, - b_tilde: BlockVector = None, - epsilon: float = 1.0, - algo: dict = options(default=True)["algo"], - ): - super().__init__(particles) - - self._epsilon = epsilon + class Variables: + def __init__(self): + self._ions: PICVariable = None + + @property + def ions(self) -> PICVariable: + return self._ions + + @ions.setter + def ions(self, new): + assert isinstance(new, PICVariable) + assert new.space == "Particles5D" + self._ions = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # specific literals + OptsAlgo = Literal[ + "discrete_gradient_2nd_order", + "discrete_gradient_1st_order", + "discrete_gradient_1st_order_newton", + "explicit", + ] + # propagator options + phi: FEECVariable = None + evaluate_e_field: bool = False + b_tilde: FEECVariable = None + algo: OptsAlgo = "discrete_gradient_1st_order" + butcher: ButcherTableau = None + maxiter: int = 20 + tol: float = 1e-7 + mpi_sort: OptsMPIsort = "each" + verbose: bool = False + + def __post_init__(self): + # checks + check_option(self.algo, self.OptsAlgo) + check_option(self.mpi_sort, OptsMPIsort) + + # defaults + if self.phi is None: + self.phi = FEECVariable(space="H1") + + if self.algo == "explicit" and self.butcher is None: + self.butcher = ButcherTableau() + + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + # scaling factor + self._epsilon = self.variables.ions.species.equation_params.epsilon # magnetic equilibrium field self._gradB1 = self.projected_equil.gradB1 @@ -866,14 +1048,13 @@ def __init__( curl_unit_b_dot_b0 = self.projected_equil.curl_unit_b_dot_b0 # magnetic perturbation - self._b_tilde = b_tilde - if self._b_tilde is not None: + if self.options.b_tilde is not None: self._B_dot_b = self.derham.Vh["0"].zeros() self._grad_b_full = self.derham.Vh["1"].zeros() self._PB = getattr(self.basis_ops, "PB") - B_dot_b = self._PB.dot(self._b_tilde, out=self._B_dot_b) + B_dot_b = self._PB.dot(self.options.b_tilde.spline.vector, out=self._B_dot_b) B_dot_b.update_ghost_regions() grad_b_full = self.derham.grad.dot(B_dot_b, out=self._grad_b_full) @@ -886,19 +1067,19 @@ def __init__( self._B_dot_b = self._absB0 # allocate electric field - if phi is None: - phi = self.derham.Vh["0"].zeros() - self._phi = phi - self._evaluate_e_field = evaluate_e_field + self.options.phi.allocate(self.derham, domain=self.domain) + self._phi = self.options.phi.spline.vector + self._evaluate_e_field = self.options.evaluate_e_field self._e_field = self.derham.Vh["1"].zeros() - self._epsilon = epsilon # choose method - if "discrete_gradient" in algo["method"]: + particles = self.variables.ions.particles + + if "discrete_gradient" in self.options.algo: # place for storing data during iteration first_free_idx = particles.args_markers.first_free_idx - if "1st_order" in algo["method"]: + if "1st_order" in self.options.algo: # init kernels self.add_init_kernel( eval_kernels_gc.driftkinetic_hamiltonian, @@ -941,7 +1122,7 @@ def __init__( ), ) - if "newton" in algo["method"]: + if "newton" in self.options.algo: # eval kernels self.add_eval_kernel( eval_kernels_gc.driftkinetic_hamiltonian, @@ -1056,7 +1237,7 @@ def __init__( self._evaluate_e_field, ) - elif "2nd_order" in algo["method"]: + elif "2nd_order" in self.options.algo: # init kernels (evaluate at eta^n and save) self.add_init_kernel( eval_kernels_gc.driftkinetic_hamiltonian, @@ -1110,11 +1291,6 @@ def __init__( self._evaluate_e_field, ) - else: - raise NotImplementedError( - f"Chosen method {algo['method']} is not implemented.", - ) - # Pusher instance self._pusher = Pusher( particles, @@ -1124,19 +1300,22 @@ def __init__( alpha_in_kernel=alpha_in_kernel, init_kernels=self.init_kernels, eval_kernels=self.eval_kernels, - maxiter=algo["maxiter"], - tol=algo["tol"], - mpi_sort=algo["mpi_sort"], - verbose=algo["verbose"], + maxiter=self.options.maxiter, + tol=self.options.tol, + mpi_sort=self.options.mpi_sort, + verbose=self.options.verbose, ) else: - butcher = ButcherTableau(algo["method"]) + if self.options.butcher is None: + butcher = ButcherTableau() + else: + butcher = self.options.butcher # temp fix due to refactoring of ButcherTableau: - from struphy.utils.arrays import xp as np + import cunumpy as xp - butcher._a = np.diag(butcher.a, k=-1) - butcher._a = np.array(list(butcher.a) + [0.0]) + butcher._a = xp.diag(butcher.a, k=-1) + butcher._a = xp.array(list(butcher.a) + [0.0]) kernel = Pyccelkernel(pusher_kernels_gc.push_gc_Bstar_explicit_multistage) @@ -1170,10 +1349,11 @@ def __init__( self.domain.args_domain, alpha_in_kernel=1.0, n_stages=butcher.n_stages, - mpi_sort=algo["mpi_sort"], - verbose=algo["verbose"], + mpi_sort=self.options.mpi_sort, + verbose=self.options.verbose, ) + @profile def __call__(self, dt): # electric field # TODO: add out to __neg__ of StencilVector @@ -1182,8 +1362,8 @@ def __call__(self, dt): e_field.update_ghost_regions() # magnetic perturbation - if self._b_tilde is not None: - B_dot_b = self._PB.dot(self._b_tilde, out=self._B_dot_b) + if self.options.b_tilde is not None: + B_dot_b = self._PB.dot(self.options.b_tilde.spline.vector, out=self._B_dot_b) B_dot_b.update_ghost_regions() grad_b_full = self.derham.grad.dot(B_dot_b, out=self._grad_b_full) @@ -1196,8 +1376,8 @@ def __call__(self, dt): self._pusher(dt) # update_weights - if self.particles[0].control_variate: - self.particles[0].update_weights() + if self.variables.ions.species.weights_params.control_variate: + self.variables.ions.particles.update_weights() class PushDeterministicDiffusion(Propagator): @@ -1221,39 +1401,65 @@ class PushDeterministicDiffusion(Propagator): * Explicit from :class:`~struphy.ode.utils.ButcherTableau` """ - @staticmethod - def options(default=False): - dct = {} - dct["algo"] = ["rk4", "forward_euler", "heun2", "rk2", "heun3"] - dct["diffusion_coefficient"] = 1.0 - if default: - dct = descend_options_dict(dct, []) - return dct - - def __init__( - self, - particles: Particles3D, - *, - algo: str = options(default=True)["algo"], - bc_type: list = ["periodic", "periodic", "periodic"], - diffusion_coefficient: float = options()["diffusion_coefficient"], - ): - from struphy.pic.accumulation.particles_to_grid import AccumulatorVector - - super().__init__(particles) - - self._bc_type = bc_type - self._diffusion = diffusion_coefficient + class Variables: + def __init__(self): + self._var: PICVariable = None + + @property + def var(self) -> PICVariable: + return self._var + + @var.setter + def var(self, new): + assert isinstance(new, PICVariable) + assert new.space == "Particles3D" + self._var = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + butcher: ButcherTableau = None + bc_type: tuple = ("periodic", "periodic", "periodic") + diff_coeff: float = 1.0 + + def __post_init__(self): + # defaults + if self.butcher is None: + self.butcher = ButcherTableau() + + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + self._bc_type = self.options.bc_type + self._diffusion = self.options.diff_coeff self._tmp = self.derham.Vh["1"].zeros() # choose algorithm - self._butcher = ButcherTableau(algo) + self._butcher = self.options.butcher # temp fix due to refactoring of ButcherTableau: - from struphy.utils.arrays import xp as np + import cunumpy as xp - self._butcher._a = np.diag(self._butcher.a, k=-1) - self._butcher._a = np.array(list(self._butcher.a) + [0.0]) + self._butcher._a = xp.diag(self._butcher.a, k=-1) + self._butcher._a = xp.array(list(self._butcher.a) + [0.0]) + + particles = self.variables.var.particles self._u_on_grid = AccumulatorVector( particles, @@ -1290,9 +1496,10 @@ def __call__(self, dt): """ TODO """ + particles = self.variables.var.particles # accumulate - self._u_on_grid(self.particles[0].vdim) + self._u_on_grid() # take gradient pi_u = self._u_on_grid.vectors[0] @@ -1303,8 +1510,8 @@ def __call__(self, dt): self._pusher(dt) # update_weights - if self.particles[0].control_variate: - self.particles[0].update_weights() + if particles.control_variate: + particles.update_weights() class PushRandomDiffusion(Propagator): @@ -1327,36 +1534,64 @@ class PushRandomDiffusion(Propagator): * ``forward_euler`` (1st order) """ - @staticmethod - def options(default=False): - dct = {} - dct["algo"] = ["forward_euler"] - dct["diffusion_coefficient"] = 1.0 - if default: - dct = descend_options_dict(dct, []) - return dct - - def __init__( - self, - particles: Particles3D, - algo: str = options(default=True)["algo"], - bc_type: list = ["periodic", "periodic", "periodic"], - diffusion_coefficient: float = options()["diffusion_coefficient"], - ): - super().__init__(particles) - - self._bc_type = bc_type - self._diffusion = diffusion_coefficient - - self._noise = array(self.particles[0].markers[:, :3]) - - # choose algorithm - self._butcher = ButcherTableau("forward_euler") + class Variables: + def __init__(self): + self._var: PICVariable = None + + @property + def var(self) -> PICVariable: + return self._var + + @var.setter + def var(self, new): + assert isinstance(new, PICVariable) + assert new.space == "Particles3D" + self._var = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + butcher: ButcherTableau = None + bc_type: tuple = ("periodic", "periodic", "periodic") + diff_coeff: float = 1.0 + + def __post_init__(self): + # defaults + if self.butcher is None: + self.butcher = ButcherTableau() + + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + self._bc_type = self.options.bc_type + self._diffusion = self.options.diff_coeff + + particles = self.variables.var.particles + + self._noise = array(particles.markers[:, :3]) + + self._butcher = self.options.butcher # temp fix due to refactoring of ButcherTableau: - from struphy.utils.arrays import xp as np + import cunumpy as xp - self._butcher._a = np.diag(self._butcher.a, k=-1) - self._butcher._a = np.array(list(self._butcher.a) + [0.0]) + self._butcher._a = xp.diag(self._butcher.a, k=-1) + self._butcher._a = xp.array(list(self._butcher.a) + [0.0]) # instantiate Pusher args_kernel = ( @@ -1386,18 +1621,20 @@ def __call__(self, dt): TODO """ + particles = self.variables.var.particles + self._noise[:] = random.multivariate_normal( self._mean, self._cov, - len(self.particles[0].markers), + len(particles.markers), ) # push markers self._pusher(dt) # update_weights - if self.particles[0].control_variate: - self.particles[0].update_weights() + if particles.control_variate: + particles.update_weights() class PushVinSPHpressure(Propagator): @@ -1419,15 +1656,155 @@ class PushVinSPHpressure(Propagator): * Explicit from :class:`~struphy.ode.utils.ButcherTableau` """ + class Variables: + def __init__(self): + self._fluid: SPHVariable = None + + @property + def fluid(self) -> SPHVariable: + return self._fluid + + @fluid.setter + def fluid(self, new): + assert isinstance(new, SPHVariable) + assert new.space == "ParticlesSPH" + self._fluid = new + + def __init__(self): + self.variables = self.Variables() + + @dataclass + class Options: + # specific literals + OptsAlgo = Literal["forward_euler"] + OptsThermo = Literal["isothermal", "polytropic"] + # propagator options + kernel_type: OptsKernel = "gaussian_2d" + kernel_width: tuple = None + algo: OptsAlgo = "forward_euler" + gravity: tuple = (0.0, 0.0, 0.0) + thermodynamics: OptsThermo = "isothermal" + + def __post_init__(self): + # checks + check_option(self.kernel_type, OptsKernel) + check_option(self.algo, self.OptsAlgo) + check_option(self.thermodynamics, self.OptsThermo) + + @property + def options(self) -> Options: + if not hasattr(self, "_options"): + self._options = self.Options() + return self._options + + @options.setter + def options(self, new): + assert isinstance(new, self.Options) + if MPI.COMM_WORLD.Get_rank() == 0: + print(f"\nNew options for propagator '{self.__class__.__name__}':") + for k, v in new.__dict__.items(): + print(f" {k}: {v}") + self._options = new + + @profile + def allocate(self): + # init kernel for evaluating density etc. before each time step. + init_kernel = eval_kernels_gc.sph_pressure_coeffs + + particles = self.variables.fluid.particles + + first_free_idx = particles.args_markers.first_free_idx + comps = (0, 1, 2) + + boxes = particles.sorting_boxes.boxes + neighbours = particles.sorting_boxes.neighbours + holes = particles.holes + periodic = [bci == "periodic" for bci in particles.bc] + kernel_nr = particles.ker_dct()[self.options.kernel_type] + + if self.options.kernel_width is None: + self.options.kernel_width = tuple([1 / ni for ni in particles.boxes_per_dim]) + else: + assert all([hi <= 1 / ni for hi, ni in zip(self.options.kernel_width, particles.boxes_per_dim)]) + + # init kernel + args_init = ( + boxes, + neighbours, + holes, + *periodic, + kernel_nr, + *self.options.kernel_width, + ) + + self.add_init_kernel( + init_kernel, + first_free_idx, + comps, + args_init, + ) + + # pusher kernel + if self.options.thermodynamics == "isothermal": + kernel = Pyccelkernel(pusher_kernels.push_v_sph_pressure) + elif self.options.thermodynamics == "polytropic": + kernel = Pyccelkernel(pusher_kernels.push_v_sph_pressure_ideal_gas) + + gravity = xp.array(self.options.gravity, dtype=float) + + args_kernel = ( + boxes, + neighbours, + holes, + *periodic, + kernel_nr, + *self.options.kernel_width, + gravity, + ) + + # the Pusher class wraps around all kernels + self._pusher = Pusher( + particles, + kernel, + args_kernel, + self.domain.args_domain, + alpha_in_kernel=0.0, + init_kernels=self.init_kernels, + ) + + @profile + def __call__(self, dt): + self.variables.fluid.particles.put_particles_in_boxes() + self._pusher(dt) + + +class PushVinViscousPotential(Propagator): + r"""For each marker :math:`p`, solves + + .. math:: + + \frac{\textnormal d \mathbf v_p(t)}{\textnormal d t} = \kappa_p \sum_{i=1}^N w_i \left( \frac{1}{\rho^{N,h}(\boldsymbol \eta_p)} + \frac{1}{\rho^{N,h}(\boldsymbol \eta_i)} \right) DF^{-\top}\nabla W_h(\boldsymbol \eta_p - \boldsymbol \eta_i) \,, + + where :math:`DF^{-\top}` denotes the inverse transpose Jacobian, and with the smoothed density + + .. math:: + + \rho^{N,h}(\boldsymbol \eta) = \frac 1N \sum_{j=1}^N w_j \, W_h(\boldsymbol \eta - \boldsymbol \eta_j)\,, + + where :math:`W_h(\boldsymbol \eta)` is a smoothing kernel from :mod:`~struphy.pic.sph_smoothing_kernels`. + Time stepping: + + * Explicit from :class:`~struphy.ode.utils.ButcherTableau` + """ + @staticmethod def options(default=False): dct = {} dct["kernel_type"] = list(Particles.ker_dct()) + dct["kernel_width"] = None dct["algo"] = [ "forward_euler", ] # "heun2", "rk2", "heun3", "rk4"] - dct["gravity"] = (0.0, 0.0, 0.0) - dct["thermodynamics"] = ["isothermal", "polytropic"] if default: dct = descend_options_dict(dct, []) return dct @@ -1439,18 +1816,24 @@ def __init__( kernel_type: str = "gaussian_2d", kernel_width: tuple = None, algo: str = options(default=True)["algo"], # TODO: implement other algos than forward Euler - gravity: tuple = options(default=True)["gravity"], - thermodynamics: str = options(default=True)["thermodynamics"], ): # base class constructor call super().__init__(particles) # init kernel for evaluating density etc. before each time step. - init_kernel = Pyccelkernel(eval_kernels_gc.sph_pressure_coeffs) - + init_kernel_1 = Pyccelkernel(eval_kernels_gc.sph_mean_velocity_coeffs) first_free_idx = particles.args_markers.first_free_idx comps = (0, 1, 2) + init_kernel_2 = Pyccelkernel(eval_kernels_gc.sph_mean_velocity) + # first_free_idx = particles.args_markers.first_free_idx + # comps = (0, 1, 2) + + init_kernel_3 = Pyccelkernel(eval_kernels_gc.sph_grad_mean_velocity) + comps_tensor = (0, 1, 2, 3, 4, 5, 6, 7, 8) + + init_kernel_4 = Pyccelkernel(eval_kernels_gc.sph_viscosity_tensor) + boxes = particles.sorting_boxes.boxes neighbours = particles.sorting_boxes.neighbours holes = particles.holes @@ -1473,19 +1856,34 @@ def __init__( ) self.add_init_kernel( - init_kernel, + init_kernel_1, first_free_idx, comps, args_init, ) - # pusher kernel - if thermodynamics == "isothermal": - kernel = Pyccelkernel(pusher_kernels.push_v_sph_pressure) - elif thermodynamics == "polytropic": - kernel = Pyccelkernel(pusher_kernels.push_v_sph_pressure_ideal_gas) + self.add_init_kernel( + init_kernel_2, + first_free_idx + 3, # +3 so that the previous one is not overwritten + comps, + args_init, + ) - gravity = np.array(gravity, dtype=float) + self.add_init_kernel( + init_kernel_3, + first_free_idx + 6, # +3 so that the previous one is not overwritten + comps_tensor, + args_init, + ) + + self.add_init_kernel( + init_kernel_4, + first_free_idx + 15, + comps_tensor, + args_init, + ) + + kernel = Pyccelkernel(pusher_kernels.push_v_viscosity) args_kernel = ( boxes, @@ -1494,7 +1892,6 @@ def __init__( *periodic, kernel_nr, *kernel_width, - gravity, ) # the Pusher class wraps around all kernels diff --git a/src/struphy/propagators/tests/test_gyrokinetic_poisson.py b/src/struphy/propagators/tests/test_gyrokinetic_poisson.py index 6b0714f4b..ce29c8141 100644 --- a/src/struphy/propagators/tests/test_gyrokinetic_poisson.py +++ b/src/struphy/propagators/tests/test_gyrokinetic_poisson.py @@ -1,3 +1,4 @@ +import cunumpy as xp import matplotlib.pyplot as plt import pytest from psydac.ddm.mpi import mpi as MPI @@ -6,9 +7,11 @@ from struphy.feec.projectors import L2Projector from struphy.feec.psydac_derham import Derham from struphy.geometry import domains -from struphy.propagators import ImplicitDiffusion +from struphy.geometry.base import Domain +from struphy.linear_algebra.solver import SolverParameters +from struphy.models.variables import FEECVariable from struphy.propagators.base import Propagator -from struphy.utils.arrays import xp as np +from struphy.propagators.propagators_fields import ImplicitDiffusion comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -24,27 +27,19 @@ ["Orthogonal", {"Lx": 4.0, "Ly": 2.0, "alpha": 0.1, "Lz": 3.0}], ], ) -def test_poisson_M1perp_1d(direction, bc_type, mapping, show_plot=False): +@pytest.mark.parametrize("projected_rhs", [False, True]) +def test_poisson_M1perp_1d(direction, bc_type, mapping, projected_rhs, show_plot=False): """ Test the convergence of Poisson solver with M1perp diffusion matrix in 1D by means of manufactured solutions. """ - solver_params = { - "type": ("pcg", "MassMatrixPreconditioner"), - "tol": 1.0e-13, - "maxiter": 3000, - "info": True, - "verbose": False, - "recycle": False, - } - # create domain object dom_type = mapping[0] dom_params = mapping[1] domain_class = getattr(domains, dom_type) - domain = domain_class(**dom_params) + domain: Domain = domain_class(**dom_params) if dom_type == "Cuboid": Lx = dom_params["r1"] - dom_params["l1"] @@ -77,54 +72,57 @@ def test_poisson_M1perp_1d(direction, bc_type, mapping, show_plot=False): if direction == 0: Nel = [Neli, 1, 1] p = [pi, 1, 1] - e1 = np.linspace(0.0, 1.0, 50) + e1 = xp.linspace(0.0, 1.0, 50) if bc_type == "neumann": spl_kind = [False, True, True] def sol1_xyz(x, y, z): - return np.cos(np.pi / Lx * x) + return xp.cos(xp.pi / Lx * x) def rho1_xyz(x, y, z): - return np.cos(np.pi / Lx * x) * (np.pi / Lx) ** 2 + return xp.cos(xp.pi / Lx * x) * (xp.pi / Lx) ** 2 else: if bc_type == "dirichlet": spl_kind = [False, True, True] - dirichlet_bc = [[not kd] * 2 for kd in spl_kind] + dirichlet_bc = [(not kd,) * 2 for kd in spl_kind] + dirichlet_bc = tuple(dirichlet_bc) def sol1_xyz(x, y, z): - return np.sin(2 * np.pi / Lx * x) + return xp.sin(2 * xp.pi / Lx * x) def rho1_xyz(x, y, z): - return np.sin(2 * np.pi / Lx * x) * (2 * np.pi / Lx) ** 2 + return xp.sin(2 * xp.pi / Lx * x) * (2 * xp.pi / Lx) ** 2 elif direction == 1: Nel = [1, Neli, 1] p = [1, pi, 1] - e2 = np.linspace(0.0, 1.0, 50) + e2 = xp.linspace(0.0, 1.0, 50) if bc_type == "neumann": spl_kind = [True, False, True] def sol1_xyz(x, y, z): - return np.cos(np.pi / Ly * y) + return xp.cos(xp.pi / Ly * y) def rho1_xyz(x, y, z): - return np.cos(np.pi / Ly * y) * (np.pi / Ly) ** 2 + return xp.cos(xp.pi / Ly * y) * (xp.pi / Ly) ** 2 else: if bc_type == "dirichlet": spl_kind = [True, False, True] - dirichlet_bc = [[not kd] * 2 for kd in spl_kind] + dirichlet_bc = [(not kd,) * 2 for kd in spl_kind] + dirichlet_bc = tuple(dirichlet_bc) def sol1_xyz(x, y, z): - return np.sin(2 * np.pi / Ly * y) + return xp.sin(2 * xp.pi / Ly * y) def rho1_xyz(x, y, z): - return np.sin(2 * np.pi / Ly * y) * (2 * np.pi / Ly) ** 2 + return xp.sin(2 * xp.pi / Ly * y) * (2 * xp.pi / Ly) ** 2 else: print("Direction should be either 0 or 1") # create derham object + print(f"{dirichlet_bc = }") derham = Derham(Nel, p, spl_kind, dirichlet_bc=dirichlet_bc, comm=comm) # mass matrices @@ -135,30 +133,52 @@ def rho1_xyz(x, y, z): Propagator.mass_ops = mass_ops # pullbacks of right-hand side - def rho1(e1, e2, e3): - return domain.pull(rho1_xyz, e1, e2, e3, kind="0", squeeze_out=True) - - rho_vec = L2Projector("H1", mass_ops).get_dofs(rho1, apply_bc=True) + def rho_pulled(e1, e2, e3): + return domain.pull(rho1_xyz, e1, e2, e3, kind="0", squeeze_out=False) + + # define how to pass rho + if projected_rhs: + rho = FEECVariable(space="H1") + rho.allocate(derham=derham, domain=domain) + rho.spline.vector = derham.P["0"](rho_pulled) + else: + rho = rho_pulled # create Poisson solver - _phi = derham.create_spline_function("phi", "H1") - poisson_solver = ImplicitDiffusion( - _phi.vector, + solver_params = SolverParameters( + tol=1.0e-13, + maxiter=3000, + info=True, + verbose=False, + recycle=False, + ) + + _phi = FEECVariable(space="H1") + _phi.allocate(derham=derham, domain=domain) + + poisson_solver = ImplicitDiffusion() + poisson_solver.variables.phi = _phi + + poisson_solver.options = poisson_solver.Options( sigma_1=1e-12, sigma_2=0.0, sigma_3=1.0, divide_by_dt=True, diffusion_mat="M1perp", - rho=rho_vec, - solver=solver_params, + rho=rho, + solver="pcg", + precond="MassMatrixPreconditioner", + solver_params=solver_params, ) + poisson_solver.allocate() + # Solve Poisson (call propagator with dt=1.) dt = 1.0 poisson_solver(dt) # push numerical solution and compare - sol_val1 = domain.push(_phi, e1, e2, e3, kind="0") + sol_val1 = domain.push(_phi.spline, e1, e2, e3, kind="0") x, y, z = domain(e1, e2, e3) analytic_value1 = sol1_xyz(x, y, z) @@ -176,16 +196,16 @@ def rho1(e1, e2, e3): plt.title(f"{Nel = }") plt.legend() - error = np.max(np.abs(analytic_value1 - sol_val1)) + error = xp.max(xp.abs(analytic_value1 - sol_val1)) print(f"{direction = }, {pi = }, {Neli = }, {error=}") errors.append(error) h = 1 / (Neli) h_vec.append(h) - m, _ = np.polyfit(np.log(Nels), np.log(errors), deg=1) + m, _ = xp.polyfit(xp.log(Nels), xp.log(errors), deg=1) print(f"For {pi = }, solution converges in {direction=} with rate {-m = } ") - assert -m > (pi + 1 - 0.06) + assert -m > (pi + 1 - 0.07) # Plot convergence in 1D if show_plot: @@ -220,26 +240,19 @@ def rho1(e1, e2, e3): ["Orthogonal", {"Lx": 4.0, "Ly": 2.0, "alpha": 0.1, "Lz": 1.0}], ], ) -def test_poisson_M1perp_2d(Nel, p, bc_type, mapping, show_plot=False): +@pytest.mark.parametrize("projected_rhs", [False, True]) +def test_poisson_M1perp_2d(Nel, p, bc_type, mapping, projected_rhs, show_plot=False): """ Test the Poisson solver with M1perp diffusion matrix by means of manufactured solutions in 2D . """ - solver_params = { - "type": ("pcg", "MassMatrixPreconditioner"), - "tol": 1.0e-13, - "maxiter": 3000, - "info": True, - "verbose": False, - "recycle": False, - } # create domain object dom_type = mapping[0] dom_params = mapping[1] domain_class = getattr(domains, dom_type) - domain = domain_class(**dom_params) + domain: Domain = domain_class(**dom_params) if dom_type == "Cuboid": Lx = dom_params["r1"] - dom_params["l1"] @@ -250,10 +263,10 @@ def test_poisson_M1perp_2d(Nel, p, bc_type, mapping, show_plot=False): # manufactured solution in 1D (overwritten for "neumann") def sol1_xyz(x, y, z): - return np.sin(2 * np.pi / Lx * x) + return xp.sin(2 * xp.pi / Lx * x) def rho1_xyz(x, y, z): - return np.sin(2 * np.pi / Lx * x) * (2 * np.pi / Lx) ** 2 + return xp.sin(2 * xp.pi / Lx * x) * (2 * xp.pi / Lx) ** 2 # boundary conditions dirichlet_bc = None @@ -263,25 +276,26 @@ def rho1_xyz(x, y, z): # manufactured solution in 2D def sol2_xyz(x, y, z): - return np.sin(2 * np.pi * x / Lx + 4 * np.pi / Ly * y) + return xp.sin(2 * xp.pi * x / Lx + 4 * xp.pi / Ly * y) def rho2_xyz(x, y, z): - ddx = np.sin(2 * np.pi / Lx * x + 4 * np.pi / Ly * y) * (2 * np.pi / Lx) ** 2 - ddy = np.sin(2 * np.pi / Lx * x + 4 * np.pi / Ly * y) * (4 * np.pi / Ly) ** 2 + ddx = xp.sin(2 * xp.pi / Lx * x + 4 * xp.pi / Ly * y) * (2 * xp.pi / Lx) ** 2 + ddy = xp.sin(2 * xp.pi / Lx * x + 4 * xp.pi / Ly * y) * (4 * xp.pi / Ly) ** 2 return ddx + ddy elif bc_type == "dirichlet": spl_kind = [False, True, True] - dirichlet_bc = [[not kd] * 2 for kd in spl_kind] + dirichlet_bc = [(not kd,) * 2 for kd in spl_kind] + dirichlet_bc = tuple(dirichlet_bc) print(f"{dirichlet_bc = }") # manufactured solution in 2D def sol2_xyz(x, y, z): - return np.sin(np.pi * x / Lx) * np.sin(4 * np.pi / Ly * y) + return xp.sin(xp.pi * x / Lx) * xp.sin(4 * xp.pi / Ly * y) def rho2_xyz(x, y, z): - ddx = np.sin(np.pi * x / Lx) * np.sin(4 * np.pi / Ly * y) * (np.pi / Lx) ** 2 - ddy = np.sin(np.pi * x / Lx) * np.sin(4 * np.pi / Ly * y) * (4 * np.pi / Ly) ** 2 + ddx = xp.sin(xp.pi * x / Lx) * xp.sin(4 * xp.pi / Ly * y) * (xp.pi / Lx) ** 2 + ddy = xp.sin(xp.pi * x / Lx) * xp.sin(4 * xp.pi / Ly * y) * (4 * xp.pi / Ly) ** 2 return ddx + ddy elif bc_type == "neumann": @@ -289,19 +303,19 @@ def rho2_xyz(x, y, z): # manufactured solution in 2D def sol2_xyz(x, y, z): - return np.cos(np.pi * x / Lx) * np.sin(4 * np.pi / Ly * y) + return xp.cos(xp.pi * x / Lx) * xp.sin(4 * xp.pi / Ly * y) def rho2_xyz(x, y, z): - ddx = np.cos(np.pi * x / Lx) * np.sin(4 * np.pi / Ly * y) * (np.pi / Lx) ** 2 - ddy = np.cos(np.pi * x / Lx) * np.sin(4 * np.pi / Ly * y) * (4 * np.pi / Ly) ** 2 + ddx = xp.cos(xp.pi * x / Lx) * xp.sin(4 * xp.pi / Ly * y) * (xp.pi / Lx) ** 2 + ddy = xp.cos(xp.pi * x / Lx) * xp.sin(4 * xp.pi / Ly * y) * (4 * xp.pi / Ly) ** 2 return ddx + ddy # manufactured solution in 1D def sol1_xyz(x, y, z): - return np.cos(np.pi / Lx * x) + return xp.cos(xp.pi / Lx * x) def rho1_xyz(x, y, z): - return np.cos(np.pi / Lx * x) * (np.pi / Lx) ** 2 + return xp.cos(xp.pi / Lx * x) * (xp.pi / Lx) ** 2 # create derham object derham = Derham(Nel, p, spl_kind, dirichlet_bc=dirichlet_bc, comm=comm) @@ -314,49 +328,95 @@ def rho1_xyz(x, y, z): Propagator.mass_ops = mass_ops # evaluation grid - e1 = np.linspace(0.0, 1.0, 50) - e2 = np.linspace(0.0, 1.0, 50) - e3 = np.linspace(0.0, 1.0, 1) + e1 = xp.linspace(0.0, 1.0, 50) + e2 = xp.linspace(0.0, 1.0, 50) + e3 = xp.linspace(0.0, 1.0, 1) # pullbacks of right-hand side - def rho1(e1, e2, e3): - return domain.pull(rho1_xyz, e1, e2, e3, kind="0", squeeze_out=True) + def rho1_pulled(e1, e2, e3): + return domain.pull(rho1_xyz, e1, e2, e3, kind="0", squeeze_out=False) - def rho2(e1, e2, e3): - return domain.pull(rho2_xyz, e1, e2, e3, kind="0", squeeze_out=True) + def rho2_pulled(e1, e2, e3): + return domain.pull(rho2_xyz, e1, e2, e3, kind="0", squeeze_out=False) - # discrete right-hand sides - l2_proj = L2Projector("H1", mass_ops) - rho_vec1 = l2_proj.get_dofs(rho1, apply_bc=True) - rho_vec2 = l2_proj.get_dofs(rho2, apply_bc=True) + # how to pass right-hand sides + if projected_rhs: + rho1 = FEECVariable(space="H1") + rho1.allocate(derham=derham, domain=domain) + rho1.spline.vector = derham.P["0"](rho1_pulled) + + rho2 = FEECVariable(space="H1") + rho2.allocate(derham=derham, domain=domain) + rho2.spline.vector = derham.P["0"](rho2_pulled) + else: + rho1 = rho1_pulled + rho2 = rho2_pulled # Create Poisson solvers - _phi1 = derham.create_spline_function("test1", "H1") - poisson_solver1 = ImplicitDiffusion( - _phi1.vector, sigma_1=1e-8, sigma_2=0.0, sigma_3=1.0, diffusion_mat="M1perp", rho=rho_vec1, solver=solver_params + solver_params = SolverParameters( + tol=1.0e-13, + maxiter=3000, + info=True, + verbose=False, + recycle=False, + ) + + _phi1 = FEECVariable(space="H1") + _phi1.allocate(derham=derham, domain=domain) + + poisson_solver1 = ImplicitDiffusion() + poisson_solver1.variables.phi = _phi1 + + poisson_solver1.options = poisson_solver1.Options( + sigma_1=1e-8, + sigma_2=0.0, + sigma_3=1.0, + divide_by_dt=True, + diffusion_mat="M1perp", + rho=rho1, + solver="pcg", + precond="MassMatrixPreconditioner", + solver_params=solver_params, ) - _phi2 = derham.create_spline_function("test2", "H1") - poisson_solver2 = ImplicitDiffusion( - _phi2.vector, sigma_1=1e-8, sigma_2=0.0, sigma_3=1.0, diffusion_mat="M1perp", rho=rho_vec2, solver=solver_params + poisson_solver1.allocate() + + _phi2 = FEECVariable(space="H1") + _phi2.allocate(derham=derham, domain=domain) + + poisson_solver2 = ImplicitDiffusion() + poisson_solver2.variables.phi = _phi2 + + poisson_solver2.options = poisson_solver2.Options( + sigma_1=1e-8, + sigma_2=0.0, + sigma_3=1.0, + divide_by_dt=True, + diffusion_mat="M1perp", + rho=rho2, + solver="pcg", + precond="MassMatrixPreconditioner", + solver_params=solver_params, ) + poisson_solver2.allocate() + # Solve Poisson equation (call propagator with dt=1.) dt = 1.0 poisson_solver1(dt) poisson_solver2(dt) # push numerical solutions - sol_val1 = domain.push(_phi1, e1, e2, e3, kind="0") - sol_val2 = domain.push(_phi2, e1, e2, e3, kind="0") + sol_val1 = domain.push(_phi1.spline, e1, e2, e3, kind="0") + sol_val2 = domain.push(_phi2.spline, e1, e2, e3, kind="0") x, y, z = domain(e1, e2, e3) analytic_value1 = sol1_xyz(x, y, z) analytic_value2 = sol2_xyz(x, y, z) # compute error - error1 = np.max(np.abs(analytic_value1 - sol_val1)) - error2 = np.max(np.abs(analytic_value2 - sol_val2)) + error1 = xp.max(xp.abs(analytic_value1 - sol_val1)) + error2 = xp.max(xp.abs(analytic_value2 - sol_val2)) print(f"{p = }, {bc_type = }, {mapping = }") print(f"{error1 = }") @@ -386,7 +446,7 @@ def rho2(e1, e2, e3): plt.show() assert error1 < 0.0044 - assert error2 < 0.021 + assert error2 < 0.023 @pytest.mark.skip(reason="Not clear if the 2.5d strategy is sound.") @@ -408,35 +468,26 @@ def test_poisson_M1perp_3d_compare_2p5d(Nel, p, mapping, show_plot=False): from time import time - solver_params = { - "type": ("pcg", "MassMatrixPreconditioner"), - "tol": 1.0e-13, - "maxiter": 3000, - "info": False, - "verbose": False, - "recycle": False, - } - # create domain object dom_type = mapping[0] dom_params = mapping[1] domain_class = getattr(domains, dom_type) - domain = domain_class(**dom_params) + domain: Domain = domain_class(**dom_params) # boundary conditions spl_kind = [False, True, True] - dirichlet_bc = [[True, True], [False, False], [False, False]] + dirichlet_bc = ((True, True), (False, False), (False, False)) # evaluation grid - e1 = np.linspace(0.0, 1.0, 50) - e2 = np.linspace(0.0, 1.0, 60) - e3 = np.linspace(0.0, 1.0, 30) + e1 = xp.linspace(0.0, 1.0, 50) + e2 = xp.linspace(0.0, 1.0, 60) + e3 = xp.linspace(0.0, 1.0, 30) # solution and right-hand side on unit cube def rho(e1, e2, e3): - dd1 = np.sin(np.pi * e1) * np.sin(4 * np.pi * e2) * np.cos(2 * np.pi * e3) * (np.pi) ** 2 - dd2 = np.sin(np.pi * e1) * np.sin(4 * np.pi * e2) * np.cos(2 * np.pi * e3) * (4 * np.pi) ** 2 + dd1 = xp.sin(xp.pi * e1) * xp.sin(4 * xp.pi * e2) * xp.cos(2 * xp.pi * e3) * (xp.pi) ** 2 + dd2 = xp.sin(xp.pi * e1) * xp.sin(4 * xp.pi * e2) * xp.cos(2 * xp.pi * e3) * (4 * xp.pi) ** 2 return dd1 + dd2 # create 3d derham object @@ -455,20 +506,44 @@ def rho(e1, e2, e3): print(f"{rho_vec[:].shape = }") # Create 3d Poisson solver - _phi = derham.create_spline_function("test2", "H1") - _phi_2p5d = derham.create_spline_function("sol_2p5d", "H1") - poisson_solver_3d = ImplicitDiffusion( - _phi.vector, sigma_1=1e-8, sigma_2=0.0, sigma_3=1.0, diffusion_mat="M1perp", rho=rho_vec, solver=solver_params + solver_params = SolverParameters( + tol=1.0e-13, + maxiter=3000, + info=True, + verbose=False, + recycle=False, ) - s = _phi.starts - e = _phi.ends + _phi = FEECVariable(space="H1") + _phi.allocate(derham=derham, domain=domain) + + _phi_2p5d = FEECVariable(space="H1") + _phi_2p5d.allocate(derham=derham, domain=domain) + + poisson_solver_3d = ImplicitDiffusion() + poisson_solver_3d.variables.phi = _phi + + poisson_solver_3d.options = poisson_solver_3d.Options( + sigma_1=1e-8, + sigma_2=0.0, + sigma_3=1.0, + divide_by_dt=True, + diffusion_mat="M1perp", + rho=rho, + solver="pcg", + precond="MassMatrixPreconditioner", + solver_params=solver_params, + ) + + poisson_solver_3d.allocate() + + s = _phi.spline.starts + e = _phi.spline.ends # create 2.5d deRham object Nel_new = [Nel[0], Nel[1], 1] p[2] = 1 spl_kind[2] = True - dirichlet_bc[2] = [False, False] derham = Derham(Nel_new, p, spl_kind, dirichlet_bc=dirichlet_bc, comm=comm) mass_ops = WeightedMassOperators(derham, domain) @@ -476,18 +551,26 @@ def rho(e1, e2, e3): Propagator.derham = derham Propagator.mass_ops = mass_ops - _phi_small = derham.create_spline_function("test_small", "H1") - rhs = derham.create_spline_function("rhs", "H1") - poisson_solver_2p5d = ImplicitDiffusion( - _phi_small.vector, + _phi_small = FEECVariable(space="H1") + _phi_small.allocate(derham=derham, domain=domain) + + poisson_solver_2p5d = ImplicitDiffusion() + poisson_solver_2p5d.variables.phi = _phi_small + + poisson_solver_2p5d.options = poisson_solver_2p5d.Options( sigma_1=1e-8, sigma_2=0.0, sigma_3=1.0, + divide_by_dt=True, diffusion_mat="M1perp", - rho=rhs.vector, - solver=solver_params, + rho=rho, + solver="pcg", + precond="MassMatrixPreconditioner", + solver_params=solver_params, ) + poisson_solver_2p5d.allocate() + # Solve Poisson equation (call propagator with dt=1.) dt = 1.0 t0 = time() @@ -499,26 +582,24 @@ def rho(e1, e2, e3): t0 = time() t_inner = 0.0 for n in range(s[2], e[2] + 1): - # scale the rhs with Nel[2] !! - rhs.vector[s[0] : e[0] + 1, s[1] : e[1] + 1, 0] = rho_vec[s[0] : e[0] + 1, s[1] : e[1] + 1, n] * Nel[2] t0i = time() poisson_solver_2p5d(dt) t1i = time() t_inner += t1i - t0i - _tmp = _phi_small.vector.copy() - _phi_2p5d.vector[s[0] : e[0] + 1, s[1] : e[1] + 1, n] = _tmp[s[0] : e[0] + 1, s[1] : e[1] + 1, 0] + _tmp = _phi_small.spline.vector.copy() + _phi_2p5d.spline.vector[s[0] : e[0] + 1, s[1] : e[1] + 1, n] = _tmp[s[0] : e[0] + 1, s[1] : e[1] + 1, 0] t1 = time() print(f"rank {rank}, 2.5d pure solve time (without copy) = {t_inner}") print(f"rank {rank}, 2.5d solve time = {t1 - t0}") # push numerical solutions - sol_val = domain.push(_phi, e1, e2, e3, kind="0") - sol_val_2p5d = domain.push(_phi_2p5d, e1, e2, e3, kind="0") + sol_val = domain.push(_phi.spline, e1, e2, e3, kind="0") + sol_val_2p5d = domain.push(_phi_2p5d.spline, e1, e2, e3, kind="0") x, y, z = domain(e1, e2, e3) - print("max diff:", np.max(np.abs(sol_val - sol_val_2p5d))) - assert np.max(np.abs(sol_val - sol_val_2p5d)) < 0.026 + print("max diff:", xp.max(xp.abs(sol_val - sol_val_2p5d))) + assert xp.max(xp.abs(sol_val - sol_val_2p5d)) < 0.026 if show_plot and rank == 0: plt.figure("e1-e2 plane", figsize=(24, 16)) @@ -555,9 +636,9 @@ def rho(e1, e2, e3): if __name__ == "__main__": direction = 0 - bc_type = "periodic" + bc_type = "dirichlet" mapping = ["Cuboid", {"l1": 0.0, "r1": 4.0, "l2": 0.0, "r2": 2.0, "l3": 0.0, "r3": 3.0}] - # mapping = ['Orthogonal', {'Lx': 4., 'Ly': 2., 'alpha': .1, 'Lz': 3.}] + mapping = ["Orthogonal", {"Lx": 4.0, "Ly": 2.0, "alpha": 0.1, "Lz": 3.0}] test_poisson_M1perp_1d(direction, bc_type, mapping, show_plot=True) # Nel = [64, 64, 1] @@ -569,6 +650,5 @@ def rho(e1, e2, e3): # Nel = [64, 64, 16] # p = [2, 2, 1] - # mapping = ['Cuboid', {'l1': 0., 'r1': 1., - # 'l2': 0., 'r2': 1., 'l3': 0., 'r3': 1.}] + # mapping = ["Cuboid", {"l1": 0.0, "r1": 1.0, "l2": 0.0, "r2": 1.0, "l3": 0.0, "r3": 1.0}] # test_poisson_M1perp_3d_compare_2p5d(Nel, p, mapping, show_plot=True) diff --git a/src/struphy/propagators/tests/test_poisson.py b/src/struphy/propagators/tests/test_poisson.py index c68cf5041..78567f8d1 100644 --- a/src/struphy/propagators/tests/test_poisson.py +++ b/src/struphy/propagators/tests/test_poisson.py @@ -1,3 +1,4 @@ +import cunumpy as xp import matplotlib.pyplot as plt import pytest from psydac.ddm.mpi import mpi as MPI @@ -6,9 +7,23 @@ from struphy.feec.projectors import L2Projector from struphy.feec.psydac_derham import Derham from struphy.geometry import domains -from struphy.propagators import ImplicitDiffusion +from struphy.geometry.base import Domain +from struphy.initial import perturbations +from struphy.kinetic_background.maxwellians import Maxwellian3D +from struphy.linear_algebra.solver import SolverParameters +from struphy.models.variables import FEECVariable +from struphy.pic.accumulation.accum_kernels import charge_density_0form +from struphy.pic.accumulation.particles_to_grid import AccumulatorVector +from struphy.pic.particles import Particles6D +from struphy.pic.utilities import ( + BinningPlot, + BoundaryParameters, + LoadingParameters, + WeightsParameters, +) from struphy.propagators.base import Propagator -from struphy.utils.arrays import xp as np +from struphy.propagators.propagators_fields import ImplicitDiffusion, Poisson +from struphy.utils.pyccel import Pyccelkernel comm = MPI.COMM_WORLD rank = comm.Get_rank() @@ -24,26 +39,24 @@ ["Orthogonal", {"Lx": 4.0, "Ly": 2.0, "alpha": 0.1, "Lz": 3.0}], ], ) -def test_poisson_1d(direction, bc_type, mapping, show_plot=False): +@pytest.mark.parametrize("projected_rhs", [False, True]) +def test_poisson_1d( + direction: int, + bc_type: str, + mapping: list[str, dict], + projected_rhs: bool, + show_plot: bool = False, +): """ Test the convergence of Poisson solver in 1D by means of manufactured solutions. """ - solver_params = { - "type": ("pcg", "MassMatrixPreconditioner"), - "tol": 1.0e-13, - "maxiter": 3000, - "info": True, - "verbose": False, - "recycle": False, - } - # create domain object dom_type = mapping[0] dom_params = mapping[1] domain_class = getattr(domains, dom_type) - domain = domain_class(**dom_params) + domain: Domain = domain_class(**dom_params) if dom_type == "Cuboid": Lx = dom_params["r1"] - dom_params["l1"] @@ -76,74 +89,77 @@ def test_poisson_1d(direction, bc_type, mapping, show_plot=False): if direction == 0: Nel = [Neli, 1, 1] p = [pi, 1, 1] - e1 = np.linspace(0.0, 1.0, 50) + e1 = xp.linspace(0.0, 1.0, 50) if bc_type == "neumann": spl_kind = [False, True, True] def sol1_xyz(x, y, z): - return np.cos(np.pi / Lx * x) + return xp.cos(xp.pi / Lx * x) def rho1_xyz(x, y, z): - return np.cos(np.pi / Lx * x) * (np.pi / Lx) ** 2 + return xp.cos(xp.pi / Lx * x) * (xp.pi / Lx) ** 2 else: if bc_type == "dirichlet": spl_kind = [False, True, True] - dirichlet_bc = [[not kd] * 2 for kd in spl_kind] + dirichlet_bc = [(not kd,) * 2 for kd in spl_kind] + dirichlet_bc = tuple(dirichlet_bc) def sol1_xyz(x, y, z): - return np.sin(2 * np.pi / Lx * x) + return xp.sin(2 * xp.pi / Lx * x) def rho1_xyz(x, y, z): - return np.sin(2 * np.pi / Lx * x) * (2 * np.pi / Lx) ** 2 + return xp.sin(2 * xp.pi / Lx * x) * (2 * xp.pi / Lx) ** 2 elif direction == 1: Nel = [1, Neli, 1] p = [1, pi, 1] - e2 = np.linspace(0.0, 1.0, 50) + e2 = xp.linspace(0.0, 1.0, 50) if bc_type == "neumann": spl_kind = [True, False, True] def sol1_xyz(x, y, z): - return np.cos(np.pi / Ly * y) + return xp.cos(xp.pi / Ly * y) def rho1_xyz(x, y, z): - return np.cos(np.pi / Ly * y) * (np.pi / Ly) ** 2 + return xp.cos(xp.pi / Ly * y) * (xp.pi / Ly) ** 2 else: if bc_type == "dirichlet": spl_kind = [True, False, True] - dirichlet_bc = [[not kd] * 2 for kd in spl_kind] + dirichlet_bc = [(not kd,) * 2 for kd in spl_kind] + dirichlet_bc = tuple(dirichlet_bc) def sol1_xyz(x, y, z): - return np.sin(2 * np.pi / Ly * y) + return xp.sin(2 * xp.pi / Ly * y) def rho1_xyz(x, y, z): - return np.sin(2 * np.pi / Ly * y) * (2 * np.pi / Ly) ** 2 + return xp.sin(2 * xp.pi / Ly * y) * (2 * xp.pi / Ly) ** 2 elif direction == 2: Nel = [1, 1, Neli] p = [1, 1, pi] - e3 = np.linspace(0.0, 1.0, 50) + e3 = xp.linspace(0.0, 1.0, 50) if bc_type == "neumann": spl_kind = [True, True, False] def sol1_xyz(x, y, z): - return np.cos(np.pi / Lz * z) + return xp.cos(xp.pi / Lz * z) def rho1_xyz(x, y, z): - return np.cos(np.pi / Lz * z) * (np.pi / Lz) ** 2 + return xp.cos(xp.pi / Lz * z) * (xp.pi / Lz) ** 2 else: if bc_type == "dirichlet": spl_kind = [True, True, False] - dirichlet_bc = [[not kd] * 2 for kd in spl_kind] + dirichlet_bc = [(not kd,) * 2 for kd in spl_kind] + dirichlet_bc = tuple(dirichlet_bc) def sol1_xyz(x, y, z): - return np.sin(2 * np.pi / Lz * z) + return xp.sin(2 * xp.pi / Lz * z) def rho1_xyz(x, y, z): - return np.sin(2 * np.pi / Lz * z) * (2 * np.pi / Lz) ** 2 + return xp.sin(2 * xp.pi / Lz * z) * (2 * xp.pi / Lz) ** 2 else: print("Direction should be either 0, 1 or 2") @@ -158,23 +174,50 @@ def rho1_xyz(x, y, z): Propagator.mass_ops = mass_ops # pullbacks of right-hand side - def rho1(e1, e2, e3): - return domain.pull(rho1_xyz, e1, e2, e3, kind="0", squeeze_out=True) - - rho_vec = L2Projector("H1", mass_ops).get_dofs(rho1, apply_bc=True) + def rho_pulled(e1, e2, e3): + return domain.pull(rho1_xyz, e1, e2, e3, kind="0", squeeze_out=False) + + # define how to pass rho + if projected_rhs: + rho = FEECVariable(space="H1") + rho.allocate(derham=derham, domain=domain) + rho.spline.vector = derham.P["0"](rho_pulled) + else: + rho = rho_pulled # create Poisson solver - _phi = derham.create_spline_function("phi", "H1") - poisson_solver = ImplicitDiffusion( - _phi.vector, sigma_1=1e-12, sigma_2=0.0, sigma_3=1.0, rho=rho_vec, solver=solver_params + solver_params = SolverParameters( + tol=1.0e-13, + maxiter=3000, + info=True, + verbose=False, + recycle=False, + ) + + _phi = FEECVariable(space="H1") + _phi.allocate(derham=derham, domain=domain) + + poisson_solver = Poisson() + poisson_solver.variables.phi = _phi + + poisson_solver.options = poisson_solver.Options( + stab_eps=1e-12, + # sigma_2=0.0, + # sigma_3=1.0, + rho=rho, + solver="pcg", + precond="MassMatrixPreconditioner", + solver_params=solver_params, ) + poisson_solver.allocate() + # Solve Poisson (call propagator with dt=1.) dt = 1.0 poisson_solver(dt) # push numerical solution and compare - sol_val1 = domain.push(_phi, e1, e2, e3, kind="0") + sol_val1 = domain.push(_phi.spline, e1, e2, e3, kind="0") x, y, z = domain(e1, e2, e3) analytic_value1 = sol1_xyz(x, y, z) @@ -196,16 +239,16 @@ def rho1(e1, e2, e3): plt.title(f"{Nel = }") plt.legend() - error = np.max(np.abs(analytic_value1 - sol_val1)) + error = xp.max(xp.abs(analytic_value1 - sol_val1)) print(f"{direction = }, {pi = }, {Neli = }, {error=}") errors.append(error) h = 1 / (Neli) h_vec.append(h) - m, _ = np.polyfit(np.log(Nels), np.log(errors), deg=1) + m, _ = xp.polyfit(xp.log(Nels), xp.log(errors), deg=1) print(f"For {pi = }, solution converges in {direction=} with rate {-m = } ") - assert -m > (pi + 1 - 0.06) + assert -m > (pi + 1 - 0.07) # Plot convergence in 1D if show_plot: @@ -230,6 +273,162 @@ def rho1(e1, e2, e3): plt.show() +@pytest.mark.parametrize( + "mapping", + [ + ["Cuboid", {"l1": 0.0, "r1": 4.0, "l2": 0.0, "r2": 2.0, "l3": 0.0, "r3": 3.0}], + # ["Orthogonal", {"Lx": 4.0, "Ly": 2.0, "alpha": 0.1, "Lz": 3.0}], + ], +) +def test_poisson_accum_1d(mapping, do_plot=False): + """Pass accumulators as rhs.""" + # create domain object + dom_type = mapping[0] + dom_params = mapping[1] + + domain_class = getattr(domains, dom_type) + domain: Domain = domain_class(**dom_params) + + if dom_type == "Cuboid": + Lx = dom_params["r1"] - dom_params["l1"] + else: + Lx = dom_params["Lx"] + + # create derham object + Nel = (16, 1, 1) + p = (2, 1, 1) + spl_kind = (True, True, True) + derham = Derham(Nel, p, spl_kind, comm=comm) + + # mass matrices + mass_ops = WeightedMassOperators(derham, domain) + + Propagator.derham = derham + Propagator.domain = domain + Propagator.mass_ops = mass_ops + + # 6D particle object + domain_array = derham.domain_array + nprocs = derham.domain_decomposition.nprocs + domain_decomp = (domain_array, nprocs) + + lp = LoadingParameters(ppc=4000, seed=765) + wp = WeightsParameters(control_variate=True) + bp = BoundaryParameters() + + backgr = Maxwellian3D(n=(1.0, None)) + l = 1 + amp = 1e-1 + pert = perturbations.ModesCos(ls=(l,), amps=(amp,)) + maxw = Maxwellian3D(n=(1.0, pert)) + + pert_exact = lambda x, y, z: amp * xp.cos(l * 2 * xp.pi / Lx * x) + phi_exact = lambda x, y, z: amp / (l * 2 * xp.pi / Lx) ** 2 * xp.cos(l * 2 * xp.pi / Lx * x) + e_exact = lambda x, y, z: amp / (l * 2 * xp.pi / Lx) * xp.sin(l * 2 * xp.pi / Lx * x) + + particles = Particles6D( + comm_world=comm, + domain_decomp=domain_decomp, + loading_params=lp, + weights_params=wp, + boundary_params=bp, + domain=domain, + background=backgr, + initial_condition=maxw, + ) + particles.draw_markers() + particles.initialize_weights() + + # particle to grid coupling + kernel = Pyccelkernel(charge_density_0form) + accum = AccumulatorVector(particles, "H1", kernel, mass_ops, domain.args_domain) + # accum() + # if do_plot: + # accum.show_accumulated_spline_field(mass_ops) + + rho = accum + + # create Poisson solver + solver_params = SolverParameters( + tol=1.0e-13, + maxiter=3000, + info=True, + verbose=False, + recycle=False, + ) + + _phi = FEECVariable(space="H1") + _phi.allocate(derham=derham, domain=domain) + + poisson_solver = Poisson() + poisson_solver.variables.phi = _phi + + poisson_solver.options = poisson_solver.Options( + stab_eps=1e-6, + # sigma_2=0.0, + # sigma_3=1.0, + rho=rho, + solver="pcg", + precond="MassMatrixPreconditioner", + solver_params=solver_params, + ) + + poisson_solver.allocate() + + # Solve Poisson (call propagator with dt=1.) + dt = 1.0 + poisson_solver(dt) + + # push numerical solution and compare + e1 = xp.linspace(0.0, 1.0, 50) + e2 = 0.0 + e3 = 0.0 + + num_values = domain.push(_phi.spline, e1, e2, e3, kind="0") + x, y, z = domain(e1, e2, e3) + pert_values = pert_exact(x, y, z) + analytic_values = phi_exact(x, y, z) + e_values = e_exact(x, y, z) + + _e = FEECVariable(space="Hcurl") + _e.allocate(derham=derham, domain=domain) + derham.grad.dot(-_phi.spline.vector, out=_e.spline.vector) + num_values_e = domain.push(_e.spline, e1, e2, e3, kind="1") + + if do_plot: + field = derham.create_spline_function("accum_field", "H1") + field.vector = accum.vectors[0] + accum_values = field(e1, e2, e3) + + plt.figure(figsize=(18, 12)) + plt.subplot(1, 3, 1) + plt.plot(x[:, 0, 0], num_values[:, 0, 0], "ob", label="numerical") + plt.plot(x[:, 0, 0], analytic_values[:, 0, 0], "r--", label="exact") + plt.xlabel("x") + plt.title("phi") + plt.legend() + plt.subplot(1, 3, 2) + plt.plot(x[:, 0, 0], accum_values[:, 0, 0], "ob", label="numerical, without L2-proj") + plt.plot(x[:, 0, 0], pert_values[:, 0, 0], "r--", label="exact") + plt.xlabel("x") + plt.title("rhs") + plt.legend() + plt.subplot(1, 3, 3) + plt.plot(x[:, 0, 0], num_values_e[0][:, 0, 0], "ob", label="numerical") + plt.plot(x[:, 0, 0], e_values[:, 0, 0], "r--", label="exact") + plt.xlabel("x") + plt.title("e_field") + plt.legend() + + plt.show() + + error = xp.max(xp.abs(num_values_e[0][:, 0, 0] - e_values[:, 0, 0])) / xp.max(xp.abs(e_values[:, 0, 0])) + print(f"{error=}") + + assert error < 0.0086 + + +@pytest.mark.mpi(min_size=2) @pytest.mark.parametrize("Nel", [[64, 64, 1]]) @pytest.mark.parametrize("p", [[1, 1, 1], [2, 2, 1]]) @pytest.mark.parametrize("bc_type", ["periodic", "dirichlet", "neumann"]) @@ -240,25 +439,18 @@ def rho1(e1, e2, e3): ["Colella", {"Lx": 4.0, "Ly": 2.0, "alpha": 0.1, "Lz": 1.0}], ], ) -def test_poisson_2d(Nel, p, bc_type, mapping, show_plot=False): +@pytest.mark.parametrize("projected_rhs", [False, True]) +def test_poisson_2d(Nel, p, bc_type, mapping, projected_rhs, show_plot=False): """ Test the Poisson solver by means of manufactured solutions in 2D . """ - solver_params = { - "type": ("pcg", "MassMatrixPreconditioner"), - "tol": 1.0e-13, - "maxiter": 3000, - "info": True, - "verbose": False, - "recycle": False, - } # create domain object dom_type = mapping[0] dom_params = mapping[1] domain_class = getattr(domains, dom_type) - domain = domain_class(**dom_params) + domain: Domain = domain_class(**dom_params) if dom_type == "Cuboid": Lx = dom_params["r1"] - dom_params["l1"] @@ -269,10 +461,10 @@ def test_poisson_2d(Nel, p, bc_type, mapping, show_plot=False): # manufactured solution in 1D (overwritten for "neumann") def sol1_xyz(x, y, z): - return np.sin(2 * np.pi / Lx * x) + return xp.sin(2 * xp.pi / Lx * x) def rho1_xyz(x, y, z): - return np.sin(2 * np.pi / Lx * x) * (2 * np.pi / Lx) ** 2 + return xp.sin(2 * xp.pi / Lx * x) * (2 * xp.pi / Lx) ** 2 # boundary conditions dirichlet_bc = None @@ -282,25 +474,26 @@ def rho1_xyz(x, y, z): # manufactured solution in 2D def sol2_xyz(x, y, z): - return np.sin(2 * np.pi * x / Lx + 4 * np.pi / Ly * y) + return xp.sin(2 * xp.pi * x / Lx + 4 * xp.pi / Ly * y) def rho2_xyz(x, y, z): - ddx = np.sin(2 * np.pi / Lx * x + 4 * np.pi / Ly * y) * (2 * np.pi / Lx) ** 2 - ddy = np.sin(2 * np.pi / Lx * x + 4 * np.pi / Ly * y) * (4 * np.pi / Ly) ** 2 + ddx = xp.sin(2 * xp.pi / Lx * x + 4 * xp.pi / Ly * y) * (2 * xp.pi / Lx) ** 2 + ddy = xp.sin(2 * xp.pi / Lx * x + 4 * xp.pi / Ly * y) * (4 * xp.pi / Ly) ** 2 return ddx + ddy elif bc_type == "dirichlet": spl_kind = [False, True, True] - dirichlet_bc = [[not kd] * 2 for kd in spl_kind] + dirichlet_bc = [(not kd,) * 2 for kd in spl_kind] + dirichlet_bc = tuple(dirichlet_bc) print(f"{dirichlet_bc = }") # manufactured solution in 2D def sol2_xyz(x, y, z): - return np.sin(np.pi * x / Lx) * np.sin(4 * np.pi / Ly * y) + return xp.sin(xp.pi * x / Lx) * xp.sin(4 * xp.pi / Ly * y) def rho2_xyz(x, y, z): - ddx = np.sin(np.pi * x / Lx) * np.sin(4 * np.pi / Ly * y) * (np.pi / Lx) ** 2 - ddy = np.sin(np.pi * x / Lx) * np.sin(4 * np.pi / Ly * y) * (4 * np.pi / Ly) ** 2 + ddx = xp.sin(xp.pi * x / Lx) * xp.sin(4 * xp.pi / Ly * y) * (xp.pi / Lx) ** 2 + ddy = xp.sin(xp.pi * x / Lx) * xp.sin(4 * xp.pi / Ly * y) * (4 * xp.pi / Ly) ** 2 return ddx + ddy elif bc_type == "neumann": @@ -308,19 +501,19 @@ def rho2_xyz(x, y, z): # manufactured solution in 2D def sol2_xyz(x, y, z): - return np.cos(np.pi * x / Lx) * np.sin(4 * np.pi / Ly * y) + return xp.cos(xp.pi * x / Lx) * xp.sin(4 * xp.pi / Ly * y) def rho2_xyz(x, y, z): - ddx = np.cos(np.pi * x / Lx) * np.sin(4 * np.pi / Ly * y) * (np.pi / Lx) ** 2 - ddy = np.cos(np.pi * x / Lx) * np.sin(4 * np.pi / Ly * y) * (4 * np.pi / Ly) ** 2 + ddx = xp.cos(xp.pi * x / Lx) * xp.sin(4 * xp.pi / Ly * y) * (xp.pi / Lx) ** 2 + ddy = xp.cos(xp.pi * x / Lx) * xp.sin(4 * xp.pi / Ly * y) * (4 * xp.pi / Ly) ** 2 return ddx + ddy # manufactured solution in 1D def sol1_xyz(x, y, z): - return np.cos(np.pi / Lx * x) + return xp.cos(xp.pi / Lx * x) def rho1_xyz(x, y, z): - return np.cos(np.pi / Lx * x) * (np.pi / Lx) ** 2 + return xp.cos(xp.pi / Lx * x) * (xp.pi / Lx) ** 2 # create derham object derham = Derham(Nel, p, spl_kind, dirichlet_bc=dirichlet_bc, comm=comm) @@ -333,49 +526,107 @@ def rho1_xyz(x, y, z): Propagator.mass_ops = mass_ops # evaluation grid - e1 = np.linspace(0.0, 1.0, 50) - e2 = np.linspace(0.0, 1.0, 50) - e3 = np.linspace(0.0, 1.0, 1) + e1 = xp.linspace(0.0, 1.0, 50) + e2 = xp.linspace(0.0, 1.0, 50) + e3 = xp.linspace(0.0, 1.0, 1) # pullbacks of right-hand side - def rho1(e1, e2, e3): - return domain.pull(rho1_xyz, e1, e2, e3, kind="0", squeeze_out=True) + def rho1_pulled(e1, e2, e3): + return domain.pull(rho1_xyz, e1, e2, e3, kind="0", squeeze_out=False) + + def rho2_pulled(e1, e2, e3): + return domain.pull(rho2_xyz, e1, e2, e3, kind="0", squeeze_out=False) - def rho2(e1, e2, e3): - return domain.pull(rho2_xyz, e1, e2, e3, kind="0", squeeze_out=True) + # how to pass right-hand sides + if projected_rhs: + rho1 = FEECVariable(space="H1") + rho1.allocate(derham=derham, domain=domain) + rho1.spline.vector = derham.P["0"](rho1_pulled) - # discrete right-hand sides - l2_proj = L2Projector("H1", mass_ops) - rho_vec1 = l2_proj.get_dofs(rho1, apply_bc=True) - rho_vec2 = l2_proj.get_dofs(rho2, apply_bc=True) + rho2 = FEECVariable(space="H1") + rho2.allocate(derham=derham, domain=domain) + rho2.spline.vector = derham.P["0"](rho2_pulled) + else: + rho1 = rho1_pulled + rho2 = rho2_pulled # Create Poisson solvers - _phi1 = derham.create_spline_function("test1", "H1") - poisson_solver1 = ImplicitDiffusion( - _phi1.vector, sigma_1=1e-8, sigma_2=0.0, sigma_3=1.0, rho=rho_vec1, solver=solver_params + solver_params = SolverParameters( + tol=1.0e-13, + maxiter=3000, + info=True, + verbose=False, + recycle=False, + ) + + _phi1 = FEECVariable(space="H1") + _phi1.allocate(derham=derham, domain=domain) + + poisson_solver1 = Poisson() + poisson_solver1.variables.phi = _phi1 + + poisson_solver1.options = poisson_solver1.Options( + stab_eps=1e-8, + # sigma_2=0.0, + # sigma_3=1.0, + rho=rho1, + solver="pcg", + precond="MassMatrixPreconditioner", + solver_params=solver_params, ) - _phi2 = derham.create_spline_function("test2", "H1") - poisson_solver2 = ImplicitDiffusion( - _phi2.vector, sigma_1=1e-8, sigma_2=0.0, sigma_3=1.0, rho=rho_vec2, solver=solver_params + poisson_solver1.allocate() + + # _phi1 = derham.create_spline_function("test1", "H1") + # poisson_solver1 = Poisson( + # _phi1.vector, sigma_1=1e-8, sigma_2=0.0, sigma_3=1.0, rho=rho_vec1, solver=solver_params + # ) + + _phi2 = FEECVariable(space="H1") + _phi2.allocate(derham=derham, domain=domain) + + poisson_solver2 = Poisson() + poisson_solver2.variables.phi = _phi2 + + stab_eps = 1e-8 + err_lim = 0.03 + if bc_type == "neumann" and dom_type == "Colella": + stab_eps = 1e-4 + err_lim = 0.046 + + poisson_solver2.options = poisson_solver2.Options( + stab_eps=stab_eps, + # sigma_2=0.0, + # sigma_3=1.0, + rho=rho2, + solver="pcg", + precond="MassMatrixPreconditioner", + solver_params=solver_params, ) + poisson_solver2.allocate() + + # _phi2 = derham.create_spline_function("test2", "H1") + # poisson_solver2 = Poisson( + # _phi2.vector, sigma_1=1e-8, sigma_2=0.0, sigma_3=1.0, rho=rho_vec2, solver=solver_params + # ) + # Solve Poisson equation (call propagator with dt=1.) dt = 1.0 poisson_solver1(dt) poisson_solver2(dt) # push numerical solutions - sol_val1 = domain.push(_phi1, e1, e2, e3, kind="0") - sol_val2 = domain.push(_phi2, e1, e2, e3, kind="0") + sol_val1 = domain.push(_phi1.spline, e1, e2, e3, kind="0") + sol_val2 = domain.push(_phi2.spline, e1, e2, e3, kind="0") x, y, z = domain(e1, e2, e3) analytic_value1 = sol1_xyz(x, y, z) analytic_value2 = sol2_xyz(x, y, z) # compute error - error1 = np.max(np.abs(analytic_value1 - sol_val1)) - error2 = np.max(np.abs(analytic_value2 - sol_val2)) + error1 = xp.max(xp.abs(analytic_value1 - sol_val1)) + error2 = xp.max(xp.abs(analytic_value2 - sol_val2)) print(f"{p = }, {bc_type = }, {mapping = }") print(f"{error1 = }") @@ -407,21 +658,23 @@ def rho2(e1, e2, e3): if p[0] == 1 and bc_type == "neumann" and mapping[0] == "Colella": pass else: - assert error1 < 0.0044 - assert error2 < 0.021 + assert error1 < 0.0053 + assert error2 < err_lim if __name__ == "__main__": - direction = 0 - bc_type = "dirichlet" + # direction = 0 + # bc_type = "dirichlet" mapping = ["Cuboid", {"l1": 0.0, "r1": 4.0, "l2": 0.0, "r2": 2.0, "l3": 0.0, "r3": 3.0}] # mapping = ['Orthogonal', {'Lx': 4., 'Ly': 2., 'alpha': .1, 'Lz': 3.}] - test_poisson_1d(direction, bc_type, mapping, show_plot=True) + # test_poisson_1d(direction, bc_type, mapping, projected_rhs=True, show_plot=True) # Nel = [64, 64, 1] # p = [2, 2, 1] # bc_type = 'neumann' - # #mapping = ['Cuboid', {'l1': 0., 'r1': 4., 'l2': 0., 'r2': 2., 'l3': 0., 'r3': 3.}] - # #mapping = ['Orthogonal', {'Lx': 4., 'Ly': 2., 'alpha': .1, 'Lz': 1.}] + # # mapping = ['Cuboid', {'l1': 0., 'r1': 4., 'l2': 0., 'r2': 2., 'l3': 0., 'r3': 3.}] + # # mapping = ['Orthogonal', {'Lx': 4., 'Ly': 2., 'alpha': .1, 'Lz': 1.}] # mapping = ['Colella', {'Lx': 4., 'Ly': 2., 'alpha': .1, 'Lz': 1.}] - # test_poisson_2d(Nel, p, bc_type, mapping, show_plot=True) + # test_poisson_2d(Nel, p, bc_type, mapping, projected_rhs=True, show_plot=True) + + test_poisson_accum_1d(mapping, do_plot=True) diff --git a/src/struphy/topology/__init__.py b/src/struphy/topology/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/struphy/topology/grids.py b/src/struphy/topology/grids.py new file mode 100644 index 000000000..b26887326 --- /dev/null +++ b/src/struphy/topology/grids.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass + +import numpy as np + + +@dataclass +class TensorProductGrid: + """Grid as a tensor product of 1d grids. + + Parameters + ---------- + Nel : tuple[int] + Number of elements in each direction. + + mpi_dims_mask: Tuple of bool + True if the dimension is to be used in the domain decomposition (=default for each dimension). + If mpi_dims_mask[i]=False, the i-th dimension will not be decomposed. + """ + + Nel: tuple = (24, 10, 1) + mpi_dims_mask: tuple = (True, True, True) diff --git a/src/struphy/tutorials/tests/test_tutorials.py b/src/struphy/tutorials/tests/test_tutorials.py deleted file mode 100644 index c8de4d5c8..000000000 --- a/src/struphy/tutorials/tests/test_tutorials.py +++ /dev/null @@ -1,172 +0,0 @@ -import os - -import pytest -import yaml -from psydac.ddm.mpi import mpi as MPI - -import struphy -from struphy.main import main -from struphy.post_processing import pproc_struphy - -comm = MPI.COMM_WORLD -rank = comm.Get_rank() - -libpath = struphy.__path__[0] -i_path = os.path.join(libpath, "io", "inp") -o_path = os.path.join(libpath, "io", "out") - - -def test_tutorial_02(): - main( - "LinearMHDVlasovCC", - os.path.join(i_path, "tutorials", "params_02.yml"), - os.path.join(o_path, "tutorial_02"), - supress_out=True, - ) - - -def test_tutorial_03(): - main( - "LinearMHD", - os.path.join(i_path, "tutorials", "params_03.yml"), - os.path.join(o_path, "tutorial_03"), - supress_out=True, - ) - - comm.Barrier() - if rank == 0: - pproc_struphy.main(os.path.join(o_path, "tutorial_03"), physical=True) - - -def test_tutorial_04(fast): - main( - "Maxwell", - os.path.join(i_path, "tutorials", "params_04a.yml"), - os.path.join(o_path, "tutorial_04a"), - supress_out=True, - ) - - comm.Barrier() - if rank == 0: - pproc_struphy.main(os.path.join(o_path, "tutorial_04a")) - - main( - "LinearMHD", - os.path.join(i_path, "tutorials", "params_04b.yml"), - os.path.join(o_path, "tutorial_04b"), - supress_out=True, - ) - - comm.Barrier() - if rank == 0: - pproc_struphy.main(os.path.join(o_path, "tutorial_04b")) - - if not fast: - main( - "VariationalMHD", - os.path.join(i_path, "tutorials", "params_04c.yml"), - os.path.join(o_path, "tutorial_04c"), - supress_out=True, - ) - - comm.Barrier() - if rank == 0: - pproc_struphy.main(os.path.join(o_path, "tutorial_04c")) - - -def test_tutorial_05(): - main( - "Vlasov", - os.path.join(i_path, "tutorials", "params_05a.yml"), - os.path.join(o_path, "tutorial_05a"), - supress_out=True, - ) - - comm.Barrier() - if rank == 0: - pproc_struphy.main(os.path.join(o_path, "tutorial_05a")) - - main( - "Vlasov", - os.path.join(i_path, "tutorials", "params_05b.yml"), - os.path.join(o_path, "tutorial_05b"), - supress_out=True, - ) - - comm.Barrier() - if rank == 0: - pproc_struphy.main(os.path.join(o_path, "tutorial_05b")) - - main( - "GuidingCenter", - os.path.join(i_path, "tutorials", "params_05c.yml"), - os.path.join(o_path, "tutorial_05c"), - supress_out=True, - ) - - comm.Barrier() - if rank == 0: - pproc_struphy.main(os.path.join(o_path, "tutorial_05c")) - - main( - "GuidingCenter", - os.path.join(i_path, "tutorials", "params_05d.yml"), - os.path.join(o_path, "tutorial_05d"), - supress_out=True, - ) - - comm.Barrier() - if rank == 0: - pproc_struphy.main(os.path.join(o_path, "tutorial_05d")) - - main( - "GuidingCenter", - os.path.join(i_path, "tutorials", "params_05e.yml"), - os.path.join(o_path, "tutorial_05e"), - supress_out=True, - ) - - comm.Barrier() - if rank == 0: - pproc_struphy.main(os.path.join(o_path, "tutorial_05e")) - - main( - "GuidingCenter", - os.path.join(i_path, "tutorials", "params_05f.yml"), - os.path.join(o_path, "tutorial_05f"), - supress_out=True, - ) - - comm.Barrier() - if rank == 0: - pproc_struphy.main(os.path.join(o_path, "tutorial_05f")) - - -def test_tutorial_12(): - main( - "Vlasov", - os.path.join(i_path, "tutorials", "params_12a.yml"), - os.path.join(o_path, "tutorial_12a"), - save_step=100, - supress_out=True, - ) - - comm.Barrier() - if rank == 0: - pproc_struphy.main(os.path.join(o_path, "tutorial_12a")) - - main( - "GuidingCenter", - os.path.join(i_path, "tutorials", "params_12b.yml"), - os.path.join(o_path, "tutorial_12b"), - save_step=10, - supress_out=True, - ) - - comm.Barrier() - if rank == 0: - pproc_struphy.main(os.path.join(o_path, "tutorial_12b")) - - -if __name__ == "__main__": - test_tutorial_04(True) diff --git a/src/struphy/utils/arrays.py b/src/struphy/utils/arrays.py deleted file mode 100644 index ae2b9ef82..000000000 --- a/src/struphy/utils/arrays.py +++ /dev/null @@ -1,59 +0,0 @@ -import os -from types import ModuleType -from typing import TYPE_CHECKING, Literal - -BackendType = Literal["numpy", "cupy"] - - -class ArrayBackend: - def __init__( - self, - backend: BackendType = "numpy", - verbose: bool = False, - ) -> None: - assert backend.lower() in ["numpy", "cupy"], "Array backend must be either 'numpy' or 'cupy'." - - self._backend: BackendType = "cupy" if backend.lower() == "cupy" else "numpy" - - # Import numpy/cupy - if self.backend == "cupy": - try: - import cupy as cp - - self._xp = cp - except ImportError: - if verbose: - print("CuPy not available.") - self._backend = "numpy" - - if self.backend == "numpy": - import numpy as np - - self._xp = np - - assert isinstance(self.xp, ModuleType) - - if verbose: - print(f"Using {self.xp.__name__} backend.") - - @property - def backend(self) -> BackendType: - return self._backend - - @property - def xp(self) -> ModuleType: - return self._xp - - -# TODO: Make this configurable via environment variable or config file. -array_backend = ArrayBackend( - backend="cupy" if os.getenv("ARRAY_BACKEND", "numpy").lower() == "cupy" else "numpy", - verbose=True, -) - -# TYPE_CHECKING is True when type checking (e.g., mypy), but False at runtime. -# This allows us to use autocompletion for xp (i.e., numpy/cupy) as if numpy was imported. -if TYPE_CHECKING: - import numpy as xp -else: - xp = array_backend.xp diff --git a/src/struphy/utils/clone_config.py b/src/struphy/utils/clone_config.py index a8c5ac289..d23bee47c 100644 --- a/src/struphy/utils/clone_config.py +++ b/src/struphy/utils/clone_config.py @@ -1,8 +1,7 @@ +import cunumpy as xp from psydac.ddm.mpi import MockComm from psydac.ddm.mpi import mpi as MPI -from struphy.utils.arrays import xp as np - class CloneConfig: """ @@ -19,7 +18,7 @@ class CloneConfig: def __init__( self, comm: MPI.Intracomm, - params=None, + params: None, num_clones=1, ): """ @@ -28,8 +27,8 @@ def __init__( Parameters: comm : (MPI.Intracomm) The MPI communicator covering all processes. - params : dict, optional - Dictionary containing simulation parameters. + params : StruphyParameters + Struphy simulation parameters. num_clones : int, optional The number of clones to create. The total number of MPI ranks must be divisible by this number. """ @@ -122,10 +121,10 @@ def get_Np_global(self, species_name): if "Np" in markers: return markers["Np"] elif "ppc" in markers: - n_cells = np.prod(self.params["grid"]["Nel"], dtype=int) + n_cells = xp.prod(self.params["grid"]["Nel"], dtype=int) return int(markers["ppc"] * n_cells) elif "ppb" in markers: - n_boxes = np.prod(species["boxes_per_dim"], dtype=int) * self.num_clones + n_boxes = xp.prod(species["boxes_per_dim"], dtype=int) * self.num_clones return int(markers["ppb"] * n_boxes) def print_clone_config(self): @@ -210,7 +209,7 @@ def print_particle_config(self): row = f"{i_clone:6} " # Np = self.params["kinetic"][species_name]["markers"]["Np"] Np = self.get_Np_global(species_name) - n_cells_clone = np.prod(self.params["grid"]["Nel"]) + n_cells_clone = xp.prod(self.params["grid"]["Nel"]) Np_clone = self.get_Np_clone(Np, clone_id=i_clone) ppc_clone = Np_clone / n_cells_clone diff --git a/src/struphy/utils/cupy_vs_numpy.py b/src/struphy/utils/cupy_vs_numpy.py index d32044a58..43f214d73 100644 --- a/src/struphy/utils/cupy_vs_numpy.py +++ b/src/struphy/utils/cupy_vs_numpy.py @@ -1,6 +1,6 @@ import time -from arrays import xp +import cunumpy as xp def main(N=8192): diff --git a/src/struphy/utils/mpi.py b/src/struphy/utils/mpi.py deleted file mode 100644 index bb4eb10d4..000000000 --- a/src/struphy/utils/mpi.py +++ /dev/null @@ -1,102 +0,0 @@ -from dataclasses import dataclass -from time import time -from typing import TYPE_CHECKING - - -# Might not be needed -class MPICommWrapper: - def __init__(self, use_mpi=True): - self.use_mpi = use_mpi - if use_mpi: - from mpi4py import MPI - - self.comm = MPI.COMM_WORLD - else: - self.comm = MockComm() - - def __getattr__(self, name): - return getattr(self.comm, name) - - -class MockComm: - def __getattr__(self, name): - # Return a function that does nothing and returns None - def dummy(*args, **kwargs): - return None - - return dummy - - # Override some functions - def Get_rank(self): - return 0 - - def Get_size(self): - return 1 - - def Barrier(self): - return - - -class MPIwrapper: - def __init__(self, use_mpi: bool = False): - self.use_mpi = use_mpi - if use_mpi: - from mpi4py import MPI - - self._MPI = MPI - print("MPI is enabled") - else: - self._MPI = MockMPI() - print("MPI is NOT enabled") - - @property - def MPI(self): - return self._MPI - - -class MockMPI: - def __getattr__(self, name): - # Return a function that does nothing and returns None - def dummy(*args, **kwargs): - return None - - return dummy - - # Override some functions - @property - def COMM_WORLD(self): - return MockComm() - - # def comm_Get_rank(self): - # return 0 - - # def comm_Get_size(self): - # return 1 - - -try: - from mpi4py import MPI - - _comm = MPI.COMM_WORLD - rank = _comm.Get_rank() - size = _comm.Get_size() - mpi_enabled = size > 1 -except ImportError: - # mpi4py not installed - mpi_enabled = False -except Exception: - # mpi4py installed but not running under mpirun - mpi_enabled = False - -# TODO: add environment variable for mpi use -mpi_wrapper = MPIwrapper(use_mpi=mpi_enabled) - -# TYPE_CHECKING is True when type checking (e.g., mypy), but False at runtime. -if TYPE_CHECKING: - from mpi4py import MPI - - mpi = MPI -else: - mpi = mpi_wrapper.MPI - -print(f"{mpi = }") diff --git a/src/struphy/utils/utils.py b/src/struphy/utils/utils.py index f3d96c0f9..1707f0c07 100644 --- a/src/struphy/utils/utils.py +++ b/src/struphy/utils/utils.py @@ -61,19 +61,20 @@ def save_state(state, libpath=STRUPHY_LIBPATH): def print_all_attr(obj): """Print all object's attributes that do not start with "_" to screen.""" - from struphy.utils.arrays import xp as np + import cunumpy as xp for k in dir(obj): if k[0] != "_": v = getattr(obj, k) - if isinstance(v, np.ndarray): + if isinstance(v, xp.ndarray): v = f"{type(getattr(obj, k))} of shape {v.shape}" if "proj_" in k or "quad_grid_" in k: v = "(arrays not displayed)" print(k.ljust(26), v) -def dict_to_yaml(dictionary, output): +def dict_to_yaml(dictionary: dict, output: str): + """Write dictionary to file and save in output.""" with open(output, "w") as file: yaml.dump( dictionary, @@ -110,15 +111,15 @@ def refresh_models(): list_fluid = [] fluid_string = "" for name, obj in inspect.getmembers(fluid): - if inspect.isclass(obj): - if name not in {"StruphyModel", "Propagator"}: - list_fluid += [name] - fluid_string += '"' + name + '"\n' + if inspect.isclass(obj) and obj.__module__ == fluid.__name__: + # if name not in {"StruphyModel", "Propagator"}: + list_fluid += [name] + fluid_string += '"' + name + '"\n' list_kinetic = [] kinetic_string = "" for name, obj in inspect.getmembers(kinetic): - if inspect.isclass(obj): + if inspect.isclass(obj) and obj.__module__ == kinetic.__name__: if name not in {"StruphyModel", "Propagator"}: list_kinetic += [name] kinetic_string += '"' + name + '"\n' @@ -126,7 +127,7 @@ def refresh_models(): list_hybrid = [] hybrid_string = "" for name, obj in inspect.getmembers(hybrid): - if inspect.isclass(obj): + if inspect.isclass(obj) and obj.__module__ == hybrid.__name__: if name not in {"StruphyModel", "Propagator"}: list_hybrid += [name] hybrid_string += '"' + name + '"\n' @@ -134,7 +135,7 @@ def refresh_models(): list_toy = [] toy_string = "" for name, obj in inspect.getmembers(toy): - if inspect.isclass(obj): + if inspect.isclass(obj) and obj.__module__ == toy.__name__: if name not in {"StruphyModel", "Propagator"}: list_toy += [name] toy_string += '"' + name + '"\n' diff --git a/doc/tutorials/tutorial_11_data_structures.ipynb b/tutorial_07_data_structures.ipynb similarity index 76% rename from doc/tutorials/tutorial_11_data_structures.ipynb rename to tutorial_07_data_structures.ipynb index d59e3da86..62727c733 100644 --- a/doc/tutorials/tutorial_11_data_structures.ipynb +++ b/tutorial_07_data_structures.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 11 - Struphy data structures\n", + "# 7 - Struphy data structures\n", "\n", "In this tutorial we will learn about the data structures used in Struphy to store FEEC and particle variables. \n", "\n", @@ -874,7 +874,7 @@ "from struphy.pic import particles\n", "\n", "for name, obj in inspect.getmembers(particles):\n", - " if inspect.isclass(obj):\n", + " if inspect.isclass(obj) and obj.__module__ == particles.__name__:\n", " print(obj)" ] }, @@ -882,7 +882,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Next, we want to instantiate the model [Vlasov](https://struphy.pages.mpcdf.de/struphy/sections/models.html#struphy.models.toy.Vlasov) and then access its PIC data structures. For this we set the I/O paths to default and create the standard parameter files. We shall also consider the model [GuidingCenter](https://struphy.pages.mpcdf.de/struphy/sections/models.html#struphy.models.toy.GuidingCenter) as an example of a 5D PIC model:" + "We now create an instance of `Particles6D` with default parameters:" ] }, { @@ -891,108 +891,13 @@ "metadata": {}, "outputs": [], "source": [ - "!{'struphy --set-i d'}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!{'struphy params Vlasov -y'}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!{'struphy params GuidingCenter -y'}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us read-in the generated parameter files:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import struphy\n", - "import yaml\n", + "from struphy.pic.particles import Particles6D\n", + "from struphy.pic.utilities import LoadingParameters\n", "\n", - "vl_name = os.path.join(struphy.__path__[0], 'io/inp', 'params_Vlasov.yml')\n", - "dk_name = os.path.join(struphy.__path__[0], 'io/inp', 'params_GuidingCenter.yml')\n", - "\n", - "with open(vl_name) as file:\n", - " vl_params = yaml.load(file, Loader=yaml.FullLoader)\n", - " \n", - "with open(dk_name) as file:\n", - " dk_params = yaml.load(file, Loader=yaml.FullLoader)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Even before instantiating a model, we can look at its `species`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.models.toy import Vlasov, GuidingCenter\n", - "from psydac.ddm.mpi import mpi as MPI\n", + "loading_params = LoadingParameters(Np=120)\n", + "particles = Particles6D(loading_params=loading_params)\n", "\n", - "print(Vlasov.species()['kinetic'])\n", - "print(GuidingCenter.species()['kinetic'])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We now create an instance of `Vlasov` and initialize the variables (here just `ions`) from the default parameter file:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "comm = MPI.COMM_WORLD\n", - "\n", - "vlasov = Vlasov(vl_params, comm)\n", - "vlasov.initialize_from_params()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The screen output shows some information on the current instance of `Vlasov`. The corresponding instance of the `Particles6D` class can be accessed via the `pointer` attribute:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "p6d = vlasov.pointer['ions']\n", - "print(type(p6d))" + "particles.draw_markers()" ] }, { @@ -1008,17 +913,15 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{p6d.Np = }')\n", - "print(f'{p6d.markers.shape = }')\n", - "print(f'{p6d.markers_wo_holes.shape = }')" + "print(f'{particles.Np = }')\n", + "print(f'{particles.markers.shape = }')\n", + "print(f'{particles.markers_wo_holes.shape = }')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "In the class `Particles6D`, there can be at most 16 attributes attached ed to each marker, corresponding to the number of columns. Moreover, we see that the `markers` row number is larger than the amount of particles, in order not to run out of space during communication.\n", - "\n", "Let us look at some parameters/coordinates of the first five markers:" ] }, @@ -1028,203 +931,19 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'p6d.positions[:5] = \\n{p6d.positions[:5]}\\n')\n", - "print(f'p6d.velocities[:5] = \\n{p6d.velocities[:5]}\\n')\n", - "print(f'p6d.phasespace_coords[:5] = \\n{p6d.phasespace_coords[:5]}\\n')\n", - "print(f'{p6d.weights[:5] = }\\n')\n", - "print(f'{p6d.sampling_density[:5] = }\\n')\n", - "print(f'{p6d.weights0[:5] = }\\n')\n", - "print(f'{p6d.marker_ids[:5] = }')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Remark that the marker positions in Struphy are always in the unit cube `[0, 1]^3`. The above attributes of the `Particles` class have a setter method. The setter method only works for setting, for instance the `positions`, **for all markers** on the current process: " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "p6d.positions = np.zeros(p6d.positions.shape)\n", - "print(f'p6d.positions[:5] = \\n{p6d.positions[:5]}\\n')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If we want to set the positions of a selected group of markers, we have to do this by hand in the `markers` array. However, the column indices for `positions` can be accessed from the `index` dictionary:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "p6d.markers[2, p6d.index['pos']] = 1.\n", - "print(f'p6d.positions[:5] = \\n{p6d.positions[:5]}\\n')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If we want to set just one position coordinate of a specific particle, we can check the corresponding `index` and write directly into the markers array:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(p6d.index['pos'])\n", - "\n", - "p6d.markers[2, 1] = 2.\n", - "print(f'p6d.positions[:5] = \\n{p6d.positions[:5]}\\n')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### MPI sort markers\n", - "\n", - "Let look at the size of `Particles.markers` in a parallel run:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def show_shapes():\n", - "\n", - " import os\n", - " import struphy\n", - " import yaml\n", - " \n", - " from struphy.models.toy import Vlasov\n", - " from psydac.ddm.mpi import mpi as MPI\n", - "\n", - " vl_name = os.path.join(struphy.__path__[0], 'io/inp', 'params_Vlasov.yml')\n", - "\n", - " with open(vl_name) as file:\n", - " vl_params = yaml.load(file, Loader=yaml.FullLoader)\n", - " \n", - " comm = MPI.COMM_WORLD\n", - " rank = comm.Get_rank()\n", - "\n", - " vlasov = Vlasov(vl_params, comm)\n", - " vlasov.initialize_from_params()\n", - " \n", - " p6d = vlasov.pointer['ions']\n", - " \n", - " out = f'{rank = }, {p6d.Np = }, {p6d.markers.shape = }, {p6d.markers_wo_holes.shape = }, {p6d.n_mks_loc = }'\n", - "\n", - " #out = f'{rank = }, before update: {x0[s[0], s[1], :] = }:'\n", - "\n", - " return out\n", - "\n", - "with ipp.Cluster(engines='mpi', n=4) as rc:\n", - " view = rc.broadcast_view()\n", - " r = view.apply_sync(show_shapes)\n", - " print(\"\\n\".join(r))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here, the total number of markers drawn (on all processes) `n_mks` is 6720, the row-size of the markers array on each process is 2141, and the number of drawn markers on each process `n_mks_loc` is \n", - "\n", - "* 1691 on rank 0\n", - "* 1709 on rank 1\n", - "* 1646 on rank 2\n", - "* 1674 on rank 3\n", - "\n", - "The differences arise because of the random draw in the decomposed domain.\n", - "\n", - "Let us check how the `mpi_sort_markers` algorithm works:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def show_sorting():\n", - "\n", - " import os\n", - " import struphy\n", - " import yaml\n", - " \n", - " from struphy.models.toy import Vlasov\n", - " from psydac.ddm.mpi import mpi as MPI\n", - "\n", - " vl_name = os.path.join(struphy.__path__[0], 'io/inp', 'params_Vlasov.yml')\n", - "\n", - " with open(vl_name) as file:\n", - " vl_params = yaml.load(file, Loader=yaml.FullLoader)\n", - " \n", - " comm = MPI.COMM_WORLD\n", - " rank = comm.Get_rank()\n", - "\n", - " vlasov = Vlasov(vl_params, comm)\n", - " vlasov.initialize_from_params()\n", - " \n", - " p6d = vlasov.pointer['ions']\n", - " \n", - " out = f'\\n{rank = }, {p6d.domain_array[rank] = }, \\n\\ninitial array: p6d.markers[:5, :3] = \\n{p6d.markers[:5, :3]}'\n", - "\n", - " if rank == 0:\n", - " p6d.markers[1:3, 1] = 0.6\n", - " elif rank == 1:\n", - " p6d.markers[3, 1] = 0.1\n", - "\n", - " out += f'\\nafter update, before send: p6d.markers[:5, :3] = \\n{p6d.markers[:5, :3]}'\n", - "\n", - " p6d.mpi_sort_markers()\n", - " \n", - " out += f'\\nafter send: p6d.markers[:5, :3] = \\n{p6d.markers[:5, :3]}'\n", - "\n", - " return out\n", - "\n", - "with ipp.Cluster(engines='mpi', n=2) as rc:\n", - " view = rc.broadcast_view()\n", - " r = view.apply_sync(show_sorting)\n", - " print(\"\\n\".join(r))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here, the `domain_array` has to be read as follows: \n", - "\n", - "* In the first direction, the domain is `[0., 1.]` with 12 elements on both processes\n", - "* In the second direction, the domain is `[0, .5]` on rank 0 and `[.5, 1.]` on rank 1, both with 7 elements\n", - "* In the third direction, the domain is `[0., 1.]` with 4 elements on both processes\n", - "\n", - "Therefore, after the initial drawing, rank 0 holds only markers with $\\eta_2 < 0.5$ and rank 1 holds only markers with $\\eta_2 > 0.5$, which is confirmed by looking at the second column of the `initial_arrays`.\n", - "\n", - "We now update $\\eta_2 = 0.6$ for the 2nd and the 3rd marker on rank 0, and $\\eta_2 = 0.1$ for the 4th marker on rank 1. These markers are thus not on the correct process anymore, according to `domain_array`. \n", - "\n", - "After `mpi_sort_markers`, the 2nd and 3rd marker are sent from ranks `0 -> 1` and the 4th marker is sent from ranks `1 -> 0`. The latter fills the hole in the 2nd row on rank 0, however the hole in the 3rd row (designated with `-1.`) remains open, because there is no incoming marker to fill it. " + "print(f'{particles.positions[:5] = }\\n')\n", + "print(f'{particles.velocities[:5] = }\\n')\n", + "print(f'{particles.phasespace_coords[:5] = }\\n')\n", + "print(f'{particles.weights[:5] = }\\n')\n", + "print(f'{particles.sampling_density[:5] = }\\n')\n", + "print(f'{particles.weights0[:5] = }\\n')\n", + "print(f'{particles.marker_ids[:5] = }')" ] } ], "metadata": { "kernelspec": { - "display_name": "tenv", + "display_name": "env", "language": "python", "name": "python3" }, @@ -1238,7 +957,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/tutorials/tutorial_01_parameter_files.ipynb b/tutorials/tutorial_01_parameter_files.ipynb new file mode 100644 index 000000000..7621ed6b1 --- /dev/null +++ b/tutorials/tutorial_01_parameter_files.ipynb @@ -0,0 +1,415 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# 1 - Parameters and `struphy.main`\n", + "\n", + "Struphy is a collection of \"models\". Each model is a parallelized simulation code for a physics model described py a set partial differntial equations (PDEs).\n", + "Check the documentation for a [list of currently available models](https://struphy.pages.mpcdf.de/struphy/sections/models.html).\n", + "A model is launched through its parameter file, where all simulation parameters can be specified.\n", + "\n", + "Model parameter files are Python scripts (.py) that can be executed with the Python interpreter.\n", + "For each `MODEL`, the default parameter file can be generated from the console:\n", + "\n", + "```\n", + "struphy params MODEL\n", + "```\n", + "\n", + "This will create a file `params_MODEL.py` in the current working directory. To run the model type\n", + "\n", + "```\n", + "python3 params_MODEL.py\n", + "```\n", + "\n", + "One can modify the parameter file to launch a specific simulation.\n", + "For example, the parameter file of the model [Vlasov](https://struphy.pages.mpcdf.de/struphy/sections/subsections/models_toy.html#struphy.models.toy.Vlasov) can be generated from\n", + "\n", + "```\n", + "struphy params Vlasov\n", + "```\n", + "\n", + "To see its contents, open the file in your preferred editor or type\n", + "\n", + "```\n", + "cat params_Vlasov.py\n", + "```\n", + "\n", + "We shall discuss this parameter file in what follows. Parameter files of all models have a similar structure.\n", + "\n", + "## Part 1: Imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", + "from struphy.geometry import domains\n", + "from struphy.fields_background import equils\n", + "from struphy.topology import grids\n", + "from struphy.io.options import DerhamOptions\n", + "from struphy.io.options import FieldsBackground\n", + "from struphy.initial import perturbations\n", + "from struphy.kinetic_background import maxwellians\n", + "from struphy.pic.utilities import (LoadingParameters, \n", + " WeightsParameters, \n", + " BoundaryParameters,\n", + " BinningPlot,\n", + " KernelDensityPlot,\n", + " )\n", + "from struphy import main\n", + "\n", + "# import model, set verbosity\n", + "from struphy.models.toy import Vlasov" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "All parameter files import the modules listed above, even though some of them might not be needed in a specific model. \n", + "The last import imports the model itself.\n", + "\n", + "## Part 2: Generic options\n", + "\n", + "The following lines refer to options that can be set for any model. These are:\n", + "\n", + "* Environment options (paths, saving, domain cloning)\n", + "* Model units\n", + "* Time options\n", + "* Problem geometry (mapped domain)\n", + "* Static background (equilibrium) \n", + "* Grid \n", + "* Derham complex\n", + "\n", + "Check the respective classes for possible options." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "# environment options\n", + "env = EnvironmentOptions()\n", + "\n", + "# units\n", + "base_units = BaseUnits()\n", + "\n", + "# time stepping\n", + "time_opts = Time(dt=0.2, Tend=10.0)\n", + "\n", + "# geometry\n", + "l1 = -5.0\n", + "r1 = 5.0\n", + "l2 = -7.0\n", + "r2 = 7.0\n", + "l3 = -1.0\n", + "r3 = 1.0\n", + "domain = domains.Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)\n", + "\n", + "# fluid equilibrium (can be used as part of initial conditions)\n", + "equil = None\n", + "\n", + "# grid\n", + "grid = grids.TensorProductGrid()\n", + "\n", + "# derham options\n", + "derham_opts = DerhamOptions()" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "## Part 3: Model instance\n", + "\n", + "A model has a predefined number of \"species\". Each species is a collection of \"variables\", which are the unknowns of the model.\n", + "In the parameter file, a light-weight instance of the model is created, without allocating memory. \n", + "The light-weight instance is used to set some model-specific parameters for each species.\n", + "\n", + "For instance, for each species one can set its charge- and mass number:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "# light-weight model instance\n", + "model = Vlasov()\n", + "\n", + "# species parameters\n", + "model.kinetic_ions.set_phys_params(charge_number=3, mass_number=12)" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "In case of a kinetic species, one can also set parameters regarding marker drawing, box sorting and data saving: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "loading_params = LoadingParameters(Np=15)\n", + "weights_params = WeightsParameters()\n", + "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", + "model.kinetic_ions.set_markers(loading_params=loading_params, \n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params)\n", + "\n", + "model.kinetic_ions.set_sorting_boxes()\n", + "model.kinetic_ions.set_save_data(n_markers=1.0)" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## Part 4: Propagator options\n", + "\n", + "A model is a collection of \"propagators\" which perform the time stepping. Each propagator refers to part of a single time step and can be viewed as a splitting step. For instance, if only a single propagator is present in a model, then all variables of the model are updated by this propagator. If two or more propagators are present, they are executed in sequence, according to the chosen splitting algorithm (Lie-Trotter, Strang, etc.).\n", + "\n", + "Each propagator has options that can be set in the parameter file as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "# propagator options\n", + "model.propagators.push_vxb.options = model.propagators.push_vxb.Options()\n", + "model.propagators.push_eta.options = model.propagators.push_eta.Options()" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## Part 5: Initial conditions\n", + "\n", + "One can use the methods `Variable.add_background()` and `Variable.add_perturbation()` to set initial conditions for each variable of a species. Variables that are not specified are intialized as zero." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "# initial conditions (background + perturbation)\n", + "perturbation = None\n", + "\n", + "background = maxwellians.Maxwellian3D(n=(1.0, perturbation))\n", + "model.kinetic_ions.var.add_background(background)" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "## Part 6: `main.run`\n", + "\n", + "In the final part of the parameter file, the `main.run` command is invoked. This command will allocate memory and run the specified simulation. The run command is not executed when the parameter file is imported in another Python script. The `verbose` flag controls the screen output during the simulation run." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "verbose = True\n", + "\n", + "main.run(model, \n", + " params_path=None, \n", + " env=env, \n", + " base_units=base_units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## Post processing: `main.pproc`\n", + "\n", + "Aside from `run`, the Struphy `main` module has also a `pproc` routine for post-processing of raw simulation data:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "path = os.path.join(os.getcwd(), \"sim_1\")\n", + "\n", + "main.pproc(path)" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "One can also post-process directly from the console:\n", + "\n", + "```\n", + "struphy pproc sim_1\n", + "```\n", + "\n", + "Type \n", + "\n", + "```\n", + "struphy pproc -h\n", + "```\n", + "\n", + "for more info on this command." + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "## Loading data: `main.load_data`\n", + "\n", + "After post-processing, the generated data can be loaded via `main.load_data`. This function returns a `SimData` object, which you can inspect to get further info on possible data to load." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "simdata = main.load_data(path)" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "## Plotting particle orbits\n", + "\n", + "In this example, for the species `kinetic_ions` some particle orbits have been saved. Under `simdata.orbits[]` one finds a three-dimensional numpy array; the first index refers to the time step, the second index to the particle and the third index to the particel attribute. The first three attributes are the particle positions, followed by the velocities and the (initial and time-dependent) weights." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "orbits = simdata.orbits[\"kinetic_ions\"]\n", + "\n", + "Nt = simdata.Nt[\"kinetic_ions\"]\n", + "Np = simdata.Np[\"kinetic_ions\"]\n", + "Nattr = simdata.Nattr[\"kinetic_ions\"]" + ] + }, + { + "cell_type": "markdown", + "id": "21", + "metadata": {}, + "source": [ + "Let us plot the orbits:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "import numpy as np\n", + "\n", + "fig = plt.figure()\n", + "ax = fig.gca()\n", + "\n", + "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "\n", + "# create alpha for color scaling\n", + "Tend = time_opts.Tend\n", + "alpha = np.linspace(1., 0., Nt + 1)\n", + "\n", + "# loop through particles, plot all time steps\n", + "for i in range(Np):\n", + " ax.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], alpha=alpha)\n", + " \n", + "ax.plot([l1, l1], [l2, r2], 'k')\n", + "ax.plot([r1, r1], [l2, r2], 'k')\n", + "ax.plot([l1, r1], [l2, l2], 'k')\n", + "ax.plot([l1, r1], [r2, r2], 'k')\n", + "ax.set_xlabel('x')\n", + "ax.set_ylabel('y')\n", + "ax.set_xlim(-6.5, 6.5)\n", + "ax.set_ylim(-9, 9)\n", + "ax.set_title(f'{int(Nt - 1)} time steps (full color at t=0)');" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/tutorial_02_test_particles.ipynb b/tutorials/tutorial_02_test_particles.ipynb new file mode 100644 index 000000000..e95b6df81 --- /dev/null +++ b/tutorials/tutorial_02_test_particles.ipynb @@ -0,0 +1,1308 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# 2 - Test particles\n", + "\n", + "Let us explore some options of the models [Vlasov](https://struphy.pages.mpcdf.de/struphy/sections/subsections/models_toy.html#struphy.models.toy.Vlasov), and [GuidingCenter](https://struphy.pages.mpcdf.de/struphy/sections/subsections/models_toy.html#struphy.models.toy.GuidingCenter). In particular, we will\n", + "\n", + "1. Change the geometry\n", + "2. Change the loading of the markers\n", + "3. Set a static background magnetic field\n", + "\n", + "## Particles in a cylinder\n", + "\n", + "As in Tutorial 1, we shall re-create the parameter files in the notebook for the purpose of discussion. The default parameter files can be created from the console via\n", + "\n", + "```\n", + "struphy params Vlasov\n", + "struphy params GuidingCenter \n", + "```\n", + "\n", + "This time, we set the simulation domain $\\Omega$ to be a cylinder. We explore two options for drawing markers in posittion space:\n", + "\n", + "- uniform in logical space $[0, 1]^3 = F^{-1}(\\Omega)$\n", + "- uniform on the cylinder $\\Omega$\n", + "\n", + "We start with the generic imports:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", + "from struphy.geometry import domains\n", + "from struphy.fields_background import equils\n", + "from struphy.topology import grids\n", + "from struphy.io.options import DerhamOptions\n", + "from struphy.io.options import FieldsBackground\n", + "from struphy.initial import perturbations\n", + "from struphy.kinetic_background import maxwellians\n", + "from struphy.pic.utilities import (LoadingParameters, \n", + " WeightsParameters, \n", + " BoundaryParameters,\n", + " BinningPlot,\n", + " KernelDensityPlot,\n", + " )\n", + "from struphy import main\n", + "\n", + "# import model, set verbosity\n", + "from struphy.models.toy import Vlasov" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "We shall create two simulations, which we store in different output folders, defined throught the environment variables:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "# environment options\n", + "env = EnvironmentOptions()\n", + "env_2 = EnvironmentOptions(sim_folder=\"sim_2\")" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "The other generic options will be the same for both simulations. Here, we just perform one time step und load a cylindrical geometry:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "# units\n", + "base_units = BaseUnits()\n", + "\n", + "# time stepping\n", + "time_opts = Time(dt=0.2, Tend=0.2)\n", + "\n", + "# geometry\n", + "a1 = 0.\n", + "a2 = 5.\n", + "Lz = 20.\n", + "domain = domains.HollowCylinder(a1=a1, a2=a2, Lz=Lz)" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "We can already look a t the simulation domain:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "domain.show()" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "We can leave the equilibrium, grid and Derham complex empty:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "# fluid equilibrium (can be used as part of initial conditions)\n", + "equil = None\n", + "\n", + "# grid\n", + "grid = grids.TensorProductGrid()\n", + "\n", + "# derham options\n", + "derham_opts = DerhamOptions()" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "For each simulation, we must create the light-weight model instance and set parameters:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "# light-weight model instance\n", + "model = Vlasov()\n", + "model_2 = Vlasov()\n", + "\n", + "# species parameters\n", + "model.kinetic_ions.set_phys_params()\n", + "model_2.kinetic_ions.set_phys_params()" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "For the second simulation, in the parameters for particle loading we choose `spatial=\"disc\"` in order to draw uniformly on the cross section of the cylinder: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "loading_params = LoadingParameters(Np=1000)\n", + "loading_params_2 = LoadingParameters(Np=1000, spatial=\"disc\")\n", + "\n", + "weights_params = WeightsParameters()\n", + "boundary_params = BoundaryParameters()\n", + "\n", + "model.kinetic_ions.set_markers(loading_params=loading_params, \n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params)\n", + "model_2.kinetic_ions.set_markers(loading_params=loading_params_2, \n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params)\n", + "\n", + "model.kinetic_ions.set_sorting_boxes()\n", + "model_2.kinetic_ions.set_sorting_boxes()\n", + "\n", + "model.kinetic_ions.set_save_data(n_markers=1.0)\n", + "model_2.kinetic_ions.set_save_data(n_markers=1.0)" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "Propagator options and initial conditions shall be the same in both simulations:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "# propagator options\n", + "model.propagators.push_vxb.options = model.propagators.push_vxb.Options()\n", + "model.propagators.push_eta.options = model.propagators.push_eta.Options()\n", + "\n", + "model_2.propagators.push_vxb.options = model_2.propagators.push_vxb.Options()\n", + "model_2.propagators.push_eta.options = model_2.propagators.push_eta.Options()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "# initial conditions (background + perturbation)\n", + "perturbation = None\n", + "background = maxwellians.Maxwellian3D(n=(1.0, perturbation))\n", + "\n", + "model.kinetic_ions.var.add_background(background)\n", + "model_2.kinetic_ions.var.add_background(background)" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "Let us now run the first simulation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "verbose = False\n", + "\n", + "main.run(model, \n", + " params_path=None, \n", + " env=env, \n", + " base_units=base_units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "And now the second simulation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "main.run(model_2, \n", + " params_path=None, \n", + " env=env_2, \n", + " base_units=base_units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "21", + "metadata": {}, + "source": [ + "We now post-process both runs, load the generated data and plot the initial particle positions on a cross section of the cylinder:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "path = os.path.join(os.getcwd(), \"sim_1\")\n", + "path_2 = os.path.join(os.getcwd(), \"sim_2\")\n", + "\n", + "main.pproc(path)\n", + "main.pproc(path_2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "simdata = main.load_data(path)\n", + "simdata_2 = main.load_data(path_2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "\n", + "fig = plt.figure(figsize=(10, 6)) \n", + "\n", + "orbits = simdata.orbits[\"kinetic_ions\"]\n", + "orbits_uni = simdata_2.orbits[\"kinetic_ions\"]\n", + "\n", + "# orbits = simdata.pic_species[\"kinetic_ions\"][\"orbits\"]\n", + "# orbits_uni = simdata_2.pic_species[\"kinetic_ions\"][\"orbits\"]\n", + "\n", + "plt.subplot(1, 2, 1)\n", + "plt.scatter(orbits[0, :, 0], orbits[0, :, 1], s=2.)\n", + "circle1 = plt.Circle((0, 0), a2, color='k', fill=False)\n", + "ax = plt.gca()\n", + "ax.add_patch(circle1)\n", + "ax.set_aspect('equal')\n", + "plt.xlabel('x')\n", + "plt.ylabel('y')\n", + "plt.title('sim_1: draw uniform in logical space')\n", + "\n", + "plt.subplot(1, 2, 2)\n", + "plt.scatter(orbits_uni[0, :, 0], orbits_uni[0, :, 1], s=2.)\n", + "circle2 = plt.Circle((0, 0), a2, color='k', fill=False)\n", + "ax = plt.gca()\n", + "ax.add_patch(circle2)\n", + "ax.set_aspect('equal')\n", + "plt.xlabel('x')\n", + "plt.ylabel('y')\n", + "plt.title('sim_2: draw uniform on disc');" + ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "## Reflecting boundary conditions \n", + "\n", + "Let us now run for 50 time steps and with 15 particles in the cylinder.\n", + "Moreover, we set reflecting boundary conditions in radial direction, which in Struphy is always the logical direction $\\eta_1$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "time_opts = Time(dt=0.2, Tend=10.0)\n", + "loading_params = LoadingParameters(Np=15, spatial=\"disc\")\n", + "boundary_params = BoundaryParameters(bc=('reflect', 'periodic', 'periodic'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "# light-weight model instance\n", + "model = Vlasov()\n", + "\n", + "# species parameters\n", + "model.kinetic_ions.set_phys_params()\n", + "\n", + "model.kinetic_ions.set_markers(loading_params=loading_params, \n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params)\n", + "model.kinetic_ions.set_sorting_boxes()\n", + "model.kinetic_ions.set_save_data(n_markers=1.0)" + ] + }, + { + "cell_type": "markdown", + "id": "28", + "metadata": {}, + "source": [ + "We still have to set the propagator options and the initial conditions:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "# propagator options\n", + "model.propagators.push_vxb.options = model.propagators.push_vxb.Options()\n", + "model.propagators.push_eta.options = model.propagators.push_eta.Options()\n", + "\n", + "# initial conditions (background + perturbation)\n", + "perturbation = None\n", + "background = maxwellians.Maxwellian3D(n=(1.0, perturbation))\n", + "\n", + "model.kinetic_ions.var.add_background(background)" + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "We can now run the simulation, then post-process the data and plot the resulting orbits:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "verbose = False\n", + "\n", + "main.run(model, \n", + " params_path=None, \n", + " env=env, \n", + " base_units=base_units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": {}, + "outputs": [], + "source": [ + "path = os.path.join(os.getcwd(), \"sim_1\")\n", + "main.pproc(path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": {}, + "outputs": [], + "source": [ + "simdata = main.load_data(path)" + ] + }, + { + "cell_type": "markdown", + "id": "34", + "metadata": {}, + "source": [ + "Under `simdata.orbits[]` one finds a three-dimensional numpy array; the first index refers to the time step, the second index to the particle and the third index to the particel attribute. The first three attributes are the partciel positions, followed by the velocities and the (initial and time-dependent) weights." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "orbits = simdata.orbits[\"kinetic_ions\"]\n", + "\n", + "Nt = simdata.Nt[\"kinetic_ions\"]\n", + "Np = simdata.Np[\"kinetic_ions\"]\n", + "Nattr = simdata.Nattr[\"kinetic_ions\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "fig = plt.figure()\n", + "ax = fig.gca()\n", + "\n", + "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "\n", + "# create alpha for color scaling\n", + "Tend = time_opts.Tend\n", + "alpha = np.linspace(1., 0., Nt + 1)\n", + "\n", + "# loop through particles, plot all time steps\n", + "for i in range(Np):\n", + " ax.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], alpha=alpha)\n", + " \n", + "circle1 = plt.Circle((0, 0), a2, color='k', fill=False)\n", + "\n", + "ax.add_patch(circle1)\n", + "ax.set_aspect('equal')\n", + "ax.set_xlabel('x')\n", + "ax.set_ylabel('y')\n", + "ax.set_title(f'{Nt - 1} time steps (full color at t=0)');" + ] + }, + { + "cell_type": "markdown", + "id": "37", + "metadata": {}, + "source": [ + "## Particles in a cylinder with a magnetic field\n", + "\n", + "Let us add a magnetic field to the simulation. This can be done by setting an [MHDequilibrium](https://struphy.pages.mpcdf.de/struphy/sections/subsections/mhd_equils.html#mhd-equilibria):\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38", + "metadata": {}, + "outputs": [], + "source": [ + "B0x = 0.\n", + "B0y = 0.\n", + "B0z = 1.\n", + "equil = equils.HomogenSlab(B0x=B0x, B0y=B0y, B0z=B0z)" + ] + }, + { + "cell_type": "markdown", + "id": "39", + "metadata": {}, + "source": [ + "In order to project the equilibrium on the spline basis for fast evaluation in the particle kernels, we need a Derham complex:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": {}, + "outputs": [], + "source": [ + "spl_kind = (False, True, True)\n", + "derham_opts = DerhamOptions(spl_kind=spl_kind)" + ] + }, + { + "cell_type": "markdown", + "id": "41", + "metadata": {}, + "source": [ + "Now we create the light-weight instance of the model and set the species options. We shall `remove` particles that hit the boundary in $\\eta_1$ (radial) direction:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42", + "metadata": {}, + "outputs": [], + "source": [ + "# light-weight model instance\n", + "model = Vlasov()\n", + "\n", + "# species parameters\n", + "model.kinetic_ions.set_phys_params()\n", + "\n", + "loading_params = LoadingParameters(Np=20)\n", + "boundary_params = BoundaryParameters(bc=('remove', 'periodic', 'periodic'))\n", + "model.kinetic_ions.set_markers(loading_params=loading_params, \n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params)\n", + "model.kinetic_ions.set_sorting_boxes()\n", + "model.kinetic_ions.set_save_data(n_markers=1.0)\n", + "\n", + "# propagator options\n", + "model.propagators.push_vxb.options = model.propagators.push_vxb.Options()\n", + "model.propagators.push_eta.options = model.propagators.push_eta.Options()\n", + "\n", + "# initial conditions (background + perturbation)\n", + "perturbation = None\n", + "background = maxwellians.Maxwellian3D(n=(1.0, perturbation))\n", + "\n", + "model.kinetic_ions.var.add_background(background)" + ] + }, + { + "cell_type": "markdown", + "id": "43", + "metadata": {}, + "source": [ + "Now the usual procedure: run, post-process, load data and finally plot the orbits:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44", + "metadata": {}, + "outputs": [], + "source": [ + "# run\n", + "verbose = False\n", + "\n", + "main.run(model, \n", + " params_path=None, \n", + " env=env, \n", + " base_units=base_units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45", + "metadata": {}, + "outputs": [], + "source": [ + "path = os.path.join(os.getcwd(), \"sim_1\")\n", + "main.pproc(path)\n", + "\n", + "simdata = main.load_data(path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46", + "metadata": {}, + "outputs": [], + "source": [ + "orbits = simdata.orbits[\"kinetic_ions\"]\n", + "\n", + "Nt = simdata.Nt[\"kinetic_ions\"]\n", + "Np = simdata.Np[\"kinetic_ions\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47", + "metadata": {}, + "outputs": [], + "source": [ + "fig = plt.figure()\n", + "ax = fig.gca()\n", + "\n", + "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "\n", + "# create alpha for color scaling\n", + "Tend = time_opts.Tend\n", + "alpha = np.linspace(1., 0., Nt + 1)\n", + "\n", + "# loop through particles, plot all time steps\n", + "for i in range(Np):\n", + " ax.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], alpha=alpha)\n", + " \n", + "circle1 = plt.Circle((0, 0), a2, color='k', fill=False)\n", + "\n", + "ax.add_patch(circle1)\n", + "ax.set_aspect('equal')\n", + "ax.set_xlabel('x')\n", + "ax.set_ylabel('y')\n", + "ax.set_title(f'{int(Nt - 1)} time steps (full color at t=0)');" + ] + }, + { + "cell_type": "markdown", + "id": "48", + "metadata": {}, + "source": [ + "## Particles in a Tokamak equilibrium\n", + "\n", + "Let us try a more complicated [MHDequilibrium](https://struphy.pages.mpcdf.de/struphy/sections/subsections/mhd_equils.html#mhd-equilibria), namely from an ASDEX-Upgrade equilibrium stored in an EQDSK file. We instatiate an [EQDSKequilibrium](https://struphy.pages.mpcdf.de/struphy/sections/subsections/mhd_equils_sub.html#struphy.fields_background.mhd_equil.equils.EQDSKequilibrium) with many of its default parameters, except for the density:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49", + "metadata": {}, + "outputs": [], + "source": [ + "n1 = 0.\n", + "n2 = 0.\n", + "na = 1.\n", + "equil = equils.EQDSKequilibrium(n1=n1, n2=n2, na=na)" + ] + }, + { + "cell_type": "markdown", + "id": "50", + "metadata": {}, + "source": [ + "Since [EQDSKequilibrium](https://struphy.pages.mpcdf.de/struphy/sections/subsections/mhd_equils_sub.html#struphy.fields_background.mhd_equil.equils.EQDSKequilibrium) is an [AxisymmMHDequilibrium](https://struphy.pages.mpcdf.de/struphy/sections/subsections/mhd_equils_sub.html#struphy.fields_background.mhd_equil.base.AxisymmMHDequilibrium), which in turn is a [CartesianMHDequilibrium](https://struphy.pages.mpcdf.de/struphy/sections/subsections/mhd_equils_sub.html#struphy.fields_background.mhd_equil.base.CartesianMHDequilibrium), we are free to choose any mapping for the simulation (e.g. a Cuboid for Cartesian coordinates). In order to be conforming to the boundary of the equilibrium, we shall choose the [Tokamak](https://struphy.pages.mpcdf.de/struphy/sections/subsections/domains_avail.html#struphy.geometry.domains.Tokamak) mapping:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51", + "metadata": {}, + "outputs": [], + "source": [ + "Nel = (28, 72)\n", + "p = (3, 3)\n", + "psi_power = 0.6\n", + "psi_shifts = (1e-6, 1.)\n", + "domain = domains.Tokamak(equilibrium=equil, \n", + " Nel=Nel,\n", + " p=p,\n", + " psi_power=psi_power,\n", + " psi_shifts=psi_shifts)" + ] + }, + { + "cell_type": "markdown", + "id": "52", + "metadata": {}, + "source": [ + "The [Tokamak](https://struphy.pages.mpcdf.de/struphy/sections/subsections/domains_avail.html#struphy.geometry.domains.Tokamak) domain is a [PoloidalSplineTorus](https://struphy.pages.mpcdf.de/struphy/sections/subsections/domains_base.html#struphy.geometry.base.PoloidalSplineTorus), hence\n", + "\n", + "$$\n", + " \\begin{align*}\n", + " x &= R \\cos(\\phi)\\,,\n", + " \\\\\n", + " y &= -R \\sin(\\phi)\\,,\n", + " \\\\\n", + " z &= Z\\,,\n", + " \\end{align*}\n", + "$$\n", + "\n", + "between Cartesian $(x, y, z)$- and Tokamak $(R, Z, \\phi)$-coordinates holds, where $(R, Z)$ spans a poloidal plane. Moreover, the Tokamak coordinates are related to general torus coordinates $(r, \\theta, \\phi)$ via a polar mapping in the poloidal plane:\n", + "\n", + "$$\n", + " \\begin{align*}\n", + " R &= R_0 + r \\cos(\\theta)\\,,\n", + " \\\\\n", + " Z &= r \\sin(\\theta)\\,,\n", + " \\\\\n", + " \\phi &= \\phi\\,.\n", + " \\end{align*}\n", + "$$\n", + "\n", + "The torus coordinates are related to Struphy logical coordinates $\\boldsymbol \\eta = (\\eta_1, \\eta_2, \\eta_3) \\in [0, 1]^3$ as \n", + "\n", + "$$\n", + " \\begin{align*}\n", + " r &= a_1 + (a_2 - a_1) \\eta_1\\,,\n", + " \\\\\n", + " \\theta &= 2\\pi \\eta_2\\,,\n", + " \\\\\n", + " \\phi &= 2\\pi \\eta_3\\,,\n", + " \\end{align*}\n", + "$$\n", + "\n", + "where $a_2 > a_1 \\geq 0$ are boundaries in the radial $r$-direction.\n", + "This can be seen for instance in the [HollowTorus](https://struphy.pages.mpcdf.de/struphy/sections/subsections/domains_avail.html#struphy.geometry.domains.HollowTorus) mapping (more complicated angle parametrizations $\\theta(\\eta_1, \\eta_2)$ are also available, but not discussed here).\n", + "\n", + "Let us plot the equilibrium magnetic field strength:\n", + "\n", + "1. in the poloidal plane at $\\phi = 0$\n", + "2. in the top view at $z = 0$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "# logical grid on the unit cube\n", + "e1 = np.linspace(0., 1., 101)\n", + "e2 = np.linspace(0., 1., 101)\n", + "e3 = np.linspace(0., 1., 101)\n", + "\n", + "# move away from the singular point r = 0\n", + "e1[0] += 1e-5" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54", + "metadata": {}, + "outputs": [], + "source": [ + "# logical coordinates of the poloidal plane at phi = 0\n", + "eta_poloidal = (e1, e2, 0.)\n", + "# logical coordinates of the top view at theta = 0\n", + "eta_topview_1 = (e1, 0., e3)\n", + "# logical coordinates of the top view at theta = pi\n", + "eta_topview_2 = (e1, .5, e3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55", + "metadata": {}, + "outputs": [], + "source": [ + "# Cartesian coordinates (squeezed)\n", + "x_pol, y_pol, z_pol = domain(*eta_poloidal, squeeze_out=True)\n", + "x_top1, y_top1, z_top1 = domain(*eta_topview_1, squeeze_out=True)\n", + "x_top2, y_top2, z_top2 = domain(*eta_topview_2, squeeze_out=True)\n", + "\n", + "print(f'{x_pol.shape = }')\n", + "print(f'{x_top1.shape = }')\n", + "print(f'{x_top2.shape = }')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56", + "metadata": {}, + "outputs": [], + "source": [ + "# generate two axes\n", + "fig, axs = plt.subplots(2, 1, figsize=(8, 16))\n", + "ax = axs[0]\n", + "ax_top = axs[1]\n", + "\n", + "# min/max of field strength\n", + "equil.domain = domain\n", + "Bmax = np.max(equil.absB0(*eta_topview_2, squeeze_out=True))\n", + "Bmin = np.min(equil.absB0(*eta_topview_1, squeeze_out=True))\n", + "levels = np.linspace(Bmin, Bmax, 51)\n", + "\n", + "# absolute magnetic field at phi = 0\n", + "im = ax.contourf(x_pol, z_pol, equil.absB0(*eta_poloidal, squeeze_out=True), levels=levels)\n", + "\n", + "# absolute magnetic field at Z = 0\n", + "im_top = ax_top.contourf(x_top1, y_top1, equil.absB0(*eta_topview_1, squeeze_out=True), levels=levels)\n", + "ax_top.contourf(x_top2, y_top2, equil.absB0(*eta_topview_2, squeeze_out=True), levels=levels)\n", + "\n", + "# last closed flux surface, poloidal\n", + "ax.plot(x_pol[-1], z_pol[-1], color='k')\n", + "\n", + "# last closed flux surface, toroidal\n", + "ax_top.plot(x_top1[-1], y_top1[-1], color='k')\n", + "ax_top.plot(x_top2[-1], y_top2[-1], color='k')\n", + "\n", + "# limiter, poloidal\n", + "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, 'tab:orange')\n", + "ax.axis('equal')\n", + "ax.set_xlabel('R')\n", + "ax.set_ylabel('Z')\n", + "ax.set_title('abs(B) at $\\phi=0$')\n", + "fig.colorbar(im);\n", + "\n", + "# limiter, toroidal\n", + "limiter_Rmax = np.max(equil.limiter_pts_R)\n", + "limiter_Rmin = np.min(equil.limiter_pts_R)\n", + "\n", + "thetas = 2*np.pi*e2\n", + "limiter_x_max = limiter_Rmax * np.cos(thetas)\n", + "limiter_y_max = - limiter_Rmax * np.sin(thetas)\n", + "limiter_x_min = limiter_Rmin * np.cos(thetas)\n", + "limiter_y_min = - limiter_Rmin * np.sin(thetas)\n", + "\n", + "ax_top.plot(limiter_x_max, limiter_y_max, 'tab:orange')\n", + "ax_top.plot(limiter_x_min, limiter_y_min, 'tab:orange')\n", + "ax_top.axis('equal')\n", + "ax_top.set_xlabel('x')\n", + "ax_top.set_ylabel('y')\n", + "ax_top.set_title('abs(B) at $Z=0$')\n", + "fig.colorbar(im_top);" + ] + }, + { + "cell_type": "markdown", + "id": "57", + "metadata": {}, + "source": [ + "We now set up a simualtion of 4 specific particle orbits in this equilibrium:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58", + "metadata": {}, + "outputs": [], + "source": [ + "# light-weight model instance\n", + "model = Vlasov()\n", + "\n", + "# species parameters\n", + "model.kinetic_ions.set_phys_params()\n", + "\n", + "initial = ((.501, 0.001, 0.001, 0., 0.0450, -0.04), # co-passing particle\n", + " (.511, 0.001, 0.001, 0., -0.0450, -0.04), # counter passing particle\n", + " (.521, 0.001, 0.001, 0., 0.0105, -0.04), # co-trapped particle\n", + " (.531, 0.001, 0.001, 0., -0.0155, -0.04))\n", + "\n", + "loading_params = LoadingParameters(Np=4, seed=1608, specific_markers=initial)\n", + "boundary_params = BoundaryParameters(bc=('remove', 'periodic', 'periodic'))\n", + "model.kinetic_ions.set_markers(loading_params=loading_params, \n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params,\n", + " bufsize=2.)\n", + "model.kinetic_ions.set_sorting_boxes()\n", + "model.kinetic_ions.set_save_data(n_markers=1.0)\n", + "\n", + "# propagator options\n", + "model.propagators.push_vxb.options = model.propagators.push_vxb.Options()\n", + "model.propagators.push_eta.options = model.propagators.push_eta.Options()\n", + "\n", + "# initial conditions (background + perturbation)\n", + "perturbation = None\n", + "background = maxwellians.Maxwellian3D(n=(1.0, perturbation))\n", + "\n", + "model.kinetic_ions.var.add_background(background)" + ] + }, + { + "cell_type": "markdown", + "id": "59", + "metadata": {}, + "source": [ + "We again need a Derham complex for the projection of the equilibirum onto the spline basis:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60", + "metadata": {}, + "outputs": [], + "source": [ + "Nel = (32, 72, 1)\n", + "grid = grids.TensorProductGrid(Nel=Nel)\n", + "\n", + "p = (3, 3, 1)\n", + "spl_kind = (False, True, True)\n", + "derham_opts = DerhamOptions(p=p, spl_kind=spl_kind)" + ] + }, + { + "cell_type": "markdown", + "id": "61", + "metadata": {}, + "source": [ + "We aim to simulate 15000 time steps with a second-order splitting algorithm:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "62", + "metadata": {}, + "outputs": [], + "source": [ + "time_opts = Time(dt=0.2, Tend=3000, split_algo=\"Strang\")\n", + "\n", + "verbose = False\n", + "\n", + "main.run(model, \n", + " params_path=None, \n", + " env=env, \n", + " base_units=base_units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from struphy import main\n", + "\n", + "path = os.path.join(os.getcwd(), \"sim_1\")\n", + "main.pproc(path)\n", + "\n", + "simdata = main.load_data(path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64", + "metadata": {}, + "outputs": [], + "source": [ + "orbits = simdata.orbits[\"kinetic_ions\"]\n", + "\n", + "Nt = simdata.Nt[\"kinetic_ions\"]\n", + "Np = simdata.Np[\"kinetic_ions\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "\n", + "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "\n", + "dt = time_opts.dt\n", + "Tend = time_opts.Tend\n", + "\n", + "for i in range(Np):\n", + " r = np.sqrt(orbits[:, i, 0]**2 + orbits[:, i, 1]**2)\n", + " # poloidal \n", + " ax.scatter(r, orbits[:, i, 2], c=colors[i % 4], s=1)\n", + " # top view\n", + " ax_top.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], s=1)\n", + " \n", + "ax.set_title(f'{math.ceil(Tend/dt)} time steps')\n", + "ax_top.set_title(f'{math.ceil(Tend/dt)} time steps');\n", + "\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "id": "66", + "metadata": {}, + "source": [ + "## Guiding-centers in a Tokamak equilibrium\n", + "\n", + "Let us run a similar test for guiding-centers:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67", + "metadata": {}, + "outputs": [], + "source": [ + "from struphy.models.toy import GuidingCenter\n", + "\n", + "# light-weight model instance\n", + "model = GuidingCenter()\n", + "\n", + "# species parameters\n", + "model.kinetic_ions.set_phys_params()\n", + "\n", + "initial = ((.501, 0.001, 0.001, -1.935 , 1.72), # co-passing particle\n", + " (.501, 0.001, 0.001, 1.935 , 1.72), # couner-passing particle\n", + " (.501, 0.001, 0.001, -0.6665, 1.72), # co-trapped particle\n", + " (.501, 0.001, 0.001, 0.4515, 1.72)) # counter-trapped particl\n", + "\n", + "loading_params = LoadingParameters(Np=4, seed=1608, specific_markers=initial)\n", + "boundary_params = BoundaryParameters(bc=('remove', 'periodic', 'periodic'))\n", + "model.kinetic_ions.set_markers(loading_params=loading_params, \n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params,\n", + " bufsize=2.)\n", + "model.kinetic_ions.set_sorting_boxes()\n", + "model.kinetic_ions.set_save_data(n_markers=1.0)\n", + "\n", + "# propagator options\n", + "model.propagators.push_bxe.options = model.propagators.push_bxe.Options(tol=1e-5)\n", + "model.propagators.push_parallel.options = model.propagators.push_parallel.Options(tol=1e-5)\n", + "\n", + "# initial conditions (background + perturbation)\n", + "perturbation = None\n", + "background = maxwellians.GyroMaxwellian2D(n=(1.0, perturbation), equil=equil)\n", + "\n", + "model.kinetic_ions.var.add_background(background)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68", + "metadata": {}, + "outputs": [], + "source": [ + "# generate two axes\n", + "fig, axs = plt.subplots(2, 1, figsize=(8, 16))\n", + "ax = axs[0]\n", + "ax_top = axs[1]\n", + "\n", + "# min/max of field strength\n", + "equil.domain = domain\n", + "Bmax = np.max(equil.absB0(*eta_topview_2, squeeze_out=True))\n", + "Bmin = np.min(equil.absB0(*eta_topview_1, squeeze_out=True))\n", + "levels = np.linspace(Bmin, Bmax, 51)\n", + "\n", + "# absolute magnetic field at phi = 0\n", + "im = ax.contourf(x_pol, z_pol, equil.absB0(*eta_poloidal, squeeze_out=True), levels=levels)\n", + "\n", + "# absolute magnetic field at Z = 0\n", + "im_top = ax_top.contourf(x_top1, y_top1, equil.absB0(*eta_topview_1, squeeze_out=True), levels=levels)\n", + "ax_top.contourf(x_top2, y_top2, equil.absB0(*eta_topview_2, squeeze_out=True), levels=levels)\n", + "\n", + "# last closed flux surface, poloidal\n", + "ax.plot(x_pol[-1], z_pol[-1], color='k')\n", + "\n", + "# last closed flux surface, toroidal\n", + "ax_top.plot(x_top1[-1], y_top1[-1], color='k')\n", + "ax_top.plot(x_top2[-1], y_top2[-1], color='k')\n", + "\n", + "# limiter, poloidal\n", + "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, 'tab:orange')\n", + "ax.axis('equal')\n", + "ax.set_xlabel('R')\n", + "ax.set_ylabel('Z')\n", + "ax.set_title('abs(B) at $\\phi=0$')\n", + "fig.colorbar(im);\n", + "\n", + "# limiter, toroidal\n", + "limiter_Rmax = np.max(equil.limiter_pts_R)\n", + "limiter_Rmin = np.min(equil.limiter_pts_R)\n", + "\n", + "thetas = 2*np.pi*e2\n", + "limiter_x_max = limiter_Rmax * np.cos(thetas)\n", + "limiter_y_max = - limiter_Rmax * np.sin(thetas)\n", + "limiter_x_min = limiter_Rmin * np.cos(thetas)\n", + "limiter_y_min = - limiter_Rmin * np.sin(thetas)\n", + "\n", + "ax_top.plot(limiter_x_max, limiter_y_max, 'tab:orange')\n", + "ax_top.plot(limiter_x_min, limiter_y_min, 'tab:orange')\n", + "ax_top.axis('equal')\n", + "ax_top.set_xlabel('x')\n", + "ax_top.set_ylabel('y')\n", + "ax_top.set_title('abs(B) at $Z=0$')\n", + "fig.colorbar(im_top);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69", + "metadata": {}, + "outputs": [], + "source": [ + "time_opts = Time(dt=0.1, Tend=100, split_algo=\"Strang\")\n", + "\n", + "verbose = False\n", + "\n", + "main.run(model, \n", + " params_path=None, \n", + " env=env, \n", + " base_units=base_units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from struphy import main\n", + "\n", + "path = os.path.join(os.getcwd(), \"sim_1\")\n", + "main.pproc(path)\n", + "\n", + "simdata = main.load_data(path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "71", + "metadata": {}, + "outputs": [], + "source": [ + "orbits = simdata.orbits[\"kinetic_ions\"]\n", + "\n", + "Nt = simdata.Nt[\"kinetic_ions\"]\n", + "Np = simdata.Np[\"kinetic_ions\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "\n", + "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "\n", + "dt = time_opts.dt\n", + "Tend = time_opts.Tend\n", + "\n", + "for i in range(Np):\n", + " r = np.sqrt(orbits[:, i, 0]**2 + orbits[:, i, 1]**2)\n", + " # poloidal \n", + " ax.scatter(r, orbits[:, i, 2], c=colors[i % 4], s=1)\n", + " # top view\n", + " ax_top.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], s=1)\n", + " \n", + "ax.set_title(f'{math.ceil(Tend/dt)} time steps')\n", + "ax_top.set_title(f'{math.ceil(Tend/dt)} time steps');\n", + "\n", + "fig" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb b/tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb new file mode 100644 index 000000000..922282da1 --- /dev/null +++ b/tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb @@ -0,0 +1,983 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# 3 - Smoothed-particle hydrodynamics\n", + "\n", + "Struphy provides several models for smoothed-particle hydrodynamics (SPH). In this tutorial, we shall exlore\n", + "\n", + "1. Pressure-less fluid flow in a Beltrami force field (model [PressureLessSPH](https://struphy.pages.mpcdf.de/struphy/sections/subsections/models_toy.html#struphy.models.toy.PressureLessSPH))\n", + "2. Gas expansion with SPH (model [EulerSPH](https://struphy.pages.mpcdf.de/struphy/sections/subsections/models_fluid.html#struphy.models.fluid.EulerSPH))\n", + "\n", + "The parameter files discussed below can be obtained with the console commands\n", + "\n", + "```\n", + "struphy params PressureLessSPH\n", + "struphy params EulerSPH \n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Pressure-less fluid flow in Beltrami force field\n", + "\n", + "Let $\\mathbf u_0(\\mathbf x)$ stand for an initial flow velocity field. Moreover,\n", + "let $\\Omega \\subset \\mathbb R^3$ be a box (cuboid). We search for trajectories $(\\mathbf x_p, \\mathbf v_p): [0,T] \\to \\Omega \\times \\mathbb R^3$, $p = 0, \\ldots, N-1$ that satisfy\n", + "\n", + "$$\n", + "\\begin{align}\n", + " \\dot{\\mathbf x}_p &= \\mathbf v_p\\,,\\qquad && \\mathbf x_p(0) = \\mathbf x_{p0}\\,,\n", + " \\\\[2mm]\n", + " \\dot{\\mathbf v}_p &= -\\nabla p(\\mathbf x_p) \\qquad && \\mathbf v_p(0) = \\mathbf u_0(\\mathbf x_p(0))\\,,\n", + " \\end{align}\n", + "$$\n", + "\n", + "where $p \\in H^1(\\Omega)$ is some given function. In what follows we shall set $p$ to give a Beltrami force field to guide the fluid particles.\n", + "\n", + "We start with the generic imports:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", + "from struphy.geometry import domains\n", + "from struphy.fields_background import equils\n", + "from struphy.topology import grids\n", + "from struphy.io.options import DerhamOptions\n", + "from struphy.io.options import FieldsBackground\n", + "from struphy.initial import perturbations\n", + "from struphy.kinetic_background import maxwellians\n", + "from struphy.pic.utilities import (LoadingParameters,\n", + " WeightsParameters,\n", + " BoundaryParameters,\n", + " BinningPlot,\n", + " KernelDensityPlot,\n", + " )\n", + "from struphy import main\n", + "\n", + "# import model, set verbosity\n", + "from struphy.models.toy import PressureLessSPH" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "We shall use the Strang splitting algorithm for the propagators, and simulate in Cartesian geometry (Cuboid):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "# environment options\n", + "env = EnvironmentOptions()\n", + "\n", + "# units\n", + "base_units = BaseUnits()\n", + "\n", + "# time stepping\n", + "time_opts = Time(dt=0.02, Tend=4, split_algo=\"Strang\")\n", + "\n", + "# geometry\n", + "l1 = -.5\n", + "r1 = .5\n", + "l2 = -.5\n", + "r2 = .5\n", + "l3 = 0.\n", + "r3 = 1.\n", + "domain = domains.Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "The Beltrami flow can be specified as a lambda function and then passed to `GenericCartesianFluidEquilibrium`. This fluid equilibirum will be set as initial condition below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "# construct Beltrami flow\n", + "import numpy as np\n", + "\n", + "def u_fun(x, y, z):\n", + " ux = -np.cos(np.pi*x)*np.sin(np.pi*y)\n", + " uy = np.sin(np.pi*x)*np.cos(np.pi*y)\n", + " uz = 0 * x \n", + " return ux, uy, uz\n", + "\n", + "p_fun = lambda x, y, z: 0.5*(np.sin(np.pi*x)**2 + np.sin(np.pi*y)**2)\n", + "n_fun = lambda x, y, z: 1. + 0*x\n", + "\n", + "# put the functions in a generic equilibirum container\n", + "from struphy.fields_background.generic import GenericCartesianFluidEquilibrium\n", + "bel_flow = GenericCartesianFluidEquilibrium(u_xyz=u_fun, p_xyz=p_fun, n_xyz=n_fun)" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "We will also need a grid and Derham complex for projecting the fluid equilibirum onto a spline basis:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "# fluid equilibrium (can be used as part of initial conditions)\n", + "equil = None\n", + "\n", + "# grid\n", + "grid = grids.TensorProductGrid(Nel=(64, 64, 1))\n", + "\n", + "# derham options\n", + "derham_opts = DerhamOptions(p=(3, 3, 1), spl_kind=(False, False, True))" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "In the next step, the light-weight instance of the model is created. We set the parameter `epsilon` to 1.0, appearing in the propagator [PushVinEfield](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_markers.html#struphy.propagators.propagators_markers.PushVinEfield). Moreover, we launch with 1000 particles and save 100 % percent of them through `n_markers=1.0`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "# light-weight model instance\n", + "model = PressureLessSPH()\n", + "\n", + "# species parameters\n", + "model.cold_fluid.set_phys_params(epsilon=1.0)\n", + "\n", + "loading_params = LoadingParameters(Np=1000)\n", + "weights_params = WeightsParameters()\n", + "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", + "model.cold_fluid.set_markers(loading_params=loading_params,\n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params,\n", + " )\n", + "model.cold_fluid.set_sorting_boxes(boxes_per_dim=(1, 1, 1))\n", + "model.cold_fluid.set_save_data(n_markers=1.0)" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "We now set the propagator options. Here, it is important to pass the pressure from the Beltrami flow as auxiliary field to `push_v`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "# propagator options\n", + "from struphy.ode.utils import ButcherTableau\n", + "butcher = ButcherTableau(algo=\"forward_euler\")\n", + "model.propagators.push_eta.options = model.propagators.push_eta.Options(butcher=butcher)\n", + "\n", + "phi = bel_flow.p0\n", + "model.propagators.push_v.options = model.propagators.push_v.Options(phi=phi)" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "The initial condition of the species is defined by a background function, plus possible perturbations, which we ignore here. The background of the speceis is set to the Beltrami flow:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "# background, perturbations and initial conditions\n", + "model.cold_fluid.var.add_background(bel_flow)" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "Let us start the run, post-process the raw data, load the post-processed data and plot the particle trajectories:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "verbose = False\n", + "\n", + "main.run(model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "path = os.path.join(os.getcwd(), \"sim_1\")\n", + "\n", + "main.pproc(path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "simdata = main.load_data(path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "plt.figure(figsize=(12, 28))\n", + "\n", + "orbits = simdata.orbits[\"cold_fluid\"]\n", + "\n", + "coloring = np.select([orbits[0, :, 0]<=-0.2, \n", + " np.abs(orbits[0, :, 0]) < +0.2, \n", + " orbits[0, :, 0] >= 0.2],\n", + " [-1.0, 0.0, +1.0])\n", + "\n", + "dt = time_opts.dt\n", + "Nt = simdata.t_grid.size - 1\n", + "interval = Nt/20\n", + "plot_ct = 0\n", + "for i in range(Nt):\n", + " if i % interval == 0:\n", + " print(f'{i = }')\n", + " plot_ct += 1\n", + " plt.subplot(5, 2, plot_ct)\n", + " ax = plt.gca() \n", + " plt.scatter(orbits[i, :, 0], orbits[i, :, 1], c=coloring)\n", + " plt.axis('square')\n", + " plt.title('n0_scatter')\n", + " plt.xlim(l1, r1)\n", + " plt.ylim(l2, r2)\n", + " plt.colorbar()\n", + " plt.title(f'Gas at t={i*dt}')\n", + " if plot_ct == 10:\n", + " break" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "Let us perform another simulation, similar to the previous one. We will save the results in the folder `sim_2`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "# environment options\n", + "env = EnvironmentOptions(sim_folder=\"sim_2\")" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "This time, we shall draw the markers on a regular grid obtained from a tesselation of the domain: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "# light-weight model instance\n", + "model = PressureLessSPH()\n", + "\n", + "# species parameters\n", + "model.cold_fluid.set_phys_params(epsilon=1.0)\n", + "\n", + "loading_params = LoadingParameters(ppb=4, loading=\"tesselation\")\n", + "weights_params = WeightsParameters()\n", + "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", + "model.cold_fluid.set_markers(loading_params=loading_params,\n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params,\n", + " bufsize=0.5\n", + " )\n", + "model.cold_fluid.set_sorting_boxes(boxes_per_dim=(16, 16, 1))\n", + "model.cold_fluid.set_save_data(n_markers=1.0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "# propagator options\n", + "from struphy.ode.utils import ButcherTableau\n", + "butcher = ButcherTableau(algo=\"forward_euler\")\n", + "model.propagators.push_eta.options = model.propagators.push_eta.Options(butcher=butcher)\n", + "\n", + "phi = bel_flow.p0\n", + "model.propagators.push_v.options = model.propagators.push_v.Options(phi=phi)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "# background, perturbations and initial conditions\n", + "model.cold_fluid.var.add_background(bel_flow)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "main.run(model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "path = os.path.join(os.getcwd(), \"sim_2\")\n", + "\n", + "main.pproc(path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": {}, + "outputs": [], + "source": [ + "simdata = main.load_data(path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "plt.figure(figsize=(12, 28))\n", + "\n", + "orbits = simdata.orbits[\"cold_fluid\"]\n", + "\n", + "coloring = np.select([orbits[0, :, 0]<=-0.2, \n", + " np.abs(orbits[0, :, 0]) < +0.2, \n", + " orbits[0, :, 0] >= 0.2],\n", + " [-1.0, 0.0, +1.0])\n", + "\n", + "dt = time_opts.dt\n", + "Nt = simdata.t_grid.size - 1\n", + "interval = Nt/20\n", + "plot_ct = 0\n", + "for i in range(Nt):\n", + " if i % interval == 0:\n", + " print(f'{i = }')\n", + " plot_ct += 1\n", + " plt.subplot(5, 2, plot_ct)\n", + " ax = plt.gca() \n", + " plt.scatter(orbits[i, :, 0], orbits[i, :, 1], c=coloring)\n", + " plt.axis('square')\n", + " plt.title('n0_scatter')\n", + " plt.xlim(l1, r1)\n", + " plt.ylim(l2, r2)\n", + " plt.colorbar()\n", + " plt.title(f'Gas at t={i*dt}')\n", + " if plot_ct == 10:\n", + " break" + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "## Gas expansion\n", + "\n", + "We use SPH to solve Euler's equations (model [EulerSPH](https://struphy.pages.mpcdf.de/struphy/sections/subsections/models_fluid.html#struphy.models.fluid.EulerSPH)):\n", + "\n", + "$$\n", + "\\begin{align}\n", + " \\partial_t \\rho + \\nabla \\cdot (\\rho \\mathbf u) &= 0\\,,\n", + " \\\\[2mm]\n", + " \\rho(\\partial_t \\mathbf u + \\mathbf u \\cdot \\nabla \\mathbf u) &= - \\nabla \\left(\\rho^2 \\frac{\\partial \\mathcal U(\\rho, S)}{\\partial \\rho} \\right)\\,,\n", + " \\\\[2mm]\n", + " \\partial_t S + \\mathbf u \\cdot \\nabla S &= 0\\,,\n", + " \\end{align}\n", + "$$\n", + "\n", + "where $S$ denotes the entropy per unit mass and the internal energy per unit mass is \n", + "\n", + "$$\n", + "\\mathcal U(\\rho, S) = \\kappa(S) \\log \\rho\\,.\n", + "$$\n", + "\n", + "The SPH discretization leads to ODEs for $N$ particles indexed by $p$,\n", + "\n", + "$$\n", + "\\begin{align}\n", + " \\dot{\\mathbf x}_p &= \\mathbf v_p\\,,\\qquad && \\mathbf x_p(0) = \\mathbf x_{p0}\\,,\n", + " \\\\[2mm]\n", + " \\dot{\\mathbf v}_p &= -\\kappa_{p}(0) \\sum_{i=1}^N w_i \\left(\\frac{1}{\\rho^{N,h}(\\mathbf x_p)} + \\frac{1}{\\rho^{N,h}(\\mathbf x_i)} \\right) \\nabla W_h(\\mathbf x_p - \\mathbf x_i) \\qquad && \\mathbf v_p(0) = \\mathbf u(\\mathbf x_p(0))\\,,\n", + " \\end{align}\n", + "$$\n", + "\n", + "where the smoothed density reads\n", + "\n", + "$$\n", + " \\rho^{N,h}(\\mathbf x) = \\sum_{j=1}^N w_j W_h(\\mathbf x - \\mathbf x_j)\\,,\n", + "$$\n", + "\n", + "with weights $w_p = const.$ and where $W_h(\\mathbf x)$ is a suitable smoothing kernel.\n", + "The velocity update is performed with the Propagator [PushVinSPHpressure](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_markers.html#struphy.propagators.propagators_markers.PushVinSPHpressure).\n", + "\n", + "We shall now compute a gas expansion in 2d (nonlinear example). First, check out some of the smoothing kernels available for SPH evaluations:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "from struphy.pic.sph_smoothing_kernels import linear_uni, trigonometric_uni, gaussian_uni\n", + "\n", + "x = np.linspace(-1, 1, 200)\n", + "out1 = np.zeros_like(x)\n", + "out2 = np.zeros_like(x)\n", + "out3 = np.zeros_like(x)\n", + "\n", + "for i, xi in enumerate(x):\n", + " out1[i] = trigonometric_uni(xi, 1.)\n", + " out2[i] = gaussian_uni(xi, 1.)\n", + " out3[i] = linear_uni(xi, 1.)\n", + "plt.plot(x, out1, label=\"trigonometric\")\n", + "plt.plot(x, out2, label=\"gaussian\")\n", + "plt.plot(x, out3, label = \"linear\")\n", + "plt.title('Some smoothing kernels')\n", + "plt.legend()" + ] + }, + { + "cell_type": "markdown", + "id": "32", + "metadata": {}, + "source": [ + "We start with the generic imports and also import the model `EulerSPH`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": {}, + "outputs": [], + "source": [ + "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", + "from struphy.geometry import domains\n", + "from struphy.fields_background import equils\n", + "from struphy.topology import grids\n", + "from struphy.io.options import DerhamOptions\n", + "from struphy.io.options import FieldsBackground\n", + "from struphy.initial import perturbations\n", + "from struphy.kinetic_background import maxwellians\n", + "from struphy.pic.utilities import (LoadingParameters,\n", + " WeightsParameters,\n", + " BoundaryParameters,\n", + " BinningPlot,\n", + " KernelDensityPlot,\n", + " )\n", + "from struphy import main\n", + "\n", + "# import model, set verbosity\n", + "from struphy.models.fluid import EulerSPH" + ] + }, + { + "cell_type": "markdown", + "id": "34", + "metadata": {}, + "source": [ + "Here, it is important to set the base unit `kBT` in order to derive the velocity unit:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "# environment options\n", + "env = EnvironmentOptions()\n", + "\n", + "# units\n", + "base_units = BaseUnits(kBT=1.0)\n", + "\n", + "# time stepping\n", + "time_opts = Time(dt=0.04, Tend=1.6, split_algo=\"Strang\")\n", + "\n", + "# geometry\n", + "l1 = -3.0\n", + "r1 = 3.0\n", + "l2 = -3.0\n", + "r2 = 3.0\n", + "l3 = 0.\n", + "r3 = 1.\n", + "domain = domains.Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" + ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "As background, which goes into the initial condition below, we define a Gaussian blob in the xy-plane:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": {}, + "outputs": [], + "source": [ + "# gaussian initial blob\n", + "from struphy.fields_background.generic import GenericCartesianFluidEquilibrium\n", + "import numpy as np\n", + "T_h = 0.2\n", + "gamma = 5/3\n", + "n_fun = lambda x, y, z: np.exp(-(x**2 + y**2)/T_h) / 35\n", + "\n", + "blob = GenericCartesianFluidEquilibrium(n_xyz=n_fun)" + ] + }, + { + "cell_type": "markdown", + "id": "38", + "metadata": {}, + "source": [ + "We also need a grid and Derham complex for projecting the fluid background on a spline basis:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39", + "metadata": {}, + "outputs": [], + "source": [ + "# fluid equilibrium (can be used as part of initial conditions)\n", + "equil = None\n", + "\n", + "# grid\n", + "grid = grids.TensorProductGrid(Nel=(64, 64, 1))\n", + "\n", + "# derham options\n", + "derham_opts = DerhamOptions(p=(3, 3, 1), spl_kind=(False, False, True))" + ] + }, + { + "cell_type": "markdown", + "id": "40", + "metadata": {}, + "source": [ + "Now we create the light-weight instance of `EulerSPH`, without the optional propagator for particles in a magnetic background field. Note as well that we shall reject particles whose weight is below a certain threshold in order to save computing time:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41", + "metadata": {}, + "outputs": [], + "source": [ + "# light-weight model instance\n", + "model = EulerSPH(with_B0=False)\n", + "\n", + "# species parameters\n", + "model.euler_fluid.set_phys_params()\n", + "\n", + "loading_params = LoadingParameters(ppb=400)\n", + "weights_params = WeightsParameters(reject_weights=True, threshold=3e-3)\n", + "boundary_params = BoundaryParameters()\n", + "model.euler_fluid.set_markers(loading_params=loading_params,\n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params,\n", + " )\n", + "nx = 16\n", + "ny = 16\n", + "model.euler_fluid.set_sorting_boxes(boxes_per_dim=(nx, ny, 1))" + ] + }, + { + "cell_type": "markdown", + "id": "42", + "metadata": {}, + "source": [ + "For visualization of the result, we want to save a binning plot and a kernel density plot (sph evaluation of the fluid density):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43", + "metadata": {}, + "outputs": [], + "source": [ + "bin_plot = BinningPlot(slice=\"e1_e2\", \n", + " n_bins=(64, 64), \n", + " ranges=((0.0, 1.0), (0.0, 1.0)), \n", + " divide_by_jac=False,\n", + " )\n", + "pts_e1 = 100\n", + "pts_e2 = 90\n", + "kd_plot = KernelDensityPlot(pts_e1=pts_e1, pts_e2=pts_e2, pts_e3=1)\n", + "model.euler_fluid.set_save_data(n_markers=1.0,\n", + " binning_plots=(bin_plot,),\n", + " kernel_density_plots=(kd_plot,),\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "44", + "metadata": {}, + "source": [ + "We choose `gaussian_2d` as the smoothing kernel for sph evaluations during the pressure step:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45", + "metadata": {}, + "outputs": [], + "source": [ + "# propagator options\n", + "from struphy.ode.utils import ButcherTableau\n", + "butcher = ButcherTableau(algo=\"forward_euler\")\n", + "model.propagators.push_eta.options = model.propagators.push_eta.Options(butcher=butcher)\n", + "\n", + "model.propagators.push_sph_p.options = model.propagators.push_sph_p.Options(kernel_type=\"gaussian_2d\")" + ] + }, + { + "cell_type": "markdown", + "id": "46", + "metadata": {}, + "source": [ + "Now we set the initial condition and run the simulation. The time steps take long at the beginning, but get faster towards the end, when particles are spread out over the domain. The simulation launched in the console will be a lot faster than in the notebook, especially when using MPI, or compiling with GPU." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47", + "metadata": {}, + "outputs": [], + "source": [ + "# background, perturbations and initial conditions\n", + "model.euler_fluid.var.add_background(blob)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48", + "metadata": {}, + "outputs": [], + "source": [ + "verbose = True\n", + "\n", + "main.run(model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "path = os.path.join(os.getcwd(), \"sim_1\")\n", + "main.pproc(path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "50", + "metadata": {}, + "outputs": [], + "source": [ + "simdata = main.load_data(path)" + ] + }, + { + "cell_type": "markdown", + "id": "51", + "metadata": {}, + "source": [ + "The above output of the `simdata` object tells us where to find the post-processed simulation data:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52", + "metadata": {}, + "outputs": [], + "source": [ + "# analytical functions\n", + "n_xyz = blob.n_xyz\n", + "n3 = blob.n3\n", + "\n", + "# grids\n", + "x = np.linspace(l1, r1, pts_e1)\n", + "y = np.linspace(l2, r2, pts_e2)\n", + "xx, yy = np.meshgrid(x, y, indexing=\"ij\")\n", + "ee1, ee2, ee3 = simdata.n_sph[\"euler_fluid\"][\"view_0\"][\"grid_n_sph\"]\n", + "eta1 = ee1[:, 0, 0]\n", + "eta2 = ee2[0, :, 0]\n", + "bc_x = simdata.f[\"euler_fluid\"][\"e1_e2\"][\"grid_e1\"]\n", + "bc_y = simdata.f[\"euler_fluid\"][\"e1_e2\"][\"grid_e2\"]\n", + "\n", + "# markers\n", + "orbits = simdata.orbits[\"euler_fluid\"]\n", + "positions = orbits[0, :, :3]\n", + "weights = orbits[0, :, 6]\n", + "\n", + "# binning and sph eval\n", + "n_sph = simdata.n_sph[\"euler_fluid\"][\"view_0\"][\"n_sph\"][0]\n", + "f_bin = simdata.f[\"euler_fluid\"][\"e1_e2\"][\"f_binned\"][0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt \n", + "plt.figure(figsize=(12, 15))\n", + "\n", + "# plots\n", + "plt.subplot(3, 2, 1)\n", + "plt.pcolor(xx, yy, n_fun(xx, yy, 0))\n", + "plt.axis('square')\n", + "plt.title('n_xyz initial')\n", + "plt.colorbar()\n", + "\n", + "plt.subplot(3, 2, 2)\n", + "plt.pcolor(eta1, eta2, n3(eta1, eta2, 0, squeeze_out=True).T)\n", + "plt.axis('square')\n", + "plt.title('$\\hat{n}^{\\t{vol}}$ initial (volume form)')\n", + "plt.colorbar()\n", + "\n", + "make_scatter = True\n", + "if make_scatter:\n", + " plt.subplot(3, 2, 3)\n", + " ax = plt.gca()\n", + " ax.set_xticks(np.linspace(l1, r1, nx + 1))\n", + " ax.set_yticks(np.linspace(l2, r2, ny + 1))\n", + " plt.tick_params(labelbottom = False) \n", + " coloring = weights\n", + " plt.scatter(positions[:, 0], positions[:, 1], c=coloring, s=.25)\n", + " plt.grid(c='k')\n", + " plt.axis('square')\n", + " plt.title('$\\hat{n}^{\\t{vol}}$ initial scatter (random)')\n", + " plt.xlim(l1, r1)\n", + " plt.ylim(l2, r2)\n", + " plt.colorbar()\n", + "\n", + "plt.subplot(3, 2, 4)\n", + "ax = plt.gca()\n", + "ax.set_xticks(np.linspace(0, 1, nx + 1))\n", + "ax.set_yticks(np.linspace(0, 1., ny + 1))\n", + "plt.tick_params(labelbottom = False) \n", + "plt.pcolor(ee1[:,:,0], ee2[:,:,0], n_sph[:,:,0])\n", + "plt.grid()\n", + "plt.axis('square')\n", + "plt.title(f'n_sph initial (random)')\n", + "plt.colorbar()\n", + "\n", + "plt.subplot(3, 2, 5)\n", + "ax = plt.gca()\n", + "plt.pcolor(bc_x, bc_y, f_bin)\n", + "plt.axis('square')\n", + "plt.title(f'n_binned initial (random)')\n", + "plt.colorbar()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54", + "metadata": {}, + "outputs": [], + "source": [ + "dt = time_opts.dt\n", + "Nt = simdata.t_grid.size - 1\n", + "\n", + "positions = orbits[:, :, :3]\n", + "\n", + "interval = Nt/10\n", + "plot_ct = 0\n", + "\n", + "plt.figure(figsize=(12, 24))\n", + "for i in range(Nt):\n", + " if i % interval == 0:\n", + " print(f'{i = }')\n", + " plot_ct += 1\n", + " plt.subplot(4, 2, plot_ct)\n", + " ax = plt.gca() \n", + " coloring = weights\n", + " plt.scatter(positions[i, :, 0], positions[i, :, 1], c=coloring, s=.25)\n", + " plt.axis('square')\n", + " plt.title('n0_scatter')\n", + " plt.xlim(l1, r1)\n", + " plt.ylim(l2, r2)\n", + " plt.colorbar()\n", + " plt.title(f'Gas at t={i*dt}')\n", + " if plot_ct == 8:\n", + " break" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/tutorial_04_vlasov_maxwell.ipynb b/tutorials/tutorial_04_vlasov_maxwell.ipynb new file mode 100644 index 000000000..f35f0443f --- /dev/null +++ b/tutorials/tutorial_04_vlasov_maxwell.ipynb @@ -0,0 +1,370 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 4 - Vlasov-Ampère equations\n", + "\n", + "The equations we will solve are described in the model [VlasovAmpereOneSpecies](https://struphy.pages.mpcdf.de/struphy/sections/subsections/models_kinetic.html#struphy.models.kinetic.VlasovAmpereOneSpecies).\n", + "To create the default parameter file from the console:\n", + "\n", + "```\n", + "struphy params VlasovAmpereOneSpecies\n", + "```\n", + "\n", + "Adapt the parameters and run the model with\n", + "\n", + "```\n", + "python3 params_VlasovAmpereOneSpecies.py\n", + "```\n", + "\n", + "In this notebook we shall re-create the parameter file and perform some tests.\n", + "\n", + "## Weak Landau damping\n", + "\n", + "1. Imports:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", + "from struphy.geometry import domains\n", + "from struphy.fields_background import equils\n", + "from struphy.topology import grids\n", + "from struphy.io.options import DerhamOptions\n", + "from struphy.io.options import FieldsBackground\n", + "from struphy.initial import perturbations\n", + "from struphy.kinetic_background import maxwellians\n", + "from struphy.pic.utilities import LoadingParameters, WeightsParameters, BoundaryParameters, BinningPlot\n", + "from struphy import main\n", + "\n", + "from struphy.models.kinetic import VlasovAmpereOneSpecies" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "2. Generic options:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# environment options\n", + "env = EnvironmentOptions()\n", + "\n", + "# units\n", + "base_units = BaseUnits()\n", + "\n", + "# time stepping\n", + "time_opts = Time(dt = 0.05, Tend = 0.5)#, Tend = 3.5\n", + "\n", + "# geometry\n", + "r1 = 12.56\n", + "domain = domains.Cuboid(r1=r1)\n", + "\n", + "# fluid equilibrium (can be used as part of initial conditions)\n", + "equil = None\n", + "\n", + "# grid\n", + "grid = grids.TensorProductGrid(Nel=(32, 1, 1))\n", + "\n", + "# derham options\n", + "derham_opts = DerhamOptions()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "3. Model instance and physics parameters:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model = VlasovAmpereOneSpecies()\n", + "\n", + "model.kinetic_ions.set_phys_params(alpha=1.0, epsilon=1.0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "4. Kinetic species parameters:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "loading_params = LoadingParameters(ppc=10000)\n", + "weights_params = WeightsParameters(control_variate=True)\n", + "boundary_params = BoundaryParameters()\n", + "model.kinetic_ions.set_markers(loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params)\n", + "model.kinetic_ions.set_sorting_boxes()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# particle binning\n", + "binplot_1 = BinningPlot(slice=\"e1\", n_bins=128, ranges=(0.0, 1.0))\n", + "binplot_2 = BinningPlot(slice=\"v1\", n_bins=128, ranges=(-5.0, 5.0))\n", + "binplot_3 = BinningPlot(slice=\"e1_v1\", n_bins=(128, 128), ranges=((0.0, 1.0), (-5.0, 5.0)))\n", + "\n", + "binning_plots = (binplot_1, binplot_2, binplot_3)\n", + "\n", + "model.kinetic_ions.set_save_data(binning_plots=binning_plots)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "5. Propagator options:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model.propagators.push_eta.options = model.propagators.push_eta.Options()\n", + "model.propagators.coupling_va.options = model.propagators.coupling_va.Options()\n", + "model.initial_poisson.options = model.initial_poisson.Options(stab_eps=1e-12)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "6. Initial conditions:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "background = maxwellians.Maxwellian3D(n=(1.0, None))\n", + "model.kinetic_ions.var.add_background(background)\n", + "\n", + "perturbation = perturbations.ModesCos(ls=[1], amps=[0.001])\n", + "init = maxwellians.Maxwellian3D(n=(1.0, perturbation))\n", + "model.kinetic_ions.var.add_initial_condition(init)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us run the model. However, depending on the confguration, running in a notebook might be very slow. In order to get fast execution, run from the console. First, create the default parameter file and rename it \n", + "\n", + "```\n", + "struphy params VlasovAmpereOneSpecies\n", + "mv params_VlasovAmpereOneSpecies.py landau.py\n", + "```\n", + "\n", + "Adapt it with the parameters from this notebook. Start the run with\n", + "\n", + "```\n", + "python landau.py\n", + "```\n", + "\n", + "or \n", + "\n", + "```\n", + "mpirun -n 2 python landau.py\n", + "```\n", + "\n", + "for a run on two threads. Line profiling can be enabled with \n", + "\n", + "```\n", + "LINE_PROFILE=1 mpirun -n 2 landau.py\n", + "```\n", + "\n", + "Let us look a the slower run in the notebook:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "verbose = True\n", + "\n", + "main.run(model, \n", + " params_path=None, \n", + " env=env, \n", + " base_units=base_units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "path = os.path.join(os.getcwd(), \"sim_1\")\n", + "main.pproc(path, celldivide=8)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "simdata = main.load_data(path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# plot in v1\n", + "from matplotlib import pyplot as plt\n", + "\n", + "v1_bins = simdata.f[\"kinetic_ions\"][\"v1\"][\"grid_v1\"]\n", + "f_v1_init = simdata.f[\"kinetic_ions\"][\"v1\"][\"f_binned\"][0]\n", + "\n", + "plt.plot(v1_bins, f_v1_init)\n", + "plt.xlabel('vx')\n", + "plt.title('Initial Maxwellian');" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# plot in e1\n", + "\n", + "e1_bins = simdata.f[\"kinetic_ions\"][\"e1\"][\"grid_e1\"]\n", + "df_e1_init = simdata.f[\"kinetic_ions\"][\"e1\"][\"delta_f_binned\"][0]\n", + "\n", + "plt.plot(e1_bins, df_e1_init)\n", + "plt.xlabel('$\\eta_1$')\n", + "plt.title('Initial spatial perturbation');" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# plot in e1-v1\n", + "\n", + "e1_bins = simdata.f[\"kinetic_ions\"][\"e1_v1\"][\"grid_e1\"]\n", + "v1_bins = simdata.f[\"kinetic_ions\"][\"e1_v1\"][\"grid_v1\"]\n", + "f_init = simdata.f[\"kinetic_ions\"][\"e1_v1\"][\"f_binned\"][0]\n", + "df_init = simdata.f[\"kinetic_ions\"][\"e1_v1\"][\"delta_f_binned\"][0]\n", + "f_end = simdata.f[\"kinetic_ions\"][\"e1_v1\"][\"f_binned\"][-1]\n", + "df_end = simdata.f[\"kinetic_ions\"][\"e1_v1\"][\"delta_f_binned\"][-1]\n", + "\n", + "plt.figure(figsize=(14, 10))\n", + "\n", + "plt.subplot(2, 2, 1)\n", + "plt.pcolor(e1_bins, v1_bins, f_init.T)\n", + "plt.xlabel('$\\eta_1$')\n", + "plt.ylabel('$v_x$')\n", + "plt.title('Initial Maxwellian')\n", + "plt.colorbar()\n", + "\n", + "plt.subplot(2, 2, 2)\n", + "plt.pcolor(e1_bins, v1_bins, df_init.T)\n", + "plt.xlabel('$\\eta_1$')\n", + "plt.ylabel('$v_x$')\n", + "plt.title('Initial perturbation')\n", + "plt.colorbar()\n", + "\n", + "plt.subplot(2, 2, 3)\n", + "plt.pcolor(e1_bins, v1_bins, f_end.T)\n", + "plt.xlabel('$\\eta_1$')\n", + "plt.ylabel('$v_x$')\n", + "plt.title('Final Maxwellian')\n", + "plt.colorbar()\n", + "\n", + "plt.subplot(2, 2, 4)\n", + "plt.pcolor(e1_bins, v1_bins, df_end.T)\n", + "plt.xlabel('$\\eta_1$')\n", + "plt.ylabel('$v_x$')\n", + "plt.title('Final perturbation')\n", + "plt.colorbar();" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# electric field\n", + "\n", + "e1, e2, e3 = simdata.grids_log \n", + "e_vals = simdata.spline_values[\"em_fields\"][\"e_field_log\"][0][0]\n", + "\n", + "plt.plot(e1, e_vals[:, 0, 0], label='E')\n", + "plt.xlabel('$\\eta_1$')\n", + "plt.title('Initial electric field')\n", + "plt.legend();" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/doc/tutorials/tutorial_04_mapped_domains.ipynb b/tutorials/tutorial_05_mapped_domains.ipynb similarity index 98% rename from doc/tutorials/tutorial_04_mapped_domains.ipynb rename to tutorials/tutorial_05_mapped_domains.ipynb index 4ef0a0ef0..f26724fdf 100644 --- a/doc/tutorials/tutorial_04_mapped_domains.ipynb +++ b/tutorials/tutorial_05_mapped_domains.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 4 - Mapped domains with polar singularity\n", + "# 5 - Mapped domains with polar singularity\n", "\n", "This tutorial provides access to [Struphy domains](https://struphy.pages.mpcdf.de/struphy/sections/domains.html) which can be defined from analytical formulas or through third-party software (VMEC, GVEC, DESC, etc.).\n", "\n", @@ -408,13 +408,6 @@ "source": [ "domain.show()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/doc/tutorials/tutorial_05_mhd_equilibria.ipynb b/tutorials/tutorial_06_mhd_equilibria.ipynb similarity index 99% rename from doc/tutorials/tutorial_05_mhd_equilibria.ipynb rename to tutorials/tutorial_06_mhd_equilibria.ipynb index ad7d89358..176374284 100644 --- a/doc/tutorials/tutorial_05_mhd_equilibria.ipynb +++ b/tutorials/tutorial_06_mhd_equilibria.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 5 - MHD equilibria\n", + "# 6 - MHD equilibria\n", "\n", "This tutorial provides acces to the [Struphy MHD equlibrium interface](https://struphy.pages.mpcdf.de/struphy/sections/subsections/mhd_equils.html). We shall plot some available equilibria, in particular:\n", "\n", diff --git a/doc/tutorials/tutorial_01_kinetic_particles.ipynb b/tutorials_old/tutorial_01_kinetic_particles.ipynb similarity index 100% rename from doc/tutorials/tutorial_01_kinetic_particles.ipynb rename to tutorials_old/tutorial_01_kinetic_particles.ipynb diff --git a/tutorials_old/tutorial_01_parameter_files.ipynb b/tutorials_old/tutorial_01_parameter_files.ipynb new file mode 100644 index 000000000..e803e3746 --- /dev/null +++ b/tutorials_old/tutorial_01_parameter_files.ipynb @@ -0,0 +1,378 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d34c79c5", + "metadata": {}, + "source": [ + "# Parameter files and `struphy.main`\n", + "\n", + "Struphy parameter files are Python scripts (.py) that can be executed with the Python interpreter.\n", + "For each `MODEL`, the default parameter file can be generated from the console via\n", + "\n", + "```\n", + "struphy params MODEL\n", + "```\n", + "\n", + "This will create a file `params_MODEL.py` in the current working directory. To run the model type\n", + "\n", + "```\n", + "python params_MODEL.py\n", + "```\n", + "\n", + "The user can modify the parameter file to launch a specific simulation.\n", + "As an example, let us discuss the parameter file of the model [Vlasov](https://struphy.pages.mpcdf.de/struphy/sections/subsections/models_toy.html#struphy.models.toy.Vlasov) and run some simple examples. \n", + "The file can be generated from\n", + "\n", + "```\n", + "struphy params Vlasov\n", + "```\n", + "\n", + "To see its contents, open the file in your preferred editor or type\n", + "\n", + "```\n", + "cat params_Vlasov.py\n", + "```\n", + "\n", + "## Parameters part 1: Imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ecab659", + "metadata": {}, + "outputs": [], + "source": [ + "from struphy.io.options import EnvironmentOptions, Units, Time\n", + "from struphy.geometry import domains\n", + "from struphy.fields_background import equils\n", + "from struphy.topology import grids\n", + "from struphy.io.options import DerhamOptions\n", + "from struphy.io.options import FieldsBackground\n", + "from struphy.initial import perturbations\n", + "from struphy.kinetic_background import maxwellians\n", + "from struphy.pic.utilities import LoadingParameters, WeightsParameters, BoundaryParameters\n", + "from struphy import main\n", + "\n", + "# import model, set verbosity\n", + "from struphy.models.toy import Vlasov as Model\n", + "verbose = True" + ] + }, + { + "cell_type": "markdown", + "id": "5cf6d9c7", + "metadata": {}, + "source": [ + "All Struphy parameter files import the modules listed above, even though some of them might not be needed in a specific model. The last import imports the model itself, always under the alias `Model`.\n", + "\n", + "## Parameters part 2: Generic options\n", + "\n", + "The following lines refer to options that can be set for any model. These are:\n", + "\n", + "* Environment options (paths, saving, domain cloning)\n", + "* Model units\n", + "* Time options\n", + "* Problem geometry (mapped domain)\n", + "* Static background (equilibrium)\n", + "* Grid\n", + "* Derham complex\n", + "\n", + "Check the respective classes for possible options." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc43d2fc", + "metadata": {}, + "outputs": [], + "source": [ + "# environment options\n", + "env = EnvironmentOptions()\n", + "\n", + "# units\n", + "units = Units()\n", + "\n", + "# time stepping\n", + "time_opts = Time(dt=0.2, Tend=10.0)\n", + "\n", + "# geometry\n", + "l1 = -5.0\n", + "r1 = 5.0\n", + "l2 = -7.0\n", + "r2 = 7.0\n", + "l3 = -1.0\n", + "r3 = 1.0\n", + "domain = domains.Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)\n", + "\n", + "# fluid equilibrium (can be used as part of initial conditions)\n", + "equil = None\n", + "\n", + "# grid\n", + "grid = None\n", + "\n", + "# derham options\n", + "derham_opts = None" + ] + }, + { + "cell_type": "markdown", + "id": "74e6f739", + "metadata": {}, + "source": [ + "## Parameters part 3: Model instance\n", + "\n", + "Here, a light-weight instance of the model is created, without allocating memory. The light-weight instance is used to set model-specific parameters for the model's species.\n", + "\n", + "Check the functions \n", + "\n", + "* `Species.set_phys_params()`\n", + "* `KineticSpecies.set_markers()`\n", + "* `KineticSpecies.set_sorting_boxes()`\n", + "* `KineticSpecies.set_save_data()`\n", + "\n", + " for possible options." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c498fb3", + "metadata": {}, + "outputs": [], + "source": [ + "# light-weight model instance\n", + "model = Model()\n", + "\n", + "# species parameters\n", + "model.kinetic_ions.set_phys_params()\n", + "\n", + "loading_params = LoadingParameters(Np=15)\n", + "weights_params = WeightsParameters()\n", + "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", + "model.kinetic_ions.set_markers(loading_params=loading_params, \n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params)\n", + "model.kinetic_ions.set_sorting_boxes()\n", + "model.kinetic_ions.set_save_data(n_markers=1.0)" + ] + }, + { + "cell_type": "markdown", + "id": "b0f65b0a", + "metadata": {}, + "source": [ + "## Parameters part 4: Propagator options\n", + "\n", + "Check the method `set_options()` of each propagator for possible options." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be6875e7", + "metadata": {}, + "outputs": [], + "source": [ + "# propagator options\n", + "model.propagators.push_vxb.set_options()\n", + "model.propagators.push_eta.set_options()" + ] + }, + { + "cell_type": "markdown", + "id": "b1ef8b97", + "metadata": {}, + "source": [ + "## Parameters part 5: Initial conditions\n", + "\n", + "Use the methods `Variable.add_background()` and `Variable.add_perturbation()` to set initial conditions for each variable of a species. Variables that are not specified are intialized as zero." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc0ac424", + "metadata": {}, + "outputs": [], + "source": [ + "# initial conditions (background + perturbation)\n", + "perturbation = None\n", + "\n", + "background = maxwellians.Maxwellian3D(n=(1.0, perturbation))\n", + "model.kinetic_ions.var.add_background(background)" + ] + }, + { + "cell_type": "markdown", + "id": "879978af", + "metadata": {}, + "source": [ + "## Parameters part 6: `main.run`\n", + "\n", + "In the final part of the parameter file, the `main.run` command is invoked. This command will allocate memory and run the specified simulation. The run command is not executed when the parameter file is imported in another Python script." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "03764138", + "metadata": {}, + "outputs": [], + "source": [ + "main.run(model, \n", + " params_path=None, \n", + " env=env, \n", + " units=units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "f9ce5099", + "metadata": {}, + "source": [ + "## Post processing: `main.pproc`\n", + "\n", + "Aside from `run`, the Struphy `main` module has also a `pproc` routine for post-processing raw simulation data:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd7cfe62", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "path = os.path.join(os.getcwd(), \"sim_1\")\n", + "\n", + "main.pproc(path, physical=True)" + ] + }, + { + "cell_type": "markdown", + "id": "e317f88d", + "metadata": {}, + "source": [ + "## Loading data: `main.load_data`\n", + "\n", + "After post-processing, the generated data can be loaded via `main.load_data`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78b8cbab", + "metadata": {}, + "outputs": [], + "source": [ + "simdata = main.load_data(path)" + ] + }, + { + "cell_type": "markdown", + "id": "a9468479", + "metadata": {}, + "source": [ + "`main.load_data` returns a `SimData` object, which you can inspect to get further info on possible data to load:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0531679e", + "metadata": {}, + "outputs": [], + "source": [ + "for k, v in simdata.__dict__.items():\n", + " print(k, type(v))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a329eb38", + "metadata": {}, + "outputs": [], + "source": [ + "for k, v in simdata.pic_species[\"kinetic_ions\"][\"orbits\"].items():\n", + " print(f\"{k = }, {type(v) = }\")" + ] + }, + { + "cell_type": "markdown", + "id": "08544a91", + "metadata": {}, + "source": [ + "## Plotting particle orbits\n", + "\n", + "In this example, for the species `kinetic_ions` some particle orbits have been saved. Let us plot them:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7af3facd", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "\n", + "fig = plt.figure()\n", + "ax = fig.gca()\n", + "\n", + "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "\n", + "time = 0.\n", + "dt = time_opts.dt\n", + "Tend = time_opts.Tend\n", + "for k, v in simdata.pic_species[\"kinetic_ions\"][\"orbits\"].items():\n", + " # print(f\"{v[0] = }\")\n", + " alpha = (Tend - time)/Tend\n", + " for i, particle in enumerate(v):\n", + " ax.scatter(particle[0], particle[1], c=colors[i % 4], alpha=alpha)\n", + " time += dt\n", + " \n", + "ax.plot([l1, l1], [l2, r2], 'k')\n", + "ax.plot([r1, r1], [l2, r2], 'k')\n", + "ax.plot([l1, r1], [l2, l2], 'k')\n", + "ax.plot([l1, r1], [r2, r2], 'k')\n", + "ax.set_xlabel('x')\n", + "ax.set_ylabel('y')\n", + "ax.set_xlim(-6.5, 6.5)\n", + "ax.set_ylim(-9, 9)\n", + "ax.set_title(f'{int(Tend/dt)} time steps (full color at t=0)');" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials_old/tutorial_01_particles.ipynb b/tutorials_old/tutorial_01_particles.ipynb new file mode 100644 index 000000000..dc4bc05b5 --- /dev/null +++ b/tutorials_old/tutorial_01_particles.ipynb @@ -0,0 +1,273 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d34c79c5", + "metadata": {}, + "source": [ + "# Parameter files\n", + "\n", + "Struphy parameter files are Python scripts (.py) that can be executed with the Python interpreter.\n", + "For each `MODEL`, the default parameter file can be generated from the console via\n", + "\n", + "```\n", + "struphy params MODEL\n", + "```\n", + "\n", + "This will create a file `params_MODEL.py` in the current working directory. To run the model type\n", + "\n", + "```\n", + "python params_MODEL.py\n", + "```\n", + "\n", + "The user should modify the parameter file to launch a specific simulation.\n", + "As an example, in what follows we discuss the parameter file of the model [Vlasov](https://struphy.pages.mpcdf.de/struphy/sections/subsections/models_toy.html#struphy.models.toy.Vlasov) and run some simple examples. The file can be generated from\n", + "\n", + "```\n", + "struphy params Vlasov\n", + "```\n", + "\n", + "To see its contents, open the file in your preferred editor or type\n", + "\n", + "```\n", + "cat params_Vlasov.py\n", + "```\n", + "\n", + "## Part 1: imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ecab659", + "metadata": {}, + "outputs": [], + "source": [ + "from struphy.io.options import EnvironmentOptions, Units, Time\n", + "from struphy.geometry import domains\n", + "from struphy.fields_background import equils\n", + "from struphy.topology import grids\n", + "from struphy.io.options import DerhamOptions\n", + "from struphy.io.options import FieldsBackground\n", + "from struphy.initial import perturbations\n", + "from struphy.kinetic_background import maxwellians\n", + "from struphy.pic.utilities import LoadingParameters, WeightsParameters, BoundaryParameters\n", + "from struphy import main\n", + "\n", + "# import model, set verbosity\n", + "from struphy.models.toy import Vlasov as Model\n", + "verbose = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc43d2fc", + "metadata": {}, + "outputs": [], + "source": [ + "# environment options\n", + "env = EnvironmentOptions()\n", + "\n", + "# units\n", + "units = Units()\n", + "\n", + "# time stepping\n", + "time_opts = Time(dt=0.2, Tend=10.0)\n", + "\n", + "# geometry\n", + "l1 = -5.0\n", + "r1 = 5.0\n", + "l2 = -7.0\n", + "r2 = 7.0\n", + "l3 = -1.0\n", + "r3 = 1.0\n", + "domain = domains.Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)\n", + "\n", + "# fluid equilibrium (can be used as part of initial conditions)\n", + "equil = None\n", + "\n", + "# grid\n", + "grid = None\n", + "\n", + "# derham options\n", + "derham_opts = None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c498fb3", + "metadata": {}, + "outputs": [], + "source": [ + "# light-weight model instance\n", + "model = Model()\n", + "\n", + "# species parameters\n", + "model.kinetic_ions.set_phys_params()\n", + "\n", + "loading_params = LoadingParameters(Np=15)\n", + "weights_params = WeightsParameters()\n", + "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", + "model.kinetic_ions.set_markers(loading_params=loading_params, \n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params)\n", + "model.kinetic_ions.set_sorting_boxes()\n", + "model.kinetic_ions.set_save_data(n_markers=1.0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be6875e7", + "metadata": {}, + "outputs": [], + "source": [ + "# propagator options\n", + "model.propagators.push_vxb.set_options()\n", + "model.propagators.push_eta.set_options()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc0ac424", + "metadata": {}, + "outputs": [], + "source": [ + "# initial conditions (background + perturbation)\n", + "perturbation = None\n", + "\n", + "background = maxwellians.Maxwellian3D(n=(1.0, perturbation))\n", + "model.kinetic_ions.var.add_background(background)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "03764138", + "metadata": {}, + "outputs": [], + "source": [ + "main.run(model, \n", + " params_path=None, \n", + " env=env, \n", + " units=units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "f9ce5099", + "metadata": {}, + "source": [ + "## Post processing" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd7cfe62", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "path = os.path.join(os.getcwd(), \"sim_1\")\n", + "main.pproc(path, physical=True)" + ] + }, + { + "cell_type": "markdown", + "id": "e317f88d", + "metadata": {}, + "source": [ + "## Viewing particle orbits" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78b8cbab", + "metadata": {}, + "outputs": [], + "source": [ + "simdata = main.load_data(path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a329eb38", + "metadata": {}, + "outputs": [], + "source": [ + "for k, v in simdata.pic_species[\"kinetic_ions\"][\"orbits\"].items():\n", + " print(f\"{k = }, {type(v) = }\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7af3facd", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "\n", + "fig = plt.figure()\n", + "ax = fig.gca()\n", + "\n", + "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "\n", + "time = 0.\n", + "dt = time_opts.dt\n", + "Tend = time_opts.Tend\n", + "for k, v in simdata.pic_species[\"kinetic_ions\"][\"orbits\"].items():\n", + " # print(k, v)\n", + " alpha = (Tend - time)/Tend\n", + " for i, particle in enumerate(v):\n", + " ax.scatter(particle[1], particle[2], c=colors[i % 4], alpha=alpha)\n", + " time += dt\n", + " \n", + "ax.plot([l1, l1], [l2, r2], 'k')\n", + "ax.plot([r1, r1], [l2, r2], 'k')\n", + "ax.plot([l1, r1], [l2, l2], 'k')\n", + "ax.plot([l1, r1], [r2, r2], 'k')\n", + "ax.set_xlabel('x')\n", + "ax.set_ylabel('y')\n", + "ax.set_xlim(-6.5, 6.5)\n", + "ax.set_ylim(-9, 9)\n", + "ax.set_title(f'{int(Tend/dt)} time steps (full color at t=0)');" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/doc/tutorials/tutorial_02_fluid_particles.ipynb b/tutorials_old/tutorial_02_fluid_particles.ipynb similarity index 100% rename from doc/tutorials/tutorial_02_fluid_particles.ipynb rename to tutorials_old/tutorial_02_fluid_particles.ipynb diff --git a/doc/tutorials/tutorial_03_discrete_derham.ipynb b/tutorials_old/tutorial_03_discrete_derham.ipynb similarity index 100% rename from doc/tutorials/tutorial_03_discrete_derham.ipynb rename to tutorials_old/tutorial_03_discrete_derham.ipynb diff --git a/doc/tutorials/tutorial_06_poisson.ipynb b/tutorials_old/tutorial_06_poisson.ipynb similarity index 100% rename from doc/tutorials/tutorial_06_poisson.ipynb rename to tutorials_old/tutorial_06_poisson.ipynb diff --git a/doc/tutorials/tutorial_07_heat_equation.ipynb b/tutorials_old/tutorial_07_heat_equation.ipynb similarity index 100% rename from doc/tutorials/tutorial_07_heat_equation.ipynb rename to tutorials_old/tutorial_07_heat_equation.ipynb diff --git a/doc/tutorials/tutorial_08_maxwell.ipynb b/tutorials_old/tutorial_08_maxwell.ipynb similarity index 100% rename from doc/tutorials/tutorial_08_maxwell.ipynb rename to tutorials_old/tutorial_08_maxwell.ipynb diff --git a/doc/tutorials/tutorial_09_vlasov_maxwell.ipynb b/tutorials_old/tutorial_09_vlasov_maxwell.ipynb similarity index 100% rename from doc/tutorials/tutorial_09_vlasov_maxwell.ipynb rename to tutorials_old/tutorial_09_vlasov_maxwell.ipynb diff --git a/doc/tutorials/tutorial_10_linear_mhd.ipynb b/tutorials_old/tutorial_10_linear_mhd.ipynb similarity index 100% rename from doc/tutorials/tutorial_10_linear_mhd.ipynb rename to tutorials_old/tutorial_10_linear_mhd.ipynb diff --git a/doc/tutorials/tutorial_12_struphy_data_pproc.ipynb b/tutorials_old/tutorial_12_struphy_data_pproc.ipynb similarity index 100% rename from doc/tutorials/tutorial_12_struphy_data_pproc.ipynb rename to tutorials_old/tutorial_12_struphy_data_pproc.ipynb From a010f7ec2952cf8cd579cb5b91130f2496881913 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 30 Oct 2025 17:05:02 +0100 Subject: [PATCH 04/83] Add trailing commas (#73) Redo of https://github.com/struphy-hub/struphy/pull/36 Added trailing commas. This can be merged into devel after 318 is done. --------- Co-authored-by: Stefan Possanner Co-authored-by: Byung Kyu Na Co-authored-by: Stefan Possanner <86720346+spossann@users.noreply.github.com> --- .github/issue_template.md | 13 + .github/pull_request_template.md | 15 + src/struphy/bsplines/bsplines_kernels.py | 16 +- src/struphy/bsplines/evaluation_kernels_1d.py | 7 +- src/struphy/bsplines/evaluation_kernels_3d.py | 289 +++- .../bsplines/tests/test_eval_spline_mpi.py | 11 +- src/struphy/console/compile.py | 18 +- src/struphy/console/format.py | 22 +- src/struphy/console/params.py | 2 +- src/struphy/console/profile.py | 4 +- src/struphy/console/run.py | 2 +- src/struphy/console/tests/test_console.py | 26 +- src/struphy/diagnostics/continuous_spectra.py | 9 +- src/struphy/diagnostics/diagn_tools.py | 4 +- .../diagnostics/paraview/mesh_creator.py | 7 +- src/struphy/dispersion_relations/analytic.py | 14 +- .../kernels_projectors_global.py | 62 +- .../legacy/MHD_eigenvalues_cylinder_1D.py | 45 +- .../fB_massless_control_variate.py | 2 +- .../fB_massless_kernels_control_variate.py | 42 +- .../fnB_massless_control_variate.py | 4 +- .../fnB_massless_kernels_control_variate.py | 56 +- .../massless_control_variate.py | 4 +- .../massless_kernels_control_variate.py | 128 +- .../legacy/emw_operators.py | 6 +- .../legacy/mass_matrices_3d_pre.py | 18 +- .../legacy/massless_operators/fB_bv_kernel.py | 24 +- .../fB_massless_linear_operators.py | 33 +- .../legacy/massless_operators/fB_vv_kernel.py | 6 +- .../pro_local/mhd_operators_3d_local.py | 116 +- .../pro_local/projectors_local.py | 48 +- .../shape_L2_projector_kernel.py | 54 +- .../shape_function_projectors_L2.py | 37 +- .../shape_function_projectors_local.py | 41 +- .../shape_local_projector_kernel.py | 141 +- .../eigenvalue_solvers/mass_matrices_3d.py | 9 +- .../mhd_axisymmetric_main.py | 14 +- .../mhd_axisymmetric_pproc.py | 5 +- .../eigenvalue_solvers/mhd_operators.py | 45 +- .../eigenvalue_solvers/mhd_operators_core.py | 284 ++-- .../eigenvalue_solvers/projectors_global.py | 91 +- .../eigenvalue_solvers/spline_space.py | 109 +- src/struphy/examples/_draw_parallel.py | 2 +- src/struphy/feec/basis_projection_ops.py | 14 +- src/struphy/feec/linear_operators.py | 4 +- src/struphy/feec/local_projectors_kernels.py | 20 +- src/struphy/feec/mass.py | 56 +- src/struphy/feec/mass_kernels.py | 12 +- src/struphy/feec/preconditioner.py | 2 +- src/struphy/feec/projectors.py | 27 +- src/struphy/feec/psydac_derham.py | 16 +- src/struphy/feec/tests/test_basis_ops.py | 13 +- src/struphy/feec/tests/test_derham.py | 32 +- src/struphy/feec/tests/test_eval_field.py | 18 +- src/struphy/feec/tests/test_field_init.py | 89 +- src/struphy/feec/tests/test_l2_projectors.py | 10 +- .../feec/tests/test_local_projectors.py | 46 +- .../feec/tests/test_lowdim_nel_is_1.py | 14 +- src/struphy/feec/tests/test_mass_matrices.py | 62 +- .../feec/tests/test_toarray_struphy.py | 3 +- .../feec/tests/test_tosparse_struphy.py | 23 +- src/struphy/feec/utilities.py | 12 +- .../feec/utilities_local_projectors.py | 23 +- src/struphy/feec/variational_utilities.py | 23 +- src/struphy/fields_background/base.py | 15 +- .../fields_background/coil_fields/base.py | 6 +- .../coil_fields/coil_fields.py | 20 +- src/struphy/fields_background/equils.py | 36 +- .../mhd_equil/eqdsk/readeqdsk.py | 6 +- .../tests/test_desc_equil.py | 12 +- src/struphy/geometry/base.py | 10 +- src/struphy/geometry/domains.py | 2 +- src/struphy/geometry/mappings_kernels.py | 192 ++- src/struphy/geometry/transform_kernels.py | 24 +- src/struphy/initial/eigenfunctions.py | 11 +- src/struphy/initial/perturbations.py | 2 +- .../initial/tests/test_init_perturbations.py | 18 +- src/struphy/io/output_handling.py | 8 +- src/struphy/io/setup.py | 36 +- src/struphy/kinetic_background/base.py | 6 +- src/struphy/kinetic_background/maxwellians.py | 6 +- .../tests/test_maxwellians.py | 142 +- src/struphy/linear_algebra/linalg_kron.py | 15 +- src/struphy/linear_algebra/saddle_point.py | 15 +- .../tests/test_saddle_point_propagator.py | 2 +- .../tests/test_saddlepoint_massmatrices.py | 2 +- src/struphy/main.py | 34 +- src/struphy/models/base.py | 12 +- src/struphy/models/fluid.py | 136 +- src/struphy/models/hybrid.py | 30 +- src/struphy/models/kinetic.py | 6 +- src/struphy/models/species.py | 6 +- src/struphy/models/tests/test_models.py | 10 +- .../models/tests/test_verif_EulerSPH.py | 6 +- .../models/tests/test_verif_LinearMHD.py | 6 +- .../models/tests/test_verif_Maxwell.py | 22 +- .../models/tests/test_verif_Poisson.py | 6 +- .../test_verif_VlasovAmpereOneSpecies.py | 4 +- src/struphy/models/tests/test_xxpproc.py | 2 +- src/struphy/models/tests/verification.py | 40 +- src/struphy/models/toy.py | 25 +- src/struphy/models/variables.py | 6 +- src/struphy/ode/tests/test_ode_feec.py | 12 +- .../pic/accumulation/accum_kernels_gc.py | 110 +- .../accumulation/particle_to_mat_kernels.py | 42 +- src/struphy/pic/base.py | 144 +- src/struphy/pic/particles.py | 3 +- src/struphy/pic/pushing/pusher.py | 18 +- src/struphy/pic/sobol_seq.py | 14 +- src/struphy/pic/sph_eval_kernels.py | 14 +- src/struphy/pic/tests/test_accum_vec_H1.py | 7 +- src/struphy/pic/tests/test_accumulation.py | 3 +- src/struphy/pic/tests/test_binning.py | 16 +- src/struphy/pic/tests/test_mat_vec_filler.py | 30 +- .../test_pic_legacy_files/accumulation.py | 12 +- .../accumulation_kernels_3d.py | 192 ++- .../test_pic_legacy_files/mappings_3d.py | 212 ++- .../test_pic_legacy_files/mappings_3d_fast.py | 305 +++- .../tests/test_pic_legacy_files/pusher_pos.py | 1257 +++++++++++++++-- .../test_pic_legacy_files/pusher_vel_2d.py | 200 ++- .../test_pic_legacy_files/pusher_vel_3d.py | 294 +++- .../spline_evaluation_3d.py | 266 +++- src/struphy/pic/tests/test_pushers.py | 24 +- src/struphy/pic/tests/test_sorting.py | 3 +- src/struphy/pic/tests/test_sph.py | 66 +- src/struphy/pic/tests/test_tesselation.py | 2 +- src/struphy/polar/basic.py | 2 +- src/struphy/polar/extraction_operators.py | 11 +- src/struphy/polar/linear_operators.py | 9 +- .../likwid/plot_likwidproject.py | 2 +- .../likwid/plot_time_traces.py | 6 +- src/struphy/post_processing/pproc_struphy.py | 26 +- .../post_processing/profile_struphy.py | 4 +- src/struphy/propagators/base.py | 4 +- .../propagators/propagators_coupling.py | 12 +- src/struphy/propagators/propagators_fields.py | 156 +- .../propagators/propagators_markers.py | 4 +- .../tests/test_gyrokinetic_poisson.py | 29 +- src/struphy/propagators/tests/test_poisson.py | 25 +- src/struphy/utils/clone_config.py | 2 +- src/struphy/utils/test_clone_config.py | 6 +- src/struphy/utils/utils.py | 6 +- 142 files changed, 5415 insertions(+), 1476 deletions(-) create mode 100644 .github/issue_template.md create mode 100644 .github/pull_request_template.md diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 000000000..b2c4eb7bc --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,13 @@ +**Bug description / feature request:** + +... + +**Expected behavior:** + +... + +**Proposed solution:** + +... + + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..87f9ff3cc --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +**Solves the following issue(s):** + +Closes #... + +**Core changes:** + +None + +**Model-specific changes:** + +None + +**Documentation changes:** + +None \ No newline at end of file diff --git a/src/struphy/bsplines/bsplines_kernels.py b/src/struphy/bsplines/bsplines_kernels.py index 17374f178..9fa1a9521 100644 --- a/src/struphy/bsplines/bsplines_kernels.py +++ b/src/struphy/bsplines/bsplines_kernels.py @@ -83,7 +83,13 @@ def find_span(t: "Final[float[:]]", p: "int", eta: "float") -> "int": @pure def basis_funs( - t: "Final[float[:]]", p: "int", eta: "float", span: "int", left: "float[:]", right: "float[:]", values: "float[:]" + t: "Final[float[:]]", + p: "int", + eta: "float", + span: "int", + left: "float[:]", + right: "float[:]", + values: "float[:]", ): """ Parameters @@ -595,7 +601,13 @@ def basis_funs_and_der( @pure @stack_array("values_b") def basis_funs_1st_der( - t: "Final[float[:]]", p: "int", eta: "float", span: "int", left: "float[:]", right: "float[:]", values: "float[:]" + t: "Final[float[:]]", + p: "int", + eta: "float", + span: "int", + left: "float[:]", + right: "float[:]", + values: "float[:]", ): """ Parameters diff --git a/src/struphy/bsplines/evaluation_kernels_1d.py b/src/struphy/bsplines/evaluation_kernels_1d.py index a6ec8b7a5..6510eafff 100644 --- a/src/struphy/bsplines/evaluation_kernels_1d.py +++ b/src/struphy/bsplines/evaluation_kernels_1d.py @@ -61,7 +61,12 @@ def evaluation_kernel_1d(p1: int, basis1: "Final[float[:]]", ind1: "Final[int[:] @pure @stack_array("tmp1", "tmp2") def evaluate( - kind1: int, t1: "Final[float[:]]", p1: int, ind1: "Final[int[:,:]]", coeff: "Final[float[:]]", eta1: float + kind1: int, + t1: "Final[float[:]]", + p1: int, + ind1: "Final[int[:,:]]", + coeff: "Final[float[:]]", + eta1: float, ) -> float: """ Point-wise evaluation of a spline. diff --git a/src/struphy/bsplines/evaluation_kernels_3d.py b/src/struphy/bsplines/evaluation_kernels_3d.py index 8ccaa252b..a6c900616 100644 --- a/src/struphy/bsplines/evaluation_kernels_3d.py +++ b/src/struphy/bsplines/evaluation_kernels_3d.py @@ -246,47 +246,212 @@ def evaluate_tensor_product( for i3 in range(len(eta3)): if kind == 0: spline_values[i1, i2, i3] = evaluate_3d( - 1, 1, 1, t1, t2, t3, p1, p2, p3, ind1, ind2, ind3, coeff, eta1[i1], eta2[i2], eta3[i3] + 1, + 1, + 1, + t1, + t2, + t3, + p1, + p2, + p3, + ind1, + ind2, + ind3, + coeff, + eta1[i1], + eta2[i2], + eta3[i3], ) elif kind == 11: spline_values[i1, i2, i3] = evaluate_3d( - 2, 1, 1, t1, t2, t3, p1, p2, p3, ind1, ind2, ind3, coeff, eta1[i1], eta2[i2], eta3[i3] + 2, + 1, + 1, + t1, + t2, + t3, + p1, + p2, + p3, + ind1, + ind2, + ind3, + coeff, + eta1[i1], + eta2[i2], + eta3[i3], ) elif kind == 12: spline_values[i1, i2, i3] = evaluate_3d( - 1, 2, 1, t1, t2, t3, p1, p2, p3, ind1, ind2, ind3, coeff, eta1[i1], eta2[i2], eta3[i3] + 1, + 2, + 1, + t1, + t2, + t3, + p1, + p2, + p3, + ind1, + ind2, + ind3, + coeff, + eta1[i1], + eta2[i2], + eta3[i3], ) elif kind == 13: spline_values[i1, i2, i3] = evaluate_3d( - 1, 1, 2, t1, t2, t3, p1, p2, p3, ind1, ind2, ind3, coeff, eta1[i1], eta2[i2], eta3[i3] + 1, + 1, + 2, + t1, + t2, + t3, + p1, + p2, + p3, + ind1, + ind2, + ind3, + coeff, + eta1[i1], + eta2[i2], + eta3[i3], ) elif kind == 21: spline_values[i1, i2, i3] = evaluate_3d( - 1, 2, 2, t1, t2, t3, p1, p2, p3, ind1, ind2, ind3, coeff, eta1[i1], eta2[i2], eta3[i3] + 1, + 2, + 2, + t1, + t2, + t3, + p1, + p2, + p3, + ind1, + ind2, + ind3, + coeff, + eta1[i1], + eta2[i2], + eta3[i3], ) elif kind == 22: spline_values[i1, i2, i3] = evaluate_3d( - 2, 1, 2, t1, t2, t3, p1, p2, p3, ind1, ind2, ind3, coeff, eta1[i1], eta2[i2], eta3[i3] + 2, + 1, + 2, + t1, + t2, + t3, + p1, + p2, + p3, + ind1, + ind2, + ind3, + coeff, + eta1[i1], + eta2[i2], + eta3[i3], ) elif kind == 23: spline_values[i1, i2, i3] = evaluate_3d( - 2, 2, 1, t1, t2, t3, p1, p2, p3, ind1, ind2, ind3, coeff, eta1[i1], eta2[i2], eta3[i3] + 2, + 2, + 1, + t1, + t2, + t3, + p1, + p2, + p3, + ind1, + ind2, + ind3, + coeff, + eta1[i1], + eta2[i2], + eta3[i3], ) elif kind == 3: spline_values[i1, i2, i3] = evaluate_3d( - 2, 2, 2, t1, t2, t3, p1, p2, p3, ind1, ind2, ind3, coeff, eta1[i1], eta2[i2], eta3[i3] + 2, + 2, + 2, + t1, + t2, + t3, + p1, + p2, + p3, + ind1, + ind2, + ind3, + coeff, + eta1[i1], + eta2[i2], + eta3[i3], ) elif kind == 41: spline_values[i1, i2, i3] = evaluate_3d( - 3, 1, 1, t1, t2, t3, p1, p2, p3, ind1, ind2, ind3, coeff, eta1[i1], eta2[i2], eta3[i3] + 3, + 1, + 1, + t1, + t2, + t3, + p1, + p2, + p3, + ind1, + ind2, + ind3, + coeff, + eta1[i1], + eta2[i2], + eta3[i3], ) elif kind == 42: spline_values[i1, i2, i3] = evaluate_3d( - 1, 3, 1, t1, t2, t3, p1, p2, p3, ind1, ind2, ind3, coeff, eta1[i1], eta2[i2], eta3[i3] + 1, + 3, + 1, + t1, + t2, + t3, + p1, + p2, + p3, + ind1, + ind2, + ind3, + coeff, + eta1[i1], + eta2[i2], + eta3[i3], ) elif kind == 43: spline_values[i1, i2, i3] = evaluate_3d( - 1, 1, 3, t1, t2, t3, p1, p2, p3, ind1, ind2, ind3, coeff, eta1[i1], eta2[i2], eta3[i3] + 1, + 1, + 3, + t1, + t2, + t3, + p1, + p2, + p3, + ind1, + ind2, + ind3, + coeff, + eta1[i1], + eta2[i2], + eta3[i3], ) @@ -1051,7 +1216,17 @@ def eval_spline_mpi( b3 = bd3 value = eval_spline_mpi_kernel( - pn[0] - kind[0], pn[1] - kind[1], pn[2] - kind[2], b1, b2, b3, span1, span2, span3, _data, starts + pn[0] - kind[0], + pn[1] - kind[1], + pn[2] - kind[2], + b1, + b2, + b3, + span1, + span2, + span3, + _data, + starts, ) return value @@ -1196,7 +1371,17 @@ def eval_spline_mpi_tensor_product_fast( b3 = bd3 values[i, j, k] = eval_spline_mpi_kernel( - pn[0] - kind[0], pn[1] - kind[1], pn[2] - kind[2], b1, b2, b3, span1, span2, span3, _data, starts + pn[0] - kind[0], + pn[1] - kind[1], + pn[2] - kind[2], + b1, + b2, + b3, + span1, + span2, + span3, + _data, + starts, ) @@ -1262,7 +1447,17 @@ def eval_spline_mpi_tensor_product_fixed( b3[:] = b3s[k, :] values[i, j, k] = eval_spline_mpi_kernel( - pn[0] - kind[0], pn[1] - kind[1], pn[2] - kind[2], b1, b2, b3, span1, span2, span3, _data, starts + pn[0] - kind[0], + pn[1] - kind[1], + pn[2] - kind[2], + b1, + b2, + b3, + span1, + span2, + span3, + _data, + starts, ) @@ -1320,7 +1515,16 @@ def eval_spline_mpi_matrix( continue # point not in process domain values[i, j, k] = eval_spline_mpi( - eta1[i, j, k], eta2[i, j, k], eta3[i, j, k], _data, kind, pn, tn1, tn2, tn3, starts + eta1[i, j, k], + eta2[i, j, k], + eta3[i, j, k], + _data, + kind, + pn, + tn1, + tn2, + tn3, + starts, ) @@ -1382,7 +1586,16 @@ def eval_spline_mpi_sparse_meshgrid( continue # point not in process domain values[i, j, k] = eval_spline_mpi( - eta1[i, 0, 0], eta2[0, j, 0], eta3[0, 0, k], _data, kind, pn, tn1, tn2, tn3, starts + eta1[i, 0, 0], + eta2[0, j, 0], + eta3[0, 0, k], + _data, + kind, + pn, + tn1, + tn2, + tn3, + starts, ) @@ -1432,7 +1645,16 @@ def eval_spline_mpi_markers( continue # point not in process domain values[ip] = eval_spline_mpi( - markers[ip, 0], markers[ip, 1], markers[ip, 2], _data, kind, pn, tn1, tn2, tn3, starts + markers[ip, 0], + markers[ip, 1], + markers[ip, 2], + _data, + kind, + pn, + tn1, + tn2, + tn3, + starts, ) @@ -1453,20 +1675,39 @@ def get_spans(eta1: float, eta2: float, eta3: float, args_derham: "DerhamArgumen # get spline values at eta bsplines_kernels.b_d_splines_slim( - args_derham.tn1, args_derham.pn[0], eta1, int(span1), args_derham.bn1, args_derham.bd1 + args_derham.tn1, + args_derham.pn[0], + eta1, + int(span1), + args_derham.bn1, + args_derham.bd1, ) bsplines_kernels.b_d_splines_slim( - args_derham.tn2, args_derham.pn[1], eta2, int(span2), args_derham.bn2, args_derham.bd2 + args_derham.tn2, + args_derham.pn[1], + eta2, + int(span2), + args_derham.bn2, + args_derham.bd2, ) bsplines_kernels.b_d_splines_slim( - args_derham.tn3, args_derham.pn[2], eta3, int(span3), args_derham.bn3, args_derham.bd3 + args_derham.tn3, + args_derham.pn[2], + eta3, + int(span3), + args_derham.bn3, + args_derham.bd3, ) return span1, span2, span3 def eval_0form_spline_mpi( - span1: int, span2: int, span3: int, args_derham: "DerhamArguments", form_coeffs: "float[:,:,:]" + span1: int, + span2: int, + span3: int, + args_derham: "DerhamArguments", + form_coeffs: "float[:,:,:]", ) -> float: """Single-point evaluation of Derham 0-form spline defined by form_coeffs, given N-spline values (in bn) and knot span indices span.""" @@ -1602,7 +1843,11 @@ def eval_2form_spline_mpi( def eval_3form_spline_mpi( - span1: int, span2: int, span3: int, args_derham: "DerhamArguments", form_coeffs: "float[:,:,:]" + span1: int, + span2: int, + span3: int, + args_derham: "DerhamArguments", + form_coeffs: "float[:,:,:]", ) -> float: """Single-point evaluation of Derham 0-form spline defined by form_coeffs, given D-spline values (in bd) and knot span indices span.""" diff --git a/src/struphy/bsplines/tests/test_eval_spline_mpi.py b/src/struphy/bsplines/tests/test_eval_spline_mpi.py index 0703fb418..923fc8ea6 100644 --- a/src/struphy/bsplines/tests/test_eval_spline_mpi.py +++ b/src/struphy/bsplines/tests/test_eval_spline_mpi.py @@ -700,7 +700,10 @@ def test_eval_tensor_product_grid(Nel, p, spl_kind, n_markers=10): # Histopolation grids spaces = derham.Vh_fem["3"].spaces ptsG, wtsG, spans, bases, subs = prepare_projection_of_basis( - spaces, spaces, derham.Vh["3"].starts, derham.Vh["3"].ends + spaces, + spaces, + derham.Vh["3"].starts, + derham.Vh["3"].ends, ) eta1s = ptsG[0].flatten() eta2s = ptsG[1].flatten() @@ -715,9 +718,9 @@ def test_eval_tensor_product_grid(Nel, p, spl_kind, n_markers=10): comm.Barrier() sleep(0.02 * (rank + 1)) - print(f"rank {rank} | {eta1s = }") - print(f"rank {rank} | {eta2s = }") - print(f"rank {rank} | {eta3s = }\n") + print(f"rank {rank} | {eta1s =}") + print(f"rank {rank} | {eta2s =}") + print(f"rank {rank} | {eta3s =}\n") comm.Barrier() # compare spline evaluation routines diff --git a/src/struphy/console/compile.py b/src/struphy/console/compile.py index 0cb628b18..432e4fa1f 100644 --- a/src/struphy/console/compile.py +++ b/src/struphy/console/compile.py @@ -4,7 +4,17 @@ def struphy_compile( - language, compiler, compiler_config, omp_pic, omp_feec, delete, status, verbose, dependencies, time_execution, yes + language, + compiler, + compiler_config, + omp_pic, + omp_feec, + delete, + status, + verbose, + dependencies, + time_execution, + yes, ): """Compile Struphy kernels. All files that contain "kernels" are detected automatically and saved to state.yml. @@ -187,9 +197,9 @@ def struphy_compile( deps = depmod.get_dependencies(ker.replace(".py", so_suffix)) deps_li = deps.split(" ") print("-" * 28) - print(f"{ker = }") + print(f"{ker =}") for dep in deps_li: - print(f"{dep = }") + print(f"{dep =}") else: # struphy and psydac (change dir not to be in source path) @@ -258,7 +268,7 @@ def struphy_compile( # only install (from .whl) if psydac not up-to-date if psydac_ver < struphy_ver: print( - f"You have psydac version {psydac_ver}, but version {struphy_ver} is available. Please re-install struphy (e.g. pip install .)\n" + f"You have psydac version {psydac_ver}, but version {struphy_ver} is available. Please re-install struphy (e.g. pip install .)\n", ) sys.exit(1) else: diff --git a/src/struphy/console/format.py b/src/struphy/console/format.py index 747e2d0c1..eec22f784 100644 --- a/src/struphy/console/format.py +++ b/src/struphy/console/format.py @@ -576,7 +576,7 @@ def parse_json_file_to_html(json_file_path, html_output_path): "", "", "Code Analysis Report", - ] + ], ) # Include external CSS and JS libraries @@ -586,7 +586,7 @@ def parse_json_file_to_html(json_file_path, html_output_path): "", "", "", - ] + ], ) # Custom CSS for light mode and code prettification @@ -700,7 +700,7 @@ def parse_json_file_to_html(json_file_path, html_output_path): text-align: center; color: #999999; } -""" +""", ) html_content.append("") @@ -715,7 +715,7 @@ def parse_json_file_to_html(json_file_path, html_output_path): }); }); -""" +""", ) html_content.extend(["", "", "

Code Issues Report

"]) @@ -729,7 +729,7 @@ def parse_json_file_to_html(json_file_path, html_output_path):

Total Issues: {total_issues}

Number of files: {total_files}

-""" +""", ) # Navigation menu @@ -753,7 +753,7 @@ def parse_json_file_to_html(json_file_path, html_output_path): f"""
File: {display_name} -""" +""", ) issue_data = {} @@ -803,7 +803,7 @@ def parse_json_file_to_html(json_file_path, html_output_path): f"{code} - " f"{message}
" f"Location: " - f"{display_name}:{row}:{column}
" + f"{display_name}:{row}:{column}
", ) html_content.append("

") @@ -846,7 +846,7 @@ def parse_json_file_to_html(json_file_path, html_output_path): html_content.append( # f"
" # f"{line_number}{line_content}
" - f"{line_number}: {line_content}" + f"{line_number}: {line_content}", ) html_content.append("") # Include fix details if available @@ -854,12 +854,12 @@ def parse_json_file_to_html(json_file_path, html_output_path): html_content.append("
") html_content.append( f"

Fix Available ({fix.get('applicability', 'Unknown')}): " - f"ruff check --select ALL --fix {display_name}

" + f"ruff check --select ALL --fix {display_name}

", ) html_content.append("
") else: html_content.append( - f"

Cannot read file {filename} or invalid row {row}.

" + f"

Cannot read file {filename} or invalid row {row}.

", ) html_content.append("") @@ -873,7 +873,7 @@ def parse_json_file_to_html(json_file_path, html_output_path):

Generated by on {time.strftime("%Y-%m-%d %H:%M:%S")}

-""" +""", ) html_content.extend(["", ""]) diff --git a/src/struphy/console/params.py b/src/struphy/console/params.py index ade6c5ea5..4916cd1c0 100644 --- a/src/struphy/console/params.py +++ b/src/struphy/console/params.py @@ -29,7 +29,7 @@ def struphy_params(model_name: str, params_path: str, yes: bool = False, check_f except AttributeError: pass - print(f"{model_name = }") + print(f"{model_name =}") # print units if check_file: diff --git a/src/struphy/console/profile.py b/src/struphy/console/profile.py index 9577a00d9..a2f10ce66 100644 --- a/src/struphy/console/profile.py +++ b/src/struphy/console/profile.py @@ -106,7 +106,7 @@ def struphy_profile(dirs, replace, all, n_lines, print_callers, savefig): + "ncalls".ljust(15) + "tottime".ljust(15) + "percall".ljust(15) - + "cumtime".ljust(15) + + "cumtime".ljust(15), ) print("-" * 154) for position, key in enumerate(dicts[0].keys()): @@ -207,7 +207,7 @@ def struphy_profile(dirs, replace, all, n_lines, print_callers, savefig): ax.set( title="Weak scaling for cells/mpi_size=" + str(xp.prod(val["Nel"][0]) / val["mpi_size"][0]) - + "=const." + + "=const.", ) ax.legend(loc="upper left") # ax.loglog(val['mpi_size'], val['time'][0]*xp.ones_like(val['time']), 'k--', alpha=0.3) diff --git a/src/struphy/console/run.py b/src/struphy/console/run.py index 1845230e9..3fa8ee56d 100644 --- a/src/struphy/console/run.py +++ b/src/struphy/console/run.py @@ -210,7 +210,7 @@ def struphy_run( if likwid: command = likwid_command + command + ["--likwid"] - print(f"Running with likwid with {likwid_repetitions = }") + print(f"Running with likwid with {likwid_repetitions =}") f.write(f"# Launching likwid {likwid_repetitions} times with likwid-mpirun\n") for i in range(likwid_repetitions): f.write(f"\n\n# Run number {i + 1:03}\n") diff --git a/src/struphy/console/tests/test_console.py b/src/struphy/console/tests/test_console.py index abdd51384..5855e7cc3 100644 --- a/src/struphy/console/tests/test_console.py +++ b/src/struphy/console/tests/test_console.py @@ -290,7 +290,7 @@ def mock_remove(path): # Otherwise, we will not remove all the *_tmp.py files # We can not use the real os.remove becuase then # the state and all compiled files will be removed - print(f"{path = }") + print(f"{path =}") if "_tmp.py" in path: print("Not mock remove") os_remove(path) @@ -318,19 +318,19 @@ def mock_remove(path): time_execution=time_execution, yes=yes, ) - print(f"{language = }") - print(f"{compiler = }") - print(f"{omp_pic = }") - print(f"{omp_feec = }") - print(f"{delete = }") + print(f"{language =}") + print(f"{compiler =}") + print(f"{omp_pic =}") + print(f"{omp_feec =}") + print(f"{delete =}") print(f"{status} = ") - print(f"{verbose = }") - print(f"{dependencies = }") - print(f"{time_execution = }") - print(f"{yes = }") - print(f"{mock_save_state.call_count = }") - print(f"{mock_subprocess_run.call_count = }") - print(f"{mock_os_remove.call_count = }") + print(f"{verbose =}") + print(f"{dependencies =}") + print(f"{time_execution =}") + print(f"{yes =}") + print(f"{mock_save_state.call_count =}") + print(f"{mock_subprocess_run.call_count =}") + print(f"{mock_os_remove.call_count =}") if delete: print("if delete") diff --git a/src/struphy/diagnostics/continuous_spectra.py b/src/struphy/diagnostics/continuous_spectra.py index 5ed069179..c31d789c7 100644 --- a/src/struphy/diagnostics/continuous_spectra.py +++ b/src/struphy/diagnostics/continuous_spectra.py @@ -157,7 +157,7 @@ def get_mhd_continua_2d(space, domain, omega2, U_eig, m_range, omega_A, div_tol, # parse arguments parser = argparse.ArgumentParser( - description="Looks for eigenmodes in a given MHD eigenspectrum in a certain poloidal mode number range and plots the continuous shear Alfvén and slow sound spectra (frequency versus radial-like coordinate)." + description="Looks for eigenmodes in a given MHD eigenspectrum in a certain poloidal mode number range and plots the continuous shear Alfvén and slow sound spectra (frequency versus radial-like coordinate).", ) parser.add_argument("m_l_alfvén", type=int, help="lower bound of poloidal mode number range for Alfvénic modes") @@ -252,7 +252,12 @@ def get_mhd_continua_2d(space, domain, omega2, U_eig, m_range, omega_A, div_tol, fem_1d_2 = Spline_space_1d(Nel[1], p[1], spl_kind[1], nq_el[1], dirichlet_bc[1]) fem_2d = Tensor_spline_space( - [fem_1d_1, fem_1d_2], polar_ck, domain.cx[:, :, 0], domain.cy[:, :, 0], n_tor=n_tor, basis_tor="i" + [fem_1d_1, fem_1d_2], + polar_ck, + domain.cx[:, :, 0], + domain.cy[:, :, 0], + n_tor=n_tor, + basis_tor="i", ) # load and analyze spectrum diff --git a/src/struphy/diagnostics/diagn_tools.py b/src/struphy/diagnostics/diagn_tools.py index d714306f4..b9e66dbb6 100644 --- a/src/struphy/diagnostics/diagn_tools.py +++ b/src/struphy/diagnostics/diagn_tools.py @@ -173,7 +173,7 @@ def power_spectrum_2d( # print(f"{intersec = }") # print(f"{[omega[intersec[n]] for n in range(fit_branches)]}") assert len(intersec) == fit_branches, ( - f"Number of found branches {len(intersec)} is not {fit_branches = }! \ + f"Number of found branches {len(intersec)} is not {fit_branches =}! \ Try to lower 'noise_level' or increase 'extr_order'." ) k_fit += [k] @@ -184,7 +184,7 @@ def power_spectrum_2d( coeffs = [] for m, om in omega_fit.items(): coeffs += [xp.polyfit(k_fit, om, deg=fit_degree[n])] - print(f"\nFitted {coeffs = }") + print(f"\nFitted {coeffs =}") if do_plot: _, ax = plt.subplots(1, 1, figsize=(10, 10)) diff --git a/src/struphy/diagnostics/paraview/mesh_creator.py b/src/struphy/diagnostics/paraview/mesh_creator.py index c6c8d89a0..4bf83211c 100644 --- a/src/struphy/diagnostics/paraview/mesh_creator.py +++ b/src/struphy/diagnostics/paraview/mesh_creator.py @@ -37,7 +37,12 @@ def make_ugrid_and_write_vtu(filename: str, writer, vtk_dir, gvec, s_range, u_ra point_data = {} cell_data = {} vtk_points, suv_points, xyz_points, point_indices = gen_vtk_points( - gvec, s_range, u_range, v_range, point_data, cell_data + gvec, + s_range, + u_range, + v_range, + point_data, + cell_data, ) print("vtk_points.GetNumberOfPoints()", vtk_points.GetNumberOfPoints(), flush=True) diff --git a/src/struphy/dispersion_relations/analytic.py b/src/struphy/dispersion_relations/analytic.py index fb7a40ccd..a54355428 100644 --- a/src/struphy/dispersion_relations/analytic.py +++ b/src/struphy/dispersion_relations/analytic.py @@ -261,7 +261,15 @@ class FluidSlabITG(DispersionRelations1D): def __init__(self, vstar=10.0, vi=1.0, Z=1.0, kz=1.0, gamma=5 / 3): super().__init__( - "wave 1", "wave 2", "wave 3", velocity_scale="thermal", vstar=vstar, vi=vi, Z=Z, kz=kz, gamma=gamma + "wave 1", + "wave 2", + "wave 3", + velocity_scale="thermal", + vstar=vstar, + vi=vi, + Z=Z, + kz=kz, + gamma=gamma, ) def __call__(self, k): @@ -1018,7 +1026,7 @@ def __call__(self, x, m, n): # slow sound continuum specs["slow_sound"] = xp.sqrt( - gamma * p(x) * F(x, m, n) ** 2 / (rho(x) * (gamma * p(x) + By(x) ** 2 + Bz(x) ** 2)) + gamma * p(x) * F(x, m, n) ** 2 / (rho(x) * (gamma * p(x) + By(x) ** 2 + Bz(x) ** 2)), ) return specs @@ -1125,7 +1133,7 @@ def __call__(self, r, m, n): # slow sound continuum specs["slow_sound"] = xp.sqrt( - gamma * p(r) * F(r, m, n) ** 2 / (rho(r) * (gamma * p(r) + Bt(r) ** 2 + Bz(r) ** 2)) + gamma * p(r) * F(r, m, n) ** 2 / (rho(r) * (gamma * p(r) + Bt(r) ** 2 + Bz(r) ** 2)), ) return specs diff --git a/src/struphy/eigenvalue_solvers/kernels_projectors_global.py b/src/struphy/eigenvalue_solvers/kernels_projectors_global.py index 4f6c01392..f01cefc1c 100644 --- a/src/struphy/eigenvalue_solvers/kernels_projectors_global.py +++ b/src/struphy/eigenvalue_solvers/kernels_projectors_global.py @@ -24,7 +24,13 @@ def kernel_int_2d(nq1: "int", nq2: "int", w1: "float[:]", w2: "float[:]", mat_f: # ========= kernel for integration in 3d ================== def kernel_int_3d( - nq1: "int", nq2: "int", nq3: "int", w1: "float[:]", w2: "float[:]", w3: "float[:]", mat_f: "float[:,:,:]" + nq1: "int", + nq2: "int", + nq3: "int", + w1: "float[:]", + w2: "float[:]", + w3: "float[:]", + mat_f: "float[:,:,:]", ) -> "float": f_loc = 0.0 @@ -47,7 +53,11 @@ def kernel_int_3d( # ========= kernel for integration along eta1 direction, reducing to a 2d array ============================ def kernel_int_2d_eta1( - subs1: "int[:]", subs_cum1: "int[:]", w1: "float[:,:]", mat_f: "float[:,:,:]", f_int: "float[:,:]" + subs1: "int[:]", + subs_cum1: "int[:]", + w1: "float[:,:]", + mat_f: "float[:,:,:]", + f_int: "float[:,:]", ): n1, n2 = shape(f_int) @@ -66,7 +76,11 @@ def kernel_int_2d_eta1( # ========= kernel for integration along eta2 direction, reducing to a 2d array ============================ def kernel_int_2d_eta2( - subs2: "int[:]", subs_cum2: "int[:]", w2: "float[:,:]", mat_f: "float[:,:,:]", f_int: "float[:,:]" + subs2: "int[:]", + subs_cum2: "int[:]", + w2: "float[:,:]", + mat_f: "float[:,:,:]", + f_int: "float[:,:]", ): n1, n2 = shape(f_int) @@ -167,7 +181,11 @@ def kernel_int_2d_eta1_eta2_old(w1: "float[:,:]", w2: "float[:,:]", mat_f: "floa # ========= kernel for integration along eta1 direction, reducing to a 3d array ============================ def kernel_int_3d_eta1( - subs1: "int[:]", subs_cum1: "int[:]", w1: "float[:,:]", mat_f: "float[:,:,:,:]", f_int: "float[:,:,:]" + subs1: "int[:]", + subs_cum1: "int[:]", + w1: "float[:,:]", + mat_f: "float[:,:,:,:]", + f_int: "float[:,:,:]", ): n1, n2, n3 = shape(f_int) @@ -186,7 +204,11 @@ def kernel_int_3d_eta1( def kernel_int_3d_eta1_transpose( - subs1: "int[:]", subs_cum1: "int[:]", w1: "float[:,:]", f_int: "float[:,:,:]", mat_f: "float[:,:,:,:]" + subs1: "int[:]", + subs_cum1: "int[:]", + w1: "float[:,:]", + f_int: "float[:,:,:]", + mat_f: "float[:,:,:,:]", ): n1, n2, n3 = shape(f_int) @@ -205,7 +227,11 @@ def kernel_int_3d_eta1_transpose( # ========= kernel for integration along eta2 direction, reducing to a 3d array ============================ def kernel_int_3d_eta2( - subs2: "int[:]", subs_cum2: "int[:]", w2: "float[:,:]", mat_f: "float[:,:,:,:]", f_int: "float[:,:,:]" + subs2: "int[:]", + subs_cum2: "int[:]", + w2: "float[:,:]", + mat_f: "float[:,:,:,:]", + f_int: "float[:,:,:]", ): n1, n2, n3 = shape(f_int) @@ -224,7 +250,11 @@ def kernel_int_3d_eta2( def kernel_int_3d_eta2_transpose( - subs2: "int[:]", subs_cum2: "int[:]", w2: "float[:,:]", f_int: "float[:,:,:]", mat_f: "float[:,:,:,:]" + subs2: "int[:]", + subs_cum2: "int[:]", + w2: "float[:,:]", + f_int: "float[:,:,:]", + mat_f: "float[:,:,:,:]", ): n1, n2, n3 = shape(f_int) @@ -243,7 +273,11 @@ def kernel_int_3d_eta2_transpose( # ========= kernel for integration along eta3 direction, reducing to a 3d array ============================ def kernel_int_3d_eta3( - subs3: "int[:]", subs_cum3: "int[:]", w3: "float[:,:]", mat_f: "float[:,:,:,:]", f_int: "float[:,:,:]" + subs3: "int[:]", + subs_cum3: "int[:]", + w3: "float[:,:]", + mat_f: "float[:,:,:,:]", + f_int: "float[:,:,:]", ): n1, n2, n3 = shape(f_int) @@ -262,7 +296,11 @@ def kernel_int_3d_eta3( def kernel_int_3d_eta3_transpose( - subs3: "int[:]", subs_cum3: "int[:]", w3: "float[:,:]", f_int: "float[:,:,:]", mat_f: "float[:,:,:,:]" + subs3: "int[:]", + subs_cum3: "int[:]", + w3: "float[:,:]", + f_int: "float[:,:,:]", + mat_f: "float[:,:,:,:]", ): n1, n2, n3 = shape(f_int) @@ -655,7 +693,11 @@ def kernel_int_3d_eta1_eta2_eta3_transpose( # ========= kernel for integration in eta1-eta2-eta3 cell, reducing to a 3d array ======================= def kernel_int_3d_eta1_eta2_eta3_old( - w1: "float[:,:]", w2: "float[:,:]", w3: "float[:,:]", mat_f: "float[:,:,:,:,:,:]", f_int: "float[:,:,:]" + w1: "float[:,:]", + w2: "float[:,:]", + w3: "float[:,:]", + mat_f: "float[:,:,:,:,:,:]", + f_int: "float[:,:,:]", ): ne1, nq1, ne2, nq2, ne3, nq3 = shape(mat_f) diff --git a/src/struphy/eigenvalue_solvers/legacy/MHD_eigenvalues_cylinder_1D.py b/src/struphy/eigenvalue_solvers/legacy/MHD_eigenvalues_cylinder_1D.py index 9fbc38ce8..a16b44e3d 100644 --- a/src/struphy/eigenvalue_solvers/legacy/MHD_eigenvalues_cylinder_1D.py +++ b/src/struphy/eigenvalue_solvers/legacy/MHD_eigenvalues_cylinder_1D.py @@ -120,17 +120,17 @@ def solve_ev_problem(rho, B_phi, dB_phi, B_z, p, gamma, a, k, m, num_params, bcZ ) W_dXZ = lambda eta: B_phi(r(eta)) * gamma * m * p(r(eta)) / r(eta) ** 2 + B_z(r(eta)) * gamma * k * p(r(eta)) / r( - eta + eta, ) W_ZdX = lambda eta: B_phi(r(eta)) * gamma * m * p(r(eta)) / r(eta) ** 2 + B_z(r(eta)) * gamma * k * p(r(eta)) / r( - eta + eta, ) W_VZ = lambda eta: B_phi(r(eta)) * gamma * m**2 * p(r(eta)) / r(eta) ** 2 + B_z(r(eta)) * gamma * k * m * p( - r(eta) + r(eta), ) / r(eta) W_ZV = lambda eta: B_phi(r(eta)) * gamma * m**2 * p(r(eta)) / r(eta) ** 2 + B_z(r(eta)) * gamma * k * m * p( - r(eta) + r(eta), ) / r(eta) # compute matrices @@ -289,10 +289,16 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, # evaluate basis functions on quadrature points in format (interval, local quad. point, global basis function) basis_his_N = bsp.collocation_matrix(splines.T, splines.p, pts.flatten(), False, normalize=kind_splines[0]).reshape( - pts.shape[0], pts.shape[1], splines.NbaseN + pts.shape[0], + pts.shape[1], + splines.NbaseN, ) basis_his_D = bsp.collocation_matrix( - splines.t, splines.p - 1, pts.flatten(), False, normalize=kind_splines[1] + splines.t, + splines.p - 1, + pts.flatten(), + False, + normalize=kind_splines[1], ).reshape(pts.shape[0], pts.shape[1], splines.NbaseD) # shift first interpolation point away from pole @@ -459,12 +465,14 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, ) rhs0_N_phi = spa.csr_matrix( - (rhs0_N_phi, (pi0_N_i[0], pi0_N_i[1])), shape=(splines.NbaseN, splines.NbaseN) + (rhs0_N_phi, (pi0_N_i[0], pi0_N_i[1])), + shape=(splines.NbaseN, splines.NbaseN), ).toarray() rhs0_N_z = spa.csr_matrix((rhs0_N_z, (pi0_N_i[0], pi0_N_i[1])), shape=(splines.NbaseN, splines.NbaseN)).toarray() rhs1_D_phi = spa.csr_matrix( - (rhs1_D_phi, (pi1_D_i[0], pi1_D_i[1])), shape=(splines.NbaseD, splines.NbaseD) + (rhs1_D_phi, (pi1_D_i[0], pi1_D_i[1])), + shape=(splines.NbaseD, splines.NbaseD), ).toarray() rhs1_D_z = spa.csr_matrix((rhs1_D_z, (pi1_D_i[0], pi1_D_i[1])), shape=(splines.NbaseD, splines.NbaseD)).toarray() @@ -474,10 +482,12 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, rhs1_D_pr = spa.csr_matrix((rhs1_D_pr, (pi1_D_i[0], pi1_D_i[1])), shape=(splines.NbaseD, splines.NbaseD)).toarray() rhs0_N_rho = spa.csr_matrix( - (rhs0_N_rho, (pi0_N_i[0], pi0_N_i[1])), shape=(splines.NbaseN, splines.NbaseN) + (rhs0_N_rho, (pi0_N_i[0], pi0_N_i[1])), + shape=(splines.NbaseN, splines.NbaseN), ).toarray() rhs1_D_rho = spa.csr_matrix( - (rhs1_D_rho, (pi1_D_i[0], pi1_D_i[1])), shape=(splines.NbaseD, splines.NbaseD) + (rhs1_D_rho, (pi1_D_i[0], pi1_D_i[1])), + shape=(splines.NbaseD, splines.NbaseD), ).toarray() pi0_N_phi = xp.linalg.inv(proj.N.toarray()[1:-1, 1:-1]).dot(rhs0_N_phi[1:-1, 1:-1]) @@ -514,7 +524,7 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, [I_11, xp.zeros((len(u2_r) - 2, len(u2_phi))), xp.zeros((len(u2_r) - 2, len(u2_z) - 1))], [I_21, I_22, I_23[:, 1:]], [I_31[1:, :], I_32[1:, :], I_33[1:, 1:]], - ] + ], ) # ======= matrices in strong pressure equation ================ @@ -534,7 +544,7 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, [A_1, xp.zeros((A_1.shape[0], A_2.shape[1])), xp.zeros((A_1.shape[0], A_3.shape[1]))], [xp.zeros((A_2.shape[0], A_1.shape[1])), A_2, xp.zeros((A_2.shape[0], A_3.shape[1]))], [xp.zeros((A_3.shape[0], A_1.shape[1])), xp.zeros((A_3.shape[0], A_2.shape[1])), A_3], - ] + ], ) MB_11 = 2 * xp.pi * n * pi0_N_z.T.dot(M2_r) + 2 * xp.pi * m * pi0_N_phi.T.dot(M2_r) @@ -553,7 +563,7 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, MB_34 = 2 * xp.pi * n * M3 MB_b_all = xp.block( - [[MB_11, MB_12, MB_13[:, 1:]], [MB_21, MB_22, MB_23[:, 1:]], [MB_31[1:, :], MB_32[1:, :], MB_33[1:, 1:]]] + [[MB_11, MB_12, MB_13[:, 1:]], [MB_21, MB_22, MB_23[:, 1:]], [MB_31[1:, :], MB_32[1:, :], MB_33[1:, 1:]]], ) MB_p_all = xp.block([[MB_14[:, 1:]], [MB_24[:, 1:]], [MB_34[1:, 1:]]]) @@ -664,7 +674,7 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, xp.zeros((len(p3) - 1, A_all.shape[1])), xp.identity(len(p3) - 1), ], - ] + ], ) RHS = xp.block( @@ -672,7 +682,7 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, [xp.zeros((MB_b_all.shape[0], I_all.shape[1])), MB_b_all, MB_p_all], [I_all, xp.zeros((I_all.shape[0], MB_b_all.shape[1])), xp.zeros((I_all.shape[0], MB_p_all.shape[1]))], [P_all, xp.zeros((P_all.shape[0], MB_b_all.shape[1])), xp.zeros((P_all.shape[0], MB_p_all.shape[1]))], - ] + ], ) dt = 0.05 @@ -716,13 +726,14 @@ def solve_ev_problem_FEEC(Rho, B_phi, dB_phi, B_z, dB_z, P, gamma, a, R0, n, m, b2_phi_all[n, :], b2_z_all[n, 1:], p3_all[n, 1:], - ) + ), ) new = UPDATE.dot(old) # extract components unew, bnew, pnew = xp.split( - new, [len(u2_r) - 2 + len(u2_phi) + len(u2_z) - 1, 2 * (len(u2_r) - 2 + len(u2_phi) + len(u2_z) - 1)] + new, + [len(u2_r) - 2 + len(u2_phi) + len(u2_z) - 1, 2 * (len(u2_r) - 2 + len(u2_phi) + len(u2_z) - 1)], ) u2_r_all[n + 1, :] = xp.array([0.0] + list(unew[: (splines.NbaseN - 2)]) + [0.0]) diff --git a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fB_massless_control_variate.py b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fB_massless_control_variate.py index 49be8c3ef..39156f985 100644 --- a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fB_massless_control_variate.py +++ b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fB_massless_control_variate.py @@ -204,7 +204,7 @@ def bv_right( ) # ========================= C.T =========================== return tensor_space_FEM.C.T.dot( - xp.concatenate((temp_twoform1.flatten(), temp_twoform2.flatten(), temp_twoform3.flatten())) + xp.concatenate((temp_twoform1.flatten(), temp_twoform2.flatten(), temp_twoform3.flatten())), ) diff --git a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fB_massless_kernels_control_variate.py b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fB_massless_kernels_control_variate.py index 9220cdc69..7f15931ec 100644 --- a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fB_massless_kernels_control_variate.py +++ b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fB_massless_kernels_control_variate.py @@ -584,13 +584,49 @@ def vv( bd3[:] = b3[pd3, :pn3] * d3[:] vel[0] = eva.evaluation_kernel( - pd1, pn2, pn3, bd1, bn2, bn3, span1 - 1, span2, span3, NbaseD[0], NbaseN[1], NbaseN[2], bb1 + pd1, + pn2, + pn3, + bd1, + bn2, + bn3, + span1 - 1, + span2, + span3, + NbaseD[0], + NbaseN[1], + NbaseN[2], + bb1, ) vel[1] = eva.evaluation_kernel( - pn1, pd2, pn3, bn1, bd2, bn3, span1, span2 - 1, span3, NbaseN[0], NbaseD[1], NbaseN[2], bb2 + pn1, + pd2, + pn3, + bn1, + bd2, + bn3, + span1, + span2 - 1, + span3, + NbaseN[0], + NbaseD[1], + NbaseN[2], + bb2, ) vel[2] = eva.evaluation_kernel( - pn1, pn2, pd3, bn1, bn2, bd3, span1, span2, span3 - 1, NbaseN[0], NbaseN[1], NbaseD[2], bb3 + pn1, + pn2, + pd3, + bn1, + bn2, + bd3, + span1, + span2, + span3 - 1, + NbaseN[0], + NbaseN[1], + NbaseD[2], + bb3, ) # ======= here we use the linear hat function =========== ie1 = int(eta1 * Nel[0]) diff --git a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fnB_massless_control_variate.py b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fnB_massless_control_variate.py index d52a3e90e..5e9c04eb0 100644 --- a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fnB_massless_control_variate.py +++ b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fnB_massless_control_variate.py @@ -248,7 +248,7 @@ def bv_right( ) # ========================= C.T =========================== return tensor_space_FEM.C.T.dot( - xp.concatenate((temp_twoform1.flatten(), temp_twoform2.flatten(), temp_twoform3.flatten())) + xp.concatenate((temp_twoform1.flatten(), temp_twoform2.flatten(), temp_twoform3.flatten())), ) @@ -429,7 +429,7 @@ def uv_right( ) # ========================= C.T =========================== temp_final = temp_final_0.flatten() + tensor_space_FEM.G.T.dot( - xp.concatenate((temp_final_1.flatten(), temp_final_2.flatten(), temp_final_3.flatten())) + xp.concatenate((temp_final_1.flatten(), temp_final_2.flatten(), temp_final_3.flatten())), ) return temp_final diff --git a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fnB_massless_kernels_control_variate.py b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fnB_massless_kernels_control_variate.py index f3b6fca0e..965a7af33 100644 --- a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fnB_massless_kernels_control_variate.py +++ b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/fnB_massless_kernels_control_variate.py @@ -212,17 +212,65 @@ def vv( bd3[:] = b3[pd3, :pn3] * d3[:] vel[0] = eva.evaluation_kernel( - pd1, pn2, pn3, bd1, bn2, bn3, span1 - 1, span2, span3, NbaseD[0], NbaseN[1], NbaseN[2], bb1 + pd1, + pn2, + pn3, + bd1, + bn2, + bn3, + span1 - 1, + span2, + span3, + NbaseD[0], + NbaseN[1], + NbaseN[2], + bb1, ) vel[1] = eva.evaluation_kernel( - pn1, pd2, pn3, bn1, bd2, bn3, span1, span2 - 1, span3, NbaseN[0], NbaseD[1], NbaseN[2], bb2 + pn1, + pd2, + pn3, + bn1, + bd2, + bn3, + span1, + span2 - 1, + span3, + NbaseN[0], + NbaseD[1], + NbaseN[2], + bb2, ) vel[2] = eva.evaluation_kernel( - pn1, pn2, pd3, bn1, bn2, bd3, span1, span2, span3 - 1, NbaseN[0], NbaseN[1], NbaseD[2], bb3 + pn1, + pn2, + pd3, + bn1, + bn2, + bd3, + span1, + span2, + span3 - 1, + NbaseN[0], + NbaseN[1], + NbaseD[2], + bb3, ) tt = eva.evaluation_kernel( - pn1, pn2, pn3, bn1, bn2, bn3, span1, span2, span3, NbaseN[0], NbaseN[1], NbaseN[2], n + pn1, + pn2, + pn3, + bn1, + bn2, + bn3, + span1, + span2, + span3, + NbaseN[0], + NbaseN[1], + NbaseN[2], + n, ) if abs(tt) > tol: U_value = 1.0 / tt diff --git a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/massless_control_variate.py b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/massless_control_variate.py index 4e97422f0..3459ff7b2 100644 --- a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/massless_control_variate.py +++ b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/massless_control_variate.py @@ -247,7 +247,7 @@ def bv_right( ) # ========================= C.T =========================== return tensor_space_FEM.C.T.dot( - xp.concatenate((temp_twoform1.flatten(), temp_twoform2.flatten(), temp_twoform3.flatten())) + xp.concatenate((temp_twoform1.flatten(), temp_twoform2.flatten(), temp_twoform3.flatten())), ) @@ -430,7 +430,7 @@ def uv_right( ) # ========================= C.T =========================== temp_final = temp_final_0.flatten() + tensor_space_FEM.G.T.dot( - xp.concatenate((temp_final_1.flatten(), temp_final_2.flatten(), temp_final_3.flatten())) + xp.concatenate((temp_final_1.flatten(), temp_final_2.flatten(), temp_final_3.flatten())), ) return temp_final diff --git a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/massless_kernels_control_variate.py b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/massless_kernels_control_variate.py index 28ce7bbb2..bfff64b2a 100644 --- a/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/massless_kernels_control_variate.py +++ b/src/struphy/eigenvalue_solvers/legacy/control_variates/kinetic_extended/massless_kernels_control_variate.py @@ -196,34 +196,84 @@ def uvright( for q2 in range(nq2): for q3 in range(nq3): dft[0, 0] = DFI_11[ - ie1, ie2, ie3, q1, q2, q3 + ie1, + ie2, + ie3, + q1, + q2, + q3, ] # mappings_analytical.df_inv(pts1[ie1, q1], pts2[ie2,q2], pts3[ie3,q3], kind_map, params_map, components[0, 0]) dft[0, 1] = DFI_21[ - ie1, ie2, ie3, q1, q2, q3 + ie1, + ie2, + ie3, + q1, + q2, + q3, ] # mappings_analytical.df_inv(pts1[ie1, q1], pts2[ie2,q2], pts3[ie3,q3], kind_map, params_map, components[0, 1]) dft[0, 2] = DFI_31[ - ie1, ie2, ie3, q1, q2, q3 + ie1, + ie2, + ie3, + q1, + q2, + q3, ] # mappings_analytical.df_inv(pts1[ie1, q1], pts2[ie2,q2], pts3[ie3,q3], kind_map, params_map, components[0, 2]) dft[1, 0] = DFI_12[ - ie1, ie2, ie3, q1, q2, q3 + ie1, + ie2, + ie3, + q1, + q2, + q3, ] # mappings_analytical.df_inv(pts1[ie1, q1], pts2[ie2,q2], pts3[ie3,q3], kind_map, params_map, components[1, 0]) dft[1, 1] = DFI_22[ - ie1, ie2, ie3, q1, q2, q3 + ie1, + ie2, + ie3, + q1, + q2, + q3, ] # mappings_analytical.df_inv(pts1[ie1, q1], pts2[ie2,q2], pts3[ie3,q3], kind_map, params_map, components[1, 1]) dft[1, 2] = DFI_32[ - ie1, ie2, ie3, q1, q2, q3 + ie1, + ie2, + ie3, + q1, + q2, + q3, ] # mappings_analytical.df_inv(pts1[ie1, q1], pts2[ie2,q2], pts3[ie3,q3], kind_map, params_map, components[1, 2]) dft[2, 0] = DFI_13[ - ie1, ie2, ie3, q1, q2, q3 + ie1, + ie2, + ie3, + q1, + q2, + q3, ] # mappings_analytical.df_inv(pts1[ie1, q1], pts2[ie2,q2], pts3[ie3,q3], kind_map, params_map, components[2, 0]) dft[2, 1] = DFI_23[ - ie1, ie2, ie3, q1, q2, q3 + ie1, + ie2, + ie3, + q1, + q2, + q3, ] # mappings_analytical.df_inv(pts1[ie1, q1], pts2[ie2,q2], pts3[ie3,q3], kind_map, params_map, components[2, 1]) dft[2, 2] = DFI_33[ - ie1, ie2, ie3, q1, q2, q3 + ie1, + ie2, + ie3, + q1, + q2, + q3, ] # mappings_analytical.df_inv(pts1[ie1, q1], pts2[ie2,q2], pts3[ie3,q3], kind_map, params_map, components[2, 2]) detdet = df_det[ - ie1, ie2, ie3, q1, q2, q3 + ie1, + ie2, + ie3, + q1, + q2, + q3, ] # mappings_analytical.det_df(pts1[ie1, q1], pts2[ie2,q2], pts3[ie3,q3], kind_map, params_map) Jeq[0] = Jeqx[ie1, ie2, ie3, q1, q2, q3] Jeq[1] = Jeqy[ie1, ie2, ie3, q1, q2, q3] @@ -706,19 +756,67 @@ def vv( bd3[:] = b3[pd3, :pn3] * d3[:] vel[0] = eva.evaluation_kernel( - pd1, pn2, pn3, bd1, bn2, bn3, span1 - 1, span2, span3, NbaseD[0], NbaseN[1], NbaseN[2], bb1 + pd1, + pn2, + pn3, + bd1, + bn2, + bn3, + span1 - 1, + span2, + span3, + NbaseD[0], + NbaseN[1], + NbaseN[2], + bb1, ) vel[1] = eva.evaluation_kernel( - pn1, pd2, pn3, bn1, bd2, bn3, span1, span2 - 1, span3, NbaseN[0], NbaseD[1], NbaseN[2], bb2 + pn1, + pd2, + pn3, + bn1, + bd2, + bn3, + span1, + span2 - 1, + span3, + NbaseN[0], + NbaseD[1], + NbaseN[2], + bb2, ) vel[2] = eva.evaluation_kernel( - pn1, pn2, pd3, bn1, bn2, bd3, span1, span2, span3 - 1, NbaseN[0], NbaseN[1], NbaseD[2], bb3 + pn1, + pn2, + pd3, + bn1, + bn2, + bd3, + span1, + span2, + span3 - 1, + NbaseN[0], + NbaseN[1], + NbaseD[2], + bb3, ) U_value = exp( -eva.evaluation_kernel( - pn1, pn2, pn3, bn1, bn2, bn3, span1, span2, span3, NbaseN[0], NbaseN[1], NbaseN[2], u - ) + pn1, + pn2, + pn3, + bn1, + bn2, + bn3, + span1, + span2, + span3, + NbaseN[0], + NbaseN[1], + NbaseN[2], + u, + ), ) # ========= mapping evaluation ============= diff --git a/src/struphy/eigenvalue_solvers/legacy/emw_operators.py b/src/struphy/eigenvalue_solvers/legacy/emw_operators.py index e2fd4ed22..9e8c95b3f 100755 --- a/src/struphy/eigenvalue_solvers/legacy/emw_operators.py +++ b/src/struphy/eigenvalue_solvers/legacy/emw_operators.py @@ -198,12 +198,14 @@ def __assemble_M1_cross(self, weight): col = Nj[1] * Nj[2] * col1 + Nj[2] * col2 + col3 M[a][b] = spa.csr_matrix( - (M[a][b].flatten(), (row, col.flatten())), shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]) + (M[a][b].flatten(), (row, col.flatten())), + shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]), ) M[a][b].eliminate_zeros() M = spa.bmat( - [[M[0][0], M[0][1], M[0][2]], [M[1][0], M[1][1], M[1][2]], [M[2][0], M[2][1], M[2][2]]], format="csr" + [[M[0][0], M[0][1], M[0][2]], [M[1][0], M[1][1], M[1][2]], [M[2][0], M[2][1], M[2][2]]], + format="csr", ) self.R1_mat = -self.SPACES.E1_0.dot(M.dot(self.SPACES.E1_0.T)).tocsr() diff --git a/src/struphy/eigenvalue_solvers/legacy/mass_matrices_3d_pre.py b/src/struphy/eigenvalue_solvers/legacy/mass_matrices_3d_pre.py index eecb68293..a46097a8f 100644 --- a/src/struphy/eigenvalue_solvers/legacy/mass_matrices_3d_pre.py +++ b/src/struphy/eigenvalue_solvers/legacy/mass_matrices_3d_pre.py @@ -273,10 +273,12 @@ def get_M1_PRE_3(tensor_space_FEM, mats_pol=None): def solve(x): x1 = x[: tensor_space_FEM.E1_pol_0.shape[0] * tensor_space_FEM.NbaseN[2]].reshape( - tensor_space_FEM.E1_pol_0.shape[0], tensor_space_FEM.NbaseN[2] + tensor_space_FEM.E1_pol_0.shape[0], + tensor_space_FEM.NbaseN[2], ) x2 = x[tensor_space_FEM.E1_pol_0.shape[0] * tensor_space_FEM.NbaseN[2] :].reshape( - tensor_space_FEM.E0_pol_0.shape[0], tensor_space_FEM.NbaseD[2] + tensor_space_FEM.E0_pol_0.shape[0], + tensor_space_FEM.NbaseD[2], ) r1 = linkron.kron_fftsolve_2d(M1_pol_0_11_LU, tor_vec0, x1).flatten() @@ -311,10 +313,12 @@ def get_M2_PRE_3(tensor_space_FEM, mats_pol=None): def solve(x): x1 = x[: tensor_space_FEM.E2_pol_0.shape[0] * tensor_space_FEM.NbaseD[2]].reshape( - tensor_space_FEM.E2_pol_0.shape[0], tensor_space_FEM.NbaseD[2] + tensor_space_FEM.E2_pol_0.shape[0], + tensor_space_FEM.NbaseD[2], ) x2 = x[tensor_space_FEM.E2_pol_0.shape[0] * tensor_space_FEM.NbaseD[2] :].reshape( - tensor_space_FEM.E3_pol_0.shape[0], tensor_space_FEM.NbaseN[2] + tensor_space_FEM.E3_pol_0.shape[0], + tensor_space_FEM.NbaseN[2], ) r1 = linkron.kron_fftsolve_2d(M2_pol_0_11_LU, tor_vec1, x1).flatten() @@ -373,10 +377,12 @@ def get_Mv_PRE_3(tensor_space_FEM, mats_pol=None): def solve(x): x1 = x[: tensor_space_FEM.Ev_pol_0.shape[0] * tensor_space_FEM.NbaseN[2]].reshape( - tensor_space_FEM.Ev_pol_0.shape[0], tensor_space_FEM.NbaseN[2] + tensor_space_FEM.Ev_pol_0.shape[0], + tensor_space_FEM.NbaseN[2], ) x2 = x[tensor_space_FEM.Ev_pol_0.shape[0] * tensor_space_FEM.NbaseN[2] :].reshape( - tensor_space_FEM.E0_pol.shape[0], tensor_space_FEM.NbaseN[2] + tensor_space_FEM.E0_pol.shape[0], + tensor_space_FEM.NbaseN[2], ) r1 = linkron.kron_fftsolve_2d(Mv_pol_0_11_LU, tor_vec0, x1).flatten() diff --git a/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_bv_kernel.py b/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_bv_kernel.py index 5cf3830a4..e477940e6 100644 --- a/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_bv_kernel.py +++ b/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_bv_kernel.py @@ -226,7 +226,9 @@ def right_hand2( * bd2[ie2, il2, 0, q2] * bd3[ie3, il3, 0, q3] * temp_vector_1[ - N_index_x[ie1, il1], D_index_y[ie2, il2], D_index_z[ie3, il3] + N_index_x[ie1, il1], + D_index_y[ie2, il2], + D_index_z[ie3, il3], ] ) @@ -252,7 +254,9 @@ def right_hand2( * bn2[ie2, il2, 0, q2] * bd3[ie3, il3, 0, q3] * temp_vector_2[ - D_index_x[ie1, il1], N_index_y[ie2, il2], D_index_z[ie3, il3] + D_index_x[ie1, il1], + N_index_y[ie2, il2], + D_index_z[ie3, il3], ] ) @@ -278,7 +282,9 @@ def right_hand2( * bd2[ie2, il2, 0, q2] * bn3[ie3, il3, 0, q3] * temp_vector_3[ - D_index_x[ie1, il1], D_index_y[ie2, il2], N_index_z[ie3, il3] + D_index_x[ie1, il1], + D_index_y[ie2, il2], + N_index_z[ie3, il3], ] ) @@ -338,7 +344,9 @@ def right_hand1( * bn2[ie2, il2, 0, q2] * bn3[ie3, il3, 0, q3] * temp_vector_1[ - D_index_x[ie1, il1], N_index_y[ie2, il2], N_index_z[ie3, il3] + D_index_x[ie1, il1], + N_index_y[ie2, il2], + N_index_z[ie3, il3], ] ) @@ -364,7 +372,9 @@ def right_hand1( * bd2[ie2, il2, 0, q2] * bn3[ie3, il3, 0, q3] * temp_vector_2[ - N_index_x[ie1, il1], D_index_y[ie2, il2], N_index_z[ie3, il3] + N_index_x[ie1, il1], + D_index_y[ie2, il2], + N_index_z[ie3, il3], ] ) @@ -390,7 +400,9 @@ def right_hand1( * bn2[ie2, il2, 0, q2] * bd3[ie3, il3, 0, q3] * temp_vector_3[ - N_index_x[ie1, il1], N_index_y[ie2, il2], D_index_z[ie3, il3] + N_index_x[ie1, il1], + N_index_y[ie2, il2], + D_index_z[ie3, il3], ] ) diff --git a/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_massless_linear_operators.py b/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_massless_linear_operators.py index 2ddddc64a..bdf01bf8b 100644 --- a/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_massless_linear_operators.py +++ b/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_massless_linear_operators.py @@ -725,7 +725,8 @@ def linearoperator_step3( # ========================= C =========================== # time1 = time.time() twoform_temp1_long[:], twoform_temp2_long[:], twoform_temp3_long[:] = xp.split( - tensor_space_FEM.C.dot(input_vector), [Ntot_2form[0], Ntot_2form[0] + Ntot_2form[1]] + tensor_space_FEM.C.dot(input_vector), + [Ntot_2form[0], Ntot_2form[0] + Ntot_2form[1]], ) temp_vector_1[:, :, :] = twoform_temp1_long.reshape(Nbase_2form[0]) temp_vector_2[:, :, :] = twoform_temp2_long.reshape(Nbase_2form[1]) @@ -826,7 +827,7 @@ def linearoperator_step3( # ========================= C.T =========================== # time1 = time.time() temp_final = tensor_space_FEM.M1.dot(input_vector) - dt / 2.0 * tensor_space_FEM.C.T.dot( - xp.concatenate((temp_vector_1.flatten(), temp_vector_2.flatten(), temp_vector_3.flatten())) + xp.concatenate((temp_vector_1.flatten(), temp_vector_2.flatten(), temp_vector_3.flatten())), ) # time2 = time.time() # print('second_curl_time', time2 - time1) @@ -929,7 +930,8 @@ def linearoperator_right_step3( # ================================================================== # ========================= C =========================== twoform_temp1_long[:], twoform_temp2_long[:], twoform_temp3_long[:] = xp.split( - tensor_space_FEM.C.dot(input_vector), [Ntot_2form[0], Ntot_2form[0] + Ntot_2form[1]] + tensor_space_FEM.C.dot(input_vector), + [Ntot_2form[0], Ntot_2form[0] + Ntot_2form[1]], ) temp_vector_1[:, :, :] = twoform_temp1_long.reshape(Nbase_2form[0]) temp_vector_2[:, :, :] = twoform_temp2_long.reshape(Nbase_2form[1]) @@ -1082,7 +1084,7 @@ def linearoperator_right_step3( # print('final_bb', time2 - time1) # ========================= C.T =========================== temp_final = tensor_space_FEM.M1.dot(input_vector) + dt / 2.0 * tensor_space_FEM.C.T.dot( - xp.concatenate((temp_vector_1.flatten(), temp_vector_2.flatten(), temp_vector_3.flatten())) + xp.concatenate((temp_vector_1.flatten(), temp_vector_2.flatten(), temp_vector_3.flatten())), ) return temp_final @@ -1146,7 +1148,8 @@ def substep4_linear_operator( # ========================================== acc.twoform_temp1_long[:], acc.twoform_temp2_long[:], acc.twoform_temp3_long[:] = xp.split( - tensor_space_FEM.C.dot(input), [Ntot_2form[0], Ntot_2form[0] + Ntot_2form[1]] + tensor_space_FEM.C.dot(input), + [Ntot_2form[0], Ntot_2form[0] + Ntot_2form[1]], ) acc.twoform_temp1[:, :, :] = acc.twoform_temp1_long.reshape(Nbase_2form[0]) acc.twoform_temp2[:, :, :] = acc.twoform_temp2_long.reshape(Nbase_2form[1]) @@ -1359,7 +1362,7 @@ def substep4_linear_operator( ) return M1.dot(input) + dt**2 / 4.0 * tensor_space_FEM.C.T.dot( - xp.concatenate((acc.twoform_temp1.flatten(), acc.twoform_temp2.flatten(), acc.twoform_temp3.flatten())) + xp.concatenate((acc.twoform_temp1.flatten(), acc.twoform_temp2.flatten(), acc.twoform_temp3.flatten())), ) # ========================================================================================================== @@ -1532,7 +1535,7 @@ def substep4_linear_operator_right( xp.concatenate((acc.oneform_temp1.flatten(), acc.oneform_temp2.flatten(), acc.oneform_temp3.flatten())), tol=10 ** (-10), M=M1_PRE, - )[0] + )[0], ) acc.oneform_temp1_long[:], acc.oneform_temp2_long[:], acc.oneform_temp3_long[:] = xp.split( @@ -1640,7 +1643,7 @@ def substep4_linear_operator_right( ) return M1.dot(xp.concatenate((bb1.flatten(), bb2.flatten(), bb3.flatten()))) - CURL.T.dot( - xp.concatenate((acc.twoform_temp1.flatten(), acc.twoform_temp2.flatten(), acc.twoform_temp3.flatten())) + xp.concatenate((acc.twoform_temp1.flatten(), acc.twoform_temp2.flatten(), acc.twoform_temp3.flatten())), ) # ========================================================================================================== @@ -1961,7 +1964,8 @@ def substep4_localproj_linear_operator( # ========================================== acc.twoform_temp1_long[:], acc.twoform_temp2_long[:], acc.twoform_temp3_long[:] = xp.split( - tensor_space_FEM.C.dot(input), [Ntot_2form[0], Ntot_2form[0] + Ntot_2form[1]] + tensor_space_FEM.C.dot(input), + [Ntot_2form[0], Ntot_2form[0] + Ntot_2form[1]], ) acc.twoform_temp1[:, :, :] = acc.twoform_temp1_long.reshape(Nbase_2form[0]) acc.twoform_temp2[:, :, :] = acc.twoform_temp2_long.reshape(Nbase_2form[1]) @@ -2065,7 +2069,7 @@ def substep4_localproj_linear_operator( acc.oneform_temp1_long[:], acc.oneform_temp2_long[:], acc.oneform_temp3_long[:] = xp.split( mat.dot( - xp.concatenate((acc.oneform_temp1.flatten(), acc.oneform_temp2.flatten(), acc.oneform_temp3.flatten())) + xp.concatenate((acc.oneform_temp1.flatten(), acc.oneform_temp2.flatten(), acc.oneform_temp3.flatten())), ), [Ntot_1form[0], Ntot_1form[0] + Ntot_1form[1]], ) @@ -2171,7 +2175,7 @@ def substep4_localproj_linear_operator( ) return M1.dot(input) + dt**2 / 4.0 * tensor_space_FEM.C.T.dot( - xp.concatenate((acc.twoform_temp1.flatten(), acc.twoform_temp2.flatten(), acc.twoform_temp3.flatten())) + xp.concatenate((acc.twoform_temp1.flatten(), acc.twoform_temp2.flatten(), acc.twoform_temp3.flatten())), ) # ========================================================================================================== @@ -2339,11 +2343,12 @@ def substep4_localproj_linear_operator_right( tensor_space_FEM.basisD[2], ) acc.oneform_temp_long[:] = mat.dot( - xp.concatenate((acc.oneform_temp1.flatten(), acc.oneform_temp2.flatten(), acc.oneform_temp3.flatten())) + xp.concatenate((acc.oneform_temp1.flatten(), acc.oneform_temp2.flatten(), acc.oneform_temp3.flatten())), ) acc.oneform_temp1_long[:], acc.oneform_temp2_long[:], acc.oneform_temp3_long[:] = xp.split( - (dt**2.0 / 4.0 * acc.oneform_temp_long + dt * vec), [Ntot_1form[0], Ntot_1form[0] + Ntot_1form[1]] + (dt**2.0 / 4.0 * acc.oneform_temp_long + dt * vec), + [Ntot_1form[0], Ntot_1form[0] + Ntot_1form[1]], ) acc.oneform_temp1[:, :, :] = acc.oneform_temp1_long.reshape(Nbase_1form[0]) @@ -2447,7 +2452,7 @@ def substep4_localproj_linear_operator_right( ) return M1.dot(xp.concatenate((bb1.flatten(), bb2.flatten(), bb3.flatten()))) - CURL.T.dot( - xp.concatenate((acc.twoform_temp1.flatten(), acc.twoform_temp2.flatten(), acc.twoform_temp3.flatten())) + xp.concatenate((acc.twoform_temp1.flatten(), acc.twoform_temp2.flatten(), acc.twoform_temp3.flatten())), ) # ========================================================================================================== diff --git a/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_vv_kernel.py b/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_vv_kernel.py index 4a5a2dbe0..1ef3376dc 100644 --- a/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_vv_kernel.py +++ b/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_vv_kernel.py @@ -542,7 +542,8 @@ def piecewise_gather( for q2 in range(n_quad[1]): for q3 in range(n_quad[2]): temp1[0] = (cell_left[0] + il1) / Nel[0] + pts1[ - 0, q1 + 0, + q1, ] # quadrature points in the cell x direction temp4[0] = abs(temp1[0] - eta1) - compact[0] / 2.0 # if > 0, result is 0 @@ -741,7 +742,8 @@ def piecewise_scatter( for q2 in range(n_quad[1]): for q3 in range(n_quad[2]): temp1[0] = (cell_left[0] + il1) / Nel[0] + pts1[ - 0, q1 + 0, + q1, ] # quadrature points in the cell x direction temp4[0] = abs(temp1[0] - eta1) - compact[0] / 2 # if > 0, result is 0 diff --git a/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/mhd_operators_3d_local.py b/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/mhd_operators_3d_local.py index 0c96616de..49464aa58 100644 --- a/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/mhd_operators_3d_local.py +++ b/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/mhd_operators_3d_local.py @@ -505,7 +505,9 @@ def __init__(self, tensor_space, n_quad): # quadrature points and weights self.pts[a], self.wts[a] = bsp.quadrature_grid( - xp.unique(self.x_his[a].flatten()), self.pts_loc[a], self.wts_loc[a] + xp.unique(self.x_his[a].flatten()), + self.pts_loc[a], + self.wts_loc[a], ) else: @@ -535,7 +537,9 @@ def __init__(self, tensor_space, n_quad): # quadrature points and weights self.pts[a], self.wts[a] = bsp.quadrature_grid( - xp.append(xp.unique(self.x_his[a].flatten() % 1.0), 1.0), self.pts_loc[a], self.wts_loc[a] + xp.append(xp.unique(self.x_his[a].flatten() % 1.0), 1.0), + self.pts_loc[a], + self.wts_loc[a], ) # evaluate N basis functions at interpolation and quadrature points @@ -556,7 +560,9 @@ def __init__(self, tensor_space, n_quad): self.basisD_his = [ bsp.collocation_matrix(T[1:-1], p - 1, pts.flatten(), bc, normalize=True).reshape( - pts[:, 0].size, pts[0, :].size, NbaseD + pts[:, 0].size, + pts[0, :].size, + NbaseD, ) for T, p, pts, bc, NbaseD in zip(self.T, self.p, self.pts, self.bc, self.NbaseD) ] @@ -802,7 +808,7 @@ def projection_Q_0form(self, domain): self.n_int_nvcof_N[0], self.n_his_nvcof_N[1], self.n_his_nvcof_N[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -827,7 +833,7 @@ def projection_Q_0form(self, domain): self.n_his_nvcof_N[0], self.n_int_nvcof_N[1], self.n_his_nvcof_N[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -852,7 +858,7 @@ def projection_Q_0form(self, domain): self.n_his_nvcof_N[0], self.n_his_nvcof_N[1], self.n_int_nvcof_N[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -1111,7 +1117,7 @@ def projection_Q_2form(self, domain): self.n_int_nvcof_N[0], self.n_his_nvcof_D[1], self.n_his_nvcof_D[2], - ) + ), ) row = self.NbaseD[1] * self.NbaseD[2] * indices[0] + self.NbaseD[2] * indices[1] + indices[2] @@ -1136,7 +1142,7 @@ def projection_Q_2form(self, domain): self.n_his_nvcof_D[0], self.n_int_nvcof_N[1], self.n_his_nvcof_D[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseD[2] * indices[0] + self.NbaseD[2] * indices[1] + indices[2] @@ -1161,7 +1167,7 @@ def projection_Q_2form(self, domain): self.n_his_nvcof_D[0], self.n_his_nvcof_D[1], self.n_int_nvcof_N[2], - ) + ), ) row = self.NbaseD[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -1292,7 +1298,7 @@ def projection_W_0form(self, domain): self.n_int_nvcof_N[0], self.n_int_nvcof_N[1], self.n_int_nvcof_N[2], - ) + ), ) # row indices @@ -1735,7 +1741,7 @@ def projection_T_0form(self, domain): self.n_his_nvcof_N[0], self.n_int_nvcof_N[1], self.n_int_nvcof_N[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -1759,7 +1765,7 @@ def projection_T_0form(self, domain): self.n_his_nvcof_N[0], self.n_int_nvcof_N[1], self.n_int_nvcof_N[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -1784,7 +1790,7 @@ def projection_T_0form(self, domain): self.n_int_nvcof_N[0], self.n_his_nvcof_N[1], self.n_int_nvcof_N[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -1808,7 +1814,7 @@ def projection_T_0form(self, domain): self.n_int_nvcof_N[0], self.n_his_nvcof_N[1], self.n_int_nvcof_N[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -1833,7 +1839,7 @@ def projection_T_0form(self, domain): self.n_int_nvcof_N[0], self.n_int_nvcof_N[1], self.n_his_nvcof_N[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -1857,7 +1863,7 @@ def projection_T_0form(self, domain): self.n_int_nvcof_N[0], self.n_int_nvcof_N[1], self.n_his_nvcof_N[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -2268,7 +2274,7 @@ def projection_T_1form(self, domain): self.n_his_nvcof_N[0], self.n_int_nvcof_D[1], self.n_int_nvcof_N[2], - ) + ), ) row = self.NbaseD[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -2292,7 +2298,7 @@ def projection_T_1form(self, domain): self.n_his_nvcof_N[0], self.n_int_nvcof_N[1], self.n_int_nvcof_D[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseD[2] * indices[0] + self.NbaseD[2] * indices[1] + indices[2] @@ -2317,7 +2323,7 @@ def projection_T_1form(self, domain): self.n_int_nvcof_D[0], self.n_his_nvcof_N[1], self.n_int_nvcof_N[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -2341,7 +2347,7 @@ def projection_T_1form(self, domain): self.n_int_nvcof_N[0], self.n_his_nvcof_N[1], self.n_int_nvcof_D[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseD[2] * indices[0] + self.NbaseD[2] * indices[1] + indices[2] @@ -2366,7 +2372,7 @@ def projection_T_1form(self, domain): self.n_int_nvcof_D[0], self.n_int_nvcof_N[1], self.n_his_nvcof_N[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -2390,7 +2396,7 @@ def projection_T_1form(self, domain): self.n_int_nvcof_N[0], self.n_int_nvcof_D[1], self.n_his_nvcof_N[2], - ) + ), ) row = self.NbaseD[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -2801,7 +2807,7 @@ def projection_T_2form(self, domain): self.n_his_nvcof_D[0], self.n_int_nvcof_N[1], self.n_int_nvcof_D[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseD[2] * indices[0] + self.NbaseD[2] * indices[1] + indices[2] @@ -2825,7 +2831,7 @@ def projection_T_2form(self, domain): self.n_his_nvcof_D[0], self.n_int_nvcof_D[1], self.n_int_nvcof_N[2], - ) + ), ) row = self.NbaseD[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -2850,7 +2856,7 @@ def projection_T_2form(self, domain): self.n_int_nvcof_N[0], self.n_his_nvcof_D[1], self.n_int_nvcof_D[2], - ) + ), ) row = self.NbaseD[1] * self.NbaseD[2] * indices[0] + self.NbaseD[2] * indices[1] + indices[2] @@ -2874,7 +2880,7 @@ def projection_T_2form(self, domain): self.n_int_nvcof_D[0], self.n_his_nvcof_D[1], self.n_int_nvcof_N[2], - ) + ), ) row = self.NbaseD[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -2899,7 +2905,7 @@ def projection_T_2form(self, domain): self.n_int_nvcof_N[0], self.n_int_nvcof_D[1], self.n_his_nvcof_D[2], - ) + ), ) row = self.NbaseD[1] * self.NbaseD[2] * indices[0] + self.NbaseD[2] * indices[1] + indices[2] @@ -2923,7 +2929,7 @@ def projection_T_2form(self, domain): self.n_int_nvcof_D[0], self.n_int_nvcof_N[1], self.n_his_nvcof_D[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseD[2] * indices[0] + self.NbaseD[2] * indices[1] + indices[2] @@ -3182,7 +3188,7 @@ def projection_S_0form(self, domain): self.n_int_nvcof_N[0], self.n_his_nvcof_N[1], self.n_his_nvcof_N[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -3207,7 +3213,7 @@ def projection_S_0form(self, domain): self.n_his_nvcof_N[0], self.n_int_nvcof_N[1], self.n_his_nvcof_N[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -3232,7 +3238,7 @@ def projection_S_0form(self, domain): self.n_his_nvcof_N[0], self.n_his_nvcof_N[1], self.n_int_nvcof_N[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -3491,7 +3497,7 @@ def projection_S_2form(self, domain): self.n_int_nvcof_N[0], self.n_his_nvcof_D[1], self.n_his_nvcof_D[2], - ) + ), ) row = self.NbaseD[1] * self.NbaseD[2] * indices[0] + self.NbaseD[2] * indices[1] + indices[2] @@ -3516,7 +3522,7 @@ def projection_S_2form(self, domain): self.n_his_nvcof_D[0], self.n_int_nvcof_N[1], self.n_his_nvcof_D[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseD[2] * indices[0] + self.NbaseD[2] * indices[1] + indices[2] @@ -3541,7 +3547,7 @@ def projection_S_2form(self, domain): self.n_his_nvcof_D[0], self.n_his_nvcof_D[1], self.n_int_nvcof_N[2], - ) + ), ) row = self.NbaseD[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -3664,7 +3670,7 @@ def projection_K_3form(self, domain): self.n_his_nvcof_D[0], self.n_his_nvcof_D[1], self.n_his_nvcof_D[2], - ) + ), ) # row indices @@ -3927,7 +3933,7 @@ def projection_N_0form(self, domain): self.n_int_nvcof_N[0], self.n_his_nvcof_N[1], self.n_his_nvcof_N[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -3952,7 +3958,7 @@ def projection_N_0form(self, domain): self.n_his_nvcof_N[0], self.n_int_nvcof_N[1], self.n_his_nvcof_N[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -3977,7 +3983,7 @@ def projection_N_0form(self, domain): self.n_his_nvcof_N[0], self.n_his_nvcof_N[1], self.n_int_nvcof_N[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -4212,7 +4218,7 @@ def projection_N_2form(self, domain): self.n_int_nvcof_N[0], self.n_his_nvcof_D[1], self.n_his_nvcof_D[2], - ) + ), ) row = self.NbaseD[1] * self.NbaseD[2] * indices[0] + self.NbaseD[2] * indices[1] + indices[2] @@ -4237,7 +4243,7 @@ def projection_N_2form(self, domain): self.n_his_nvcof_D[0], self.n_int_nvcof_N[1], self.n_his_nvcof_D[2], - ) + ), ) row = self.NbaseN[1] * self.NbaseD[2] * indices[0] + self.NbaseD[2] * indices[1] + indices[2] @@ -4262,7 +4268,7 @@ def projection_N_2form(self, domain): self.n_his_nvcof_D[0], self.n_his_nvcof_D[1], self.n_int_nvcof_N[2], - ) + ), ) row = self.NbaseD[1] * self.NbaseN[2] * indices[0] + self.NbaseN[2] * indices[1] + indices[2] @@ -4323,7 +4329,15 @@ class term_curl_beq: """ def __init__( - self, tensor_space, mapping, kind_map=None, params_map=None, tensor_space_F=None, cx=None, cy=None, cz=None + self, + tensor_space, + mapping, + kind_map=None, + params_map=None, + tensor_space_F=None, + cx=None, + cy=None, + cz=None, ): self.p = tensor_space.p # spline degrees self.Nel = tensor_space.Nel # number of elements @@ -4357,13 +4371,16 @@ def __init__( # ============= evaluation of background magnetic field at quadrature points ========= self.mat_curl_beq_1 = xp.empty( - (self.Nel[0], self.Nel[1], self.Nel[2], self.n_quad[0], self.n_quad[1], self.n_quad[2]), dtype=float + (self.Nel[0], self.Nel[1], self.Nel[2], self.n_quad[0], self.n_quad[1], self.n_quad[2]), + dtype=float, ) self.mat_curl_beq_2 = xp.empty( - (self.Nel[0], self.Nel[1], self.Nel[2], self.n_quad[0], self.n_quad[1], self.n_quad[2]), dtype=float + (self.Nel[0], self.Nel[1], self.Nel[2], self.n_quad[0], self.n_quad[1], self.n_quad[2]), + dtype=float, ) self.mat_curl_beq_3 = xp.empty( - (self.Nel[0], self.Nel[1], self.Nel[2], self.n_quad[0], self.n_quad[1], self.n_quad[2]), dtype=float + (self.Nel[0], self.Nel[1], self.Nel[2], self.n_quad[0], self.n_quad[1], self.n_quad[2]), + dtype=float, ) if mapping == 0: @@ -4455,13 +4472,16 @@ def __init__( # ====================== perturbed magnetic field at quadrature points ========== self.B1 = xp.empty( - (self.Nel[0], self.Nel[1], self.Nel[2], self.n_quad[0], self.n_quad[1], self.n_quad[2]), dtype=float + (self.Nel[0], self.Nel[1], self.Nel[2], self.n_quad[0], self.n_quad[1], self.n_quad[2]), + dtype=float, ) self.B2 = xp.empty( - (self.Nel[0], self.Nel[1], self.Nel[2], self.n_quad[0], self.n_quad[1], self.n_quad[2]), dtype=float + (self.Nel[0], self.Nel[1], self.Nel[2], self.n_quad[0], self.n_quad[1], self.n_quad[2]), + dtype=float, ) self.B3 = xp.empty( - (self.Nel[0], self.Nel[1], self.Nel[2], self.n_quad[0], self.n_quad[1], self.n_quad[2]), dtype=float + (self.Nel[0], self.Nel[1], self.Nel[2], self.n_quad[0], self.n_quad[1], self.n_quad[2]), + dtype=float, ) # ========================== inner products ===================================== diff --git a/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/projectors_local.py b/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/projectors_local.py index dacc4f243..3b27b1b5f 100644 --- a/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/projectors_local.py +++ b/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/projectors_local.py @@ -137,10 +137,12 @@ def __init__(self, spline_space, n_quad): self.x_int = xp.zeros((n_lambda_int, self.n_int), dtype=float) # interpolation points for each coeff. self.int_global_N = xp.zeros( - (n_lambda_int, self.n_int_locbf_N), dtype=int + (n_lambda_int, self.n_int_locbf_N), + dtype=int, ) # global indices of non-vanishing N bf self.int_global_D = xp.zeros( - (n_lambda_int, self.n_int_locbf_D), dtype=int + (n_lambda_int, self.n_int_locbf_D), + dtype=int, ) # global indices of non-vanishing D bf self.int_loccof_N = xp.zeros((n_lambda_int, self.n_int_locbf_N), dtype=int) # index of non-vanishing coeff. (N) @@ -423,7 +425,9 @@ def __init__(self, spline_space, n_quad): # quadrature points and weights self.pts, self.wts = bsp.quadrature_grid( - xp.append(xp.unique(self.x_his.flatten() % 1.0), 1.0), self.pts_loc, self.wts_loc + xp.append(xp.unique(self.x_his.flatten() % 1.0), 1.0), + self.pts_loc, + self.wts_loc, ) # quasi interpolation @@ -1075,7 +1079,9 @@ def __init__(self, tensor_space, n_quad): # quadrature points and weights self.pts[a], self.wts[a] = bsp.quadrature_grid( - xp.unique(self.x_his[a].flatten()), self.pts_loc[a], self.wts_loc[a] + xp.unique(self.x_his[a].flatten()), + self.pts_loc[a], + self.wts_loc[a], ) else: @@ -1105,7 +1111,9 @@ def __init__(self, tensor_space, n_quad): # quadrature points and weights self.pts[a], self.wts[a] = bsp.quadrature_grid( - xp.append(xp.unique(self.x_his[a].flatten() % 1.0), 1.0), self.pts_loc[a], self.wts_loc[a] + xp.append(xp.unique(self.x_his[a].flatten() % 1.0), 1.0), + self.pts_loc[a], + self.wts_loc[a], ) # projector on space V0 (interpolation) @@ -1427,7 +1435,11 @@ def pi_2(self, fun, include_bc=True, eval_kind="meshgrid"): self.wts[1], self.wts[2], mat_f.reshape( - x_int1.size, self.pts[1].shape[0], self.pts[1].shape[1], self.pts[2].shape[0], self.pts[2].shape[1] + x_int1.size, + self.pts[1].shape[0], + self.pts[1].shape[1], + self.pts[2].shape[0], + self.pts[2].shape[1], ), lambdas1, ) @@ -1478,7 +1490,11 @@ def pi_2(self, fun, include_bc=True, eval_kind="meshgrid"): self.wts[0], self.wts[2], mat_f.reshape( - self.pts[0].shape[0], self.pts[0].shape[1], x_int2.size, self.pts[2].shape[0], self.pts[2].shape[1] + self.pts[0].shape[0], + self.pts[0].shape[1], + x_int2.size, + self.pts[2].shape[0], + self.pts[2].shape[1], ), lambdas2, ) @@ -1529,7 +1545,11 @@ def pi_2(self, fun, include_bc=True, eval_kind="meshgrid"): self.wts[0], self.wts[1], mat_f.reshape( - self.pts[0].shape[0], self.pts[0].shape[1], self.pts[1].shape[0], self.pts[1].shape[1], x_int3.size + self.pts[0].shape[0], + self.pts[0].shape[1], + self.pts[1].shape[0], + self.pts[1].shape[1], + x_int3.size, ), lambdas3, ) @@ -1560,7 +1580,8 @@ def pi_3(self, fun, include_bc=True, eval_kind="meshgrid"): # evaluation of function at quadrature points mat_f = xp.empty( - (self.pts[0].flatten().size, self.pts[1].flatten().size, self.pts[2].flatten().size), dtype=float + (self.pts[0].flatten().size, self.pts[1].flatten().size, self.pts[2].flatten().size), + dtype=float, ) # external function call if a callable is passed @@ -1568,7 +1589,10 @@ def pi_3(self, fun, include_bc=True, eval_kind="meshgrid"): # create a meshgrid and evaluate function on point set if eval_kind == "meshgrid": pts1, pts2, pts3 = xp.meshgrid( - self.pts[0].flatten(), self.pts[1].flatten(), self.pts[2].flatten(), indexing="ij" + self.pts[0].flatten(), + self.pts[1].flatten(), + self.pts[2].flatten(), + indexing="ij", ) mat_f[:, :, :] = fun(pts1, pts2, pts3) @@ -1582,7 +1606,9 @@ def pi_3(self, fun, include_bc=True, eval_kind="meshgrid"): for i2 in range(self.pts[1].size): for i3 in range(self.pts[2].size): mat_f[i1, i2, i3] = fun( - self.pts[0].flatten()[i1], self.pts[1].flatten()[i2], self.pts[2].flatten()[i3] + self.pts[0].flatten()[i1], + self.pts[1].flatten()[i2], + self.pts[2].flatten()[i3], ) # internal function call diff --git a/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_L2_projector_kernel.py b/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_L2_projector_kernel.py index 9aa26b243..0e711dbcf 100644 --- a/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_L2_projector_kernel.py +++ b/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_L2_projector_kernel.py @@ -1390,33 +1390,42 @@ def vv_1_form( # evaluation of function at interpolation/quadrature points mat_11 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_21 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_31 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_12 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_22 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_32 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_13 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) mat_23 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) mat_33 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) for i1 in range(cell_number[0]): @@ -1922,33 +1931,42 @@ def vv_push( # evaluation of function at interpolation/quadrature points mat_11 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_21 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_31 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_12 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_22 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_32 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_13 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) mat_23 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) mat_33 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) for i1 in range(cell_number[0]): diff --git a/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_L2.py b/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_L2.py index 20814c8ac..8978e2464 100644 --- a/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_L2.py +++ b/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_L2.py @@ -86,7 +86,7 @@ def __init__(self, tensor_space, p_shape, p_size, NbaseN, NbaseD, mpi_comm): for a in range(3): # self.related[a] = int(xp.floor(NbaseN[a]/2.0)) self.related[a] = int( - xp.floor((3 * int((self.p_size[a] * (self.p_shape[a] + 1)) * self.Nel[a] + 1) + 3 * self.p[a]) / 2.0) + xp.floor((3 * int((self.p_size[a] * (self.p_shape[a] + 1)) * self.Nel[a] + 1) + 3 * self.p[a]) / 2.0), ) if (2 * self.related[a] + 1) > NbaseN[a]: self.related[a] = int(xp.floor(NbaseN[a] / 2.0)) @@ -302,7 +302,7 @@ def assemble_0_form(self, tensor_space_FEM, mpi_comm): # conversion to sparse matrix indices = xp.indices( - (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) + (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1), ) shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] @@ -316,7 +316,8 @@ def assemble_0_form(self, tensor_space_FEM, mpi_comm): col = Nj[1] * Ni[2] * col1 + Ni[2] * col2 + col3 M = spa.csr_matrix( - (self.kernel_0.flatten(), (row, col.flatten())), shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]) + (self.kernel_0.flatten(), (row, col.flatten())), + shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]), ) M.eliminate_zeros() @@ -359,7 +360,7 @@ def assemble_1_form(self, tensor_space_FEM): # convert to sparse matrix indices = xp.indices( - (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) + (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1), ) shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] @@ -373,7 +374,8 @@ def assemble_1_form(self, tensor_space_FEM): col = Nj[1] * Nj[2] * col1 + Nj[2] * col2 + col3 M11 = spa.csr_matrix( - (self.kernel_1_11.flatten(), (row, col.flatten())), shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]) + (self.kernel_1_11.flatten(), (row, col.flatten())), + shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]), ) M11.eliminate_zeros() @@ -385,7 +387,7 @@ def assemble_1_form(self, tensor_space_FEM): # convert to sparse matrix indices = xp.indices( - (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) + (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1), ) shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] @@ -399,7 +401,8 @@ def assemble_1_form(self, tensor_space_FEM): col = Nj[1] * Nj[2] * col1 + Nj[2] * col2 + col3 M12 = spa.csr_matrix( - (self.kernel_1_12.flatten(), (row, col.flatten())), shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]) + (self.kernel_1_12.flatten(), (row, col.flatten())), + shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]), ) M12.eliminate_zeros() @@ -411,7 +414,7 @@ def assemble_1_form(self, tensor_space_FEM): # convert to sparse matrix indices = xp.indices( - (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) + (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1), ) shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] @@ -425,7 +428,8 @@ def assemble_1_form(self, tensor_space_FEM): col = Nj[1] * Nj[2] * col1 + Nj[2] * col2 + col3 M13 = spa.csr_matrix( - (self.kernel_1_13.flatten(), (row, col.flatten())), shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]) + (self.kernel_1_13.flatten(), (row, col.flatten())), + shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]), ) M13.eliminate_zeros() @@ -437,7 +441,7 @@ def assemble_1_form(self, tensor_space_FEM): # convert to sparse matrix indices = xp.indices( - (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) + (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1), ) shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] @@ -451,7 +455,8 @@ def assemble_1_form(self, tensor_space_FEM): col = Nj[1] * Nj[2] * col1 + Nj[2] * col2 + col3 M22 = spa.csr_matrix( - (self.kernel_1_22.flatten(), (row, col.flatten())), shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]) + (self.kernel_1_22.flatten(), (row, col.flatten())), + shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]), ) M22.eliminate_zeros() @@ -463,7 +468,7 @@ def assemble_1_form(self, tensor_space_FEM): # convert to sparse matrix indices = xp.indices( - (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) + (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1), ) shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] @@ -477,7 +482,8 @@ def assemble_1_form(self, tensor_space_FEM): col = Nj[1] * Nj[2] * col1 + Nj[2] * col2 + col3 M23 = spa.csr_matrix( - (self.kernel_1_23.flatten(), (row, col.flatten())), shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]) + (self.kernel_1_23.flatten(), (row, col.flatten())), + shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]), ) M23.eliminate_zeros() @@ -489,7 +495,7 @@ def assemble_1_form(self, tensor_space_FEM): # convert to sparse matrix indices = xp.indices( - (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) + (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1), ) shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] @@ -503,7 +509,8 @@ def assemble_1_form(self, tensor_space_FEM): col = Nj[1] * Nj[2] * col1 + Nj[2] * col2 + col3 M33 = spa.csr_matrix( - (self.kernel_1_33.flatten(), (row, col.flatten())), shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]) + (self.kernel_1_33.flatten(), (row, col.flatten())), + shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]), ) M33.eliminate_zeros() diff --git a/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_local.py b/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_local.py index 7c9425e47..43c8c8ff9 100644 --- a/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_local.py +++ b/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_local.py @@ -87,7 +87,7 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co for a in range(3): # self.related[a] = int(xp.floor(NbaseN[a]/2.0)) self.related[a] = int( - xp.floor((3 * int((self.p_size[a] * (self.p_shape[a] + 1)) * self.Nel[a] + 1) + 3 * self.p[a]) / 2.0) + xp.floor((3 * int((self.p_size[a] * (self.p_shape[a] + 1)) * self.Nel[a] + 1) + 3 * self.p[a]) / 2.0), ) if (2 * self.related[a] + 1) > NbaseN[a]: self.related[a] = int(xp.floor(NbaseN[a] / 2.0)) @@ -294,7 +294,9 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co self.wts = [0, 0, 0] for a in range(3): self.pts[a], self.wts[a] = bsp.quadrature_grid( - [0, 1.0 / 2.0 / self.Nel[a]], self.pts_loc[a], self.wts_loc[a] + [0, 1.0 / 2.0 / self.Nel[a]], + self.pts_loc[a], + self.wts_loc[a], ) # print('check_pts', self.pts[0].shape, self.pts[1].shape, self.pts[2].shape) # print('check_pts', self.wts) @@ -403,7 +405,7 @@ def assemble_0_form(self, tensor_space_FEM, mpi_comm): # conversion to sparse matrix indices = xp.indices( - (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) + (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1), ) shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] @@ -417,7 +419,8 @@ def assemble_0_form(self, tensor_space_FEM, mpi_comm): col = Nj[1] * Ni[2] * col1 + Ni[2] * col2 + col3 M = spa.csr_matrix( - (self.kernel_0.flatten(), (row, col.flatten())), shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]) + (self.kernel_0.flatten(), (row, col.flatten())), + shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]), ) M.eliminate_zeros() @@ -460,7 +463,7 @@ def assemble_1_form(self, tensor_space_FEM): # convert to sparse matrix indices = xp.indices( - (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) + (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1), ) shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] @@ -474,7 +477,8 @@ def assemble_1_form(self, tensor_space_FEM): col = Nj[1] * Nj[2] * col1 + Nj[2] * col2 + col3 M11 = spa.csr_matrix( - (self.kernel_1_11.flatten(), (row, col.flatten())), shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]) + (self.kernel_1_11.flatten(), (row, col.flatten())), + shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]), ) M11.eliminate_zeros() @@ -486,7 +490,7 @@ def assemble_1_form(self, tensor_space_FEM): # convert to sparse matrix indices = xp.indices( - (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) + (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1), ) shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] @@ -500,7 +504,8 @@ def assemble_1_form(self, tensor_space_FEM): col = Nj[1] * Nj[2] * col1 + Nj[2] * col2 + col3 M12 = spa.csr_matrix( - (self.kernel_1_12.flatten(), (row, col.flatten())), shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]) + (self.kernel_1_12.flatten(), (row, col.flatten())), + shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]), ) M12.eliminate_zeros() @@ -512,7 +517,7 @@ def assemble_1_form(self, tensor_space_FEM): # convert to sparse matrix indices = xp.indices( - (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) + (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1), ) shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] @@ -526,7 +531,8 @@ def assemble_1_form(self, tensor_space_FEM): col = Nj[1] * Nj[2] * col1 + Nj[2] * col2 + col3 M13 = spa.csr_matrix( - (self.kernel_1_13.flatten(), (row, col.flatten())), shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]) + (self.kernel_1_13.flatten(), (row, col.flatten())), + shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]), ) M13.eliminate_zeros() @@ -538,7 +544,7 @@ def assemble_1_form(self, tensor_space_FEM): # convert to sparse matrix indices = xp.indices( - (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) + (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1), ) shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] @@ -552,7 +558,8 @@ def assemble_1_form(self, tensor_space_FEM): col = Nj[1] * Nj[2] * col1 + Nj[2] * col2 + col3 M22 = spa.csr_matrix( - (self.kernel_1_22.flatten(), (row, col.flatten())), shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]) + (self.kernel_1_22.flatten(), (row, col.flatten())), + shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]), ) M22.eliminate_zeros() @@ -564,7 +571,7 @@ def assemble_1_form(self, tensor_space_FEM): # convert to sparse matrix indices = xp.indices( - (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) + (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1), ) shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] @@ -578,7 +585,8 @@ def assemble_1_form(self, tensor_space_FEM): col = Nj[1] * Nj[2] * col1 + Nj[2] * col2 + col3 M23 = spa.csr_matrix( - (self.kernel_1_23.flatten(), (row, col.flatten())), shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]) + (self.kernel_1_23.flatten(), (row, col.flatten())), + shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]), ) M23.eliminate_zeros() @@ -590,7 +598,7 @@ def assemble_1_form(self, tensor_space_FEM): # convert to sparse matrix indices = xp.indices( - (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1) + (Ni[0], Ni[1], Ni[2], 2 * self.related[0] + 1, 2 * self.related[1] + 1, 2 * self.related[2] + 1), ) shift = [xp.arange(Ni) - offset for Ni, offset in zip(Ni, self.related)] @@ -604,7 +612,8 @@ def assemble_1_form(self, tensor_space_FEM): col = Nj[1] * Nj[2] * col1 + Nj[2] * col2 + col3 M33 = spa.csr_matrix( - (self.kernel_1_33.flatten(), (row, col.flatten())), shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]) + (self.kernel_1_33.flatten(), (row, col.flatten())), + shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]), ) M33.eliminate_zeros() diff --git a/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_local_projector_kernel.py b/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_local_projector_kernel.py index c0ebc624d..6db315daa 100644 --- a/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_local_projector_kernel.py +++ b/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_local_projector_kernel.py @@ -108,7 +108,8 @@ def kernel_0_form( width[il1] = p[il1] + cell_number[il1] - 1 mat_f = empty( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], num_cell[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], num_cell[2]), + dtype=float, ) mat_f[:, :, :, :, :, :] = 0.0 @@ -335,7 +336,8 @@ def potential_kernel_0_form( width[il1] = p[il1] + cell_number[il1] - 1 mat_f = empty( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], num_cell[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], num_cell[2]), + dtype=float, ) mat_f[:, :, :, :, :, :] = 0.0 @@ -539,33 +541,42 @@ def kernel_1_form( # evaluation of function at interpolation/quadrature points mat_11 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_21 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_31 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_12 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_22 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_32 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_13 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) mat_23 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) mat_33 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) for i1 in range(cell_number[0]): @@ -1259,33 +1270,42 @@ def bv_localproj_push( # evaluation of function at interpolation/quadrature points mat_11 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_21 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_31 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_12 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_22 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_32 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_13 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) mat_23 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) mat_33 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) for i1 in range(cell_number[0]): @@ -1805,33 +1825,42 @@ def kernel_1_heavy( # evaluation of function at interpolation/quadrature points mat_11 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_21 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_31 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_12 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_22 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_32 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_13 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) mat_23 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) mat_33 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) for i1 in range(cell_number[0]): @@ -2373,33 +2402,42 @@ def vv_1_form( # evaluation of function at interpolation/quadrature points mat_11 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_21 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_31 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_12 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_22 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_32 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_13 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) mat_23 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) mat_33 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) for i1 in range(cell_number[0]): @@ -2905,33 +2943,42 @@ def vv_push( # evaluation of function at interpolation/quadrature points mat_11 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_21 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_31 = zeros( - (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], 2, num_cell[1], num_cell[2], quad[0]), + dtype=float, ) mat_12 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_22 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_32 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], 2, num_cell[2], quad[1]), + dtype=float, ) mat_13 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) mat_23 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) mat_33 = zeros( - (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), dtype=float + (cell_number[0], cell_number[1], cell_number[2], num_cell[0], num_cell[1], 2, quad[2]), + dtype=float, ) for i1 in range(cell_number[0]): diff --git a/src/struphy/eigenvalue_solvers/mass_matrices_3d.py b/src/struphy/eigenvalue_solvers/mass_matrices_3d.py index 05f019e13..d3dc4cad2 100644 --- a/src/struphy/eigenvalue_solvers/mass_matrices_3d.py +++ b/src/struphy/eigenvalue_solvers/mass_matrices_3d.py @@ -209,7 +209,8 @@ def get_M1(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): col = Nj[1] * Nj[2] * col1 + Nj[2] * col2 + col3 M[a][b] = spa.csr_matrix( - (M[a][b].flatten(), (row, col.flatten())), shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]) + (M[a][b].flatten(), (row, col.flatten())), + shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]), ) M[a][b].eliminate_zeros() @@ -328,7 +329,8 @@ def get_M2(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): col = Nj[1] * Nj[2] * col1 + Nj[2] * col2 + col3 M[a][b] = spa.csr_matrix( - (M[a][b].flatten(), (row, col.flatten())), shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]) + (M[a][b].flatten(), (row, col.flatten())), + shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]), ) M[a][b].eliminate_zeros() @@ -563,7 +565,8 @@ def get_Mv(tensor_space_FEM, domain, apply_boundary_ops=False, weight=None): col = Nj[1] * Nj[2] * col1 + Nj[2] * col2 + col3 M[a][b] = spa.csr_matrix( - (M[a][b].flatten(), (row, col.flatten())), shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]) + (M[a][b].flatten(), (row, col.flatten())), + shape=(Ni[0] * Ni[1] * Ni[2], Nj[0] * Nj[1] * Nj[2]), ) M[a][b].eliminate_zeros() diff --git a/src/struphy/eigenvalue_solvers/mhd_axisymmetric_main.py b/src/struphy/eigenvalue_solvers/mhd_axisymmetric_main.py index c44c97335..b8d4aaf81 100644 --- a/src/struphy/eigenvalue_solvers/mhd_axisymmetric_main.py +++ b/src/struphy/eigenvalue_solvers/mhd_axisymmetric_main.py @@ -72,7 +72,12 @@ def solve_mhd_ev_problem_2d(num_params, eq_mhd, n_tor, basis_tor="i", path_out=N # set up 2d tensor-product space space_2d = Tensor_spline_space( - [space_1d_1, space_1d_2], polar_ck, eq_mhd.domain.cx[:, :, 0], eq_mhd.domain.cy[:, :, 0], n_tor, basis_tor + [space_1d_1, space_1d_2], + polar_ck, + eq_mhd.domain.cx[:, :, 0], + eq_mhd.domain.cy[:, :, 0], + n_tor, + basis_tor, ) # set up 2d projectors @@ -141,7 +146,7 @@ def solve_mhd_ev_problem_2d(num_params, eq_mhd, n_tor, basis_tor="i", path_out=N .dot( EF.T.dot(space_2d.C0.conjugate().T.dot(M2_0.dot(space_2d.C0.dot(EF)))) + mhd_ops.MJ_mat.dot(space_2d.C0.dot(EF)) - - space_2d.D0.conjugate().T.dot(M3_0.dot(L)) + - space_2d.D0.conjugate().T.dot(M3_0.dot(L)), ) .toarray() ) @@ -162,7 +167,8 @@ def solve_mhd_ev_problem_2d(num_params, eq_mhd, n_tor, basis_tor="i", path_out=N n_tor_str = "+" + str(n_tor) xp.save( - os.path.join(path_out, "spec_n_" + n_tor_str + ".npy"), xp.vstack((omega2.reshape(1, omega2.size), U2_eig)) + os.path.join(path_out, "spec_n_" + n_tor_str + ".npy"), + xp.vstack((omega2.reshape(1, omega2.size), U2_eig)), ) # or return eigenfrequencies, eigenvectors and system matrix @@ -180,7 +186,7 @@ def solve_mhd_ev_problem_2d(num_params, eq_mhd, n_tor, basis_tor="i", path_out=N # parse arguments parser = argparse.ArgumentParser( - description="Computes the complete eigenspectrum for a given axisymmetric MHD equilibrium." + description="Computes the complete eigenspectrum for a given axisymmetric MHD equilibrium.", ) parser.add_argument("n_tor", type=int, help="the toroidal mode number") diff --git a/src/struphy/eigenvalue_solvers/mhd_axisymmetric_pproc.py b/src/struphy/eigenvalue_solvers/mhd_axisymmetric_pproc.py index 10e9e274e..1685147e1 100644 --- a/src/struphy/eigenvalue_solvers/mhd_axisymmetric_pproc.py +++ b/src/struphy/eigenvalue_solvers/mhd_axisymmetric_pproc.py @@ -21,7 +21,10 @@ def main(): ) parser.add_argument( - "--input-abs", type=str, metavar="DIR", help="directory with eigenspectrum (.npy) file, absolute path" + "--input-abs", + type=str, + metavar="DIR", + help="directory with eigenspectrum (.npy) file, absolute path", ) parser.add_argument("lower", type=float, help="lower range of squared eigenfrequency") diff --git a/src/struphy/eigenvalue_solvers/mhd_operators.py b/src/struphy/eigenvalue_solvers/mhd_operators.py index 5f1462c24..6f7325c6b 100644 --- a/src/struphy/eigenvalue_solvers/mhd_operators.py +++ b/src/struphy/eigenvalue_solvers/mhd_operators.py @@ -658,11 +658,13 @@ def __Mn(self, u): if self.Mn_as_tensor: if self.core.basis_u == 0: out = self.core.space.apply_Mv_ten( - u, [[self.Mn_mat[0], self.core.space.M0_tor], [self.Mn_mat[1], self.core.space.M0_tor]] + u, + [[self.Mn_mat[0], self.core.space.M0_tor], [self.Mn_mat[1], self.core.space.M0_tor]], ) elif self.core.basis_u == 2: out = self.core.space.apply_M2_ten( - u, [[self.Mn_mat[0], self.core.space.M1_tor], [self.Mn_mat[1], self.core.space.M0_tor]] + u, + [[self.Mn_mat[0], self.core.space.M1_tor], [self.Mn_mat[1], self.core.space.M0_tor]], ) else: @@ -681,7 +683,8 @@ def __MJ(self, b): out = xp.zeros(self.core.space.Ev_0.shape[0], dtype=float) elif self.core.basis_u == 2: out = self.core.space.apply_M2_ten( - b, [[self.MJ_mat[0], self.core.space.M1_tor], [self.MJ_mat[1], self.core.space.M0_tor]] + b, + [[self.MJ_mat[0], self.core.space.M1_tor], [self.MJ_mat[1], self.core.space.M0_tor]], ) else: @@ -700,7 +703,7 @@ def __L(self, u): if self.core.basis_u == 0: out = -self.core.space.D0.dot(self.__PF(u)) - (self.gamma - 1) * self.__PR( - self.core.space.D0.dot(self.__JF(u)) + self.core.space.D0.dot(self.__JF(u)), ) elif self.core.basis_u == 2: out = -self.core.space.D0.dot(self.__PF(u)) - (self.gamma - 1) * self.__PR(self.core.space.D0.dot(u)) @@ -782,27 +785,32 @@ def set_operators(self, dt_2=1.0, dt_6=1.0): if hasattr(self, "dofs_Mn"): self.Mn = spa.linalg.LinearOperator( - (self.core.space.Ev_0.shape[0], self.core.space.Ev_0.shape[0]), matvec=self.__Mn + (self.core.space.Ev_0.shape[0], self.core.space.Ev_0.shape[0]), + matvec=self.__Mn, ) if hasattr(self, "dofs_MJ"): self.MJ = spa.linalg.LinearOperator( - (self.core.space.Ev_0.shape[0], self.core.space.E2_0.shape[0]), matvec=self.__MJ + (self.core.space.Ev_0.shape[0], self.core.space.E2_0.shape[0]), + matvec=self.__MJ, ) if hasattr(self, "dofs_PF") and hasattr(self, "dofs_PR") and hasattr(self, "dofs_JF"): self.L = spa.linalg.LinearOperator( - (self.core.space.E3_0.shape[0], self.core.space.Ev_0.shape[0]), matvec=self.__L + (self.core.space.E3_0.shape[0], self.core.space.Ev_0.shape[0]), + matvec=self.__L, ) if hasattr(self, "Mn_mat") and hasattr(self, "dofs_EF"): self.S2 = spa.linalg.LinearOperator( - (self.core.space.Ev_0.shape[0], self.core.space.Ev_0.shape[0]), matvec=self.__S2 + (self.core.space.Ev_0.shape[0], self.core.space.Ev_0.shape[0]), + matvec=self.__S2, ) if hasattr(self, "Mn_mat") and hasattr(self, "L"): self.S6 = spa.linalg.LinearOperator( - (self.core.space.Ev_0.shape[0], self.core.space.Ev_0.shape[0]), matvec=self.__S6 + (self.core.space.Ev_0.shape[0], self.core.space.Ev_0.shape[0]), + matvec=self.__S6, ) elif self.core.basis_u == 2: @@ -836,27 +844,32 @@ def set_operators(self, dt_2=1.0, dt_6=1.0): if hasattr(self, "Mn_mat"): self.Mn = spa.linalg.LinearOperator( - (self.core.space.E2_0.shape[0], self.core.space.E2_0.shape[0]), matvec=self.__Mn + (self.core.space.E2_0.shape[0], self.core.space.E2_0.shape[0]), + matvec=self.__Mn, ) if hasattr(self, "MJ_mat"): self.MJ = spa.linalg.LinearOperator( - (self.core.space.E2_0.shape[0], self.core.space.E2_0.shape[0]), matvec=self.__MJ + (self.core.space.E2_0.shape[0], self.core.space.E2_0.shape[0]), + matvec=self.__MJ, ) if hasattr(self, "dofs_PF") and hasattr(self, "dofs_PR"): self.L = spa.linalg.LinearOperator( - (self.core.space.E3_0.shape[0], self.core.space.E2_0.shape[0]), matvec=self.__L + (self.core.space.E3_0.shape[0], self.core.space.E2_0.shape[0]), + matvec=self.__L, ) if hasattr(self, "Mn_mat") and hasattr(self, "dofs_EF"): self.S2 = spa.linalg.LinearOperator( - (self.core.space.E2_0.shape[0], self.core.space.E2_0.shape[0]), matvec=self.__S2 + (self.core.space.E2_0.shape[0], self.core.space.E2_0.shape[0]), + matvec=self.__S2, ) if hasattr(self, "Mn_mat") and hasattr(self, "L"): self.S6 = spa.linalg.LinearOperator( - (self.core.space.E2_0.shape[0], self.core.space.E2_0.shape[0]), matvec=self.__S6 + (self.core.space.E2_0.shape[0], self.core.space.E2_0.shape[0]), + matvec=self.__S6, ) # ====================================== @@ -1024,7 +1037,7 @@ def set_preconditioner_S2(self, which, tol_inv=1e-15, drop_tol=1e-4, fill_fac=10 # assemble approximate S2 matrix S2_approx = Mn + self.dt_2**2 / 4 * EF_approx.T.dot( - self.core.space.C0.T.dot(M2_0.dot(self.core.space.C0.dot(EF_approx))) + self.core.space.C0.T.dot(M2_0.dot(self.core.space.C0.dot(EF_approx))), ) del Mn, EF_approx, M2_0 @@ -1123,7 +1136,7 @@ def set_preconditioner_S6(self, which, tol_inv=1e-15, drop_tol=1e-4, fill_fac=10 # assemble approximate L matrix if self.core.basis_u == 0: L_approx = -self.core.space.D0.dot(PF_approx) - (self.gamma - 1) * PR_approx.dot( - self.core.space.D0.dot(JF_approx) + self.core.space.D0.dot(JF_approx), ) del PF_approx, PR_approx diff --git a/src/struphy/eigenvalue_solvers/mhd_operators_core.py b/src/struphy/eigenvalue_solvers/mhd_operators_core.py index 76752ccc3..61d534148 100644 --- a/src/struphy/eigenvalue_solvers/mhd_operators_core.py +++ b/src/struphy/eigenvalue_solvers/mhd_operators_core.py @@ -139,7 +139,8 @@ def get_blocks_EF(self, pol=True): ) EF_12 = spa.csr_matrix( - (val, (row, col)), shape=(self.space.Ntot_1form[0] // self.N3, self.space.Ntot_0form // self.N3) + (val, (row, col)), + shape=(self.space.Ntot_1form[0] // self.N3, self.space.Ntot_0form // self.N3), ) EF_12.eliminate_zeros() # ---------------------------------------------------- @@ -173,7 +174,8 @@ def get_blocks_EF(self, pol=True): ) EF_13 = spa.csr_matrix( - (val, (row, col)), shape=(self.space.Ntot_1form[0] // self.N3, self.space.Ntot_0form // self.N3) + (val, (row, col)), + shape=(self.space.Ntot_1form[0] // self.N3, self.space.Ntot_0form // self.N3), ) EF_13.eliminate_zeros() # ---------------------------------------------------- @@ -207,7 +209,8 @@ def get_blocks_EF(self, pol=True): ) EF_21 = spa.csr_matrix( - (val, (row, col)), shape=(self.space.Ntot_1form[1] // self.N3, self.space.Ntot_0form // self.N3) + (val, (row, col)), + shape=(self.space.Ntot_1form[1] // self.N3, self.space.Ntot_0form // self.N3), ) EF_21.eliminate_zeros() # ---------------------------------------------------- @@ -241,7 +244,8 @@ def get_blocks_EF(self, pol=True): ) EF_23 = spa.csr_matrix( - (val, (row, col)), shape=(self.space.Ntot_1form[1] // self.N3, self.space.Ntot_0form // self.N3) + (val, (row, col)), + shape=(self.space.Ntot_1form[1] // self.N3, self.space.Ntot_0form // self.N3), ) EF_23.eliminate_zeros() # ---------------------------------------------------- @@ -269,7 +273,8 @@ def get_blocks_EF(self, pol=True): ) EF_31 = spa.csr_matrix( - (val, (row, col)), shape=(self.space.Ntot_1form[2] // self.D3, self.space.Ntot_0form // self.N3) + (val, (row, col)), + shape=(self.space.Ntot_1form[2] // self.D3, self.space.Ntot_0form // self.N3), ) EF_31.eliminate_zeros() # ---------------------------------------------------- @@ -297,7 +302,8 @@ def get_blocks_EF(self, pol=True): ) EF_32 = spa.csr_matrix( - (val, (row, col)), shape=(self.space.Ntot_1form[2] // self.D3, self.space.Ntot_0form // self.N3) + (val, (row, col)), + shape=(self.space.Ntot_1form[2] // self.D3, self.space.Ntot_0form // self.N3), ) EF_32.eliminate_zeros() # ---------------------------------------------------- @@ -310,13 +316,16 @@ def get_blocks_EF(self, pol=True): # assemble sparse matrix val = xp.empty( - self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=float + self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=float, ) row = xp.empty( - self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int + self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=int, ) col = xp.empty( - self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int + self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=int, ) ker.rhs11( @@ -351,13 +360,16 @@ def get_blocks_EF(self, pol=True): # assemble sparse matrix val = xp.empty( - self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=float + self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=float, ) row = xp.empty( - self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int + self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=int, ) col = xp.empty( - self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int + self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=int, ) ker.rhs11( @@ -392,13 +404,16 @@ def get_blocks_EF(self, pol=True): # assemble sparse matrix val = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=float + self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=float, ) row = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int + self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=int, ) col = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int + self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=int, ) ker.rhs12( @@ -433,13 +448,16 @@ def get_blocks_EF(self, pol=True): # assemble sparse matrix val = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=float + self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=float, ) row = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int + self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=int, ) col = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int + self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=int, ) ker.rhs12( @@ -474,13 +492,16 @@ def get_blocks_EF(self, pol=True): # assemble sparse matrix val = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=float + self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, + dtype=float, ) row = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=int + self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, + dtype=int, ) col = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=int + self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, + dtype=int, ) ker.rhs13( @@ -515,13 +536,16 @@ def get_blocks_EF(self, pol=True): # assemble sparse matrix val = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=float + self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, + dtype=float, ) row = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=int + self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, + dtype=int, ) col = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=int + self.dofs_0_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, + dtype=int, ) ker.rhs13( @@ -584,7 +608,8 @@ def get_blocks_EF(self, pol=True): ) EF_12 = spa.csr_matrix( - (val, (row, col)), shape=(self.space.Ntot_1form[0] // self.N3, self.space.Ntot_2form[1] // self.D3) + (val, (row, col)), + shape=(self.space.Ntot_1form[0] // self.N3, self.space.Ntot_2form[1] // self.D3), ) EF_12.eliminate_zeros() # ---------------------------------------------------- @@ -622,7 +647,8 @@ def get_blocks_EF(self, pol=True): ) EF_13 = spa.csr_matrix( - (val, (row, col)), shape=(self.space.Ntot_1form[0] // self.N3, self.space.Ntot_2form[2] // self.N3) + (val, (row, col)), + shape=(self.space.Ntot_1form[0] // self.N3, self.space.Ntot_2form[2] // self.N3), ) EF_13.eliminate_zeros() # ---------------------------------------------------- @@ -660,7 +686,8 @@ def get_blocks_EF(self, pol=True): ) EF_21 = spa.csr_matrix( - (val, (row, col)), shape=(self.space.Ntot_1form[1] // self.N3, self.space.Ntot_2form[0] // self.D3) + (val, (row, col)), + shape=(self.space.Ntot_1form[1] // self.N3, self.space.Ntot_2form[0] // self.D3), ) EF_21.eliminate_zeros() # ---------------------------------------------------- @@ -698,7 +725,8 @@ def get_blocks_EF(self, pol=True): ) EF_23 = spa.csr_matrix( - (val, (row, col)), shape=(self.space.Ntot_1form[1] // self.N3, self.space.Ntot_2form[2] // self.N3) + (val, (row, col)), + shape=(self.space.Ntot_1form[1] // self.N3, self.space.Ntot_2form[2] // self.N3), ) EF_23.eliminate_zeros() # ---------------------------------------------------- @@ -729,7 +757,8 @@ def get_blocks_EF(self, pol=True): ) EF_31 = spa.csr_matrix( - (val, (row, col)), shape=(self.space.Ntot_1form[2] // self.D3, self.space.Ntot_2form[0] // self.D3) + (val, (row, col)), + shape=(self.space.Ntot_1form[2] // self.D3, self.space.Ntot_2form[0] // self.D3), ) EF_31.eliminate_zeros() # ---------------------------------------------------- @@ -760,7 +789,8 @@ def get_blocks_EF(self, pol=True): ) EF_32 = spa.csr_matrix( - (val, (row, col)), shape=(self.space.Ntot_1form[2] // self.D3, self.space.Ntot_2form[1] // self.D3) + (val, (row, col)), + shape=(self.space.Ntot_1form[2] // self.D3, self.space.Ntot_2form[1] // self.D3), ) EF_32.eliminate_zeros() # ---------------------------------------------------- @@ -773,19 +803,22 @@ def get_blocks_EF(self, pol=True): # evaluate Jacobian determinant at at interpolation and quadrature points det_dF = abs( - self.equilibrium.domain.jacobian_det(self.eta_his[0].flatten(), self.eta_int[1], self.eta_int[2]) + self.equilibrium.domain.jacobian_det(self.eta_his[0].flatten(), self.eta_int[1], self.eta_int[2]), ) det_dF = det_dF.reshape(self.nhis[0], self.nq[0], self.nint[1], self.nint[2]) # assemble sparse matrix val = xp.empty( - self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_D_i[2][0].size, dtype=float + self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_D_i[2][0].size, + dtype=float, ) row = xp.empty( - self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_D_i[2][0].size, dtype=int + self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_D_i[2][0].size, + dtype=int, ) col = xp.empty( - self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_D_i[2][0].size, dtype=int + self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_0_D_i[2][0].size, + dtype=int, ) ker.rhs11( @@ -820,19 +853,22 @@ def get_blocks_EF(self, pol=True): # evaluate Jacobian determinant at at interpolation and quadrature points det_dF = abs( - self.equilibrium.domain.jacobian_det(self.eta_his[0].flatten(), self.eta_int[1], self.eta_int[2]) + self.equilibrium.domain.jacobian_det(self.eta_his[0].flatten(), self.eta_int[1], self.eta_int[2]), ) det_dF = det_dF.reshape(self.nhis[0], self.nq[0], self.nint[1], self.nint[2]) # assemble sparse matrix val = xp.empty( - self.dofs_1_D_i[0][0].size * self.dofs_0_D_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=float + self.dofs_1_D_i[0][0].size * self.dofs_0_D_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=float, ) row = xp.empty( - self.dofs_1_D_i[0][0].size * self.dofs_0_D_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int + self.dofs_1_D_i[0][0].size * self.dofs_0_D_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=int, ) col = xp.empty( - self.dofs_1_D_i[0][0].size * self.dofs_0_D_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int + self.dofs_1_D_i[0][0].size * self.dofs_0_D_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=int, ) ker.rhs11( @@ -867,19 +903,22 @@ def get_blocks_EF(self, pol=True): # evaluate Jacobian determinant at at interpolation and quadrature points det_dF = abs( - self.equilibrium.domain.jacobian_det(self.eta_int[0], self.eta_his[1].flatten(), self.eta_int[2]) + self.equilibrium.domain.jacobian_det(self.eta_int[0], self.eta_his[1].flatten(), self.eta_int[2]), ) det_dF = det_dF.reshape(self.nint[0], self.nhis[1], self.nq[1], self.nint[2]) # assemble sparse matrix val = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_D_i[2][0].size, dtype=float + self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_D_i[2][0].size, + dtype=float, ) row = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_D_i[2][0].size, dtype=int + self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_D_i[2][0].size, + dtype=int, ) col = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_D_i[2][0].size, dtype=int + self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_D_i[2][0].size, + dtype=int, ) ker.rhs12( @@ -914,19 +953,22 @@ def get_blocks_EF(self, pol=True): # evaluate Jacobian determinant at at interpolation and quadrature points det_dF = abs( - self.equilibrium.domain.jacobian_det(self.eta_int[0], self.eta_his[1].flatten(), self.eta_int[2]) + self.equilibrium.domain.jacobian_det(self.eta_int[0], self.eta_his[1].flatten(), self.eta_int[2]), ) det_dF = det_dF.reshape(self.nint[0], self.nhis[1], self.nq[1], self.nint[2]) # assemble sparse matrix val = xp.empty( - self.dofs_0_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=float + self.dofs_0_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=float, ) row = xp.empty( - self.dofs_0_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int + self.dofs_0_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=int, ) col = xp.empty( - self.dofs_0_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int + self.dofs_0_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=int, ) ker.rhs12( @@ -961,19 +1003,22 @@ def get_blocks_EF(self, pol=True): # evaluate Jacobian determinant at at interpolation and quadrature points det_dF = abs( - self.equilibrium.domain.jacobian_det(self.eta_int[0], self.eta_int[1], self.eta_his[2].flatten()) + self.equilibrium.domain.jacobian_det(self.eta_int[0], self.eta_int[1], self.eta_his[2].flatten()), ) det_dF = det_dF.reshape(self.nint[0], self.nint[1], self.nhis[2], self.nq[2]) # assemble sparse matrix val = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_0_D_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=float + self.dofs_0_N_i[0][0].size * self.dofs_0_D_i[1][0].size * self.dofs_1_D_i[2][0].size, + dtype=float, ) row = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_0_D_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int + self.dofs_0_N_i[0][0].size * self.dofs_0_D_i[1][0].size * self.dofs_1_D_i[2][0].size, + dtype=int, ) col = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_0_D_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int + self.dofs_0_N_i[0][0].size * self.dofs_0_D_i[1][0].size * self.dofs_1_D_i[2][0].size, + dtype=int, ) ker.rhs13( @@ -1008,19 +1053,22 @@ def get_blocks_EF(self, pol=True): # evaluate Jacobian determinant at at interpolation and quadrature points det_dF = abs( - self.equilibrium.domain.jacobian_det(self.eta_int[0], self.eta_int[1], self.eta_his[2].flatten()) + self.equilibrium.domain.jacobian_det(self.eta_int[0], self.eta_int[1], self.eta_his[2].flatten()), ) det_dF = det_dF.reshape(self.nint[0], self.nint[1], self.nhis[2], self.nq[2]) # assemble sparse matrix val = xp.empty( - self.dofs_0_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=float + self.dofs_0_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_D_i[2][0].size, + dtype=float, ) row = xp.empty( - self.dofs_0_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int + self.dofs_0_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_D_i[2][0].size, + dtype=int, ) col = xp.empty( - self.dofs_0_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int + self.dofs_0_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_D_i[2][0].size, + dtype=int, ) ker.rhs13( @@ -1116,7 +1164,8 @@ def get_blocks_FL(self, which, pol=True): ) F_11 = spa.csr_matrix( - (val, (row, col)), shape=(self.space.Ntot_2form[0] // self.D3, self.space.Ntot_0form // self.N3) + (val, (row, col)), + shape=(self.space.Ntot_2form[0] // self.D3, self.space.Ntot_0form // self.N3), ) F_11.eliminate_zeros() # ------------------------------------------------------------ @@ -1156,7 +1205,8 @@ def get_blocks_FL(self, which, pol=True): ) F_22 = spa.csr_matrix( - (val, (row, col)), shape=(self.space.Ntot_2form[1] // self.D3, self.space.Ntot_0form // self.N3) + (val, (row, col)), + shape=(self.space.Ntot_2form[1] // self.D3, self.space.Ntot_0form // self.N3), ) F_22.eliminate_zeros() # ------------------------------------------------------------ @@ -1199,7 +1249,8 @@ def get_blocks_FL(self, which, pol=True): ) F_33 = spa.csr_matrix( - (val, (row, col)), shape=(self.space.Ntot_2form[2] // self.N3, self.space.Ntot_0form // self.N3) + (val, (row, col)), + shape=(self.space.Ntot_2form[2] // self.N3, self.space.Ntot_0form // self.N3), ) F_33.eliminate_zeros() # ------------------------------------------------------------ @@ -1213,20 +1264,25 @@ def get_blocks_FL(self, which, pol=True): EQ = self.equilibrium.p3(self.eta_int[0], self.eta_his[1].flatten(), self.eta_his[2].flatten()) else: EQ = self.equilibrium.domain.jacobian_det( - self.eta_int[0], self.eta_his[1].flatten(), self.eta_his[2].flatten() + self.eta_int[0], + self.eta_his[1].flatten(), + self.eta_his[2].flatten(), ) EQ = EQ.reshape(self.nint[0], self.nhis[1], self.nq[1], self.nhis[2], self.nq[2]) # assemble sparse matrix val = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=float + self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_1_N_i[2][0].size, + dtype=float, ) row = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=int + self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_1_N_i[2][0].size, + dtype=int, ) col = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=int + self.dofs_0_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_1_N_i[2][0].size, + dtype=int, ) ker.rhs21( @@ -1265,20 +1321,25 @@ def get_blocks_FL(self, which, pol=True): EQ = self.equilibrium.p3(self.eta_his[0].flatten(), self.eta_int[1], self.eta_his[2].flatten()) else: EQ = self.equilibrium.domain.jacobian_det( - self.eta_his[0].flatten(), self.eta_int[1], self.eta_his[2].flatten() + self.eta_his[0].flatten(), + self.eta_int[1], + self.eta_his[2].flatten(), ) EQ = EQ.reshape(self.nhis[0], self.nq[0], self.nint[1], self.nhis[2], self.nq[2]) # assemble sparse matrix val = xp.empty( - self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=float + self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, + dtype=float, ) row = xp.empty( - self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=int + self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, + dtype=int, ) col = xp.empty( - self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, dtype=int + self.dofs_1_N_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_N_i[2][0].size, + dtype=int, ) ker.rhs22( @@ -1317,20 +1378,25 @@ def get_blocks_FL(self, which, pol=True): EQ = self.equilibrium.p3(self.eta_his[0].flatten(), self.eta_his[1].flatten(), self.eta_int[2]) else: EQ = self.equilibrium.domain.jacobian_det( - self.eta_his[0].flatten(), self.eta_his[1].flatten(), self.eta_int[2] + self.eta_his[0].flatten(), + self.eta_his[1].flatten(), + self.eta_int[2], ) EQ = EQ.reshape(self.nhis[0], self.nq[0], self.nhis[1], self.nq[1], self.nint[2]) # assemble sparse matrix val = xp.empty( - self.dofs_1_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=float + self.dofs_1_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=float, ) row = xp.empty( - self.dofs_1_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int + self.dofs_1_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=int, ) col = xp.empty( - self.dofs_1_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int + self.dofs_1_N_i[0][0].size * self.dofs_1_N_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=int, ) ker.rhs23( @@ -1400,7 +1466,8 @@ def get_blocks_FL(self, which, pol=True): ) F_11 = spa.csr_matrix( - (val, (row, col)), shape=(self.space.Ntot_2form[0] // self.D3, self.space.Ntot_2form[0] // self.D3) + (val, (row, col)), + shape=(self.space.Ntot_2form[0] // self.D3, self.space.Ntot_2form[0] // self.D3), ) F_11.eliminate_zeros() # ------------------------------------------------------------ @@ -1442,7 +1509,8 @@ def get_blocks_FL(self, which, pol=True): ) F_22 = spa.csr_matrix( - (val, (row, col)), shape=(self.space.Ntot_2form[1] // self.D3, self.space.Ntot_2form[1] // self.D3) + (val, (row, col)), + shape=(self.space.Ntot_2form[1] // self.D3, self.space.Ntot_2form[1] // self.D3), ) F_22.eliminate_zeros() # ------------------------------------------------------------ @@ -1458,7 +1526,7 @@ def get_blocks_FL(self, which, pol=True): # evaluate Jacobian determinant at at interpolation and quadrature points det_dF = abs( - self.equilibrium.domain.jacobian_det(self.eta_his[0].flatten(), self.eta_his[1].flatten(), 0.0) + self.equilibrium.domain.jacobian_det(self.eta_his[0].flatten(), self.eta_his[1].flatten(), 0.0), ) det_dF = det_dF.reshape(self.nhis[0], self.nq[0], self.nhis[1], self.nq[1]) @@ -1489,7 +1557,8 @@ def get_blocks_FL(self, which, pol=True): ) F_33 = spa.csr_matrix( - (val, (row, col)), shape=(self.space.Ntot_2form[2] // self.N3, self.space.Ntot_2form[2] // self.N3) + (val, (row, col)), + shape=(self.space.Ntot_2form[2] // self.N3, self.space.Ntot_2form[2] // self.N3), ) F_33.eliminate_zeros() # ------------------------------------------------------------ @@ -1507,20 +1576,25 @@ def get_blocks_FL(self, which, pol=True): # evaluate Jacobian determinant at at interpolation and quadrature points det_dF = abs( self.equilibrium.domain.jacobian_det( - self.eta_int[0], self.eta_his[1].flatten(), self.eta_his[2].flatten() - ) + self.eta_int[0], + self.eta_his[1].flatten(), + self.eta_his[2].flatten(), + ), ) det_dF = det_dF.reshape(self.nint[0], self.nhis[1], self.nq[1], self.nhis[2], self.nq[2]) # assemble sparse matrix val = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=float + self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_1_D_i[2][0].size, + dtype=float, ) row = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int + self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_1_D_i[2][0].size, + dtype=int, ) col = xp.empty( - self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int + self.dofs_0_N_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_1_D_i[2][0].size, + dtype=int, ) ker.rhs21( @@ -1563,20 +1637,25 @@ def get_blocks_FL(self, which, pol=True): # evaluate Jacobian determinant at at interpolation and quadrature points det_dF = abs( self.equilibrium.domain.jacobian_det( - self.eta_his[0].flatten(), self.eta_int[1], self.eta_his[2].flatten() - ) + self.eta_his[0].flatten(), + self.eta_int[1], + self.eta_his[2].flatten(), + ), ) det_dF = det_dF.reshape(self.nhis[0], self.nq[0], self.nint[1], self.nhis[2], self.nq[2]) # assemble sparse matrix val = xp.empty( - self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=float + self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_D_i[2][0].size, + dtype=float, ) row = xp.empty( - self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int + self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_D_i[2][0].size, + dtype=int, ) col = xp.empty( - self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int + self.dofs_1_D_i[0][0].size * self.dofs_0_N_i[1][0].size * self.dofs_1_D_i[2][0].size, + dtype=int, ) ker.rhs22( @@ -1619,20 +1698,25 @@ def get_blocks_FL(self, which, pol=True): # evaluate Jacobian determinant at at interpolation and quadrature points det_dF = abs( self.equilibrium.domain.jacobian_det( - self.eta_his[0].flatten(), self.eta_his[1].flatten(), self.eta_int[2] - ) + self.eta_his[0].flatten(), + self.eta_his[1].flatten(), + self.eta_int[2], + ), ) det_dF = det_dF.reshape(self.nhis[0], self.nq[0], self.nhis[1], self.nq[1], self.nint[2]) # assemble sparse matrix val = xp.empty( - self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=float + self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=float, ) row = xp.empty( - self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int + self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=int, ) col = xp.empty( - self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_N_i[2][0].size, dtype=int + self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_0_N_i[2][0].size, + dtype=int, ) ker.rhs23( @@ -1691,7 +1775,7 @@ def get_blocks_PR(self, pol=True): # evaluate Jacobian determinant at at interpolation and quadrature points det_dF = abs( - self.equilibrium.domain.jacobian_det(self.eta_his[0].flatten(), self.eta_his[1].flatten(), 0.0) + self.equilibrium.domain.jacobian_det(self.eta_his[0].flatten(), self.eta_his[1].flatten(), 0.0), ) det_dF = det_dF.reshape(self.nhis[0], self.nq[0], self.nhis[1], self.nq[1]) @@ -1722,7 +1806,8 @@ def get_blocks_PR(self, pol=True): ) PR = spa.csr_matrix( - (val, (row, col)), shape=(self.space.Ntot_3form // self.D3, self.space.Ntot_3form // self.D3) + (val, (row, col)), + shape=(self.space.Ntot_3form // self.D3, self.space.Ntot_3form // self.D3), ) PR.eliminate_zeros() # ----------------------------------------------------- @@ -1731,7 +1816,9 @@ def get_blocks_PR(self, pol=True): # --------------- ([his, his, his] of DDD) ------------ # evaluate equilibrium pressure at quadrature points P3_pts = self.equilibrium.p3( - self.eta_his[0].flatten(), self.eta_his[1].flatten(), self.eta_his[2].flatten() + self.eta_his[0].flatten(), + self.eta_his[1].flatten(), + self.eta_his[2].flatten(), ) P3_pts = P3_pts.reshape(self.nhis[0], self.nq[0], self.nhis[1], self.nq[1], self.nhis[2], self.nq[2]) @@ -1739,20 +1826,25 @@ def get_blocks_PR(self, pol=True): # evaluate Jacobian determinant at at interpolation and quadrature points det_dF = abs( self.equilibrium.domain.jacobian_det( - self.eta_his[0].flatten(), self.eta_his[1].flatten(), self.eta_his[2].flatten() - ) + self.eta_his[0].flatten(), + self.eta_his[1].flatten(), + self.eta_his[2].flatten(), + ), ) det_dF = det_dF.reshape(self.nhis[0], self.nq[0], self.nhis[1], self.nq[1], self.nhis[2], self.nq[2]) # assemble sparse matrix val = xp.empty( - self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=float + self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_1_D_i[2][0].size, + dtype=float, ) row = xp.empty( - self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int + self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_1_D_i[2][0].size, + dtype=int, ) col = xp.empty( - self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_1_D_i[2][0].size, dtype=int + self.dofs_1_D_i[0][0].size * self.dofs_1_D_i[1][0].size * self.dofs_1_D_i[2][0].size, + dtype=int, ) ker.rhs3( diff --git a/src/struphy/eigenvalue_solvers/projectors_global.py b/src/struphy/eigenvalue_solvers/projectors_global.py index fa7b69958..ca67c66e6 100644 --- a/src/struphy/eigenvalue_solvers/projectors_global.py +++ b/src/struphy/eigenvalue_solvers/projectors_global.py @@ -271,10 +271,18 @@ def __init__(self, spline_space, n_quad=6): BM_splines = [False, True] self.N_int = bsp.collocation_matrix( - spline_space.T, spline_space.p - 0, self.x_int, spline_space.spl_kind, BM_splines[0] + spline_space.T, + spline_space.p - 0, + self.x_int, + spline_space.spl_kind, + BM_splines[0], ) self.D_int = bsp.collocation_matrix( - spline_space.t, spline_space.p - 1, self.x_int, spline_space.spl_kind, BM_splines[1] + spline_space.t, + spline_space.p - 1, + self.x_int, + spline_space.spl_kind, + BM_splines[1], ) self.N_int[self.N_int < 1e-12] = 0.0 @@ -284,10 +292,18 @@ def __init__(self, spline_space, n_quad=6): self.D_int = spa.csr_matrix(self.D_int) self.N_pts = bsp.collocation_matrix( - spline_space.T, spline_space.p - 0, self.pts.flatten(), spline_space.spl_kind, BM_splines[0] + spline_space.T, + spline_space.p - 0, + self.pts.flatten(), + spline_space.spl_kind, + BM_splines[0], ) self.D_pts = bsp.collocation_matrix( - spline_space.t, spline_space.p - 1, self.pts.flatten(), spline_space.spl_kind, BM_splines[1] + spline_space.t, + spline_space.p - 1, + self.pts.flatten(), + spline_space.spl_kind, + BM_splines[1], ) self.N_pts = spa.csr_matrix(self.N_pts) @@ -542,29 +558,29 @@ def D_jD_k(eta): dofs_1_DD_i_red[i] = xp.nonzero(un == nv[i])[0] dofs_0_NN_indices = xp.vstack( - (dofs_0_NN_indices[0], dofs_0_NN_indices[1], dofs_0_NN_indices[2], dofs_0_NN_i_red) + (dofs_0_NN_indices[0], dofs_0_NN_indices[1], dofs_0_NN_indices[2], dofs_0_NN_i_red), ) dofs_0_DN_indices = xp.vstack( - (dofs_0_DN_indices[0], dofs_0_DN_indices[1], dofs_0_DN_indices[2], dofs_0_DN_i_red) + (dofs_0_DN_indices[0], dofs_0_DN_indices[1], dofs_0_DN_indices[2], dofs_0_DN_i_red), ) dofs_0_ND_indices = xp.vstack( - (dofs_0_ND_indices[0], dofs_0_ND_indices[1], dofs_0_ND_indices[2], dofs_0_ND_i_red) + (dofs_0_ND_indices[0], dofs_0_ND_indices[1], dofs_0_ND_indices[2], dofs_0_ND_i_red), ) dofs_0_DD_indices = xp.vstack( - (dofs_0_DD_indices[0], dofs_0_DD_indices[1], dofs_0_DD_indices[2], dofs_0_DD_i_red) + (dofs_0_DD_indices[0], dofs_0_DD_indices[1], dofs_0_DD_indices[2], dofs_0_DD_i_red), ) dofs_1_NN_indices = xp.vstack( - (dofs_1_NN_indices[0], dofs_1_NN_indices[1], dofs_1_NN_indices[2], dofs_1_NN_i_red) + (dofs_1_NN_indices[0], dofs_1_NN_indices[1], dofs_1_NN_indices[2], dofs_1_NN_i_red), ) dofs_1_DN_indices = xp.vstack( - (dofs_1_DN_indices[0], dofs_1_DN_indices[1], dofs_1_DN_indices[2], dofs_1_DN_i_red) + (dofs_1_DN_indices[0], dofs_1_DN_indices[1], dofs_1_DN_indices[2], dofs_1_DN_i_red), ) dofs_1_ND_indices = xp.vstack( - (dofs_1_ND_indices[0], dofs_1_ND_indices[1], dofs_1_ND_indices[2], dofs_1_ND_i_red) + (dofs_1_ND_indices[0], dofs_1_ND_indices[1], dofs_1_ND_indices[2], dofs_1_ND_i_red), ) dofs_1_DD_indices = xp.vstack( - (dofs_1_DD_indices[0], dofs_1_DD_indices[1], dofs_1_DD_indices[2], dofs_1_DD_i_red) + (dofs_1_DD_indices[0], dofs_1_DD_indices[1], dofs_1_DD_indices[2], dofs_1_DD_i_red), ) return ( @@ -1119,7 +1135,8 @@ def dofs(self, comp, mat_f): if comp == "0": dofs = kron_matvec_3d( - [spa.identity(mat_f.shape[0]), spa.identity(mat_f.shape[1]), spa.identity(mat_f.shape[2])], mat_f + [spa.identity(mat_f.shape[0]), spa.identity(mat_f.shape[1]), spa.identity(mat_f.shape[2])], + mat_f, ) elif comp == "11": @@ -1172,15 +1189,18 @@ def dofs_T(self, comp, mat_dofs): elif comp == "11": rhs = kron_matvec_3d( - [self.Q1.T, spa.identity(mat_dofs.shape[1]), spa.identity(mat_dofs.shape[2])], mat_dofs + [self.Q1.T, spa.identity(mat_dofs.shape[1]), spa.identity(mat_dofs.shape[2])], + mat_dofs, ) elif comp == "12": rhs = kron_matvec_3d( - [spa.identity(mat_dofs.shape[0]), self.Q2.T, spa.identity(mat_dofs.shape[2])], mat_dofs + [spa.identity(mat_dofs.shape[0]), self.Q2.T, spa.identity(mat_dofs.shape[2])], + mat_dofs, ) elif comp == "13": rhs = kron_matvec_3d( - [spa.identity(mat_dofs.shape[0]), spa.identity(mat_dofs.shape[1]), self.Q3.T], mat_dofs + [spa.identity(mat_dofs.shape[0]), spa.identity(mat_dofs.shape[1]), self.Q3.T], + mat_dofs, ) elif comp == "21": @@ -1836,10 +1856,12 @@ def solve_V1(self, dofs_1, include_bc): # with boundary splines if include_bc: dofs_11 = dofs_1[: self.P1_pol.shape[0] * self.I_tor.shape[0]].reshape( - self.P1_pol.shape[0], self.I_tor.shape[0] + self.P1_pol.shape[0], + self.I_tor.shape[0], ) dofs_12 = dofs_1[self.P1_pol.shape[0] * self.I_tor.shape[0] :].reshape( - self.P0_pol.shape[0], self.H_tor.shape[0] + self.P0_pol.shape[0], + self.H_tor.shape[0], ) coeffs1 = self.I_tor_LU.solve(self.I1_pol_LU.solve(dofs_11).T).T @@ -1848,10 +1870,12 @@ def solve_V1(self, dofs_1, include_bc): # without boundary splines else: dofs_11 = dofs_1[: self.P1_pol_0.shape[0] * self.I0_tor.shape[0]].reshape( - self.P1_pol_0.shape[0], self.I0_tor.shape[0] + self.P1_pol_0.shape[0], + self.I0_tor.shape[0], ) dofs_12 = dofs_1[self.P1_pol_0.shape[0] * self.I0_tor.shape[0] :].reshape( - self.P0_pol_0.shape[0], self.H0_tor.shape[0] + self.P0_pol_0.shape[0], + self.H0_tor.shape[0], ) coeffs1 = self.I0_tor_LU.solve(self.I1_pol_0_LU.solve(dofs_11).T).T @@ -1864,10 +1888,12 @@ def solve_V2(self, dofs_2, include_bc): # with boundary splines if include_bc: dofs_21 = dofs_2[: self.P2_pol.shape[0] * self.H_tor.shape[0]].reshape( - self.P2_pol.shape[0], self.H_tor.shape[0] + self.P2_pol.shape[0], + self.H_tor.shape[0], ) dofs_22 = dofs_2[self.P2_pol.shape[0] * self.H_tor.shape[0] :].reshape( - self.P3_pol.shape[0], self.I_tor.shape[0] + self.P3_pol.shape[0], + self.I_tor.shape[0], ) coeffs1 = self.H_tor_LU.solve(self.I2_pol_LU.solve(dofs_21).T).T @@ -1876,10 +1902,12 @@ def solve_V2(self, dofs_2, include_bc): # without boundary splines else: dofs_21 = dofs_2[: self.P2_pol_0.shape[0] * self.H0_tor.shape[0]].reshape( - self.P2_pol_0.shape[0], self.H0_tor.shape[0] + self.P2_pol_0.shape[0], + self.H0_tor.shape[0], ) dofs_22 = dofs_2[self.P2_pol_0.shape[0] * self.H0_tor.shape[0] :].reshape( - self.P3_pol_0.shape[0], self.I0_tor.shape[0] + self.P3_pol_0.shape[0], + self.I0_tor.shape[0], ) coeffs1 = self.H0_tor_LU.solve(self.I2_pol_0_LU.solve(dofs_21).T).T @@ -1938,10 +1966,12 @@ def apply_IinvT_V1(self, rhs, include_bc=False): # without boundary splines else: rhs1 = rhs[: self.P1_pol_0.shape[0] * self.I0_tor.shape[0]].reshape( - self.P1_pol_0.shape[0], self.I0_tor.shape[0] + self.P1_pol_0.shape[0], + self.I0_tor.shape[0], ) rhs2 = rhs[self.P1_pol_0.shape[0] * self.I0_tor.shape[0] :].reshape( - self.P0_pol_0.shape[0], self.H0_tor.shape[0] + self.P0_pol_0.shape[0], + self.H0_tor.shape[0], ) rhs1 = self.I1_pol_0_T_LU.solve(self.I0_tor_T_LU.solve(rhs1.T).T) @@ -1968,10 +1998,12 @@ def apply_IinvT_V2(self, rhs, include_bc=False): # without boundary splines else: rhs1 = rhs[: self.P2_pol_0.shape[0] * self.H0_tor.shape[0]].reshape( - self.P2_pol_0.shape[0], self.H0_tor.shape[0] + self.P2_pol_0.shape[0], + self.H0_tor.shape[0], ) rhs2 = rhs[self.P2_pol_0.shape[0] * self.H0_tor.shape[0] :].reshape( - self.P3_pol_0.shape[0], self.I0_tor.shape[0] + self.P3_pol_0.shape[0], + self.I0_tor.shape[0], ) rhs1 = self.I2_pol_0_T_LU.solve(self.H0_tor_T_LU.solve(rhs1.T).T) @@ -2004,7 +2036,8 @@ def dofs_0(self, fun, include_bc=True, eval_kind="meshgrid"): # get dofs on tensor-product grid dofs = kron_matvec_3d( - [spa.identity(dofs.shape[0]), spa.identity(dofs.shape[1]), spa.identity(dofs.shape[2])], dofs + [spa.identity(dofs.shape[0]), spa.identity(dofs.shape[1]), spa.identity(dofs.shape[2])], + dofs, ) # apply extraction operator for dofs diff --git a/src/struphy/eigenvalue_solvers/spline_space.py b/src/struphy/eigenvalue_solvers/spline_space.py index b36ad73db..c10124e57 100644 --- a/src/struphy/eigenvalue_solvers/spline_space.py +++ b/src/struphy/eigenvalue_solvers/spline_space.py @@ -712,27 +712,33 @@ def __init__(self, spline_spaces, ck=-1, cx=None, cy=None, n_tor=0, basis_tor="r # extraction operators for 3D diagram: without boundary conditions self.E0 = spa.kron(self.E0_pol, self.E0_tor, format="csr") self.E1 = spa.bmat( - [[spa.kron(self.E1_pol, self.E0_tor), None], [None, spa.kron(self.E0_pol, self.E1_tor)]], format="csr" + [[spa.kron(self.E1_pol, self.E0_tor), None], [None, spa.kron(self.E0_pol, self.E1_tor)]], + format="csr", ) self.E2 = spa.bmat( - [[spa.kron(self.E2_pol, self.E1_tor), None], [None, spa.kron(self.E3_pol, self.E0_tor)]], format="csr" + [[spa.kron(self.E2_pol, self.E1_tor), None], [None, spa.kron(self.E3_pol, self.E0_tor)]], + format="csr", ) self.E3 = spa.kron(self.E3_pol, self.E1_tor, format="csr") self.Ev = spa.bmat( - [[spa.kron(self.Ev_pol, self.E0_tor), None], [None, spa.kron(self.E0_pol, self.E0_tor)]], format="csr" + [[spa.kron(self.Ev_pol, self.E0_tor), None], [None, spa.kron(self.E0_pol, self.E0_tor)]], + format="csr", ) # boundary operators for 3D diagram self.B0 = spa.kron(self.B0_pol, self.B0_tor, format="csr") self.B1 = spa.bmat( - [[spa.kron(self.B1_pol, self.B0_tor), None], [None, spa.kron(self.B0_pol, self.B1_tor)]], format="csr" + [[spa.kron(self.B1_pol, self.B0_tor), None], [None, spa.kron(self.B0_pol, self.B1_tor)]], + format="csr", ) self.B2 = spa.bmat( - [[spa.kron(self.B2_pol, self.B1_tor), None], [None, spa.kron(self.B3_pol, self.B0_tor)]], format="csr" + [[spa.kron(self.B2_pol, self.B1_tor), None], [None, spa.kron(self.B3_pol, self.B0_tor)]], + format="csr", ) self.B3 = spa.kron(self.B3_pol, self.B1_tor, format="csr") self.Bv = spa.bmat( - [[spa.kron(self.Bv_pol, self.E0_tor), None], [None, spa.kron(Bv3, self.B0_tor)]], format="csr" + [[spa.kron(self.Bv_pol, self.E0_tor), None], [None, spa.kron(Bv3, self.B0_tor)]], + format="csr", ) # extraction operators for 3D diagram: with boundary conditions @@ -841,10 +847,10 @@ def apply_M1_0_ten(self, x, mats): x1, x2 = self.reshape_pol_1(x) out1 = self.B0_tor.dot( - mats[0][1].dot(self.B0_tor.T.dot(self.B1_pol.dot(mats[0][0].dot(self.B1_pol.T.dot(x1))).T)) + mats[0][1].dot(self.B0_tor.T.dot(self.B1_pol.dot(mats[0][0].dot(self.B1_pol.T.dot(x1))).T)), ).T out2 = self.B1_tor.dot( - mats[1][1].dot(self.B1_tor.T.dot(self.B0_pol.dot(mats[1][0].dot(self.B0_pol.T.dot(x2))).T)) + mats[1][1].dot(self.B1_tor.T.dot(self.B0_pol.dot(mats[1][0].dot(self.B0_pol.T.dot(x2))).T)), ).T return xp.concatenate((out1.flatten(), out2.flatten())) @@ -857,10 +863,10 @@ def apply_M2_0_ten(self, x, mats): x1, x2 = self.reshape_pol_2(x) out1 = self.B1_tor.dot( - mats[0][1].dot(self.B1_tor.T.dot(self.B2_pol.dot(mats[0][0].dot(self.B2_pol.T.dot(x1))).T)) + mats[0][1].dot(self.B1_tor.T.dot(self.B2_pol.dot(mats[0][0].dot(self.B2_pol.T.dot(x1))).T)), ).T out2 = self.B0_tor.dot( - mats[1][1].dot(self.B0_tor.T.dot(self.B3_pol.dot(mats[1][0].dot(self.B3_pol.T.dot(x2))).T)) + mats[1][1].dot(self.B0_tor.T.dot(self.B3_pol.dot(mats[1][0].dot(self.B3_pol.T.dot(x2))).T)), ).T return xp.concatenate((out1.flatten(), out2.flatten())) @@ -928,10 +934,12 @@ def __assemble_M1(self, domain, as_tensor=False): self.M1_pol_mat = mass_2d.get_M1(self, domain) matvec = lambda x: self.apply_M1_ten( - x, [[self.M1_pol_mat[0], self.M0_tor], [self.M1_pol_mat[1], self.M1_tor]] + x, + [[self.M1_pol_mat[0], self.M0_tor], [self.M1_pol_mat[1], self.M1_tor]], ) matvec_0 = lambda x: self.apply_M1_0_ten( - x, [[self.M1_pol_mat[0], self.M0_tor], [self.M1_pol_mat[1], self.M1_tor]] + x, + [[self.M1_pol_mat[0], self.M0_tor], [self.M1_pol_mat[1], self.M1_tor]], ) # 3D @@ -939,7 +947,8 @@ def __assemble_M1(self, domain, as_tensor=False): if self.dim == 2: M11, M22 = mass_2d.get_M1(self, domain) self.M1_mat = spa.bmat( - [[spa.kron(M11, self.M0_tor), None], [None, spa.kron(M22, self.M1_tor)]], format="csr" + [[spa.kron(M11, self.M0_tor), None], [None, spa.kron(M22, self.M1_tor)]], + format="csr", ) else: self.M1_mat = mass_3d.get_M1(self, domain) @@ -963,10 +972,12 @@ def __assemble_M2(self, domain, as_tensor=False): self.M2_pol_mat = mass_2d.get_M2(self, domain) matvec = lambda x: self.apply_M2_ten( - x, [[self.M2_pol_mat[0], self.M1_tor], [self.M2_pol_mat[1], self.M0_tor]] + x, + [[self.M2_pol_mat[0], self.M1_tor], [self.M2_pol_mat[1], self.M0_tor]], ) matvec_0 = lambda x: self.apply_M2_0_ten( - x, [[self.M2_pol_mat[0], self.M1_tor], [self.M2_pol_mat[1], self.M0_tor]] + x, + [[self.M2_pol_mat[0], self.M1_tor], [self.M2_pol_mat[1], self.M0_tor]], ) # 3D @@ -974,7 +985,8 @@ def __assemble_M2(self, domain, as_tensor=False): if self.dim == 2: M11, M22 = mass_2d.get_M2(self, domain) self.M2_mat = spa.bmat( - [[spa.kron(M11, self.M1_tor), None], [None, spa.kron(M22, self.M0_tor)]], format="csr" + [[spa.kron(M11, self.M1_tor), None], [None, spa.kron(M22, self.M0_tor)]], + format="csr", ) else: self.M2_mat = mass_3d.get_M2(self, domain) @@ -1026,10 +1038,12 @@ def __assemble_Mv(self, domain, as_tensor=False): self.Mv_pol_mat = mass_2d.get_Mv(self, domain) matvec = lambda x: self.apply_Mv_ten( - x, [[self.Mv_pol_mat[0], self.M0_tor], [self.Mv_pol_mat[1], self.M0_tor]] + x, + [[self.Mv_pol_mat[0], self.M0_tor], [self.Mv_pol_mat[1], self.M0_tor]], ) matvec_0 = lambda x: self.apply_Mv_0_ten( - x, [[self.Mv_pol_mat[0], self.M0_tor], [self.Mv_pol_mat[1], self.M0_tor]] + x, + [[self.Mv_pol_mat[0], self.M0_tor], [self.Mv_pol_mat[1], self.M0_tor]], ) # 3D @@ -1037,7 +1051,8 @@ def __assemble_Mv(self, domain, as_tensor=False): if self.dim == 2: M11, M22 = mass_2d.get_Mv(self, domain) self.Mv_mat = spa.bmat( - [[spa.kron(M11, self.M0_tor), None], [None, spa.kron(M22, self.M0_tor)]], format="csr" + [[spa.kron(M11, self.M0_tor), None], [None, spa.kron(M22, self.M0_tor)]], + format="csr", ) else: self.Mv_mat = mass_3d.get_Mv(self, domain) @@ -1093,17 +1108,21 @@ def reshape_pol_1(self, coeff): if c_size == self.E1.shape[0]: coeff1_pol_1 = coeff[: self.E1_pol.shape[0] * self.E0_tor.shape[0]].reshape( - self.E1_pol.shape[0], self.E0_tor.shape[0] + self.E1_pol.shape[0], + self.E0_tor.shape[0], ) coeff1_pol_3 = coeff[self.E1_pol.shape[0] * self.E0_tor.shape[0] :].reshape( - self.E0_pol.shape[0], self.E1_tor.shape[0] + self.E0_pol.shape[0], + self.E1_tor.shape[0], ) else: coeff1_pol_1 = coeff[: self.E1_pol_0.shape[0] * self.E0_tor_0.shape[0]].reshape( - self.E1_pol_0.shape[0], self.E0_tor_0.shape[0] + self.E1_pol_0.shape[0], + self.E0_tor_0.shape[0], ) coeff1_pol_3 = coeff[self.E1_pol_0.shape[0] * self.E0_tor_0.shape[0] :].reshape( - self.E0_pol_0.shape[0], self.E1_tor_0.shape[0] + self.E0_pol_0.shape[0], + self.E1_tor_0.shape[0], ) return coeff1_pol_1, coeff1_pol_3 @@ -1119,17 +1138,21 @@ def reshape_pol_2(self, coeff): if c_size == self.E2.shape[0]: coeff2_pol_1 = coeff[: self.E2_pol.shape[0] * self.E1_tor.shape[0]].reshape( - self.E2_pol.shape[0], self.E1_tor.shape[0] + self.E2_pol.shape[0], + self.E1_tor.shape[0], ) coeff2_pol_3 = coeff[self.E2_pol.shape[0] * self.E1_tor.shape[0] :].reshape( - self.E3_pol.shape[0], self.E0_tor.shape[0] + self.E3_pol.shape[0], + self.E0_tor.shape[0], ) else: coeff2_pol_1 = coeff[: self.E2_pol_0.shape[0] * self.E1_tor_0.shape[0]].reshape( - self.E2_pol_0.shape[0], self.E1_tor_0.shape[0] + self.E2_pol_0.shape[0], + self.E1_tor_0.shape[0], ) coeff2_pol_3 = coeff[self.E2_pol_0.shape[0] * self.E1_tor_0.shape[0] :].reshape( - self.E3_pol_0.shape[0], self.E0_tor_0.shape[0] + self.E3_pol_0.shape[0], + self.E0_tor_0.shape[0], ) return coeff2_pol_1, coeff2_pol_3 @@ -1161,18 +1184,22 @@ def reshape_pol_v(self, coeff): if c_size == self.Ev.shape[0]: coeffv_pol_1 = coeff[: self.Ev_pol.shape[0] * self.E0_tor.shape[0]].reshape( - self.Ev_pol.shape[0], self.E0_tor.shape[0] + self.Ev_pol.shape[0], + self.E0_tor.shape[0], ) coeffv_pol_3 = coeff[self.Ev_pol.shape[0] * self.E0_tor.shape[0] :].reshape( - self.E0_pol.shape[0], self.E0_tor.shape[0] + self.E0_pol.shape[0], + self.E0_tor.shape[0], ) else: coeffv_pol_1 = coeff[: self.Ev_pol_0.shape[0] * self.E0_tor.shape[0]].reshape( - self.Ev_pol_0.shape[0], self.E0_tor.shape[0] + self.Ev_pol_0.shape[0], + self.E0_tor.shape[0], ) coeffv_pol_3 = coeff[self.Ev_pol_0.shape[0] * self.E0_tor.shape[0] :].reshape( - self.E0_pol.shape[0], self.E0_tor_0.shape[0] + self.E0_pol.shape[0], + self.E0_tor_0.shape[0], ) return coeffv_pol_1, coeffv_pol_3 @@ -1527,10 +1554,26 @@ def evaluate_NN(self, eta1, eta2, eta3, coeff, which="V0", part="r"): if self.n_tor != 0 and self.basis_tor == "r": real_2 = eva_2d.evaluate_n_n( - self.T[0], self.T[1], self.p[0], self.p[1], self.indN[0], self.indN[1], coeff_r[:, :, 1], eta1, eta2 + self.T[0], + self.T[1], + self.p[0], + self.p[1], + self.indN[0], + self.indN[1], + coeff_r[:, :, 1], + eta1, + eta2, ) imag_2 = eva_2d.evaluate_n_n( - self.T[0], self.T[1], self.p[0], self.p[1], self.indN[0], self.indN[1], coeff_i[:, :, 1], eta1, eta2 + self.T[0], + self.T[1], + self.p[0], + self.p[1], + self.indN[0], + self.indN[1], + coeff_i[:, :, 1], + eta1, + eta2, ) # multiply with Fourier basis in third direction if |n_tor| > 0 diff --git a/src/struphy/examples/_draw_parallel.py b/src/struphy/examples/_draw_parallel.py index e95598b3c..b31ba19c4 100644 --- a/src/struphy/examples/_draw_parallel.py +++ b/src/struphy/examples/_draw_parallel.py @@ -81,7 +81,7 @@ def main(): print( f"rank {rank} | markers not on correct process: {xp.nonzero(xp.logical_and(~stay, ~holes))} \ - \n corresponding positions:\n {error_mks[:, :3]}" + \n corresponding positions:\n {error_mks[:, :3]}", ) assert error_mks.size == 0 diff --git a/src/struphy/feec/basis_projection_ops.py b/src/struphy/feec/basis_projection_ops.py index 3bce4701f..d76dc7ca9 100644 --- a/src/struphy/feec/basis_projection_ops.py +++ b/src/struphy/feec/basis_projection_ops.py @@ -147,7 +147,7 @@ def K3(self): e3, ) / self.sqrt_g(e1, e2, e3), - ] + ], ] self._K3 = self.create_basis_op( fun, @@ -263,7 +263,7 @@ def Q3(self): e3, ) / self.sqrt_g(e1, e2, e3), - ] + ], ] self._Q3 = self.create_basis_op( fun, @@ -1064,7 +1064,7 @@ def __init__( self._V1ds[1][2].nbasis, ], [self._V1ds[2][0].nbasis, self._V1ds[2][1].nbasis, self._V1ds[2][2].nbasis], - ] + ], ) # output space: 3d StencilVectorSpaces and 1d SplineSpaces of each component @@ -1363,7 +1363,7 @@ def assemble(self, verbose=False): col0, col1, col2, - ] + ], ), self._VNbasis[hh], Aux._data, @@ -1443,7 +1443,7 @@ def assemble(self, verbose=False): col0, col1, col2, - ] + ], ), self._VNbasis, Aux[h]._data, @@ -1544,7 +1544,7 @@ def assemble(self, verbose=False): col0, col1, col2, - ] + ], ), self._VNbasis[hh], Aux[h]._data, @@ -2043,7 +2043,7 @@ def assemble(self, weights=None, verbose=False): getattr( basis_projection_kernels, "assemble_dofs_for_weighted_basisfuns_" + str(V.ldim) + "d", - ) + ), ) if rank == 0 and verbose: diff --git a/src/struphy/feec/linear_operators.py b/src/struphy/feec/linear_operators.py index 3a29ea821..7469d68e9 100644 --- a/src/struphy/feec/linear_operators.py +++ b/src/struphy/feec/linear_operators.py @@ -136,7 +136,7 @@ def toarray_struphy(self, out=None, is_sparse=False, format="csr"): itterables = [] for i in range(ndim[h]): itterables.append( - range(allstarts[currentrank][i + npredim], allends[currentrank][i + npredim] + 1) + range(allstarts[currentrank][i + npredim], allends[currentrank][i + npredim] + 1), ) # We iterate over all the entries that belong to rank number currentrank for i in itertools.product(*itterables): @@ -293,7 +293,7 @@ def toarray_struphy(self, out=None, is_sparse=False, format="csr"): return sparse.csr_matrix((all_data, (all_rows, all_cols)), shape=(numrows, numcols)).todia() else: raise Exception( - "The selected sparse matrix format must be one of the following : csr, csc, bsr, lil, dok, coo or dia." + "The selected sparse matrix format must be one of the following : csr, csc, bsr, lil, dok, coo or dia.", ) diff --git a/src/struphy/feec/local_projectors_kernels.py b/src/struphy/feec/local_projectors_kernels.py index 09b4c182f..f1eb285c9 100644 --- a/src/struphy/feec/local_projectors_kernels.py +++ b/src/struphy/feec/local_projectors_kernels.py @@ -168,7 +168,10 @@ def get_dofs_local_1_form_ec_component_weighted( @stack_array("shp") def get_dofs_local_1_form_ec_component( - args_solve: LocalProjectorsArguments, f3: "float[:,:,:]", f_eval_aux: "float[:,:,:]", c: int + args_solve: LocalProjectorsArguments, + f3: "float[:,:,:]", + f_eval_aux: "float[:,:,:]", + c: int, ): """Kernel for evaluating the degrees of freedom for the c-th component of 1-forms. This function is for local commuting projetors. @@ -220,7 +223,10 @@ def get_dofs_local_1_form_ec_component( @stack_array("shp") def get_dofs_local_2_form_ec_component( - args_solve: LocalProjectorsArguments, fc: "float[:,:,:]", f_eval_aux: "float[:,:,:]", c: int + args_solve: LocalProjectorsArguments, + fc: "float[:,:,:]", + f_eval_aux: "float[:,:,:]", + c: int, ): """Kernel for evaluating the degrees of freedom for the c-th component of 2-forms. This function is for local commuting projetors. @@ -1037,7 +1043,15 @@ def get_rows_periodic(starts: int, ends: int, modl: int, modr: int, Nbasis: int, def get_rows( - col: int, starts: int, ends: int, p: int, Nbasis: int, periodic: bool, IoH: bool, BoD: bool, aux: "int[:]" + col: int, + starts: int, + ends: int, + p: int, + Nbasis: int, + periodic: bool, + IoH: bool, + BoD: bool, + aux: "int[:]", ): """Kernel for getting the list of rows that are non-zero for the current BasisProjectionLocal column, within the start and end indices of the current MPI rank. diff --git a/src/struphy/feec/mass.py b/src/struphy/feec/mass.py index 76a26bd4d..16f0109d9 100644 --- a/src/struphy/feec/mass.py +++ b/src/struphy/feec/mass.py @@ -449,7 +449,7 @@ def M2B_div0(self): self.weights[self.selected_weight].a1_1, self.weights[self.selected_weight].a1_2, self.weights[self.selected_weight].a1_3, - ] + ], ) tmp_b2 = self.derham.curl.dot(a_eq) @@ -555,7 +555,7 @@ def M2Bn(self): self.weights[self.selected_weight].a1_1, self.weights[self.selected_weight].a1_2, self.weights[self.selected_weight].a1_3, - ] + ], ) tmp_b2 = self.derham.curl.dot(a_eq) @@ -918,7 +918,11 @@ def DFinvT(e1, e2, e3): for n in range(3): fun[-1] += [ lambda e1, e2, e3, m=m, n=n: self._matrix_operate(e1, e2, e3, *weights_rank2)[ - :, :, :, m, n + :, + :, + :, + m, + n, ], ] # Scalar operations second @@ -945,14 +949,14 @@ def DFinvT(e1, e2, e3): fun = [ [ lambda e1, e2, e3: 1.0 / weights_rank0[0](e1, e2, e3), - ] + ], ] for f2, op in zip(weights_rank0[1:], operations[1:]): fun = [ [ lambda e1, e2, e3, f=fun[0][0], op=op, f2=f2: self._operate(f, f2, op, e1, e2, e3), - ] + ], ] V_id = self.derham.space_to_form[V_id] @@ -1626,7 +1630,7 @@ def M2B_div0(self): self.weights[self.selected_weight].a1_1, self.weights[self.selected_weight].a1_2, self.weights[self.selected_weight].a1_3, - ] + ], ) tmp_b2 = self.derham.curl.dot(a_eq) @@ -1740,7 +1744,7 @@ def M2Bn(self): self.weights[self.selected_weight].a1_1, self.weights[self.selected_weight].a1_2, self.weights[self.selected_weight].a1_3, - ] + ], ) tmp_b2 = self.derham.curl.dot(a_eq) @@ -1861,7 +1865,11 @@ def M1perp(self): for n in range(3): fun[-1] += [ lambda e1, e2, e3, m=m, n=n: (self.DFinv(e1, e2, e3) @ self.D @ self.DFinv(e1, e2, e3))[ - :, :, :, m, n + :, + :, + :, + m, + n, ] * self.sqrt_g( e1, @@ -1898,7 +1906,7 @@ def M0ad(self): e3, ) * self.sqrt_g(e1, e2, e3), - ] + ], ] self._M0ad = self._assemble_weighted_mass( @@ -2359,7 +2367,8 @@ def __init__( pts = [ quad_grid[nquad].points.flatten() for quad_grid, nquad in zip( - self.derham.get_quad_grids(wspace, nquads=self.nquads), self.nquads + self.derham.get_quad_grids(wspace, nquads=self.nquads), + self.nquads, ) ] @@ -2473,7 +2482,7 @@ def __init__( getattr( mass_kernels, "kernel_" + str(self._V.ldim) + "d_mat", - ) + ), ) @property @@ -2779,7 +2788,8 @@ def assemble(self, weights=None, clear=True, verbose=True): codomain_spans = [ quad_grid[nquad].spans for quad_grid, nquad in zip( - self.derham.get_quad_grids(codomain_space, nquads=self.nquads), self.nquads + self.derham.get_quad_grids(codomain_space, nquads=self.nquads), + self.nquads, ) ] @@ -2792,7 +2802,8 @@ def assemble(self, weights=None, clear=True, verbose=True): pts = [ quad_grid[nquad].points.flatten() for quad_grid, nquad in zip( - self.derham.get_quad_grids(codomain_space, nquads=self.nquads), self.nquads + self.derham.get_quad_grids(codomain_space, nquads=self.nquads), + self.nquads, ) ] wts = [ @@ -2807,7 +2818,8 @@ def assemble(self, weights=None, clear=True, verbose=True): codomain_basis = [ quad_grid[nquad].basis for quad_grid, nquad in zip( - self.derham.get_quad_grids(codomain_space, nquads=self.nquads), self.nquads + self.derham.get_quad_grids(codomain_space, nquads=self.nquads), + self.nquads, ) ] @@ -2850,7 +2862,8 @@ def assemble(self, weights=None, clear=True, verbose=True): domain_basis = [ quad_grid[nquad].basis for quad_grid, nquad in zip( - self.derham.get_quad_grids(domain_space, nquads=self.nquads), self.nquads + self.derham.get_quad_grids(domain_space, nquads=self.nquads), + self.nquads, ) ] @@ -3036,7 +3049,8 @@ def eval_quad(W, coeffs, out=None): [ q_grid[nquad].points.size for q_grid, nquad in zip( - self.derham.get_quad_grids(space, nquads=self.nquads), self.nquads + self.derham.get_quad_grids(space, nquads=self.nquads), + self.nquads, ) ], dtype=float, @@ -3150,14 +3164,14 @@ def __init__(self, derham, V, W, weights=None, nquads=None): getattr( mass_kernels, "kernel_" + str(self._V.ldim) + "d_matrixfree", - ) + ), ) self._diag_kernel = Pyccelkernel( getattr( mass_kernels, "kernel_" + str(self._V.ldim) + "d_diag", - ) + ), ) shape = tuple(e - s + 1 for s, e in zip(V.coeff_space.starts, V.coeff_space.ends)) @@ -3245,7 +3259,11 @@ def toarray(self): def transpose(self, conjugate=False): return StencilMatrixFreeMassOperator( - self._derham, self._codomain, self._domain, self._weights, nquads=self._nquads + self._derham, + self._codomain, + self._domain, + self._weights, + nquads=self._nquads, ) @property diff --git a/src/struphy/feec/mass_kernels.py b/src/struphy/feec/mass_kernels.py index 7e62b3248..7b4f09720 100644 --- a/src/struphy/feec/mass_kernels.py +++ b/src/struphy/feec/mass_kernels.py @@ -341,7 +341,9 @@ def kernel_3d_mat( for iel2 in range(ne2): for iel3 in range(ne3): tmp_mat_fun[:, :, :] = mat_fun[ - iel1 * nq1 : (iel1 + 1) * nq1, iel2 * nq2 : (iel2 + 1) * nq2, iel3 * nq3 : (iel3 + 1) * nq3 + iel1 * nq1 : (iel1 + 1) * nq1, + iel2 * nq2 : (iel2 + 1) * nq2, + iel3 * nq3 : (iel3 + 1) * nq3, ] tmp_w1[:] = w1[iel1, :] @@ -600,7 +602,9 @@ def kernel_3d_matrixfree( for iel2 in range(ne2): for iel3 in range(ne3): tmp_mat_fun[:, :, :] = mat_fun[ - iel1 * nq1 : (iel1 + 1) * nq1, iel2 * nq2 : (iel2 + 1) * nq2, iel3 * nq3 : (iel3 + 1) * nq3 + iel1 * nq1 : (iel1 + 1) * nq1, + iel2 * nq2 : (iel2 + 1) * nq2, + iel3 * nq3 : (iel3 + 1) * nq3, ] tmp_w1[:] = w1[iel1, :] @@ -713,7 +717,9 @@ def kernel_3d_diag( for iel2 in range(ne2): for iel3 in range(ne3): tmp_mat_fun[:, :, :] = mat_fun[ - iel1 * nq1 : (iel1 + 1) * nq1, iel2 * nq2 : (iel2 + 1) * nq2, iel3 * nq3 : (iel3 + 1) * nq3 + iel1 * nq1 : (iel1 + 1) * nq1, + iel2 * nq2 : (iel2 + 1) * nq2, + iel3 * nq3 : (iel3 + 1) * nq3, ] tmp_w1[:] = w1[iel1, :] diff --git a/src/struphy/feec/preconditioner.py b/src/struphy/feec/preconditioner.py index f3016b532..87b7e89fb 100644 --- a/src/struphy/feec/preconditioner.py +++ b/src/struphy/feec/preconditioner.py @@ -915,7 +915,7 @@ def solve(self, rhs, out=None, transposed=False): out[:] = solve_circulant(self._column, rhs.T).T except xp.linalg.LinAlgError: eps = 1e-4 - print(f"Stabilizing singular preconditioning FFTSolver with {eps = }:") + print(f"Stabilizing singular preconditioning FFTSolver with {eps =}:") self._column[0] *= 1.0 + eps out[:] = solve_circulant(self._column, rhs.T).T diff --git a/src/struphy/feec/projectors.py b/src/struphy/feec/projectors.py index a291d18bd..115ed0aa1 100644 --- a/src/struphy/feec/projectors.py +++ b/src/struphy/feec/projectors.py @@ -80,7 +80,11 @@ class CommutingProjector: """ def __init__( - self, projector_tensor: GlobalProjector, dofs_extraction_op=None, base_extraction_op=None, boundary_op=None + self, + projector_tensor: GlobalProjector, + dofs_extraction_op=None, + base_extraction_op=None, + boundary_op=None, ): self._projector_tensor = projector_tensor @@ -746,7 +750,7 @@ def __init__( # List that will contain the LocalProjectorsArguments for each value of h = 0,1,2. self._solve_args = [] else: - raise TypeError(f"{fem_space = } is not of type FemSpace.") + raise TypeError(f"{fem_space =} is not of type FemSpace.") if isinstance(fem_space, TensorFemSpace): if space_id == "H1": @@ -1425,7 +1429,7 @@ def get_dofs(self, fun, dofs=None): fh = fun(*self._meshgrid[h])[h] # Case in which fun is a list of three functions, each one with one output. else: - assert len(fun) == 3, f"List input only for vector-valued spaces of size 3, but {len(fun) = }." + assert len(fun) == 3, f"List input only for vector-valued spaces of size 3, but {len(fun) =}." # Evaluation of the function to compute the h component fh = fun[h](*self._meshgrid[h]) @@ -1461,7 +1465,7 @@ def get_dofs(self, fun, dofs=None): fun, ) == 3 - ), f"List input only for vector-valued spaces of size 3, but {len(fun) = }." + ), f"List input only for vector-valued spaces of size 3, but {len(fun) =}." for h in range(3): f_eval.append(fun[h](*self._meshgrid[h])) @@ -1481,7 +1485,7 @@ def get_dofs_weighted(self, fun, dofs=None, first_go=True, pre_computed_dofs=Non pre_computed_dofs = [fun(*self._meshgrid)] elif self._space_key == "1" or self._space_key == "2": - assert len(fun) == 3, f"List input only for vector-valued spaces of size 3, but {len(fun) = }." + assert len(fun) == 3, f"List input only for vector-valued spaces of size 3, but {len(fun) =}." self._do_nothing = xp.zeros(3, dtype=int) f_eval = [] @@ -1561,7 +1565,7 @@ def get_dofs_weighted(self, fun, dofs=None, first_go=True, pre_computed_dofs=Non ) elif self._space_key == "v": - assert len(fun) == 3, f"List input only for vector-valued spaces of size 3, but {len(fun) = }." + assert len(fun) == 3, f"List input only for vector-valued spaces of size 3, but {len(fun) =}." self._do_nothing = xp.zeros(3, dtype=int) for h in range(3): @@ -1668,7 +1672,8 @@ def __call__( return self.solve_weighted(rhs, out=out), rhs_weights else: return self.solve_weighted( - self.get_dofs_weighted(fun, dofs=dofs, first_go=False, pre_computed_dofs=pre_computed_dofs), out=out + self.get_dofs_weighted(fun, dofs=dofs, first_go=False, pre_computed_dofs=pre_computed_dofs), + out=out, ) def get_translation_b(self, i, h): @@ -2027,7 +2032,7 @@ def get_dofs(self, fun, dofs=None, apply_bc=False, clear=True): fun_weights = fun(*self._quad_grid_mesh) elif isinstance(fun, xp.ndarray): assert fun.shape == self._quad_grid_mesh[0].shape, ( - f"Expected shape {self._quad_grid_mesh[0].shape}, got {fun.shape = } instead." + f"Expected shape {self._quad_grid_mesh[0].shape}, got {fun.shape =} instead." ) fun_weights = fun else: @@ -2036,7 +2041,7 @@ def get_dofs(self, fun, dofs=None, apply_bc=False, clear=True): fun, ) == 3 - ), f"List input only for vector-valued spaces of size 3, but {len(fun) = }." + ), f"List input only for vector-valued spaces of size 3, but {len(fun) =}." fun_weights = [] # loop over rows (different meshes) for mesh in self._quad_grid_mesh: @@ -2046,11 +2051,11 @@ def get_dofs(self, fun, dofs=None, apply_bc=False, clear=True): if callable(f): fun_weights[-1] += [f(*mesh)] elif isinstance(f, xp.ndarray): - assert f.shape == mesh[0].shape, f"Expected shape {mesh[0].shape}, got {f.shape = } instead." + assert f.shape == mesh[0].shape, f"Expected shape {mesh[0].shape}, got {f.shape =} instead." fun_weights[-1] += [f] else: raise ValueError( - f"Expected callable or numpy array, got {type(f) = } instead.", + f"Expected callable or numpy array, got {type(f) =} instead.", ) # check output vector diff --git a/src/struphy/feec/psydac_derham.py b/src/struphy/feec/psydac_derham.py index f296d95a9..523a5bb97 100644 --- a/src/struphy/feec/psydac_derham.py +++ b/src/struphy/feec/psydac_derham.py @@ -356,7 +356,7 @@ def __init__( self._spline_types_pyccel[sp_form], ) else: - raise TypeError(f"{fem_space = } is not a valid type.") + raise TypeError(f"{fem_space =} is not a valid type.") # break points self._breaks = [space.breaks for space in _derham.spaces[0].spaces] @@ -1093,7 +1093,7 @@ def _discretize_space( elif V == "L2": Wh = Vh.reduce_degree(axes=[0, 1, 2], multiplicity=Vh.multiplicity, basis=basis) else: - raise ValueError(f"V must be one of H1, Hcurl, Hdiv or L2, but is {V = }.") + raise ValueError(f"V must be one of H1, Hcurl, Hdiv or L2, but is {V =}.") Wh.symbolic_space = V for key in Wh._refined_space: @@ -1573,7 +1573,9 @@ def vector(self, value): e1, e2, e3 = self.ends self._vector.tp[s1 : e1 + 1, s2 : e2 + 1, s3 : e3 + 1] = value[1][ - s1 : e1 + 1, s2 : e2 + 1, s3 : e3 + 1 + s1 : e1 + 1, + s2 : e2 + 1, + s3 : e3 + 1, ] else: for n in range(3): @@ -1589,7 +1591,9 @@ def vector(self, value): e1, e2, e3 = self.ends[n] self._vector.tp[n][s1 : e1 + 1, s2 : e2 + 1, s3 : e3 + 1] = value[n][1][ - s1 : e1 + 1, s2 : e2 + 1, s3 : e3 + 1 + s1 : e1 + 1, + s2 : e2 + 1, + s3 : e3 + 1, ] self._vector.update_ghost_regions() @@ -1732,13 +1736,13 @@ def f_tmp(e1, e2, e3): else: assert equil is not None var = fb.variable - assert var in dir(MHDequilibrium), f"{var = } is not an attribute of any fields background." + assert var in dir(MHDequilibrium), f"{var =} is not an attribute of any fields background." if self.space_id in {"H1", "L2"}: fun = getattr(equil, var) else: assert (var + "_1") in dir(MHDequilibrium), ( - f"{(var + '_1') = } is not an attribute of any fields background." + f"{(var + '_1') =} is not an attribute of any fields background." ) fun = [ getattr(equil, var + "_1"), diff --git a/src/struphy/feec/tests/test_basis_ops.py b/src/struphy/feec/tests/test_basis_ops.py index 7b06e09e1..7ba56aefa 100644 --- a/src/struphy/feec/tests/test_basis_ops.py +++ b/src/struphy/feec/tests/test_basis_ops.py @@ -503,7 +503,7 @@ def test_basis_ops_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=Fal "n2": 4.0, "na": 0.0, "beta": 0.1, - } + }, ) if show_plots: @@ -800,8 +800,8 @@ def assert_ops(mpi_rank, res_PSY, res_STR, verbose=False, MPI_COMM=None): res_PSY.starts[0] : res_PSY.ends[0] + 1, res_PSY.starts[1] : res_PSY.ends[1] + 1, res_PSY.starts[2] : res_PSY.ends[2] + 1, - ] - ) + ], + ), ), ) @@ -834,5 +834,10 @@ def assert_ops(mpi_rank, res_PSY, res_STR, verbose=False, MPI_COMM=None): # mapping=["Cuboid", {"l1": 0.0, "r1": 1.0, "l2": 0.0, "r2": 1.0, "l3": 0.0, "r3": 1.0}], # ) test_basis_ops_polar( - [6, 9, 7], [2, 2, 3], [False, True, True], None, ["IGAPolarCylinder", {"a": 1.0, "Lz": 3.0}], False + [6, 9, 7], + [2, 2, 3], + [False, True, True], + None, + ["IGAPolarCylinder", {"a": 1.0, "Lz": 3.0}], + False, ) diff --git a/src/struphy/feec/tests/test_derham.py b/src/struphy/feec/tests/test_derham.py index c5c0e57ea..1e857b5a2 100644 --- a/src/struphy/feec/tests/test_derham.py +++ b/src/struphy/feec/tests/test_derham.py @@ -70,7 +70,9 @@ def test_psydac_derham(Nel, p, spl_kind): # Assign from start to end index + 1 x0_PSY[s0[0] : e0[0] + 1, s0[1] : e0[1] + 1, s0[2] : e0[2] + 1] = DR_STR.extract_0(x0)[ - s0[0] : e0[0] + 1, s0[1] : e0[1] + 1, s0[2] : e0[2] + 1 + s0[0] : e0[0] + 1, + s0[1] : e0[1] + 1, + s0[2] : e0[2] + 1, ] # Block of StencilVecttors @@ -87,13 +89,19 @@ def test_psydac_derham(Nel, p, spl_kind): x11, x12, x13 = DR_STR.extract_1(x1) x1_PSY[0][s11[0] : e11[0] + 1, s11[1] : e11[1] + 1, s11[2] : e11[2] + 1] = x11[ - s11[0] : e11[0] + 1, s11[1] : e11[1] + 1, s11[2] : e11[2] + 1 + s11[0] : e11[0] + 1, + s11[1] : e11[1] + 1, + s11[2] : e11[2] + 1, ] x1_PSY[1][s12[0] : e12[0] + 1, s12[1] : e12[1] + 1, s12[2] : e12[2] + 1] = x12[ - s12[0] : e12[0] + 1, s12[1] : e12[1] + 1, s12[2] : e12[2] + 1 + s12[0] : e12[0] + 1, + s12[1] : e12[1] + 1, + s12[2] : e12[2] + 1, ] x1_PSY[2][s13[0] : e13[0] + 1, s13[1] : e13[1] + 1, s13[2] : e13[2] + 1] = x13[ - s13[0] : e13[0] + 1, s13[1] : e13[1] + 1, s13[2] : e13[2] + 1 + s13[0] : e13[0] + 1, + s13[1] : e13[1] + 1, + s13[2] : e13[2] + 1, ] x2_PSY = BlockVector(derham.Vh["2"]) @@ -109,13 +117,19 @@ def test_psydac_derham(Nel, p, spl_kind): x21, x22, x23 = DR_STR.extract_2(x2) x2_PSY[0][s21[0] : e21[0] + 1, s21[1] : e21[1] + 1, s21[2] : e21[2] + 1] = x21[ - s21[0] : e21[0] + 1, s21[1] : e21[1] + 1, s21[2] : e21[2] + 1 + s21[0] : e21[0] + 1, + s21[1] : e21[1] + 1, + s21[2] : e21[2] + 1, ] x2_PSY[1][s22[0] : e22[0] + 1, s22[1] : e22[1] + 1, s22[2] : e22[2] + 1] = x22[ - s22[0] : e22[0] + 1, s22[1] : e22[1] + 1, s22[2] : e22[2] + 1 + s22[0] : e22[0] + 1, + s22[1] : e22[1] + 1, + s22[2] : e22[2] + 1, ] x2_PSY[2][s23[0] : e23[0] + 1, s23[1] : e23[1] + 1, s23[2] : e23[2] + 1] = x23[ - s23[0] : e23[0] + 1, s23[1] : e23[1] + 1, s23[2] : e23[2] + 1 + s23[0] : e23[0] + 1, + s23[1] : e23[1] + 1, + s23[2] : e23[2] + 1, ] x3_PSY = StencilVector(derham.Vh["3"]) @@ -130,7 +144,9 @@ def test_psydac_derham(Nel, p, spl_kind): e3 = x3_PSY.ends x3_PSY[s3[0] : e3[0] + 1, s3[1] : e3[1] + 1, s3[2] : e3[2] + 1] = DR_STR.extract_3(x3)[ - s3[0] : e3[0] + 1, s3[1] : e3[1] + 1, s3[2] : e3[2] + 1 + s3[0] : e3[0] + 1, + s3[1] : e3[1] + 1, + s3[2] : e3[2] + 1, ] ######################## diff --git a/src/struphy/feec/tests/test_eval_field.py b/src/struphy/feec/tests/test_eval_field.py index 6148fa41e..f9a00c18d 100644 --- a/src/struphy/feec/tests/test_eval_field.py +++ b/src/struphy/feec/tests/test_eval_field.py @@ -234,13 +234,13 @@ def test_eval_field(Nel, p, spl_kind): m_vals_ref_3 = E1(0.0, 0.0, eta3, squeeze_out=True) assert xp.all( - [xp.allclose(m_vals_1_i, m_vals_ref_1_i) for m_vals_1_i, m_vals_ref_1_i in zip(m_vals_1, m_vals_ref_1)] + [xp.allclose(m_vals_1_i, m_vals_ref_1_i) for m_vals_1_i, m_vals_ref_1_i in zip(m_vals_1, m_vals_ref_1)], ) assert xp.all( - [xp.allclose(m_vals_2_i, m_vals_ref_2_i) for m_vals_2_i, m_vals_ref_2_i in zip(m_vals_2, m_vals_ref_2)] + [xp.allclose(m_vals_2_i, m_vals_ref_2_i) for m_vals_2_i, m_vals_ref_2_i in zip(m_vals_2, m_vals_ref_2)], ) assert xp.all( - [xp.allclose(m_vals_3_i, m_vals_ref_3_i) for m_vals_3_i, m_vals_ref_3_i in zip(m_vals_3, m_vals_ref_3)] + [xp.allclose(m_vals_3_i, m_vals_ref_3_i) for m_vals_3_i, m_vals_ref_3_i in zip(m_vals_3, m_vals_ref_3)], ) ###### @@ -354,13 +354,13 @@ def test_eval_field(Nel, p, spl_kind): m_vals_ref_3 = B2(0.0, 0.0, eta3, squeeze_out=True) assert xp.all( - [xp.allclose(m_vals_1_i, m_vals_ref_1_i) for m_vals_1_i, m_vals_ref_1_i in zip(m_vals_1, m_vals_ref_1)] + [xp.allclose(m_vals_1_i, m_vals_ref_1_i) for m_vals_1_i, m_vals_ref_1_i in zip(m_vals_1, m_vals_ref_1)], ) assert xp.all( - [xp.allclose(m_vals_2_i, m_vals_ref_2_i) for m_vals_2_i, m_vals_ref_2_i in zip(m_vals_2, m_vals_ref_2)] + [xp.allclose(m_vals_2_i, m_vals_ref_2_i) for m_vals_2_i, m_vals_ref_2_i in zip(m_vals_2, m_vals_ref_2)], ) assert xp.all( - [xp.allclose(m_vals_3_i, m_vals_ref_3_i) for m_vals_3_i, m_vals_ref_3_i in zip(m_vals_3, m_vals_ref_3)] + [xp.allclose(m_vals_3_i, m_vals_ref_3_i) for m_vals_3_i, m_vals_ref_3_i in zip(m_vals_3, m_vals_ref_3)], ) ###### @@ -526,13 +526,13 @@ def test_eval_field(Nel, p, spl_kind): m_vals_ref_3 = uv(0.0, 0.0, eta3, squeeze_out=True) assert xp.all( - [xp.allclose(m_vals_1_i, m_vals_ref_1_i) for m_vals_1_i, m_vals_ref_1_i in zip(m_vals_1, m_vals_ref_1)] + [xp.allclose(m_vals_1_i, m_vals_ref_1_i) for m_vals_1_i, m_vals_ref_1_i in zip(m_vals_1, m_vals_ref_1)], ) assert xp.all( - [xp.allclose(m_vals_2_i, m_vals_ref_2_i) for m_vals_2_i, m_vals_ref_2_i in zip(m_vals_2, m_vals_ref_2)] + [xp.allclose(m_vals_2_i, m_vals_ref_2_i) for m_vals_2_i, m_vals_ref_2_i in zip(m_vals_2, m_vals_ref_2)], ) assert xp.all( - [xp.allclose(m_vals_3_i, m_vals_ref_3_i) for m_vals_3_i, m_vals_ref_3_i in zip(m_vals_3, m_vals_ref_3)] + [xp.allclose(m_vals_3_i, m_vals_ref_3_i) for m_vals_3_i, m_vals_ref_3_i in zip(m_vals_3, m_vals_ref_3)], ) print("\nAll assertions passed.") diff --git a/src/struphy/feec/tests/test_field_init.py b/src/struphy/feec/tests/test_field_init.py index 292edf76a..2f0da1611 100644 --- a/src/struphy/feec/tests/test_field_init.py +++ b/src/struphy/feec/tests/test_field_init.py @@ -40,7 +40,7 @@ def test_bckgr_init_const(Nel, p, spl_kind, spaces, vec_comps): background = FieldsBackground(type="LogicalConst", values=(val,)) field.initialize_coeffs(backgrounds=background) print( - f"\n{rank = }, {space = }, after init:\n {xp.max(xp.abs(field(*meshgrids) - val)) = }", + f"\n{rank =}, {space =}, after init:\n {xp.max(xp.abs(field(*meshgrids) - val)) =}", ) # print(f'{field(*meshgrids) = }') assert xp.allclose(field(*meshgrids), val) @@ -50,7 +50,7 @@ def test_bckgr_init_const(Nel, p, spl_kind, spaces, vec_comps): for j, val in enumerate(background.values): if val is not None: print( - f"\n{rank = }, {space = }, after init:\n {j = }, {xp.max(xp.abs(field(*meshgrids)[j] - val)) = }", + f"\n{rank =}, {space =}, after init:\n {j =}, {xp.max(xp.abs(field(*meshgrids)[j] - val)) =}", ) # print(f'{field(*meshgrids)[i] = }') assert xp.allclose(field(*meshgrids)[j], val) @@ -96,20 +96,20 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show # test for key, val in inspect.getmembers(equils): if inspect.isclass(val) and val.__module__ == equils.__name__: - print(f"{key = }") + print(f"{key =}") if "DESC" in key and not with_desc: - print(f"Attention: {with_desc = }, DESC not tested here !!") + print(f"Attention: {with_desc =}, DESC not tested here !!") continue if "GVEC" in key and not with_gvec: - print(f"Attention: {with_gvec = }, GVEC not tested here !!") + print(f"Attention: {with_gvec =}, GVEC not tested here !!") continue mhd_equil = val() if not isinstance(mhd_equil, FluidEquilibriumWithB): continue - print(f"{mhd_equil.params = }") + print(f"{mhd_equil.params =}") if "AdhocTorus" in key: mhd_equil.domain = domains.HollowTorus( @@ -186,7 +186,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show # scalar spaces print( - f"{xp.max(xp.abs(field_3(*meshgrids) - mhd_equil.p3(*meshgrids))) / xp.max(xp.abs(mhd_equil.p3(*meshgrids)))}" + f"{xp.max(xp.abs(field_3(*meshgrids) - mhd_equil.p3(*meshgrids))) / xp.max(xp.abs(mhd_equil.p3(*meshgrids)))}", ) assert ( xp.max( @@ -198,7 +198,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show if isinstance(mhd_equil, FluidEquilibriumWithB): print( - f"{xp.max(xp.abs(field_0(*meshgrids) - mhd_equil.absB0(*meshgrids))) / xp.max(xp.abs(mhd_equil.absB0(*meshgrids)))}" + f"{xp.max(xp.abs(field_0(*meshgrids) - mhd_equil.absB0(*meshgrids))) / xp.max(xp.abs(mhd_equil.absB0(*meshgrids)))}", ) assert ( xp.max( @@ -216,7 +216,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show else: denom = xp.max(xp.abs(ref[0])) print( - f"{xp.max(xp.abs(field_1(*meshgrids)[0] - ref[0])) / denom = }", + f"{xp.max(xp.abs(field_1(*meshgrids)[0] - ref[0])) / denom =}", ) assert xp.max(xp.abs(field_1(*meshgrids)[0] - ref[0])) / denom < 0.28 if xp.max(xp.abs(ref[1])) < 1e-11: @@ -224,7 +224,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show else: denom = xp.max(xp.abs(ref[1])) print( - f"{xp.max(xp.abs(field_1(*meshgrids)[1] - ref[1])) / denom = }", + f"{xp.max(xp.abs(field_1(*meshgrids)[1] - ref[1])) / denom =}", ) assert xp.max(xp.abs(field_1(*meshgrids)[1] - ref[1])) / denom < 0.33 if xp.max(xp.abs(ref[2])) < 1e-11: @@ -232,7 +232,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show else: denom = xp.max(xp.abs(ref[2])) print( - f"{xp.max(xp.abs(field_1(*meshgrids)[2] - ref[2])) / denom = }", + f"{xp.max(xp.abs(field_1(*meshgrids)[2] - ref[2])) / denom =}", ) assert ( xp.max( @@ -251,7 +251,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show else: denom = xp.max(xp.abs(ref[0])) print( - f"{xp.max(xp.abs(field_2(*meshgrids)[0] - ref[0])) / denom = }", + f"{xp.max(xp.abs(field_2(*meshgrids)[0] - ref[0])) / denom =}", ) assert xp.max(xp.abs(field_2(*meshgrids)[0] - ref[0])) / denom < 0.86 if xp.max(xp.abs(ref[1])) < 1e-11: @@ -259,7 +259,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show else: denom = xp.max(xp.abs(ref[1])) print( - f"{xp.max(xp.abs(field_2(*meshgrids)[1] - ref[1])) / denom = }", + f"{xp.max(xp.abs(field_2(*meshgrids)[1] - ref[1])) / denom =}", ) assert ( xp.max( @@ -275,7 +275,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show else: denom = xp.max(xp.abs(ref[2])) print( - f"{xp.max(xp.abs(field_2(*meshgrids)[2] - ref[2])) / denom = }", + f"{xp.max(xp.abs(field_2(*meshgrids)[2] - ref[2])) / denom =}", ) assert xp.max(xp.abs(field_2(*meshgrids)[2] - ref[2])) / denom < 0.21 print("u2 asserts passed.") @@ -286,7 +286,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show else: denom = xp.max(xp.abs(ref[0])) print( - f"{xp.max(xp.abs(field_4(*meshgrids)[0] - ref[0])) / denom = }", + f"{xp.max(xp.abs(field_4(*meshgrids)[0] - ref[0])) / denom =}", ) assert xp.max(xp.abs(field_4(*meshgrids)[0] - ref[0])) / denom < 0.6 if xp.max(xp.abs(ref[1])) < 1e-11: @@ -294,7 +294,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show else: denom = xp.max(xp.abs(ref[1])) print( - f"{xp.max(xp.abs(field_4(*meshgrids)[1] - ref[1])) / denom = }", + f"{xp.max(xp.abs(field_4(*meshgrids)[1] - ref[1])) / denom =}", ) assert ( xp.max( @@ -310,7 +310,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show else: denom = xp.max(xp.abs(ref[2])) print( - f"{xp.max(xp.abs(field_4(*meshgrids)[2] - ref[2])) / denom = }", + f"{xp.max(xp.abs(field_4(*meshgrids)[2] - ref[2])) / denom =}", ) assert ( xp.max( @@ -325,27 +325,27 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show # plotting fields with equilibrium if show_plot and rank == 0: - plt.figure(f"0/3-forms top, {mhd_equil = }", figsize=(24, 16)) + plt.figure(f"0/3-forms top, {mhd_equil =}", figsize=(24, 16)) plt.figure( - f"0/3-forms poloidal, {mhd_equil = }", + f"0/3-forms poloidal, {mhd_equil =}", figsize=(24, 16), ) - plt.figure(f"1-forms top, {mhd_equil = }", figsize=(24, 16)) + plt.figure(f"1-forms top, {mhd_equil =}", figsize=(24, 16)) plt.figure( - f"1-forms poloidal, {mhd_equil = }", + f"1-forms poloidal, {mhd_equil =}", figsize=(24, 16), ) - plt.figure(f"2-forms top, {mhd_equil = }", figsize=(24, 16)) + plt.figure(f"2-forms top, {mhd_equil =}", figsize=(24, 16)) plt.figure( - f"2-forms poloidal, {mhd_equil = }", + f"2-forms poloidal, {mhd_equil =}", figsize=(24, 16), ) plt.figure( - f"vector-fields top, {mhd_equil = }", + f"vector-fields top, {mhd_equil =}", figsize=(24, 16), ) plt.figure( - f"vector-fields poloidal, {mhd_equil = }", + f"vector-fields poloidal, {mhd_equil =}", figsize=(24, 16), ) x, y, z = mhd_equil.domain(*meshgrids) @@ -357,7 +357,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show levels = xp.linspace(xp.min(absB0) - 1e-10, xp.max(absB0), 20) - plt.figure(f"0/3-forms top, {mhd_equil = }") + plt.figure(f"0/3-forms top, {mhd_equil =}") plt.subplot(2, 3, 1) if "Slab" in key or "Pinch" in key: plt.contourf( @@ -443,7 +443,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show plt.colorbar() plt.title("reference, top view (e1-e3)") - plt.figure(f"0/3-forms poloidal, {mhd_equil = }") + plt.figure(f"0/3-forms poloidal, {mhd_equil =}") plt.subplot(2, 3, 1) if "Slab" in key or "Pinch" in key: plt.contourf( @@ -495,7 +495,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show levels = xp.linspace(xp.min(p3) - 1e-10, xp.max(p3), 20) - plt.figure(f"0/3-forms top, {mhd_equil = }") + plt.figure(f"0/3-forms top, {mhd_equil =}") plt.subplot(2, 3, 2) if "Slab" in key or "Pinch" in key: plt.contourf( @@ -581,7 +581,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show plt.colorbar() plt.title("reference, top view (e1-e3)") - plt.figure(f"0/3-forms poloidal, {mhd_equil = }") + plt.figure(f"0/3-forms poloidal, {mhd_equil =}") plt.subplot(2, 3, 2) if "Slab" in key or "Pinch" in key: plt.contourf( @@ -642,7 +642,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show for i, (bh, b) in enumerate(zip(b1h, b1)): levels = xp.linspace(xp.min(b) - 1e-10, xp.max(b), 20) - plt.figure(f"1-forms top, {mhd_equil = }") + plt.figure(f"1-forms top, {mhd_equil =}") plt.subplot(2, 3, 1 + i) if "Slab" in key or "Pinch" in key: plt.contourf( @@ -728,7 +728,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show plt.colorbar() plt.title("reference, top view (e1-e3)") - plt.figure(f"1-forms poloidal, {mhd_equil = }") + plt.figure(f"1-forms poloidal, {mhd_equil =}") plt.subplot(2, 3, 1 + i) if "Slab" in key or "Pinch" in key: plt.contourf( @@ -791,7 +791,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show for i, (bh, b) in enumerate(zip(b2h, b2)): levels = xp.linspace(xp.min(b) - 1e-10, xp.max(b), 20) - plt.figure(f"2-forms top, {mhd_equil = }") + plt.figure(f"2-forms top, {mhd_equil =}") plt.subplot(2, 3, 1 + i) if "Slab" in key or "Pinch" in key: plt.contourf( @@ -877,7 +877,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show plt.colorbar() plt.title("reference, top view (e1-e3)") - plt.figure(f"2-forms poloidal, {mhd_equil = }") + plt.figure(f"2-forms poloidal, {mhd_equil =}") plt.subplot(2, 3, 1 + i) if "Slab" in key or "Pinch" in key: plt.contourf( @@ -940,7 +940,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show for i, (bh, b) in enumerate(zip(bvh, bv)): levels = xp.linspace(xp.min(b) - 1e-10, xp.max(b), 20) - plt.figure(f"vector-fields top, {mhd_equil = }") + plt.figure(f"vector-fields top, {mhd_equil =}") plt.subplot(2, 3, 1 + i) if "Slab" in key or "Pinch" in key: plt.contourf( @@ -1026,7 +1026,7 @@ def test_bckgr_init_mhd(Nel, p, spl_kind, with_desc=False, with_gvec=False, show plt.colorbar() plt.title("reference, top view (e1-e3)") - plt.figure(f"vector-fields poloidal, {mhd_equil = }") + plt.figure(f"vector-fields poloidal, {mhd_equil =}") plt.subplot(2, 3, 1 + i) if "Slab" in key or "Pinch" in key: plt.contourf( @@ -1162,7 +1162,10 @@ def test_sincos_init_const(Nel, p, spl_kind, show_plot=False): field_0 = derham.create_spline_function("name_0", "H1", backgrounds=bckgr_0, perturbations=[f_sin_0, f_cos_0]) field_1 = derham.create_spline_function( - "name_1", "Hcurl", backgrounds=bckgr_1, perturbations=[f_sin_11, f_sin_13, f_cos_11, f_cos_12] + "name_1", + "Hcurl", + backgrounds=bckgr_1, + perturbations=[f_sin_11, f_sin_13, f_cos_11, f_cos_12], ) field_2 = derham.create_spline_function("name_2", "Hdiv", backgrounds=bckgr_2, perturbations=[f_cos_22]) @@ -1189,13 +1192,13 @@ def test_sincos_init_const(Nel, p, spl_kind, show_plot=False): f1_h = field_1(*meshgrids) f2_h = field_2(*meshgrids) - print(f"{xp.max(xp.abs(fun_0 - f0_h)) = }") - print(f"{xp.max(xp.abs(fun_1[0] - f1_h[0])) = }") - print(f"{xp.max(xp.abs(fun_1[1] - f1_h[1])) = }") - print(f"{xp.max(xp.abs(fun_1[2] - f1_h[2])) = }") - print(f"{xp.max(xp.abs(fun_2[0] - f2_h[0])) = }") - print(f"{xp.max(xp.abs(fun_2[1] - f2_h[1])) = }") - print(f"{xp.max(xp.abs(fun_2[2] - f2_h[2])) = }") + print(f"{xp.max(xp.abs(fun_0 - f0_h)) =}") + print(f"{xp.max(xp.abs(fun_1[0] - f1_h[0])) =}") + print(f"{xp.max(xp.abs(fun_1[1] - f1_h[1])) =}") + print(f"{xp.max(xp.abs(fun_1[2] - f1_h[2])) =}") + print(f"{xp.max(xp.abs(fun_2[0] - f2_h[0])) =}") + print(f"{xp.max(xp.abs(fun_2[1] - f2_h[1])) =}") + print(f"{xp.max(xp.abs(fun_2[2] - f2_h[2])) =}") assert xp.max(xp.abs(fun_0 - f0_h)) < 3e-5 assert xp.max(xp.abs(fun_1[0] - f1_h[0])) < 3e-5 diff --git a/src/struphy/feec/tests/test_l2_projectors.py b/src/struphy/feec/tests/test_l2_projectors.py index 6d3695caa..7da42eff4 100644 --- a/src/struphy/feec/tests/test_l2_projectors.py +++ b/src/struphy/feec/tests/test_l2_projectors.py @@ -47,11 +47,11 @@ def test_l2_projectors_mappings(Nel, p, spl_kind, array_input, with_desc, do_plo for dom_type, dom_class in zip(dom_types, dom_classes): print("#" * 80) - print(f"Testing {dom_class = }") + print(f"Testing {dom_class =}") print("#" * 80) if "DESC" in dom_type and not with_desc: - print(f"Attention: {with_desc = }, DESC not tested here !!") + print(f"Attention: {with_desc =}, DESC not tested here !!") continue domain = dom_class() @@ -103,7 +103,7 @@ def test_l2_projectors_mappings(Nel, p, spl_kind, array_input, with_desc, do_plo err = [xp.max(xp.abs(exact(ee1, ee2, ee3) - field_v)) for exact, field_v in zip(f_analytic, field_vals)] f_plot = field_vals[0] - print(f"{sp_id = }, {xp.max(err) = }") + print(f"{sp_id =}, {xp.max(err) =}") if sp_id in ("H1", "H1vec"): assert xp.max(err) < 0.004 else: @@ -233,7 +233,7 @@ def f(x, y, z): line_for_rate_p0 = [Ne ** (-rate_p0) * errors[sp_id][0] / Nels[0] ** (-rate_p0) for Ne in Nels] m, _ = xp.polyfit(xp.log(Nels), xp.log(errors[sp_id]), deg=1) - print(f"{sp_id = }, fitted convergence rate = {-m}, degree = {pi}") + print(f"{sp_id =}, fitted convergence rate = {-m}, degree = {pi}") if sp_id in ("H1", "H1vec"): assert -m > (pi + 1 - 0.05) else: @@ -247,7 +247,7 @@ def f(x, y, z): plt.loglog(Nels, line_for_rate_p0, "k--") plt.text(Nels[-2], line_for_rate_p1[-2], f"1/Nel^{rate_p1}") plt.text(Nels[-2], line_for_rate_p0[-2], f"1/Nel^{rate_p0}") - plt.title(f"{sp_id = }, degree = {pi}") + plt.title(f"{sp_id =}, degree = {pi}") plt.xlabel("Nel") if do_plot and rank == 0: diff --git a/src/struphy/feec/tests/test_local_projectors.py b/src/struphy/feec/tests/test_local_projectors.py index ce2abda01..4cf2d401c 100644 --- a/src/struphy/feec/tests/test_local_projectors.py +++ b/src/struphy/feec/tests/test_local_projectors.py @@ -142,7 +142,7 @@ def f(e1, e2, e3): errg[1] = xp.max(xp.abs(fieldg_vals[1] - field_vals[1])) errg[2] = xp.max(xp.abs(fieldg_vals[2] - field_vals[2])) - print(f"{sp_id = }, {xp.max(err) = }, {xp.max(errg) = },{exectime = }") + print(f"{sp_id =}, {xp.max(err) =}, {xp.max(errg) =},{exectime =}") if sp_id in ("H1", "H1vec"): assert xp.max(err) < 0.011 assert xp.max(errg) < 0.011 @@ -264,14 +264,14 @@ def f(x, y, z): # for those cases is better to compute the convergance rate using only the information of Nel with smaller number if -m <= (pi + 1 - 0.1): m = -xp.log2(errors[sp_id][1] / errors[sp_id][2]) - print(f"{sp_id = }, fitted convergence rate = {-m}, degree = {pi}") + print(f"{sp_id =}, fitted convergence rate = {-m}, degree = {pi}") assert -m > (pi + 1 - 0.1) else: # Sometimes for very large number of elements the convergance rate falls of a bit since the error is already so small floating point impressions become relevant # for those cases is better to compute the convergance rate using only the information of Nel with smaller number if -m <= (pi - 0.1): m = -xp.log2(errors[sp_id][1] / errors[sp_id][2]) - print(f"{sp_id = }, fitted convergence rate = {-m}, degree = {pi}") + print(f"{sp_id =}, fitted convergence rate = {-m}, degree = {pi}") assert -m > (pi - 0.1) if do_plot: @@ -282,7 +282,7 @@ def f(x, y, z): plt.loglog(Nels, line_for_rate_p0, "k--") plt.text(Nels[-2], line_for_rate_p1[-2], f"1/Nel^{rate_p1}") plt.text(Nels[-2], line_for_rate_p0[-2], f"1/Nel^{rate_p0}") - plt.title(f"{sp_id = }, degree = {pi}") + plt.title(f"{sp_id =}, degree = {pi}") plt.xlabel("Nel") if do_plot and rank == 0: @@ -500,7 +500,7 @@ def fun(eta1, eta2, eta3): VFEM1ds[1][2].nbasis, ], [VFEM1ds[2][0].nbasis, VFEM1ds[2][1].nbasis, VFEM1ds[2][2].nbasis], - ] + ], ) if in_sp_key == "0" or in_sp_key == "3": @@ -578,7 +578,7 @@ def basis3(i3, h=None): npts_in[0][0] * npts_in[0][1] * npts_in[0][2] + npts_in[1][0] * npts_in[1][1] * npts_in[1][2] + npts_in[2][0] * npts_in[2][1] * npts_in[2][2], - ) + ), ) else: @@ -591,57 +591,57 @@ def basis3(i3, h=None): ( npts_out[0][0] * npts_out[0][1] * npts_out[0][2], npts_in[0][0] * npts_in[0][1] * npts_in[0][2], - ) + ), ) matrix10 = xp.zeros( ( npts_out[1][0] * npts_out[1][1] * npts_out[1][2], npts_in[0][0] * npts_in[0][1] * npts_in[0][2], - ) + ), ) matrix20 = xp.zeros( ( npts_out[2][0] * npts_out[2][1] * npts_out[2][2], npts_in[0][0] * npts_in[0][1] * npts_in[0][2], - ) + ), ) matrix01 = xp.zeros( ( npts_out[0][0] * npts_out[0][1] * npts_out[0][2], npts_in[1][0] * npts_in[1][1] * npts_in[1][2], - ) + ), ) matrix11 = xp.zeros( ( npts_out[1][0] * npts_out[1][1] * npts_out[1][2], npts_in[1][0] * npts_in[1][1] * npts_in[1][2], - ) + ), ) matrix21 = xp.zeros( ( npts_out[2][0] * npts_out[2][1] * npts_out[2][2], npts_in[1][0] * npts_in[1][1] * npts_in[1][2], - ) + ), ) matrix02 = xp.zeros( ( npts_out[0][0] * npts_out[0][1] * npts_out[0][2], npts_in[2][0] * npts_in[2][1] * npts_in[2][2], - ) + ), ) matrix12 = xp.zeros( ( npts_out[1][0] * npts_out[1][1] * npts_out[1][2], npts_in[2][0] * npts_in[2][1] * npts_in[2][2], - ) + ), ) matrix22 = xp.zeros( ( npts_out[2][0] * npts_out[2][1] * npts_out[2][2], npts_in[2][0] * npts_in[2][1] * npts_in[2][2], - ) + ), ) # We build the BasisProjectionOperator by hand @@ -1233,7 +1233,7 @@ def f_analytic3(e1, e2, e3): * basis1(random_i0)(*meshgrid) * basis2(random_i1)(*meshgrid) * basis3(random_i2)(*meshgrid), - ] + ], ) else: @@ -1307,7 +1307,7 @@ def f_analytic22(e1, e2, e3): * basis2(random_i1, random_h)(*meshgrid) * basis3(random_i2, random_h)(*meshgrid) for dim in range(3) - ] + ], ) FE_loc = matrix_new.dot(input) @@ -1367,10 +1367,10 @@ def f_analytic22(e1, e2, e3): if rank == 0: assert reducemeanlocal < 10.0 * reducemeanglobal or reducemeanlocal < 10.0**-5 - print(f"{reducemeanlocal = }") - print(f"{reducemaxlocal = }") - print(f"{reducemeanglobal = }") - print(f"{reducemaxglobal = }") + print(f"{reducemeanlocal =}") + print(f"{reducemaxlocal =}") + print(f"{reducemeanglobal =}") + print(f"{reducemaxglobal =}") if do_plot: if out_sp_key == "0" or out_sp_key == "3": @@ -1502,7 +1502,7 @@ def error(e1, e2, e3): maxerrorB = auxerror inputB[col0, col1, col2] = 0.0 - print(f"{maxerrorB = }") + print(f"{maxerrorB =}") assert maxerrorB < 10.0**-13 maxerrorD = 0.0 @@ -1530,7 +1530,7 @@ def error(e1, e2, e3): maxerrorD = auxerror inputD[col0, col1, col2] = 0.0 - print(f"{maxerrorD = }") + print(f"{maxerrorD =}") assert maxerrorD < 10.0**-13 print("Test spline evaluation passed.") diff --git a/src/struphy/feec/tests/test_lowdim_nel_is_1.py b/src/struphy/feec/tests/test_lowdim_nel_is_1.py index fdbe6be3d..cefcddf61 100644 --- a/src/struphy/feec/tests/test_lowdim_nel_is_1.py +++ b/src/struphy/feec/tests/test_lowdim_nel_is_1.py @@ -161,7 +161,7 @@ def div_f(x, y, z): # a) projection error err_f0 = xp.max(xp.abs(f(e1, e2, e3) - field_f0_vals)) - print(f"\n{err_f0 = }") + print(f"\n{err_f0 =}") assert err_f0 < 1e-2 # b) commuting property @@ -174,7 +174,7 @@ def div_f(x, y, z): field_df0_vals = field_df0(e1, e2, e3, squeeze_out=True) err_df0 = [xp.max(xp.abs(exact(e1, e2, e3) - field_v)) for exact, field_v in zip(grad_f, field_df0_vals)] - print(f"{err_df0 = }") + print(f"{err_df0 =}") assert xp.max(err_df0) < 0.64 # d) plotting @@ -203,7 +203,7 @@ def div_f(x, y, z): # a) projection error err_f1 = [xp.max(xp.abs(exact(e1, e2, e3) - field_v)) for exact, field_v in zip([f, f, f], field_f1_vals)] - print(f"{err_f1 = }") + print(f"{err_f1 =}") assert xp.max(err_f1) < 0.09 # b) commuting property @@ -216,7 +216,7 @@ def div_f(x, y, z): field_df1_vals = field_df1(e1, e2, e3, squeeze_out=True) err_df1 = [xp.max(xp.abs(exact(e1, e2, e3) - field_v)) for exact, field_v in zip(curl_f, field_df1_vals)] - print(f"{err_df1 = }") + print(f"{err_df1 =}") assert xp.max(err_df1) < 0.64 # d) plotting @@ -250,7 +250,7 @@ def div_f(x, y, z): # a) projection error err_f2 = [xp.max(xp.abs(exact(e1, e2, e3) - field_v)) for exact, field_v in zip([f, f, f], field_f2_vals)] - print(f"{err_f2 = }") + print(f"{err_f2 =}") assert xp.max(err_f2) < 0.09 # b) commuting property @@ -263,7 +263,7 @@ def div_f(x, y, z): field_df2_vals = field_df2(e1, e2, e3, squeeze_out=True) err_df2 = xp.max(xp.abs(div_f(e1, e2, e3) - field_df2_vals)) - print(f"{err_df2 = }") + print(f"{err_df2 =}") assert xp.max(err_df2) < 0.64 # d) plotting @@ -292,7 +292,7 @@ def div_f(x, y, z): # a) projection error err_f3 = xp.max(xp.abs(f(e1, e2, e3) - field_f3_vals)) - print(f"{err_f3 = }") + print(f"{err_f3 =}") assert err_f3 < 0.09 # d) plotting diff --git a/src/struphy/feec/tests/test_mass_matrices.py b/src/struphy/feec/tests/test_mass_matrices.py index a57d67cdc..e1d629c2e 100644 --- a/src/struphy/feec/tests/test_mass_matrices.py +++ b/src/struphy/feec/tests/test_mass_matrices.py @@ -56,7 +56,7 @@ def test_mass(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): "n2": 4.0, "na": 0.0, "beta": 0.1, - } + }, ) elif mapping[0] == "Colella": @@ -71,7 +71,7 @@ def test_mass(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): "n2": 4.0, "na": 0.0, "beta": 0.1, - } + }, ) if show_plots: @@ -89,7 +89,7 @@ def test_mass(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): "n2": 4.0, "na": 0.0, "beta": 0.1, - } + }, ) if show_plots: @@ -106,7 +106,7 @@ def test_mass(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): dirichlet_bc = [(False, False)] * 3 dirichlet_bc = tuple(dirichlet_bc) - print(f"{dirichlet_bc = }") + print(f"{dirichlet_bc =}") # derham object derham = Derham(Nel, p, spl_kind, comm=mpi_comm, dirichlet_bc=dirichlet_bc) @@ -124,7 +124,7 @@ def test_mass(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): # test calling the diagonal method aaa = mass_mats.M0.matrix.diagonal() bbb = mass_mats.M1.matrix.diagonal() - print(f"{aaa = }, {bbb[0, 0] = }, {bbb[0, 1] = }") + print(f"{aaa =}, {bbb[0, 0] =}, {bbb[0, 1] =}") # compare to old STRUPHY bc_old = [[None, None], [None, None], [None, None]] @@ -220,7 +220,11 @@ def test_mass(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): # Change order of input in callable rM1ninvswitch_psy = mass_mats.create_weighted_mass( - "Hcurl", "Hcurl", weights=["sqrt_g", "1/eq_n0", "Ginv"], name="M1ninv", assemble=True + "Hcurl", + "Hcurl", + weights=["sqrt_g", "1/eq_n0", "Ginv"], + name="M1ninv", + assemble=True, ).dot(x1_psy, apply_bc=True) rot_B = RotationMatrix( @@ -229,7 +233,11 @@ def test_mass(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): mass_mats.weights[mass_mats.selected_weight].b2_3, ) rM1Bninvswitch_psy = mass_mats.create_weighted_mass( - "Hcurl", "Hcurl", weights=["1/eq_n0", "sqrt_g", "Ginv", rot_B, "Ginv"], name="M1Bninv", assemble=True + "Hcurl", + "Hcurl", + weights=["1/eq_n0", "sqrt_g", "Ginv", rot_B, "Ginv"], + name="M1Bninv", + assemble=True, ).dot(x1_psy, apply_bc=True) # Test matrix free operators @@ -255,7 +263,11 @@ def test_mass(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): # Change order of input in callable rM1ninvswitch_fre = mass_mats_free.create_weighted_mass( - "Hcurl", "Hcurl", weights=["sqrt_g", "1/eq_n0", "Ginv"], name="M1ninvswitch", assemble=True + "Hcurl", + "Hcurl", + weights=["sqrt_g", "1/eq_n0", "Ginv"], + name="M1ninvswitch", + assemble=True, ).dot(x1_psy, apply_bc=True) rot_B = RotationMatrix( mass_mats_free.weights[mass_mats_free.selected_weight].b2_1, @@ -264,7 +276,11 @@ def test_mass(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): ) rM1Bninvswitch_fre = mass_mats_free.create_weighted_mass( - "Hcurl", "Hcurl", weights=["1/eq_n0", "sqrt_g", "Ginv", rot_B, "Ginv"], name="M1Bninvswitch", assemble=True + "Hcurl", + "Hcurl", + weights=["1/eq_n0", "sqrt_g", "Ginv", rot_B, "Ginv"], + name="M1Bninvswitch", + assemble=True, ).dot(x1_psy, apply_bc=True) # compare output arrays @@ -420,7 +436,7 @@ def test_mass_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): "n2": 4.0, "na": 0.0, "beta": 0.1, - } + }, ) if show_plots: @@ -440,7 +456,14 @@ def test_mass_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots=False): # derham object derham = Derham( - Nel, p, spl_kind, comm=mpi_comm, dirichlet_bc=dirichlet_bc, with_projectors=False, polar_ck=1, domain=domain + Nel, + p, + spl_kind, + comm=mpi_comm, + dirichlet_bc=dirichlet_bc, + with_projectors=False, + polar_ck=1, + domain=domain, ) print(f"Rank {mpi_rank} | Local domain : " + str(derham.domain_array[mpi_rank])) @@ -619,7 +642,7 @@ def test_mass_preconditioner(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots "n2": 4.0, "na": 0.0, "beta": 0.1, - } + }, ) elif mapping[0] == "Colella": @@ -634,7 +657,7 @@ def test_mass_preconditioner(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots "n2": 4.0, "na": 0.0, "beta": 0.1, - } + }, ) if show_plots: @@ -652,7 +675,7 @@ def test_mass_preconditioner(Nel, p, spl_kind, dirichlet_bc, mapping, show_plots "n2": 4.0, "na": 0.0, "beta": 0.1, - } + }, ) if show_plots: @@ -926,7 +949,7 @@ def test_mass_preconditioner_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show "n2": 4.0, "na": 0.0, "beta": 0.1, - } + }, ) if show_plots: @@ -946,7 +969,14 @@ def test_mass_preconditioner_polar(Nel, p, spl_kind, dirichlet_bc, mapping, show # derham object derham = Derham( - Nel, p, spl_kind, comm=mpi_comm, dirichlet_bc=dirichlet_bc, with_projectors=False, polar_ck=1, domain=domain + Nel, + p, + spl_kind, + comm=mpi_comm, + dirichlet_bc=dirichlet_bc, + with_projectors=False, + polar_ck=1, + domain=domain, ) print(f"Rank {mpi_rank} | Local domain : " + str(derham.domain_array[mpi_rank])) diff --git a/src/struphy/feec/tests/test_toarray_struphy.py b/src/struphy/feec/tests/test_toarray_struphy.py index 0279293ea..90427d8e4 100644 --- a/src/struphy/feec/tests/test_toarray_struphy.py +++ b/src/struphy/feec/tests/test_toarray_struphy.py @@ -5,7 +5,8 @@ @pytest.mark.parametrize("p", [[3, 2, 1]]) @pytest.mark.parametrize("spl_kind", [[False, True, True], [True, False, False]]) @pytest.mark.parametrize( - "mapping", [["Cuboid", {"l1": 1.0, "r1": 2.0, "l2": 10.0, "r2": 20.0, "l3": 100.0, "r3": 200.0}]] + "mapping", + [["Cuboid", {"l1": 1.0, "r1": 2.0, "l2": 10.0, "r2": 20.0, "l3": 100.0, "r3": 200.0}]], ) def test_toarray_struphy(Nel, p, spl_kind, mapping): """ diff --git a/src/struphy/feec/tests/test_tosparse_struphy.py b/src/struphy/feec/tests/test_tosparse_struphy.py index 020ab9e29..48cbfd7a2 100644 --- a/src/struphy/feec/tests/test_tosparse_struphy.py +++ b/src/struphy/feec/tests/test_tosparse_struphy.py @@ -7,7 +7,8 @@ @pytest.mark.parametrize("p", [[3, 2, 1]]) @pytest.mark.parametrize("spl_kind", [[False, True, True], [True, False, False]]) @pytest.mark.parametrize( - "mapping", [["Cuboid", {"l1": 1.0, "r1": 2.0, "l2": 10.0, "r2": 20.0, "l3": 100.0, "r3": 200.0}]] + "mapping", + [["Cuboid", {"l1": 1.0, "r1": 2.0, "l2": 10.0, "r2": 20.0, "l3": 100.0, "r3": 200.0}]], ) def test_tosparse_struphy(Nel, p, spl_kind, mapping): """ @@ -115,14 +116,26 @@ def test_tosparse_struphy(Nel, p, spl_kind, mapping): if __name__ == "__main__": test_tosparse_struphy( - [32, 2, 2], [2, 1, 1], [True, True, True], ["Colella", {"Lx": 1.0, "Ly": 2.0, "alpha": 0.5, "Lz": 3.0}] + [32, 2, 2], + [2, 1, 1], + [True, True, True], + ["Colella", {"Lx": 1.0, "Ly": 2.0, "alpha": 0.5, "Lz": 3.0}], ) test_tosparse_struphy( - [2, 32, 2], [1, 2, 1], [True, True, True], ["Colella", {"Lx": 1.0, "Ly": 2.0, "alpha": 0.5, "Lz": 3.0}] + [2, 32, 2], + [1, 2, 1], + [True, True, True], + ["Colella", {"Lx": 1.0, "Ly": 2.0, "alpha": 0.5, "Lz": 3.0}], ) test_tosparse_struphy( - [2, 2, 32], [1, 1, 2], [True, True, True], ["Colella", {"Lx": 1.0, "Ly": 2.0, "alpha": 0.5, "Lz": 3.0}] + [2, 2, 32], + [1, 1, 2], + [True, True, True], + ["Colella", {"Lx": 1.0, "Ly": 2.0, "alpha": 0.5, "Lz": 3.0}], ) test_tosparse_struphy( - [2, 2, 32], [1, 1, 2], [False, False, False], ["Colella", {"Lx": 1.0, "Ly": 2.0, "alpha": 0.5, "Lz": 3.0}] + [2, 2, 32], + [1, 1, 2], + [False, False, False], + ["Colella", {"Lx": 1.0, "Ly": 2.0, "alpha": 0.5, "Lz": 3.0}], ) diff --git a/src/struphy/feec/utilities.py b/src/struphy/feec/utilities.py index aa3912857..2fffe38e3 100644 --- a/src/struphy/feec/utilities.py +++ b/src/struphy/feec/utilities.py @@ -45,7 +45,7 @@ def __call__(self, e1, e2, e3): [ [self._cross_mask[m][n] * fun(e1, e2, e3) for n, fun in enumerate(row)] for m, row in enumerate(self._funs) - ] + ], ) # numpy operates on the last two indices with @ @@ -99,7 +99,9 @@ def create_equal_random_arrays(V, seed=123, flattened=False): e = arr_psy.ends arr_psy[s[0] : e[0] + 1, s[1] : e[1] + 1, s[2] : e[2] + 1] = arr[-1][ - s[0] : e[0] + 1, s[1] : e[1] + 1, s[2] : e[2] + 1 + s[0] : e[0] + 1, + s[1] : e[1] + 1, + s[2] : e[2] + 1, ] if flattened: @@ -117,7 +119,9 @@ def create_equal_random_arrays(V, seed=123, flattened=False): e = block.ends arr_psy[d][s[0] : e[0] + 1, s[1] : e[1] + 1, s[2] : e[2] + 1] = arr[-1][ - s[0] : e[0] + 1, s[1] : e[1] + 1, s[2] : e[2] + 1 + s[0] : e[0] + 1, + s[1] : e[1] + 1, + s[2] : e[2] + 1, ] if flattened: @@ -126,7 +130,7 @@ def create_equal_random_arrays(V, seed=123, flattened=False): arr[0].flatten(), arr[1].flatten(), arr[2].flatten(), - ) + ), ) arr_psy.update_ghost_regions() diff --git a/src/struphy/feec/utilities_local_projectors.py b/src/struphy/feec/utilities_local_projectors.py index 9ff91a120..3ce6590f2 100644 --- a/src/struphy/feec/utilities_local_projectors.py +++ b/src/struphy/feec/utilities_local_projectors.py @@ -592,10 +592,10 @@ def build_translation_list_for_non_zero_spline_indices( for h in range(3): translation_indices_B_or_D_splines[h]["B"][Basis_functions_indices_B[h]] = xp.arange( - len(Basis_functions_indices_B[h]) + len(Basis_functions_indices_B[h]), ) translation_indices_B_or_D_splines[h]["D"][Basis_functions_indices_D[h]] = xp.arange( - len(Basis_functions_indices_D[h]) + len(Basis_functions_indices_D[h]), ) if sp_id in {"Hcurl", "Hdiv", "H1vec"}: @@ -610,7 +610,11 @@ def build_translation_list_for_non_zero_spline_indices( def evaluate_relevant_splines_at_relevant_points( - localpts, Bspaces_1d, Dspaces_1d, Basis_functions_indices_B, Basis_functions_indices_D + localpts, + Bspaces_1d, + Dspaces_1d, + Basis_functions_indices_B, + Basis_functions_indices_D, ): """This function evaluates all the B and D-splines that produce non-zeros in the BasisProjectionOperatorLocal's rows that belong to the current MPI rank over all the local evaluation points. They are store as float arrays in a dictionary of lists. @@ -693,7 +697,15 @@ def evaluate_relevant_splines_at_relevant_points( def determine_non_zero_rows_for_each_spline( - Basis_functions_indices_B, Basis_functions_indices_D, starts, ends, p, B_nbasis, D_nbasis, periodic, IoH + Basis_functions_indices_B, + Basis_functions_indices_D, + starts, + ends, + p, + B_nbasis, + D_nbasis, + periodic, + IoH, ): """This function determines for which rows (amongst those belonging to the current MPI rank) of the BasisProjectionOperatorLocal each B and D spline, of relevance for the current MPI rank, produces non-zero entries and annotates this regions of non-zeros by saving the rows at which each region starts and ends. @@ -792,7 +804,8 @@ def process_splines(indices, nbasis, is_D, h): def get_splines_that_are_relevant_for_at_least_one_block( - Basis_function_indices_agreggated_B, Basis_function_indices_agreggated_D + Basis_function_indices_agreggated_B, + Basis_function_indices_agreggated_D, ): """This function builds one list with all the B-spline indices (and another one for the D-splines) that are required for at least one block of the FE coefficients the current MPI rank needs to build its share of the BasisProjectionOperatorLocal. diff --git a/src/struphy/feec/variational_utilities.py b/src/struphy/feec/variational_utilities.py index 12695bfa2..d03a75e3d 100644 --- a/src/struphy/feec/variational_utilities.py +++ b/src/struphy/feec/variational_utilities.py @@ -264,19 +264,27 @@ def dot(self, v, out=None): self.gv3f.vector = grad_3_v vf_values = self.vf.eval_tp_fixed_loc( - self.interpolation_grid_spans, [self.interpolation_grid_bn] * 3, out=self._vf_values + self.interpolation_grid_spans, + [self.interpolation_grid_bn] * 3, + out=self._vf_values, ) gvf1_values = self.gv1f.eval_tp_fixed_loc( - self.interpolation_grid_spans, self.interpolation_grid_gradient, out=self._gvf1_values + self.interpolation_grid_spans, + self.interpolation_grid_gradient, + out=self._gvf1_values, ) gvf2_values = self.gv2f.eval_tp_fixed_loc( - self.interpolation_grid_spans, self.interpolation_grid_gradient, out=self._gvf2_values + self.interpolation_grid_spans, + self.interpolation_grid_gradient, + out=self._gvf2_values, ) gvf3_values = self.gv3f.eval_tp_fixed_loc( - self.interpolation_grid_spans, self.interpolation_grid_gradient, out=self._gvf3_values + self.interpolation_grid_spans, + self.interpolation_grid_gradient, + out=self._gvf3_values, ) self.PiuT.update_weights([[vf_values[0], vf_values[1], vf_values[2]]]) @@ -1383,7 +1391,12 @@ def update_weight(self, coeffs): self._pc.update_mass_operator(self._massop) def _create_inv( - self, type="pcg", pc_type="MassMatrixDiagonalPreconditioner", tol=1e-16, maxiter=500, verbose=False + self, + type="pcg", + pc_type="MassMatrixDiagonalPreconditioner", + tol=1e-16, + maxiter=500, + verbose=False, ): """Inverse the weighted mass matrix""" if pc_type is None: diff --git a/src/struphy/fields_background/base.py b/src/struphy/fields_background/base.py index 4696acf54..7ad6e3887 100644 --- a/src/struphy/fields_background/base.py +++ b/src/struphy/fields_background/base.py @@ -783,19 +783,28 @@ def u_cart(self, *etas, squeeze_out=False): def curl_unit_b1(self, *etas, squeeze_out=False): """1-form components of curl of unit magnetic field evaluated on logical cube [0, 1]^3. Returns also (x,y,z).""" return self.domain.pull( - self.curl_unit_b_cart(*etas, squeeze_out=False)[0], *etas, kind="1", squeeze_out=squeeze_out + self.curl_unit_b_cart(*etas, squeeze_out=False)[0], + *etas, + kind="1", + squeeze_out=squeeze_out, ) def curl_unit_b2(self, *etas, squeeze_out=False): """2-form components of curl of unit magnetic field evaluated on logical cube [0, 1]^3. Returns also (x,y,z).""" return self.domain.pull( - self.curl_unit_b_cart(*etas, squeeze_out=False)[0], *etas, kind="2", squeeze_out=squeeze_out + self.curl_unit_b_cart(*etas, squeeze_out=False)[0], + *etas, + kind="2", + squeeze_out=squeeze_out, ) def curl_unit_bv(self, *etas, squeeze_out=False): """Contra-variant components of curl of unit magnetic field evaluated on logical cube [0, 1]^3. Returns also (x,y,z).""" return self.domain.pull( - self.curl_unit_b_cart(*etas, squeeze_out=False)[0], *etas, kind="v", squeeze_out=squeeze_out + self.curl_unit_b_cart(*etas, squeeze_out=False)[0], + *etas, + kind="v", + squeeze_out=squeeze_out, ) def curl_unit_b_cart(self, *etas, squeeze_out=False): diff --git a/src/struphy/fields_background/coil_fields/base.py b/src/struphy/fields_background/coil_fields/base.py index 3b068c62d..331e89e7d 100644 --- a/src/struphy/fields_background/coil_fields/base.py +++ b/src/struphy/fields_background/coil_fields/base.py @@ -71,7 +71,11 @@ def bv(self, *etas, squeeze_out=False): def b_cart(self, *etas, squeeze_out=False): """Cartesian components of equilibrium magnetic field evaluated on logical cube [0, 1]^3. Returns also (x,y,z).""" b_out = self.domain.push( - self.bv(*etas, squeeze_out=False), *etas, kind="v", a_kwargs={"squeeze_out": False}, squeeze_out=squeeze_out + self.bv(*etas, squeeze_out=False), + *etas, + kind="v", + a_kwargs={"squeeze_out": False}, + squeeze_out=squeeze_out, ) return b_out, self.domain(*etas, squeeze_out=squeeze_out) diff --git a/src/struphy/fields_background/coil_fields/coil_fields.py b/src/struphy/fields_background/coil_fields/coil_fields.py index e1f66c71d..1b5c66a15 100644 --- a/src/struphy/fields_background/coil_fields/coil_fields.py +++ b/src/struphy/fields_background/coil_fields/coil_fields.py @@ -15,7 +15,9 @@ def __init__(self, csv_path=None, Nel=[16, 16, 16], p=[3, 3, 3], domain=None, ** self._ratgui_csv_data = load_csv_data(csv_path) derham = Derham( - Nel=Nel, p=p, spl_kind=[False, False, True] + Nel=Nel, + p=p, + spl_kind=[False, False, True], ) # Assuming (R=eta1, Z=eta2, phi=eta3) coordinates for csv data (periodic in eta3 only). self._interpolate = derham.P[ "v" @@ -34,14 +36,14 @@ def __init__(self, csv_path=None, Nel=[16, 16, 16], p=[3, 3, 3], domain=None, ** self.rhs[1][:] = B_Z self.rhs[2][:] = B_phi - print(f"{self.rhs = }") - print(f"{derham.nbasis['v'] = }") - print(f"{self.rhs[0] = }") - print(f"{self.rhs[1] = }") - print(f"{self.rhs[2] = }") - print(f"{self.rhs[0][:].shape = }") - print(f"{self.rhs[1][:].shape = }") - print(f"{self.rhs[2][:].shape = }") + print(f"{self.rhs =}") + print(f"{derham.nbasis['v'] =}") + print(f"{self.rhs[0] =}") + print(f"{self.rhs[1] =}") + print(f"{self.rhs[2] =}") + print(f"{self.rhs[0][:].shape =}") + print(f"{self.rhs[1][:].shape =}") + print(f"{self.rhs[2][:].shape =}") # We need to choose Nel and p such that the csv_data fits into this vector. # For a periodic direction, the size of the vector is Nel, for non-periodic (spl_kind=False) the size is Nel + p. # See the Tutorial on FEEC data structures https://struphy.pages.mpcdf.de/struphy/tutorials/tutorial_06_data_structures.html#FEEC-data-structures on how to address such a vector diff --git a/src/struphy/fields_background/equils.py b/src/struphy/fields_background/equils.py index 93688b55f..f1afacd35 100644 --- a/src/struphy/fields_background/equils.py +++ b/src/struphy/fields_background/equils.py @@ -1759,7 +1759,7 @@ def __init__( units["p"] = 1.0 units["n"] = 1e20 warnings.warn( - f"{units = }, no rescaling performed in EQDSK output.", + f"{units =}, no rescaling performed in EQDSK output.", ) self._units = units @@ -2133,7 +2133,7 @@ def __init__( with pytest.raises(SystemExit) as exc: print("Simulation aborted, gvec must be installed (pip install gvec)!") sys.exit(1) - print(f"{exc.value.code = }") + print(f"{exc.value.code =}") import gvec @@ -2148,7 +2148,7 @@ def __init__( units["p"] = 1.0 units["n"] = 1e20 warnings.warn( - f"{units = }, no rescaling performed in GVEC output.", + f"{units =}, no rescaling performed in GVEC output.", ) self._units = units @@ -2428,7 +2428,7 @@ def __init__( units["p"] = 1.0 units["n"] = 1e20 warnings.warn( - f"{units = }, no rescaling performed in DESC output.", + f"{units =}, no rescaling performed in DESC output.", ) self._units = units @@ -2880,23 +2880,23 @@ def desc_eval( if verbose: # import sys - print(f"\n{nfp = }") - print(f"{self.eq.axis = }") - print(f"{rho.size = }") - print(f"{theta.size = }") - print(f"{zeta.size = }") - print(f"{grid_3d.num_rho = }") - print(f"{grid_3d.num_theta = }") - print(f"{grid_3d.num_zeta = }") + print(f"\n{nfp =}") + print(f"{self.eq.axis =}") + print(f"{rho.size =}") + print(f"{theta.size =}") + print(f"{zeta.size =}") + print(f"{grid_3d.num_rho =}") + print(f"{grid_3d.num_theta =}") + print(f"{grid_3d.num_zeta =}") # print(f'\n{grid_3d.nodes[:, 0] = }') # print(f'\n{grid_3d.nodes[:, 1] = }') # print(f'\n{grid_3d.nodes[:, 2] = }') - print(f"\n{rho = }") - print(f"{rho1 = }") - print(f"\n{theta = }") - print(f"{theta1 = }") - print(f"\n{zeta = }") - print(f"{zeta1 = }") + print(f"\n{rho =}") + print(f"{rho1 =}") + print(f"\n{theta =}") + print(f"{theta1 =}") + print(f"\n{zeta =}") + print(f"{zeta1 =}") # make c-contiguous out = xp.ascontiguousarray(out) diff --git a/src/struphy/fields_background/mhd_equil/eqdsk/readeqdsk.py b/src/struphy/fields_background/mhd_equil/eqdsk/readeqdsk.py index 67578ce17..812381e87 100644 --- a/src/struphy/fields_background/mhd_equil/eqdsk/readeqdsk.py +++ b/src/struphy/fields_background/mhd_equil/eqdsk/readeqdsk.py @@ -173,7 +173,11 @@ def main(): action="store_true", ) parser.add_option( - "-v", "--vars", dest="vars", help="comma separated list of variables (use '-v \"*\"' for all)", default="*" + "-v", + "--vars", + dest="vars", + help="comma separated list of variables (use '-v \"*\"' for all)", + default="*", ) parser.add_option( "-p", diff --git a/src/struphy/fields_background/tests/test_desc_equil.py b/src/struphy/fields_background/tests/test_desc_equil.py index 5aca31b8d..c7130f0a3 100644 --- a/src/struphy/fields_background/tests/test_desc_equil.py +++ b/src/struphy/fields_background/tests/test_desc_equil.py @@ -167,7 +167,7 @@ def test_desc_equil(do_plot=False): err_lim = 0.09 for nfp in nfps: - print(f"\n{nfp = }") + print(f"\n{nfp =}") for var in vars: if var in ("B_R", "B_phi", "B_Z", "J_R", "J_phi", "J_Z"): continue @@ -179,7 +179,7 @@ def test_desc_equil(do_plot=False): assert err < err_lim print( - f"compare {var}: {err = }", + f"compare {var}: {err =}", ) if do_plot: @@ -193,7 +193,7 @@ def test_desc_equil(do_plot=False): plt.subplot(2, 2, 1) map1 = plt.contourf(R, Z, outs[nfp][var][:, :, 0], levels=levels) - plt.title(f"DESC, {var = }, {nfp = }") + plt.title(f"DESC, {var =}, {nfp =}") plt.xlabel("$R$") plt.ylabel("$Z$") plt.axis("equal") @@ -201,7 +201,7 @@ def test_desc_equil(do_plot=False): plt.subplot(2, 2, 2) map2 = plt.contourf(R, Z, outs_struphy[nfp][var][:, :, 0], levels=levels) - plt.title(f"Struphy, {err = }") + plt.title(f"Struphy, {err =}") plt.xlabel("$R$") plt.ylabel("$Z$") plt.axis("equal") @@ -217,7 +217,7 @@ def test_desc_equil(do_plot=False): plt.subplot(2, 2, 3) map3 = plt.contourf(x1, y1, outs[nfp][var][:, 0, :], levels=levels) map3b = plt.contourf(x2, y2, outs[nfp][var][:, n2 // 2, :], levels=levels) - plt.title(f"DESC, {var = }, {nfp = }") + plt.title(f"DESC, {var =}, {nfp =}") plt.xlabel("$x$") plt.ylabel("$y$") plt.axis("equal") @@ -226,7 +226,7 @@ def test_desc_equil(do_plot=False): plt.subplot(2, 2, 4) map4 = plt.contourf(x1, y1, outs_struphy[nfp][var][:, 0, :], levels=levels) map4b = plt.contourf(x2, y2, outs_struphy[nfp][var][:, n2 // 2, :], levels=levels) - plt.title(f"Struphy, {err = }") + plt.title(f"Struphy, {err =}") plt.xlabel("$x$") plt.ylabel("$y$") plt.axis("equal") diff --git a/src/struphy/geometry/base.py b/src/struphy/geometry/base.py index a789913e7..d2b21688e 100644 --- a/src/struphy/geometry/base.py +++ b/src/struphy/geometry/base.py @@ -1287,9 +1287,9 @@ def prepare_arg(a_in, *Xs, is_sparse_meshgrid=False, a_kwargs={}): elif isinstance(component, xp.ndarray): if flat_eval: - assert component.ndim == 1, print(f"{component.ndim = }") + assert component.ndim == 1, print(f"{component.ndim =}") else: - assert component.ndim == 3, print(f"{component.ndim = }") + assert component.ndim == 3, print(f"{component.ndim =}") a_out += [component] @@ -1312,7 +1312,7 @@ def prepare_arg(a_in, *Xs, is_sparse_meshgrid=False, a_kwargs={}): else: raise ValueError( "Input array a_in must be either 1d (scalar) or \ - 2d (vector-valued, shape (3,:)) for flat evaluation!" + 2d (vector-valued, shape (3,:)) for flat evaluation!", ) else: @@ -1331,13 +1331,13 @@ def prepare_arg(a_in, *Xs, is_sparse_meshgrid=False, a_kwargs={}): else: raise ValueError( "Input array a_in must be either 3d (scalar) or \ - 4d (vector-valued, shape (3,:,:,:)) for non-flat evaluation!" + 4d (vector-valued, shape (3,:,:,:)) for non-flat evaluation!", ) else: raise TypeError( "Argument a must be either a float OR a list/tuple of 1 or 3 callable(s)/numpy array(s)/float(s) \ - OR a single numpy array OR a single callable!" + OR a single numpy array OR a single callable!", ) # make sure that output array is 2d and of shape (:, 1) or (:, 3) for flat evaluation diff --git a/src/struphy/geometry/domains.py b/src/struphy/geometry/domains.py index 024b980c9..7b2c25064 100644 --- a/src/struphy/geometry/domains.py +++ b/src/struphy/geometry/domains.py @@ -743,7 +743,7 @@ def __init__( self.params = copy.deepcopy(locals()) self.params_numpy = self.get_params_numpy() - assert a2 <= R0, f"The minor radius must be smaller or equal than the major radius! {a2 = }, {R0 = }" + assert a2 <= R0, f"The minor radius must be smaller or equal than the major radius! {a2 =}, {R0 =}" if sfl: assert pol_period == 1, ( diff --git a/src/struphy/geometry/mappings_kernels.py b/src/struphy/geometry/mappings_kernels.py index 8b643855d..83e6275fd 100644 --- a/src/struphy/geometry/mappings_kernels.py +++ b/src/struphy/geometry/mappings_kernels.py @@ -49,13 +49,40 @@ def spline_3d( tmp3 = ind3[span3 - int(p[2]), :] f_out[0] = evaluation_kernels_3d.evaluation_kernel_3d( - int(p[0]), int(p[1]), int(p[2]), b1, b2, b3, tmp1, tmp2, tmp3, args.cx + int(p[0]), + int(p[1]), + int(p[2]), + b1, + b2, + b3, + tmp1, + tmp2, + tmp3, + args.cx, ) f_out[1] = evaluation_kernels_3d.evaluation_kernel_3d( - int(p[0]), int(p[1]), int(p[2]), b1, b2, b3, tmp1, tmp2, tmp3, args.cy + int(p[0]), + int(p[1]), + int(p[2]), + b1, + b2, + b3, + tmp1, + tmp2, + tmp3, + args.cy, ) f_out[2] = evaluation_kernels_3d.evaluation_kernel_3d( - int(p[0]), int(p[1]), int(p[2]), b1, b2, b3, tmp1, tmp2, tmp3, args.cz + int(p[0]), + int(p[1]), + int(p[2]), + b1, + b2, + b3, + tmp1, + tmp2, + tmp3, + args.cz, ) @@ -97,31 +124,112 @@ def spline_3d_df( tmp3 = ind3[span3 - int(p[2]), :] df_out[0, 0] = evaluation_kernels_3d.evaluation_kernel_3d( - int(p[0]), int(p[1]), int(p[2]), der1, b2, b3, tmp1, tmp2, tmp3, args.cx + int(p[0]), + int(p[1]), + int(p[2]), + der1, + b2, + b3, + tmp1, + tmp2, + tmp3, + args.cx, ) df_out[0, 1] = evaluation_kernels_3d.evaluation_kernel_3d( - int(p[0]), int(p[1]), int(p[2]), b1, der2, b3, tmp1, tmp2, tmp3, args.cx + int(p[0]), + int(p[1]), + int(p[2]), + b1, + der2, + b3, + tmp1, + tmp2, + tmp3, + args.cx, ) df_out[0, 2] = evaluation_kernels_3d.evaluation_kernel_3d( - int(p[0]), int(p[1]), int(p[2]), b1, b2, der3, tmp1, tmp2, tmp3, args.cx + int(p[0]), + int(p[1]), + int(p[2]), + b1, + b2, + der3, + tmp1, + tmp2, + tmp3, + args.cx, ) df_out[1, 0] = evaluation_kernels_3d.evaluation_kernel_3d( - int(p[0]), int(p[1]), int(p[2]), der1, b2, b3, tmp1, tmp2, tmp3, args.cy + int(p[0]), + int(p[1]), + int(p[2]), + der1, + b2, + b3, + tmp1, + tmp2, + tmp3, + args.cy, ) df_out[1, 1] = evaluation_kernels_3d.evaluation_kernel_3d( - int(p[0]), int(p[1]), int(p[2]), b1, der2, b3, tmp1, tmp2, tmp3, args.cy + int(p[0]), + int(p[1]), + int(p[2]), + b1, + der2, + b3, + tmp1, + tmp2, + tmp3, + args.cy, ) df_out[1, 2] = evaluation_kernels_3d.evaluation_kernel_3d( - int(p[0]), int(p[1]), int(p[2]), b1, b2, der3, tmp1, tmp2, tmp3, args.cy + int(p[0]), + int(p[1]), + int(p[2]), + b1, + b2, + der3, + tmp1, + tmp2, + tmp3, + args.cy, ) df_out[2, 0] = evaluation_kernels_3d.evaluation_kernel_3d( - int(p[0]), int(p[1]), int(p[2]), der1, b2, b3, tmp1, tmp2, tmp3, args.cz + int(p[0]), + int(p[1]), + int(p[2]), + der1, + b2, + b3, + tmp1, + tmp2, + tmp3, + args.cz, ) df_out[2, 1] = evaluation_kernels_3d.evaluation_kernel_3d( - int(p[0]), int(p[1]), int(p[2]), b1, der2, b3, tmp1, tmp2, tmp3, args.cz + int(p[0]), + int(p[1]), + int(p[2]), + b1, + der2, + b3, + tmp1, + tmp2, + tmp3, + args.cz, ) df_out[2, 2] = evaluation_kernels_3d.evaluation_kernel_3d( - int(p[0]), int(p[1]), int(p[2]), b1, b2, der3, tmp1, tmp2, tmp3, args.cz + int(p[0]), + int(p[1]), + int(p[2]), + b1, + b2, + der3, + tmp1, + tmp2, + tmp3, + args.cz, ) @@ -276,7 +384,7 @@ def spline_2d_torus( tmp2 = ind2[span2 - int(p[1]), :] f_out[0] = evaluation_kernels_2d.evaluation_kernel_2d(int(p[0]), int(p[1]), b1, b2, tmp1, tmp2, cx) * cos( - 2 * pi * eta3 / tor_period + 2 * pi * eta3 / tor_period, ) f_out[1] = ( evaluation_kernels_2d.evaluation_kernel_2d(int(p[0]), int(p[1]), b1, b2, tmp1, tmp2, cx) @@ -329,10 +437,10 @@ def spline_2d_torus_df( tmp2 = ind2[span2 - int(p[1]), :] df_out[0, 0] = evaluation_kernels_2d.evaluation_kernel_2d(int(p[0]), int(p[1]), der1, b2, tmp1, tmp2, cx) * cos( - 2 * pi * eta3 / tor_period + 2 * pi * eta3 / tor_period, ) df_out[0, 1] = evaluation_kernels_2d.evaluation_kernel_2d(int(p[0]), int(p[1]), b1, der2, tmp1, tmp2, cx) * cos( - 2 * pi * eta3 / tor_period + 2 * pi * eta3 / tor_period, ) df_out[0, 2] = ( evaluation_kernels_2d.evaluation_kernel_2d(int(p[0]), int(p[1]), b1, b2, tmp1, tmp2, cx) @@ -621,7 +729,14 @@ def hollow_cyl_df(eta1: float, eta2: float, a1: float, a2: float, lz: float, poc @pure def powered_ellipse( - eta1: float, eta2: float, eta3: float, rx: float, ry: float, lz: float, s: float, f_out: "float[:]" + eta1: float, + eta2: float, + eta3: float, + rx: float, + ry: float, + lz: float, + s: float, + f_out: "float[:]", ): r""" Point-wise evaluation of @@ -664,7 +779,14 @@ def powered_ellipse( @pure def powered_ellipse_df( - eta1: float, eta2: float, eta3: float, rx: float, ry: float, lz: float, s: float, df_out: "float[:,:]" + eta1: float, + eta2: float, + eta3: float, + rx: float, + ry: float, + lz: float, + s: float, + df_out: "float[:,:]", ): """Jacobian matrix for :meth:`struphy.geometry.mappings_kernels.powered_ellipse`.""" @@ -843,7 +965,14 @@ def hollow_torus_df( @pure def shafranov_shift( - eta1: float, eta2: float, eta3: float, rx: float, ry: float, lz: float, de: float, f_out: "float[:]" + eta1: float, + eta2: float, + eta3: float, + rx: float, + ry: float, + lz: float, + de: float, + f_out: "float[:]", ): r""" Point-wise evaluation of @@ -887,7 +1016,14 @@ def shafranov_shift( @pure def shafranov_shift_df( - eta1: float, eta2: float, eta3: float, rx: float, ry: float, lz: float, de: float, df_out: "float[:,:]" + eta1: float, + eta2: float, + eta3: float, + rx: float, + ry: float, + lz: float, + de: float, + df_out: "float[:,:]", ): """Jacobian matrix for :meth:`struphy.geometry.mappings_kernels.shafranov_shift`.""" @@ -904,7 +1040,14 @@ def shafranov_shift_df( @pure def shafranov_sqrt( - eta1: float, eta2: float, eta3: float, rx: float, ry: float, lz: float, de: float, f_out: "float[:]" + eta1: float, + eta2: float, + eta3: float, + rx: float, + ry: float, + lz: float, + de: float, + f_out: "float[:]", ): r""" Point-wise evaluation of @@ -946,7 +1089,14 @@ def shafranov_sqrt( @pure def shafranov_sqrt_df( - eta1: float, eta2: float, eta3: float, rx: float, ry: float, lz: float, de: float, df_out: "float[:,:]" + eta1: float, + eta2: float, + eta3: float, + rx: float, + ry: float, + lz: float, + de: float, + df_out: "float[:,:]", ): """Jacobian matrix for :meth:`struphy.geometry.mappings_kernels.shafranov_sqrt`.""" diff --git a/src/struphy/geometry/transform_kernels.py b/src/struphy/geometry/transform_kernels.py index c95156b96..f9e6d8077 100644 --- a/src/struphy/geometry/transform_kernels.py +++ b/src/struphy/geometry/transform_kernels.py @@ -54,7 +54,13 @@ @stack_array("dfmat1", "dfmat2") def pull( - a: "float[:]", eta1: float, eta2: float, eta3: float, kind_fun: int, args_domain: "DomainArguments", out: "float[:]" + a: "float[:]", + eta1: float, + eta2: float, + eta3: float, + kind_fun: int, + args_domain: "DomainArguments", + out: "float[:]", ): """ Pull-back of a Cartesian scalar/vector field to a differential p-form. @@ -114,7 +120,13 @@ def pull( @stack_array("dfmat1", "dfmat2", "dfmat3") def push( - a: "float[:]", eta1: float, eta2: float, eta3: float, kind_fun: int, args_domain: "DomainArguments", out: "float[:]" + a: "float[:]", + eta1: float, + eta2: float, + eta3: float, + kind_fun: int, + args_domain: "DomainArguments", + out: "float[:]", ): """ Pushforward of a differential p-forms to a Cartesian scalar/vector field. @@ -172,7 +184,13 @@ def push( @stack_array("dfmat1", "dfmat2", "dfmat3", "vec1", "vec2") def tran( - a: "float[:]", eta1: float, eta2: float, eta3: float, kind_fun: int, args_domain: "DomainArguments", out: "float[:]" + a: "float[:]", + eta1: float, + eta2: float, + eta3: float, + kind_fun: int, + args_domain: "DomainArguments", + out: "float[:]", ): """ Transformations between differential p-forms and/or vector fields. diff --git a/src/struphy/initial/eigenfunctions.py b/src/struphy/initial/eigenfunctions.py index 2f66fcacb..239ae24a9 100644 --- a/src/struphy/initial/eigenfunctions.py +++ b/src/struphy/initial/eigenfunctions.py @@ -67,13 +67,16 @@ def __init__(self, derham, **params): nnz_tor = derham.boundary_ops["2"].dim_nz_tor eig_vec_1 = U2_eig[ - 0 * nnz_pol[0] + 0 * nnz_pol[1] + 0 * nnz_pol[2] : 1 * nnz_pol[0] + 0 * nnz_pol[1] + 0 * nnz_pol[2], mode + 0 * nnz_pol[0] + 0 * nnz_pol[1] + 0 * nnz_pol[2] : 1 * nnz_pol[0] + 0 * nnz_pol[1] + 0 * nnz_pol[2], + mode, ] eig_vec_2 = U2_eig[ - 1 * nnz_pol[0] + 0 * nnz_pol[1] + 0 * nnz_pol[2] : 1 * nnz_pol[0] + 1 * nnz_pol[1] + 0 * nnz_pol[2], mode + 1 * nnz_pol[0] + 0 * nnz_pol[1] + 0 * nnz_pol[2] : 1 * nnz_pol[0] + 1 * nnz_pol[1] + 0 * nnz_pol[2], + mode, ] eig_vec_3 = U2_eig[ - 1 * nnz_pol[0] + 1 * nnz_pol[1] + 0 * nnz_pol[2] : 1 * nnz_pol[0] + 1 * nnz_pol[1] + 1 * nnz_pol[2], mode + 1 * nnz_pol[0] + 1 * nnz_pol[1] + 0 * nnz_pol[2] : 1 * nnz_pol[0] + 1 * nnz_pol[1] + 1 * nnz_pol[2], + mode, ] del omega2, U2_eig @@ -182,7 +185,7 @@ def __init__(self, derham, **params): ] eigvec_1_ten[derham.Vh_pol["2"].n_rings[0] : derham.nbasis["2"][0][0] - bc1_2, :, :] = eig_vec_1[1].reshape( - n_v2_0[0] + n_v2_0[0], ) eigvec_2_ten[derham.Vh_pol["2"].n_rings[1] :, :, :] = eig_vec_2[1].reshape(n_v2_0[1]) eigvec_3_ten[derham.Vh_pol["2"].n_rings[2] :, :, :] = eig_vec_3[1].reshape(n_v2_0[2]) diff --git a/src/struphy/initial/perturbations.py b/src/struphy/initial/perturbations.py index 767f899c9..4c113d02d 100644 --- a/src/struphy/initial/perturbations.py +++ b/src/struphy/initial/perturbations.py @@ -418,7 +418,7 @@ def __call__(self, eta1, eta2, eta3): z = eta3 val += (self._a * scipy.special.jv(self._m, r) + self._b * scipy.special.yn(self._m, r)) * xp.cos( - self._m * theta + self._m * theta, ) return val diff --git a/src/struphy/initial/tests/test_init_perturbations.py b/src/struphy/initial/tests/test_init_perturbations.py index 2f5fa3176..dd391cf56 100644 --- a/src/struphy/initial/tests/test_init_perturbations.py +++ b/src/struphy/initial/tests/test_init_perturbations.py @@ -222,7 +222,7 @@ def test_init_modes(Nel, p, spl_kind, mapping, combine_comps=None, do_plot=False params = { key: { "given_in_basis": [fun_form] * 3, - } + }, } if "Modes" in key: @@ -267,12 +267,20 @@ def test_init_modes(Nel, p, spl_kind, mapping, combine_comps=None, do_plot=False fun3_xyz = perturbation(eee1, eee2, eee3) elif fun_form == "norm": tmp1, tmp2, tmp3 = domain.transform( - [perturbation, perturbation, perturbation], eee1, eee2, eee3, kind=fun_form + "_to_v" + [perturbation, perturbation, perturbation], + eee1, + eee2, + eee3, + kind=fun_form + "_to_v", ) fun1_xyz, fun2_xyz, fun3_xyz = domain.push([tmp1, tmp2, tmp3], eee1, eee2, eee3, kind="v") else: fun1_xyz, fun2_xyz, fun3_xyz = domain.push( - [perturbation, perturbation, perturbation], eee1, eee2, eee3, kind=fun_form + [perturbation, perturbation, perturbation], + eee1, + eee2, + eee3, + kind=fun_form, ) fun_xyz_vec = [fun1_xyz, fun2_xyz, fun3_xyz] @@ -299,7 +307,7 @@ def test_init_modes(Nel, p, spl_kind, mapping, combine_comps=None, do_plot=False plt.ylabel("y") plt.colorbar() plt.title( - f"component {c + 1}, init was {fun_form}, (m,n)=({kwargs['ms'][0]},{kwargs['ns'][0]})" + f"component {c + 1}, init was {fun_form}, (m,n)=({kwargs['ms'][0]},{kwargs['ns'][0]})", ) ax = plt.gca() ax.set_aspect("equal", adjustable="box") @@ -316,7 +324,7 @@ def test_init_modes(Nel, p, spl_kind, mapping, combine_comps=None, do_plot=False plt.ylabel("z") plt.colorbar() plt.title( - f"component {c + 1}, init was {fun_form}, (m,n)=({kwargs['ms'][0]},{kwargs['ns'][0]})" + f"component {c + 1}, init was {fun_form}, (m,n)=({kwargs['ms'][0]},{kwargs['ns'][0]})", ) ax = plt.gca() ax.set_aspect("equal", adjustable="box") diff --git a/src/struphy/io/output_handling.py b/src/struphy/io/output_handling.py index 734a2d1a3..808c2bcf3 100644 --- a/src/struphy/io/output_handling.py +++ b/src/struphy/io/output_handling.py @@ -54,7 +54,7 @@ def __init__(self, path_out, file_name=None, comm=None): dataset_keys = [] self._file.visit( - lambda key: dataset_keys.append(key) if isinstance(self._file[key], h5py.Dataset) else None + lambda key: dataset_keys.append(key) if isinstance(self._file[key], h5py.Dataset) else None, ) for key in dataset_keys: @@ -110,7 +110,11 @@ def add_data(self, data_dict): self._file[key][0] = val[0] else: self._file.create_dataset( - key, (1,) + val.shape, maxshape=(None,) + val.shape, dtype=val.dtype, chunks=True + key, + (1,) + val.shape, + maxshape=(None,) + val.shape, + dtype=val.dtype, + chunks=True, ) self._file[key][0] = val diff --git a/src/struphy/io/setup.py b/src/struphy/io/setup.py index 78a295b35..f38654160 100644 --- a/src/struphy/io/setup.py +++ b/src/struphy/io/setup.py @@ -233,35 +233,35 @@ def descend_options_dict( out = copy.deepcopy(d) if verbose: - print(f"{d = }") - print(f"{out = }") - print(f"{d_default = }") - print(f"{d_opts = }") - print(f"{keys = }") - print(f"{depth = }") - print(f"{pop_again = }") + print(f"{d =}") + print(f"{out =}") + print(f"{d_default =}") + print(f"{d_opts =}") + print(f"{keys =}") + print(f"{depth =}") + print(f"{pop_again =}") if verbose: - print(f"{d = }") - print(f"{out = }") - print(f"{d_default = }") - print(f"{d_opts = }") - print(f"{keys = }") - print(f"{depth = }") - print(f"{pop_again = }") + print(f"{d =}") + print(f"{out =}") + print(f"{d_default =}") + print(f"{d_opts =}") + print(f"{keys =}") + print(f"{depth =}") + print(f"{pop_again =}") count = 0 for key, val in d.items(): count += 1 if verbose: - print(f"\n{keys = } | {key = }, {type(val) = }, {count = }\n") + print(f"\n{keys =} | {key =}, {type(val) =}, {count =}\n") if isinstance(val, list): # create default parameter dict "out" if verbose: - print(f"{val = }") + print(f"{val =}") if d_default is None: if len(keys) == 0: @@ -299,10 +299,10 @@ def descend_options_dict( out += [out_sublist] if verbose: - print(f"{out = }") + print(f"{out =}") if verbose: - print(f"{out = }") + print(f"{out =}") # recurse if necessary elif isinstance(val, dict): diff --git a/src/struphy/kinetic_background/base.py b/src/struphy/kinetic_background/base.py index 41c7b6575..765ee1508 100644 --- a/src/struphy/kinetic_background/base.py +++ b/src/struphy/kinetic_background/base.py @@ -395,7 +395,7 @@ def gaussian(self, v, u=0.0, vth=1.0, polar=False, volume_form=False): """ if isinstance(v, xp.ndarray) and isinstance(u, xp.ndarray): - assert v.shape == u.shape, f"{v.shape = } but {u.shape = }" + assert v.shape == u.shape, f"{v.shape =} but {u.shape =}" if not polar: out = 1.0 / vth * 1.0 / xp.sqrt(2.0 * xp.pi) * xp.exp(-((v - u) ** 2) / (2.0 * vth**2)) @@ -434,9 +434,9 @@ def __call__(self, *args): # Check that all args have the same shape shape0 = xp.shape(args[0]) for i, arg in enumerate(args): - assert xp.shape(arg) == shape0, f"Argument {i} has {xp.shape(arg) = }, but must be {shape0 = }." + assert xp.shape(arg) == shape0, f"Argument {i} has {xp.shape(arg) =}, but must be {shape0 =}." assert xp.ndim(arg) == 1 or xp.ndim(arg) == 3 + self.vdim, ( - f"{xp.ndim(arg) = } not allowed for Maxwellian evaluation." + f"{xp.ndim(arg) =} not allowed for Maxwellian evaluation." ) # flat or meshgrid evaluation # Get result evaluated at eta's diff --git a/src/struphy/kinetic_background/maxwellians.py b/src/struphy/kinetic_background/maxwellians.py index c7dea067a..d9e34dcab 100644 --- a/src/struphy/kinetic_background/maxwellians.py +++ b/src/struphy/kinetic_background/maxwellians.py @@ -431,7 +431,7 @@ def gaussian(self, e, vth=1.0): """ if isinstance(vth, xp.ndarray): - assert e.shape == vth.shape, f"{e.shape = } but {vth.shape = }" + assert e.shape == vth.shape, f"{e.shape =} but {vth.shape =}" return 2.0 * xp.sqrt(e / xp.pi) / vth**3 * xp.exp(-e / vth**2) @@ -462,9 +462,9 @@ def __call__(self, *args): # Check that all args have the same shape shape0 = xp.shape(args[0]) for i, arg in enumerate(args): - assert xp.shape(arg) == shape0, f"Argument {i} has {xp.shape(arg) = }, but must be {shape0 = }." + assert xp.shape(arg) == shape0, f"Argument {i} has {xp.shape(arg) =}, but must be {shape0 =}." assert xp.ndim(arg) == 1 or xp.ndim(arg) == 3, ( - f"{xp.ndim(arg) = } not allowed for canonical Maxwellian evaluation." + f"{xp.ndim(arg) =} not allowed for canonical Maxwellian evaluation." ) # flat or meshgrid evaluation # Get result evaluated with each particles' psic diff --git a/src/struphy/kinetic_background/tests/test_maxwellians.py b/src/struphy/kinetic_background/tests/test_maxwellians.py index edf24af4c..710a88262 100644 --- a/src/struphy/kinetic_background/tests/test_maxwellians.py +++ b/src/struphy/kinetic_background/tests/test_maxwellians.py @@ -287,10 +287,10 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): for key, val in inspect.getmembers(equils): if inspect.isclass(val) and val.__module__ == equils.__name__: - print(f"{key = }") + print(f"{key =}") if "DESCequilibrium" in key and not with_desc: - print(f"Attention: {with_desc = }, DESC not tested here !!") + print(f"Attention: {with_desc =}, DESC not tested here !!") continue if "GVECequilibrium" in key: @@ -298,16 +298,22 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): mhd_equil = val() assert isinstance(mhd_equil, FluidEquilibrium) - print(f"{mhd_equil.params = }") + print(f"{mhd_equil.params =}") if "AdhocTorus" in key: mhd_equil.domain = domains.HollowTorus( - a1=1e-3, a2=mhd_equil.params["a"], R0=mhd_equil.params["R0"], tor_period=1 + a1=1e-3, + a2=mhd_equil.params["a"], + R0=mhd_equil.params["R0"], + tor_period=1, ) elif "EQDSKequilibrium" in key: mhd_equil.domain = domains.Tokamak(equilibrium=mhd_equil) elif "CircularTokamak" in key: mhd_equil.domain = domains.HollowTorus( - a1=1e-3, a2=mhd_equil.params["a"], R0=mhd_equil.params["R0"], tor_period=1 + a1=1e-3, + a2=mhd_equil.params["a"], + R0=mhd_equil.params["R0"], + tor_period=1, ) elif "HomogenSlab" in key: mhd_equil.domain = domains.Cuboid() @@ -319,11 +325,15 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): ) elif "ShearFluid" in key: mhd_equil.domain = domains.Cuboid( - r1=mhd_equil.params["a"], r2=mhd_equil.params["b"], r3=mhd_equil.params["c"] + r1=mhd_equil.params["a"], + r2=mhd_equil.params["b"], + r3=mhd_equil.params["c"], ) elif "ScrewPinch" in key: mhd_equil.domain = domains.HollowCylinder( - a1=1e-3, a2=mhd_equil.params["a"], Lz=mhd_equil.params["R0"] * 2 * xp.pi + a1=1e-3, + a2=mhd_equil.params["a"], + Lz=mhd_equil.params["R0"] * 2 * xp.pi, ) else: try: @@ -354,11 +364,13 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): # test meshgrid evaluation n0 = mhd_equil.n0(*e_meshgrids) assert xp.allclose( - maxwellian(*meshgrids)[:, :, :, 0, 0, 0], n0 * maxwellian_1(*meshgrids)[:, :, :, 0, 0, 0] + maxwellian(*meshgrids)[:, :, :, 0, 0, 0], + n0 * maxwellian_1(*meshgrids)[:, :, :, 0, 0, 0], ) assert xp.allclose( - maxwellian(*meshgrids)[:, :, :, 0, 1, 2], n0 * maxwellian_1(*meshgrids)[:, :, :, 0, 1, 2] + maxwellian(*meshgrids)[:, :, :, 0, 1, 2], + n0 * maxwellian_1(*meshgrids)[:, :, :, 0, 1, 2], ) # test flat evaluation @@ -378,7 +390,7 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): # plotting moments if show_plot: - plt.figure(f"{mhd_equil = }", figsize=(24, 16)) + plt.figure(f"{mhd_equil =}", figsize=(24, 16)) x, y, z = mhd_equil.domain(*e_meshgrids) # density plots @@ -390,14 +402,20 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): if "Slab" in key or "Pinch" in key: plt.contourf(x[:, 0, :], z[:, 0, :], n_cart[:, 0, :], levels=levels) plt.contourf( - x[:, Nel[1] // 2, :], z[:, Nel[1] // 2 - 1, :], n_cart[:, Nel[1] // 2, :], levels=levels + x[:, Nel[1] // 2, :], + z[:, Nel[1] // 2 - 1, :], + n_cart[:, Nel[1] // 2, :], + levels=levels, ) plt.xlabel("x") plt.ylabel("z") else: plt.contourf(x[:, 0, :], y[:, 0, :], n_cart[:, 0, :], levels=levels) plt.contourf( - x[:, Nel[1] // 2, :], y[:, Nel[1] // 2 - 1, :], n_cart[:, Nel[1] // 2, :], levels=levels + x[:, Nel[1] // 2, :], + y[:, Nel[1] // 2 - 1, :], + n_cart[:, Nel[1] // 2, :], + levels=levels, ) plt.xlabel("x") plt.ylabel("y") @@ -459,14 +477,20 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): if "Slab" in key or "Pinch" in key: plt.contourf(x[:, 0, :], z[:, 0, :], vth_cart[:, 0, :], levels=levels) plt.contourf( - x[:, Nel[1] // 2, :], z[:, Nel[1] // 2 - 1, :], vth_cart[:, Nel[1] // 2, :], levels=levels + x[:, Nel[1] // 2, :], + z[:, Nel[1] // 2 - 1, :], + vth_cart[:, Nel[1] // 2, :], + levels=levels, ) plt.xlabel("x") plt.ylabel("z") else: plt.contourf(x[:, 0, :], y[:, 0, :], vth_cart[:, 0, :], levels=levels) plt.contourf( - x[:, Nel[1] // 2, :], y[:, Nel[1] // 2 - 1, :], vth_cart[:, Nel[1] // 2, :], levels=levels + x[:, Nel[1] // 2, :], + y[:, Nel[1] // 2 - 1, :], + vth_cart[:, Nel[1] // 2, :], + levels=levels, ) plt.xlabel("x") plt.ylabel("y") @@ -496,7 +520,7 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): if inspect.isclass(val_2) and val_2.__module__ == perturbations.__name__: pert = val_2() assert isinstance(pert, Perturbation) - print(f"{pert = }") + print(f"{pert =}") if isinstance(pert, perturbations.Noise): continue @@ -550,14 +574,20 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): if "Slab" in key or "Pinch" in key: plt.contourf(x[:, 0, :], z[:, 0, :], n_cart[:, 0, :], levels=levels) plt.contourf( - x[:, Nel[1] // 2, :], z[:, Nel[1] // 2, :], n_cart[:, Nel[1] // 2, :], levels=levels + x[:, Nel[1] // 2, :], + z[:, Nel[1] // 2, :], + n_cart[:, Nel[1] // 2, :], + levels=levels, ) plt.xlabel("x") plt.ylabel("z") else: plt.contourf(x[:, 0, :], y[:, 0, :], n_cart[:, 0, :], levels=levels) plt.contourf( - x[:, Nel[1] // 2, :], y[:, Nel[1] // 2, :], n_cart[:, Nel[1] // 2, :], levels=levels + x[:, Nel[1] // 2, :], + y[:, Nel[1] // 2, :], + n_cart[:, Nel[1] // 2, :], + levels=levels, ) plt.xlabel("x") plt.ylabel("y") @@ -586,14 +616,20 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): if "Slab" in key or "Pinch" in key: plt.contourf(x[:, 0, :], z[:, 0, :], u[:, 0, :], levels=levels) plt.contourf( - x[:, Nel[1] // 2, :], z[:, Nel[1] // 2, :], u[:, Nel[1] // 2, :], levels=levels + x[:, Nel[1] // 2, :], + z[:, Nel[1] // 2, :], + u[:, Nel[1] // 2, :], + levels=levels, ) plt.xlabel("x") plt.ylabel("z") else: plt.contourf(x[:, 0, :], y[:, 0, :], u[:, 0, :], levels=levels) plt.contourf( - x[:, Nel[1] // 2, :], y[:, Nel[1] // 2, :], u[:, Nel[1] // 2, :], levels=levels + x[:, Nel[1] // 2, :], + y[:, Nel[1] // 2, :], + u[:, Nel[1] // 2, :], + levels=levels, ) plt.xlabel("x") plt.ylabel("y") @@ -1047,10 +1083,10 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): for key, val in inspect.getmembers(equils): if inspect.isclass(val) and val.__module__ == equils.__name__: - print(f"{key = }") + print(f"{key =}") if "DESCequilibrium" in key and not with_desc: - print(f"Attention: {with_desc = }, DESC not tested here !!") + print(f"Attention: {with_desc =}, DESC not tested here !!") continue if "GVECequilibrium" in key: @@ -1060,16 +1096,22 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): if not isinstance(mhd_equil, FluidEquilibriumWithB): continue - print(f"{mhd_equil.params = }") + print(f"{mhd_equil.params =}") if "AdhocTorus" in key: mhd_equil.domain = domains.HollowTorus( - a1=1e-3, a2=mhd_equil.params["a"], R0=mhd_equil.params["R0"], tor_period=1 + a1=1e-3, + a2=mhd_equil.params["a"], + R0=mhd_equil.params["R0"], + tor_period=1, ) elif "EQDSKequilibrium" in key: mhd_equil.domain = domains.Tokamak(equilibrium=mhd_equil) elif "CircularTokamak" in key: mhd_equil.domain = domains.HollowTorus( - a1=1e-3, a2=mhd_equil.params["a"], R0=mhd_equil.params["R0"], tor_period=1 + a1=1e-3, + a2=mhd_equil.params["a"], + R0=mhd_equil.params["R0"], + tor_period=1, ) elif "HomogenSlab" in key: mhd_equil.domain = domains.Cuboid() @@ -1081,11 +1123,15 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): ) elif "ShearFluid" in key: mhd_equil.domain = domains.Cuboid( - r1=mhd_equil.params["a"], r2=mhd_equil.params["b"], r3=mhd_equil.params["c"] + r1=mhd_equil.params["a"], + r2=mhd_equil.params["b"], + r3=mhd_equil.params["c"], ) elif "ScrewPinch" in key: mhd_equil.domain = domains.HollowCylinder( - a1=1e-3, a2=mhd_equil.params["a"], Lz=mhd_equil.params["R0"] * 2 * xp.pi + a1=1e-3, + a2=mhd_equil.params["a"], + Lz=mhd_equil.params["R0"] * 2 * xp.pi, ) else: try: @@ -1135,7 +1181,7 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): # plotting moments if show_plot: - plt.figure(f"{mhd_equil = }", figsize=(24, 16)) + plt.figure(f"{mhd_equil =}", figsize=(24, 16)) x, y, z = mhd_equil.domain(*e_meshgrids) # density plots @@ -1147,14 +1193,20 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): if "Slab" in key or "Pinch" in key: plt.contourf(x[:, 0, :], z[:, 0, :], n_cart[:, 0, :], levels=levels) plt.contourf( - x[:, Nel[1] // 2, :], z[:, Nel[1] // 2 - 1, :], n_cart[:, Nel[1] // 2, :], levels=levels + x[:, Nel[1] // 2, :], + z[:, Nel[1] // 2 - 1, :], + n_cart[:, Nel[1] // 2, :], + levels=levels, ) plt.xlabel("x") plt.ylabel("z") else: plt.contourf(x[:, 0, :], y[:, 0, :], n_cart[:, 0, :], levels=levels) plt.contourf( - x[:, Nel[1] // 2, :], y[:, Nel[1] // 2 - 1, :], n_cart[:, Nel[1] // 2, :], levels=levels + x[:, Nel[1] // 2, :], + y[:, Nel[1] // 2 - 1, :], + n_cart[:, Nel[1] // 2, :], + levels=levels, ) plt.xlabel("x") plt.ylabel("y") @@ -1216,14 +1268,20 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): if "Slab" in key or "Pinch" in key: plt.contourf(x[:, 0, :], z[:, 0, :], vth_cart[:, 0, :], levels=levels) plt.contourf( - x[:, Nel[1] // 2, :], z[:, Nel[1] // 2 - 1, :], vth_cart[:, Nel[1] // 2, :], levels=levels + x[:, Nel[1] // 2, :], + z[:, Nel[1] // 2 - 1, :], + vth_cart[:, Nel[1] // 2, :], + levels=levels, ) plt.xlabel("x") plt.ylabel("z") else: plt.contourf(x[:, 0, :], y[:, 0, :], vth_cart[:, 0, :], levels=levels) plt.contourf( - x[:, Nel[1] // 2, :], y[:, Nel[1] // 2 - 1, :], vth_cart[:, Nel[1] // 2, :], levels=levels + x[:, Nel[1] // 2, :], + y[:, Nel[1] // 2 - 1, :], + vth_cart[:, Nel[1] // 2, :], + levels=levels, ) plt.xlabel("x") plt.ylabel("y") @@ -1250,7 +1308,7 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): for key_2, val_2 in inspect.getmembers(perturbations): if inspect.isclass(val_2) and val_2.__module__ == perturbations.__name__: pert = val_2() - print(f"{pert = }") + print(f"{pert =}") assert isinstance(pert, Perturbation) if isinstance(pert, perturbations.Noise): @@ -1301,14 +1359,20 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): if "Slab" in key or "Pinch" in key: plt.contourf(x[:, 0, :], z[:, 0, :], n_cart[:, 0, :], levels=levels) plt.contourf( - x[:, Nel[1] // 2, :], z[:, Nel[1] // 2, :], n_cart[:, Nel[1] // 2, :], levels=levels + x[:, Nel[1] // 2, :], + z[:, Nel[1] // 2, :], + n_cart[:, Nel[1] // 2, :], + levels=levels, ) plt.xlabel("x") plt.ylabel("z") else: plt.contourf(x[:, 0, :], y[:, 0, :], n_cart[:, 0, :], levels=levels) plt.contourf( - x[:, Nel[1] // 2, :], y[:, Nel[1] // 2, :], n_cart[:, Nel[1] // 2, :], levels=levels + x[:, Nel[1] // 2, :], + y[:, Nel[1] // 2, :], + n_cart[:, Nel[1] // 2, :], + levels=levels, ) plt.xlabel("x") plt.ylabel("y") @@ -1337,14 +1401,20 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): if "Slab" in key or "Pinch" in key: plt.contourf(x[:, 0, :], z[:, 0, :], u[:, 0, :], levels=levels) plt.contourf( - x[:, Nel[1] // 2, :], z[:, Nel[1] // 2, :], u[:, Nel[1] // 2, :], levels=levels + x[:, Nel[1] // 2, :], + z[:, Nel[1] // 2, :], + u[:, Nel[1] // 2, :], + levels=levels, ) plt.xlabel("x") plt.ylabel("z") else: plt.contourf(x[:, 0, :], y[:, 0, :], u[:, 0, :], levels=levels) plt.contourf( - x[:, Nel[1] // 2, :], y[:, Nel[1] // 2, :], u[:, Nel[1] // 2, :], levels=levels + x[:, Nel[1] // 2, :], + y[:, Nel[1] // 2, :], + u[:, Nel[1] // 2, :], + levels=levels, ) plt.xlabel("x") plt.ylabel("y") diff --git a/src/struphy/linear_algebra/linalg_kron.py b/src/struphy/linear_algebra/linalg_kron.py index 2e0dd57dc..fedd05979 100644 --- a/src/struphy/linear_algebra/linalg_kron.py +++ b/src/struphy/linear_algebra/linalg_kron.py @@ -82,8 +82,9 @@ def kron_matvec_3d(kmat, vec3d): ( kmat[2].dot( ((kmat[1].dot(((kmat[0].dot(vec3d.reshape(v0, v1 * v2))).T).reshape(v1, v2 * k0))).T).reshape( - v2, k0 * k1 - ) + v2, + k0 * k1, + ), ) ).T ).reshape(k0, k1, k2) @@ -278,8 +279,9 @@ def kron_lusolve_3d(kmatlu, rhs): ( kmatlu[2].solve( ((kmatlu[1].solve(((kmatlu[0].solve(rhs.reshape(r0, r1 * r2))).T).reshape(r1, r2 * r0))).T).reshape( - r2, r0 * r1 - ) + r2, + r0 * r1, + ), ) ).T ).reshape(r0, r1, r2) @@ -320,7 +322,7 @@ def kron_solve_3d(kmat, rhs): splu(kmat[2]).solve( ( (splu(kmat[1]).solve(((splu(kmat[0]).solve(rhs.reshape(r0, r1 * r2))).T).reshape(r1, r2 * r0))).T - ).reshape(r2, r0 * r1) + ).reshape(r2, r0 * r1), ) ).T ).reshape(r0, r1, r2) @@ -361,7 +363,8 @@ def kron_fftsolve_3d(cvec, rhs): ( ( solve_circulant( - cvec[1], ((solve_circulant(cvec[0], rhs.reshape(r0, r1 * r2))).T).reshape(r1, r2 * r0) + cvec[1], + ((solve_circulant(cvec[0], rhs.reshape(r0, r1 * r2))).T).reshape(r1, r2 * r0), ) ).T ).reshape(r2, r0 * r1), diff --git a/src/struphy/linear_algebra/saddle_point.py b/src/struphy/linear_algebra/saddle_point.py index 47dd36190..3a191cde4 100644 --- a/src/struphy/linear_algebra/saddle_point.py +++ b/src/struphy/linear_algebra/saddle_point.py @@ -413,7 +413,10 @@ def _setup_inverses(self): # === Inverse for A[1] if hasattr(self, "_Aenpinv") and self._is_inverse_still_valid( - self._Aenpinv, A1, "A[1]", pre=self._A22npinv + self._Aenpinv, + A1, + "A[1]", + pre=self._A22npinv, ): pass else: @@ -497,7 +500,7 @@ def _spectral_analysis(self): # print(f'{minbeforeA11_abs = }') # print(f'{minbeforeA11 = }') # print(f'{specA11_bef = }') - print(f"{specA11_bef_abs = }") + print(f"{specA11_bef_abs =}") # A22 before if self._method_to_solve in ("DirectNPInverse", "InexactNPInverse"): @@ -517,8 +520,8 @@ def _spectral_analysis(self): # print(f'{minbeforeA22_abs = }') # print(f'{minbeforeA22 = }') # print(f'{specA22_bef = }') - print(f"{specA22_bef_abs = }") - print(f"{condA22_before = }") + print(f"{specA22_bef_abs =}") + print(f"{condA22_before =}") if self._preconditioner == True: # A11 after preconditioning with its inverse @@ -537,7 +540,7 @@ def _spectral_analysis(self): # print(f'{minafterA11_abs_prec = }') # print(f'{minafterA11_prec = }') # print(f'{specA11_aft_prec = }') - print(f"{specA11_aft_abs_prec = }") + print(f"{specA11_aft_abs_prec =}") # A22 after preconditioning with its inverse if self._method_to_solve in ("DirectNPInverse", "InexactNPInverse"): @@ -557,7 +560,7 @@ def _spectral_analysis(self): # print(f'{minafterA22_abs_prec = }') # print(f'{minafterA22_prec = }') # print(f'{specA22_aft_prec = }') - print(f"{specA22_aft_abs_prec = }") + print(f"{specA22_aft_abs_prec =}") return condA22_before, specA22_bef_abs, condA11_before, condA22_after, specA22_aft_abs_prec diff --git a/src/struphy/linear_algebra/tests/test_saddle_point_propagator.py b/src/struphy/linear_algebra/tests/test_saddle_point_propagator.py index 06482f958..3aa3f4ab0 100644 --- a/src/struphy/linear_algebra/tests/test_saddle_point_propagator.py +++ b/src/struphy/linear_algebra/tests/test_saddle_point_propagator.py @@ -306,7 +306,7 @@ def test_propagator2D(Nel, p, spl_kind, dirichlet_bc, mapping, epsilon, dt): "ManufacturedSolutionPotential": { "given_in_basis": "physical", "dimension": "2D", - } + }, } uvec.initialize_coeffs(domain=domain, pert_params=pp_u) diff --git a/src/struphy/linear_algebra/tests/test_saddlepoint_massmatrices.py b/src/struphy/linear_algebra/tests/test_saddlepoint_massmatrices.py index ed5ff4d8a..42d3ae8d3 100644 --- a/src/struphy/linear_algebra/tests/test_saddlepoint_massmatrices.py +++ b/src/struphy/linear_algebra/tests/test_saddlepoint_massmatrices.py @@ -323,7 +323,7 @@ def test_saddlepointsolver(method_for_solving, Nel, p, spl_kind, dirichlet_bc, m compare_arrays(y1_rdm, y_uzawa, mpi_rank, atol=1e-5) compare_arrays(x1, x_uzawa[0], mpi_rank, atol=1e-5) compare_arrays(x2, x_uzawa[1], mpi_rank, atol=1e-5) - print(f"{info = }") + print(f"{info =}") elif isinstance(x_uzawa[0], BlockVector): # Output as Blockvector Rx1 = x1 - x_uzawa[0] diff --git a/src/struphy/main.py b/src/struphy/main.py index 524e19eb7..4b7b65645 100644 --- a/src/struphy/main.py +++ b/src/struphy/main.py @@ -338,7 +338,8 @@ def run( t1 = time.time() if rank == 0 and verbose: message = "Particles sorted | wall clock [s]: {0:8.4f} | sorting duration [s]: {1:8.4f}".format( - run_time_now * 60, t1 - t0 + run_time_now * 60, + t1 - t0, ) print(message, end="\n") print() @@ -383,10 +384,12 @@ def run( message = "time step: " + step + "/" + str(total_steps) message += " | " + "time: {0:10.5f}/{1:10.5f}".format(time_state["value"][0], Tend) message += " | " + "phys. time [s]: {0:12.10f}/{1:12.10f}".format( - time_state["value_sec"][0], Tend * model.units.t + time_state["value_sec"][0], + Tend * model.units.t, ) message += " | " + "wall clock [s]: {0:8.4f} | last step duration [s]: {1:8.4f}".format( - run_time_now * 60, t1 - t0 + run_time_now * 60, + t1 - t0, ) print(message, end="\n") @@ -513,7 +516,10 @@ def pproc( if physical: point_data_phy, grids_log, grids_phy = eval_femfields( - params_in, fields, celldivide=[celldivide] * 3, physical=True + params_in, + fields, + celldivide=[celldivide] * 3, + physical=True, ) # directory for field data @@ -699,7 +705,7 @@ def load_data(path: str) -> SimData: path_pproc = os.path.join(path, "post_processing") assert os.path.exists(path_pproc), f"Path {path_pproc} does not exist, run 'pproc' first?" print("\n*** Loading post-processed simulation data:") - print(f"{path = }") + print(f"{path =}") simdata = SimData(path) @@ -738,7 +744,7 @@ def load_data(path: str) -> SimData: if os.path.exists(path_kinetic): # species folders species = next(os.walk(path_kinetic))[1] - print(f"{species = }") + print(f"{species =}") for spec in species: path_spec = os.path.join(path_kinetic, spec) wlk = os.walk(path_spec) @@ -792,20 +798,20 @@ def load_data(path: str) -> SimData: simdata._n_sph[spec][sli][name] = tmp else: - print(f"{folder = }") + print(f"{folder =}") raise NotImplementedError print("\nThe following data has been loaded:") print(f"\ngrids:") - print(f"{simdata.t_grid.shape = }") + print(f"{simdata.t_grid.shape =}") if simdata.grids_log is not None: - print(f"{simdata.grids_log[0].shape = }") - print(f"{simdata.grids_log[1].shape = }") - print(f"{simdata.grids_log[2].shape = }") + print(f"{simdata.grids_log[0].shape =}") + print(f"{simdata.grids_log[1].shape =}") + print(f"{simdata.grids_log[2].shape =}") if simdata.grids_phy is not None: - print(f"{simdata.grids_phy[0].shape = }") - print(f"{simdata.grids_phy[1].shape = }") - print(f"{simdata.grids_phy[2].shape = }") + print(f"{simdata.grids_phy[0].shape =}") + print(f"{simdata.grids_phy[1].shape =}") + print(f"{simdata.grids_phy[2].shape =}") print(f"\nsimdata.spline_values:") for k, v in simdata.spline_values.items(): print(f" {k}") diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index d18942154..e6eb5b665 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -174,7 +174,7 @@ def allocate_feec(self, grid: TensorProductGrid, derham_opts: DerhamOptions): if grid is None or derham_opts is None: if MPI.COMM_WORLD.Get_rank() == 0: - print(f"\n{grid = }, {derham_opts = }: no Derham object set up.") + print(f"\n{grid =}, {derham_opts =}: no Derham object set up.") self._derham = None else: self._derham = setup_derham( @@ -472,7 +472,7 @@ def add_scalar(self, name: str, variable: PICVariable | SPHVariable = None, comp assert isinstance(name, str), "name must be a string" if compute == "from_particles": - assert isinstance(variable, (PICVariable, SPHVariable)), f"Variable is needed when {compute = }" + assert isinstance(variable, (PICVariable, SPHVariable)), f"Variable is needed when {compute =}" if not hasattr(self, "_scalar_quantities"): self._scalar_quantities = {} @@ -1172,7 +1172,7 @@ def show_options(cls): print( 'Options are given under the keyword "options" for each species dict. \ -Available options stand in lists as dict values.\nThe first entry of a list denotes the default value.' +Available options stand in lists as dict values.\nThe first entry of a list denotes the default value.', ) tab = " " @@ -1408,7 +1408,7 @@ def generate_default_parameter_file( BoundaryParameters,\n\ BinningPlot,\n\ KernelDensityPlot,\n\ - )\n" + )\n", ) file.write("from struphy import main\n") @@ -1484,14 +1484,14 @@ def generate_default_parameter_file( grid=grid,\n\ derham_opts=derham_opts,\n\ verbose=verbose,\n\ - )" + )", ) file.close() print( f"\nDefault parameter file for '{self.__class__.__name__}' has been created in the cwd ({path}).\n\ -You can now launch a simulation with 'python params_{self.__class__.__name__}.py'" +You can now launch a simulation with 'python params_{self.__class__.__name__}.py'", ) return path diff --git a/src/struphy/models/fluid.py b/src/struphy/models/fluid.py index 2b832ba00..a4916e39d 100644 --- a/src/struphy/models/fluid.py +++ b/src/struphy/models/fluid.py @@ -157,7 +157,7 @@ def generate_default_parameter_file(self, path=None, prompt=True): for line in f: if "mag_sonic.Options" in line: new_file += [ - "model.propagators.mag_sonic.options = model.propagators.mag_sonic.Options(b_field=model.em_fields.b_field)\n" + "model.propagators.mag_sonic.options = model.propagators.mag_sonic.Options(b_field=model.em_fields.b_field)\n", ] else: new_file += [line] @@ -332,7 +332,7 @@ def generate_default_parameter_file(self, path=None, prompt=True): for line in f: if "hall.Options" in line: new_file += [ - "model.propagators.hall.options = model.propagators.hall.Options(epsilon_from=model.mhd)\n" + "model.propagators.hall.options = model.propagators.hall.Options(epsilon_from=model.mhd)\n", ] else: new_file += [line] @@ -652,25 +652,25 @@ def generate_default_parameter_file(self, path=None, prompt=True): for line in f: if "variat_dens.Options" in line: new_file += [ - "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='full',\n" + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='full',\n", ] new_file += [ - " s=model.mhd.entropy)\n" + " s=model.mhd.entropy)\n", ] elif "variat_ent.Options" in line: new_file += [ - "model.propagators.variat_ent.options = model.propagators.variat_ent.Options(model='full',\n" + "model.propagators.variat_ent.options = model.propagators.variat_ent.Options(model='full',\n", ] new_file += [ - " rho=model.mhd.density)\n" + " rho=model.mhd.density)\n", ] elif "variat_viscous.Options" in line: new_file += [ - "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(rho=model.mhd.density)\n" + "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(rho=model.mhd.density)\n", ] elif "variat_resist.Options" in line: new_file += [ - "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(rho=model.mhd.density)\n" + "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(rho=model.mhd.density)\n", ] elif "entropy.add_background" in line: new_file += ["model.mhd.density.add_background(FieldsBackground())\n"] @@ -842,21 +842,21 @@ def generate_default_parameter_file(self, path=None, prompt=True): for line in f: if "variat_dens.Options" in line: new_file += [ - "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='full',\n" + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='full',\n", ] new_file += [ - " s=model.fluid.entropy)\n" + " s=model.fluid.entropy)\n", ] elif "variat_ent.Options" in line: new_file += [ - "model.propagators.variat_ent.options = model.propagators.variat_ent.Options(model='full',\n" + "model.propagators.variat_ent.options = model.propagators.variat_ent.Options(model='full',\n", ] new_file += [ - " rho=model.fluid.density)\n" + " rho=model.fluid.density)\n", ] elif "variat_viscous.Options" in line: new_file += [ - "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(rho=model.fluid.density)\n" + "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(rho=model.fluid.density)\n", ] elif "entropy.add_background" in line: new_file += ["model.fluid.density.add_background(FieldsBackground())\n"] @@ -1042,18 +1042,18 @@ def generate_default_parameter_file(self, path=None, prompt=True): for line in f: if "variat_pb.Options" in line: new_file += [ - "model.propagators.variat_pb.options = model.propagators.variat_pb.Options(div_u=model.diagnostics.div_u,\n" + "model.propagators.variat_pb.options = model.propagators.variat_pb.Options(div_u=model.diagnostics.div_u,\n", ] new_file += [ - " u2=model.diagnostics.u2)\n" + " u2=model.diagnostics.u2)\n", ] elif "variat_viscous.Options" in line: new_file += [ - "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(rho=model.mhd.density)\n" + "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(rho=model.mhd.density)\n", ] elif "variat_resist.Options" in line: new_file += [ - "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(rho=model.mhd.density)\n" + "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(rho=model.mhd.density)\n", ] elif "pressure.add_background" in line: new_file += ["model.mhd.density.add_background(FieldsBackground())\n"] @@ -1255,40 +1255,40 @@ def generate_default_parameter_file(self, path=None, prompt=True): for line in f: if "variat_dens.Options" in line: new_file += [ - "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='linear')\n" + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='linear')\n", ] elif "variat_pb.Options" in line: new_file += [ - "model.propagators.variat_pb.options = model.propagators.variat_pb.Options(model='linear',\n" + "model.propagators.variat_pb.options = model.propagators.variat_pb.Options(model='linear',\n", ] new_file += [ - " div_u=model.diagnostics.div_u,\n" + " div_u=model.diagnostics.div_u,\n", ] new_file += [ - " u2=model.diagnostics.u2,\n" + " u2=model.diagnostics.u2,\n", ] new_file += [ - " pt3=model.diagnostics.pt3,\n" + " pt3=model.diagnostics.pt3,\n", ] new_file += [ - " bt2=model.diagnostics.bt2)\n" + " bt2=model.diagnostics.bt2)\n", ] elif "variat_viscous.Options" in line: new_file += [ - "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(model='linear_p',\n" + "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(model='linear_p',\n", ] new_file += [ - " rho=model.mhd.density)\n" + " rho=model.mhd.density)\n", ] elif "variat_resist.Options" in line: new_file += [ - "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(model='linear_p',\n" + "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(model='linear_p',\n", ] new_file += [ - " rho=model.mhd.density,\n" + " rho=model.mhd.density,\n", ] new_file += [ - " pt3=model.diagnostics.pt3)\n" + " pt3=model.diagnostics.pt3)\n", ] elif "pressure.add_background" in line: new_file += ["model.mhd.density.add_background(FieldsBackground())\n"] @@ -1493,31 +1493,31 @@ def generate_default_parameter_file(self, path=None, prompt=True): for line in f: if "variat_dens.Options" in line: new_file += [ - "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='deltaf')\n" + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='deltaf')\n", ] elif "variat_pb.Options" in line: new_file += [ - "model.propagators.variat_pb.options = model.propagators.variat_pb.Options(model='deltaf',\n" + "model.propagators.variat_pb.options = model.propagators.variat_pb.Options(model='deltaf',\n", ] new_file += [ - " pt3=model.diagnostics.pt3,\n" + " pt3=model.diagnostics.pt3,\n", ] new_file += [ - " bt2=model.diagnostics.bt2)\n" + " bt2=model.diagnostics.bt2)\n", ] elif "variat_viscous.Options" in line: new_file += [ - "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(model='full_p',\n" + "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(model='full_p',\n", ] new_file += [ - " rho=model.mhd.density)\n" + " rho=model.mhd.density)\n", ] elif "variat_resist.Options" in line: new_file += [ - "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(model='full_p',\n" + "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(model='full_p',\n", ] new_file += [ - " rho=model.mhd.density)\n" + " rho=model.mhd.density)\n", ] elif "pressure.add_background" in line: new_file += ["model.mhd.density.add_background(FieldsBackground())\n"] @@ -1705,25 +1705,25 @@ def generate_default_parameter_file(self, path=None, prompt=True): for line in f: if "variat_dens.Options" in line: new_file += [ - "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='full_q')\n" + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='full_q')\n", ] elif "variat_qb.Options" in line: new_file += [ - "model.propagators.variat_qb.options = model.propagators.variat_qb.Options(model='full_q')\n" + "model.propagators.variat_qb.options = model.propagators.variat_qb.Options(model='full_q')\n", ] elif "variat_viscous.Options" in line: new_file += [ - "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(model='full_q',\n" + "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(model='full_q',\n", ] new_file += [ - " rho=model.mhd.density)\n" + " rho=model.mhd.density)\n", ] elif "variat_resist.Options" in line: new_file += [ - "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(model='full_q',\n" + "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(model='full_q',\n", ] new_file += [ - " rho=model.mhd.density)\n" + " rho=model.mhd.density)\n", ] elif "sqrt_p.add_background" in line: new_file += ["model.mhd.density.add_background(FieldsBackground())\n"] @@ -1909,43 +1909,43 @@ def generate_default_parameter_file(self, path=None, prompt=True): for line in f: if "variat_dens.Options" in line: new_file += [ - "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='linear_q')\n" + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='linear_q')\n", ] elif "variat_qb.Options" in line: new_file += [ - "model.propagators.variat_qb.options = model.propagators.variat_qb.Options(model='linear_q',\n" + "model.propagators.variat_qb.options = model.propagators.variat_qb.Options(model='linear_q',\n", ] new_file += [ - " div_u=model.diagnostics.div_u,\n" + " div_u=model.diagnostics.div_u,\n", ] new_file += [ - " u2=model.diagnostics.u2,\n" + " u2=model.diagnostics.u2,\n", ] new_file += [ - " qt3=model.diagnostics.qt3,\n" + " qt3=model.diagnostics.qt3,\n", ] new_file += [ - " bt2=model.diagnostics.bt2)\n" + " bt2=model.diagnostics.bt2)\n", ] elif "variat_viscous.Options" in line: new_file += [ - "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(model='linear_q',\n" + "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(model='linear_q',\n", ] new_file += [ - " rho=model.mhd.density,\n" + " rho=model.mhd.density,\n", ] new_file += [ - " pt3=model.diagnostics.qt3)\n" + " pt3=model.diagnostics.qt3)\n", ] elif "variat_resist.Options" in line: new_file += [ - "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(model='linear_q',\n" + "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(model='linear_q',\n", ] new_file += [ - " rho=model.mhd.density,\n" + " rho=model.mhd.density,\n", ] new_file += [ - " pt3=model.diagnostics.qt3)\n" + " pt3=model.diagnostics.qt3)\n", ] elif "sqrt_p.add_background" in line: new_file += ["model.mhd.density.add_background(FieldsBackground())\n"] @@ -2134,43 +2134,43 @@ def generate_default_parameter_file(self, path=None, prompt=True): for line in f: if "variat_dens.Options" in line: new_file += [ - "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='deltaf_q')\n" + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='deltaf_q')\n", ] elif "variat_qb.Options" in line: new_file += [ - "model.propagators.variat_qb.options = model.propagators.variat_qb.Options(model='deltaf_q',\n" + "model.propagators.variat_qb.options = model.propagators.variat_qb.Options(model='deltaf_q',\n", ] new_file += [ - " div_u=model.diagnostics.div_u,\n" + " div_u=model.diagnostics.div_u,\n", ] new_file += [ - " u2=model.diagnostics.u2,\n" + " u2=model.diagnostics.u2,\n", ] new_file += [ - " qt3=model.diagnostics.qt3,\n" + " qt3=model.diagnostics.qt3,\n", ] new_file += [ - " bt2=model.diagnostics.bt2)\n" + " bt2=model.diagnostics.bt2)\n", ] elif "variat_viscous.Options" in line: new_file += [ - "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(model='deltaf_q',\n" + "model.propagators.variat_viscous.options = model.propagators.variat_viscous.Options(model='deltaf_q',\n", ] new_file += [ - " rho=model.mhd.density,\n" + " rho=model.mhd.density,\n", ] new_file += [ - " pt3=model.diagnostics.qt3)\n" + " pt3=model.diagnostics.qt3)\n", ] elif "variat_resist.Options" in line: new_file += [ - "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(model='deltaf_q',\n" + "model.propagators.variat_resist.options = model.propagators.variat_resist.Options(model='deltaf_q',\n", ] new_file += [ - " rho=model.mhd.density,\n" + " rho=model.mhd.density,\n", ] new_file += [ - " pt3=model.diagnostics.qt3)\n" + " pt3=model.diagnostics.qt3)\n", ] elif "sqrt_p.add_background" in line: new_file += ["model.mhd.density.add_background(FieldsBackground())\n"] @@ -2280,7 +2280,7 @@ def update_scalar_quantities(self): particles = self.euler_fluid.var.particles valid_markers = particles.markers_wo_holes_and_ghost en_kin = valid_markers[:, 6].dot( - valid_markers[:, 3] ** 2 + valid_markers[:, 4] ** 2 + valid_markers[:, 5] ** 2 + valid_markers[:, 3] ** 2 + valid_markers[:, 4] ** 2 + valid_markers[:, 5] ** 2, ) / (2.0 * particles.Np) self.update_scalar("en_kin", en_kin) @@ -2425,7 +2425,7 @@ def generate_default_parameter_file(self, path=None, prompt=True): for line in f: if "hw.Options" in line: new_file += [ - "model.propagators.hw.options = model.propagators.hw.Options(phi=model.em_fields.phi)\n" + "model.propagators.hw.options = model.propagators.hw.Options(phi=model.em_fields.phi)\n", ] elif "vorticity.add_background" in line: new_file += ["model.plasma.density.add_background(FieldsBackground())\n"] diff --git a/src/struphy/models/hybrid.py b/src/struphy/models/hybrid.py index 6994534f9..bcc9f6492 100644 --- a/src/struphy/models/hybrid.py +++ b/src/struphy/models/hybrid.py @@ -211,18 +211,18 @@ def generate_default_parameter_file(self, path=None, prompt=True): for line in f: if "mag_sonic.Options" in line: new_file += [ - "model.propagators.mag_sonic.options = model.propagators.mag_sonic.Options(b_field=model.em_fields.b_field)\n" + "model.propagators.mag_sonic.options = model.propagators.mag_sonic.Options(b_field=model.em_fields.b_field)\n", ] elif "couple_dens.Options" in line: new_file += [ - "model.propagators.couple_dens.options = model.propagators.couple_dens.Options(energetic_ions=model.energetic_ions.var,\n" + "model.propagators.couple_dens.options = model.propagators.couple_dens.Options(energetic_ions=model.energetic_ions.var,\n", ] new_file += [ - " b_tilde=model.em_fields.b_field)\n" + " b_tilde=model.em_fields.b_field)\n", ] elif "couple_curr.Options" in line: new_file += [ - "model.propagators.couple_curr.options = model.propagators.couple_curr.Options(b_tilde=model.em_fields.b_field)\n" + "model.propagators.couple_curr.options = model.propagators.couple_curr.Options(b_tilde=model.em_fields.b_field)\n", ] elif "set_save_data" in line: new_file += ["\nbinplot = BinningPlot(slice='e1', n_bins=128, ranges=(0.0, 1.0))\n"] @@ -418,7 +418,7 @@ def update_scalar_quantities(self): particles.markers[~particles.holes, 6].dot( particles.markers[~particles.holes, 3] ** 2 + particles.markers[~particles.holes, 4] ** 2 - + particles.markers[~particles.holes, 5] ** 2 + + particles.markers[~particles.holes, 5] ** 2, ) / 2.0 * Ah @@ -461,19 +461,19 @@ def generate_default_parameter_file(self, path=None, prompt=True): if "magnetosonic.Options" in line: new_file += [ """model.propagators.magnetosonic.options = model.propagators.magnetosonic.Options( - b_field=model.em_fields.b_field,)\n""" + b_field=model.em_fields.b_field,)\n""", ] elif "push_eta_pc.Options" in line: new_file += [ """model.propagators.push_eta_pc.options = model.propagators.push_eta_pc.Options( - u_tilde = model.mhd.velocity,)\n""" + u_tilde = model.mhd.velocity,)\n""", ] elif "push_vxb.Options" in line: new_file += [ """model.propagators.push_vxb.options = model.propagators.push_vxb.Options( - b2_var = model.em_fields.b_field,)\n""" + b2_var = model.em_fields.b_field,)\n""", ] else: @@ -743,44 +743,44 @@ def generate_default_parameter_file(self, path=None, prompt=True): if "shearalfen_cc5d.Options" in line: new_file += [ """model.propagators.shearalfen_cc5d.options = model.propagators.shearalfen_cc5d.Options( - energetic_ions = model.energetic_ions.var,)\n""" + energetic_ions = model.energetic_ions.var,)\n""", ] elif "magnetosonic.Options" in line: new_file += [ """model.propagators.magnetosonic.options = model.propagators.magnetosonic.Options( - b_field=model.em_fields.b_field,)\n""" + b_field=model.em_fields.b_field,)\n""", ] elif "cc5d_density.Options" in line: new_file += [ """model.propagators.cc5d_density.options = model.propagators.cc5d_density.Options( energetic_ions = model.energetic_ions.var, - b_tilde = model.em_fields.b_field,)\n""" + b_tilde = model.em_fields.b_field,)\n""", ] elif "cc5d_curlb.Options" in line: new_file += [ """model.propagators.cc5d_curlb.options = model.propagators.cc5d_curlb.Options( - b_tilde = model.em_fields.b_field,)\n""" + b_tilde = model.em_fields.b_field,)\n""", ] elif "cc5d_gradb.Options" in line: new_file += [ """model.propagators.cc5d_gradb.options = model.propagators.cc5d_gradb.Options( - b_tilde = model.em_fields.b_field,)\n""" + b_tilde = model.em_fields.b_field,)\n""", ] elif "push_bxe.Options" in line: new_file += [ """model.propagators.push_bxe.options = model.propagators.push_bxe.Options( - b_tilde = model.em_fields.b_field,)\n""" + b_tilde = model.em_fields.b_field,)\n""", ] elif "push_parallel.Options" in line: new_file += [ """model.propagators.push_parallel.options = model.propagators.push_parallel.Options( - b_tilde = model.em_fields.b_field,)\n""" + b_tilde = model.em_fields.b_field,)\n""", ] else: diff --git a/src/struphy/models/kinetic.py b/src/struphy/models/kinetic.py index 9a01e38fd..3f327e9b2 100644 --- a/src/struphy/models/kinetic.py +++ b/src/struphy/models/kinetic.py @@ -477,7 +477,7 @@ def generate_default_parameter_file(self, path=None, prompt=True): new_file += ["model.initial_poisson.options = model.initial_poisson.Options()\n"] elif "push_vxb.Options" in line: new_file += [ - "model.propagators.push_vxb.options = model.propagators.push_vxb.Options(b2_var=model.em_fields.b_field)\n" + "model.propagators.push_vxb.options = model.propagators.push_vxb.Options(b2_var=model.em_fields.b_field)\n", ] elif "set_save_data" in line: new_file += ["\nbinplot = BinningPlot(slice='e1', n_bins=128, ranges=(0.0, 1.0))\n"] @@ -1079,11 +1079,11 @@ def generate_default_parameter_file(self, path=None, prompt=True): new_file += ["base_units = BaseUnits(kBT=1.0)\n"] elif "push_gc_bxe.Options" in line: new_file += [ - "model.propagators.push_gc_bxe.options = model.propagators.push_gc_bxe.Options(phi=model.em_fields.phi)\n" + "model.propagators.push_gc_bxe.options = model.propagators.push_gc_bxe.Options(phi=model.em_fields.phi)\n", ] elif "push_gc_para.Options" in line: new_file += [ - "model.propagators.push_gc_para.options = model.propagators.push_gc_para.Options(phi=model.em_fields.phi)\n" + "model.propagators.push_gc_para.options = model.propagators.push_gc_para.Options(phi=model.em_fields.phi)\n", ] elif "set_save_data" in line: new_file += ["\nbinplot = BinningPlot(slice='e1', n_bins=128, ranges=(0.0, 1.0))\n"] diff --git a/src/struphy/models/species.py b/src/struphy/models/species.py index 56a338ca9..8a9f11008 100644 --- a/src/struphy/models/species.py +++ b/src/struphy/models/species.py @@ -91,21 +91,21 @@ def __init__( else: self.alpha = alpha if MPI.COMM_WORLD.Get_rank() == 0: - warnings.warn(f"Override equation parameter {self.alpha = }") + warnings.warn(f"Override equation parameter {self.alpha =}") if epsilon is None: self.epsilon = 1.0 / (om_c * units.t) else: self.epsilon = epsilon if MPI.COMM_WORLD.Get_rank() == 0: - warnings.warn(f"Override equation parameter {self.epsilon = }") + warnings.warn(f"Override equation parameter {self.epsilon =}") if kappa is None: self.kappa = om_p * units.t else: self.kappa = kappa if MPI.COMM_WORLD.Get_rank() == 0: - warnings.warn(f"Override equation parameter {self.kappa = }") + warnings.warn(f"Override equation parameter {self.kappa =}") if verbose and MPI.COMM_WORLD.Get_rank() == 0: print(f"\nSet normalization parameters for species {species.__class__.__name__}:") diff --git a/src/struphy/models/tests/test_models.py b/src/struphy/models/tests/test_models.py index 36a9ea01b..b9802abdc 100644 --- a/src/struphy/models/tests/test_models.py +++ b/src/struphy/models/tests/test_models.py @@ -19,28 +19,28 @@ if inspect.isclass(obj) and "models.toy" in obj.__module__: toy_models += [name] if rank == 0: - print(f"\n{toy_models = }") + print(f"\n{toy_models =}") fluid_models = [] for name, obj in inspect.getmembers(fluid): if inspect.isclass(obj) and "models.fluid" in obj.__module__: fluid_models += [name] if rank == 0: - print(f"\n{fluid_models = }") + print(f"\n{fluid_models =}") kinetic_models = [] for name, obj in inspect.getmembers(kinetic): if inspect.isclass(obj) and "models.kinetic" in obj.__module__: kinetic_models += [name] if rank == 0: - print(f"\n{kinetic_models = }") + print(f"\n{kinetic_models =}") hybrid_models = [] for name, obj in inspect.getmembers(hybrid): if inspect.isclass(obj) and "models.hybrid" in obj.__module__: hybrid_models += [name] if rank == 0: - print(f"\n{hybrid_models = }") + print(f"\n{hybrid_models =}") # folder for test simulations @@ -54,7 +54,7 @@ def call_test(model_name: str, module: ModuleType = None, verbose=True): # exceptions if model_name == "TwoFluidQuasiNeutralToy" and MPI.COMM_WORLD.Get_size() > 1: - print(f"WARNING: Model {model_name} cannot be tested for {MPI.COMM_WORLD.Get_size() = }") + print(f"WARNING: Model {model_name} cannot be tested for {MPI.COMM_WORLD.Get_size() =}") return if module is None: diff --git a/src/struphy/models/tests/test_verif_EulerSPH.py b/src/struphy/models/tests/test_verif_EulerSPH.py index 79a248ac9..48eb8a7a8 100644 --- a/src/struphy/models/tests/test_verif_EulerSPH.py +++ b/src/struphy/models/tests/test_verif_EulerSPH.py @@ -133,7 +133,7 @@ def test_soundwave_1d(nx: int, plot_pts: int, do_plot: bool = False): plot_ct = 0 for i in range(0, Nt + 1): if i % interval == 0: - print(f"{i = }") + print(f"{i =}") plot_ct += 1 ax = plt.gca() @@ -150,14 +150,14 @@ def test_soundwave_1d(nx: int, plot_pts: int, do_plot: bool = False): plt.xlabel("x") plt.ylabel(r"$\rho$") - plt.title(f"standing sound wave ($c_s = 1$) for {nx = } and {ppb = }") + plt.title(f"standing sound wave ($c_s = 1$) for {nx =} and {ppb =}") if plot_ct == 11: break plt.show() error = xp.max(xp.abs(n_sph[0] - n_sph[-1])) - print(f"SPH sound wave {error = }.") + print(f"SPH sound wave {error =}.") assert error < 6e-4 print("Assertion passed.") diff --git a/src/struphy/models/tests/test_verif_LinearMHD.py b/src/struphy/models/tests/test_verif_LinearMHD.py index 5cbbbc9fd..475b11aef 100644 --- a/src/struphy/models/tests/test_verif_LinearMHD.py +++ b/src/struphy/models/tests/test_verif_LinearMHD.py @@ -115,7 +115,7 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): # assert vA = xp.sqrt(Bsquare / n0) v_alfven = vA * B0z / xp.sqrt(Bsquare) - print(f"{v_alfven = }") + print(f"{v_alfven =}") assert xp.abs(coeffs[0][0] - v_alfven) < 0.07 # second fft @@ -144,8 +144,8 @@ def test_slab_waves_1d(algo: str, do_plot: bool = False): delta = (4 * B0z**2 * cS**2 * vA**2) / ((cS**2 + vA**2) ** 2 * Bsquare) v_slow = xp.sqrt(1 / 2 * (cS**2 + vA**2) * (1 - xp.sqrt(1 - delta))) v_fast = xp.sqrt(1 / 2 * (cS**2 + vA**2) * (1 + xp.sqrt(1 - delta))) - print(f"{v_slow = }") - print(f"{v_fast = }") + print(f"{v_slow =}") + print(f"{v_fast =}") assert xp.abs(coeffs[0][0] - v_slow) < 0.05 assert xp.abs(coeffs[1][0] - v_fast) < 0.19 diff --git a/src/struphy/models/tests/test_verif_Maxwell.py b/src/struphy/models/tests/test_verif_Maxwell.py index e97675e7b..ccea67c18 100644 --- a/src/struphy/models/tests/test_verif_Maxwell.py +++ b/src/struphy/models/tests/test_verif_Maxwell.py @@ -203,7 +203,7 @@ def E_theta(X, Y, Z, m, t): r = (X**2 + Y**2) ** 0.5 theta = xp.arctan2(Y, X) return ((m / r * jv(m, r) - jv(m + 1, r)) - 0.28 * (m / r * yn(m, r) - yn(m + 1, r))) * xp.sin( - m * theta - t + m * theta - t, ) def to_E_r(X, Y, E_x, E_y): @@ -222,7 +222,13 @@ def to_E_theta(X, Y, E_x, E_y): vmax = E_theta(X, Y, grids_phy[0], modes, 0).max() fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4)) plot_exac = ax1.contourf( - X, Y, E_theta(X, Y, grids_phy[0], modes, t_grid[-1]), cmap="plasma", levels=100, vmin=vmin, vmax=vmax + X, + Y, + E_theta(X, Y, grids_phy[0], modes, t_grid[-1]), + cmap="plasma", + levels=100, + vmin=vmin, + vmax=vmax, ) ax2.contourf( X, @@ -256,12 +262,12 @@ def to_E_theta(X, Y, E_x, E_y): rel_err_Bz = error_Bz / xp.max(xp.abs(Bz_exact)) print("") - assert rel_err_Bz < 0.0021, f"Assertion for magnetic field Maxwell failed: {rel_err_Bz = }" - print(f"Assertion for magnetic field Maxwell passed ({rel_err_Bz = }).") - assert rel_err_Etheta < 0.0021, f"Assertion for electric (E_theta) field Maxwell failed: {rel_err_Etheta = }" - print(f"Assertion for electric field Maxwell passed ({rel_err_Etheta = }).") - assert rel_err_Er < 0.0021, f"Assertion for electric (E_r) field Maxwell failed: {rel_err_Er = }" - print(f"Assertion for electric field Maxwell passed ({rel_err_Er = }).") + assert rel_err_Bz < 0.0021, f"Assertion for magnetic field Maxwell failed: {rel_err_Bz =}" + print(f"Assertion for magnetic field Maxwell passed ({rel_err_Bz =}).") + assert rel_err_Etheta < 0.0021, f"Assertion for electric (E_theta) field Maxwell failed: {rel_err_Etheta =}" + print(f"Assertion for electric field Maxwell passed ({rel_err_Etheta =}).") + assert rel_err_Er < 0.0021, f"Assertion for electric (E_r) field Maxwell failed: {rel_err_Er =}" + print(f"Assertion for electric field Maxwell passed ({rel_err_Er =}).") if __name__ == "__main__": diff --git a/src/struphy/models/tests/test_verif_Poisson.py b/src/struphy/models/tests/test_verif_Poisson.py index 5b62d61ab..e82ea22c7 100644 --- a/src/struphy/models/tests/test_verif_Poisson.py +++ b/src/struphy/models/tests/test_verif_Poisson.py @@ -124,14 +124,14 @@ def test_poisson_1d(do_plot=False): plt.subplot(5, 2, 2 * c + 1) plt.plot(x, phi_h, label="phi") plt.plot(x, phi_e, "r--", label="exact") - plt.title(f"phi at {t = }") + plt.title(f"phi at {t =}") plt.ylim(-amp / (l * 2 * xp.pi / Lx) ** 2, amp / (l * 2 * xp.pi / Lx) ** 2) plt.legend() plt.subplot(5, 2, 2 * c + 2) plt.plot(x, source[t][0][:, 0, 0], label="rhs") plt.plot(x, rhs_exact(x, 0, 0, t), "r--", label="exact") - plt.title(f"source at {t = }") + plt.title(f"source at {t =}") plt.ylim(-amp, amp) plt.legend() @@ -140,7 +140,7 @@ def test_poisson_1d(do_plot=False): break plt.show() - print(f"{err = }") + print(f"{err =}") assert err < 0.0057 diff --git a/src/struphy/models/tests/test_verif_VlasovAmpereOneSpecies.py b/src/struphy/models/tests/test_verif_VlasovAmpereOneSpecies.py index 585a9776a..a2625ba17 100644 --- a/src/struphy/models/tests/test_verif_VlasovAmpereOneSpecies.py +++ b/src/struphy/models/tests/test_verif_VlasovAmpereOneSpecies.py @@ -159,8 +159,8 @@ def E_exact(t): # assert rel_error = xp.abs(gamma_num - gamma) / xp.abs(gamma) - assert rel_error < 0.22, f"Assertion for weak Landau damping failed: {gamma_num = } vs. {gamma = }." - print(f"Assertion for weak Landau damping passed ({rel_error = }).") + assert rel_error < 0.22, f"Assertion for weak Landau damping failed: {gamma_num =} vs. {gamma =}." + print(f"Assertion for weak Landau damping passed ({rel_error =}).") if __name__ == "__main__": diff --git a/src/struphy/models/tests/test_xxpproc.py b/src/struphy/models/tests/test_xxpproc.py index acb8a6590..3d4fef2f0 100644 --- a/src/struphy/models/tests/test_xxpproc.py +++ b/src/struphy/models/tests/test_xxpproc.py @@ -49,7 +49,7 @@ def test_pproc_codes(model: str = None, group: str = None): elif group == "toy": list_models = list_toy else: - raise ValueError(f"{group = } is not a valid group specification.") + raise ValueError(f"{group =} is not a valid group specification.") if comm.Get_rank() == 0: if model is None: diff --git a/src/struphy/models/tests/verification.py b/src/struphy/models/tests/verification.py index 75b0a1047..d28fc3d6e 100644 --- a/src/struphy/models/tests/verification.py +++ b/src/struphy/models/tests/verification.py @@ -85,8 +85,8 @@ def E_exact(t): # assert rel_error = xp.abs(gamma_num - gamma) / xp.abs(gamma) - assert rel_error < 0.25, f"{rank = }: Assertion for weak Landau damping failed: {gamma_num = } vs. {gamma = }." - print(f"{rank = }: Assertion for weak Landau damping passed ({rel_error = }).") + assert rel_error < 0.25, f"{rank =}: Assertion for weak Landau damping failed: {gamma_num =} vs. {gamma =}." + print(f"{rank =}: Assertion for weak Landau damping passed ({rel_error =}).") def LinearVlasovAmpereOneSpecies_weakLandau( @@ -161,8 +161,8 @@ def E_exact(t): # assert rel_error = xp.abs(gamma_num - gamma) / xp.abs(gamma) - assert rel_error < 0.25, f"{rank = }: Assertion for weak Landau damping failed: {gamma_num = } vs. {gamma = }." - print(f"{rank = }: Assertion for weak Landau damping passed ({rel_error = }).") + assert rel_error < 0.25, f"{rank =}: Assertion for weak Landau damping failed: {gamma_num =} vs. {gamma =}." + print(f"{rank =}: Assertion for weak Landau damping passed ({rel_error =}).") def IsothermalEulerSPH_soundwave( @@ -207,7 +207,7 @@ def IsothermalEulerSPH_soundwave( plot_ct = 0 for i in range(0, Nt + 1): if i % interval == 0: - print(f"{i = }") + print(f"{i =}") plot_ct += 1 ax = plt.gca() @@ -224,7 +224,7 @@ def IsothermalEulerSPH_soundwave( plt.xlabel("x") plt.ylabel(r"$\rho$") - plt.title(f"standing sound wave ($c_s = 1$) for {nx = } and {ppb = }") + plt.title(f"standing sound wave ($c_s = 1$) for {nx =} and {ppb =}") if plot_ct == 11: break @@ -232,7 +232,7 @@ def IsothermalEulerSPH_soundwave( # assert error = xp.max(xp.abs(n_sph[0] - n_sph[-1])) - print(f"{rank = }: Assertion for SPH sound wave passed ({error = }).") + print(f"{rank =}: Assertion for SPH sound wave passed ({error =}).") assert error < 1.3e-3 @@ -311,7 +311,13 @@ def to_E_theta(X, Y, E_x, E_y): vmax = E_theta(X, Y, grids_phy[0], modes, 0).max() fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4)) plot_exac = ax1.contourf( - X, Y, E_theta(X, Y, grids_phy[0], modes, t_grid[-1]), cmap="plasma", levels=100, vmin=vmin, vmax=vmax + X, + Y, + E_theta(X, Y, grids_phy[0], modes, t_grid[-1]), + cmap="plasma", + levels=100, + vmin=vmin, + vmax=vmax, ) ax2.contourf( X, @@ -344,18 +350,18 @@ def to_E_theta(X, Y, E_x, E_y): rel_err_Etheta = error_Etheta / xp.max(xp.abs(Etheta_exact)) rel_err_Bz = error_Bz / xp.max(xp.abs(Bz_exact)) - print(f"{rel_err_Er = }") - print(f"{rel_err_Etheta = }") - print(f"{rel_err_Bz = }") + print(f"{rel_err_Er =}") + print(f"{rel_err_Etheta =}") + print(f"{rel_err_Bz =}") - assert rel_err_Bz < 0.0021, f"{rank = }: Assertion for magnetic field Maxwell failed: {rel_err_Bz = }" - print(f"{rank = }: Assertion for magnetic field Maxwell passed ({rel_err_Bz = }).") + assert rel_err_Bz < 0.0021, f"{rank =}: Assertion for magnetic field Maxwell failed: {rel_err_Bz =}" + print(f"{rank =}: Assertion for magnetic field Maxwell passed ({rel_err_Bz =}).") assert rel_err_Etheta < 0.0021, ( - f"{rank = }: Assertion for electric (E_theta) field Maxwell failed: {rel_err_Etheta = }" + f"{rank =}: Assertion for electric (E_theta) field Maxwell failed: {rel_err_Etheta =}" ) - print(f"{rank = }: Assertion for electric field Maxwell passed ({rel_err_Etheta = }).") - assert rel_err_Er < 0.0021, f"{rank = }: Assertion for electric (E_r) field Maxwell failed: {rel_err_Er = }" - print(f"{rank = }: Assertion for electric field Maxwell passed ({rel_err_Er = }).") + print(f"{rank =}: Assertion for electric field Maxwell passed ({rel_err_Etheta =}).") + assert rel_err_Er < 0.0021, f"{rank =}: Assertion for electric (E_r) field Maxwell failed: {rel_err_Er =}" + print(f"{rank =}: Assertion for electric field Maxwell passed ({rel_err_Er =}).") if __name__ == "__main__": diff --git a/src/struphy/models/toy.py b/src/struphy/models/toy.py index bad2b7916..fd36b5d5f 100644 --- a/src/struphy/models/toy.py +++ b/src/struphy/models/toy.py @@ -81,10 +81,12 @@ def allocate_helpers(self): def update_scalar_quantities(self): en_E = 0.5 * self.mass_ops.M1.dot_inner( - self.em_fields.e_field.spline.vector, self.em_fields.e_field.spline.vector + self.em_fields.e_field.spline.vector, + self.em_fields.e_field.spline.vector, ) en_B = 0.5 * self.mass_ops.M2.dot_inner( - self.em_fields.b_field.spline.vector, self.em_fields.b_field.spline.vector + self.em_fields.b_field.spline.vector, + self.em_fields.b_field.spline.vector, ) self.update_scalar("electric energy", en_E) @@ -336,7 +338,7 @@ def allocate_helpers(self): self.equil.b2_1, self.equil.b2_2, self.equil.b2_3, - ] + ], ) # temporary vectors for scalar quantities @@ -371,7 +373,8 @@ def update_scalar_quantities(self): # perturbed fields en_U = 0.5 * self.mass_ops.M2n.dot_inner(self.mhd.velocity.spline.vector, self.mhd.velocity.spline.vector) en_B = 0.5 * self.mass_ops.M2.dot_inner( - self.em_fields.b_field.spline.vector, self.em_fields.b_field.spline.vector + self.em_fields.b_field.spline.vector, + self.em_fields.b_field.spline.vector, ) self.update_scalar("en_U", en_U) @@ -477,7 +480,7 @@ def generate_default_parameter_file(self, path=None, prompt=True): for line in f: if "variat_dens.Options" in line: new_file += [ - "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='pressureless')\n" + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='pressureless')\n", ] else: new_file += [line] @@ -583,7 +586,7 @@ def generate_default_parameter_file(self, path=None, prompt=True): for line in f: if "variat_dens.Options" in line: new_file += [ - "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='barotropic')\n" + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='barotropic')\n", ] else: new_file += [line] @@ -703,17 +706,17 @@ def generate_default_parameter_file(self, path=None, prompt=True): for line in f: if "variat_dens.Options" in line: new_file += [ - "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='full',\n" + "model.propagators.variat_dens.options = model.propagators.variat_dens.Options(model='full',\n", ] new_file += [ - " s=model.fluid.entropy)\n" + " s=model.fluid.entropy)\n", ] elif "variat_ent.Options" in line: new_file += [ - "model.propagators.variat_ent.options = model.propagators.variat_ent.Options(model='full',\n" + "model.propagators.variat_ent.options = model.propagators.variat_ent.Options(model='full',\n", ] new_file += [ - " rho=model.fluid.density)\n" + " rho=model.fluid.density)\n", ] elif "entropy.add_background" in line: new_file += ["model.fluid.density.add_background(FieldsBackground())\n"] @@ -859,7 +862,7 @@ def generate_default_parameter_file(self, path=None, prompt=True): for line in f: if "poisson.Options" in line: new_file += [ - "model.propagators.poisson.options = model.propagators.poisson.Options(rho=model.em_fields.source)\n" + "model.propagators.poisson.options = model.propagators.poisson.Options(rho=model.em_fields.source)\n", ] else: new_file += [line] diff --git a/src/struphy/models/variables.py b/src/struphy/models/variables.py index d71f4644d..e47ab1cd0 100644 --- a/src/struphy/models/variables.py +++ b/src/struphy/models/variables.py @@ -82,7 +82,7 @@ def add_background(self, background, verbose=True): if verbose and MPI.COMM_WORLD.Get_rank() == 0: print( - f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - added background '{background.__class__.__name__}' with:" + f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - added background '{background.__class__.__name__}' with:", ) for k, v in background.__dict__.items(): print(f" {k}: {v}") @@ -120,7 +120,7 @@ def add_perturbation(self, perturbation: Perturbation, verbose=True): if verbose and MPI.COMM_WORLD.Get_rank() == 0: print( - f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - added perturbation '{perturbation.__class__.__name__}' with:" + f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - added perturbation '{perturbation.__class__.__name__}' with:", ) for k, v in perturbation.__dict__.items(): print(f" {k}: {v}") @@ -175,7 +175,7 @@ def add_initial_condition(self, init: KineticBackground, verbose=True): self._initial_condition = init if verbose and MPI.COMM_WORLD.Get_rank() == 0: print( - f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - added initial condition '{init.__class__.__name__}' with:" + f"\nVariable '{self.__name__}' of species '{self.species.__class__.__name__}' - added initial condition '{init.__class__.__name__}' with:", ) for k, v in init.__dict__.items(): print(f" {k}: {v}") diff --git a/src/struphy/ode/tests/test_ode_feec.py b/src/struphy/ode/tests/test_ode_feec.py index dadd3aad3..c0ef51b08 100644 --- a/src/struphy/ode/tests/test_ode_feec.py +++ b/src/struphy/ode/tests/test_ode_feec.py @@ -100,9 +100,9 @@ def f(t, y1, y2, y3, out=out): vector_field[var] = f - print(f"{vector_field = }") + print(f"{vector_field =}") butcher = ButcherTableau(algo=algo) - print(f"{butcher = }") + print(f"{butcher =}") solver = ODEsolverFEEC(vector_field, butcher=butcher) @@ -118,7 +118,7 @@ def f(t, y1, y2, y3, out=out): for i, h in enumerate(hs): errors[h] = {} time = xp.linspace(0, Tend, int(Tend / h) + 1) - print(f"{h = }, {time.size = }") + print(f"{h =}, {time.size =}") yvec = y_exact(time) ymax = {} for var in vector_field: @@ -139,7 +139,7 @@ def f(t, y1, y2, y3, out=out): # checks for var in vector_field: errors[h][var] = h * xp.sum(xp.abs(yvec - ymax[var])) / (h * xp.sum(xp.abs(yvec))) - print(f"{errors[h][var] = }") + print(f"{errors[h][var] =}") assert errors[h][var] < 0.31 if rank == 0: @@ -162,9 +162,9 @@ def f(t, y1, y2, y3, out=out): err_vec += [dct[var]] m, _ = xp.polyfit(xp.log(h_vec), xp.log(err_vec), deg=1) - print(f"{spaces[j]}-space, fitted convergence rate = {m} for {algo = } with {solver.butcher.conv_rate = }") + print(f"{spaces[j]}-space, fitted convergence rate = {m} for {algo =} with {solver.butcher.conv_rate =}") assert xp.abs(m - solver.butcher.conv_rate) < 0.1 - print(f"Convergence check passed on {rank = }.") + print(f"Convergence check passed on {rank =}.") if rank == 0: plt.loglog(h_vec, h_vec, "--", label=f"h") diff --git a/src/struphy/pic/accumulation/accum_kernels_gc.py b/src/struphy/pic/accumulation/accum_kernels_gc.py index c5e836f81..fecf6a255 100644 --- a/src/struphy/pic/accumulation/accum_kernels_gc.py +++ b/src/struphy/pic/accumulation/accum_kernels_gc.py @@ -240,7 +240,16 @@ def cc_lin_mhd_5d_D( # call the appropriate matvec filler particle_to_mat_kernels.mat_fill_v0vec_asym( - args_derham, span1, span2, span3, mat12, mat13, mat23, filling_m12, filling_m13, filling_m23 + args_derham, + span1, + span2, + span3, + mat12, + mat13, + mat23, + filling_m12, + filling_m13, + filling_m23, ) elif basis_u == 1: @@ -257,7 +266,16 @@ def cc_lin_mhd_5d_D( # call the appropriate matvec filler particle_to_mat_kernels.mat_fill_v1_asym( - args_derham, span1, span2, span3, mat12, mat13, mat23, filling_m12, filling_m13, filling_m23 + args_derham, + span1, + span2, + span3, + mat12, + mat13, + mat23, + filling_m12, + filling_m13, + filling_m23, ) elif basis_u == 2: @@ -268,7 +286,16 @@ def cc_lin_mhd_5d_D( # call the appropriate matvec filler particle_to_mat_kernels.mat_fill_v2_asym( - args_derham, span1, span2, span3, mat12, mat13, mat23, filling_m12, filling_m13, filling_m23 + args_derham, + span1, + span2, + span3, + mat12, + mat13, + mat23, + filling_m12, + filling_m13, + filling_m23, ) # -- removed omp: #$ omp end parallel @@ -579,7 +606,16 @@ def cc_lin_mhd_5d_M( filling_v[:] = weight * mu / det_df * scale_vec * norm_b1 particle_to_mat_kernels.vec_fill_v2( - args_derham, span1, span2, span3, vec1, vec2, vec3, filling_v[0], filling_v[1], filling_v[2] + args_derham, + span1, + span2, + span3, + vec1, + vec2, + vec3, + filling_v[0], + filling_v[1], + filling_v[2], ) vec1 /= Np @@ -759,7 +795,16 @@ def cc_lin_mhd_5d_gradB( # call the appropriate matvec filler particle_to_mat_kernels.vec_fill_v0vec( - args_derham, span1, span2, span3, vec1, vec2, vec3, filling_v[0], filling_v[1], filling_v[2] + args_derham, + span1, + span2, + span3, + vec1, + vec2, + vec3, + filling_v[0], + filling_v[1], + filling_v[2], ) elif basis_u == 2: @@ -770,7 +815,16 @@ def cc_lin_mhd_5d_gradB( # call the appropriate matvec filler particle_to_mat_kernels.vec_fill_v2( - args_derham, span1, span2, span3, vec1, vec2, vec3, filling_v[0], filling_v[1], filling_v[2] + args_derham, + span1, + span2, + span3, + vec1, + vec2, + vec3, + filling_v[0], + filling_v[1], + filling_v[2], ) vec1 /= Np vec2 /= Np @@ -955,7 +1009,16 @@ def cc_lin_mhd_5d_gradB_dg_init( # call the appropriate matvec filler particle_to_mat_kernels.vec_fill_v0vec( - args_derham, span1, span2, span3, vec1, vec2, vec3, filling_v[0], filling_v[1], filling_v[2] + args_derham, + span1, + span2, + span3, + vec1, + vec2, + vec3, + filling_v[0], + filling_v[1], + filling_v[2], ) elif basis_u == 2: @@ -981,7 +1044,16 @@ def cc_lin_mhd_5d_gradB_dg_init( # call the appropriate matvec filler particle_to_mat_kernels.vec_fill_v2( - args_derham, span1, span2, span3, vec1, vec2, vec3, filling_v[0], filling_v[1], filling_v[2] + args_derham, + span1, + span2, + span3, + vec1, + vec2, + vec3, + filling_v[0], + filling_v[1], + filling_v[2], ) vec1 /= Np @@ -1180,7 +1252,16 @@ def cc_lin_mhd_5d_gradB_dg( # call the appropriate matvec filler particle_to_mat_kernels.vec_fill_v0vec( - args_derham, span1, span2, span3, vec1, vec2, vec3, filling_v[0], filling_v[1], filling_v[2] + args_derham, + span1, + span2, + span3, + vec1, + vec2, + vec3, + filling_v[0], + filling_v[1], + filling_v[2], ) elif basis_u == 2: @@ -1218,7 +1299,16 @@ def cc_lin_mhd_5d_gradB_dg( # call the appropriate matvec filler particle_to_mat_kernels.vec_fill_v2( - args_derham, span1, span2, span3, vec1, vec2, vec3, filling_v[0], filling_v[1], filling_v[2] + args_derham, + span1, + span2, + span3, + vec1, + vec2, + vec3, + filling_v[0], + filling_v[1], + filling_v[2], ) vec1 /= Np diff --git a/src/struphy/pic/accumulation/particle_to_mat_kernels.py b/src/struphy/pic/accumulation/particle_to_mat_kernels.py index 576d4571c..bc9364f6a 100644 --- a/src/struphy/pic/accumulation/particle_to_mat_kernels.py +++ b/src/struphy/pic/accumulation/particle_to_mat_kernels.py @@ -5834,7 +5834,12 @@ def m_v_fill_v2_full( def mat_fill_b_v0( - args_derham: "DerhamArguments", eta1: float, eta2: float, eta3: float, mat: "float[:,:,:,:,:,:]", fill: float + args_derham: "DerhamArguments", + eta1: float, + eta2: float, + eta3: float, + mat: "float[:,:,:,:,:,:]", + fill: float, ): """ Adds the contribution of one particle to the elements of an accumulation matrix V0 -> V0. The result is returned in mat. @@ -5969,7 +5974,12 @@ def m_v_fill_b_v0( def mat_fill_b_v3( - args_derham: "DerhamArguments", eta1: float, eta2: float, eta3: float, mat: "float[:,:,:,:,:,:]", fill: float + args_derham: "DerhamArguments", + eta1: float, + eta2: float, + eta3: float, + mat: "float[:,:,:,:,:,:]", + fill: float, ): """ Adds the contribution of one particle to the elements of an accumulation matrix V3 -> V3. The result is returned in mat. @@ -6112,7 +6122,12 @@ def m_v_fill_b_v3( def mat_fill_v0( - args_derham: "DerhamArguments", span1: int, span2: int, span3: int, mat: "float[:,:,:,:,:,:]", fill: float + args_derham: "DerhamArguments", + span1: int, + span2: int, + span3: int, + mat: "float[:,:,:,:,:,:]", + fill: float, ): """ Adds the contribution of one particle to the elements of an accumulation matrix V0 -> V0. The result is returned in mat. @@ -6239,7 +6254,12 @@ def m_v_fill_v0( def mat_fill_v3( - args_derham: "DerhamArguments", span1: int, span2: int, span3: int, mat: "float[:,:,:,:,:,:]", fill: float + args_derham: "DerhamArguments", + span1: int, + span2: int, + span3: int, + mat: "float[:,:,:,:,:,:]", + fill: float, ): """ Adds the contribution of one particle to the elements of an accumulation block matrix V3 -> V3. The result is returned in mat. @@ -12949,7 +12969,12 @@ def vec_fill_v0vec( def vec_fill_b_v0( - args_derham: "DerhamArguments", eta1: float, eta2: float, eta3: float, vec: "float[:,:,:]", fill: float + args_derham: "DerhamArguments", + eta1: float, + eta2: float, + eta3: float, + vec: "float[:,:,:]", + fill: float, ): """TODO""" @@ -13127,7 +13152,12 @@ def vec_fill_b_v2( def vec_fill_b_v3( - args_derham: "DerhamArguments", eta1: float, eta2: float, eta3: float, vec: "float[:,:,:]", fill: float + args_derham: "DerhamArguments", + eta1: float, + eta2: float, + eta3: float, + vec: "float[:,:,:]", + fill: float, ): """TODO""" diff --git a/src/struphy/pic/base.py b/src/struphy/pic/base.py index 0f92323ae..84900418d 100644 --- a/src/struphy/pic/base.py +++ b/src/struphy/pic/base.py @@ -230,10 +230,10 @@ def __init__( n_boxes = self.mpi_size * self.num_clones else: assert all([nboxes >= nproc for nboxes, nproc in zip(self.boxes_per_dim, self.nprocs)]), ( - f"There must be at least one box {self.boxes_per_dim = } on each process {self.nprocs = } in each direction." + f"There must be at least one box {self.boxes_per_dim =} on each process {self.nprocs =} in each direction." ) assert all([nboxes % nproc == 0 for nboxes, nproc in zip(self.boxes_per_dim, self.nprocs)]), ( - f"Number of boxes {self.boxes_per_dim = } must be divisible by number of processes {self.nprocs = } in each direction." + f"Number of boxes {self.boxes_per_dim =} must be divisible by number of processes {self.nprocs =} in each direction." ) n_boxes = xp.prod(self.boxes_per_dim, dtype=int) * self.num_clones @@ -776,7 +776,7 @@ def velocities(self): @velocities.setter def velocities(self, new): assert isinstance(new, xp.ndarray) - assert new.shape == (self.n_mks_loc, self.vdim), f"{self.n_mks_loc = } and {self.vdim = } but {new.shape = }" + assert new.shape == (self.n_mks_loc, self.vdim), f"{self.n_mks_loc =} and {self.vdim =} but {new.shape =}" self._markers[self.valid_mks, self.index["vel"]] = new @property @@ -1126,7 +1126,7 @@ def _allocate_marker_array(self): # Have at least 3 spare places in markers array assert self.args_markers.first_free_idx + 2 < self.n_cols - 1, ( - f"{self.args_markers.first_free_idx + 2} is not smaller than {self.n_cols - 1 = }; not enough columns in marker array !!" + f"{self.args_markers.first_free_idx + 2} is not smaller than {self.n_cols - 1 =}; not enough columns in marker array !!" ) def _initialize_sorting_boxes(self): @@ -1565,7 +1565,7 @@ def draw_markers( num_loaded_particles_loc += num_valid # make sure all particles are loaded - assert self.Np == int(num_loaded_particles_glob), f"{self.Np = }, {int(num_loaded_particles_glob) = }" + assert self.Np == int(num_loaded_particles_glob), f"{self.Np =}, {int(num_loaded_particles_glob) =}" # set new n_mks_load self._gather_scalar_in_subcomm_array(num_loaded_particles_loc, out=self.n_mks_load) @@ -1843,7 +1843,7 @@ def initialize_weights( self.update_holes() self.reset_marker_ids() print( - f"\nWeights < {self.threshold} have been rejected, number of valid markers on process {self.mpi_rank} is {self.n_mks_loc}." + f"\nWeights < {self.threshold} have been rejected, number of valid markers on process {self.mpi_rank} is {self.n_mks_loc}.", ) # compute (time-dependent) weights at vdim + 3 @@ -2464,7 +2464,7 @@ def _set_boundary_boxes(self): self._bnd_boxes_x_p.append(flatten_index(self.nx, j, k, self.nx, self.ny, self.nz)) if self._verbose: - print(f"eta1 boundary on {self._rank = }:\n{self._bnd_boxes_x_m = }\n{self._bnd_boxes_x_p = }") + print(f"eta1 boundary on {self._rank =}:\n{self._bnd_boxes_x_m =}\n{self._bnd_boxes_x_p =}") # y boundary # negative direction @@ -2479,7 +2479,7 @@ def _set_boundary_boxes(self): self._bnd_boxes_y_p.append(flatten_index(i, self.ny, k, self.nx, self.ny, self.nz)) if self._verbose: - print(f"eta2 boundary on {self._rank = }:\n{self._bnd_boxes_y_m = }\n{self._bnd_boxes_y_p = }") + print(f"eta2 boundary on {self._rank =}:\n{self._bnd_boxes_y_m =}\n{self._bnd_boxes_y_p =}") # z boundary # negative direction @@ -2494,7 +2494,7 @@ def _set_boundary_boxes(self): self._bnd_boxes_z_p.append(flatten_index(i, j, self.nz, self.nx, self.ny, self.nz)) if self._verbose: - print(f"eta3 boundary on {self._rank = }:\n{self._bnd_boxes_z_m = }\n{self._bnd_boxes_z_p = }") + print(f"eta3 boundary on {self._rank =}:\n{self._bnd_boxes_z_m =}\n{self._bnd_boxes_z_p =}") # x-y edges self._bnd_boxes_x_m_y_m = [] @@ -2512,11 +2512,11 @@ def _set_boundary_boxes(self): if self._verbose: print( ( - f"eta1-eta2 edge on {self._rank = }:\n{self._bnd_boxes_x_m_y_m = }" - f"\n{self._bnd_boxes_x_m_y_p = }" - f"\n{self._bnd_boxes_x_p_y_m = }" - f"\n{self._bnd_boxes_x_p_y_p = }" - ) + f"eta1-eta2 edge on {self._rank =}:\n{self._bnd_boxes_x_m_y_m =}" + f"\n{self._bnd_boxes_x_m_y_p =}" + f"\n{self._bnd_boxes_x_p_y_m =}" + f"\n{self._bnd_boxes_x_p_y_p =}" + ), ) # x-z edges @@ -2535,11 +2535,11 @@ def _set_boundary_boxes(self): if self._verbose: print( ( - f"eta1-eta3 edge on {self._rank = }:\n{self._bnd_boxes_x_m_z_m = }" - f"\n{self._bnd_boxes_x_m_z_p = }" - f"\n{self._bnd_boxes_x_p_z_m = }" - f"\n{self._bnd_boxes_x_p_z_p = }" - ) + f"eta1-eta3 edge on {self._rank =}:\n{self._bnd_boxes_x_m_z_m =}" + f"\n{self._bnd_boxes_x_m_z_p =}" + f"\n{self._bnd_boxes_x_p_z_m =}" + f"\n{self._bnd_boxes_x_p_z_p =}" + ), ) # y-z edges @@ -2558,11 +2558,11 @@ def _set_boundary_boxes(self): if self._verbose: print( ( - f"eta2-eta3 edge on {self._rank = }:\n{self._bnd_boxes_y_m_z_m = }" - f"\n{self._bnd_boxes_y_m_z_p = }" - f"\n{self._bnd_boxes_y_p_z_m = }" - f"\n{self._bnd_boxes_y_p_z_p = }" - ) + f"eta2-eta3 edge on {self._rank =}:\n{self._bnd_boxes_y_m_z_m =}" + f"\n{self._bnd_boxes_y_m_z_p =}" + f"\n{self._bnd_boxes_y_p_z_m =}" + f"\n{self._bnd_boxes_y_p_z_p =}" + ), ) # corners @@ -2588,15 +2588,15 @@ def _set_boundary_boxes(self): if self._verbose: print( ( - f"corners on {self._rank = }:\n{self._bnd_boxes_x_m_y_m_z_m = }" - f"\n{self._bnd_boxes_x_m_y_m_z_p = }" - f"\n{self._bnd_boxes_x_m_y_p_z_m = }" - f"\n{self._bnd_boxes_x_p_y_m_z_m = }" - f"\n{self._bnd_boxes_x_m_y_p_z_p = }" - f"\n{self._bnd_boxes_x_p_y_m_z_p = }" - f"\n{self._bnd_boxes_x_p_y_p_z_m = }" - f"\n{self._bnd_boxes_x_p_y_p_z_p = }" - ) + f"corners on {self._rank =}:\n{self._bnd_boxes_x_m_y_m_z_m =}" + f"\n{self._bnd_boxes_x_m_y_m_z_p =}" + f"\n{self._bnd_boxes_x_m_y_p_z_m =}" + f"\n{self._bnd_boxes_x_p_y_m_z_m =}" + f"\n{self._bnd_boxes_x_m_y_p_z_p =}" + f"\n{self._bnd_boxes_x_p_y_m_z_p =}" + f"\n{self._bnd_boxes_x_p_y_p_z_m =}" + f"\n{self._bnd_boxes_x_p_y_p_z_p =}" + ), ) def _sort_boxed_particles_numpy(self): @@ -2650,7 +2650,7 @@ def check_and_assign_particles_to_boxes(self): f'Strong load imbalance detected in sorting boxes: \ max number of markers in a box ({max_in_box}) on rank {self.mpi_rank} \ exceeds the column-size of the box array ({self._sorting_boxes.boxes.shape[1]}). \ -Increasing the value of "box_bufsize" in the markers parameters for the next run.' +Increasing the value of "box_bufsize" in the markers parameters for the next run.', ) self.mpi_comm.Abort() @@ -2734,17 +2734,23 @@ def prepare_ghost_particles(self): # Mirror position for boundary condition if self.bc_sph[0] in ("mirror", "fixed"): self._mirror_particles( - "_markers_x_m", "_markers_x_p", is_domain_boundary=self.sorting_boxes.is_domain_boundary + "_markers_x_m", + "_markers_x_p", + is_domain_boundary=self.sorting_boxes.is_domain_boundary, ) if self.bc_sph[1] in ("mirror", "fixed"): self._mirror_particles( - "_markers_y_m", "_markers_y_p", is_domain_boundary=self.sorting_boxes.is_domain_boundary + "_markers_y_m", + "_markers_y_p", + is_domain_boundary=self.sorting_boxes.is_domain_boundary, ) if self.bc_sph[2] in ("mirror", "fixed"): self._mirror_particles( - "_markers_z_m", "_markers_z_p", is_domain_boundary=self.sorting_boxes.is_domain_boundary + "_markers_z_m", + "_markers_z_p", + is_domain_boundary=self.sorting_boxes.is_domain_boundary, ) ## Edges x-y @@ -2899,7 +2905,8 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): arr[:, 0] *= -1.0 if self.bc_sph[0] == "fixed" and arr_name not in self._fixed_markers_set: boundary_values = self.f_init( - *arr[:, :3].T, flat_eval=True + *arr[:, :3].T, + flat_eval=True, ) # evaluation outside of the unit cube - maybe not working for all f_init! arr[:, self.index["weights"]] = -boundary_values / self.s0( *arr[:, :3].T, @@ -2911,7 +2918,8 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): arr[:, 0] = 2.0 - arr[:, 0] if self.bc_sph[0] == "fixed" and arr_name not in self._fixed_markers_set: boundary_values = self.f_init( - *arr[:, :3].T, flat_eval=True + *arr[:, :3].T, + flat_eval=True, ) # evaluation outside of the unit cube - maybe not working for all f_init! arr[:, self.index["weights"]] = -boundary_values / self.s0( *arr[:, :3].T, @@ -2926,7 +2934,8 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): arr[:, 1] *= -1.0 if self.bc_sph[1] == "fixed" and arr_name not in self._fixed_markers_set: boundary_values = self.f_init( - *arr[:, :3].T, flat_eval=True + *arr[:, :3].T, + flat_eval=True, ) # evaluation outside of the unit cube - maybe not working for all f_init! arr[:, self.index["weights"]] = -boundary_values / self.s0( *arr[:, :3].T, @@ -2938,7 +2947,8 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): arr[:, 1] = 2.0 - arr[:, 1] if self.bc_sph[1] == "fixed" and arr_name not in self._fixed_markers_set: boundary_values = self.f_init( - *arr[:, :3].T, flat_eval=True + *arr[:, :3].T, + flat_eval=True, ) # evaluation outside of the unit cube - maybe not working for all f_init! arr[:, self.index["weights"]] = -boundary_values / self.s0( *arr[:, :3].T, @@ -2953,7 +2963,8 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): arr[:, 2] *= -1.0 if self.bc_sph[2] == "fixed" and arr_name not in self._fixed_markers_set: boundary_values = self.f_init( - *arr[:, :3].T, flat_eval=True + *arr[:, :3].T, + flat_eval=True, ) # evaluation outside of the unit cube - maybe not working for all f_init! arr[:, self.index["weights"]] = -boundary_values / self.s0( *arr[:, :3].T, @@ -2965,7 +2976,8 @@ def _mirror_particles(self, *marker_array_names, is_domain_boundary=None): arr[:, 2] = 2.0 - arr[:, 2] if self.bc_sph[2] == "fixed" and arr_name not in self._fixed_markers_set: boundary_values = self.f_init( - *arr[:, :3].T, flat_eval=True + *arr[:, :3].T, + flat_eval=True, ) # evaluation outside of the unit cube - maybe not working for all f_init! arr[:, self.index["weights"]] = -boundary_values / self.s0( *arr[:, :3].T, @@ -3018,124 +3030,124 @@ def get_destinations_box(self): # if self._x_m_y_m_proc is not None: self._send_info_box[self._x_m_y_m_proc] += len(self._markers_x_m_y_m) self._send_list_box[self._x_m_y_m_proc] = xp.concatenate( - (self._send_list_box[self._x_m_y_m_proc], self._markers_x_m_y_m) + (self._send_list_box[self._x_m_y_m_proc], self._markers_x_m_y_m), ) # if self._x_m_y_p_proc is not None: self._send_info_box[self._x_m_y_p_proc] += len(self._markers_x_m_y_p) self._send_list_box[self._x_m_y_p_proc] = xp.concatenate( - (self._send_list_box[self._x_m_y_p_proc], self._markers_x_m_y_p) + (self._send_list_box[self._x_m_y_p_proc], self._markers_x_m_y_p), ) # if self._x_p_y_m_proc is not None: self._send_info_box[self._x_p_y_m_proc] += len(self._markers_x_p_y_m) self._send_list_box[self._x_p_y_m_proc] = xp.concatenate( - (self._send_list_box[self._x_p_y_m_proc], self._markers_x_p_y_m) + (self._send_list_box[self._x_p_y_m_proc], self._markers_x_p_y_m), ) # if self._x_p_y_p_proc is not None: self._send_info_box[self._x_p_y_p_proc] += len(self._markers_x_p_y_p) self._send_list_box[self._x_p_y_p_proc] = xp.concatenate( - (self._send_list_box[self._x_p_y_p_proc], self._markers_x_p_y_p) + (self._send_list_box[self._x_p_y_p_proc], self._markers_x_p_y_p), ) # x-z edges # if self._x_m_z_m_proc is not None: self._send_info_box[self._x_m_z_m_proc] += len(self._markers_x_m_z_m) self._send_list_box[self._x_m_z_m_proc] = xp.concatenate( - (self._send_list_box[self._x_m_z_m_proc], self._markers_x_m_z_m) + (self._send_list_box[self._x_m_z_m_proc], self._markers_x_m_z_m), ) # if self._x_m_z_p_proc is not None: self._send_info_box[self._x_m_z_p_proc] += len(self._markers_x_m_z_p) self._send_list_box[self._x_m_z_p_proc] = xp.concatenate( - (self._send_list_box[self._x_m_z_p_proc], self._markers_x_m_z_p) + (self._send_list_box[self._x_m_z_p_proc], self._markers_x_m_z_p), ) # if self._x_p_z_m_proc is not None: self._send_info_box[self._x_p_z_m_proc] += len(self._markers_x_p_z_m) self._send_list_box[self._x_p_z_m_proc] = xp.concatenate( - (self._send_list_box[self._x_p_z_m_proc], self._markers_x_p_z_m) + (self._send_list_box[self._x_p_z_m_proc], self._markers_x_p_z_m), ) # if self._x_p_z_p_proc is not None: self._send_info_box[self._x_p_z_p_proc] += len(self._markers_x_p_z_p) self._send_list_box[self._x_p_z_p_proc] = xp.concatenate( - (self._send_list_box[self._x_p_z_p_proc], self._markers_x_p_z_p) + (self._send_list_box[self._x_p_z_p_proc], self._markers_x_p_z_p), ) # y-z edges # if self._y_m_z_m_proc is not None: self._send_info_box[self._y_m_z_m_proc] += len(self._markers_y_m_z_m) self._send_list_box[self._y_m_z_m_proc] = xp.concatenate( - (self._send_list_box[self._y_m_z_m_proc], self._markers_y_m_z_m) + (self._send_list_box[self._y_m_z_m_proc], self._markers_y_m_z_m), ) # if self._y_m_z_p_proc is not None: self._send_info_box[self._y_m_z_p_proc] += len(self._markers_y_m_z_p) self._send_list_box[self._y_m_z_p_proc] = xp.concatenate( - (self._send_list_box[self._y_m_z_p_proc], self._markers_y_m_z_p) + (self._send_list_box[self._y_m_z_p_proc], self._markers_y_m_z_p), ) # if self._y_p_z_m_proc is not None: self._send_info_box[self._y_p_z_m_proc] += len(self._markers_y_p_z_m) self._send_list_box[self._y_p_z_m_proc] = xp.concatenate( - (self._send_list_box[self._y_p_z_m_proc], self._markers_y_p_z_m) + (self._send_list_box[self._y_p_z_m_proc], self._markers_y_p_z_m), ) # if self._y_p_z_p_proc is not None: self._send_info_box[self._y_p_z_p_proc] += len(self._markers_y_p_z_p) self._send_list_box[self._y_p_z_p_proc] = xp.concatenate( - (self._send_list_box[self._y_p_z_p_proc], self._markers_y_p_z_p) + (self._send_list_box[self._y_p_z_p_proc], self._markers_y_p_z_p), ) # corners # if self._x_m_y_m_z_m_proc is not None: self._send_info_box[self._x_m_y_m_z_m_proc] += len(self._markers_x_m_y_m_z_m) self._send_list_box[self._x_m_y_m_z_m_proc] = xp.concatenate( - (self._send_list_box[self._x_m_y_m_z_m_proc], self._markers_x_m_y_m_z_m) + (self._send_list_box[self._x_m_y_m_z_m_proc], self._markers_x_m_y_m_z_m), ) # if self._x_m_y_m_z_p_proc is not None: self._send_info_box[self._x_m_y_m_z_p_proc] += len(self._markers_x_m_y_m_z_p) self._send_list_box[self._x_m_y_m_z_p_proc] = xp.concatenate( - (self._send_list_box[self._x_m_y_m_z_p_proc], self._markers_x_m_y_m_z_p) + (self._send_list_box[self._x_m_y_m_z_p_proc], self._markers_x_m_y_m_z_p), ) # if self._x_m_y_p_z_m_proc is not None: self._send_info_box[self._x_m_y_p_z_m_proc] += len(self._markers_x_m_y_p_z_m) self._send_list_box[self._x_m_y_p_z_m_proc] = xp.concatenate( - (self._send_list_box[self._x_m_y_p_z_m_proc], self._markers_x_m_y_p_z_m) + (self._send_list_box[self._x_m_y_p_z_m_proc], self._markers_x_m_y_p_z_m), ) # if self._x_m_y_p_z_p_proc is not None: self._send_info_box[self._x_m_y_p_z_p_proc] += len(self._markers_x_m_y_p_z_p) self._send_list_box[self._x_m_y_p_z_p_proc] = xp.concatenate( - (self._send_list_box[self._x_m_y_p_z_p_proc], self._markers_x_m_y_p_z_p) + (self._send_list_box[self._x_m_y_p_z_p_proc], self._markers_x_m_y_p_z_p), ) # if self._x_p_y_m_z_m_proc is not None: self._send_info_box[self._x_p_y_m_z_m_proc] += len(self._markers_x_p_y_m_z_m) self._send_list_box[self._x_p_y_m_z_m_proc] = xp.concatenate( - (self._send_list_box[self._x_p_y_m_z_m_proc], self._markers_x_p_y_m_z_m) + (self._send_list_box[self._x_p_y_m_z_m_proc], self._markers_x_p_y_m_z_m), ) # if self._x_p_y_m_z_p_proc is not None: self._send_info_box[self._x_p_y_m_z_p_proc] += len(self._markers_x_p_y_m_z_p) self._send_list_box[self._x_p_y_m_z_p_proc] = xp.concatenate( - (self._send_list_box[self._x_p_y_m_z_p_proc], self._markers_x_p_y_m_z_p) + (self._send_list_box[self._x_p_y_m_z_p_proc], self._markers_x_p_y_m_z_p), ) # if self._x_p_y_p_z_m_proc is not None: self._send_info_box[self._x_p_y_p_z_m_proc] += len(self._markers_x_p_y_p_z_m) self._send_list_box[self._x_p_y_p_z_m_proc] = xp.concatenate( - (self._send_list_box[self._x_p_y_p_z_m_proc], self._markers_x_p_y_p_z_m) + (self._send_list_box[self._x_p_y_p_z_m_proc], self._markers_x_p_y_p_z_m), ) # if self._x_p_y_p_z_p_proc is not None: self._send_info_box[self._x_p_y_p_z_p_proc] += len(self._markers_x_p_y_p_z_p) self._send_list_box[self._x_p_y_p_z_p_proc] = xp.concatenate( - (self._send_list_box[self._x_p_y_p_z_p_proc], self._markers_x_p_y_p_z_p) + (self._send_list_box[self._x_p_y_p_z_p_proc], self._markers_x_p_y_p_z_p), ) def self_communication_boxes(self): @@ -3151,7 +3163,7 @@ def self_communication_boxes(self): f'Strong load imbalance detected: \ number of holes ({holes_inds.size}) on rank {self.mpi_rank} \ is smaller than number of incoming particles ({self._send_info_box[self.mpi_rank]}). \ -Increasing the value of "bufsize" in the markers parameters for the next run.' +Increasing the value of "bufsize" in the markers parameters for the next run.', ) self.mpi_comm.Abort() @@ -3242,7 +3254,7 @@ def sendrecv_markers_boxes(self): f'Strong load imbalance detected: \ number of holes ({hole_inds.size}) on rank {self.mpi_rank} \ is smaller than number of incoming particles ({first_hole[i] + self._recv_info_box[i]}). \ -Increasing the value of "bufsize" in the markers parameters for the next run.' +Increasing the value of "bufsize" in the markers parameters for the next run.', ) self.mpi_comm.Abort() # exit() @@ -4007,7 +4019,7 @@ def sendrecv_markers(self, recv_info, hole_inds_after_send): f'Strong load imbalance detected: \ number of holes ({hole_inds_after_send.size}) on rank {self.mpi_rank} \ is smaller than number of incoming particles ({first_hole[i] + recv_info[i]}). \ -Increasing the value of "bufsize" in the markers parameters for the next run.' +Increasing the value of "bufsize" in the markers parameters for the next run.', ) self.mpi_comm.Abort() diff --git a/src/struphy/pic/particles.py b/src/struphy/pic/particles.py index 21e50ffda..79634d13a 100644 --- a/src/struphy/pic/particles.py +++ b/src/struphy/pic/particles.py @@ -219,7 +219,8 @@ def save_constants_of_motion(self): # send particles to the guiding center positions self.markers[~self.holes, self.first_pusher_idx : self.first_pusher_idx + 3] = self.markers[ - ~self.holes, slice_gc + ~self.holes, + slice_gc, ] if self.mpi_comm is not None: self.mpi_sort_markers(alpha=1) diff --git a/src/struphy/pic/pushing/pusher.py b/src/struphy/pic/pushing/pusher.py index 190525de9..14c756b31 100644 --- a/src/struphy/pic/pushing/pusher.py +++ b/src/struphy/pic/pushing/pusher.py @@ -136,7 +136,7 @@ def __init__( # check marker array column number assert isinstance(comps, xp.ndarray) assert column_nr + comps.size < particles.n_cols, ( - f"{column_nr + comps.size} not smaller than {particles.n_cols = }; not enough columns in marker array !!" + f"{column_nr + comps.size} not smaller than {particles.n_cols =}; not enough columns in marker array !!" ) # prepare and check eval_kernels @@ -148,7 +148,7 @@ def __init__( # check marker array column number assert isinstance(comps, xp.ndarray) assert column_nr + comps.size < particles.n_cols, ( - f"{column_nr + comps.size} not smaller than {particles.n_cols = }; not enough columns in marker array !!" + f"{column_nr + comps.size} not smaller than {particles.n_cols =}; not enough columns in marker array !!" ) self._init_kernels = init_kernels @@ -178,10 +178,10 @@ def __call__(self, dt: float): residual_idx = self.particles.residual_idx if self.verbose: - print(f"{first_pusher_idx = }") - print(f"{first_shift_idx = }") - print(f"{residual_idx = }") - print(f"{self.particles.n_cols = }") + print(f"{first_pusher_idx =}") + print(f"{first_shift_idx =}") + print(f"{residual_idx =}") + print(f"{self.particles.n_cols =}") init_slice = slice(first_pusher_idx, first_shift_idx) shift_slice = slice(first_shift_idx, residual_idx) @@ -231,7 +231,7 @@ def __call__(self, dt: float): if self.verbose and self.maxiter > 1: max_res = 1.0 print( - f"rank {rank}: {k = }, tol: {self._tol}, {n_not_converged[0] = }, {max_res = }", + f"rank {rank}: {k =}, tol: {self._tol}, {n_not_converged[0] =}, {max_res =}", ) if self.particles.mpi_comm is not None: self.particles.mpi_comm.Barrier() @@ -309,7 +309,7 @@ def __call__(self, dt: float): if self.verbose: print( - f"rank {rank}: {k = }, tol: {self._tol}, {n_not_converged[0] = }, {max_res = }", + f"rank {rank}: {k =}, tol: {self._tol}, {n_not_converged[0] =}, {max_res =}", ) if self.particles.mpi_comm is not None: self.particles.mpi_comm.Barrier() @@ -329,7 +329,7 @@ def __call__(self, dt: float): if self.maxiter > 1: rank = self.particles.mpi_rank print( - f"rank {rank}: {k = }, maxiter={self.maxiter} reached! tol: {self._tol}, {n_not_converged[0] = }, {max_res = }", + f"rank {rank}: {k =}, maxiter={self.maxiter} reached! tol: {self._tol}, {n_not_converged[0] =}, {max_res =}", ) # sort markers according to domain decomposition if self.mpi_sort == "each": diff --git a/src/struphy/pic/sobol_seq.py b/src/struphy/pic/sobol_seq.py index ce965cc8f..f4c01347a 100644 --- a/src/struphy/pic/sobol_seq.py +++ b/src/struphy/pic/sobol_seq.py @@ -264,7 +264,7 @@ def i4_sobol(dim_num, seed): 1, 1, 1, - ] + ], ) v[2:40, 1] = xp.transpose( @@ -307,7 +307,7 @@ def i4_sobol(dim_num, seed): 3, 1, 3, - ] + ], ) v[3:40, 2] = xp.transpose( @@ -349,7 +349,7 @@ def i4_sobol(dim_num, seed): 1, 3, 3, - ] + ], ) v[5:40, 3] = xp.transpose( @@ -389,7 +389,7 @@ def i4_sobol(dim_num, seed): 1, 7, 9, - ] + ], ) v[7:40, 4] = xp.transpose( @@ -427,15 +427,15 @@ def i4_sobol(dim_num, seed): 9, 31, 9, - ] + ], ) v[13:40, 5] = xp.transpose( - [37, 33, 7, 5, 11, 39, 63, 27, 17, 15, 23, 29, 3, 21, 13, 31, 25, 9, 49, 33, 19, 29, 11, 19, 27, 15, 25] + [37, 33, 7, 5, 11, 39, 63, 27, 17, 15, 23, 29, 3, 21, 13, 31, 25, 9, 49, 33, 19, 29, 11, 19, 27, 15, 25], ) v[19:40, 6] = xp.transpose( - [13, 33, 115, 41, 79, 17, 29, 119, 75, 73, 105, 7, 59, 65, 21, 3, 113, 61, 89, 45, 107] + [13, 33, 115, 41, 79, 17, 29, 119, 75, 73, 105, 7, 59, 65, 21, 3, 113, 61, 89, 45, 107], ) v[37:40, 7] = xp.transpose([7, 23, 39]) diff --git a/src/struphy/pic/sph_eval_kernels.py b/src/struphy/pic/sph_eval_kernels.py index 37414f447..4c63e0156 100644 --- a/src/struphy/pic/sph_eval_kernels.py +++ b/src/struphy/pic/sph_eval_kernels.py @@ -297,7 +297,19 @@ def naive_evaluation_meshgrid( e2 = eta2[i, j, k] e3 = eta3[i, j, k] out[i, j, k] = naive_evaluation_kernel( - args_markers, e1, e2, e3, holes, periodic1, periodic2, periodic3, index, kernel_type, h1, h2, h3 + args_markers, + e1, + e2, + e3, + holes, + periodic1, + periodic2, + periodic3, + index, + kernel_type, + h1, + h2, + h3, ) diff --git a/src/struphy/pic/tests/test_accum_vec_H1.py b/src/struphy/pic/tests/test_accum_vec_H1.py index 7ed52b153..cb5cbb17e 100644 --- a/src/struphy/pic/tests/test_accum_vec_H1.py +++ b/src/struphy/pic/tests/test_accum_vec_H1.py @@ -6,7 +6,8 @@ @pytest.mark.parametrize("Nel", [[8, 9, 10]]) @pytest.mark.parametrize("p", [[2, 3, 4]]) @pytest.mark.parametrize( - "spl_kind", [[False, False, True], [False, True, True], [True, False, True], [True, True, True]] + "spl_kind", + [[False, False, True], [False, True, True], [True, False, True], [True, True, True]], ) @pytest.mark.parametrize( "mapping", @@ -156,7 +157,7 @@ def test_accum_poisson(Nel, p, spl_kind, mapping, num_clones, Np=1000): if clone_config is not None: clone_config.sub_comm.Allreduce(MPI.IN_PLACE, _sum_within_clone, op=MPI.SUM) - print(f"rank {mpi_rank}: {_sum_within_clone = }, {_sqrtg = }") + print(f"rank {mpi_rank}: {_sum_within_clone =}, {_sqrtg =}") # Check within clone assert xp.isclose(_sum_within_clone, _sqrtg) @@ -169,7 +170,7 @@ def test_accum_poisson(Nel, p, spl_kind, mapping, num_clones, Np=1000): mpi_comm.Allreduce(MPI.IN_PLACE, _sum_between_clones, op=MPI.SUM) clone_config.inter_comm.Allreduce(MPI.IN_PLACE, _sqrtg, op=MPI.SUM) - print(f"rank {mpi_rank}: {_sum_between_clones = }, {_sqrtg = }") + print(f"rank {mpi_rank}: {_sum_between_clones =}, {_sqrtg =}") # Check within clone assert xp.isclose(_sum_between_clones, _sqrtg) diff --git a/src/struphy/pic/tests/test_accumulation.py b/src/struphy/pic/tests/test_accumulation.py index 805c578d5..ed3a41ff4 100644 --- a/src/struphy/pic/tests/test_accumulation.py +++ b/src/struphy/pic/tests/test_accumulation.py @@ -6,7 +6,8 @@ @pytest.mark.parametrize("Nel", [[8, 9, 10]]) @pytest.mark.parametrize("p", [[2, 3, 4]]) @pytest.mark.parametrize( - "spl_kind", [[False, False, True], [False, True, False], [True, False, True], [True, True, False]] + "spl_kind", + [[False, False, True], [False, True, False], [True, False, True], [True, True, False]], ) @pytest.mark.parametrize( "mapping", diff --git a/src/struphy/pic/tests/test_binning.py b/src/struphy/pic/tests/test_binning.py index a6a1dde6e..cda2524e7 100644 --- a/src/struphy/pic/tests/test_binning.py +++ b/src/struphy/pic/tests/test_binning.py @@ -631,8 +631,8 @@ def test_binning_6D_full_f_mpi(mapping, show_plot=False): "given_in_basis": "0", "ls": [l_n1], "amps": [amp_n1], - } - } + }, + }, }, "Maxwellian3D_2": { "n": { @@ -640,8 +640,8 @@ def test_binning_6D_full_f_mpi(mapping, show_plot=False): "given_in_basis": "0", "ls": [l_n2], "amps": [amp_n2], - } - } + }, + }, }, } pert_1 = perturbations.ModesCos(ls=(l_n1,), amps=(amp_n1,)) @@ -814,8 +814,8 @@ def test_binning_6D_delta_f_mpi(mapping, show_plot=False): "given_in_basis": "0", "ls": [l_n], "amps": [amp_n], - } - } + }, + }, } pert = perturbations.ModesCos(ls=(l_n,), amps=(amp_n,)) background = Maxwellian3D(n=(1.0, pert)) @@ -891,7 +891,7 @@ def test_binning_6D_delta_f_mpi(mapping, show_plot=False): "given_in_basis": "0", "ls": [l_n1], "amps": [amp_n1], - } + }, }, }, "Maxwellian3D_2": { @@ -901,7 +901,7 @@ def test_binning_6D_delta_f_mpi(mapping, show_plot=False): "given_in_basis": "0", "ls": [l_n2], "amps": [amp_n2], - } + }, }, }, } diff --git a/src/struphy/pic/tests/test_mat_vec_filler.py b/src/struphy/pic/tests/test_mat_vec_filler.py index 491e1f20e..c6bee1faa 100644 --- a/src/struphy/pic/tests/test_mat_vec_filler.py +++ b/src/struphy/pic/tests/test_mat_vec_filler.py @@ -70,8 +70,11 @@ def test_particle_to_mat_kernels(Nel, p, spl_kind, n_markers=1): for j in range(3): mat["v1"][-1] += [ StencilMatrix( - DR.Vh["1"].spaces[i], DR.Vh["1"].spaces[j], backend=PSYDAC_BACKEND_GPYCCEL, precompiled=True - )._data + DR.Vh["1"].spaces[i], + DR.Vh["1"].spaces[j], + backend=PSYDAC_BACKEND_GPYCCEL, + precompiled=True, + )._data, ] vec["v1"] = [] @@ -84,8 +87,11 @@ def test_particle_to_mat_kernels(Nel, p, spl_kind, n_markers=1): for j in range(3): mat["v2"][-1] += [ StencilMatrix( - DR.Vh["2"].spaces[i], DR.Vh["2"].spaces[j], backend=PSYDAC_BACKEND_GPYCCEL, precompiled=True - )._data + DR.Vh["2"].spaces[i], + DR.Vh["2"].spaces[j], + backend=PSYDAC_BACKEND_GPYCCEL, + precompiled=True, + )._data, ] vec["v2"] = [] @@ -213,7 +219,13 @@ def test_particle_to_mat_kernels(Nel, p, spl_kind, n_markers=1): for n, ij in enumerate(ind_pairs): assert_mat( - args[n], rows, cols, basis[space][ij[0]], basis[space][ij[1]], rank, verbose=False + args[n], + rows, + cols, + basis[space][ij[0]], + basis[space][ij[1]], + rank, + verbose=False, ) # assertion test of mat if mv == "m_v": for i in range(3): @@ -230,7 +242,13 @@ def test_particle_to_mat_kernels(Nel, p, spl_kind, n_markers=1): for n, ij in enumerate(ind_pairs): assert_mat( - args[n], rows, cols, basis[space][ij[0]], basis[space][ij[1]], rank, verbose=False + args[n], + rows, + cols, + basis[space][ij[0]], + basis[space][ij[1]], + rank, + verbose=False, ) # assertion test of mat if mv == "m_v": for i in range(3): diff --git a/src/struphy/pic/tests/test_pic_legacy_files/accumulation.py b/src/struphy/pic/tests/test_pic_legacy_files/accumulation.py index 2ee5ba7b2..6bb225571 100644 --- a/src/struphy/pic/tests/test_pic_legacy_files/accumulation.py +++ b/src/struphy/pic/tests/test_pic_legacy_files/accumulation.py @@ -159,7 +159,8 @@ def to_sparse_step1(self): # final block matrix M = spa.bmat( - [[None, M[0][1], M[0][2]], [-M[0][1].T, None, M[1][2]], [-M[0][2].T, -M[1][2].T, None]], format="csr" + [[None, M[0][1], M[0][2]], [-M[0][1].T, None, M[1][2]], [-M[0][2].T, -M[1][2].T, None]], + format="csr", ) # apply extraction operator @@ -226,7 +227,8 @@ def to_sparse_step3(self): # final block matrix M = spa.bmat( - [[M[0][0], M[0][1], M[0][2]], [M[0][1].T, M[1][1], M[1][2]], [M[0][2].T, M[1][2].T, M[2][2]]], format="csr" + [[M[0][0], M[0][1], M[0][2]], [M[0][1].T, M[1][1], M[1][2]], [M[0][2].T, M[1][2].T, M[2][2]]], + format="csr", ) # apply extraction operator @@ -528,15 +530,15 @@ def assemble_step3(self, b2_eq, b2): # build global sparse matrix and global vector if self.basis_u == 0: return self.to_sparse_step3(), self.space.Ev_0.dot( - xp.concatenate((self.vecs[0].flatten(), self.vecs[1].flatten(), self.vecs[2].flatten())) + xp.concatenate((self.vecs[0].flatten(), self.vecs[1].flatten(), self.vecs[2].flatten())), ) elif self.basis_u == 1: return self.to_sparse_step3(), self.space.E1_0.dot( - xp.concatenate((self.vecs[0].flatten(), self.vecs[1].flatten(), self.vecs[2].flatten())) + xp.concatenate((self.vecs[0].flatten(), self.vecs[1].flatten(), self.vecs[2].flatten())), ) elif self.basis_u == 2: return self.to_sparse_step3(), self.space.E2_0.dot( - xp.concatenate((self.vecs[0].flatten(), self.vecs[1].flatten(), self.vecs[2].flatten())) + xp.concatenate((self.vecs[0].flatten(), self.vecs[1].flatten(), self.vecs[2].flatten())), ) diff --git a/src/struphy/pic/tests/test_pic_legacy_files/accumulation_kernels_3d.py b/src/struphy/pic/tests/test_pic_legacy_files/accumulation_kernels_3d.py index c70261023..349cca379 100644 --- a/src/struphy/pic/tests/test_pic_legacy_files/accumulation_kernels_3d.py +++ b/src/struphy/pic/tests/test_pic_legacy_files/accumulation_kernels_3d.py @@ -185,13 +185,49 @@ def kernel_step1( bsp.b_d_splines_slim(t3, int(pn3), eta3, int(span3), bn3, bd3) b[0] = eva3.evaluation_kernel_3d( - pn1, pd2, pd3, bn1, bd2, bd3, span1, span2 - 1, span3 - 1, nbase_n[0], nbase_d[1], nbase_d[2], b2_1 + pn1, + pd2, + pd3, + bn1, + bd2, + bd3, + span1, + span2 - 1, + span3 - 1, + nbase_n[0], + nbase_d[1], + nbase_d[2], + b2_1, ) b[1] = eva3.evaluation_kernel_3d( - pd1, pn2, pd3, bd1, bn2, bd3, span1 - 1, span2, span3 - 1, nbase_d[0], nbase_n[1], nbase_d[2], b2_2 + pd1, + pn2, + pd3, + bd1, + bn2, + bd3, + span1 - 1, + span2, + span3 - 1, + nbase_d[0], + nbase_n[1], + nbase_d[2], + b2_2, ) b[2] = eva3.evaluation_kernel_3d( - pd1, pd2, pn3, bd1, bd2, bn3, span1 - 1, span2 - 1, span3, nbase_d[0], nbase_d[1], nbase_n[2], b2_3 + pd1, + pd2, + pn3, + bd1, + bd2, + bn3, + span1 - 1, + span2 - 1, + span3, + nbase_d[0], + nbase_d[1], + nbase_n[2], + b2_3, ) b_prod[0, 1] = -b[2] @@ -554,13 +590,49 @@ def kernel_step3( bsp.b_d_splines_slim(t3, int(pn3), eta3, int(span3), bn3, bd3) b[0] = eva3.evaluation_kernel_3d( - pn1, pd2, pd3, bn1, bd2, bd3, span1, span2 - 1, span3 - 1, nbase_n[0], nbase_d[1], nbase_d[2], b2_1 + pn1, + pd2, + pd3, + bn1, + bd2, + bd3, + span1, + span2 - 1, + span3 - 1, + nbase_n[0], + nbase_d[1], + nbase_d[2], + b2_1, ) b[1] = eva3.evaluation_kernel_3d( - pd1, pn2, pd3, bd1, bn2, bd3, span1 - 1, span2, span3 - 1, nbase_d[0], nbase_n[1], nbase_d[2], b2_2 + pd1, + pn2, + pd3, + bd1, + bn2, + bd3, + span1 - 1, + span2, + span3 - 1, + nbase_d[0], + nbase_n[1], + nbase_d[2], + b2_2, ) b[2] = eva3.evaluation_kernel_3d( - pd1, pd2, pn3, bd1, bd2, bn3, span1 - 1, span2 - 1, span3, nbase_d[0], nbase_d[1], nbase_n[2], b2_3 + pd1, + pd2, + pn3, + bd1, + bd2, + bn3, + span1 - 1, + span2 - 1, + span3, + nbase_d[0], + nbase_d[1], + nbase_n[2], + b2_3, ) b_prod[0, 1] = -b[2] @@ -1130,7 +1202,14 @@ def kernel_step_ph_full( for vp in range(3): for vq in range(3): mat11[ - i1, i2, i3, pn1 + jl1 - il1, pn2 + jl2 - il2, pn3 + jl3 - il3, vp, vq + i1, + i2, + i3, + pn1 + jl1 - il1, + pn2 + jl2 - il2, + pn3 + jl3 - il3, + vp, + vq, ] += bj3 * v[vp] * v[vq] for jl1 in range(pn1 + 1): @@ -1142,7 +1221,14 @@ def kernel_step_ph_full( for vp in range(3): for vq in range(3): mat12[ - i1, i2, i3, pn1 + jl1 - il1, pn2 + jl2 - il2, pn3 + jl3 - il3, vp, vq + i1, + i2, + i3, + pn1 + jl1 - il1, + pn2 + jl2 - il2, + pn3 + jl3 - il3, + vp, + vq, ] += bj3 * v[vp] * v[vq] for jl1 in range(pn1 + 1): @@ -1154,7 +1240,14 @@ def kernel_step_ph_full( for vp in range(3): for vq in range(3): mat13[ - i1, i2, i3, pn1 + jl1 - il1, pn2 + jl2 - il2, pn3 + jl3 - il3, vp, vq + i1, + i2, + i3, + pn1 + jl1 - il1, + pn2 + jl2 - il2, + pn3 + jl3 - il3, + vp, + vq, ] += bj3 * v[vp] * v[vq] # add contribution to 22 component (NDN NDN) and 23 component (NDN NND) @@ -1179,7 +1272,14 @@ def kernel_step_ph_full( for vp in range(3): for vq in range(3): mat22[ - i1, i2, i3, pn1 + jl1 - il1, pn2 + jl2 - il2, pn3 + jl3 - il3, vp, vq + i1, + i2, + i3, + pn1 + jl1 - il1, + pn2 + jl2 - il2, + pn3 + jl3 - il3, + vp, + vq, ] += bj3 * v[vp] * v[vq] for jl1 in range(pn1 + 1): @@ -1191,7 +1291,14 @@ def kernel_step_ph_full( for vp in range(3): for vq in range(3): mat23[ - i1, i2, i3, pn1 + jl1 - il1, pn2 + jl2 - il2, pn3 + jl3 - il3, vp, vq + i1, + i2, + i3, + pn1 + jl1 - il1, + pn2 + jl2 - il2, + pn3 + jl3 - il3, + vp, + vq, ] += bj3 * v[vp] * v[vq] # add contribution to 33 component (NND NND) @@ -1216,7 +1323,14 @@ def kernel_step_ph_full( for vp in range(3): for vq in range(3): mat33[ - i1, i2, i3, pn1 + jl1 - il1, pn2 + jl2 - il2, pn3 + jl3 - il3, vp, vq + i1, + i2, + i3, + pn1 + jl1 - il1, + pn2 + jl2 - il2, + pn3 + jl3 - il3, + vp, + vq, ] += bj3 * v[vp] * v[vq] elif basis_u == 2: @@ -1242,7 +1356,14 @@ def kernel_step_ph_full( for vp in range(3): for vq in range(3): mat11[ - i1, i2, i3, pn1 + jl1 - il1, pn2 + jl2 - il2, pn3 + jl3 - il3, vp, vq + i1, + i2, + i3, + pn1 + jl1 - il1, + pn2 + jl2 - il2, + pn3 + jl3 - il3, + vp, + vq, ] += bj3 * v[vp] * v[vq] for jl1 in range(pd1 + 1): @@ -1254,7 +1375,14 @@ def kernel_step_ph_full( for vp in range(3): for vq in range(3): mat12[ - i1, i2, i3, pn1 + jl1 - il1, pn2 + jl2 - il2, pn3 + jl3 - il3, vp, vq + i1, + i2, + i3, + pn1 + jl1 - il1, + pn2 + jl2 - il2, + pn3 + jl3 - il3, + vp, + vq, ] += bj3 * v[vp] * v[vq] for jl1 in range(pd1 + 1): @@ -1266,7 +1394,14 @@ def kernel_step_ph_full( for vp in range(3): for vq in range(3): mat13[ - i1, i2, i3, pn1 + jl1 - il1, pn2 + jl2 - il2, pn3 + jl3 - il3, vp, vq + i1, + i2, + i3, + pn1 + jl1 - il1, + pn2 + jl2 - il2, + pn3 + jl3 - il3, + vp, + vq, ] += bj3 * v[vp] * v[vq] # add contribution to 22 component (DND DND) and 23 component (DND DDN) @@ -1291,7 +1426,14 @@ def kernel_step_ph_full( for vp in range(3): for vq in range(3): mat22[ - i1, i2, i3, pn1 + jl1 - il1, pn2 + jl2 - il2, pn3 + jl3 - il3, vp, vq + i1, + i2, + i3, + pn1 + jl1 - il1, + pn2 + jl2 - il2, + pn3 + jl3 - il3, + vp, + vq, ] += bj3 * v[vp] * v[vq] for jl1 in range(pd1 + 1): @@ -1303,7 +1445,14 @@ def kernel_step_ph_full( for vp in range(3): for vq in range(3): mat23[ - i1, i2, i3, pn1 + jl1 - il1, pn2 + jl2 - il2, pn3 + jl3 - il3, vp, vq + i1, + i2, + i3, + pn1 + jl1 - il1, + pn2 + jl2 - il2, + pn3 + jl3 - il3, + vp, + vq, ] += bj3 * v[vp] * v[vq] # add contribution to 33 component (DDN DDN) @@ -1328,7 +1477,14 @@ def kernel_step_ph_full( for vp in range(3): for vq in range(3): mat33[ - i1, i2, i3, pn1 + jl1 - il1, pn2 + jl2 - il2, pn3 + jl3 - il3, vp, vq + i1, + i2, + i3, + pn1 + jl1 - il1, + pn2 + jl2 - il2, + pn3 + jl3 - il3, + vp, + vq, ] += bj3 * v[vp] * v[vq] # -- removed omp: #$ omp end parallel diff --git a/src/struphy/pic/tests/test_pic_legacy_files/mappings_3d.py b/src/struphy/pic/tests/test_pic_legacy_files/mappings_3d.py index 587b8b15f..2e54d34dd 100644 --- a/src/struphy/pic/tests/test_pic_legacy_files/mappings_3d.py +++ b/src/struphy/pic/tests/test_pic_legacy_files/mappings_3d.py @@ -74,17 +74,53 @@ def f( if kind_map == 0: if component == 1: value = eva_3d.evaluate_n_n_n( - tn1, tn2, tn3, pn[0], pn[1], pn[2], nbase_n[0], nbase_n[1], nbase_n[2], cx, eta1, eta2, eta3 + tn1, + tn2, + tn3, + pn[0], + pn[1], + pn[2], + nbase_n[0], + nbase_n[1], + nbase_n[2], + cx, + eta1, + eta2, + eta3, ) elif component == 2: value = eva_3d.evaluate_n_n_n( - tn1, tn2, tn3, pn[0], pn[1], pn[2], nbase_n[0], nbase_n[1], nbase_n[2], cy, eta1, eta2, eta3 + tn1, + tn2, + tn3, + pn[0], + pn[1], + pn[2], + nbase_n[0], + nbase_n[1], + nbase_n[2], + cy, + eta1, + eta2, + eta3, ) elif component == 3: value = eva_3d.evaluate_n_n_n( - tn1, tn2, tn3, pn[0], pn[1], pn[2], nbase_n[0], nbase_n[1], nbase_n[2], cz, eta1, eta2, eta3 + tn1, + tn2, + tn3, + pn[0], + pn[1], + pn[2], + nbase_n[0], + nbase_n[1], + nbase_n[2], + cz, + eta1, + eta2, + eta3, ) # ==== 2d spline (straight in 3rd direction) === @@ -110,7 +146,7 @@ def f( elif kind_map == 2: if component == 1: value = eva_2d.evaluate_n_n(tn1, tn2, pn[0], pn[1], nbase_n[0], nbase_n[1], cx[:, :, 0], eta1, eta2) * cos( - 2 * pi * eta3 + 2 * pi * eta3, ) if eta1 == 0.0 and cx[0, 0, 0] == cx[0, 1, 0]: @@ -124,7 +160,7 @@ def f( elif component == 3: value = eva_2d.evaluate_n_n(tn1, tn2, pn[0], pn[1], nbase_n[0], nbase_n[1], cx[:, :, 0], eta1, eta2) * sin( - 2 * pi * eta3 + 2 * pi * eta3, ) if eta1 == 0.0 and cx[0, 0, 0] == cx[0, 1, 0]: @@ -299,39 +335,147 @@ def df( if kind_map == 0: if component == 11: value = eva_3d.evaluate_diffn_n_n( - tn1, tn2, tn3, pn[0], pn[1], pn[2], nbase_n[0], nbase_n[1], nbase_n[2], cx, eta1, eta2, eta3 + tn1, + tn2, + tn3, + pn[0], + pn[1], + pn[2], + nbase_n[0], + nbase_n[1], + nbase_n[2], + cx, + eta1, + eta2, + eta3, ) elif component == 12: value = eva_3d.evaluate_n_diffn_n( - tn1, tn2, tn3, pn[0], pn[1], pn[2], nbase_n[0], nbase_n[1], nbase_n[2], cx, eta1, eta2, eta3 + tn1, + tn2, + tn3, + pn[0], + pn[1], + pn[2], + nbase_n[0], + nbase_n[1], + nbase_n[2], + cx, + eta1, + eta2, + eta3, ) elif component == 13: value = eva_3d.evaluate_n_n_diffn( - tn1, tn2, tn3, pn[0], pn[1], pn[2], nbase_n[0], nbase_n[1], nbase_n[2], cx, eta1, eta2, eta3 + tn1, + tn2, + tn3, + pn[0], + pn[1], + pn[2], + nbase_n[0], + nbase_n[1], + nbase_n[2], + cx, + eta1, + eta2, + eta3, ) elif component == 21: value = eva_3d.evaluate_diffn_n_n( - tn1, tn2, tn3, pn[0], pn[1], pn[2], nbase_n[0], nbase_n[1], nbase_n[2], cy, eta1, eta2, eta3 + tn1, + tn2, + tn3, + pn[0], + pn[1], + pn[2], + nbase_n[0], + nbase_n[1], + nbase_n[2], + cy, + eta1, + eta2, + eta3, ) elif component == 22: value = eva_3d.evaluate_n_diffn_n( - tn1, tn2, tn3, pn[0], pn[1], pn[2], nbase_n[0], nbase_n[1], nbase_n[2], cy, eta1, eta2, eta3 + tn1, + tn2, + tn3, + pn[0], + pn[1], + pn[2], + nbase_n[0], + nbase_n[1], + nbase_n[2], + cy, + eta1, + eta2, + eta3, ) elif component == 23: value = eva_3d.evaluate_n_n_diffn( - tn1, tn2, tn3, pn[0], pn[1], pn[2], nbase_n[0], nbase_n[1], nbase_n[2], cy, eta1, eta2, eta3 + tn1, + tn2, + tn3, + pn[0], + pn[1], + pn[2], + nbase_n[0], + nbase_n[1], + nbase_n[2], + cy, + eta1, + eta2, + eta3, ) elif component == 31: value = eva_3d.evaluate_diffn_n_n( - tn1, tn2, tn3, pn[0], pn[1], pn[2], nbase_n[0], nbase_n[1], nbase_n[2], cz, eta1, eta2, eta3 + tn1, + tn2, + tn3, + pn[0], + pn[1], + pn[2], + nbase_n[0], + nbase_n[1], + nbase_n[2], + cz, + eta1, + eta2, + eta3, ) elif component == 32: value = eva_3d.evaluate_n_diffn_n( - tn1, tn2, tn3, pn[0], pn[1], pn[2], nbase_n[0], nbase_n[1], nbase_n[2], cz, eta1, eta2, eta3 + tn1, + tn2, + tn3, + pn[0], + pn[1], + pn[2], + nbase_n[0], + nbase_n[1], + nbase_n[2], + cz, + eta1, + eta2, + eta3, ) elif component == 33: value = eva_3d.evaluate_n_n_diffn( - tn1, tn2, tn3, pn[0], pn[1], pn[2], nbase_n[0], nbase_n[1], nbase_n[2], cz, eta1, eta2, eta3 + tn1, + tn2, + tn3, + pn[0], + pn[1], + pn[2], + nbase_n[0], + nbase_n[1], + nbase_n[2], + cz, + eta1, + eta2, + eta3, ) # ==== 2d spline (straight in 3rd direction) === @@ -369,11 +513,27 @@ def df( elif kind_map == 2: if component == 11: value = eva_2d.evaluate_diffn_n( - tn1, tn2, pn[0], pn[1], nbase_n[0], nbase_n[1], cx[:, :, 0], eta1, eta2 + tn1, + tn2, + pn[0], + pn[1], + nbase_n[0], + nbase_n[1], + cx[:, :, 0], + eta1, + eta2, ) * cos(2 * pi * eta3) elif component == 12: value = eva_2d.evaluate_n_diffn( - tn1, tn2, pn[0], pn[1], nbase_n[0], nbase_n[1], cx[:, :, 0], eta1, eta2 + tn1, + tn2, + pn[0], + pn[1], + nbase_n[0], + nbase_n[1], + cx[:, :, 0], + eta1, + eta2, ) * cos(2 * pi * eta3) if eta1 == 0.0 and cx[0, 0, 0] == cx[0, 1, 0]: @@ -397,11 +557,27 @@ def df( value = 0.0 elif component == 31: value = eva_2d.evaluate_diffn_n( - tn1, tn2, pn[0], pn[1], nbase_n[0], nbase_n[1], cx[:, :, 0], eta1, eta2 + tn1, + tn2, + pn[0], + pn[1], + nbase_n[0], + nbase_n[1], + cx[:, :, 0], + eta1, + eta2, ) * sin(2 * pi * eta3) elif component == 32: value = eva_2d.evaluate_n_diffn( - tn1, tn2, pn[0], pn[1], nbase_n[0], nbase_n[1], cx[:, :, 0], eta1, eta2 + tn1, + tn2, + pn[0], + pn[1], + nbase_n[0], + nbase_n[1], + cx[:, :, 0], + eta1, + eta2, ) * sin(2 * pi * eta3) if eta1 == 0.0 and cx[0, 0, 0] == cx[0, 1, 0]: diff --git a/src/struphy/pic/tests/test_pic_legacy_files/mappings_3d_fast.py b/src/struphy/pic/tests/test_pic_legacy_files/mappings_3d_fast.py index fbd912b39..f87380685 100644 --- a/src/struphy/pic/tests/test_pic_legacy_files/mappings_3d_fast.py +++ b/src/struphy/pic/tests/test_pic_legacy_files/mappings_3d_fast.py @@ -264,19 +264,51 @@ def df_all( if mat_or_vec == 0 or mat_or_vec == 2: # sum-up non-vanishing contributions (line 1: df_11, df_12 and df_13) mat_out[0, 0] = evaluation_kernel_2d( - pn[0], pn[1], der1, b2[pn[1]], span_n1, span_n2, nbase_n[0], nbase_n[1], cx[:, :, 0] + pn[0], + pn[1], + der1, + b2[pn[1]], + span_n1, + span_n2, + nbase_n[0], + nbase_n[1], + cx[:, :, 0], ) mat_out[0, 1] = evaluation_kernel_2d( - pn[0], pn[1], b1[pn[0]], der2, span_n1, span_n2, nbase_n[0], nbase_n[1], cx[:, :, 0] + pn[0], + pn[1], + b1[pn[0]], + der2, + span_n1, + span_n2, + nbase_n[0], + nbase_n[1], + cx[:, :, 0], ) mat_out[0, 2] = 0.0 # sum-up non-vanishing contributions (line 2: df_21, df_22 and df_23) mat_out[1, 0] = evaluation_kernel_2d( - pn[0], pn[1], der1, b2[pn[1]], span_n1, span_n2, nbase_n[0], nbase_n[1], cy[:, :, 0] + pn[0], + pn[1], + der1, + b2[pn[1]], + span_n1, + span_n2, + nbase_n[0], + nbase_n[1], + cy[:, :, 0], ) mat_out[1, 1] = evaluation_kernel_2d( - pn[0], pn[1], b1[pn[0]], der2, span_n1, span_n2, nbase_n[0], nbase_n[1], cy[:, :, 0] + pn[0], + pn[1], + b1[pn[0]], + der2, + span_n1, + span_n2, + nbase_n[0], + nbase_n[1], + cy[:, :, 0], ) mat_out[1, 2] = 0.0 @@ -288,10 +320,26 @@ def df_all( # evaluate mapping if mat_or_vec == 1 or mat_or_vec == 2: vec_out[0] = evaluation_kernel_2d( - pn[0], pn[1], b1[pn[0]], b2[pn[1]], span_n1, span_n2, nbase_n[0], nbase_n[1], cx[:, :, 0] + pn[0], + pn[1], + b1[pn[0]], + b2[pn[1]], + span_n1, + span_n2, + nbase_n[0], + nbase_n[1], + cx[:, :, 0], ) vec_out[1] = evaluation_kernel_2d( - pn[0], pn[1], b1[pn[0]], b2[pn[1]], span_n1, span_n2, nbase_n[0], nbase_n[1], cy[:, :, 0] + pn[0], + pn[1], + b1[pn[0]], + b2[pn[1]], + span_n1, + span_n2, + nbase_n[0], + nbase_n[1], + cy[:, :, 0], ) vec_out[2] = lz * eta3 @@ -305,14 +353,38 @@ def df_all( if mat_or_vec == 0 or mat_or_vec == 2: # sum-up non-vanishing contributions (line 1: df_11, df_12 and df_13) mat_out[0, 0] = evaluation_kernel_2d( - pn[0], pn[1], der1, b2[pn[1]], span_n1, span_n2, nbase_n[0], nbase_n[1], cx[:, :, 0] + pn[0], + pn[1], + der1, + b2[pn[1]], + span_n1, + span_n2, + nbase_n[0], + nbase_n[1], + cx[:, :, 0], ) * cos(2 * pi * eta3) mat_out[0, 1] = evaluation_kernel_2d( - pn[0], pn[1], b1[pn[0]], der2, span_n1, span_n2, nbase_n[0], nbase_n[1], cx[:, :, 0] + pn[0], + pn[1], + b1[pn[0]], + der2, + span_n1, + span_n2, + nbase_n[0], + nbase_n[1], + cx[:, :, 0], ) * cos(2 * pi * eta3) mat_out[0, 2] = ( evaluation_kernel_2d( - pn[0], pn[1], b1[pn[0]], b2[pn[1]], span_n1, span_n2, nbase_n[0], nbase_n[1], cx[:, :, 0] + pn[0], + pn[1], + b1[pn[0]], + b2[pn[1]], + span_n1, + span_n2, + nbase_n[0], + nbase_n[1], + cx[:, :, 0], ) * sin(2 * pi * eta3) * (-2 * pi) @@ -320,23 +392,63 @@ def df_all( # sum-up non-vanishing contributions (line 2: df_21, df_22 and df_23) mat_out[1, 0] = evaluation_kernel_2d( - pn[0], pn[1], der1, b2[pn[1]], span_n1, span_n2, nbase_n[0], nbase_n[1], cy[:, :, 0] + pn[0], + pn[1], + der1, + b2[pn[1]], + span_n1, + span_n2, + nbase_n[0], + nbase_n[1], + cy[:, :, 0], ) mat_out[1, 1] = evaluation_kernel_2d( - pn[0], pn[1], b1[pn[0]], der2, span_n1, span_n2, nbase_n[0], nbase_n[1], cy[:, :, 0] + pn[0], + pn[1], + b1[pn[0]], + der2, + span_n1, + span_n2, + nbase_n[0], + nbase_n[1], + cy[:, :, 0], ) mat_out[1, 2] = 0.0 # sum-up non-vanishing contributions (line 3: df_31, df_32 and df_33) mat_out[2, 0] = evaluation_kernel_2d( - pn[0], pn[1], der1, b2[pn[1]], span_n1, span_n2, nbase_n[0], nbase_n[1], cx[:, :, 0] + pn[0], + pn[1], + der1, + b2[pn[1]], + span_n1, + span_n2, + nbase_n[0], + nbase_n[1], + cx[:, :, 0], ) * sin(2 * pi * eta3) mat_out[2, 1] = evaluation_kernel_2d( - pn[0], pn[1], b1[pn[0]], der2, span_n1, span_n2, nbase_n[0], nbase_n[1], cx[:, :, 0] + pn[0], + pn[1], + b1[pn[0]], + der2, + span_n1, + span_n2, + nbase_n[0], + nbase_n[1], + cx[:, :, 0], ) * sin(2 * pi * eta3) mat_out[2, 2] = ( evaluation_kernel_2d( - pn[0], pn[1], b1[pn[0]], b2[pn[1]], span_n1, span_n2, nbase_n[0], nbase_n[1], cx[:, :, 0] + pn[0], + pn[1], + b1[pn[0]], + b2[pn[1]], + span_n1, + span_n2, + nbase_n[0], + nbase_n[1], + cx[:, :, 0], ) * cos(2 * pi * eta3) * 2 @@ -346,13 +458,37 @@ def df_all( # evaluate mapping if mat_or_vec == 1 or mat_or_vec == 2: vec_out[0] = evaluation_kernel_2d( - pn[0], pn[1], b1[pn[0]], b2[pn[1]], span_n1, span_n2, nbase_n[0], nbase_n[1], cx[:, :, 0] + pn[0], + pn[1], + b1[pn[0]], + b2[pn[1]], + span_n1, + span_n2, + nbase_n[0], + nbase_n[1], + cx[:, :, 0], ) * cos(2 * pi * eta3) vec_out[1] = evaluation_kernel_2d( - pn[0], pn[1], b1[pn[0]], b2[pn[1]], span_n1, span_n2, nbase_n[0], nbase_n[1], cy[:, :, 0] + pn[0], + pn[1], + b1[pn[0]], + b2[pn[1]], + span_n1, + span_n2, + nbase_n[0], + nbase_n[1], + cy[:, :, 0], ) vec_out[2] = evaluation_kernel_2d( - pn[0], pn[1], b1[pn[0]], b2[pn[1]], span_n1, span_n2, nbase_n[0], nbase_n[1], cx[:, :, 0] + pn[0], + pn[1], + b1[pn[0]], + b2[pn[1]], + span_n1, + span_n2, + nbase_n[0], + nbase_n[1], + cx[:, :, 0], ) * sin(2 * pi * eta3) # analytical mapping @@ -360,33 +496,150 @@ def df_all( # evaluate Jacobian matrix if mat_or_vec == 0 or mat_or_vec == 2: mat_out[0, 0] = mapping.df( - eta1, eta2, eta3, 11, kind_map, params_map, tn1, tn2, tn3, pn, nbase_n, cx, cy, cz + eta1, + eta2, + eta3, + 11, + kind_map, + params_map, + tn1, + tn2, + tn3, + pn, + nbase_n, + cx, + cy, + cz, ) mat_out[0, 1] = mapping.df( - eta1, eta2, eta3, 12, kind_map, params_map, tn1, tn2, tn3, pn, nbase_n, cx, cy, cz + eta1, + eta2, + eta3, + 12, + kind_map, + params_map, + tn1, + tn2, + tn3, + pn, + nbase_n, + cx, + cy, + cz, ) mat_out[0, 2] = mapping.df( - eta1, eta2, eta3, 13, kind_map, params_map, tn1, tn2, tn3, pn, nbase_n, cx, cy, cz + eta1, + eta2, + eta3, + 13, + kind_map, + params_map, + tn1, + tn2, + tn3, + pn, + nbase_n, + cx, + cy, + cz, ) mat_out[1, 0] = mapping.df( - eta1, eta2, eta3, 21, kind_map, params_map, tn1, tn2, tn3, pn, nbase_n, cx, cy, cz + eta1, + eta2, + eta3, + 21, + kind_map, + params_map, + tn1, + tn2, + tn3, + pn, + nbase_n, + cx, + cy, + cz, ) mat_out[1, 1] = mapping.df( - eta1, eta2, eta3, 22, kind_map, params_map, tn1, tn2, tn3, pn, nbase_n, cx, cy, cz + eta1, + eta2, + eta3, + 22, + kind_map, + params_map, + tn1, + tn2, + tn3, + pn, + nbase_n, + cx, + cy, + cz, ) mat_out[1, 2] = mapping.df( - eta1, eta2, eta3, 23, kind_map, params_map, tn1, tn2, tn3, pn, nbase_n, cx, cy, cz + eta1, + eta2, + eta3, + 23, + kind_map, + params_map, + tn1, + tn2, + tn3, + pn, + nbase_n, + cx, + cy, + cz, ) mat_out[2, 0] = mapping.df( - eta1, eta2, eta3, 31, kind_map, params_map, tn1, tn2, tn3, pn, nbase_n, cx, cy, cz + eta1, + eta2, + eta3, + 31, + kind_map, + params_map, + tn1, + tn2, + tn3, + pn, + nbase_n, + cx, + cy, + cz, ) mat_out[2, 1] = mapping.df( - eta1, eta2, eta3, 32, kind_map, params_map, tn1, tn2, tn3, pn, nbase_n, cx, cy, cz + eta1, + eta2, + eta3, + 32, + kind_map, + params_map, + tn1, + tn2, + tn3, + pn, + nbase_n, + cx, + cy, + cz, ) mat_out[2, 2] = mapping.df( - eta1, eta2, eta3, 33, kind_map, params_map, tn1, tn2, tn3, pn, nbase_n, cx, cy, cz + eta1, + eta2, + eta3, + 33, + kind_map, + params_map, + tn1, + tn2, + tn3, + pn, + nbase_n, + cx, + cy, + cz, ) # evaluate mapping diff --git a/src/struphy/pic/tests/test_pic_legacy_files/pusher_pos.py b/src/struphy/pic/tests/test_pic_legacy_files/pusher_pos.py index 78631440e..81b5e1e53 100644 --- a/src/struphy/pic/tests/test_pic_legacy_files/pusher_pos.py +++ b/src/struphy/pic/tests/test_pic_legacy_files/pusher_pos.py @@ -532,13 +532,52 @@ def pusher_step4_pcart( # compute old pseudo-cartesian coordinates fx_pseudo[0] = mapping.f( - eta[0], eta[1], eta[2], 1, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 1, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) fx_pseudo[1] = mapping.f( - eta[0], eta[1], eta[2], 2, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 2, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) fx_pseudo[2] = mapping.f( - eta[0], eta[1], eta[2], 3, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 3, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) # evaluate old Jacobian matrix of mapping F @@ -588,33 +627,150 @@ def pusher_step4_pcart( # evaluate old Jacobian matrix of mapping F_pseudo df_pseudo_old[0, 0] = mapping.df( - eta[0], eta[1], eta[2], 11, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 11, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo_old[0, 1] = mapping.df( - eta[0], eta[1], eta[2], 12, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 12, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo_old[0, 2] = mapping.df( - eta[0], eta[1], eta[2], 13, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 13, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo_old[1, 0] = mapping.df( - eta[0], eta[1], eta[2], 21, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 21, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo_old[1, 1] = mapping.df( - eta[0], eta[1], eta[2], 22, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 22, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo_old[1, 2] = mapping.df( - eta[0], eta[1], eta[2], 23, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 23, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo_old[2, 0] = mapping.df( - eta[0], eta[1], eta[2], 31, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 31, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo_old[2, 1] = mapping.df( - eta[0], eta[1], eta[2], 32, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 32, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo_old[2, 2] = mapping.df( - eta[0], eta[1], eta[2], 33, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 33, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) while True: @@ -687,33 +843,150 @@ def pusher_step4_pcart( # evaluate Jacobian matrix of mapping F_pseudo df_pseudo[0, 0] = mapping.df( - eta[0], eta[1], eta[2], 11, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 11, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[0, 1] = mapping.df( - eta[0], eta[1], eta[2], 12, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 12, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[0, 2] = mapping.df( - eta[0], eta[1], eta[2], 13, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 13, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[1, 0] = mapping.df( - eta[0], eta[1], eta[2], 21, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 21, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[1, 1] = mapping.df( - eta[0], eta[1], eta[2], 22, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 22, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[1, 2] = mapping.df( - eta[0], eta[1], eta[2], 23, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 23, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[2, 0] = mapping.df( - eta[0], eta[1], eta[2], 31, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 31, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[2, 1] = mapping.df( - eta[0], eta[1], eta[2], 32, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 32, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[2, 2] = mapping.df( - eta[0], eta[1], eta[2], 33, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 33, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) # compute df_pseudo*df_inv*v @@ -784,33 +1057,150 @@ def pusher_step4_pcart( # evaluate Jacobian matrix of mapping F_pseudo df_pseudo[0, 0] = mapping.df( - eta[0], eta[1], eta[2], 11, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 11, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[0, 1] = mapping.df( - eta[0], eta[1], eta[2], 12, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 12, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[0, 2] = mapping.df( - eta[0], eta[1], eta[2], 13, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 13, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[1, 0] = mapping.df( - eta[0], eta[1], eta[2], 21, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 21, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[1, 1] = mapping.df( - eta[0], eta[1], eta[2], 22, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 22, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[1, 2] = mapping.df( - eta[0], eta[1], eta[2], 23, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 23, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[2, 0] = mapping.df( - eta[0], eta[1], eta[2], 31, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 31, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[2, 1] = mapping.df( - eta[0], eta[1], eta[2], 32, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 32, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[2, 2] = mapping.df( - eta[0], eta[1], eta[2], 33, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 33, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) # compute df_pseudo*df_inv*v @@ -881,33 +1271,150 @@ def pusher_step4_pcart( # evaluate Jacobian matrix of mapping F_pseudo df_pseudo[0, 0] = mapping.df( - eta[0], eta[1], eta[2], 11, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 11, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[0, 1] = mapping.df( - eta[0], eta[1], eta[2], 12, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 12, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[0, 2] = mapping.df( - eta[0], eta[1], eta[2], 13, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 13, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[1, 0] = mapping.df( - eta[0], eta[1], eta[2], 21, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 21, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[1, 1] = mapping.df( - eta[0], eta[1], eta[2], 22, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 22, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[1, 2] = mapping.df( - eta[0], eta[1], eta[2], 23, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 23, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[2, 0] = mapping.df( - eta[0], eta[1], eta[2], 31, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 31, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[2, 1] = mapping.df( - eta[0], eta[1], eta[2], 32, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 32, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) df_pseudo[2, 2] = mapping.df( - eta[0], eta[1], eta[2], 33, map_pseudo, params_pseudo, tf1, tf2, tf3, pf, nbasef, cx, cy, cz + eta[0], + eta[1], + eta[2], + 33, + map_pseudo, + params_pseudo, + tf1, + tf2, + tf3, + pf, + nbasef, + cx, + cy, + cz, ) # compute df_pseudo*df_inv*v @@ -1374,26 +1881,98 @@ def pusher_rk4_pc_full( # velocity field if basis_u == 1: u[0] = eva3.evaluation_kernel_3d( - pd1, pn2, pn3, bd1, bn2, bn3, span1 - 1, span2, span3, nbase_d[0], nbase_n[1], nbase_n[2], u1 + pd1, + pn2, + pn3, + bd1, + bn2, + bn3, + span1 - 1, + span2, + span3, + nbase_d[0], + nbase_n[1], + nbase_n[2], + u1, ) u[1] = eva3.evaluation_kernel_3d( - pn1, pd2, pn3, bn1, bd2, bn3, span1, span2 - 1, span3, nbase_n[0], nbase_d[1], nbase_n[2], u2 + pn1, + pd2, + pn3, + bn1, + bd2, + bn3, + span1, + span2 - 1, + span3, + nbase_n[0], + nbase_d[1], + nbase_n[2], + u2, ) u[2] = eva3.evaluation_kernel_3d( - pn1, pn2, pd3, bn1, bn2, bd3, span1, span2, span3 - 1, nbase_n[0], nbase_n[1], nbase_d[2], u3 + pn1, + pn2, + pd3, + bn1, + bn2, + bd3, + span1, + span2, + span3 - 1, + nbase_n[0], + nbase_n[1], + nbase_d[2], + u3, ) linalg.matrix_vector(Ginv, u, k1_u) elif basis_u == 2: u[0] = eva3.evaluation_kernel_3d( - pn1, pd2, pd3, bn1, bd2, bd3, span1, span2 - 1, span3 - 1, nbase_n[0], nbase_d[1], nbase_d[2], u1 + pn1, + pd2, + pd3, + bn1, + bd2, + bd3, + span1, + span2 - 1, + span3 - 1, + nbase_n[0], + nbase_d[1], + nbase_d[2], + u1, ) u[1] = eva3.evaluation_kernel_3d( - pd1, pn2, pd3, bd1, bn2, bd3, span1 - 1, span2, span3 - 1, nbase_d[0], nbase_n[1], nbase_d[2], u2 + pd1, + pn2, + pd3, + bd1, + bn2, + bd3, + span1 - 1, + span2, + span3 - 1, + nbase_d[0], + nbase_n[1], + nbase_d[2], + u2, ) u[2] = eva3.evaluation_kernel_3d( - pd1, pd2, pn3, bd1, bd2, bn3, span1 - 1, span2 - 1, span3, nbase_d[0], nbase_d[1], nbase_n[2], u3 + pd1, + pd2, + pn3, + bd1, + bd2, + bn3, + span1 - 1, + span2 - 1, + span3, + nbase_d[0], + nbase_d[1], + nbase_n[2], + u3, ) k1_u[:] = u / det_df @@ -1491,26 +2070,98 @@ def pusher_rk4_pc_full( # velocity field if basis_u == 1: u[0] = eva3.evaluation_kernel_3d( - pd1, pn2, pn3, bd1, bn2, bn3, span1 - 1, span2, span3, nbase_d[0], nbase_n[1], nbase_n[2], u1 + pd1, + pn2, + pn3, + bd1, + bn2, + bn3, + span1 - 1, + span2, + span3, + nbase_d[0], + nbase_n[1], + nbase_n[2], + u1, ) u[1] = eva3.evaluation_kernel_3d( - pn1, pd2, pn3, bn1, bd2, bn3, span1, span2 - 1, span3, nbase_n[0], nbase_d[1], nbase_n[2], u2 + pn1, + pd2, + pn3, + bn1, + bd2, + bn3, + span1, + span2 - 1, + span3, + nbase_n[0], + nbase_d[1], + nbase_n[2], + u2, ) u[2] = eva3.evaluation_kernel_3d( - pn1, pn2, pd3, bn1, bn2, bd3, span1, span2, span3 - 1, nbase_n[0], nbase_n[1], nbase_d[2], u3 + pn1, + pn2, + pd3, + bn1, + bn2, + bd3, + span1, + span2, + span3 - 1, + nbase_n[0], + nbase_n[1], + nbase_d[2], + u3, ) linalg.matrix_vector(Ginv, u, k2_u) elif basis_u == 2: u[0] = eva3.evaluation_kernel_3d( - pn1, pd2, pd3, bn1, bd2, bd3, span1, span2 - 1, span3 - 1, nbase_n[0], nbase_d[1], nbase_d[2], u1 + pn1, + pd2, + pd3, + bn1, + bd2, + bd3, + span1, + span2 - 1, + span3 - 1, + nbase_n[0], + nbase_d[1], + nbase_d[2], + u1, ) u[1] = eva3.evaluation_kernel_3d( - pd1, pn2, pd3, bd1, bn2, bd3, span1 - 1, span2, span3 - 1, nbase_d[0], nbase_n[1], nbase_d[2], u2 + pd1, + pn2, + pd3, + bd1, + bn2, + bd3, + span1 - 1, + span2, + span3 - 1, + nbase_d[0], + nbase_n[1], + nbase_d[2], + u2, ) u[2] = eva3.evaluation_kernel_3d( - pd1, pd2, pn3, bd1, bd2, bn3, span1 - 1, span2 - 1, span3, nbase_d[0], nbase_d[1], nbase_n[2], u3 + pd1, + pd2, + pn3, + bd1, + bd2, + bn3, + span1 - 1, + span2 - 1, + span3, + nbase_d[0], + nbase_d[1], + nbase_n[2], + u3, ) k2_u[:] = u / det_df @@ -1608,26 +2259,98 @@ def pusher_rk4_pc_full( # velocity field if basis_u == 1: u[0] = eva3.evaluation_kernel_3d( - pd1, pn2, pn3, bd1, bn2, bn3, span1 - 1, span2, span3, nbase_d[0], nbase_n[1], nbase_n[2], u1 + pd1, + pn2, + pn3, + bd1, + bn2, + bn3, + span1 - 1, + span2, + span3, + nbase_d[0], + nbase_n[1], + nbase_n[2], + u1, ) u[1] = eva3.evaluation_kernel_3d( - pn1, pd2, pn3, bn1, bd2, bn3, span1, span2 - 1, span3, nbase_n[0], nbase_d[1], nbase_n[2], u2 + pn1, + pd2, + pn3, + bn1, + bd2, + bn3, + span1, + span2 - 1, + span3, + nbase_n[0], + nbase_d[1], + nbase_n[2], + u2, ) u[2] = eva3.evaluation_kernel_3d( - pn1, pn2, pd3, bn1, bn2, bd3, span1, span2, span3 - 1, nbase_n[0], nbase_n[1], nbase_d[2], u3 + pn1, + pn2, + pd3, + bn1, + bn2, + bd3, + span1, + span2, + span3 - 1, + nbase_n[0], + nbase_n[1], + nbase_d[2], + u3, ) linalg.matrix_vector(Ginv, u, k3_u) elif basis_u == 2: u[0] = eva3.evaluation_kernel_3d( - pn1, pd2, pd3, bn1, bd2, bd3, span1, span2 - 1, span3 - 1, nbase_n[0], nbase_d[1], nbase_d[2], u1 + pn1, + pd2, + pd3, + bn1, + bd2, + bd3, + span1, + span2 - 1, + span3 - 1, + nbase_n[0], + nbase_d[1], + nbase_d[2], + u1, ) u[1] = eva3.evaluation_kernel_3d( - pd1, pn2, pd3, bd1, bn2, bd3, span1 - 1, span2, span3 - 1, nbase_d[0], nbase_n[1], nbase_d[2], u2 + pd1, + pn2, + pd3, + bd1, + bn2, + bd3, + span1 - 1, + span2, + span3 - 1, + nbase_d[0], + nbase_n[1], + nbase_d[2], + u2, ) u[2] = eva3.evaluation_kernel_3d( - pd1, pd2, pn3, bd1, bd2, bn3, span1 - 1, span2 - 1, span3, nbase_d[0], nbase_d[1], nbase_n[2], u3 + pd1, + pd2, + pn3, + bd1, + bd2, + bn3, + span1 - 1, + span2 - 1, + span3, + nbase_d[0], + nbase_d[1], + nbase_n[2], + u3, ) k3_u[:] = u / det_df @@ -1722,26 +2445,98 @@ def pusher_rk4_pc_full( # velocity field if basis_u == 1: u[0] = eva3.evaluation_kernel_3d( - pd1, pn2, pn3, bd1, bn2, bn3, span1 - 1, span2, span3, nbase_d[0], nbase_n[1], nbase_n[2], u1 + pd1, + pn2, + pn3, + bd1, + bn2, + bn3, + span1 - 1, + span2, + span3, + nbase_d[0], + nbase_n[1], + nbase_n[2], + u1, ) u[1] = eva3.evaluation_kernel_3d( - pn1, pd2, pn3, bn1, bd2, bn3, span1, span2 - 1, span3, nbase_n[0], nbase_d[1], nbase_n[2], u2 + pn1, + pd2, + pn3, + bn1, + bd2, + bn3, + span1, + span2 - 1, + span3, + nbase_n[0], + nbase_d[1], + nbase_n[2], + u2, ) u[2] = eva3.evaluation_kernel_3d( - pn1, pn2, pd3, bn1, bn2, bd3, span1, span2, span3 - 1, nbase_n[0], nbase_n[1], nbase_d[2], u3 + pn1, + pn2, + pd3, + bn1, + bn2, + bd3, + span1, + span2, + span3 - 1, + nbase_n[0], + nbase_n[1], + nbase_d[2], + u3, ) linalg.matrix_vector(Ginv, u, k4_u) elif basis_u == 2: u[0] = eva3.evaluation_kernel_3d( - pn1, pd2, pd3, bn1, bd2, bd3, span1, span2 - 1, span3 - 1, nbase_n[0], nbase_d[1], nbase_d[2], u1 + pn1, + pd2, + pd3, + bn1, + bd2, + bd3, + span1, + span2 - 1, + span3 - 1, + nbase_n[0], + nbase_d[1], + nbase_d[2], + u1, ) u[1] = eva3.evaluation_kernel_3d( - pd1, pn2, pd3, bd1, bn2, bd3, span1 - 1, span2, span3 - 1, nbase_d[0], nbase_n[1], nbase_d[2], u2 + pd1, + pn2, + pd3, + bd1, + bn2, + bd3, + span1 - 1, + span2, + span3 - 1, + nbase_d[0], + nbase_n[1], + nbase_d[2], + u2, ) u[2] = eva3.evaluation_kernel_3d( - pd1, pd2, pn3, bd1, bd2, bn3, span1 - 1, span2 - 1, span3, nbase_d[0], nbase_d[1], nbase_n[2], u3 + pd1, + pd2, + pn3, + bd1, + bd2, + bn3, + span1 - 1, + span2 - 1, + span3, + nbase_d[0], + nbase_d[1], + nbase_n[2], + u3, ) k4_u[:] = u / det_df @@ -1992,26 +2787,98 @@ def pusher_rk4_pc_perp( # velocity field if basis_u == 1: u[0] = eva3.evaluation_kernel_3d( - pd1, pn2, pn3, bd1, bn2, bn3, span1 - 1, span2, span3, nbase_d[0], nbase_n[1], nbase_n[2], u1 + pd1, + pn2, + pn3, + bd1, + bn2, + bn3, + span1 - 1, + span2, + span3, + nbase_d[0], + nbase_n[1], + nbase_n[2], + u1, ) u[1] = eva3.evaluation_kernel_3d( - pn1, pd2, pn3, bn1, bd2, bn3, span1, span2 - 1, span3, nbase_n[0], nbase_d[1], nbase_n[2], u2 + pn1, + pd2, + pn3, + bn1, + bd2, + bn3, + span1, + span2 - 1, + span3, + nbase_n[0], + nbase_d[1], + nbase_n[2], + u2, ) u[2] = eva3.evaluation_kernel_3d( - pn1, pn2, pd3, bn1, bn2, bd3, span1, span2, span3 - 1, nbase_n[0], nbase_n[1], nbase_d[2], u3 + pn1, + pn2, + pd3, + bn1, + bn2, + bd3, + span1, + span2, + span3 - 1, + nbase_n[0], + nbase_n[1], + nbase_d[2], + u3, ) linalg.matrix_vector(Ginv, u, k1_u) elif basis_u == 2: u[0] = eva3.evaluation_kernel_3d( - pn1, pd2, pd3, bn1, bd2, bd3, span1, span2 - 1, span3 - 1, nbase_n[0], nbase_d[1], nbase_d[2], u1 + pn1, + pd2, + pd3, + bn1, + bd2, + bd3, + span1, + span2 - 1, + span3 - 1, + nbase_n[0], + nbase_d[1], + nbase_d[2], + u1, ) u[1] = eva3.evaluation_kernel_3d( - pd1, pn2, pd3, bd1, bn2, bd3, span1 - 1, span2, span3 - 1, nbase_d[0], nbase_n[1], nbase_d[2], u2 + pd1, + pn2, + pd3, + bd1, + bn2, + bd3, + span1 - 1, + span2, + span3 - 1, + nbase_d[0], + nbase_n[1], + nbase_d[2], + u2, ) u[2] = eva3.evaluation_kernel_3d( - pd1, pd2, pn3, bd1, bd2, bn3, span1 - 1, span2 - 1, span3, nbase_d[0], nbase_d[1], nbase_n[2], u3 + pd1, + pd2, + pn3, + bd1, + bd2, + bn3, + span1 - 1, + span2 - 1, + span3, + nbase_d[0], + nbase_d[1], + nbase_n[2], + u3, ) k1_u[:] = u / det_df @@ -2108,26 +2975,98 @@ def pusher_rk4_pc_perp( # velocity field if basis_u == 1: u[0] = eva3.evaluation_kernel_3d( - pd1, pn2, pn3, bd1, bn2, bn3, span1 - 1, span2, span3, nbase_d[0], nbase_n[1], nbase_n[2], u1 + pd1, + pn2, + pn3, + bd1, + bn2, + bn3, + span1 - 1, + span2, + span3, + nbase_d[0], + nbase_n[1], + nbase_n[2], + u1, ) u[1] = eva3.evaluation_kernel_3d( - pn1, pd2, pn3, bn1, bd2, bn3, span1, span2 - 1, span3, nbase_n[0], nbase_d[1], nbase_n[2], u2 + pn1, + pd2, + pn3, + bn1, + bd2, + bn3, + span1, + span2 - 1, + span3, + nbase_n[0], + nbase_d[1], + nbase_n[2], + u2, ) u[2] = eva3.evaluation_kernel_3d( - pn1, pn2, pd3, bn1, bn2, bd3, span1, span2, span3 - 1, nbase_n[0], nbase_n[1], nbase_d[2], u3 + pn1, + pn2, + pd3, + bn1, + bn2, + bd3, + span1, + span2, + span3 - 1, + nbase_n[0], + nbase_n[1], + nbase_d[2], + u3, ) linalg.matrix_vector(Ginv, u, k2_u) elif basis_u == 2: u[0] = eva3.evaluation_kernel_3d( - pn1, pd2, pd3, bn1, bd2, bd3, span1, span2 - 1, span3 - 1, nbase_n[0], nbase_d[1], nbase_d[2], u1 + pn1, + pd2, + pd3, + bn1, + bd2, + bd3, + span1, + span2 - 1, + span3 - 1, + nbase_n[0], + nbase_d[1], + nbase_d[2], + u1, ) u[1] = eva3.evaluation_kernel_3d( - pd1, pn2, pd3, bd1, bn2, bd3, span1 - 1, span2, span3 - 1, nbase_d[0], nbase_n[1], nbase_d[2], u2 + pd1, + pn2, + pd3, + bd1, + bn2, + bd3, + span1 - 1, + span2, + span3 - 1, + nbase_d[0], + nbase_n[1], + nbase_d[2], + u2, ) u[2] = eva3.evaluation_kernel_3d( - pd1, pd2, pn3, bd1, bd2, bn3, span1 - 1, span2 - 1, span3, nbase_d[0], nbase_d[1], nbase_n[2], u3 + pd1, + pd2, + pn3, + bd1, + bd2, + bn3, + span1 - 1, + span2 - 1, + span3, + nbase_d[0], + nbase_d[1], + nbase_n[2], + u3, ) k2_u[:] = u / det_df @@ -2223,26 +3162,98 @@ def pusher_rk4_pc_perp( # velocity field if basis_u == 1: u[0] = eva3.evaluation_kernel_3d( - pd1, pn2, pn3, bd1, bn2, bn3, span1 - 1, span2, span3, nbase_d[0], nbase_n[1], nbase_n[2], u1 + pd1, + pn2, + pn3, + bd1, + bn2, + bn3, + span1 - 1, + span2, + span3, + nbase_d[0], + nbase_n[1], + nbase_n[2], + u1, ) u[1] = eva3.evaluation_kernel_3d( - pn1, pd2, pn3, bn1, bd2, bn3, span1, span2 - 1, span3, nbase_n[0], nbase_d[1], nbase_n[2], u2 + pn1, + pd2, + pn3, + bn1, + bd2, + bn3, + span1, + span2 - 1, + span3, + nbase_n[0], + nbase_d[1], + nbase_n[2], + u2, ) u[2] = eva3.evaluation_kernel_3d( - pn1, pn2, pd3, bn1, bn2, bd3, span1, span2, span3 - 1, nbase_n[0], nbase_n[1], nbase_d[2], u3 + pn1, + pn2, + pd3, + bn1, + bn2, + bd3, + span1, + span2, + span3 - 1, + nbase_n[0], + nbase_n[1], + nbase_d[2], + u3, ) linalg.matrix_vector(Ginv, u, k3_u) elif basis_u == 2: u[0] = eva3.evaluation_kernel_3d( - pn1, pd2, pd3, bn1, bd2, bd3, span1, span2 - 1, span3 - 1, nbase_n[0], nbase_d[1], nbase_d[2], u1 + pn1, + pd2, + pd3, + bn1, + bd2, + bd3, + span1, + span2 - 1, + span3 - 1, + nbase_n[0], + nbase_d[1], + nbase_d[2], + u1, ) u[1] = eva3.evaluation_kernel_3d( - pd1, pn2, pd3, bd1, bn2, bd3, span1 - 1, span2, span3 - 1, nbase_d[0], nbase_n[1], nbase_d[2], u2 + pd1, + pn2, + pd3, + bd1, + bn2, + bd3, + span1 - 1, + span2, + span3 - 1, + nbase_d[0], + nbase_n[1], + nbase_d[2], + u2, ) u[2] = eva3.evaluation_kernel_3d( - pd1, pd2, pn3, bd1, bd2, bn3, span1 - 1, span2 - 1, span3, nbase_d[0], nbase_d[1], nbase_n[2], u3 + pd1, + pd2, + pn3, + bd1, + bd2, + bn3, + span1 - 1, + span2 - 1, + span3, + nbase_d[0], + nbase_d[1], + nbase_n[2], + u3, ) k3_u[:] = u / det_df @@ -2338,26 +3349,98 @@ def pusher_rk4_pc_perp( # velocity field if basis_u == 1: u[0] = eva3.evaluation_kernel_3d( - pd1, pn2, pn3, bd1, bn2, bn3, span1 - 1, span2, span3, nbase_d[0], nbase_n[1], nbase_n[2], u1 + pd1, + pn2, + pn3, + bd1, + bn2, + bn3, + span1 - 1, + span2, + span3, + nbase_d[0], + nbase_n[1], + nbase_n[2], + u1, ) u[1] = eva3.evaluation_kernel_3d( - pn1, pd2, pn3, bn1, bd2, bn3, span1, span2 - 1, span3, nbase_n[0], nbase_d[1], nbase_n[2], u2 + pn1, + pd2, + pn3, + bn1, + bd2, + bn3, + span1, + span2 - 1, + span3, + nbase_n[0], + nbase_d[1], + nbase_n[2], + u2, ) u[2] = eva3.evaluation_kernel_3d( - pn1, pn2, pd3, bn1, bn2, bd3, span1, span2, span3 - 1, nbase_n[0], nbase_n[1], nbase_d[2], u3 + pn1, + pn2, + pd3, + bn1, + bn2, + bd3, + span1, + span2, + span3 - 1, + nbase_n[0], + nbase_n[1], + nbase_d[2], + u3, ) linalg.matrix_vector(Ginv, u, k4_u) elif basis_u == 2: u[0] = eva3.evaluation_kernel_3d( - pn1, pd2, pd3, bn1, bd2, bd3, span1, span2 - 1, span3 - 1, nbase_n[0], nbase_d[1], nbase_d[2], u1 + pn1, + pd2, + pd3, + bn1, + bd2, + bd3, + span1, + span2 - 1, + span3 - 1, + nbase_n[0], + nbase_d[1], + nbase_d[2], + u1, ) u[1] = eva3.evaluation_kernel_3d( - pd1, pn2, pd3, bd1, bn2, bd3, span1 - 1, span2, span3 - 1, nbase_d[0], nbase_n[1], nbase_d[2], u2 + pd1, + pn2, + pd3, + bd1, + bn2, + bd3, + span1 - 1, + span2, + span3 - 1, + nbase_d[0], + nbase_n[1], + nbase_d[2], + u2, ) u[2] = eva3.evaluation_kernel_3d( - pd1, pd2, pn3, bd1, bd2, bn3, span1 - 1, span2 - 1, span3, nbase_d[0], nbase_d[1], nbase_n[2], u3 + pd1, + pd2, + pn3, + bd1, + bd2, + bn3, + span1 - 1, + span2 - 1, + span3, + nbase_d[0], + nbase_d[1], + nbase_n[2], + u3, ) k4_u[:] = u / det_df diff --git a/src/struphy/pic/tests/test_pic_legacy_files/pusher_vel_2d.py b/src/struphy/pic/tests/test_pic_legacy_files/pusher_vel_2d.py index 43e320311..0fcc29751 100644 --- a/src/struphy/pic/tests/test_pic_legacy_files/pusher_vel_2d.py +++ b/src/struphy/pic/tests/test_pic_legacy_files/pusher_vel_2d.py @@ -248,19 +248,43 @@ def pusher_step3( for i in range(nbase_n[2]): u[0] += ( eva2.evaluation_kernel_2d( - pd1, pn2, bd1, bn2, span1 - 1, span2 - 0, nbase_d[0], nbase_n[1], u1[:, :, i] + pd1, + pn2, + bd1, + bn2, + span1 - 1, + span2 - 0, + nbase_d[0], + nbase_n[1], + u1[:, :, i], ) * cs[i] ) u[1] += ( eva2.evaluation_kernel_2d( - pn1, pd2, bn1, bd2, span1 - 0, span2 - 1, nbase_n[0], nbase_d[1], u2[:, :, i] + pn1, + pd2, + bn1, + bd2, + span1 - 0, + span2 - 1, + nbase_n[0], + nbase_d[1], + u2[:, :, i], ) * cs[i] ) u[2] += ( eva2.evaluation_kernel_2d( - pn1, pn2, bn1, bn2, span1 - 0, span2 - 0, nbase_n[0], nbase_n[1], u3[:, :, i] + pn1, + pn2, + bn1, + bn2, + span1 - 0, + span2 - 0, + nbase_n[0], + nbase_n[1], + u3[:, :, i], ) * cs[i] ) @@ -274,19 +298,43 @@ def pusher_step3( for i in range(nbase_n[2]): u[0] += ( eva2.evaluation_kernel_2d( - pn1, pd2, bn1, bd2, span1 - 0, span2 - 1, nbase_n[0], nbase_d[1], u1[:, :, i] + pn1, + pd2, + bn1, + bd2, + span1 - 0, + span2 - 1, + nbase_n[0], + nbase_d[1], + u1[:, :, i], ) * cs[i] ) u[1] += ( eva2.evaluation_kernel_2d( - pd1, pn2, bd1, bn2, span1 - 1, span2 - 0, nbase_d[0], nbase_n[1], u2[:, :, i] + pd1, + pn2, + bd1, + bn2, + span1 - 1, + span2 - 0, + nbase_d[0], + nbase_n[1], + u2[:, :, i], ) * cs[i] ) u[2] += ( eva2.evaluation_kernel_2d( - pd1, pd2, bd1, bd2, span1 - 1, span2 - 1, nbase_d[0], nbase_d[1], u3[:, :, i] + pd1, + pd2, + bd1, + bd2, + span1 - 1, + span2 - 1, + nbase_d[0], + nbase_d[1], + u3[:, :, i], ) * cs[i] ) @@ -299,32 +347,80 @@ def pusher_step3( # equilibrium magnetic field (2-form) b[0] = eva2.evaluation_kernel_2d( - pn1, pd2, bn1, bd2, span1 - 0, span2 - 1, nbase_n[0], nbase_d[1], b_eq_1[:, :, 0] + pn1, + pd2, + bn1, + bd2, + span1 - 0, + span2 - 1, + nbase_n[0], + nbase_d[1], + b_eq_1[:, :, 0], ) b[1] = eva2.evaluation_kernel_2d( - pd1, pn2, bd1, bn2, span1 - 1, span2 - 0, nbase_d[0], nbase_n[1], b_eq_2[:, :, 0] + pd1, + pn2, + bd1, + bn2, + span1 - 1, + span2 - 0, + nbase_d[0], + nbase_n[1], + b_eq_2[:, :, 0], ) b[2] = eva2.evaluation_kernel_2d( - pd1, pd2, bd1, bd2, span1 - 1, span2 - 1, nbase_d[0], nbase_d[1], b_eq_3[:, :, 0] + pd1, + pd2, + bd1, + bd2, + span1 - 1, + span2 - 1, + nbase_d[0], + nbase_d[1], + b_eq_3[:, :, 0], ) # perturbed magnetic field (2-form) for i in range(nbase_n[2]): b[0] += ( eva2.evaluation_kernel_2d( - pn1, pd2, bn1, bd2, span1 - 0, span2 - 1, nbase_n[0], nbase_d[1], b_p_1[:, :, i] + pn1, + pd2, + bn1, + bd2, + span1 - 0, + span2 - 1, + nbase_n[0], + nbase_d[1], + b_p_1[:, :, i], ) * cs[i] ) b[1] += ( eva2.evaluation_kernel_2d( - pd1, pn2, bd1, bn2, span1 - 1, span2 - 0, nbase_d[0], nbase_n[1], b_p_2[:, :, i] + pd1, + pn2, + bd1, + bn2, + span1 - 1, + span2 - 0, + nbase_d[0], + nbase_n[1], + b_p_2[:, :, i], ) * cs[i] ) b[2] += ( eva2.evaluation_kernel_2d( - pd1, pd2, bd1, bd2, span1 - 1, span2 - 1, nbase_d[0], nbase_d[1], b_p_3[:, :, i] + pd1, + pd2, + bd1, + bd2, + span1 - 1, + span2 - 1, + nbase_d[0], + nbase_d[1], + b_p_3[:, :, i], ) * cs[i] ) @@ -338,10 +434,26 @@ def pusher_step3( # gradient of absolute value of magnetic field (1-form) b_grad[0] = eva2.evaluation_kernel_2d( - pn1, pn2, der1, bn2, span1, span2, nbase_n[0], nbase_n[1], b_norm[:, :, 0] + pn1, + pn2, + der1, + bn2, + span1, + span2, + nbase_n[0], + nbase_n[1], + b_norm[:, :, 0], ) b_grad[1] = eva2.evaluation_kernel_2d( - pn1, pn2, bn1, der2, span1, span2, nbase_n[0], nbase_n[1], b_norm[:, :, 0] + pn1, + pn2, + bn1, + der2, + span1, + span2, + nbase_n[0], + nbase_n[1], + b_norm[:, :, 0], ) b_grad[2] = 0.0 @@ -562,32 +674,80 @@ def pusher_step5( # equilibrium magnetic field (2-form) b[0] = eva2.evaluation_kernel_2d( - pn1, pd2, bn1, bd2, span1 - 0, span2 - 1, nbase_n[0], nbase_d[1], b_eq_1[:, :, 0] + pn1, + pd2, + bn1, + bd2, + span1 - 0, + span2 - 1, + nbase_n[0], + nbase_d[1], + b_eq_1[:, :, 0], ) b[1] = eva2.evaluation_kernel_2d( - pd1, pn2, bd1, bn2, span1 - 1, span2 - 0, nbase_d[0], nbase_n[1], b_eq_2[:, :, 0] + pd1, + pn2, + bd1, + bn2, + span1 - 1, + span2 - 0, + nbase_d[0], + nbase_n[1], + b_eq_2[:, :, 0], ) b[2] = eva2.evaluation_kernel_2d( - pd1, pd2, bd1, bd2, span1 - 1, span2 - 1, nbase_d[0], nbase_d[1], b_eq_3[:, :, 0] + pd1, + pd2, + bd1, + bd2, + span1 - 1, + span2 - 1, + nbase_d[0], + nbase_d[1], + b_eq_3[:, :, 0], ) # perturbed magnetic field (2-form) for i in range(nbase_n[2]): b[0] += ( eva2.evaluation_kernel_2d( - pn1, pd2, bn1, bd2, span1 - 0, span2 - 1, nbase_n[0], nbase_d[1], b_p_1[:, :, i] + pn1, + pd2, + bn1, + bd2, + span1 - 0, + span2 - 1, + nbase_n[0], + nbase_d[1], + b_p_1[:, :, i], ) * cs[i] ) b[1] += ( eva2.evaluation_kernel_2d( - pd1, pn2, bd1, bn2, span1 - 1, span2 - 0, nbase_d[0], nbase_n[1], b_p_2[:, :, i] + pd1, + pn2, + bd1, + bn2, + span1 - 1, + span2 - 0, + nbase_d[0], + nbase_n[1], + b_p_2[:, :, i], ) * cs[i] ) b[2] += ( eva2.evaluation_kernel_2d( - pd1, pd2, bd1, bd2, span1 - 1, span2 - 1, nbase_d[0], nbase_d[1], b_p_3[:, :, i] + pd1, + pd2, + bd1, + bd2, + span1 - 1, + span2 - 1, + nbase_d[0], + nbase_d[1], + b_p_3[:, :, i], ) * cs[i] ) diff --git a/src/struphy/pic/tests/test_pic_legacy_files/pusher_vel_3d.py b/src/struphy/pic/tests/test_pic_legacy_files/pusher_vel_3d.py index 4eabb26dd..cd3884209 100644 --- a/src/struphy/pic/tests/test_pic_legacy_files/pusher_vel_3d.py +++ b/src/struphy/pic/tests/test_pic_legacy_files/pusher_vel_3d.py @@ -227,13 +227,49 @@ def pusher_step3( # velocity field (0-form, push-forward with df) if basis_u == 0: u[0] = eva3.evaluation_kernel_3d( - pn1, pn2, pn3, bn1, bn2, bn3, span1, span2, span3, nbase_n[0], nbase_n[1], nbase_n[2], u1 + pn1, + pn2, + pn3, + bn1, + bn2, + bn3, + span1, + span2, + span3, + nbase_n[0], + nbase_n[1], + nbase_n[2], + u1, ) u[1] = eva3.evaluation_kernel_3d( - pn1, pn2, pn3, bn1, bn2, bn3, span1, span2, span3, nbase_n[0], nbase_n[1], nbase_n[2], u2 + pn1, + pn2, + pn3, + bn1, + bn2, + bn3, + span1, + span2, + span3, + nbase_n[0], + nbase_n[1], + nbase_n[2], + u2, ) u[2] = eva3.evaluation_kernel_3d( - pn1, pn2, pn3, bn1, bn2, bn3, span1, span2, span3, nbase_n[0], nbase_n[1], nbase_n[2], u3 + pn1, + pn2, + pn3, + bn1, + bn2, + bn3, + span1, + span2, + span3, + nbase_n[0], + nbase_n[1], + nbase_n[2], + u3, ) linalg.matrix_vector(df, u, u_cart) @@ -241,13 +277,49 @@ def pusher_step3( # velocity field (1-form, push forward with df^(-T)) elif basis_u == 1: u[0] = eva3.evaluation_kernel_3d( - pd1, pn2, pn3, bd1, bn2, bn3, span1 - 1, span2, span3, nbase_d[0], nbase_n[1], nbase_n[2], u1 + pd1, + pn2, + pn3, + bd1, + bn2, + bn3, + span1 - 1, + span2, + span3, + nbase_d[0], + nbase_n[1], + nbase_n[2], + u1, ) u[1] = eva3.evaluation_kernel_3d( - pn1, pd2, pn3, bn1, bd2, bn3, span1, span2 - 1, span3, nbase_n[0], nbase_d[1], nbase_n[2], u2 + pn1, + pd2, + pn3, + bn1, + bd2, + bn3, + span1, + span2 - 1, + span3, + nbase_n[0], + nbase_d[1], + nbase_n[2], + u2, ) u[2] = eva3.evaluation_kernel_3d( - pn1, pn2, pd3, bn1, bn2, bd3, span1, span2, span3 - 1, nbase_n[0], nbase_n[1], nbase_d[2], u3 + pn1, + pn2, + pd3, + bn1, + bn2, + bd3, + span1, + span2, + span3 - 1, + nbase_n[0], + nbase_n[1], + nbase_d[2], + u3, ) linalg.matrix_vector(dfinv_t, u, u_cart) @@ -255,13 +327,49 @@ def pusher_step3( # velocity field (2-form, push forward with df/|det df|) elif basis_u == 2: u[0] = eva3.evaluation_kernel_3d( - pn1, pd2, pd3, bn1, bd2, bd3, span1, span2 - 1, span3 - 1, nbase_n[0], nbase_d[1], nbase_d[2], u1 + pn1, + pd2, + pd3, + bn1, + bd2, + bd3, + span1, + span2 - 1, + span3 - 1, + nbase_n[0], + nbase_d[1], + nbase_d[2], + u1, ) u[1] = eva3.evaluation_kernel_3d( - pd1, pn2, pd3, bd1, bn2, bd3, span1 - 1, span2, span3 - 1, nbase_d[0], nbase_n[1], nbase_d[2], u2 + pd1, + pn2, + pd3, + bd1, + bn2, + bd3, + span1 - 1, + span2, + span3 - 1, + nbase_d[0], + nbase_n[1], + nbase_d[2], + u2, ) u[2] = eva3.evaluation_kernel_3d( - pd1, pd2, pn3, bd1, bd2, bn3, span1 - 1, span2 - 1, span3, nbase_d[0], nbase_d[1], nbase_n[2], u3 + pd1, + pd2, + pn3, + bd1, + bd2, + bn3, + span1 - 1, + span2 - 1, + span3, + nbase_d[0], + nbase_d[1], + nbase_n[2], + u3, ) linalg.matrix_vector(df, u, u_cart) @@ -272,13 +380,49 @@ def pusher_step3( # magnetic field (2-form) b[0] = eva3.evaluation_kernel_3d( - pn1, pd2, pd3, bn1, bd2, bd3, span1, span2 - 1, span3 - 1, nbase_n[0], nbase_d[1], nbase_d[2], b2_1 + pn1, + pd2, + pd3, + bn1, + bd2, + bd3, + span1, + span2 - 1, + span3 - 1, + nbase_n[0], + nbase_d[1], + nbase_d[2], + b2_1, ) b[1] = eva3.evaluation_kernel_3d( - pd1, pn2, pd3, bd1, bn2, bd3, span1 - 1, span2, span3 - 1, nbase_d[0], nbase_n[1], nbase_d[2], b2_2 + pd1, + pn2, + pd3, + bd1, + bn2, + bd3, + span1 - 1, + span2, + span3 - 1, + nbase_d[0], + nbase_n[1], + nbase_d[2], + b2_2, ) b[2] = eva3.evaluation_kernel_3d( - pd1, pd2, pn3, bd1, bd2, bn3, span1 - 1, span2 - 1, span3, nbase_d[0], nbase_d[1], nbase_n[2], b2_3 + pd1, + pd2, + pn3, + bd1, + bd2, + bn3, + span1 - 1, + span2 - 1, + span3, + nbase_d[0], + nbase_d[1], + nbase_n[2], + b2_3, ) # push-forward to physical domain @@ -290,13 +434,49 @@ def pusher_step3( # gradient of absolute value of magnetic field (1-form) b_grad[0] = eva3.evaluation_kernel_3d( - pn1, pn2, pn3, der1, bn2, bn3, span1, span2, span3, nbase_n[0], nbase_n[1], nbase_n[2], b0 + pn1, + pn2, + pn3, + der1, + bn2, + bn3, + span1, + span2, + span3, + nbase_n[0], + nbase_n[1], + nbase_n[2], + b0, ) b_grad[1] = eva3.evaluation_kernel_3d( - pn1, pn2, pn3, bn1, der2, bn3, span1, span2, span3, nbase_n[0], nbase_n[1], nbase_n[2], b0 + pn1, + pn2, + pn3, + bn1, + der2, + bn3, + span1, + span2, + span3, + nbase_n[0], + nbase_n[1], + nbase_n[2], + b0, ) b_grad[2] = eva3.evaluation_kernel_3d( - pn1, pn2, pn3, bn1, bn2, der3, span1, span2, span3, nbase_n[0], nbase_n[1], nbase_n[2], b0 + pn1, + pn2, + pn3, + bn1, + bn2, + der3, + span1, + span2, + span3, + nbase_n[0], + nbase_n[1], + nbase_n[2], + b0, ) # push-forward to physical domain @@ -537,13 +717,49 @@ def pusher_step5_old( # magnetic field (2-form) b[0] = eva3.evaluation_kernel_3d( - pn1, pd2, pd3, bn1, bd2, bd3, span1, span2 - 1, span3 - 1, nbase_n[0], nbase_d[1], nbase_d[2], b2_1 + pn1, + pd2, + pd3, + bn1, + bd2, + bd3, + span1, + span2 - 1, + span3 - 1, + nbase_n[0], + nbase_d[1], + nbase_d[2], + b2_1, ) b[1] = eva3.evaluation_kernel_3d( - pd1, pn2, pd3, bd1, bn2, bd3, span1 - 1, span2, span3 - 1, nbase_d[0], nbase_n[1], nbase_d[2], b2_2 + pd1, + pn2, + pd3, + bd1, + bn2, + bd3, + span1 - 1, + span2, + span3 - 1, + nbase_d[0], + nbase_n[1], + nbase_d[2], + b2_2, ) b[2] = eva3.evaluation_kernel_3d( - pd1, pd2, pn3, bd1, bd2, bn3, span1 - 1, span2 - 1, span3, nbase_d[0], nbase_d[1], nbase_n[2], b2_3 + pd1, + pd2, + pn3, + bd1, + bd2, + bn3, + span1 - 1, + span2 - 1, + span3, + nbase_d[0], + nbase_d[1], + nbase_n[2], + b2_3, ) b_prod[0, 1] = -b[2] @@ -794,13 +1010,49 @@ def pusher_step5( # magnetic field (2-form) b[0] = eva3.evaluation_kernel_3d( - pn1, pd2, pd3, bn1, bd2, bd3, span1, span2 - 1, span3 - 1, nbase_n[0], nbase_d[1], nbase_d[2], b2_1 + pn1, + pd2, + pd3, + bn1, + bd2, + bd3, + span1, + span2 - 1, + span3 - 1, + nbase_n[0], + nbase_d[1], + nbase_d[2], + b2_1, ) b[1] = eva3.evaluation_kernel_3d( - pd1, pn2, pd3, bd1, bn2, bd3, span1 - 1, span2, span3 - 1, nbase_d[0], nbase_n[1], nbase_d[2], b2_2 + pd1, + pn2, + pd3, + bd1, + bn2, + bd3, + span1 - 1, + span2, + span3 - 1, + nbase_d[0], + nbase_n[1], + nbase_d[2], + b2_2, ) b[2] = eva3.evaluation_kernel_3d( - pd1, pd2, pn3, bd1, bd2, bn3, span1 - 1, span2 - 1, span3, nbase_d[0], nbase_d[1], nbase_n[2], b2_3 + pd1, + pd2, + pn3, + bd1, + bd2, + bn3, + span1 - 1, + span2 - 1, + span3, + nbase_d[0], + nbase_d[1], + nbase_n[2], + b2_3, ) # push-forward to physical domain diff --git a/src/struphy/pic/tests/test_pic_legacy_files/spline_evaluation_3d.py b/src/struphy/pic/tests/test_pic_legacy_files/spline_evaluation_3d.py index a40d12fc9..7923b3966 100644 --- a/src/struphy/pic/tests/test_pic_legacy_files/spline_evaluation_3d.py +++ b/src/struphy/pic/tests/test_pic_legacy_files/spline_evaluation_3d.py @@ -127,7 +127,19 @@ def evaluate_n_n_n( # sum up non-vanishing contributions value = evaluation_kernel_3d( - pn1, pn2, pn3, bn1, bn2, bn3, span_n1, span_n2, span_n3, nbase_n1, nbase_n2, nbase_n3, coeff + pn1, + pn2, + pn3, + bn1, + bn2, + bn3, + span_n1, + span_n2, + span_n3, + nbase_n1, + nbase_n2, + nbase_n3, + coeff, ) return value @@ -189,7 +201,19 @@ def evaluate_diffn_n_n( # sum up non-vanishing contributions value = evaluation_kernel_3d( - pn1, pn2, pn3, bn1, bn2, bn3, span_n1, span_n2, span_n3, nbase_n1, nbase_n2, nbase_n3, coeff + pn1, + pn2, + pn3, + bn1, + bn2, + bn3, + span_n1, + span_n2, + span_n3, + nbase_n1, + nbase_n2, + nbase_n3, + coeff, ) return value @@ -251,7 +275,19 @@ def evaluate_n_diffn_n( # sum up non-vanishing contributions value = evaluation_kernel_3d( - pn1, pn2, pn3, bn1, bn2, bn3, span_n1, span_n2, span_n3, nbase_n1, nbase_n2, nbase_n3, coeff + pn1, + pn2, + pn3, + bn1, + bn2, + bn3, + span_n1, + span_n2, + span_n3, + nbase_n1, + nbase_n2, + nbase_n3, + coeff, ) return value @@ -313,7 +349,19 @@ def evaluate_n_n_diffn( # sum up non-vanishing contributions value = evaluation_kernel_3d( - pn1, pn2, pn3, bn1, bn2, bn3, span_n1, span_n2, span_n3, nbase_n1, nbase_n2, nbase_n3, coeff + pn1, + pn2, + pn3, + bn1, + bn2, + bn3, + span_n1, + span_n2, + span_n3, + nbase_n1, + nbase_n2, + nbase_n3, + coeff, ) return value @@ -377,7 +425,19 @@ def evaluate_d_n_n( # sum up non-vanishing contributions value = evaluation_kernel_3d( - pd1, pn2, pn3, bd1, bn2, bn3, span_d1, span_n2, span_n3, nbase_d1, nbase_n2, nbase_n3, coeff + pd1, + pn2, + pn3, + bd1, + bn2, + bn3, + span_d1, + span_n2, + span_n3, + nbase_d1, + nbase_n2, + nbase_n3, + coeff, ) return value @@ -441,7 +501,19 @@ def evaluate_n_d_n( # sum up non-vanishing contributions value = evaluation_kernel_3d( - pn1, pd2, pn3, bn1, bd2, bn3, span_n1, span_d2, span_n3, nbase_n1, nbase_d2, nbase_n3, coeff + pn1, + pd2, + pn3, + bn1, + bd2, + bn3, + span_n1, + span_d2, + span_n3, + nbase_n1, + nbase_d2, + nbase_n3, + coeff, ) return value @@ -505,7 +577,19 @@ def evaluate_n_n_d( # sum up non-vanishing contributions value = evaluation_kernel_3d( - pn1, pn2, pd3, bn1, bn2, bd3, span_n1, span_n2, span_d3, nbase_n1, nbase_n2, nbase_d3, coeff + pn1, + pn2, + pd3, + bn1, + bn2, + bd3, + span_n1, + span_n2, + span_d3, + nbase_n1, + nbase_n2, + nbase_d3, + coeff, ) return value @@ -570,7 +654,19 @@ def evaluate_n_d_d( # sum up non-vanishing contributions value = evaluation_kernel_3d( - pn1, pd2, pd3, bn1, bd2, bd3, span_n1, span_d2, span_d3, nbase_n1, nbase_d2, nbase_d3, coeff + pn1, + pd2, + pd3, + bn1, + bd2, + bd3, + span_n1, + span_d2, + span_d3, + nbase_n1, + nbase_d2, + nbase_d3, + coeff, ) return value @@ -635,7 +731,19 @@ def evaluate_d_n_d( # sum up non-vanishing contributions value = evaluation_kernel_3d( - pd1, pn2, pd3, bd1, bn2, bd3, span_d1, span_n2, span_d3, nbase_d1, nbase_n2, nbase_d3, coeff + pd1, + pn2, + pd3, + bd1, + bn2, + bd3, + span_d1, + span_n2, + span_d3, + nbase_d1, + nbase_n2, + nbase_d3, + coeff, ) return value @@ -700,7 +808,19 @@ def evaluate_d_d_n( # sum up non-vanishing contributions value = evaluation_kernel_3d( - pd1, pd2, pn3, bd1, bd2, bn3, span_d1, span_d2, span_n3, nbase_d1, nbase_d2, nbase_n3, coeff + pd1, + pd2, + pn3, + bd1, + bd2, + bn3, + span_d1, + span_d2, + span_n3, + nbase_d1, + nbase_d2, + nbase_n3, + coeff, ) return value @@ -766,7 +886,19 @@ def evaluate_d_d_d( # sum up non-vanishing contributions value = evaluation_kernel_3d( - pd1, pd2, pd3, bd1, bd2, bd3, span_d1, span_d2, span_d3, nbase_d1, nbase_d2, nbase_d3, coeff + pd1, + pd2, + pd3, + bd1, + bd2, + bd3, + span_d1, + span_d2, + span_d3, + nbase_d1, + nbase_d2, + nbase_d3, + coeff, ) return value @@ -815,41 +947,137 @@ def evaluate_tensor_product( # V0 - space if kind == 0: values[i1, i2, i3] = evaluate_n_n_n( - t1, t2, t3, p1, p2, p3, nbase_1, nbase_2, nbase_3, coeff, eta1[i1], eta2[i2], eta3[i3] + t1, + t2, + t3, + p1, + p2, + p3, + nbase_1, + nbase_2, + nbase_3, + coeff, + eta1[i1], + eta2[i2], + eta3[i3], ) # V1 - space elif kind == 11: values[i1, i2, i3] = evaluate_d_n_n( - t1, t2, t3, p1, p2, p3, nbase_1, nbase_2, nbase_3, coeff, eta1[i1], eta2[i2], eta3[i3] + t1, + t2, + t3, + p1, + p2, + p3, + nbase_1, + nbase_2, + nbase_3, + coeff, + eta1[i1], + eta2[i2], + eta3[i3], ) elif kind == 12: values[i1, i2, i3] = evaluate_n_d_n( - t1, t2, t3, p1, p2, p3, nbase_1, nbase_2, nbase_3, coeff, eta1[i1], eta2[i2], eta3[i3] + t1, + t2, + t3, + p1, + p2, + p3, + nbase_1, + nbase_2, + nbase_3, + coeff, + eta1[i1], + eta2[i2], + eta3[i3], ) elif kind == 13: values[i1, i2, i3] = evaluate_n_n_d( - t1, t2, t3, p1, p2, p3, nbase_1, nbase_2, nbase_3, coeff, eta1[i1], eta2[i2], eta3[i3] + t1, + t2, + t3, + p1, + p2, + p3, + nbase_1, + nbase_2, + nbase_3, + coeff, + eta1[i1], + eta2[i2], + eta3[i3], ) # V2 - space elif kind == 21: values[i1, i2, i3] = evaluate_n_d_d( - t1, t2, t3, p1, p2, p3, nbase_1, nbase_2, nbase_3, coeff, eta1[i1], eta2[i2], eta3[i3] + t1, + t2, + t3, + p1, + p2, + p3, + nbase_1, + nbase_2, + nbase_3, + coeff, + eta1[i1], + eta2[i2], + eta3[i3], ) elif kind == 22: values[i1, i2, i3] = evaluate_d_n_d( - t1, t2, t3, p1, p2, p3, nbase_1, nbase_2, nbase_3, coeff, eta1[i1], eta2[i2], eta3[i3] + t1, + t2, + t3, + p1, + p2, + p3, + nbase_1, + nbase_2, + nbase_3, + coeff, + eta1[i1], + eta2[i2], + eta3[i3], ) elif kind == 23: values[i1, i2, i3] = evaluate_d_d_n( - t1, t2, t3, p1, p2, p3, nbase_1, nbase_2, nbase_3, coeff, eta1[i1], eta2[i2], eta3[i3] + t1, + t2, + t3, + p1, + p2, + p3, + nbase_1, + nbase_2, + nbase_3, + coeff, + eta1[i1], + eta2[i2], + eta3[i3], ) # V3 - space elif kind == 3: values[i1, i2, i3] = evaluate_d_d_d( - t1, t2, t3, p1, p2, p3, nbase_1, nbase_2, nbase_3, coeff, eta1[i1], eta2[i2], eta3[i3] + t1, + t2, + t3, + p1, + p2, + p3, + nbase_1, + nbase_2, + nbase_3, + coeff, + eta1[i1], + eta2[i2], + eta3[i3], ) diff --git a/src/struphy/pic/tests/test_pushers.py b/src/struphy/pic/tests/test_pushers.py index d64076cd1..321ab9aba 100644 --- a/src/struphy/pic/tests/test_pushers.py +++ b/src/struphy/pic/tests/test_pushers.py @@ -6,7 +6,8 @@ @pytest.mark.parametrize("Nel", [[8, 9, 5], [7, 8, 9]]) @pytest.mark.parametrize("p", [[2, 3, 1], [1, 2, 3]]) @pytest.mark.parametrize( - "spl_kind", [[False, True, True], [True, False, True], [False, False, True], [True, True, True]] + "spl_kind", + [[False, True, True], [True, False, True], [False, False, True], [True, True, True]], ) @pytest.mark.parametrize( "mapping", @@ -141,7 +142,8 @@ def test_push_vxb_analytic(Nel, p, spl_kind, mapping, show_plots=False): @pytest.mark.parametrize("Nel", [[8, 9, 5], [7, 8, 9]]) @pytest.mark.parametrize("p", [[2, 3, 1], [1, 2, 3]]) @pytest.mark.parametrize( - "spl_kind", [[False, True, True], [True, False, True], [False, False, True], [True, True, True]] + "spl_kind", + [[False, True, True], [True, False, True], [False, False, True], [True, True, True]], ) @pytest.mark.parametrize( "mapping", @@ -287,7 +289,8 @@ def test_push_bxu_Hdiv(Nel, p, spl_kind, mapping, show_plots=False): @pytest.mark.parametrize("Nel", [[8, 9, 5], [7, 8, 9]]) @pytest.mark.parametrize("p", [[2, 3, 1], [1, 2, 3]]) @pytest.mark.parametrize( - "spl_kind", [[False, True, True], [True, False, True], [False, False, True], [True, True, True]] + "spl_kind", + [[False, True, True], [True, False, True], [False, False, True], [True, True, True]], ) @pytest.mark.parametrize( "mapping", @@ -433,7 +436,8 @@ def test_push_bxu_Hcurl(Nel, p, spl_kind, mapping, show_plots=False): @pytest.mark.parametrize("Nel", [[8, 9, 5], [7, 8, 9]]) @pytest.mark.parametrize("p", [[2, 3, 1], [1, 2, 3]]) @pytest.mark.parametrize( - "spl_kind", [[False, True, True], [True, False, True], [False, False, True], [True, True, True]] + "spl_kind", + [[False, True, True], [True, False, True], [False, False, True], [True, True, True]], ) @pytest.mark.parametrize( "mapping", @@ -579,7 +583,8 @@ def test_push_bxu_H1vec(Nel, p, spl_kind, mapping, show_plots=False): @pytest.mark.parametrize("Nel", [[8, 9, 5], [7, 8, 9]]) @pytest.mark.parametrize("p", [[2, 3, 1], [1, 2, 3]]) @pytest.mark.parametrize( - "spl_kind", [[False, True, True], [True, False, True], [False, False, True], [True, True, True]] + "spl_kind", + [[False, True, True], [True, False, True], [False, False, True], [True, True, True]], ) @pytest.mark.parametrize( "mapping", @@ -727,7 +732,8 @@ def test_push_bxu_Hdiv_pauli(Nel, p, spl_kind, mapping, show_plots=False): @pytest.mark.parametrize("Nel", [[8, 9, 5], [7, 8, 9]]) @pytest.mark.parametrize("p", [[2, 3, 1], [1, 2, 3]]) @pytest.mark.parametrize( - "spl_kind", [[False, True, True], [True, False, True], [False, False, True], [True, True, True]] + "spl_kind", + [[False, True, True], [True, False, True], [False, False, True], [True, True, True]], ) @pytest.mark.parametrize( "mapping", @@ -880,7 +886,11 @@ def test_push_eta_rk4(Nel, p, spl_kind, mapping, show_plots=False): if __name__ == "__main__": test_push_vxb_analytic( - [8, 9, 5], [4, 2, 3], [False, True, True], ["Colella", {"Lx": 2.0, "Ly": 2.0, "alpha": 0.1, "Lz": 4.0}], False + [8, 9, 5], + [4, 2, 3], + [False, True, True], + ["Colella", {"Lx": 2.0, "Ly": 2.0, "alpha": 0.1, "Lz": 4.0}], + False, ) # test_push_bxu_Hdiv([8, 9, 5], [4, 2, 3], [False, True, True], ['Colella', { # 'Lx': 2., 'Ly': 2., 'alpha': 0.1, 'Lz': 4.}], False) diff --git a/src/struphy/pic/tests/test_sorting.py b/src/struphy/pic/tests/test_sorting.py index 337dfaede..a11c9600e 100644 --- a/src/struphy/pic/tests/test_sorting.py +++ b/src/struphy/pic/tests/test_sorting.py @@ -73,7 +73,8 @@ def test_flattening(nx, ny, nz, algo): @pytest.mark.parametrize("Nel", [[8, 9, 10]]) @pytest.mark.parametrize("p", [[2, 3, 4]]) @pytest.mark.parametrize( - "spl_kind", [[False, False, True], [False, True, False], [True, False, True], [True, True, False]] + "spl_kind", + [[False, False, True], [False, True, False], [True, False, True], [True, True, False]], ) @pytest.mark.parametrize( "mapping", diff --git a/src/struphy/pic/tests/test_sph.py b/src/struphy/pic/tests/test_sph.py index 64b7b0cc3..294e7f9dc 100644 --- a/src/struphy/pic/tests/test_sph.py +++ b/src/struphy/pic/tests/test_sph.py @@ -111,9 +111,9 @@ def test_sph_evaluation_1d( err_max_norm = xp.max(xp.abs(all_eval - exact_eval)) / xp.max(xp.abs(exact_eval)) if rank == 0: - print(f"\n{boxes_per_dim = }") - print(f"{kernel = }, {derivative =}") - print(f"{bc_x = }, {eval_pts = }, {tesselation = }, {err_max_norm = }") + print(f"\n{boxes_per_dim =}") + print(f"{kernel =}, {derivative =}") + print(f"{bc_x =}, {eval_pts =}, {tesselation =}, {err_max_norm =}") if show_plot: plt.figure(figsize=(12, 8)) plt.plot(ee1.squeeze(), fun_exact(ee1, ee2, ee3).squeeze(), label="exact") @@ -235,9 +235,9 @@ def test_sph_evaluation_2d( err_max_norm = xp.max(xp.abs(all_eval - exact_eval)) / xp.max(xp.abs(exact_eval)) if rank == 0: - print(f"\n{boxes_per_dim = }") - print(f"{kernel = }, {derivative =}") - print(f"{bc_x = }, {bc_y = }, {eval_pts = }, {tesselation = }, {err_max_norm = }") + print(f"\n{boxes_per_dim =}") + print(f"{kernel =}, {derivative =}") + print(f"{bc_x =}, {bc_y =}, {eval_pts =}, {tesselation =}, {err_max_norm =}") if show_plot: plt.figure(figsize=(12, 24)) plt.subplot(2, 1, 1) @@ -354,28 +354,28 @@ def test_sph_evaluation_3d( err_max_norm = xp.max(xp.abs(all_eval - exact_eval)) if rank == 0: - print(f"\n{boxes_per_dim = }") - print(f"{kernel = }, {derivative =}") - print(f"{bc_x = }, {bc_y = }, {bc_z = }, {eval_pts = }, {tesselation = }, {err_max_norm = }") + print(f"\n{boxes_per_dim =}") + print(f"{kernel =}, {derivative =}") + print(f"{bc_x =}, {bc_y =}, {bc_z =}, {eval_pts =}, {tesselation =}, {err_max_norm =}") if show_plot: - print(f"\n{fun_exact(ee1, ee2, ee3)[5, 5, 5] = }") - print(f"{ee1[5, 5, 5] = }, {ee2[5, 5, 5] = }, {ee3[5, 5, 5] = }") - print(f"{all_eval[5, 5, 5] = }") + print(f"\n{fun_exact(ee1, ee2, ee3)[5, 5, 5] =}") + print(f"{ee1[5, 5, 5] =}, {ee2[5, 5, 5] =}, {ee3[5, 5, 5] =}") + print(f"{all_eval[5, 5, 5] =}") - print(f"\n{ee1[4, 4, 4] = }, {ee2[4, 4, 4] = }, {ee3[4, 4, 4] = }") - print(f"{all_eval[4, 4, 4] = }") + print(f"\n{ee1[4, 4, 4] =}, {ee2[4, 4, 4] =}, {ee3[4, 4, 4] =}") + print(f"{all_eval[4, 4, 4] =}") - print(f"\n{ee1[3, 3, 3] = }, {ee2[3, 3, 3] = }, {ee3[3, 3, 3] = }") - print(f"{all_eval[3, 3, 3] = }") + print(f"\n{ee1[3, 3, 3] =}, {ee2[3, 3, 3] =}, {ee3[3, 3, 3] =}") + print(f"{all_eval[3, 3, 3] =}") - print(f"\n{ee1[2, 2, 2] = }, {ee2[2, 2, 2] = }, {ee3[2, 2, 2] = }") - print(f"{all_eval[2, 2, 2] = }") + print(f"\n{ee1[2, 2, 2] =}, {ee2[2, 2, 2] =}, {ee3[2, 2, 2] =}") + print(f"{all_eval[2, 2, 2] =}") - print(f"\n{ee1[1, 1, 1] = }, {ee2[1, 1, 1] = }, {ee3[1, 1, 1] = }") - print(f"{all_eval[1, 1, 1] = }") + print(f"\n{ee1[1, 1, 1] =}, {ee2[1, 1, 1] =}, {ee3[1, 1, 1] =}") + print(f"{all_eval[1, 1, 1] =}") - print(f"\n{ee1[0, 0, 0] = }, {ee2[0, 0, 0] = }, {ee3[0, 0, 0] = }") - print(f"{all_eval[0, 0, 0] = }") + print(f"\n{ee1[0, 0, 0] =}, {ee2[0, 0, 0] =}, {ee3[0, 0, 0] =}") + print(f"{all_eval[0, 0, 0] =}") # plt.figure(figsize=(12, 24)) # plt.subplot(2, 1, 1) # plt.pcolor(ee1[0, :, :], ee2[0, :, :], fun_exact(ee1, ee2, ee3)[0, :, :]) @@ -478,12 +478,12 @@ def test_evaluation_SPH_Np_convergence_1d(boxes_per_dim, bc_x, eval_pts, tessela plt.figure() plt.plot(ee1.squeeze(), exact_eval.squeeze(), label="exact") plt.plot(ee1.squeeze(), all_eval.squeeze(), "--.", label="eval_sph") - plt.title(f"{Np = }, {ppb = }") + plt.title(f"{Np =}, {ppb =}") # plt.savefig(f"fun_{Np}_{ppb}.png") diff = xp.max(xp.abs(all_eval - exact_eval)) / xp.max(xp.abs(exact_eval)) err_vec += [diff] - print(f"{Np = }, {ppb = }, {diff = }") + print(f"{Np =}, {ppb =}, {diff =}") if tesselation: fit = xp.polyfit(xp.log(ppbs), xp.log(err_vec), 1) @@ -501,7 +501,7 @@ def test_evaluation_SPH_Np_convergence_1d(boxes_per_dim, bc_x, eval_pts, tessela # plt.savefig(f"Convergence_SPH_{tesselation=}") if rank == 0: - print(f"\n{bc_x = }, {eval_pts = }, {tesselation = }, {fit[0] = }") + print(f"\n{bc_x =}, {eval_pts =}, {tesselation =}, {fit[0] =}") if tesselation: assert fit[0] < 2e-3 @@ -594,13 +594,13 @@ def test_evaluation_SPH_h_convergence_1d(boxes_per_dim, bc_x, eval_pts, tesselat plt.figure() plt.plot(ee1.squeeze(), exact_eval.squeeze(), label="exact") plt.plot(ee1.squeeze(), all_eval.squeeze(), "--.", label="eval_sph") - plt.title(f"{h1 = }") + plt.title(f"{h1 =}") # plt.savefig(f"fun_{h1}.png") # error in max-norm diff = xp.max(xp.abs(all_eval - exact_eval)) / xp.max(xp.abs(exact_eval)) - print(f"{h1 = }, {diff = }") + print(f"{h1 =}, {diff =}") if tesselation and h1 < 0.256: assert diff < 0.036 @@ -621,7 +621,7 @@ def test_evaluation_SPH_h_convergence_1d(boxes_per_dim, bc_x, eval_pts, tesselat # plt.savefig("Convergence_SPH") if rank == 0: - print(f"\n{bc_x = }, {eval_pts = }, {tesselation = }, {fit[0] = }") + print(f"\n{bc_x =}, {eval_pts =}, {tesselation =}, {fit[0] =}") if not tesselation: assert xp.abs(fit[0] + 0.5) < 0.1 # Monte Carlo rate @@ -718,7 +718,7 @@ def test_evaluation_mc_Np_and_h_convergence_1d(boxes_per_dim, bc_x, eval_pts, te err_vec[-1] += [diff] if rank == 0: - print(f"{Np = }, {ppb = }, {diff = }") + print(f"{Np =}, {ppb =}, {diff =}") # if show_plot: # plt.figure() # plt.plot(ee1.squeeze(), fun_exact(ee1, ee2, ee3).squeeze(), label="exact") @@ -756,7 +756,7 @@ def test_evaluation_mc_Np_and_h_convergence_1d(boxes_per_dim, bc_x, eval_pts, te plt.show() if rank == 0: - print(f"\n{tesselation = }, {bc_x = }, {err_min = }") + print(f"\n{tesselation =}, {bc_x =}, {err_min =}") if tesselation: if bc_x == "periodic": @@ -879,14 +879,14 @@ def test_evaluation_SPH_Np_convergence_2d(boxes_per_dim, bc_x, bc_y, tesselation assert diff < 0.06 if rank == 0: - print(f"{Np = }, {ppb = }, {diff = }") + print(f"{Np =}, {ppb =}, {diff =}") if show_plot: fig, ax = plt.subplots() d = ax.pcolor(ee1.squeeze(), ee2.squeeze(), all_eval.squeeze(), label="eval_sph", vmin=1.0, vmax=2.0) fig.colorbar(d, ax=ax, label="2d_SPH") ax.set_xlabel("ee1") ax.set_ylabel("ee2") - ax.set_title(f"{Np}_{ppb = }") + ax.set_title(f"{Np}_{ppb =}") # fig.savefig(f"2d_sph_{Np}_{ppb}.png") if tesselation: @@ -905,7 +905,7 @@ def test_evaluation_SPH_Np_convergence_2d(boxes_per_dim, bc_x, bc_y, tesselation # plt.savefig(f"Convergence_SPH_{tesselation=}") if rank == 0: - print(f"\n{bc_x = }, {tesselation = }, {fit[0] = }") + print(f"\n{bc_x =}, {tesselation =}, {fit[0] =}") if not tesselation: assert xp.abs(fit[0] + 0.5) < 0.1 # Monte Carlo rate diff --git a/src/struphy/pic/tests/test_tesselation.py b/src/struphy/pic/tests/test_tesselation.py index cf6ed922e..b138af50a 100644 --- a/src/struphy/pic/tests/test_tesselation.py +++ b/src/struphy/pic/tests/test_tesselation.py @@ -176,7 +176,7 @@ def test_cell_average(ppb, nx, ny, nz, n_quad, show_plot=False): plt.show() # test - print(f"\n{rank = }, {xp.max(xp.abs(particles.weights - particles.f_init(particles.positions))) = }") + print(f"\n{rank =}, {xp.max(xp.abs(particles.weights - particles.f_init(particles.positions))) =}") assert xp.max(xp.abs(particles.weights - particles.f_init(particles.positions))) < 0.012 diff --git a/src/struphy/polar/basic.py b/src/struphy/polar/basic.py index 6a1c2c2f2..f737c671e 100644 --- a/src/struphy/polar/basic.py +++ b/src/struphy/polar/basic.py @@ -347,7 +347,7 @@ def toarray(self, allreduce=False): out2, self.pol[2].flatten(), out3, - ) + ), ) return out diff --git a/src/struphy/polar/extraction_operators.py b/src/struphy/polar/extraction_operators.py index f58c88f59..1c2a461ce 100644 --- a/src/struphy/polar/extraction_operators.py +++ b/src/struphy/polar/extraction_operators.py @@ -72,7 +72,7 @@ def __init__(self, domain, derham): ((self.cx[1] - self.pole[0]) * (-2)).max(), ((self.cx[1] - self.pole[0]) - xp.sqrt(3) * (self.cy[1] - self.pole[1])).max(), ((self.cx[1] - self.pole[0]) + xp.sqrt(3) * (self.cy[1] - self.pole[1])).max(), - ] + ], ) # barycentric coordinates @@ -692,7 +692,7 @@ def __init__(self, cx, cy): (-2 * (cx[1] - self.x0)).max(), ((cx[1] - self.x0) - xp.sqrt(3) * (cy[1] - self.y0)).max(), ((cx[1] - self.x0) + xp.sqrt(3) * (cy[1] - self.y0)).max(), - ] + ], ).max() self.Xi_0 = xp.zeros((3, n1), dtype=float) @@ -966,7 +966,7 @@ def __init__(self, tensor_space, cx, cy): # size of control triangle self.tau = xp.array( - [(-2 * cx[1]).max(), (cx[1] - xp.sqrt(3) * cy[1]).max(), (cx[1] + xp.sqrt(3) * cy[1]).max()] + [(-2 * cx[1]).max(), (cx[1] - xp.sqrt(3) * cy[1]).max(), (cx[1] + xp.sqrt(3) * cy[1]).max()], ).max() self.Xi_0 = xp.zeros((3, n1), dtype=float) @@ -982,7 +982,8 @@ def __init__(self, tensor_space, cx, cy): # =========== extraction operators for discrete 0-forms ================== # extraction operator for basis functions self.E0_pol = spa.bmat( - [[xp.hstack((self.Xi_0, self.Xi_1)), None], [None, spa.identity((n0 - 2) * n1)]], format="csr" + [[xp.hstack((self.Xi_0, self.Xi_1)), None], [None, spa.identity((n0 - 2) * n1)]], + format="csr", ) self.E0 = spa.kron(self.E0_pol, spa.identity(n2), format="csr") @@ -1215,7 +1216,7 @@ def __init__(self, tensor_space, cx, cy): [ [None, -spa.kron(spa.identity((n0 - 2) * d1 + 2), grad_1d_3)], [spa.kron(spa.identity((d0 - 1) * n1), grad_1d_3), None], - ] + ], ) # total polar curl diff --git a/src/struphy/polar/linear_operators.py b/src/struphy/polar/linear_operators.py index 03a3e504a..0e37c7e76 100644 --- a/src/struphy/polar/linear_operators.py +++ b/src/struphy/polar/linear_operators.py @@ -334,7 +334,14 @@ class PolarLinearOperator(LinOpWithTransp): """ def __init__( - self, V, W, tp_operator=None, blocks_pol_to_ten=None, blocks_pol_to_pol=None, blocks_e3=None, transposed=False + self, + V, + W, + tp_operator=None, + blocks_pol_to_ten=None, + blocks_pol_to_pol=None, + blocks_e3=None, + transposed=False, ): assert isinstance(V, PolarDerhamSpace) assert isinstance(W, PolarDerhamSpace) diff --git a/src/struphy/post_processing/likwid/plot_likwidproject.py b/src/struphy/post_processing/likwid/plot_likwidproject.py index feda5d3b6..cde2a2b76 100644 --- a/src/struphy/post_processing/likwid/plot_likwidproject.py +++ b/src/struphy/post_processing/likwid/plot_likwidproject.py @@ -818,7 +818,7 @@ def load_projects(data_paths, procs_per_clone="any"): ) if (procs_per_clone != "any") and (procs_per_clone != project.procs_per_clone): print( - f"Incorrect number of procs_per_clone: {project.procs_per_clone = } {procs_per_clone = }", + f"Incorrect number of procs_per_clone: {project.procs_per_clone =} {procs_per_clone =}", ) continue project.read_project() diff --git a/src/struphy/post_processing/likwid/plot_time_traces.py b/src/struphy/post_processing/likwid/plot_time_traces.py index ed0a34010..f97681ffa 100644 --- a/src/struphy/post_processing/likwid/plot_time_traces.py +++ b/src/struphy/post_processing/likwid/plot_time_traces.py @@ -54,7 +54,7 @@ def plot_time_vs_duration( plt.figure(figsize=(10, 6)) for path in paths: - print(f"{path = }") + print(f"{path =}") with open(path, "rb") as file: profiling_data = pickle.load(file) @@ -204,7 +204,7 @@ def plot_gantt_chart_plotly( Start=start_times[i], Finish=end_times[i], Duration=durations[i], - ) + ), ) if len(bars) == 0: @@ -226,7 +226,7 @@ def plot_gantt_chart_plotly( name=bar["Rank"], marker_color=rank_color_map[bar["Rank"]], hovertemplate=f"Rank: {bar['Rank']}
Start: {bar['Start']:.3f}s
Duration: {bar['Duration']:.3f}s", - ) + ), ) fig.update_layout( diff --git a/src/struphy/post_processing/pproc_struphy.py b/src/struphy/post_processing/pproc_struphy.py index 044ec0de8..940c94ab3 100644 --- a/src/struphy/post_processing/pproc_struphy.py +++ b/src/struphy/post_processing/pproc_struphy.py @@ -96,12 +96,17 @@ def main( fields, t_grid = pproc.create_femfields(path, params_in, step=step) point_data, grids_log, grids_phy = pproc.eval_femfields( - params_in, fields, celldivide=[celldivide, celldivide, celldivide] + params_in, + fields, + celldivide=[celldivide, celldivide, celldivide], ) if physical: point_data_phy, grids_log, grids_phy = pproc.eval_femfields( - params_in, fields, celldivide=[celldivide, celldivide, celldivide], physical=True + params_in, + fields, + celldivide=[celldivide, celldivide, celldivide], + physical=True, ) # directory for field data @@ -196,14 +201,19 @@ def main( libpath = struphy.__path__[0] parser = argparse.ArgumentParser( - description="Post-process data of finished Struphy runs to prepare for diagnostics." + description="Post-process data of finished Struphy runs to prepare for diagnostics.", ) # paths of simulation folders parser.add_argument("dir", type=str, metavar="DIR", help="absolute path of simulation ouput folder to post-process") parser.add_argument( - "-s", "--step", type=int, metavar="N", help="do post-processing every N-th time step (default=1)", default=1 + "-s", + "--step", + type=int, + metavar="N", + help="do post-processing every N-th time step (default=1)", + default=1, ) parser.add_argument( @@ -221,11 +231,15 @@ def main( ) parser.add_argument( - "--guiding-center", help="compute guiding-center coordinates (only from Particles6D)", action="store_true" + "--guiding-center", + help="compute guiding-center coordinates (only from Particles6D)", + action="store_true", ) parser.add_argument( - "--classify", help="classify guiding-center trajectories (passing, trapped or lost)", action="store_true" + "--classify", + help="classify guiding-center trajectories (passing, trapped or lost)", + action="store_true", ) parser.add_argument("--no-vtk", help="whether vtk files creation should be skipped", action="store_true") diff --git a/src/struphy/post_processing/profile_struphy.py b/src/struphy/post_processing/profile_struphy.py index da4632555..43d4be47d 100644 --- a/src/struphy/post_processing/profile_struphy.py +++ b/src/struphy/post_processing/profile_struphy.py @@ -93,7 +93,7 @@ def main(): + "ncalls".ljust(15) + "totime".ljust(15) + "percall".ljust(15) - + "cumtime".ljust(15) + + "cumtime".ljust(15), ) print("-" * 154) for position, key in enumerate(dicts[0].keys()): @@ -157,7 +157,7 @@ def main(): plt.xlabel("mpi_size") plt.ylabel("time [s]") plt.title( - "Weak scaling for cells/mpi_size=" + str(xp.prod(val["Nel"][0]) / val["mpi_size"][0]) + "=const." + "Weak scaling for cells/mpi_size=" + str(xp.prod(val["Nel"][0]) / val["mpi_size"][0]) + "=const.", ) plt.legend(loc="upper left") # plt.loglog(val['mpi_size'], val['time'][0]*xp.ones_like(val['time']), 'k--', alpha=0.3) diff --git a/src/struphy/propagators/base.py b/src/struphy/propagators/base.py index f69d4c7fe..945107bbd 100644 --- a/src/struphy/propagators/base.py +++ b/src/struphy/propagators/base.py @@ -259,7 +259,7 @@ def add_init_kernel( column_nr, comps, args_init, - ) + ), ] def add_eval_kernel( @@ -314,5 +314,5 @@ def add_eval_kernel( column_nr, comps, args_eval, - ) + ), ] diff --git a/src/struphy/propagators/propagators_coupling.py b/src/struphy/propagators/propagators_coupling.py index 80d483568..0b8760b6a 100644 --- a/src/struphy/propagators/propagators_coupling.py +++ b/src/struphy/propagators/propagators_coupling.py @@ -1037,7 +1037,7 @@ def allocate(self): kernel = Pyccelkernel(pusher_kernels.push_bxu_H1vec) else: raise ValueError( - f'{self.options.u_space = } not valid, choose from "Hcurl", "Hdiv" or "H1vec.', + f'{self.options.u_space =} not valid, choose from "Hcurl", "Hdiv" or "H1vec.', ) # instantiate Pusher @@ -1341,7 +1341,7 @@ def allocate(self): pusher_kernel = Pyccelkernel(pusher_kernels_gc.push_gc_cc_J1_H1vec) else: raise ValueError( - f'{self.options.u_space = } not valid, choose from "Hcurl", "Hdiv" or "H1vec.', + f'{self.options.u_space =} not valid, choose from "Hcurl", "Hdiv" or "H1vec.', ) args_pusher_kernel = ( @@ -1631,7 +1631,7 @@ def allocate(self): self._pusher_kernel = pusher_kernels_gc.push_gc_cc_J2_stage_H1vec else: raise ValueError( - f'{self.options.u_space = } not valid, choose from "Hdiv" or "H1vec.', + f'{self.options.u_space =} not valid, choose from "Hdiv" or "H1vec.', ) # temp fix due to refactoring of ButcherTableau: @@ -1978,7 +1978,7 @@ def __call__(self, dt): sum_u_diff_loc = xp.sum((u_diff.toarray() ** 2)) sum_H_diff_loc = xp.sum( - (markers[~holes, :3] - markers[~holes, first_init_idx : first_init_idx + 3]) ** 2 + (markers[~holes, :3] - markers[~holes, first_init_idx : first_init_idx + 3]) ** 2, ) buffer_array = xp.array([sum_u_diff_loc]) @@ -2072,7 +2072,7 @@ def __call__(self, dt): ) sum_H_diff_loc = xp.sum( - xp.abs(markers[~holes, 0:3] - markers[~holes, first_free_idx : first_free_idx + 3]) + xp.abs(markers[~holes, 0:3] - markers[~holes, first_free_idx : first_free_idx + 3]), ) if particles.mpi_comm is not None: @@ -2154,7 +2154,7 @@ def __call__(self, dt): if iter_num == self.options.dg_solver_params.maxiter: if self.options.dg_solver_params.info and MPI.COMM_WORLD.Get_rank() == 0: print( - f"{iter_num = }, maxiter={self.options.dg_solver_params.maxiter} reached! diff: {diff}, e_diff: {e_diff}", + f"{iter_num =}, maxiter={self.options.dg_solver_params.maxiter} reached! diff: {diff}, e_diff: {e_diff}", ) if particles.mpi_comm is not None: particles.mpi_comm.Barrier() diff --git a/src/struphy/propagators/propagators_fields.py b/src/struphy/propagators/propagators_fields.py index f7ac3a3c3..c3f3e1381 100644 --- a/src/struphy/propagators/propagators_fields.py +++ b/src/struphy/propagators/propagators_fields.py @@ -2594,7 +2594,7 @@ def allocate(self): if xp.abs(self.options.sigma_1) < 1e-14: self.options.sigma_1 = 1e-14 if MPI.COMM_WORLD.Get_rank() == 0: - print(f"Stabilizing Poisson solve with {self.options.sigma_1 = }") + print(f"Stabilizing Poisson solve with {self.options.sigma_1 =}") # model parameters self._sigma_1 = self.options.sigma_1 @@ -2617,7 +2617,7 @@ def verify_rhs(rho) -> StencilVector | FEECVariable | AccumulatorVector: elif isinstance(rho, Callable): rhs = L2Projector("H1", self.mass_ops).get_dofs(rho, apply_bc=True) else: - raise TypeError(f"{type(rho) = } is not accepted.") + raise TypeError(f"{type(rho) =} is not accepted.") return rhs @@ -3029,7 +3029,7 @@ def __call_newton(self, dt): if it == self.options.nonlin_solver.maxiter - 1 or xp.isnan(err): print( - f"!!!WARNING: Maximum iteration in VariationalMomentumAdvection reached - not converged \n {err = } \n {tol**2 = }", + f"!!!WARNING: Maximum iteration in VariationalMomentumAdvection reached - not converged \n {err =} \n {tol**2 =}", ) self.update_feec_variables(u=un1) @@ -3075,7 +3075,7 @@ def __call_picard(self, dt): if it == self.options.nonlin_solver.maxiter - 1 or xp.isnan(err): print( - f"!!!WARNING: Maximum iteration in VariationalMomentumAdvection reached - not converged \n {err = } \n {tol**2 = }", + f"!!!WARNING: Maximum iteration in VariationalMomentumAdvection reached - not converged \n {err =} \n {tol**2 =}", ) self.update_feec_variables(u=un1) @@ -3427,7 +3427,7 @@ def __call_newton(self, dt): if it == self._nonlin_solver.maxiter - 1 or xp.isnan(err): print( - f"!!!Warning: Maximum iteration in VariationalDensityEvolve reached - not converged:\n {err = } \n {tol**2 = }", + f"!!!Warning: Maximum iteration in VariationalDensityEvolve reached - not converged:\n {err =} \n {tol**2 =}", ) self._tmp_un_diff = un1 - un @@ -3605,11 +3605,15 @@ def _update_linear_form_dl_drho(self, rhon, rhon1, un, un1, sn): def _compute_init_linear_form(self): if abs(self._gamma - 5 / 3) < 1e-3: self._energy_evaluator.evaluate_exact_de_drho_grid( - self.projected_equil.n3, self.projected_equil.s3_monoatomic, out=self._init_dener_drho + self.projected_equil.n3, + self.projected_equil.s3_monoatomic, + out=self._init_dener_drho, ) elif abs(self._gamma - 7 / 5) < 1e-3: self._energy_evaluator.evaluate_exact_de_drho_grid( - self.projected_equil.n3, self.projected_equil.s3_diatomic, out=self._init_dener_drho + self.projected_equil.n3, + self.projected_equil.s3_diatomic, + out=self._init_dener_drho, ) else: raise ValueError("Gamma should be 7/5 or 5/3 for if you want to linearize") @@ -3890,7 +3894,7 @@ def __call_newton(self, dt): if it == self._nonlin_solver.maxiter - 1 or xp.isnan(err): print( - f"!!!Warning: Maximum iteration in VariationalEntropyEvolve reached - not converged:\n {err = } \n {tol**2 = }", + f"!!!Warning: Maximum iteration in VariationalEntropyEvolve reached - not converged:\n {err =} \n {tol**2 =}", ) self._tmp_sn_diff = sn1 - sn self._tmp_un_diff = un1 - un @@ -4024,11 +4028,15 @@ def _update_linear_form_dl_ds(self, rhon, sn, sn1): def _compute_init_linear_form(self): if abs(self._gamma - 5 / 3) < 1e-3: self._energy_evaluator.evaluate_exact_de_ds_grid( - self.projected_equil.n3, self.projected_equil.s3_monoatomic, out=self._init_dener_ds + self.projected_equil.n3, + self.projected_equil.s3_monoatomic, + out=self._init_dener_ds, ) elif abs(self._gamma - 7 / 5) < 1e-3: self._energy_evaluator.evaluate_exact_de_ds_grid( - self.projected_equil.n3, self.projected_equil.s3_diatomic, out=self._init_dener_ds + self.projected_equil.n3, + self.projected_equil.s3_diatomic, + out=self._init_dener_ds, ) else: raise ValueError("Gamma should be 7/5 or 5/3 for if you want to linearize") @@ -4310,7 +4318,7 @@ def __call_newton(self, dt): if it == self._nonlin_solver.maxiter - 1 or xp.isnan(err): print( - f"!!!Warning: Maximum iteration in VariationalMagFieldEvolve reached - not converged:\n {err = } \n {tol**2 = }", + f"!!!Warning: Maximum iteration in VariationalMagFieldEvolve reached - not converged:\n {err =} \n {tol**2 =}", ) self._tmp_un_diff = un1 - un @@ -4813,7 +4821,7 @@ def __call_picard(self, dt): if it == self._nonlin_solver.maxiter - 1 or xp.isnan(err): print( - f"!!!Warning: Maximum iteration in VariationalPBEvolve reached - not converged:\n {err = } \n {tol**2 = }", + f"!!!Warning: Maximum iteration in VariationalPBEvolve reached - not converged:\n {err =} \n {tol**2 =}", ) self._tmp_un_diff = un1 - un @@ -5404,7 +5412,7 @@ def __call_picard(self, dt): if it == self._nonlin_solver.maxiter - 1 or xp.isnan(err): print( - f"!!!Warning: Maximum iteration in VariationalPBEvolve reached - not converged:\n {err = } \n {tol**2 = }", + f"!!!Warning: Maximum iteration in VariationalPBEvolve reached - not converged:\n {err =} \n {tol**2 =}", ) self._tmp_un_diff = un1 - un @@ -5996,7 +6004,7 @@ def __call_newton(self, dt): if it == self._nonlin_solver["maxiter"] - 1 or xp.isnan(err): print( - f"!!!Warning: Maximum iteration in VariationalViscosity reached - not converged:\n {err = } \n {tol**2 = }", + f"!!!Warning: Maximum iteration in VariationalViscosity reached - not converged:\n {err =} \n {tol**2 =}", ) self.update_feec_variables(s=sn1, u=un1) @@ -6760,7 +6768,7 @@ def __call_newton(self, dt): if it == self._nonlin_solver["maxiter"] - 1 or xp.isnan(err): print( - f"!!!Warning: Maximum iteration in VariationalResistivity reached - not converged:\n {err = } \n {tol**2 = }", + f"!!!Warning: Maximum iteration in VariationalResistivity reached - not converged:\n {err =} \n {tol**2 =}", ) self.update_feec_variables(s=sn1, b=bn1) @@ -7222,7 +7230,7 @@ def hfun(t): def hfun(t): return xp.sin(self.options.omega * t) else: - raise NotImplementedError(f"{self.options.hfun = } not implemented.") + raise NotImplementedError(f"{self.options.hfun =} not implemented.") self._hfun = hfun self._c0 = self.variables.source.spline.vector.copy() @@ -7557,7 +7565,7 @@ def allocate(self): if self.options.c_fun == "const": c_fun = lambda e1, e2, e3: 0.0 + 0.0 * e1 else: - raise NotImplementedError(f"{self.options.c_fun = } is not available.") + raise NotImplementedError(f"{self.options.c_fun =} is not available.") # expose equation parameters self._kappa = self.options.kappa @@ -7939,16 +7947,32 @@ def allocate(self): # pullback callable funx = TransformedPformComponent( - forceterm_class, given_in_basis="physical", out_form="2", comp=0, domain=self.domain + forceterm_class, + given_in_basis="physical", + out_form="2", + comp=0, + domain=self.domain, ) funy = TransformedPformComponent( - forceterm_class, given_in_basis="physical", out_form="2", comp=1, domain=self.domain + forceterm_class, + given_in_basis="physical", + out_form="2", + comp=1, + domain=self.domain, ) fun_electronsx = TransformedPformComponent( - forcetermelectrons_class, given_in_basis="physical", out_form="2", comp=0, domain=self.domain + forcetermelectrons_class, + given_in_basis="physical", + out_form="2", + comp=0, + domain=self.domain, ) fun_electronsy = TransformedPformComponent( - forcetermelectrons_class, given_in_basis="physical", out_form="2", comp=1, domain=self.domain + forcetermelectrons_class, + given_in_basis="physical", + out_form="2", + comp=1, + domain=self.domain, ) l2_proj = L2Projector(space_id="Hdiv", mass_ops=self.mass_ops) self._F1 = l2_proj([funx, funy, _forceterm_logical]) @@ -7983,22 +8007,46 @@ def allocate(self): # pullback callable fun_pb_1 = TransformedPformComponent( - forceterm_class, given_in_basis="physical", out_form="2", comp=0, domain=self.domain + forceterm_class, + given_in_basis="physical", + out_form="2", + comp=0, + domain=self.domain, ) fun_pb_2 = TransformedPformComponent( - forceterm_class, given_in_basis="physical", out_form="2", comp=1, domain=self.domain + forceterm_class, + given_in_basis="physical", + out_form="2", + comp=1, + domain=self.domain, ) fun_pb_3 = TransformedPformComponent( - forceterm_class, given_in_basis="physical", out_form="2", comp=2, domain=self.domain + forceterm_class, + given_in_basis="physical", + out_form="2", + comp=2, + domain=self.domain, ) fun_electrons_pb_1 = TransformedPformComponent( - forcetermelectrons_class, given_in_basis="physical", out_form="2", comp=0, domain=self.domain + forcetermelectrons_class, + given_in_basis="physical", + out_form="2", + comp=0, + domain=self.domain, ) fun_electrons_pb_2 = TransformedPformComponent( - forcetermelectrons_class, given_in_basis="physical", out_form="2", comp=1, domain=self.domain + forcetermelectrons_class, + given_in_basis="physical", + out_form="2", + comp=1, + domain=self.domain, ) fun_electrons_pb_3 = TransformedPformComponent( - forcetermelectrons_class, given_in_basis="physical", out_form="2", comp=2, domain=self.domain + forcetermelectrons_class, + given_in_basis="physical", + out_form="2", + comp=2, + domain=self.domain, ) if self._lifting: l2_proj = L2Projector(space_id="Hdiv", mass_ops=self._mass_opsv0) @@ -8104,22 +8152,46 @@ def allocate(self): # pullback callable fun_pb_1 = TransformedPformComponent( - forceterm_class, given_in_basis="physical", out_form="2", comp=0, domain=self.domain + forceterm_class, + given_in_basis="physical", + out_form="2", + comp=0, + domain=self.domain, ) fun_pb_2 = TransformedPformComponent( - forceterm_class, given_in_basis="physical", out_form="2", comp=1, domain=self.domain + forceterm_class, + given_in_basis="physical", + out_form="2", + comp=1, + domain=self.domain, ) fun_pb_3 = TransformedPformComponent( - forceterm_class, given_in_basis="physical", out_form="2", comp=2, domain=self.domain + forceterm_class, + given_in_basis="physical", + out_form="2", + comp=2, + domain=self.domain, ) fun_electrons_pb_1 = TransformedPformComponent( - forcetermelectrons_class, given_in_basis="physical", out_form="2", comp=0, domain=self.domain + forcetermelectrons_class, + given_in_basis="physical", + out_form="2", + comp=0, + domain=self.domain, ) fun_electrons_pb_2 = TransformedPformComponent( - forcetermelectrons_class, given_in_basis="physical", out_form="2", comp=1, domain=self.domain + forcetermelectrons_class, + given_in_basis="physical", + out_form="2", + comp=1, + domain=self.domain, ) fun_electrons_pb_3 = TransformedPformComponent( - forcetermelectrons_class, given_in_basis="physical", out_form="2", comp=2, domain=self.domain + forcetermelectrons_class, + given_in_basis="physical", + out_form="2", + comp=2, + domain=self.domain, ) if self._lifting: l2_proj = L2Projector(space_id="Hdiv", mass_ops=self._mass_opsv0) @@ -8200,7 +8272,10 @@ def allocate(self): self._S21 = None if self.derhamv0.with_local_projectors: self._S21 = BasisProjectionOperatorLocal( - self.derhamv0._Ploc["1"], self.derhamv0.Vh_fem["2"], fun, transposed=False + self.derhamv0._Ploc["1"], + self.derhamv0.Vh_fem["2"], + fun, + transposed=False, ) if self._method_to_solve in ("DirectNPInverse", "InexactNPInverse"): @@ -8310,7 +8385,10 @@ def allocate(self): self._S21 = None if self.derham.with_local_projectors: self._S21 = BasisProjectionOperatorLocal( - self.derham._Ploc["1"], self.derham.Vh_fem["2"], fun, transposed=False + self.derham._Ploc["1"], + self.derham.Vh_fem["2"], + fun, + transposed=False, ) if self._method_to_solve in ("DirectNPInverse", "InexactNPInverse"): @@ -8603,14 +8681,18 @@ def __call__(self, dt): self._solver_UzawaNumpy.F = _Fnp if self._lifting: un, uen, phin, info, residual_norms, spectralresult = self._solver_UzawaNumpy( - u0.vector, ue0.vector, phinfeec + u0.vector, + ue0.vector, + phinfeec, ) un += u_prime.vector.toarray() uen += ue_prime.vector.toarray() else: un, uen, phin, info, residual_norms, spectralresult = self._solver_UzawaNumpy( - unfeec, uenfeec, phinfeec + unfeec, + uenfeec, + phinfeec, ) dimlist = [[shp - 2 * pi for shp, pi in zip(unfeec[i][:].shape, self.derham.p)] for i in range(3)] diff --git a/src/struphy/propagators/propagators_markers.py b/src/struphy/propagators/propagators_markers.py index 0563c9dd1..f1dbbe5f6 100644 --- a/src/struphy/propagators/propagators_markers.py +++ b/src/struphy/propagators/propagators_markers.py @@ -221,7 +221,7 @@ def allocate(self): elif self.options.algo == "implicit": kernel = Pyccelkernel(pusher_kernels.push_vxb_implicit) else: - raise ValueError(f"{self.options.algo = } not supported.") + raise ValueError(f"{self.options.algo =} not supported.") # instantiate Pusher args_kernel = ( @@ -458,7 +458,7 @@ def allocate(self): kernel = Pyccelkernel(pusher_kernels.push_pc_eta_stage_H1vec) else: raise ValueError( - f'{self.options.u_space = } not valid, choose from "Hcurl", "Hdiv" or "H1vec.', + f'{self.options.u_space =} not valid, choose from "Hcurl", "Hdiv" or "H1vec.', ) # define algorithm diff --git a/src/struphy/propagators/tests/test_gyrokinetic_poisson.py b/src/struphy/propagators/tests/test_gyrokinetic_poisson.py index ce29c8141..68ba44bcd 100644 --- a/src/struphy/propagators/tests/test_gyrokinetic_poisson.py +++ b/src/struphy/propagators/tests/test_gyrokinetic_poisson.py @@ -56,9 +56,9 @@ def test_poisson_M1perp_1d(direction, bc_type, mapping, projected_rhs, show_plot errors = [] h_vec = [] if show_plot: - plt.figure(f"degree {pi = }, {direction + 1 = }, {bc_type = }, {mapping[0] = }", figsize=(24, 16)) - plt.figure(f"degree {pi = }, {direction + 1 = }, {bc_type = }, {mapping[0] = }", figsize=(24, 16)) - plt.figure(f"degree {pi = }, {direction + 1 = }, {bc_type = }, {mapping[0] = }", figsize=(24, 16)) + plt.figure(f"degree {pi =}, {direction + 1 =}, {bc_type =}, {mapping[0] =}", figsize=(24, 16)) + plt.figure(f"degree {pi =}, {direction + 1 =}, {bc_type =}, {mapping[0] =}", figsize=(24, 16)) + plt.figure(f"degree {pi =}, {direction + 1 =}, {bc_type =}, {mapping[0] =}", figsize=(24, 16)) for n, Neli in enumerate(Nels): # boundary conditions (overwritten below) @@ -122,7 +122,7 @@ def rho1_xyz(x, y, z): print("Direction should be either 0 or 1") # create derham object - print(f"{dirichlet_bc = }") + print(f"{dirichlet_bc =}") derham = Derham(Nel, p, spl_kind, dirichlet_bc=dirichlet_bc, comm=comm) # mass matrices @@ -183,7 +183,7 @@ def rho_pulled(e1, e2, e3): analytic_value1 = sol1_xyz(x, y, z) if show_plot: - plt.figure(f"degree {pi = }, {direction + 1 = }, {bc_type = }, {mapping[0] = }") + plt.figure(f"degree {pi =}, {direction + 1 =}, {bc_type =}, {mapping[0] =}") plt.subplot(2, 3, n + 1) if direction == 0: plt.plot(x[:, 0, 0], sol_val1[:, 0, 0], "ob", label="numerical") @@ -193,24 +193,25 @@ def rho_pulled(e1, e2, e3): plt.plot(y[0, :, 0], sol_val1[0, :, 0], "ob", label="numerical") plt.plot(y[0, :, 0], analytic_value1[0, :, 0], "r--", label="exact") plt.xlabel("y") - plt.title(f"{Nel = }") + plt.title(f"{Nel =}") plt.legend() error = xp.max(xp.abs(analytic_value1 - sol_val1)) - print(f"{direction = }, {pi = }, {Neli = }, {error=}") + print(f"{direction =}, {pi =}, {Neli =}, {error=}") errors.append(error) h = 1 / (Neli) h_vec.append(h) m, _ = xp.polyfit(xp.log(Nels), xp.log(errors), deg=1) - print(f"For {pi = }, solution converges in {direction=} with rate {-m = } ") + print(f"For {pi =}, solution converges in {direction=} with rate {-m =} ") assert -m > (pi + 1 - 0.07) # Plot convergence in 1D if show_plot: plt.figure( - f"Convergence for degree {pi = }, {direction + 1 = }, {bc_type = }, {mapping[0] = }", figsize=(12, 8) + f"Convergence for degree {pi =}, {direction + 1 =}, {bc_type =}, {mapping[0] =}", + figsize=(12, 8), ) plt.plot(h_vec, errors, "o", label=f"p={p[direction]}") plt.plot( @@ -287,7 +288,7 @@ def rho2_xyz(x, y, z): spl_kind = [False, True, True] dirichlet_bc = [(not kd,) * 2 for kd in spl_kind] dirichlet_bc = tuple(dirichlet_bc) - print(f"{dirichlet_bc = }") + print(f"{dirichlet_bc =}") # manufactured solution in 2D def sol2_xyz(x, y, z): @@ -418,9 +419,9 @@ def rho2_pulled(e1, e2, e3): error1 = xp.max(xp.abs(analytic_value1 - sol_val1)) error2 = xp.max(xp.abs(analytic_value2 - sol_val2)) - print(f"{p = }, {bc_type = }, {mapping = }") - print(f"{error1 = }") - print(f"{error2 = }") + print(f"{p =}, {bc_type =}, {mapping =}") + print(f"{error1 =}") + print(f"{error2 =}") print("") if show_plot and rank == 0: @@ -503,7 +504,7 @@ def rho(e1, e2, e3): l2_proj = L2Projector("H1", mass_ops) rho_vec = l2_proj.get_dofs(rho, apply_bc=True) - print(f"{rho_vec[:].shape = }") + print(f"{rho_vec[:].shape =}") # Create 3d Poisson solver solver_params = SolverParameters( diff --git a/src/struphy/propagators/tests/test_poisson.py b/src/struphy/propagators/tests/test_poisson.py index 78567f8d1..bd425170a 100644 --- a/src/struphy/propagators/tests/test_poisson.py +++ b/src/struphy/propagators/tests/test_poisson.py @@ -73,9 +73,9 @@ def test_poisson_1d( errors = [] h_vec = [] if show_plot: - plt.figure(f"degree {pi = }, {direction + 1 = }, {bc_type = }, {mapping[0] = }", figsize=(24, 16)) - plt.figure(f"degree {pi = }, {direction + 1 = }, {bc_type = }, {mapping[0] = }", figsize=(24, 16)) - plt.figure(f"degree {pi = }, {direction + 1 = }, {bc_type = }, {mapping[0] = }", figsize=(24, 16)) + plt.figure(f"degree {pi =}, {direction + 1 =}, {bc_type =}, {mapping[0] =}", figsize=(24, 16)) + plt.figure(f"degree {pi =}, {direction + 1 =}, {bc_type =}, {mapping[0] =}", figsize=(24, 16)) + plt.figure(f"degree {pi =}, {direction + 1 =}, {bc_type =}, {mapping[0] =}", figsize=(24, 16)) for n, Neli in enumerate(Nels): # boundary conditions (overwritten below) @@ -222,7 +222,7 @@ def rho_pulled(e1, e2, e3): analytic_value1 = sol1_xyz(x, y, z) if show_plot: - plt.figure(f"degree {pi = }, {direction + 1 = }, {bc_type = }, {mapping[0] = }") + plt.figure(f"degree {pi =}, {direction + 1 =}, {bc_type =}, {mapping[0] =}") plt.subplot(2, 3, n + 1) if direction == 0: plt.plot(x[:, 0, 0], sol_val1[:, 0, 0], "ob", label="numerical") @@ -236,24 +236,25 @@ def rho_pulled(e1, e2, e3): plt.plot(z[0, 0, :], sol_val1[0, 0, :], "ob", label="numerical") plt.plot(z[0, 0, :], analytic_value1[0, 0, :], "r--", label="exact") plt.xlabel("z") - plt.title(f"{Nel = }") + plt.title(f"{Nel =}") plt.legend() error = xp.max(xp.abs(analytic_value1 - sol_val1)) - print(f"{direction = }, {pi = }, {Neli = }, {error=}") + print(f"{direction =}, {pi =}, {Neli =}, {error=}") errors.append(error) h = 1 / (Neli) h_vec.append(h) m, _ = xp.polyfit(xp.log(Nels), xp.log(errors), deg=1) - print(f"For {pi = }, solution converges in {direction=} with rate {-m = } ") + print(f"For {pi =}, solution converges in {direction=} with rate {-m =} ") assert -m > (pi + 1 - 0.07) # Plot convergence in 1D if show_plot: plt.figure( - f"Convergence for degree {pi = }, {direction + 1 = }, {bc_type = }, {mapping[0] = }", figsize=(12, 8) + f"Convergence for degree {pi =}, {direction + 1 =}, {bc_type =}, {mapping[0] =}", + figsize=(12, 8), ) plt.plot(h_vec, errors, "o", label=f"p={p[direction]}") plt.plot( @@ -485,7 +486,7 @@ def rho2_xyz(x, y, z): spl_kind = [False, True, True] dirichlet_bc = [(not kd,) * 2 for kd in spl_kind] dirichlet_bc = tuple(dirichlet_bc) - print(f"{dirichlet_bc = }") + print(f"{dirichlet_bc =}") # manufactured solution in 2D def sol2_xyz(x, y, z): @@ -628,9 +629,9 @@ def rho2_pulled(e1, e2, e3): error1 = xp.max(xp.abs(analytic_value1 - sol_val1)) error2 = xp.max(xp.abs(analytic_value2 - sol_val2)) - print(f"{p = }, {bc_type = }, {mapping = }") - print(f"{error1 = }") - print(f"{error2 = }") + print(f"{p =}, {bc_type =}, {mapping =}") + print(f"{error1 =}") + print(f"{error2 =}") print("") if show_plot and rank == 0: diff --git a/src/struphy/utils/clone_config.py b/src/struphy/utils/clone_config.py index d23bee47c..b6e14bc8d 100644 --- a/src/struphy/utils/clone_config.py +++ b/src/struphy/utils/clone_config.py @@ -230,7 +230,7 @@ def print_particle_config(self): if marker_key in self.params["kinetic"][species_name]["markers"].keys(): params_value = self.params["kinetic"][species_name]["markers"][marker_key] if params_value is not None: - assert sum_value == params_value, f"{sum_value = } and {params_value = }" + assert sum_value == params_value, f"{sum_value =} and {params_value =}" sum_row += f"| {str(sum_value):30} " # Print the final message diff --git a/src/struphy/utils/test_clone_config.py b/src/struphy/utils/test_clone_config.py index a9aa2c5c7..b1c84139b 100644 --- a/src/struphy/utils/test_clone_config.py +++ b/src/struphy/utils/test_clone_config.py @@ -24,8 +24,8 @@ def test_clone_config(Nel, Np, num_clones): species: { "markers": { "Np": Np, - } - } + }, + }, }, } @@ -37,7 +37,7 @@ def test_clone_config(Nel, Np, num_clones): # Print outputs pconf.print_clone_config() pconf.print_particle_config() - print(f"{pconf.get_Np_clone(Np) = }") + print(f"{pconf.get_Np_clone(Np) =}") if __name__ == "__main__": diff --git a/src/struphy/utils/utils.py b/src/struphy/utils/utils.py index 1707f0c07..171b61c23 100644 --- a/src/struphy/utils/utils.py +++ b/src/struphy/utils/utils.py @@ -203,6 +203,6 @@ def subp_run(cmd, cwd="libpath", check=True): for k, val in state.items(): print(k, val) i_path, o_path, b_path = get_paths(state) - print(f"{i_path = }") - print(f"{o_path = }") - print(f"{b_path = }") + print(f"{i_path =}") + print(f"{o_path =}") + print(f"{b_path =}") From 9a17cd1d7b2d16eeb2162bf4854c7d2e33f596e9 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 30 Oct 2025 17:24:00 +0100 Subject: [PATCH 05/83] Update Github workflow (#85) Update the PR templates again --- .../install-struphy-editable/action.yml | 2 +- .../install/install-struphy/action.yml | 11 ++++++++-- .github/actions/tests/models/action.yml | 21 +++++++------------ .github/actions/tests/quickstart/action.yml | 2 +- .github/actions/tests/unit/action.yml | 12 ----------- .github/workflows/static_analysis.yml | 2 +- .github/workflows/testing.yml | 12 ++++------- 7 files changed, 23 insertions(+), 39 deletions(-) diff --git a/.github/actions/install/install-struphy-editable/action.yml b/.github/actions/install/install-struphy-editable/action.yml index f395d5caa..49d871e15 100644 --- a/.github/actions/install/install-struphy-editable/action.yml +++ b/.github/actions/install/install-struphy-editable/action.yml @@ -9,6 +9,6 @@ runs: run: | pip install --upgrade pip pip uninstall -y gvec - pip install -e ".[dev,mpi,doc]" + pip install -e ".[dev,mpi]" pip list struphy -h diff --git a/.github/actions/install/install-struphy/action.yml b/.github/actions/install/install-struphy/action.yml index a9589c4c4..bce677589 100644 --- a/.github/actions/install/install-struphy/action.yml +++ b/.github/actions/install/install-struphy/action.yml @@ -7,8 +7,15 @@ runs: - name: Install struphy shell: bash run: | + echo $FC + echo $CC + echo $CXX + echo "----------------" + which gfortran + which gcc + which g++ pip install --upgrade pip - pip install ".[phys,mpi,doc]" + pip uninstall -y gvec + pip install ".[phys,mpi]" pip list struphy -h - struphy --refresh-models diff --git a/.github/actions/tests/models/action.yml b/.github/actions/tests/models/action.yml index 435276caa..0a9fafc16 100644 --- a/.github/actions/tests/models/action.yml +++ b/.github/actions/tests/models/action.yml @@ -3,21 +3,14 @@ name: "Run model tests" runs: using: composite steps: - - name: Model tests + - name: Install dependencies shell: bash run: | struphy compile --status - struphy test LinearMHD - struphy test toy - struphy test models - struphy test verification - - name: Model tests with MPI - shell: bash - run: | struphy compile --status - struphy test models - struphy test models --mpi 2 - struphy test verification --mpi 1 - struphy test verification --mpi 4 - struphy test verification --mpi 4 --nclones 2 - struphy test VlasovAmpereOneSpecies --mpi 2 --nclones 2 + struphy test models --fast + struphy test models --fast --mpi 2 + struphy test models --fast --verification --mpi 1 + struphy test models --fast --verification --mpi 4 + struphy test models --fast --verification --mpi 4 --nclones 2 + struphy test DriftKineticElectrostaticAdiabatic --mpi 2 --nclones 2 \ No newline at end of file diff --git a/.github/actions/tests/quickstart/action.yml b/.github/actions/tests/quickstart/action.yml index 3845360d3..b78ade5e7 100644 --- a/.github/actions/tests/quickstart/action.yml +++ b/.github/actions/tests/quickstart/action.yml @@ -8,7 +8,7 @@ runs: run: | struphy -p struphy -h - struphy params VlasovAmpereOneSpecies + struphy params VlasovAmpereOneSpecies -y ls -1a mv params_VlasovAmpereOneSpecies.py test.py python3 test.py diff --git a/.github/actions/tests/unit/action.yml b/.github/actions/tests/unit/action.yml index 385d6f419..2d0e70a2d 100644 --- a/.github/actions/tests/unit/action.yml +++ b/.github/actions/tests/unit/action.yml @@ -3,19 +3,7 @@ name: "Run unit tests" runs: using: composite steps: - - name: Run unit tests with MPI - shell: bash - run: | - struphy compile --status - struphy --refresh-models - struphy test unit --mpi 2 - - name: Run unit tests shell: bash run: | - struphy compile --status - struphy --refresh-models - pip show mpi4py - pip uninstall -y mpi4py - pip list struphy test unit diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 7fff5f1c3..5dc140173 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -86,7 +86,7 @@ jobs: run: | pip install isort isort --check src/ - isort --check tutorials/ + isort --check doc/tutorials/ # mypy: # runs-on: ubuntu-latest diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 654fb0fc2..3404f6348 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -22,7 +22,7 @@ jobs: python-version: ["3.12"] os: ["ubuntu-latest"] #, "macos-latest"] compile-language: ["fortran", "c"] - test-type: ["unit", "model", "quickstart", "tutorials"] + test-type: ["unit", "model"] #, "quickstart"] steps: # Checkout the repository @@ -90,10 +90,6 @@ jobs: if: matrix.test-type == 'model' uses: ./.github/actions/tests/models - - name: Run quickstart tests - if: matrix.test-type == 'quickstart' - uses: ./.github/actions/tests/quickstart - - - name: Run tutorials - if: matrix.test-type == 'tutorials' - uses: ./.github/actions/tests/tutorials + #- name: Run quickstart tests + # if: matrix.test-type == 'quickstart' + # uses: ./.github/actions/tests/quickstart From a2df495dce6a9315433efffb209471706a8271d3 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 30 Oct 2025 17:26:17 +0100 Subject: [PATCH 06/83] Add badges to readme (#81) Redo of https://github.com/struphy-hub/struphy/pull/47 Added badges to README and refactored the Github actions a bit so that we use a [reusable workflow](https://github.com/orgs/community/discussions/52616) for each OS. --- .github/workflows/docs.yml | 2 +- .github/workflows/macos-latest.yml | 20 ++++++++++++++++++++ .github/workflows/static_analysis.yml | 5 ++++- .github/workflows/testing.yml | 22 +++++++++------------- .github/workflows/ubuntu-latest.yml | 20 ++++++++++++++++++++ README.md | 13 ++++++++++++- 6 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/macos-latest.yml create mode 100644 .github/workflows/ubuntu-latest.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 79caa16e7..b4827d372 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,7 +2,7 @@ name: Deploy docs to GitHub Pages on: push: - branches: ["devel", "main"] # TODO: Set to main only after release + branches: ["devel", "main"] workflow_dispatch: defaults: diff --git a/.github/workflows/macos-latest.yml b/.github/workflows/macos-latest.yml new file mode 100644 index 000000000..2b5a1b0e5 --- /dev/null +++ b/.github/workflows/macos-latest.yml @@ -0,0 +1,20 @@ +# name: MacOS +# on: +# push: +# branches: +# - main +# - devel +# pull_request: +# branches: +# - main +# - devel + +# # concurrency: +# # group: ${{ github.ref }} +# # cancel-in-progress: true + +# jobs: +# macos-latest-build: +# uses: ./.github/workflows/testing.yml +# with: +# os: macos-latest \ No newline at end of file diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 5dc140173..85b7857ec 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -1,4 +1,4 @@ -name: Static analysis +name: isort & ruff on: push: @@ -10,6 +10,9 @@ on: - main - devel +# concurrency: +# group: ${{ github.ref }} +# cancel-in-progress: true defaults: run: shell: bash diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 3404f6348..6cfd047fa 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -1,18 +1,15 @@ name: Testing on: - push: - branches: - - main - - devel - pull_request: - branches: - - main - - devel + workflow_call: + inputs: + os: + required: true + type: string jobs: test: - runs-on: ${{ matrix.os }} + runs-on: ${{ inputs.os }} env: OMPI_MCA_rmaps_base_oversubscribe: 1 # Linux PRRTE_MCA_rmaps_base_oversubscribe: 1 # MacOS @@ -20,7 +17,6 @@ jobs: fail-fast: false matrix: python-version: ["3.12"] - os: ["ubuntu-latest"] #, "macos-latest"] compile-language: ["fortran", "c"] test-type: ["unit", "model"] #, "quickstart"] @@ -50,13 +46,13 @@ jobs: # Install prereqs # I don't think it's possible to use a single action for this because - # we can't use ${matrix.os} in an if statement, so we have to use two different actions. + # we can't use ${inputs.os} in an if statement, so we have to use two different actions. - name: Install prerequisites (Ubuntu) - if: matrix.os == 'ubuntu-latest' + if: inputs.os == 'ubuntu-latest' uses: ./.github/actions/install/ubuntu-latest - name: Install prerequisites (macOS) - if: matrix.os == 'macos-latest' + if: inputs.os == 'macos-latest' uses: ./.github/actions/install/macos-latest # Check that mpirun oversubscribing works, doesn't work unless OMPI_MCA_rmaps_base_oversubscribe==1 diff --git a/.github/workflows/ubuntu-latest.yml b/.github/workflows/ubuntu-latest.yml new file mode 100644 index 000000000..e66aeb3b3 --- /dev/null +++ b/.github/workflows/ubuntu-latest.yml @@ -0,0 +1,20 @@ +name: Ubuntu +on: + push: + branches: + - main + - devel + pull_request: + branches: + - main + - devel + +# concurrency: +# group: ${{ github.ref }} +# cancel-in-progress: true + +jobs: + ubuntu-latest-build: + uses: ./.github/workflows/testing.yml + with: + os: ubuntu-latest \ No newline at end of file diff --git a/README.md b/README.md index 1730f5e00..62263ed54 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,15 @@ -![Struphy header](https://raw.githubusercontent.com/struphy-hub/.github/refs/heads/main/profile/struphy_header_with_subs.png) +

+ +


+ +[![Ubuntu latest](https://github.com/struphy-hub/struphy/actions/workflows/ubuntu-latest.yml/badge.svg)](https://github.com/struphy-hub/struphy/actions/workflows/ubuntu-latest.yml) +[![MacOS latest](https://github.com/struphy-hub/struphy/actions/workflows/macos-latest.yml/badge.svg)](https://github.com/struphy-hub/struphy/actions/workflows/macos-latest.yml) +[![isort and ruff](https://github.com/struphy-hub/struphy/actions/workflows/static_analysis.yml/badge.svg)](https://github.com/struphy-hub/struphy/actions/workflows/static_analysis.yml) +[![PyPI](https://img.shields.io/pypi/v/struphy?label=PyPI)](https://pypi.org/project/struphy/) +[![PyPI Downloads](https://img.shields.io/pypi/dm/struphy.svg?label=PyPI%20downloads)]( +https://pypi.org/project/struphy/) +[![Release](https://img.shields.io/github/v/release/struphy-hub/struphy?label=Release)](https://github.com/struphy-hub/struphy/releases) +[![License](https://img.shields.io/badge/License-MIT-violet)](https://github.com/struphy-hub/struphy/blob/devel/LICENSE) # Welcome! From fe50756137c663fa0f7109502b824df9cff3bdef Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 30 Oct 2025 17:31:45 +0100 Subject: [PATCH 07/83] Updated psydac dependency (#80) Redo of https://github.com/struphy-hub/struphy/pull/49 **Solves the following issue(s):** Closes #30 Updated psydac dependency to https://github.com/struphy-hub/psydac-for-struphy.git --------- Co-authored-by: Stefan Possanner Co-authored-by: Byung Kyu Na Co-authored-by: Stefan Possanner <86720346+spossann@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c4a2c9d27..82c2e2605 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dependencies = [ 'numpy', 'cunumpy', 'pyccel>=2.0', - 'psydac @ git+https://github.com/max-models/psydac-for-struphy.git@devel-tiny', + 'psydac @ git+https://github.com/struphy-hub/psydac-for-struphy.git@devel-tiny', 'scipy', 'h5py', 'matplotlib', From 107f6672744a8846a2d3e7fa9a5c3cfc0a53281c Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 30 Oct 2025 17:34:20 +0100 Subject: [PATCH 08/83] Fix ruff error E713: `not X in Y` --> `X not in Y` (#79) Redo of https://github.com/struphy-hub/struphy/pull/61 **Solves the following issue(s):** Closes #58 I just ran the following command ``` ruff check --select E713 --fix ``` https://docs.astral.sh/ruff/rules/not-in-test/ --------- Co-authored-by: Stefan Possanner Co-authored-by: Byung Kyu Na Co-authored-by: Stefan Possanner <86720346+spossann@users.noreply.github.com> --- src/struphy/models/base.py | 4 +-- src/struphy/models/hybrid.py | 48 ++++++++++++++++++------------------ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index e6eb5b665..fb77bcb65 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -428,13 +428,13 @@ def getFromDict(dataDict, mapList): def setInDict(dataDict, mapList, value): # Loop over dicitionary and creaty empty dicts where the path does not exist for k in range(len(mapList)): - if not mapList[k] in getFromDict(dataDict, mapList[:k]).keys(): + if mapList[k] not in getFromDict(dataDict, mapList[:k]).keys(): getFromDict(dataDict, mapList[:k])[mapList[k]] = {} getFromDict(dataDict, mapList[:-1])[mapList[-1]] = value # make sure that the base keys are top-level keys for base_key in ["em_fields", "fluid", "kinetic"]: - if not base_key in dct.keys(): + if base_key not in dct.keys(): dct[base_key] = {} if isinstance(species, str): diff --git a/src/struphy/models/hybrid.py b/src/struphy/models/hybrid.py index bcc9f6492..c1952f59c 100644 --- a/src/struphy/models/hybrid.py +++ b/src/struphy/models/hybrid.py @@ -319,15 +319,15 @@ def __init__(self): class Propagators: def __init__(self, turn_off: tuple[str, ...] = (None,)): - if not "PushEtaPC" in turn_off: + if "PushEtaPC" not in turn_off: self.push_eta_pc = propagators_markers.PushEtaPC() - if not "PushVxB" in turn_off: + if "PushVxB" not in turn_off: self.push_vxb = propagators_markers.PushVxB() - if not "PressureCoupling6D" in turn_off: + if "PressureCoupling6D" not in turn_off: self.pc6d = propagators_coupling.PressureCoupling6D() - if not "ShearAlfven" in turn_off: + if "ShearAlfven" not in turn_off: self.shearalfven = propagators_fields.ShearAlfven() - if not "Magnetosonic" in turn_off: + if "Magnetosonic" not in turn_off: self.magnetosonic = propagators_fields.Magnetosonic() def __init__(self, turn_off: tuple[str, ...] = (None,)): @@ -343,19 +343,19 @@ def __init__(self, turn_off: tuple[str, ...] = (None,)): self.propagators = self.Propagators(turn_off) # 3. assign variables to propagators - if not "ShearAlfven" in turn_off: + if "ShearAlfven" not in turn_off: self.propagators.shearalfven.variables.u = self.mhd.velocity self.propagators.shearalfven.variables.b = self.em_fields.b_field - if not "Magnetosonic" in turn_off: + if "Magnetosonic" not in turn_off: self.propagators.magnetosonic.variables.n = self.mhd.density self.propagators.magnetosonic.variables.u = self.mhd.velocity self.propagators.magnetosonic.variables.p = self.mhd.pressure - if not "PressureCoupling6D" in turn_off: + if "PressureCoupling6D" not in turn_off: self.propagators.pc6d.variables.u = self.mhd.velocity self.propagators.pc6d.variables.energetic_ions = self.energetic_ions.var - if not "PushEtaPC" in turn_off: + if "PushEtaPC" not in turn_off: self.propagators.push_eta_pc.variables.var = self.energetic_ions.var - if not "PushVxB" in turn_off: + if "PushVxB" not in turn_off: self.propagators.push_vxb.variables.ions = self.energetic_ions.var # define scalars for update_scalar_quantities @@ -584,19 +584,19 @@ def __init__(self): class Propagators: def __init__(self, turn_off: tuple[str, ...] = (None,)): - if not "PushGuidingCenterBxEstar" in turn_off: + if "PushGuidingCenterBxEstar" not in turn_off: self.push_bxe = propagators_markers.PushGuidingCenterBxEstar() - if not "PushGuidingCenterParallel" in turn_off: + if "PushGuidingCenterParallel" not in turn_off: self.push_parallel = propagators_markers.PushGuidingCenterParallel() - if not "ShearAlfvenCurrentCoupling5D" in turn_off: + if "ShearAlfvenCurrentCoupling5D" not in turn_off: self.shearalfen_cc5d = propagators_fields.ShearAlfvenCurrentCoupling5D() - if not "Magnetosonic" in turn_off: + if "Magnetosonic" not in turn_off: self.magnetosonic = propagators_fields.Magnetosonic() - if not "CurrentCoupling5DDensity" in turn_off: + if "CurrentCoupling5DDensity" not in turn_off: self.cc5d_density = propagators_fields.CurrentCoupling5DDensity() - if not "CurrentCoupling5DGradB" in turn_off: + if "CurrentCoupling5DGradB" not in turn_off: self.cc5d_gradb = propagators_coupling.CurrentCoupling5DGradB() - if not "CurrentCoupling5DCurlb" in turn_off: + if "CurrentCoupling5DCurlb" not in turn_off: self.cc5d_curlb = propagators_coupling.CurrentCoupling5DCurlb() def __init__(self, turn_off: tuple[str, ...] = (None,)): @@ -612,24 +612,24 @@ def __init__(self, turn_off: tuple[str, ...] = (None,)): self.propagators = self.Propagators(turn_off) # 3. assign variables to propagators - if not "ShearAlfvenCurrentCoupling5D" in turn_off: + if "ShearAlfvenCurrentCoupling5D" not in turn_off: self.propagators.shearalfen_cc5d.variables.u = self.mhd.velocity self.propagators.shearalfen_cc5d.variables.b = self.em_fields.b_field - if not "Magnetosonic" in turn_off: + if "Magnetosonic" not in turn_off: self.propagators.magnetosonic.variables.n = self.mhd.density self.propagators.magnetosonic.variables.u = self.mhd.velocity self.propagators.magnetosonic.variables.p = self.mhd.pressure - if not "CurrentCoupling5DDensity" in turn_off: + if "CurrentCoupling5DDensity" not in turn_off: self.propagators.cc5d_density.variables.u = self.mhd.velocity - if not "CurrentCoupling5DGradB" in turn_off: + if "CurrentCoupling5DGradB" not in turn_off: self.propagators.cc5d_gradb.variables.u = self.mhd.velocity self.propagators.cc5d_gradb.variables.energetic_ions = self.energetic_ions.var - if not "CurrentCoupling5DCurlb" in turn_off: + if "CurrentCoupling5DCurlb" not in turn_off: self.propagators.cc5d_curlb.variables.u = self.mhd.velocity self.propagators.cc5d_curlb.variables.energetic_ions = self.energetic_ions.var - if not "PushGuidingCenterBxEstar" in turn_off: + if "PushGuidingCenterBxEstar" not in turn_off: self.propagators.push_bxe.variables.ions = self.energetic_ions.var - if not "PushGuidingCenterParallel" in turn_off: + if "PushGuidingCenterParallel" not in turn_off: self.propagators.push_parallel.variables.ions = self.energetic_ions.var # define scalars for update_scalar_quantities From 8fad62521c2ca2016d8daf372656fde020360c62 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 30 Oct 2025 17:35:48 +0100 Subject: [PATCH 09/83] Remove `==` for true-false checks of booleans (#78) Redo of https://github.com/struphy-hub/struphy/pull/62 **Solves the following issue(s):** Closes #57 I ran the following command: ``` ruff check --select E712 --unsafe-fixes --fix ``` Note the `--unsafe-fixes`, so please check the changes. --------- Co-authored-by: Stefan Possanner Co-authored-by: Byung Kyu Na Co-authored-by: Stefan Possanner <86720346+spossann@users.noreply.github.com> --- src/struphy/bsplines/bsplines.py | 4 +- .../legacy/massless_operators/fB_arrays.py | 4 +- .../pro_local/mhd_operators_3d_local.py | 6 +-- .../pro_local/projectors_local.py | 12 ++--- .../shape_function_projectors_L2.py | 8 ++-- .../shape_function_projectors_local.py | 12 ++--- .../eigenvalue_solvers/projectors_global.py | 8 ++-- src/struphy/feec/basis_projection_ops.py | 2 +- src/struphy/feec/linear_operators.py | 8 ++-- src/struphy/feec/local_projectors_kernels.py | 46 +++++++++--------- src/struphy/feec/mass.py | 10 ++-- src/struphy/feec/projectors.py | 16 +++---- src/struphy/feec/psydac_derham.py | 6 +-- src/struphy/feec/variational_utilities.py | 2 +- src/struphy/linear_algebra/saddle_point.py | 14 +++--- .../tests/test_saddlepoint_massmatrices.py | 6 +-- src/struphy/models/hybrid.py | 48 +++++++++---------- src/struphy/polar/basic.py | 2 +- src/struphy/propagators/propagators_fields.py | 6 +-- 19 files changed, 110 insertions(+), 110 deletions(-) diff --git a/src/struphy/bsplines/bsplines.py b/src/struphy/bsplines/bsplines.py index a04ee4851..9974a9ff2 100644 --- a/src/struphy/bsplines/bsplines.py +++ b/src/struphy/bsplines/bsplines.py @@ -164,7 +164,7 @@ def basis_funs(knots, degree, x, span, normalize=False): saved = left[j - r] * temp values[j + 1] = saved - if normalize == True: + if normalize: values = values * scaling_vector(knots, degree, span) return values @@ -735,7 +735,7 @@ def basis_ders_on_quad_grid(knots, degree, quad_grid, nders, normalize=False): span = find_span(knots, degree, xq) ders = basis_funs_all_ders(knots, degree, xq, span, nders) - if normalize == True: + if normalize: ders = ders * scaling_vector(knots, degree, span) basis[ie, :, :, iq] = ders.transpose() diff --git a/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_arrays.py b/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_arrays.py index e74302878..65faf9209 100644 --- a/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_arrays.py +++ b/src/struphy/eigenvalue_solvers/legacy/massless_operators/fB_arrays.py @@ -225,7 +225,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): dtype=float, ) # when using delta f method, the values of current equilibrium at all quadrature points - if control == True: + if control: self.Jeqx = xp.empty( ( self.Nel[0], @@ -761,7 +761,7 @@ def __init__(self, TENSOR_SPACE_FEM, DOMAIN, control, mpi_comm): self.df_det[ie1, ie2, ie3, q1, q2, q3] = det_number - if control == True: + if control: x1 = mapping3d.f( TENSOR_SPACE_FEM.pts[0][ie1, q1], TENSOR_SPACE_FEM.pts[1][ie2, q2], diff --git a/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/mhd_operators_3d_local.py b/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/mhd_operators_3d_local.py index 49464aa58..6734a11b0 100644 --- a/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/mhd_operators_3d_local.py +++ b/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/mhd_operators_3d_local.py @@ -52,7 +52,7 @@ def __init__(self, tensor_space, n_quad): self.coeff_h = [0, 0, 0] for a in range(3): - if self.bc[a] == True: + if self.bc[a]: self.coeff_i[a] = xp.zeros((1, 2 * self.p[a] - 1), dtype=float) self.coeff_h[a] = xp.zeros((1, 2 * self.p[a]), dtype=float) @@ -186,7 +186,7 @@ def __init__(self, tensor_space, n_quad): self.int_shift_N = [0, 0, 0] for a in range(3): - if self.bc[a] == False: + if not self.bc[a]: # maximum number of non-vanishing coefficients if self.p[a] == 1: self.n_int_nvcof_D[a] = 2 @@ -405,7 +405,7 @@ def __init__(self, tensor_space, n_quad): self.his_shift_N = [0, 0, 0] for a in range(3): - if self.bc[a] == False: + if not self.bc[a]: # maximum number of non-vanishing coefficients self.n_his_nvcof_D[a] = 3 * self.p[a] - 2 self.n_his_nvcof_N[a] = 3 * self.p[a] - 1 diff --git a/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/projectors_local.py b/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/projectors_local.py index 3b27b1b5f..9ede3f608 100644 --- a/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/projectors_local.py +++ b/src/struphy/eigenvalue_solvers/legacy/projectors_local/pro_local/projectors_local.py @@ -45,7 +45,7 @@ def __init__(self, spline_space, n_quad): self.wts_loc = xp.polynomial.legendre.leggauss(self.n_quad)[1] # set interpolation and histopolation coefficients - if self.bc == True: + if self.bc: self.coeff_i = xp.zeros((1, 2 * self.p - 1), dtype=float) self.coeff_h = xp.zeros((1, 2 * self.p), dtype=float) @@ -152,7 +152,7 @@ def __init__(self, spline_space, n_quad): self.coeffi_indices = xp.zeros(n_lambda_int, dtype=int) - if self.bc == False: + if not self.bc: # maximum number of non-vanishing coefficients if self.p == 1: self.n_int_nvcof_D = 2 @@ -318,7 +318,7 @@ def __init__(self, spline_space, n_quad): self.coeffh_indices = xp.zeros(n_lambda_his, dtype=int) - if self.bc == False: + if not self.bc: # maximum number of non-vanishing coefficients self.n_his_nvcof_D = 3 * self.p - 2 self.n_his_nvcof_N = 3 * self.p - 1 @@ -629,7 +629,7 @@ def __init__(self, tensor_space, n_quad): self.coeff_h = [0, 0, 0] for a in range(3): - if self.bc[a] == True: + if self.bc[a]: self.coeff_i[a] = xp.zeros((1, 2 * self.p[a] - 1), dtype=float) self.coeff_h[a] = xp.zeros((1, 2 * self.p[a]), dtype=float) @@ -763,7 +763,7 @@ def __init__(self, tensor_space, n_quad): self.int_shift_N = [0, 0, 0] for a in range(3): - if self.bc[a] == False: + if not self.bc[a]: # maximum number of non-vanishing coefficients if self.p[a] == 1: self.n_int_nvcof_D[a] = 2 @@ -979,7 +979,7 @@ def __init__(self, tensor_space, n_quad): self.his_shift_N = [0, 0, 0] for a in range(3): - if self.bc[a] == False: + if not self.bc[a]: # maximum number of non-vanishing coefficients self.n_his_nvcof_D[a] = 3 * self.p[a] - 2 self.n_his_nvcof_N[a] = 3 * self.p[a] - 1 diff --git a/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_L2.py b/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_L2.py index 8978e2464..137df7f09 100644 --- a/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_L2.py +++ b/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_L2.py @@ -590,7 +590,7 @@ def potential_pi_0(self, particles_loc, Np, domain, mpi_comm): ------- kernel_0 matrix """ - if self.bc[0] == True and self.bc[1] == True and self.bc[2] == True: + if self.bc[0] and self.bc[1] and self.bc[2]: ker_loc.potential_kernel_0_form( Np, self.p, @@ -637,7 +637,7 @@ def S_pi_0(self, particles_loc, Np, domain): kernel_0 matrix """ self.kernel_0[:, :, :, :, :, :] = 0.0 - if self.bc[0] == True and self.bc[1] == True and self.bc[2] == True: + if self.bc[0] and self.bc[1] and self.bc[2]: ker_loc.kernel_0_form( Np, self.p, @@ -699,7 +699,7 @@ def S_pi_1(self, particles_loc, Np, domain): self.right_loc_2[:, :, :] = 0.0 self.right_loc_3[:, :, :] = 0.0 - if self.bc[0] == True and self.bc[1] == True and self.bc[2] == True: + if self.bc[0] and self.bc[1] and self.bc[2]: ker_loc.kernel_1_form( self.indN[0], self.indN[1], @@ -764,7 +764,7 @@ def S_pi_1(self, particles_loc, Np, domain): print("non-periodic case not implemented!!!") def vv_S1(self, particles_loc, Np, domain, index_label, accvv, dt, mpi_comm): - if self.bc[0] == True and self.bc[1] == True and self.bc[2] == True: + if self.bc[0] and self.bc[1] and self.bc[2]: if index_label == 1: ker_loc.vv_1_form( self.wts[0][0], diff --git a/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_local.py b/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_local.py index 43c8c8ff9..2ebb497a3 100644 --- a/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_local.py +++ b/src/struphy/eigenvalue_solvers/legacy/projectors_local/shape_pro_local/shape_function_projectors_local.py @@ -304,7 +304,7 @@ def __init__(self, tensor_space, n_quad, p_shape, p_size, NbaseN, NbaseD, mpi_co self.coeff_i = [0, 0, 0] self.coeff_h = [0, 0, 0] for a in range(3): - if self.bc[a] == True: + if self.bc[a]: self.coeff_i[a] = xp.zeros(2 * self.p[a], dtype=float) self.coeff_h[a] = xp.zeros(2 * self.p[a], dtype=float) @@ -686,7 +686,7 @@ def potential_pi_0(self, particles_loc, Np, domain, mpi_comm): ------- kernel_0 matrix """ - if self.bc[0] == True and self.bc[1] == True and self.bc[2] == True: + if self.bc[0] and self.bc[1] and self.bc[2]: ker_loc.potential_kernel_0_form( Np, self.p, @@ -733,7 +733,7 @@ def S_pi_0(self, particles_loc, Np, domain): kernel_0 matrix """ self.kernel_0[:, :, :, :, :, :] = 0.0 - if self.bc[0] == True and self.bc[1] == True and self.bc[2] == True: + if self.bc[0] and self.bc[1] and self.bc[2]: ker_loc.kernel_0_form( Np, self.p, @@ -795,7 +795,7 @@ def S_pi_1(self, particles_loc, Np, domain): self.right_loc_2[:, :, :] = 0.0 self.right_loc_3[:, :, :] = 0.0 - if self.bc[0] == True and self.bc[1] == True and self.bc[2] == True: + if self.bc[0] and self.bc[1] and self.bc[2]: ker_loc.kernel_1_form( self.right_loc_1, self.right_loc_2, @@ -882,7 +882,7 @@ def S_pi_01(self, particles_loc, Np, domain): self.right_loc_2[:, :, :] = 0.0 self.right_loc_3[:, :, :] = 0.0 - if self.bc[0] == True and self.bc[1] == True and self.bc[2] == True: + if self.bc[0] and self.bc[1] and self.bc[2]: ker_loc.kernel_01_form( self.right_loc_1, self.right_loc_2, @@ -933,7 +933,7 @@ def S_pi_01(self, particles_loc, Np, domain): print("non-periodic case not implemented!!!") def vv_S1(self, particles_loc, Np, domain, index_label, accvv, dt, mpi_comm): - if self.bc[0] == True and self.bc[1] == True and self.bc[2] == True: + if self.bc[0] and self.bc[1] and self.bc[2]: if index_label == 1: ker_loc.vv_1_form( self.wts[0][0], diff --git a/src/struphy/eigenvalue_solvers/projectors_global.py b/src/struphy/eigenvalue_solvers/projectors_global.py index ca67c66e6..9d246cdac 100644 --- a/src/struphy/eigenvalue_solvers/projectors_global.py +++ b/src/struphy/eigenvalue_solvers/projectors_global.py @@ -169,7 +169,7 @@ def __init__(self, spline_space, n_quad=6): for i in range(spline_space.NbaseD): for br in spline_space.el_b: # left and right integration boundaries - if spline_space.spl_kind == False: + if not spline_space.spl_kind: xl = self.x_int[i] xr = self.x_int[i + 1] else: @@ -186,7 +186,7 @@ def __init__(self, spline_space, n_quad=6): self.x_his = xp.append(self.x_his, xr) break - if spline_space.spl_kind == True and spline_space.p % 2 == 0: + if spline_space.spl_kind and spline_space.p % 2 == 0: self.x_his = xp.append(self.x_his, spline_space.el_b[-1] + self.x_his[0]) # cumulative number of sub-intervals for conversion local interval --> global interval @@ -198,7 +198,7 @@ def __init__(self, spline_space, n_quad=6): # quadrature points and weights, ignoring subs (less accurate integration for even degree) self.x_hisG = self.x_int - if spline_space.spl_kind == True: + if spline_space.spl_kind: if spline_space.p % 2 == 0: self.x_hisG = xp.append(self.x_hisG, spline_space.el_b[-1] + self.x_hisG[0]) else: @@ -2153,7 +2153,7 @@ def pi_3(self, fun, include_bc=True, eval_kind="meshgrid", with_subs=True): # ======================================== def assemble_approx_inv(self, tol): - if self.approx_Ik_0_inv == False or (self.approx_Ik_0_inv == True and self.approx_Ik_0_tol != tol): + if not self.approx_Ik_0_inv or (self.approx_Ik_0_inv and self.approx_Ik_0_tol != tol): # poloidal plane I0_pol_0_inv_approx = xp.linalg.inv(self.I0_pol_0.toarray()) I1_pol_0_inv_approx = xp.linalg.inv(self.I1_pol_0.toarray()) diff --git a/src/struphy/feec/basis_projection_ops.py b/src/struphy/feec/basis_projection_ops.py index d76dc7ca9..ab0925828 100644 --- a/src/struphy/feec/basis_projection_ops.py +++ b/src/struphy/feec/basis_projection_ops.py @@ -2385,7 +2385,7 @@ def find_relative_col(col, row, Nbasis, periodic): The relative column position of col with respect to the the current row of the StencilMatrix. """ - if periodic == False: + if not periodic: relativecol = col - row # In the periodic case we must account for the possible looping of the basis functions when computing the relative row postion else: diff --git a/src/struphy/feec/linear_operators.py b/src/struphy/feec/linear_operators.py index 7469d68e9..28b4a0805 100644 --- a/src/struphy/feec/linear_operators.py +++ b/src/struphy/feec/linear_operators.py @@ -63,7 +63,7 @@ def toarray_struphy(self, out=None, is_sparse=False, format="csr"): rank = comm.Get_rank() size = comm.Get_size() - if is_sparse == False: + if not is_sparse: if out is None: # We declare the matrix form of our linear operator out = xp.zeros([self.codomain.dimension, self.domain.dimension], dtype=self.dtype) @@ -149,7 +149,7 @@ def toarray_struphy(self, out=None, is_sparse=False, format="csr"): # Compute to which column this iteration belongs col = spoint col += xp.ravel_multi_index(i, npts[h]) - if is_sparse == False: + if not is_sparse: result[:, col] = tmp2.toarray() else: aux = tmp2.toarray() @@ -220,7 +220,7 @@ def toarray_struphy(self, out=None, is_sparse=False, format="csr"): self.dot(v, out=tmp2) # Compute to which column this iteration belongs col = xp.ravel_multi_index(i, npts) - if is_sparse == False: + if not is_sparse: result[:, col] = tmp2.toarray() else: aux = tmp2.toarray() @@ -237,7 +237,7 @@ def toarray_struphy(self, out=None, is_sparse=False, format="csr"): # I cannot conceive any situation where this error should be thrown, but I put it here just in case something unexpected happens. raise Exception("Function toarray_struphy() only supports Stencil Vectors or Block Vectors.") - if is_sparse == False: + if not is_sparse: # Use Allreduce to perform addition reduction and give one copy of the result to all ranks. if comm is None or isinstance(comm, MockComm): out[:] = result diff --git a/src/struphy/feec/local_projectors_kernels.py b/src/struphy/feec/local_projectors_kernels.py index f1eb285c9..706b3f78e 100644 --- a/src/struphy/feec/local_projectors_kernels.py +++ b/src/struphy/feec/local_projectors_kernels.py @@ -63,7 +63,7 @@ def get_local_problem_size(periodic: "bool[:]", p: "int[:]", IoH: "bool[:]"): for h in range(3): # Interpolation - if IoH[h] == False: + if not IoH[h]: lenj[h] = 2 * p[h] - 1 # Histopolation else: @@ -734,7 +734,7 @@ def solve_local_main_loop_weighted( if counteri0 >= rows0[i00] and counteri0 <= rowe0[i00]: compute0 = True break - if compute0 == True: + if compute0: counteri1 = 0 for i1 in range(args_solve.starts[1], args_solve.ends[1] + 1): # This bool variable tell us if this row has a non-zero FE coefficient, based on the current basis function we are using on our projection @@ -744,7 +744,7 @@ def solve_local_main_loop_weighted( if counteri1 >= rows1[i11] and counteri1 <= rowe1[i11]: compute1 = True break - if compute1 == True: + if compute1: counteri2 = 0 for i2 in range(args_solve.starts[2], args_solve.ends[2] + 1): # This bool variable tell us if this row has a non-zero FE coefficient, based on the current basis function we are using on our projection @@ -754,7 +754,7 @@ def solve_local_main_loop_weighted( if counteri2 >= rows2[i22] and counteri2 <= rowe2[i22]: compute2 = True break - if compute2 == True: + if compute2: L123 = 0.0 startj1, endj1 = select_quasi_points( i0, @@ -850,7 +850,7 @@ def find_relative_col(col: int, row: int, Nbasis: int, periodic: bool): The relative column position of col with respect to the the current row of the StencilMatrix. """ - if periodic == False: + if not periodic: relativecol = col - row # In the periodic case we must account for the possible looping of the basis functions when computing the relative row postion else: @@ -944,7 +944,7 @@ def assemble_basis_projection_operator_local( compute0 = True break relativecol0 = find_relative_col(col[0], row0, VNbasis[0], periodic[0]) - if relativecol0 >= -p[0] and relativecol0 <= p[0] and compute0 == True: + if relativecol0 >= -p[0] and relativecol0 <= p[0] and compute0: count1 = 0 for row1 in range(starts[1], ends[1] + 1): # This bool variable tell us if this row has a non-zero FE coefficient, based on the current basis function we are using on our projection @@ -955,7 +955,7 @@ def assemble_basis_projection_operator_local( compute1 = True break relativecol1 = find_relative_col(col[1], row1, VNbasis[1], periodic[1]) - if relativecol1 >= -p[1] and relativecol1 <= p[1] and compute1 == True: + if relativecol1 >= -p[1] and relativecol1 <= p[1] and compute1: count2 = 0 for row2 in range(starts[2], ends[2] + 1): # This bool variable tell us if this row has a non-zero FE coefficient, based on the current basis function we are using on our projection @@ -966,7 +966,7 @@ def assemble_basis_projection_operator_local( compute2 = True break relativecol2 = find_relative_col(col[2], row2, VNbasis[2], periodic[2]) - if relativecol2 >= -p[2] and relativecol2 <= p[2] and compute2 == True: + if relativecol2 >= -p[2] and relativecol2 <= p[2] and compute2: mat[ count0 + pds[0], count1 + pds[1], @@ -1002,7 +1002,7 @@ def are_quadrature_points_zero(aux: "int[:]", p: int, basis: "float[:]"): if basis[in_start + ii] != 0.0: all_zero = False break - if all_zero == True: + if all_zero: aux[i] = 0 @@ -1085,33 +1085,33 @@ def get_rows( Array where we put a one if the current row could have a non-zero FE coefficient for the column given by col. """ # Periodic boundary conditions - if periodic == True: + if periodic: # Histopolation - if IoH == True: + if IoH: # D-splines - if BoD == True: + if BoD: get_rows_periodic(starts, ends, -p + 1, p, Nbasis, col, aux) # B-splines - if BoD == False: + if not BoD: get_rows_periodic(starts, ends, -p + 1, p + 1, Nbasis, col, aux) # Interpolation - if IoH == False: + if not IoH: # D-splines - if BoD == True: + if BoD: # Special case p = 1 if p == 1: get_rows_periodic(starts, ends, -1, 1, Nbasis, col, aux) if p != 1: get_rows_periodic(starts, ends, -p + 1, p - 1, Nbasis, col, aux) # B-splines - if BoD == False: + if not BoD: get_rows_periodic(starts, ends, -p + 1, p, Nbasis, col, aux) # Clamped boundary conditions - if periodic == False: + if not periodic: # Histopolation - if IoH == True: + if IoH: # D-splines - if BoD == True: + if BoD: count = 0 for row in range(starts, ends + 1): if row >= 0 and row <= (p - 2) and col >= 0 and col <= row + p - 1: @@ -1124,7 +1124,7 @@ def get_rows( aux[count] = 1 count += 1 # B-splines - if BoD == False: + if not BoD: count = 0 for row in range(starts, ends + 1): if row >= 0 and row <= (p - 2) and col >= 0 and col <= (row + p): @@ -1135,9 +1135,9 @@ def get_rows( aux[count] = 1 count += 1 # Interpolation - if IoH == False: + if not IoH: # D-splines - if BoD == True: + if BoD: count = 0 for row in range(starts, ends + 1): if row == 0 and col <= (p - 1): @@ -1152,7 +1152,7 @@ def get_rows( aux[count] = 1 count += 1 # B-splines - if BoD == False: + if not BoD: count = 0 for row in range(starts, ends + 1): if row == 0 and col <= p: diff --git a/src/struphy/feec/mass.py b/src/struphy/feec/mass.py index 16f0109d9..5964f5f7c 100644 --- a/src/struphy/feec/mass.py +++ b/src/struphy/feec/mass.py @@ -905,7 +905,7 @@ def DFinvT(e1, e2, e3): if weights_rank2: # if matrix exits fun = [] - if listinput == True and len(weights_rank2) == 1: + if listinput and len(weights_rank2) == 1: for m in range(3): fun += [[]] for n in range(3): @@ -2518,10 +2518,10 @@ def tosparse(self): if all(op is None for op in (self._W_extraction_op, self._V_extraction_op)): for bl in self._V_boundary_op.bc: for bc in bl: - assert bc == False, print(".tosparse() only works without boundary conditions at the moment") + assert not bc, print(".tosparse() only works without boundary conditions at the moment") for bl in self._W_boundary_op.bc: for bc in bl: - assert bc == False, print(".tosparse() only works without boundary conditions at the moment") + assert not bc, print(".tosparse() only works without boundary conditions at the moment") return self._mat.tosparse() elif all(isinstance(op, IdentityOperator) for op in (self._W_extraction_op, self._V_extraction_op)): @@ -2534,10 +2534,10 @@ def toarray(self): if all(op is None for op in (self._W_extraction_op, self._V_extraction_op)): for bl in self._V_boundary_op.bc: for bc in bl: - assert bc == False, print(".toarray() only works without boundary conditions at the moment") + assert not bc, print(".toarray() only works without boundary conditions at the moment") for bl in self._W_boundary_op.bc: for bc in bl: - assert bc == False, print(".toarray() only works without boundary conditions at the moment") + assert not bc, print(".toarray() only works without boundary conditions at the moment") return self._mat.toarray() elif all(isinstance(op, IdentityOperator) for op in (self._W_extraction_op, self._V_extraction_op)): diff --git a/src/struphy/feec/projectors.py b/src/struphy/feec/projectors.py index 115ed0aa1..be56cc722 100644 --- a/src/struphy/feec/projectors.py +++ b/src/struphy/feec/projectors.py @@ -1481,7 +1481,7 @@ def get_dofs_weighted(self, fun, dofs=None, first_go=True, pre_computed_dofs=Non Builds 3D numpy array with the evaluation of the right-hand-side. """ if self._space_key == "0": - if first_go == True: + if first_go: pre_computed_dofs = [fun(*self._meshgrid)] elif self._space_key == "1" or self._space_key == "2": @@ -1491,12 +1491,12 @@ def get_dofs_weighted(self, fun, dofs=None, first_go=True, pre_computed_dofs=Non f_eval = [] # If this is the first time this rank has to evaluate the weights degrees of freedom we declare the list where to store them. - if first_go == True: + if first_go: pre_computed_dofs = [] for h in range(3): # Evaluation of the function to compute the h component - if first_go == True: + if first_go: pre_computed_dofs.append(fun[h](*self._meshgrid[h])) # Array into which we will write the Dofs. @@ -1547,7 +1547,7 @@ def get_dofs_weighted(self, fun, dofs=None, first_go=True, pre_computed_dofs=Non elif self._space_key == "3": f_eval = xp.zeros(tuple(xp.shape(dim)[0] for dim in self._localpts)) # Evaluation of the function at all Gauss-Legendre quadrature points - if first_go == True: + if first_go: pre_computed_dofs = [fun(*self._meshgrid)] get_dofs_local_3_form_weighted( @@ -1578,7 +1578,7 @@ def get_dofs_weighted(self, fun, dofs=None, first_go=True, pre_computed_dofs=Non # We should do nothing here self._do_nothing[h] = 1 - if first_go == True: + if first_go: f_eval = [] for h in range(3): f_eval.append(fun[h](*self._meshgrid[h])) @@ -1588,7 +1588,7 @@ def get_dofs_weighted(self, fun, dofs=None, first_go=True, pre_computed_dofs=Non "Uknown space. It must be either H1, Hcurl, Hdiv, L2 or H1vec.", ) - if first_go == True: + if first_go: if self._space_key == "0": return pre_computed_dofs[0], pre_computed_dofs elif self._space_key == "v": @@ -1654,14 +1654,14 @@ def __call__( coeffs : psydac.linalg.basic.vector | xp.array 3D The FEM spline coefficients after projection. """ - if weighted == False: + if not weighted: return self.solve(self.get_dofs(fun, dofs=dofs), out=out) else: # We set B_or_D and basis_indices as attributes of the projectors so we can easily access them in the get_rowstarts, get_rowends and get_values functions, where they are needed. self._B_or_D = B_or_D self._basis_indices = basis_indices - if first_go == True: + if first_go: # rhs contains the evaluation over the degrees of freedom of the weights multiplied by the basis function # rhs_weights contains the evaluation over the degrees of freedom of only the weights rhs, rhs_weights = self.get_dofs_weighted( diff --git a/src/struphy/feec/psydac_derham.py b/src/struphy/feec/psydac_derham.py index 523a5bb97..e5a8cde32 100644 --- a/src/struphy/feec/psydac_derham.py +++ b/src/struphy/feec/psydac_derham.py @@ -1270,7 +1270,7 @@ def _get_neighbour_one_component(self, comp): # if only one process: check if comp is neighbour in non-peridic directions, if this is not the case then return the rank as neighbour id if size == 1: - if (comp[kinds == False] == 1).all(): + if (comp[~kinds] == 1).all(): return rank # multiple processes @@ -2055,7 +2055,7 @@ def __call__(self, *etas, out=None, tmp=None, squeeze_out=False, local=False): ) if self.derham.comm is not None: - if local == False: + if not local: self.derham.comm.Allreduce( MPI.IN_PLACE, tmp, @@ -2126,7 +2126,7 @@ def __call__(self, *etas, out=None, tmp=None, squeeze_out=False, local=False): ) if self.derham.comm is not None: - if local == False: + if not local: self.derham.comm.Allreduce( MPI.IN_PLACE, tmp, diff --git a/src/struphy/feec/variational_utilities.py b/src/struphy/feec/variational_utilities.py index d03a75e3d..8174a1a5b 100644 --- a/src/struphy/feec/variational_utilities.py +++ b/src/struphy/feec/variational_utilities.py @@ -94,7 +94,7 @@ def __init__( self.Pcoord3 = CoordinateProjector(2, derham.Vh_pol["v"], derham.Vh_pol["0"]) @ derham.boundary_ops["v"] # Initialize the BasisProjectionOperators - if derham._with_local_projectors == True: + if derham._with_local_projectors: self.PiuT = BasisProjectionOperatorLocal( P0, V1h, diff --git a/src/struphy/linear_algebra/saddle_point.py b/src/struphy/linear_algebra/saddle_point.py index 3a191cde4..337664754 100644 --- a/src/struphy/linear_algebra/saddle_point.py +++ b/src/struphy/linear_algebra/saddle_point.py @@ -304,7 +304,7 @@ def __call__(self, U_init=None, Ue_init=None, P_init=None, out=None): elif self._variant == "Uzawa": info = {} - if self._spectralanalysis == True: + if self._spectralanalysis: self._spectralresult = self._spectral_analysis() else: self._spectralresult = [] @@ -333,9 +333,9 @@ def __call__(self, U_init=None, Ue_init=None, P_init=None, out=None): self._rhs0np -= self._B1np.transpose().dot(self._Pnp) self._rhs0np -= self._Anp.dot(self._Unp) self._rhs0np += self._F[0] - if self._preconditioner == False: + if not self._preconditioner: self._Unp += self._Anpinv.dot(self._rhs0np) - elif self._preconditioner == True: + elif self._preconditioner: self._Unp += self._Anpinv.dot(self._A11npinv @ self._rhs0np) R1 = self._B1np.dot(self._Unp) @@ -344,9 +344,9 @@ def __call__(self, U_init=None, Ue_init=None, P_init=None, out=None): self._rhs1np -= self._B2np.transpose().dot(self._Pnp) self._rhs1np -= self._Aenp.dot(self._Uenp) self._rhs1np += self._F[1] - if self._preconditioner == False: + if not self._preconditioner: self._Uenp += self._Aenpinv.dot(self._rhs1np) - elif self._preconditioner == True: + elif self._preconditioner: self._Uenp += self._Aenpinv.dot(self._A22npinv @ self._rhs1np) R2 = self._B2np.dot(self._Uenp) @@ -382,7 +382,7 @@ def __call__(self, U_init=None, Ue_init=None, P_init=None, out=None): # Return with info if maximum iterations reached info["success"] = False info["niter"] = iteration + 1 - if self._verbose == True: + if self._verbose: _plot_residual_norms(self._residual_norms) return self._Unp, self._Uenp, self._Pnp, info, self._residual_norms, self._spectralresult @@ -523,7 +523,7 @@ def _spectral_analysis(self): print(f"{specA22_bef_abs =}") print(f"{condA22_before =}") - if self._preconditioner == True: + if self._preconditioner: # A11 after preconditioning with its inverse if self._method_to_solve in ("DirectNPInverse", "InexactNPInverse"): eigvalsA11_after_prec, eigvecs_after = xp.linalg.eig(self._A11npinv @ self._A[0]) # Implement this diff --git a/src/struphy/linear_algebra/tests/test_saddlepoint_massmatrices.py b/src/struphy/linear_algebra/tests/test_saddlepoint_massmatrices.py index 42d3ae8d3..8de563e8c 100644 --- a/src/struphy/linear_algebra/tests/test_saddlepoint_massmatrices.py +++ b/src/struphy/linear_algebra/tests/test_saddlepoint_massmatrices.py @@ -107,7 +107,7 @@ def test_saddlepointsolver(method_for_solving, Nel, p, spl_kind, dirichlet_bc, m Cnp = derhamnumpy.curl.toarray() # Dnp = D.toarray() # Cnp = C.toarray() - if derham.with_local_projectors == True: + if derham.with_local_projectors: S21np = S21.toarray else: S21np = S21.toarray_struphy() @@ -121,7 +121,7 @@ def test_saddlepointsolver(method_for_solving, Nel, p, spl_kind, dirichlet_bc, m Cnp = derhamnumpy.curl.tosparse() # Dnp = D.tosparse() # Cnp = C.tosparse() - if derham.with_local_projectors == True: + if derham.with_local_projectors: S21np = S21.tosparse else: S21np = S21.toarray_struphy(is_sparse=True) @@ -270,7 +270,7 @@ def test_saddlepointsolver(method_for_solving, Nel, p, spl_kind, dirichlet_bc, m x_uzawa = {} x_uzawa[0] = x_u x_uzawa[1] = x_ue - if show_plots == True: + if show_plots: _plot_residual_norms(residual_norms) elif method_for_solving == "SaddlePointSolverGMRES": # Wrong initialization to check if changed diff --git a/src/struphy/models/hybrid.py b/src/struphy/models/hybrid.py index c1952f59c..bcc9f6492 100644 --- a/src/struphy/models/hybrid.py +++ b/src/struphy/models/hybrid.py @@ -319,15 +319,15 @@ def __init__(self): class Propagators: def __init__(self, turn_off: tuple[str, ...] = (None,)): - if "PushEtaPC" not in turn_off: + if not "PushEtaPC" in turn_off: self.push_eta_pc = propagators_markers.PushEtaPC() - if "PushVxB" not in turn_off: + if not "PushVxB" in turn_off: self.push_vxb = propagators_markers.PushVxB() - if "PressureCoupling6D" not in turn_off: + if not "PressureCoupling6D" in turn_off: self.pc6d = propagators_coupling.PressureCoupling6D() - if "ShearAlfven" not in turn_off: + if not "ShearAlfven" in turn_off: self.shearalfven = propagators_fields.ShearAlfven() - if "Magnetosonic" not in turn_off: + if not "Magnetosonic" in turn_off: self.magnetosonic = propagators_fields.Magnetosonic() def __init__(self, turn_off: tuple[str, ...] = (None,)): @@ -343,19 +343,19 @@ def __init__(self, turn_off: tuple[str, ...] = (None,)): self.propagators = self.Propagators(turn_off) # 3. assign variables to propagators - if "ShearAlfven" not in turn_off: + if not "ShearAlfven" in turn_off: self.propagators.shearalfven.variables.u = self.mhd.velocity self.propagators.shearalfven.variables.b = self.em_fields.b_field - if "Magnetosonic" not in turn_off: + if not "Magnetosonic" in turn_off: self.propagators.magnetosonic.variables.n = self.mhd.density self.propagators.magnetosonic.variables.u = self.mhd.velocity self.propagators.magnetosonic.variables.p = self.mhd.pressure - if "PressureCoupling6D" not in turn_off: + if not "PressureCoupling6D" in turn_off: self.propagators.pc6d.variables.u = self.mhd.velocity self.propagators.pc6d.variables.energetic_ions = self.energetic_ions.var - if "PushEtaPC" not in turn_off: + if not "PushEtaPC" in turn_off: self.propagators.push_eta_pc.variables.var = self.energetic_ions.var - if "PushVxB" not in turn_off: + if not "PushVxB" in turn_off: self.propagators.push_vxb.variables.ions = self.energetic_ions.var # define scalars for update_scalar_quantities @@ -584,19 +584,19 @@ def __init__(self): class Propagators: def __init__(self, turn_off: tuple[str, ...] = (None,)): - if "PushGuidingCenterBxEstar" not in turn_off: + if not "PushGuidingCenterBxEstar" in turn_off: self.push_bxe = propagators_markers.PushGuidingCenterBxEstar() - if "PushGuidingCenterParallel" not in turn_off: + if not "PushGuidingCenterParallel" in turn_off: self.push_parallel = propagators_markers.PushGuidingCenterParallel() - if "ShearAlfvenCurrentCoupling5D" not in turn_off: + if not "ShearAlfvenCurrentCoupling5D" in turn_off: self.shearalfen_cc5d = propagators_fields.ShearAlfvenCurrentCoupling5D() - if "Magnetosonic" not in turn_off: + if not "Magnetosonic" in turn_off: self.magnetosonic = propagators_fields.Magnetosonic() - if "CurrentCoupling5DDensity" not in turn_off: + if not "CurrentCoupling5DDensity" in turn_off: self.cc5d_density = propagators_fields.CurrentCoupling5DDensity() - if "CurrentCoupling5DGradB" not in turn_off: + if not "CurrentCoupling5DGradB" in turn_off: self.cc5d_gradb = propagators_coupling.CurrentCoupling5DGradB() - if "CurrentCoupling5DCurlb" not in turn_off: + if not "CurrentCoupling5DCurlb" in turn_off: self.cc5d_curlb = propagators_coupling.CurrentCoupling5DCurlb() def __init__(self, turn_off: tuple[str, ...] = (None,)): @@ -612,24 +612,24 @@ def __init__(self, turn_off: tuple[str, ...] = (None,)): self.propagators = self.Propagators(turn_off) # 3. assign variables to propagators - if "ShearAlfvenCurrentCoupling5D" not in turn_off: + if not "ShearAlfvenCurrentCoupling5D" in turn_off: self.propagators.shearalfen_cc5d.variables.u = self.mhd.velocity self.propagators.shearalfen_cc5d.variables.b = self.em_fields.b_field - if "Magnetosonic" not in turn_off: + if not "Magnetosonic" in turn_off: self.propagators.magnetosonic.variables.n = self.mhd.density self.propagators.magnetosonic.variables.u = self.mhd.velocity self.propagators.magnetosonic.variables.p = self.mhd.pressure - if "CurrentCoupling5DDensity" not in turn_off: + if not "CurrentCoupling5DDensity" in turn_off: self.propagators.cc5d_density.variables.u = self.mhd.velocity - if "CurrentCoupling5DGradB" not in turn_off: + if not "CurrentCoupling5DGradB" in turn_off: self.propagators.cc5d_gradb.variables.u = self.mhd.velocity self.propagators.cc5d_gradb.variables.energetic_ions = self.energetic_ions.var - if "CurrentCoupling5DCurlb" not in turn_off: + if not "CurrentCoupling5DCurlb" in turn_off: self.propagators.cc5d_curlb.variables.u = self.mhd.velocity self.propagators.cc5d_curlb.variables.energetic_ions = self.energetic_ions.var - if "PushGuidingCenterBxEstar" not in turn_off: + if not "PushGuidingCenterBxEstar" in turn_off: self.propagators.push_bxe.variables.ions = self.energetic_ions.var - if "PushGuidingCenterParallel" not in turn_off: + if not "PushGuidingCenterParallel" in turn_off: self.propagators.push_parallel.variables.ions = self.energetic_ions.var # define scalars for update_scalar_quantities diff --git a/src/struphy/polar/basic.py b/src/struphy/polar/basic.py index f737c671e..99a95cc47 100644 --- a/src/struphy/polar/basic.py +++ b/src/struphy/polar/basic.py @@ -19,7 +19,7 @@ class PolarDerhamSpace(VectorSpace): """ def __init__(self, derham, space_id): - assert derham.spl_kind[0] == False, "Spline basis in eta1 must be clamped" + assert not derham.spl_kind[0], "Spline basis in eta1 must be clamped" assert derham.spl_kind[1], "Spline basis in eta2 must be periodic" assert (derham.Nel[1] / 3) % 1 == 0.0, "Number of elements in eta2 must be a multiple of 3" diff --git a/src/struphy/propagators/propagators_fields.py b/src/struphy/propagators/propagators_fields.py index c3f3e1381..1c14c2cd9 100644 --- a/src/struphy/propagators/propagators_fields.py +++ b/src/struphy/propagators/propagators_fields.py @@ -2190,7 +2190,7 @@ def _initialize_projection_operator_TB(self): self._bf = self.derham.create_spline_function("bf", "Hdiv") # Initialize BasisProjectionOperator - if self.derham._with_local_projectors == True: + if self.derham._with_local_projectors: self._TB = BasisProjectionOperatorLocal( P1, Vh, @@ -8638,7 +8638,7 @@ def __call__(self, dt): # _Anp[1] and _Anppre[1] remain unchanged _Anp = [A11np, A22np] - if self._preconditioner == True: + if self._preconditioner: _A11prenp = self._M2np / dt # + self._A11prenp_notimedependency _Anppre = [_A11prenp, _A22prenp] @@ -8675,7 +8675,7 @@ def __call__(self, dt): _Fnp = [_F1np, _F2np] if self.rank == 0: - if self._preconditioner == True: + if self._preconditioner: self._solver_UzawaNumpy.Apre = _Anppre self._solver_UzawaNumpy.A = _Anp self._solver_UzawaNumpy.F = _Fnp From bd9b0ed7739b6cd9125742b27a3cbb4c89dadef0 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 30 Oct 2025 17:38:30 +0100 Subject: [PATCH 10/83] Convert f strings to normal strings when curly braces arent used (#77) Redo of https://github.com/struphy-hub/struphy/pull/63 **Solves the following issue(s):** Closes #56 I ran the following command: ``` ruff check --select F541 --fix ``` https://docs.astral.sh/ruff/rules/f-string-missing-placeholders/ --------- Co-authored-by: Stefan Possanner Co-authored-by: Byung Kyu Na Co-authored-by: Stefan Possanner <86720346+spossann@users.noreply.github.com> --- src/struphy/console/compile.py | 2 +- src/struphy/console/main.py | 2 +- src/struphy/diagnostics/diagn_tools.py | 2 +- .../mhd_axisymmetric_main.py | 14 ++-- .../feec/tests/test_lowdim_nel_is_1.py | 2 +- src/struphy/geometry/domains.py | 2 +- .../initial/tests/test_init_perturbations.py | 4 +- src/struphy/io/setup.py | 12 ++-- .../tests/test_maxwellians.py | 20 +++--- .../tests/test_saddlepoint_massmatrices.py | 2 +- src/struphy/main.py | 10 +-- src/struphy/models/base.py | 68 ++++++++++--------- src/struphy/models/variables.py | 4 +- src/struphy/ode/tests/test_ode_feec.py | 8 +-- src/struphy/pic/particles.py | 2 +- src/struphy/pic/tests/test_mat_vec_filler.py | 16 ++--- .../likwid/plot_likwidproject.py | 2 +- .../post_processing/post_processing_tools.py | 2 +- src/struphy/propagators/propagators_fields.py | 2 +- .../tests/test_gyrokinetic_poisson.py | 2 +- src/struphy/propagators/tests/test_poisson.py | 2 +- ...l_03_smoothed_particle_hydrodynamics.ipynb | 4 +- .../tutorial_02_fluid_particles.ipynb | 12 ++-- 23 files changed, 99 insertions(+), 97 deletions(-) diff --git a/src/struphy/console/compile.py b/src/struphy/console/compile.py index 432e4fa1f..d92f31285 100644 --- a/src/struphy/console/compile.py +++ b/src/struphy/console/compile.py @@ -272,7 +272,7 @@ def struphy_compile( ) sys.exit(1) else: - print(f"Psydac is not installed. To install it, please re-install struphy (e.g. pip install .)\n") + print("Psydac is not installed. To install it, please re-install struphy (e.g. pip install .)\n") sys.exit(1) else: diff --git a/src/struphy/console/main.py b/src/struphy/console/main.py index f6e03ff16..545e4a24c 100644 --- a/src/struphy/console/main.py +++ b/src/struphy/console/main.py @@ -452,7 +452,7 @@ def add_parser_run(subparsers, list_models, model_message, params_files, batch_f default=None, # fallback if nothing is passed choices=list_models, metavar="MODEL", - help=model_message + f" (default: None)", + help=model_message + " (default: None)", ) parser_run.add_argument( diff --git a/src/struphy/diagnostics/diagn_tools.py b/src/struphy/diagnostics/diagn_tools.py index b9e66dbb6..e7a9d8ee3 100644 --- a/src/struphy/diagnostics/diagn_tools.py +++ b/src/struphy/diagnostics/diagn_tools.py @@ -683,7 +683,7 @@ def plots_videos_2d( df_binned = df_data[tuple(f_slicing)].squeeze() - assert t_grid.ndim == grid_1.ndim == grid_2.ndim == 1, f"Input arrays must be 1D!" + assert t_grid.ndim == grid_1.ndim == grid_2.ndim == 1, "Input arrays must be 1D!" assert df_binned.shape[0] == t_grid.size, f"{df_binned.shape =}, {t_grid.shape =}" assert df_binned.shape[1] == grid_1.size, f"{df_binned.shape =}, {grid_1.shape =}" assert df_binned.shape[2] == grid_2.size, f"{df_binned.shape =}, {grid_2.shape =}" diff --git a/src/struphy/eigenvalue_solvers/mhd_axisymmetric_main.py b/src/struphy/eigenvalue_solvers/mhd_axisymmetric_main.py index b8d4aaf81..04a194c7f 100644 --- a/src/struphy/eigenvalue_solvers/mhd_axisymmetric_main.py +++ b/src/struphy/eigenvalue_solvers/mhd_axisymmetric_main.py @@ -45,13 +45,13 @@ def solve_mhd_ev_problem_2d(num_params, eq_mhd, n_tor, basis_tor="i", path_out=N # print grid info print("\nGrid parameters:") - print(f"number of elements :", num_params["Nel"]) - print(f"spline degrees :", num_params["p"]) - print(f"periodic bcs :", num_params["spl_kind"]) - print(f"hom. Dirichlet bc :", num_params["bc"]) - print(f"GL quad pts (L2) :", num_params["nq_el"]) - print(f"GL quad pts (hist) :", num_params["nq_pr"]) - print(f"polar Ck :", num_params["polar_ck"]) + print("number of elements :", num_params["Nel"]) + print("spline degrees :", num_params["p"]) + print("periodic bcs :", num_params["spl_kind"]) + print("hom. Dirichlet bc :", num_params["bc"]) + print("GL quad pts (L2) :", num_params["nq_el"]) + print("GL quad pts (hist) :", num_params["nq_pr"]) + print("polar Ck :", num_params["polar_ck"]) print("") # extract numerical parameters diff --git a/src/struphy/feec/tests/test_lowdim_nel_is_1.py b/src/struphy/feec/tests/test_lowdim_nel_is_1.py index cefcddf61..325da31ea 100644 --- a/src/struphy/feec/tests/test_lowdim_nel_is_1.py +++ b/src/struphy/feec/tests/test_lowdim_nel_is_1.py @@ -277,7 +277,7 @@ def div_f(x, y, z): plt.subplot(2, 1, 2) plt.plot(e, div_f(e1, e2, e3), "o") plt.plot(e, field_df2_vals) - plt.title(f"div") + plt.title("div") plt.subplots_adjust(wspace=1.0, hspace=0.4) diff --git a/src/struphy/geometry/domains.py b/src/struphy/geometry/domains.py index 7b2c25064..20f995779 100644 --- a/src/struphy/geometry/domains.py +++ b/src/struphy/geometry/domains.py @@ -747,7 +747,7 @@ def __init__( if sfl: assert pol_period == 1, ( - f"Piece-of-cake is only implemented for torus coordinates, not for straight field line coordinates!" + "Piece-of-cake is only implemented for torus coordinates, not for straight field line coordinates!" ) # periodicity in eta3-direction and pole at eta1=0 diff --git a/src/struphy/initial/tests/test_init_perturbations.py b/src/struphy/initial/tests/test_init_perturbations.py index dd391cf56..5d52e9291 100644 --- a/src/struphy/initial/tests/test_init_perturbations.py +++ b/src/struphy/initial/tests/test_init_perturbations.py @@ -169,7 +169,7 @@ def test_init_modes(Nel, p, spl_kind, mapping, combine_comps=None, do_plot=False plt.xlabel("x") plt.ylabel("y") plt.colorbar() - plt.title(f"exact function") + plt.title("exact function") ax = plt.gca() ax.set_aspect("equal", adjustable="box") @@ -198,7 +198,7 @@ def test_init_modes(Nel, p, spl_kind, mapping, combine_comps=None, do_plot=False plt.xlabel("x") plt.ylabel("z") plt.colorbar() - plt.title(f"exact function") + plt.title("exact function") ax = plt.gca() ax.set_aspect("equal", adjustable="box") diff --git a/src/struphy/io/setup.py b/src/struphy/io/setup.py index f38654160..4ecd96f47 100644 --- a/src/struphy/io/setup.py +++ b/src/struphy/io/setup.py @@ -152,12 +152,12 @@ def setup_derham( if MPI.COMM_WORLD.Get_rank() == 0 and verbose: print("\nDERHAM:") - print(f"number of elements:".ljust(25), Nel) - print(f"spline degrees:".ljust(25), p) - print(f"periodic bcs:".ljust(25), spl_kind) - print(f"hom. Dirichlet bc:".ljust(25), dirichlet_bc) - print(f"GL quad pts (L2):".ljust(25), nquads) - print(f"GL quad pts (hist):".ljust(25), nq_pr) + print("number of elements:".ljust(25), Nel) + print("spline degrees:".ljust(25), p) + print("periodic bcs:".ljust(25), spl_kind) + print("hom. Dirichlet bc:".ljust(25), dirichlet_bc) + print("GL quad pts (L2):".ljust(25), nquads) + print("GL quad pts (hist):".ljust(25), nq_pr) print( "MPI proc. per dir.:".ljust(25), derham.domain_decomposition.nprocs, diff --git a/src/struphy/kinetic_background/tests/test_maxwellians.py b/src/struphy/kinetic_background/tests/test_maxwellians.py index 710a88262..4aaa0624a 100644 --- a/src/struphy/kinetic_background/tests/test_maxwellians.py +++ b/src/struphy/kinetic_background/tests/test_maxwellians.py @@ -294,7 +294,7 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): continue if "GVECequilibrium" in key: - print(f"Attention: flat (marker) evaluation not tested for GVEC at the moment.") + print("Attention: flat (marker) evaluation not tested for GVEC at the moment.") mhd_equil = val() assert isinstance(mhd_equil, FluidEquilibrium) @@ -496,7 +496,7 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): plt.ylabel("y") plt.axis("equal") plt.colorbar() - plt.title(f"Maxwellian thermal velocity $v_t$, top view (e1-e3)") + plt.title("Maxwellian thermal velocity $v_t$, top view (e1-e3)") plt.subplot(2, 5, 10) if "Slab" in key or "Pinch" in key: plt.contourf(x[:, :, 0], y[:, :, 0], vth_cart[:, :, 0], levels=levels) @@ -508,7 +508,7 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): plt.ylabel("z") plt.axis("equal") plt.colorbar() - plt.title(f"Maxwellian thermal velocity $v_t$, poloidal view (e1-e2)") + plt.title("Maxwellian thermal velocity $v_t$, poloidal view (e1-e2)") plt.show() @@ -678,7 +678,7 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): plt.ylabel("y") plt.axis("equal") plt.colorbar() - plt.title(f"Maxwellian perturbed thermal velocity $v_t$, top view (e1-e3)") + plt.title("Maxwellian perturbed thermal velocity $v_t$, top view (e1-e3)") plt.subplot(2, 5, 10) if "Slab" in key or "Pinch" in key: plt.contourf(x[:, :, 0], y[:, :, 0], vth_cart[:, :, 0], levels=levels) @@ -690,7 +690,7 @@ def test_maxwellian_3d_mhd(Nel, with_desc, show_plot=False): plt.ylabel("z") plt.axis("equal") plt.colorbar() - plt.title(f"Maxwellian perturbed thermal velocity $v_t$, poloidal view (e1-e2)") + plt.title("Maxwellian perturbed thermal velocity $v_t$, poloidal view (e1-e2)") plt.show() @@ -1090,7 +1090,7 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): continue if "GVECequilibrium" in key: - print(f"Attention: flat (marker) evaluation not tested for GVEC at the moment.") + print("Attention: flat (marker) evaluation not tested for GVEC at the moment.") mhd_equil = val() if not isinstance(mhd_equil, FluidEquilibriumWithB): @@ -1287,7 +1287,7 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): plt.ylabel("y") plt.axis("equal") plt.colorbar() - plt.title(f"Maxwellian thermal velocity $v_t$, top view (e1-e3)") + plt.title("Maxwellian thermal velocity $v_t$, top view (e1-e3)") plt.subplot(2, 4, 8) if "Slab" in key or "Pinch" in key: plt.contourf(x[:, :, 0], y[:, :, 0], vth_cart[:, :, 0], levels=levels) @@ -1299,7 +1299,7 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): plt.ylabel("z") plt.axis("equal") plt.colorbar() - plt.title(f"Maxwellian density $v_t$, poloidal view (e1-e2)") + plt.title("Maxwellian density $v_t$, poloidal view (e1-e2)") plt.show() @@ -1463,7 +1463,7 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): plt.ylabel("y") plt.axis("equal") plt.colorbar() - plt.title(f"Maxwellian perturbed thermal velocity $v_t$, top view (e1-e3)") + plt.title("Maxwellian perturbed thermal velocity $v_t$, top view (e1-e3)") plt.subplot(2, 4, 8) if "Slab" in key or "Pinch" in key: plt.contourf(x[:, :, 0], y[:, :, 0], vth_cart[:, :, 0], levels=levels) @@ -1475,7 +1475,7 @@ def test_maxwellian_2d_mhd(Nel, with_desc, show_plot=False): plt.ylabel("z") plt.axis("equal") plt.colorbar() - plt.title(f"Maxwellian perturbed density $v_t$, poloidal view (e1-e2)") + plt.title("Maxwellian perturbed density $v_t$, poloidal view (e1-e2)") plt.show() diff --git a/src/struphy/linear_algebra/tests/test_saddlepoint_massmatrices.py b/src/struphy/linear_algebra/tests/test_saddlepoint_massmatrices.py index 8de563e8c..823584bc9 100644 --- a/src/struphy/linear_algebra/tests/test_saddlepoint_massmatrices.py +++ b/src/struphy/linear_algebra/tests/test_saddlepoint_massmatrices.py @@ -242,7 +242,7 @@ def test_saddlepointsolver(method_for_solving, Nel, p, spl_kind, dirichlet_bc, m TestA11dot = TestA11.dot(x1) compare_arrays(TestA11dot, TestA11composeddot, mpi_rank, atol=1e-5) # compare_arrays(TestA11dot, TestA11npdot, mpi_rank, atol=1e-5) - print(f"Comparison numpy to psydac succesfull.") + print("Comparison numpy to psydac succesfull.") M2pre = MassMatrixPreconditioner(mass_mats.M2) diff --git a/src/struphy/main.py b/src/struphy/main.py index 4b7b65645..047abea95 100644 --- a/src/struphy/main.py +++ b/src/struphy/main.py @@ -802,7 +802,7 @@ def load_data(path: str) -> SimData: raise NotImplementedError print("\nThe following data has been loaded:") - print(f"\ngrids:") + print("\ngrids:") print(f"{simdata.t_grid.shape =}") if simdata.grids_log is not None: print(f"{simdata.grids_log[0].shape =}") @@ -812,22 +812,22 @@ def load_data(path: str) -> SimData: print(f"{simdata.grids_phy[0].shape =}") print(f"{simdata.grids_phy[1].shape =}") print(f"{simdata.grids_phy[2].shape =}") - print(f"\nsimdata.spline_values:") + print("\nsimdata.spline_values:") for k, v in simdata.spline_values.items(): print(f" {k}") for kk, vv in v.items(): print(f" {kk}") - print(f"\nsimdata.orbits:") + print("\nsimdata.orbits:") for k, v in simdata.orbits.items(): print(f" {k}") - print(f"\nsimdata.f:") + print("\nsimdata.f:") for k, v in simdata.f.items(): print(f" {k}") for kk, vv in v.items(): print(f" {kk}") for kkk, vvv in vv.items(): print(f" {kkk}") - print(f"\nsimdata.n_sph:") + print("\nsimdata.n_sph:") for k, v in simdata.n_sph.items(): print(f" {k}") for kk, vv in v.items(): diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index fb77bcb65..eb133cb45 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -106,7 +106,7 @@ def setup_domain_and_equil(self, domain: Domain, equil: FluidEquilibrium): if MPI.COMM_WORLD.Get_rank() == 0 and self.verbose: print("\nDOMAIN:") - print(f"type:".ljust(25), self.domain.__class__.__name__) + print("type:".ljust(25), self.domain.__class__.__name__) for key, val in self.domain.params.items(): if key not in {"cx", "cy", "cz"}: print((key + ":").ljust(25), val) @@ -721,7 +721,7 @@ def update_markers_to_be_saved(self): for name, species in self.particle_species.items(): assert isinstance(species, ParticleSpecies) - assert len(species.variables) == 1, f"More than 1 variable per kinetic species is not allowed." + assert len(species.variables) == 1, "More than 1 variable per kinetic species is not allowed." for _, var in species.variables.items(): assert isinstance(var, PICVariable | SPHVariable) obj = var.particles @@ -746,7 +746,7 @@ def update_distr_functions(self): for name, species in self.particle_species.items(): assert isinstance(species, ParticleSpecies) - assert len(species.variables) == 1, f"More than 1 variable per kinetic species is not allowed." + assert len(species.variables) == 1, "More than 1 variable per kinetic species is not allowed." for _, var in species.variables.items(): assert isinstance(var, PICVariable | SPHVariable) obj = var.particles @@ -1107,7 +1107,7 @@ def initialize_data_output(self, data: DataContainer, size): # save kinetic data in group 'kinetic/' for name, species in self.particle_species.items(): assert isinstance(species, ParticleSpecies) - assert len(species.variables) == 1, f"More than 1 variable per kinetic species is not allowed." + assert len(species.variables) == 1, "More than 1 variable per kinetic species is not allowed." for varname, var in species.variables.items(): assert isinstance(var, PICVariable | SPHVariable) obj = var.particles @@ -1332,15 +1332,15 @@ def generate_default_parameter_file( has_plasma = True species_params += f"model.{sn}.set_phys_params()\n" if isinstance(species, ParticleSpecies): - particle_params += f"\nloading_params = LoadingParameters()\n" - particle_params += f"weights_params = WeightsParameters()\n" - particle_params += f"boundary_params = BoundaryParameters()\n" + particle_params += "\nloading_params = LoadingParameters()\n" + particle_params += "weights_params = WeightsParameters()\n" + particle_params += "boundary_params = BoundaryParameters()\n" particle_params += f"model.{sn}.set_markers(loading_params=loading_params,\n" - txt = f"weights_params=weights_params,\n" + txt = "weights_params=weights_params,\n" particle_params += indent(txt, " " * len(f"model.{sn}.set_markers(")) - txt = f"boundary_params=boundary_params,\n" + txt = "boundary_params=boundary_params,\n" particle_params += indent(txt, " " * len(f"model.{sn}.set_markers(")) - txt = f")\n" + txt = ")\n" particle_params += indent(txt, " " * len(f"model.{sn}.set_markers(")) particle_params += f"model.{sn}.set_sorting_boxes()\n" particle_params += f"model.{sn}.set_save_data()\n" @@ -1361,38 +1361,40 @@ def generate_default_parameter_file( elif isinstance(var, PICVariable): has_pic = True - init_pert_pic = f"\n# if .add_initial_condition is not called, the background is the kinetic initial condition\n" - init_pert_pic += f"perturbation = perturbations.TorusModesCos()\n" + init_pert_pic = ( + "\n# if .add_initial_condition is not called, the background is the kinetic initial condition\n" + ) + init_pert_pic += "perturbation = perturbations.TorusModesCos()\n" if "6D" in var.space: - init_bckgr_pic = f"maxwellian_1 = maxwellians.Maxwellian3D(n=(1.0, None))\n" - init_bckgr_pic += f"maxwellian_2 = maxwellians.Maxwellian3D(n=(0.1, None))\n" - init_pert_pic += f"maxwellian_1pt = maxwellians.Maxwellian3D(n=(1.0, perturbation))\n" - init_pert_pic += f"init = maxwellian_1pt + maxwellian_2\n" + init_bckgr_pic = "maxwellian_1 = maxwellians.Maxwellian3D(n=(1.0, None))\n" + init_bckgr_pic += "maxwellian_2 = maxwellians.Maxwellian3D(n=(0.1, None))\n" + init_pert_pic += "maxwellian_1pt = maxwellians.Maxwellian3D(n=(1.0, perturbation))\n" + init_pert_pic += "init = maxwellian_1pt + maxwellian_2\n" init_pert_pic += f"model.{sn}.{vn}.add_initial_condition(init)\n" elif "5D" in var.space: - init_bckgr_pic = f"maxwellian_1 = maxwellians.GyroMaxwellian2D(n=(1.0, None), equil=equil)\n" - init_bckgr_pic += f"maxwellian_2 = maxwellians.GyroMaxwellian2D(n=(0.1, None), equil=equil)\n" + init_bckgr_pic = "maxwellian_1 = maxwellians.GyroMaxwellian2D(n=(1.0, None), equil=equil)\n" + init_bckgr_pic += "maxwellian_2 = maxwellians.GyroMaxwellian2D(n=(0.1, None), equil=equil)\n" init_pert_pic += ( - f"maxwellian_1pt = maxwellians.GyroMaxwellian2D(n=(1.0, perturbation), equil=equil)\n" + "maxwellian_1pt = maxwellians.GyroMaxwellian2D(n=(1.0, perturbation), equil=equil)\n" ) - init_pert_pic += f"init = maxwellian_1pt + maxwellian_2\n" + init_pert_pic += "init = maxwellian_1pt + maxwellian_2\n" init_pert_pic += f"model.{sn}.{vn}.add_initial_condition(init)\n" if "3D" in var.space: - init_bckgr_pic = f"maxwellian_1 = maxwellians.ColdPlasma(n=(1.0, None))\n" - init_bckgr_pic += f"maxwellian_2 = maxwellians.ColdPlasma(n=(0.1, None))\n" - init_pert_pic += f"maxwellian_1pt = maxwellians.ColdPlasma(n=(1.0, perturbation))\n" - init_pert_pic += f"init = maxwellian_1pt + maxwellian_2\n" + init_bckgr_pic = "maxwellian_1 = maxwellians.ColdPlasma(n=(1.0, None))\n" + init_bckgr_pic += "maxwellian_2 = maxwellians.ColdPlasma(n=(0.1, None))\n" + init_pert_pic += "maxwellian_1pt = maxwellians.ColdPlasma(n=(1.0, perturbation))\n" + init_pert_pic += "init = maxwellian_1pt + maxwellian_2\n" init_pert_pic += f"model.{sn}.{vn}.add_initial_condition(init)\n" - init_bckgr_pic += f"background = maxwellian_1 + maxwellian_2\n" + init_bckgr_pic += "background = maxwellian_1 + maxwellian_2\n" init_bckgr_pic += f"model.{sn}.{vn}.add_background(background)\n" - exclude = f"# model.....save_data = False\n" + exclude = "# model.....save_data = False\n" elif isinstance(var, SPHVariable): has_sph = True - init_bckgr_sph = f"background = equils.ConstantVelocity()\n" + init_bckgr_sph = "background = equils.ConstantVelocity()\n" init_bckgr_sph += f"model.{sn}.{vn}.add_background(background)\n" - init_pert_sph = f"perturbation = perturbations.TorusModesCos()\n" + init_pert_sph = "perturbation = perturbations.TorusModesCos()\n" init_pert_sph += f"model.{sn}.{vn}.add_perturbation(del_n=perturbation)\n" exclude = f"# model.{sn}.{vn}.save_data = False\n" @@ -1583,23 +1585,23 @@ def compute_plasma_params(self, verbose=True): if verbose and MPI.COMM_WORLD.Get_rank() == 0: print("\nPLASMA PARAMETERS:") print( - f"Plasma volume:".ljust(25), + "Plasma volume:".ljust(25), "{:4.3e}".format(plasma_volume) + units_affix["plasma volume"], ) print( - f"Transit length:".ljust(25), + "Transit length:".ljust(25), "{:4.3e}".format(transit_length) + units_affix["transit length"], ) print( - f"Avg. magnetic field:".ljust(25), + "Avg. magnetic field:".ljust(25), "{:4.3e}".format(magnetic_field) + units_affix["magnetic field"], ) print( - f"Max magnetic field:".ljust(25), + "Max magnetic field:".ljust(25), "{:4.3e}".format(B_max) + units_affix["magnetic field"], ) print( - f"Min magnetic field:".ljust(25), + "Min magnetic field:".ljust(25), "{:4.3e}".format(B_min) + units_affix["magnetic field"], ) diff --git a/src/struphy/models/variables.py b/src/struphy/models/variables.py index e47ab1cd0..e1c310db0 100644 --- a/src/struphy/models/variables.py +++ b/src/struphy/models/variables.py @@ -197,7 +197,7 @@ def allocate( ): # assert isinstance(self.species, KineticSpecies) assert isinstance(self.backgrounds, KineticBackground), ( - f"List input not allowed, you can sum Kineticbackgrounds before passing them to add_background." + "List input not allowed, you can sum Kineticbackgrounds before passing them to add_background." ) if derham is None: @@ -340,7 +340,7 @@ def allocate( verbose: bool = False, ): assert isinstance(self.backgrounds, FluidEquilibrium), ( - f"List input not allowed, you can sum Kineticbackgrounds before passing them to add_background." + "List input not allowed, you can sum Kineticbackgrounds before passing them to add_background." ) self.backgrounds.domain = domain diff --git a/src/struphy/ode/tests/test_ode_feec.py b/src/struphy/ode/tests/test_ode_feec.py index c0ef51b08..7dfa87a46 100644 --- a/src/struphy/ode/tests/test_ode_feec.py +++ b/src/struphy/ode/tests/test_ode_feec.py @@ -167,10 +167,10 @@ def f(t, y1, y2, y3, out=out): print(f"Convergence check passed on {rank =}.") if rank == 0: - plt.loglog(h_vec, h_vec, "--", label=f"h") - plt.loglog(h_vec, [h**2 for h in h_vec], "--", label=f"h^2") - plt.loglog(h_vec, [h**3 for h in h_vec], "--", label=f"h^3") - plt.loglog(h_vec, [h**4 for h in h_vec], "--", label=f"h^4") + plt.loglog(h_vec, h_vec, "--", label="h") + plt.loglog(h_vec, [h**2 for h in h_vec], "--", label="h^2") + plt.loglog(h_vec, [h**3 for h in h_vec], "--", label="h^3") + plt.loglog(h_vec, [h**4 for h in h_vec], "--", label="h^4") plt.loglog(h_vec, err_vec, "o-k", label=f"{spaces[j]}-space, {algo}") if rank == 0: plt.xlabel("log(h)") diff --git a/src/struphy/pic/particles.py b/src/struphy/pic/particles.py index 79634d13a..6c818b3ee 100644 --- a/src/struphy/pic/particles.py +++ b/src/struphy/pic/particles.py @@ -142,7 +142,7 @@ def s0(self, eta1, eta2, eta3, *v, flat_eval=False, remove_holes=True): The 0-form sampling density. ------- """ - assert self.domain, f"self.domain must be set to call the sampling density 0-form." + assert self.domain, "self.domain must be set to call the sampling density 0-form." return self.domain.transform( self.svol(eta1, eta2, eta3, *v), diff --git a/src/struphy/pic/tests/test_mat_vec_filler.py b/src/struphy/pic/tests/test_mat_vec_filler.py index c6bee1faa..073d52ae7 100644 --- a/src/struphy/pic/tests/test_mat_vec_filler.py +++ b/src/struphy/pic/tests/test_mat_vec_filler.py @@ -261,14 +261,14 @@ def test_particle_to_mat_kernels(Nel, p, spl_kind, n_markers=1): # testing salar spaces if rank == 0: - print(f"\nTesting mat_fill_b_v0 ...") + print("\nTesting mat_fill_b_v0 ...") ptomat.mat_fill_b_v0(DR.args_derham, eta1, eta2, eta3, mat["v0"], fill_mat[0, 0]) assert_mat(mat["v0"], rows, cols, basis["v0"], basis["v0"], rank) # assertion test of mat count += 1 comm.Barrier() if rank == 0: - print(f"\nTesting m_v_fill_b_v0 ...") + print("\nTesting m_v_fill_b_v0 ...") ptomat.m_v_fill_b_v0(DR.args_derham, eta1, eta2, eta3, mat["v0"], fill_mat[0, 0], vec["v0"], fill_vec[0]) assert_mat(mat["v0"], rows, cols, basis["v0"], basis["v0"], rank) # assertion test of mat assert_vec(vec["v0"], rows, basis["v0"], rank) # assertion test of vec @@ -276,14 +276,14 @@ def test_particle_to_mat_kernels(Nel, p, spl_kind, n_markers=1): comm.Barrier() if rank == 0: - print(f"\nTesting mat_fill_b_v3 ...") + print("\nTesting mat_fill_b_v3 ...") ptomat.mat_fill_b_v3(DR.args_derham, eta1, eta2, eta3, mat["v3"], fill_mat[0, 0]) assert_mat(mat["v3"], rows, cols, basis["v3"], basis["v3"], rank) # assertion test of mat count += 1 comm.Barrier() if rank == 0: - print(f"\nTesting m_v_fill_b_v3 ...") + print("\nTesting m_v_fill_b_v3 ...") ptomat.m_v_fill_b_v3(DR.args_derham, eta1, eta2, eta3, mat["v3"], fill_mat[0, 0], vec["v3"], fill_vec[0]) assert_mat(mat["v3"], rows, cols, basis["v3"], basis["v3"], rank) # assertion test of mat assert_vec(vec["v3"], rows, basis["v3"], rank) # assertion test of vec @@ -291,14 +291,14 @@ def test_particle_to_mat_kernels(Nel, p, spl_kind, n_markers=1): comm.Barrier() if rank == 0: - print(f"\nTesting mat_fill_v0 ...") + print("\nTesting mat_fill_v0 ...") ptomat.mat_fill_v0(DR.args_derham, span1, span2, span3, mat["v0"], fill_mat[0, 0]) assert_mat(mat["v0"], rows, cols, basis["v0"], basis["v0"], rank) # assertion test of mat count += 1 comm.Barrier() if rank == 0: - print(f"\nTesting m_v_fill_v0 ...") + print("\nTesting m_v_fill_v0 ...") ptomat.m_v_fill_v0(DR.args_derham, span1, span2, span3, mat["v0"], fill_mat[0, 0], vec["v0"], fill_vec[0]) assert_mat(mat["v0"], rows, cols, basis["v0"], basis["v0"], rank) # assertion test of mat assert_vec(vec["v0"], rows, basis["v0"], rank) # assertion test of vec @@ -306,14 +306,14 @@ def test_particle_to_mat_kernels(Nel, p, spl_kind, n_markers=1): comm.Barrier() if rank == 0: - print(f"\nTesting mat_fill_v3 ...") + print("\nTesting mat_fill_v3 ...") ptomat.mat_fill_v3(DR.args_derham, span1, span2, span3, mat["v3"], fill_mat[0, 0]) assert_mat(mat["v3"], rows, cols, basis["v3"], basis["v3"], rank) # assertion test of mat count += 1 comm.Barrier() if rank == 0: - print(f"\nTesting m_v_fill_v3 ...") + print("\nTesting m_v_fill_v3 ...") ptomat.m_v_fill_v3(DR.args_derham, span1, span2, span3, mat["v3"], fill_mat[0, 0], vec["v3"], fill_vec[0]) assert_mat(mat["v3"], rows, cols, basis["v3"], basis["v3"], rank) # assertion test of mat assert_vec(vec["v3"], rows, basis["v3"], rank) # assertion test of vec diff --git a/src/struphy/post_processing/likwid/plot_likwidproject.py b/src/struphy/post_processing/likwid/plot_likwidproject.py index cde2a2b76..f4c3bb442 100644 --- a/src/struphy/post_processing/likwid/plot_likwidproject.py +++ b/src/struphy/post_processing/likwid/plot_likwidproject.py @@ -387,7 +387,7 @@ def plot_speedup( fig.update_layout( # xaxis_title='Job name', - xaxis_title=f"MPI tasks (#)", + xaxis_title="MPI tasks (#)", yaxis_title=re.sub(r"\[.*?\]", "[relative]", metric2), showlegend=True, xaxis_tickformat=".1f", diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index 74a6288f6..e0759bb63 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -156,7 +156,7 @@ def create_femfields( # get fields names, space IDs and time grid from 0-th rank hdf5 file file = h5py.File(os.path.join(path, "data/", "data_proc0.hdf5"), "r") space_ids = {} - print(f"\nReading hdf5 data of following species:") + print("\nReading hdf5 data of following species:") for species, dset in file["feec"].items(): space_ids[species] = {} print(f"{species}:") diff --git a/src/struphy/propagators/propagators_fields.py b/src/struphy/propagators/propagators_fields.py index 1c14c2cd9..462c58f26 100644 --- a/src/struphy/propagators/propagators_fields.py +++ b/src/struphy/propagators/propagators_fields.py @@ -8722,7 +8722,7 @@ def __call__(self, dt): e = phi_temp.ends phi_temp[s[0] : e[0] + 1, s[1] : e[1] + 1, s[2] : e[2] + 1] = phin.reshape(*dimphi) else: - print(f"TwoFluidQuasiNeutralFull is only running on one MPI.") + print("TwoFluidQuasiNeutralFull is only running on one MPI.") # write new coeffs into self.feec_vars max_du, max_due, max_dphi = self.update_feec_variables(u=u_temp, ue=ue_temp, phi=phi_temp) diff --git a/src/struphy/propagators/tests/test_gyrokinetic_poisson.py b/src/struphy/propagators/tests/test_gyrokinetic_poisson.py index 68ba44bcd..747ec65c7 100644 --- a/src/struphy/propagators/tests/test_gyrokinetic_poisson.py +++ b/src/struphy/propagators/tests/test_gyrokinetic_poisson.py @@ -224,7 +224,7 @@ def rho_pulled(e1, e2, e3): plt.xscale("log") plt.xlabel("Grid Spacing h") plt.ylabel("Error") - plt.title(f"Poisson solver") + plt.title("Poisson solver") plt.legend() if show_plot and rank == 0: diff --git a/src/struphy/propagators/tests/test_poisson.py b/src/struphy/propagators/tests/test_poisson.py index bd425170a..588aa2aa1 100644 --- a/src/struphy/propagators/tests/test_poisson.py +++ b/src/struphy/propagators/tests/test_poisson.py @@ -267,7 +267,7 @@ def rho_pulled(e1, e2, e3): plt.xscale("log") plt.xlabel("Grid Spacing h") plt.ylabel("Error") - plt.title(f"Poisson solver") + plt.title("Poisson solver") plt.legend() if show_plot and rank == 0: diff --git a/tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb b/tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb index 922282da1..f50bfd8a7 100644 --- a/tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb +++ b/tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb @@ -913,14 +913,14 @@ "plt.pcolor(ee1[:,:,0], ee2[:,:,0], n_sph[:,:,0])\n", "plt.grid()\n", "plt.axis('square')\n", - "plt.title(f'n_sph initial (random)')\n", + "plt.title('n_sph initial (random)')\n", "plt.colorbar()\n", "\n", "plt.subplot(3, 2, 5)\n", "ax = plt.gca()\n", "plt.pcolor(bc_x, bc_y, f_bin)\n", "plt.axis('square')\n", - "plt.title(f'n_binned initial (random)')\n", + "plt.title('n_binned initial (random)')\n", "plt.colorbar()" ] }, diff --git a/tutorials_old/tutorial_02_fluid_particles.ipynb b/tutorials_old/tutorial_02_fluid_particles.ipynb index 49ccf724b..36ab63d1b 100644 --- a/tutorials_old/tutorial_02_fluid_particles.ipynb +++ b/tutorials_old/tutorial_02_fluid_particles.ipynb @@ -805,7 +805,7 @@ "plt.tick_params(labelbottom = False) \n", "plt.plot(eta1, n_sph_init[:, 0, 0])\n", "plt.grid()\n", - "plt.title(f'n_sph_init')\n", + "plt.title('n_sph_init')\n", "\n", "plt.subplot(2, 2, 4)\n", "ax = plt.gca()\n", @@ -815,7 +815,7 @@ "bc_x = (be_x[:-1] + be_x[1:]) / 2. # centers of binning cells\n", "plt.plot(bc_x, df_bin.T)\n", "#plt.grid()\n", - "plt.title(f'n_binned')" + "plt.title('n_binned')" ] }, { @@ -1209,7 +1209,7 @@ "plt.pcolor(ee1[:,:,0], ee2[:,:,0], n_sph_1[:,:,0])\n", "plt.grid()\n", "plt.axis('square')\n", - "plt.title(f'n_sph (random)')\n", + "plt.title('n_sph (random)')\n", "plt.colorbar()\n", "\n", "plt.subplot(4, 2, 6)\n", @@ -1220,7 +1220,7 @@ "plt.pcolor(ee1[:,:,0], ee2[:,:,0], n_sph_2[:,:,0])\n", "plt.grid()\n", "plt.axis('square')\n", - "plt.title(f'n_sph (tesselation)')\n", + "plt.title('n_sph (tesselation)')\n", "plt.colorbar()\n", "\n", "plt.subplot(4, 2, 7)\n", @@ -1233,7 +1233,7 @@ "plt.pcolor(bc_x, bc_y, f_bin_1)\n", "#plt.grid()\n", "plt.axis('square')\n", - "plt.title(f'n_binned (random)')\n", + "plt.title('n_binned (random)')\n", "plt.colorbar()\n", "\n", "plt.subplot(4, 2, 8)\n", @@ -1246,7 +1246,7 @@ "plt.pcolor(bc_x, bc_y, f_bin_2)\n", "#plt.grid()\n", "plt.axis('square')\n", - "plt.title(f'n_binned (tesselation)')\n", + "plt.title('n_binned (tesselation)')\n", "plt.colorbar()" ] }, From bdd43080c2199dbd85bb9dc536674e75924e99bd Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 30 Oct 2025 17:41:15 +0100 Subject: [PATCH 11/83] Add formatting check of the notebooks (#76) Redo of https://github.com/struphy-hub/struphy/pull/64 (with a few extra edits of the CI) **Solves the following issue(s):** Closes #54 - Formatted all the `.ipynb` files in the repo - Added the notebooks to the formatting checks in the CI --------- Co-authored-by: Stefan Possanner Co-authored-by: Byung Kyu Na Co-authored-by: Stefan Possanner <86720346+spossann@users.noreply.github.com> --- .../install-struphy-editable/action.yml | 2 +- .../install/install-struphy/action.yml | 11 +- .github/actions/tests/models/action.yml | 21 +- .github/actions/tests/quickstart/action.yml | 2 +- .github/actions/tests/unit/action.yml | 12 + .github/workflows/static_analysis.yml | 11 +- .github/workflows/testing.yml | 12 +- doc/api/1d_matrices.ipynb | 172 +++-- doc/api/disp_rels.ipynb | 11 +- doc/api/stability.ipynb | 36 +- pyproject.toml | 12 + src/struphy/console/format.py | 6 +- src/struphy/diagnostics/diagnostics_pic.ipynb | 86 +-- src/struphy/models/hybrid.py | 48 +- tutorial_07_data_structures.ipynb | 274 +++---- tutorials/tutorial_01_parameter_files.ipynb | 79 +- tutorials/tutorial_02_test_particles.ipynb | 434 +++++------ ...l_03_smoothed_particle_hydrodynamics.ipynb | 314 ++++---- tutorials/tutorial_04_vlasov_maxwell.ipynb | 84 +-- tutorials/tutorial_05_mapped_domains.ipynb | 24 +- tutorials/tutorial_06_mhd_equilibria.ipynb | 11 +- .../tutorial_01_kinetic_particles.ipynb | 560 +++++++------- .../tutorial_01_parameter_files.ipynb | 67 +- tutorials_old/tutorial_01_particles.ipynb | 66 +- .../tutorial_02_fluid_particles.ipynb | 697 +++++++++--------- .../tutorial_03_discrete_derham.ipynb | 148 ++-- tutorials_old/tutorial_06_poisson.ipynb | 35 +- tutorials_old/tutorial_07_heat_equation.ipynb | 143 ++-- tutorials_old/tutorial_08_maxwell.ipynb | 115 +-- .../tutorial_09_vlasov_maxwell.ipynb | 163 ++-- tutorials_old/tutorial_10_linear_mhd.ipynb | 226 +++--- .../tutorial_12_struphy_data_pproc.ipynb | 82 ++- utils/set_release_dependencies.py | 12 +- 33 files changed, 2036 insertions(+), 1940 deletions(-) diff --git a/.github/actions/install/install-struphy-editable/action.yml b/.github/actions/install/install-struphy-editable/action.yml index 49d871e15..f395d5caa 100644 --- a/.github/actions/install/install-struphy-editable/action.yml +++ b/.github/actions/install/install-struphy-editable/action.yml @@ -9,6 +9,6 @@ runs: run: | pip install --upgrade pip pip uninstall -y gvec - pip install -e ".[dev,mpi]" + pip install -e ".[dev,mpi,doc]" pip list struphy -h diff --git a/.github/actions/install/install-struphy/action.yml b/.github/actions/install/install-struphy/action.yml index bce677589..a9589c4c4 100644 --- a/.github/actions/install/install-struphy/action.yml +++ b/.github/actions/install/install-struphy/action.yml @@ -7,15 +7,8 @@ runs: - name: Install struphy shell: bash run: | - echo $FC - echo $CC - echo $CXX - echo "----------------" - which gfortran - which gcc - which g++ pip install --upgrade pip - pip uninstall -y gvec - pip install ".[phys,mpi]" + pip install ".[phys,mpi,doc]" pip list struphy -h + struphy --refresh-models diff --git a/.github/actions/tests/models/action.yml b/.github/actions/tests/models/action.yml index 0a9fafc16..435276caa 100644 --- a/.github/actions/tests/models/action.yml +++ b/.github/actions/tests/models/action.yml @@ -3,14 +3,21 @@ name: "Run model tests" runs: using: composite steps: - - name: Install dependencies + - name: Model tests shell: bash run: | struphy compile --status + struphy test LinearMHD + struphy test toy + struphy test models + struphy test verification + - name: Model tests with MPI + shell: bash + run: | struphy compile --status - struphy test models --fast - struphy test models --fast --mpi 2 - struphy test models --fast --verification --mpi 1 - struphy test models --fast --verification --mpi 4 - struphy test models --fast --verification --mpi 4 --nclones 2 - struphy test DriftKineticElectrostaticAdiabatic --mpi 2 --nclones 2 \ No newline at end of file + struphy test models + struphy test models --mpi 2 + struphy test verification --mpi 1 + struphy test verification --mpi 4 + struphy test verification --mpi 4 --nclones 2 + struphy test VlasovAmpereOneSpecies --mpi 2 --nclones 2 diff --git a/.github/actions/tests/quickstart/action.yml b/.github/actions/tests/quickstart/action.yml index b78ade5e7..3845360d3 100644 --- a/.github/actions/tests/quickstart/action.yml +++ b/.github/actions/tests/quickstart/action.yml @@ -8,7 +8,7 @@ runs: run: | struphy -p struphy -h - struphy params VlasovAmpereOneSpecies -y + struphy params VlasovAmpereOneSpecies ls -1a mv params_VlasovAmpereOneSpecies.py test.py python3 test.py diff --git a/.github/actions/tests/unit/action.yml b/.github/actions/tests/unit/action.yml index 2d0e70a2d..385d6f419 100644 --- a/.github/actions/tests/unit/action.yml +++ b/.github/actions/tests/unit/action.yml @@ -3,7 +3,19 @@ name: "Run unit tests" runs: using: composite steps: + - name: Run unit tests with MPI + shell: bash + run: | + struphy compile --status + struphy --refresh-models + struphy test unit --mpi 2 + - name: Run unit tests shell: bash run: | + struphy compile --status + struphy --refresh-models + pip show mpi4py + pip uninstall -y mpi4py + pip list struphy test unit diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 85b7857ec..3022a70c2 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -89,7 +89,7 @@ jobs: run: | pip install isort isort --check src/ - isort --check doc/tutorials/ + isort --check tutorials/ # mypy: # runs-on: ubuntu-latest @@ -121,10 +121,15 @@ jobs: - name: Checkout the code uses: actions/checkout@v4 - - name: Linting with ruff + # TODO: Remove --select I once all errors are fixed + - name: ruff check --select I run: | pip install ruff - ruff check --select I src/**/*.py + ruff check --select I + + - name: ruff format --check + run: | + ruff format --check # pylint: # runs-on: ubuntu-latest diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 6cfd047fa..c27af6914 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -18,7 +18,7 @@ jobs: matrix: python-version: ["3.12"] compile-language: ["fortran", "c"] - test-type: ["unit", "model"] #, "quickstart"] + test-type: ["unit", "model", "quickstart", "tutorials"] steps: # Checkout the repository @@ -86,6 +86,10 @@ jobs: if: matrix.test-type == 'model' uses: ./.github/actions/tests/models - #- name: Run quickstart tests - # if: matrix.test-type == 'quickstart' - # uses: ./.github/actions/tests/quickstart + - name: Run quickstart tests + if: matrix.test-type == 'quickstart' + uses: ./.github/actions/tests/quickstart + + - name: Run tutorials + if: matrix.test-type == 'tutorials' + uses: ./.github/actions/tests/tutorials diff --git a/doc/api/1d_matrices.ipynb b/doc/api/1d_matrices.ipynb index 960e25001..f81f5fde0 100644 --- a/doc/api/1d_matrices.ipynb +++ b/doc/api/1d_matrices.ipynb @@ -15,13 +15,12 @@ "outputs": [], "source": [ "import numpy as np\n", - "\n", - "from struphy.feec.psydac_derham import Derham\n", - "from struphy.feec.mass import WeightedMassOperator\n", - "\n", + "from psydac.core.bsplines import collocation_matrix, histopolation_matrix\n", "from psydac.ddm.cart import DomainDecomposition\n", "from psydac.fem.tensor import TensorFemSpace\n", - "from psydac.core.bsplines import collocation_matrix, histopolation_matrix" + "\n", + "from struphy.feec.mass import WeightedMassOperator\n", + "from struphy.feec.psydac_derham import Derham" ] }, { @@ -31,9 +30,9 @@ "outputs": [], "source": [ "# instance of Derham\n", - "Nel = [12, 12, 12] # Number of grid cells\n", - "p = [3, 4, 5] # spline degrees\n", - "spl_kind = [True, True, True] # Spline types (clamped vs. periodic)\n", + "Nel = [12, 12, 12] # Number of grid cells\n", + "p = [3, 4, 5] # spline degrees\n", + "spl_kind = [True, True, True] # Spline types (clamped vs. periodic)\n", "\n", "derham = Derham(Nel, p, spl_kind)" ] @@ -54,34 +53,34 @@ "source": [ "# 1d fem spaces\n", "\n", - "V0_fem = derham.Vh_fem['0'].spaces\n", - "V3_fem = derham.Vh_fem['3'].spaces\n", + "V0_fem = derham.Vh_fem[\"0\"].spaces\n", + "V3_fem = derham.Vh_fem[\"3\"].spaces\n", "\n", "for l, (V0_1d, V3_1d) in enumerate(zip(V0_fem, V3_fem)):\n", - " print(f'Direction {l + 1}')\n", - " \n", - " print('\\nH1 p: ', V0_1d.degree)\n", - " print('L2 p: ', V3_1d.degree)\n", - " \n", - " print('\\nH1 knots: ', V0_1d.knots)\n", - " print('L2 knots: ', V3_1d.knots)\n", - " \n", - " print('\\nH1 basis: ', V0_1d.basis)\n", - " print('L2 basis: ', V3_1d.basis)\n", - " \n", - " print('\\nH1 nbasis: ', V0_1d.nbasis)\n", - " print('L2 nbasis: ', V3_1d.nbasis)\n", - " \n", - " print('\\nH1 breaks: ', V0_1d.breaks)\n", - " print('L2 breaks: ', V3_1d.breaks)\n", - " \n", - " print('\\nH1 greville: ', V0_1d.greville)\n", - " print('L2 greville: ', V3_1d.greville)\n", - " \n", - " print('\\nH1 ext_greville: ', V0_1d.ext_greville)\n", - " print('L2 ext_greville: ', V3_1d.ext_greville)\n", - "\n", - " print('\\n---------------------------------------')" + " print(f\"Direction {l + 1}\")\n", + "\n", + " print(\"\\nH1 p: \", V0_1d.degree)\n", + " print(\"L2 p: \", V3_1d.degree)\n", + "\n", + " print(\"\\nH1 knots: \", V0_1d.knots)\n", + " print(\"L2 knots: \", V3_1d.knots)\n", + "\n", + " print(\"\\nH1 basis: \", V0_1d.basis)\n", + " print(\"L2 basis: \", V3_1d.basis)\n", + "\n", + " print(\"\\nH1 nbasis: \", V0_1d.nbasis)\n", + " print(\"L2 nbasis: \", V3_1d.nbasis)\n", + "\n", + " print(\"\\nH1 breaks: \", V0_1d.breaks)\n", + " print(\"L2 breaks: \", V3_1d.breaks)\n", + "\n", + " print(\"\\nH1 greville: \", V0_1d.greville)\n", + " print(\"L2 greville: \", V3_1d.greville)\n", + "\n", + " print(\"\\nH1 ext_greville: \", V0_1d.ext_greville)\n", + " print(\"L2 ext_greville: \", V3_1d.ext_greville)\n", + "\n", + " print(\"\\n---------------------------------------\")" ] }, { @@ -115,33 +114,30 @@ "# 1d mass matrices in H1 (no weight)\n", "mass_H1_1d = []\n", "for femspace_1d in V0_fem:\n", - "\n", " domain_decompos_1d = DomainDecomposition([femspace_1d.ncells], [femspace_1d.periodic])\n", " femspace_1d_tensor = TensorFemSpace(domain_decompos_1d, femspace_1d)\n", "\n", " M = WeightedMassOperator(derham, femspace_1d_tensor, femspace_1d_tensor, nquads=[femspace_1d.degree])\n", " M.assemble(verbose=False)\n", " M.matrix.exchange_assembly_data()\n", - " \n", + "\n", " mass_H1_1d += [M.matrix.toarray()]\n", "\n", "# 1d mass matrices in L2 (no weight)\n", "mass_L2_1d = []\n", "for femspace_1d in V3_fem:\n", - "\n", " domain_decompos_1d = DomainDecomposition([femspace_1d.ncells], [femspace_1d.periodic])\n", " femspace_1d_tensor = TensorFemSpace(domain_decompos_1d, femspace_1d)\n", "\n", " M = WeightedMassOperator(derham, femspace_1d_tensor, femspace_1d_tensor, nquads=[femspace_1d.degree])\n", " M.assemble(verbose=False)\n", " M.matrix.exchange_assembly_data()\n", - " \n", + "\n", " mass_L2_1d += [M.matrix.toarray()]\n", - " \n", + "\n", "# 1d mixed mass matrices: V0 -> V3\n", "mass_mixed_1d = []\n", "for V0_1d, V3_1d in zip(V0_fem, V3_fem):\n", - "\n", " domain_decompos_1d = DomainDecomposition([V0_1d.ncells], [V0_1d.periodic])\n", " V0_femspace = TensorFemSpace(domain_decompos_1d, V0_1d)\n", " V3_femspace = TensorFemSpace(domain_decompos_1d, V3_1d)\n", @@ -149,7 +145,7 @@ " M = WeightedMassOperator(derham, V0_femspace, V3_femspace, nquads=[V0_1d.degree])\n", " M.assemble(verbose=False)\n", " M.matrix.exchange_assembly_data()\n", - " \n", + "\n", " mass_mixed_1d += [M.matrix.toarray()]" ] }, @@ -159,17 +155,17 @@ "metadata": {}, "outputs": [], "source": [ - "print('Sorted eigenvalues of H1 mass matrices in 1d:')\n", + "print(\"Sorted eigenvalues of H1 mass matrices in 1d:\")\n", "for deg, M in zip(p, mass_H1_1d):\n", - " print(f'\\np={deg}:\\n', np.sort(np.linalg.eigvals(M)))\n", + " print(f\"\\np={deg}:\\n\", np.sort(np.linalg.eigvals(M)))\n", "\n", - "print('\\nSorted eigenvalues of L2 mass matrices in 1d:')\n", + "print(\"\\nSorted eigenvalues of L2 mass matrices in 1d:\")\n", "for deg, M in zip(p, mass_L2_1d):\n", - " print(f'\\np={deg}:\\n', np.sort(np.linalg.eigvals(M)))\n", - " \n", - "print('\\nSorted eigenvalues (abs) of mixed mass matrices in 1d:')\n", + " print(f\"\\np={deg}:\\n\", np.sort(np.linalg.eigvals(M)))\n", + "\n", + "print(\"\\nSorted eigenvalues (abs) of mixed mass matrices in 1d:\")\n", "for deg, M in zip(p, mass_mixed_1d):\n", - " print(f'\\np={deg}:\\n', np.sort(np.abs(np.linalg.eigvals(M))))" + " print(f\"\\np={deg}:\\n\", np.sort(np.abs(np.linalg.eigvals(M))))" ] }, { @@ -178,17 +174,17 @@ "metadata": {}, "outputs": [], "source": [ - "print('First row of circulant H1 mass matrices in 1d:')\n", + "print(\"First row of circulant H1 mass matrices in 1d:\")\n", "for deg, M in zip(p, mass_H1_1d):\n", - " print(f'\\np={deg}:\\n', M[0])\n", + " print(f\"\\np={deg}:\\n\", M[0])\n", "\n", - "print('\\nFirst row of circulant L2 mass matrices in 1d:')\n", + "print(\"\\nFirst row of circulant L2 mass matrices in 1d:\")\n", "for deg, M in zip(p, mass_L2_1d):\n", - " print(f'\\np={deg}:\\n', M[0])\n", - " \n", - "print('\\nFirst row of circulant mixed mass matrices in 1d:')\n", + " print(f\"\\np={deg}:\\n\", M[0])\n", + "\n", + "print(\"\\nFirst row of circulant mixed mass matrices in 1d:\")\n", "for deg, M in zip(p, mass_mixed_1d):\n", - " print(f'\\np={deg}:\\n', M[0])" + " print(f\"\\np={deg}:\\n\", M[0])" ] }, { @@ -212,8 +208,8 @@ "# 1d Inter-/histopolation matrices\n", "\n", "# Commuting projectors\n", - "P0 = derham.P['0']\n", - "P3 = derham.P['3']\n", + "P0 = derham.P[\"0\"]\n", + "P3 = derham.P[\"3\"]\n", "\n", "# 1d collocation matrices\n", "colloc_H1_1d = []\n", @@ -232,13 +228,13 @@ "metadata": {}, "outputs": [], "source": [ - "print('Sorted eigenvalues of H1 collocation matrices in 1d:')\n", + "print(\"Sorted eigenvalues of H1 collocation matrices in 1d:\")\n", "for deg, M in zip(p, colloc_H1_1d):\n", - " print(f'\\np={deg}:\\n', np.sort(np.abs(np.linalg.eigvals(M))))\n", + " print(f\"\\np={deg}:\\n\", np.sort(np.abs(np.linalg.eigvals(M))))\n", "\n", - "print('\\nSorted eigenvalues of L2 histopolation matrices in 1d:')\n", + "print(\"\\nSorted eigenvalues of L2 histopolation matrices in 1d:\")\n", "for deg, M in zip(p, histop_L2_1d):\n", - " print(f'\\np={deg}:\\n', np.sort(np.abs(np.linalg.eigvals(M))))" + " print(f\"\\np={deg}:\\n\", np.sort(np.abs(np.linalg.eigvals(M))))" ] }, { @@ -247,13 +243,13 @@ "metadata": {}, "outputs": [], "source": [ - "print('First row of circulant H1 collocation matrices in 1d:')\n", + "print(\"First row of circulant H1 collocation matrices in 1d:\")\n", "for deg, M in zip(p, colloc_H1_1d):\n", - " print(f'\\np={deg}:\\n', M[0])\n", + " print(f\"\\np={deg}:\\n\", M[0])\n", "\n", - "print('\\nFirst row of circulant L2 histopolation matrices in 1d:')\n", + "print(\"\\nFirst row of circulant L2 histopolation matrices in 1d:\")\n", "for deg, M in zip(p, histop_L2_1d):\n", - " print(f'\\np={deg}:\\n', M[0])" + " print(f\"\\np={deg}:\\n\", M[0])" ] }, { @@ -279,29 +275,27 @@ "# histopolation in H1\n", "histop_H1_1d = []\n", "for femspace_1d in V0_fem:\n", - " \n", " hmat = histopolation_matrix(\n", - " knots = femspace_1d.knots,\n", - " degree = femspace_1d.degree,\n", - " periodic = femspace_1d.periodic,\n", - " normalization = femspace_1d.basis,\n", - " xgrid = femspace_1d.greville\n", - " )\n", - " \n", + " knots=femspace_1d.knots,\n", + " degree=femspace_1d.degree,\n", + " periodic=femspace_1d.periodic,\n", + " normalization=femspace_1d.basis,\n", + " xgrid=femspace_1d.greville,\n", + " )\n", + "\n", " histop_H1_1d += [hmat]\n", "\n", "# interpolation in L2\n", "colloc_L2_1d = []\n", "for femspace_1d in V3_fem:\n", - " \n", " imat = collocation_matrix(\n", - " knots = femspace_1d.knots,\n", - " degree = femspace_1d.degree,\n", - " periodic = femspace_1d.periodic,\n", - " normalization = femspace_1d.basis,\n", - " xgrid = femspace_1d.ext_greville\n", - " )\n", - " \n", + " knots=femspace_1d.knots,\n", + " degree=femspace_1d.degree,\n", + " periodic=femspace_1d.periodic,\n", + " normalization=femspace_1d.basis,\n", + " xgrid=femspace_1d.ext_greville,\n", + " )\n", + "\n", " colloc_L2_1d += [imat]" ] }, @@ -311,13 +305,13 @@ "metadata": {}, "outputs": [], "source": [ - "print('Sorted eigenvalues of H1 histopolation matrices in 1d:')\n", + "print(\"Sorted eigenvalues of H1 histopolation matrices in 1d:\")\n", "for deg, M in zip(p, histop_H1_1d):\n", - " print(f'\\np={deg}:\\n', np.sort(np.abs(np.linalg.eigvals(M))))\n", + " print(f\"\\np={deg}:\\n\", np.sort(np.abs(np.linalg.eigvals(M))))\n", "\n", - "print('\\nSorted eigenvalues of L2 collocation matrices in 1d:')\n", + "print(\"\\nSorted eigenvalues of L2 collocation matrices in 1d:\")\n", "for deg, M in zip(p, colloc_L2_1d):\n", - " print(f'\\np={deg}:\\n', np.sort(np.abs(np.linalg.eigvals(M))))" + " print(f\"\\np={deg}:\\n\", np.sort(np.abs(np.linalg.eigvals(M))))" ] }, { @@ -326,13 +320,13 @@ "metadata": {}, "outputs": [], "source": [ - "print('First row of circulant H1 histopolation matrices in 1d:')\n", + "print(\"First row of circulant H1 histopolation matrices in 1d:\")\n", "for deg, M in zip(p, histop_H1_1d):\n", - " print(f'\\np={deg}:\\n', M[0])\n", + " print(f\"\\np={deg}:\\n\", M[0])\n", "\n", - "print('\\nFirst row of circulant L2 collocation matrices in 1d:')\n", + "print(\"\\nFirst row of circulant L2 collocation matrices in 1d:\")\n", "for deg, M in zip(p, colloc_L2_1d):\n", - " print(f'\\np={deg}:\\n', M[0])" + " print(f\"\\np={deg}:\\n\", M[0])" ] } ], diff --git a/doc/api/disp_rels.ipynb b/doc/api/disp_rels.ipynb index 1061ea011..a1987e86e 100644 --- a/doc/api/disp_rels.ipynb +++ b/doc/api/disp_rels.ipynb @@ -15,10 +15,11 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.dispersion_relations import analytic\n", "import numpy as np\n", "\n", - "k = np.linspace(0, .3, 100)" + "from struphy.dispersion_relations import analytic\n", + "\n", + "k = np.linspace(0, 0.3, 100)" ] }, { @@ -34,7 +35,7 @@ "metadata": {}, "outputs": [], "source": [ - "disp_rel = analytic.Maxwell1D(c=2.)\n", + "disp_rel = analytic.Maxwell1D(c=2.0)\n", "disp_rel.plot(k)" ] }, @@ -68,7 +69,7 @@ "metadata": {}, "outputs": [], "source": [ - "disp_rel = analytic.ExtendedMHDhomogenSlab(eps=1.)\n", + "disp_rel = analytic.ExtendedMHDhomogenSlab(eps=1.0)\n", "disp_rel.plot(k)" ] }, @@ -85,7 +86,7 @@ "metadata": {}, "outputs": [], "source": [ - "disp_rel = analytic.FluidSlabITG(vstar=.1)\n", + "disp_rel = analytic.FluidSlabITG(vstar=0.1)\n", "disp_rel.plot(k)" ] }, diff --git a/doc/api/stability.ipynb b/doc/api/stability.ipynb index 802072b03..9df11aad2 100644 --- a/doc/api/stability.ipynb +++ b/doc/api/stability.ipynb @@ -25,12 +25,12 @@ "source": [ "# add polynomial coeffs of methods here\n", "methods = {}\n", - "methods['explicit Euler'] = {'p': [1, 1], 'q': [0, 1]}\n", - "methods['explicit RK 2'] = {'p': [1/2, 1, 1], 'q': [0, 0, 1]}\n", - "methods['explicit RK 3'] = {'p': [1/6, 1/2, 1, 1], 'q': [0, 0, 0, 1]}\n", - "methods['explicit RK 4'] = {'p': [1/24, 1/6, 1/2, 1, 1], 'q': [0, 0, 0, 0, 1]}\n", - "methods['implicit Euler'] = {'p': [0, 1], 'q': [-1, 1]}\n", - "methods['Crank Nicolson'] = {'p': [1/2, 1], 'q': [-1/2, 1]}" + "methods[\"explicit Euler\"] = {\"p\": [1, 1], \"q\": [0, 1]}\n", + "methods[\"explicit RK 2\"] = {\"p\": [1 / 2, 1, 1], \"q\": [0, 0, 1]}\n", + "methods[\"explicit RK 3\"] = {\"p\": [1 / 6, 1 / 2, 1, 1], \"q\": [0, 0, 0, 1]}\n", + "methods[\"explicit RK 4\"] = {\"p\": [1 / 24, 1 / 6, 1 / 2, 1, 1], \"q\": [0, 0, 0, 0, 1]}\n", + "methods[\"implicit Euler\"] = {\"p\": [0, 1], \"q\": [-1, 1]}\n", + "methods[\"Crank Nicolson\"] = {\"p\": [1 / 2, 1], \"q\": [-1 / 2, 1]}" ] }, { @@ -41,11 +41,11 @@ "source": [ "# plotting parameters\n", "N = 400\n", - "bound = 3.\n", + "bound = 3.0\n", "x = np.linspace(-bound, bound, N)\n", "y = np.linspace(-bound, bound, N)\n", - "xx, yy = np.meshgrid(x, y, indexing='ij')\n", - "zz = xx + yy*1j" + "xx, yy = np.meshgrid(x, y, indexing=\"ij\")\n", + "zz = xx + yy * 1j" ] }, { @@ -56,19 +56,19 @@ "source": [ "# plot stability region for all methods\n", "for key, val in methods.items():\n", - " p = np.poly1d(val['p'])\n", - " q = np.poly1d(val['q'])\n", + " p = np.poly1d(val[\"p\"])\n", + " q = np.poly1d(val[\"q\"])\n", "\n", - " rr = np.abs(p(zz)/q(zz))\n", - " rr[rr>1.] = -1\n", + " rr = np.abs(p(zz) / q(zz))\n", + " rr[rr > 1.0] = -1\n", "\n", " fig = plt.figure(figsize=(5, 5))\n", - " plt.contourf(xx, yy, rr, [0,1], colors='b')\n", - " plt.plot([-bound, bound], [0, 0], '--k')\n", - " plt.plot([0, 0], [-bound, bound], '--k')\n", + " plt.contourf(xx, yy, rr, [0, 1], colors=\"b\")\n", + " plt.plot([-bound, bound], [0, 0], \"--k\")\n", + " plt.plot([0, 0], [-bound, bound], \"--k\")\n", " plt.title(key)\n", - " plt.xlabel('Re($z$)')\n", - " plt.ylabel('Im($z$)')\n", + " plt.xlabel(\"Re($z$)\")\n", + " plt.ylabel(\"Im($z$)\")\n", " plt.show()" ] } diff --git a/pyproject.toml b/pyproject.toml index 82c2e2605..7e82a720e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -154,6 +154,7 @@ max-line-length = 120 [tool.ruff] line-length = 120 +# exclude = ["__pyccel__"] [tool.ruff.lint] ignore = [ @@ -171,6 +172,17 @@ ignore = [ "F405", "D211", "D213", + "F841", # Ignore unused variables +] + +[tool.pytest.ini_options] +markers = [ + "models", + "toy", + "fluid", + "kinetic", + "hybrid", + "single", ] [tool.pytest.ini_options] diff --git a/src/struphy/console/format.py b/src/struphy/console/format.py index eec22f784..7ba6795c4 100644 --- a/src/struphy/console/format.py +++ b/src/struphy/console/format.py @@ -409,7 +409,7 @@ def parse_path(directory): for filename in files: if re.search(r"__\w+__", root): continue - if filename.endswith(".py") and not re.search(r"__\w+__", filename): + if (filename.endswith(".py") or filename.endswith(".ipynb")) and not re.search(r"__\w+__", filename): file_path = os.path.join(root, filename) python_files.append(file_path) # exit() @@ -484,7 +484,9 @@ def get_python_files(input_type, path=None): # python_files = [f for f in files if f.endswith(".py") and os.path.isfile(f)] python_files = [ - os.path.join(repopath, f) for f in files if f.endswith(".py") and os.path.isfile(os.path.join(repopath, f)) + os.path.join(repopath, f) + for f in files + if (f.endswith(".py") or f.endswith(".ipynb")) and os.path.isfile(os.path.join(repopath, f)) ] if not python_files: diff --git a/src/struphy/diagnostics/diagnostics_pic.ipynb b/src/struphy/diagnostics/diagnostics_pic.ipynb index d4b2f2e0f..f41425141 100644 --- a/src/struphy/diagnostics/diagnostics_pic.ipynb +++ b/src/struphy/diagnostics/diagnostics_pic.ipynb @@ -7,11 +7,13 @@ "outputs": [], "source": [ "import os\n", - "import struphy\n", + "\n", "import numpy as np\n", "from matplotlib import pyplot as plt\n", "\n", - "path_out = os.path.join(struphy.__path__[0], 'io/out', 'sim_1')\n", + "import struphy\n", + "\n", + "path_out = os.path.join(struphy.__path__[0], \"io/out\", \"sim_1\")\n", "\n", "print(path_out)\n", "os.listdir(path_out)" @@ -28,7 +30,7 @@ "metadata": {}, "outputs": [], "source": [ - "data_path = os.path.join(path_out, 'post_processing')\n", + "data_path = os.path.join(path_out, \"post_processing\")\n", "\n", "os.listdir(data_path)" ] @@ -39,7 +41,7 @@ "metadata": {}, "outputs": [], "source": [ - "t_grid = np.load(os.path.join(data_path, 't_grid.npy'))\n", + "t_grid = np.load(os.path.join(data_path, \"t_grid.npy\"))\n", "t_grid" ] }, @@ -49,7 +51,7 @@ "metadata": {}, "outputs": [], "source": [ - "f_path = os.path.join(data_path, 'kinetic_data', 'ions', 'distribution_function')\n", + "f_path = os.path.join(data_path, \"kinetic_data\", \"ions\", \"distribution_function\")\n", "\n", "print(os.listdir(f_path))" ] @@ -60,7 +62,7 @@ "metadata": {}, "outputs": [], "source": [ - "path = os.path.join(f_path, 'e1')\n", + "path = os.path.join(f_path, \"e1\")\n", "print(os.listdir(path))" ] }, @@ -70,9 +72,9 @@ "metadata": {}, "outputs": [], "source": [ - "grid = np.load(os.path.join(f_path, 'e1/', 'grid_e1.npy'))\n", - "f_binned = np.load(os.path.join(f_path, 'e1/', 'f_binned.npy'))\n", - "delta_f_e1_binned = np.load(os.path.join(f_path, 'e1/', 'delta_f_binned.npy'))\n", + "grid = np.load(os.path.join(f_path, \"e1/\", \"grid_e1.npy\"))\n", + "f_binned = np.load(os.path.join(f_path, \"e1/\", \"f_binned.npy\"))\n", + "delta_f_e1_binned = np.load(os.path.join(f_path, \"e1/\", \"delta_f_binned.npy\"))\n", "\n", "print(grid.shape)\n", "print(f_binned.shape)\n", @@ -87,18 +89,18 @@ "source": [ "steps = list(np.arange(10))\n", "\n", - "plt.figure(figsize=(12, 5*len(steps)))\n", + "plt.figure(figsize=(12, 5 * len(steps)))\n", "for n, step in enumerate(steps):\n", - " plt.subplot(len(steps), 2, 2*n + 1)\n", - " plt.plot(grid, f_binned[step], label=f'time = {t_grid[step]}')\n", - " plt.xlabel('e1')\n", - " #plt.ylim([.5, 1.5])\n", - " plt.title('full-f')\n", - " plt.subplot(len(steps), 2, 2*n + 2)\n", - " plt.plot(grid, delta_f_e1_binned[step], label=f'time = {t_grid[step]}')\n", - " plt.xlabel('e1')\n", - " #plt.ylim([-3e-3, 3e-3])\n", - " plt.title(r'$\\delta f$')\n", + " plt.subplot(len(steps), 2, 2 * n + 1)\n", + " plt.plot(grid, f_binned[step], label=f\"time = {t_grid[step]}\")\n", + " plt.xlabel(\"e1\")\n", + " # plt.ylim([.5, 1.5])\n", + " plt.title(\"full-f\")\n", + " plt.subplot(len(steps), 2, 2 * n + 2)\n", + " plt.plot(grid, delta_f_e1_binned[step], label=f\"time = {t_grid[step]}\")\n", + " plt.xlabel(\"e1\")\n", + " # plt.ylim([-3e-3, 3e-3])\n", + " plt.title(r\"$\\delta f$\")\n", " plt.legend()" ] }, @@ -108,7 +110,7 @@ "metadata": {}, "outputs": [], "source": [ - "path = os.path.join(f_path, 'e1_v1')\n", + "path = os.path.join(f_path, \"e1_v1\")\n", "print(os.listdir(path))" ] }, @@ -118,10 +120,10 @@ "metadata": {}, "outputs": [], "source": [ - "grid_e1 = np.load(os.path.join(f_path, 'e1_v1/', 'grid_e1.npy'))\n", - "grid_v1 = np.load(os.path.join(f_path, 'e1_v1/', 'grid_v1.npy'))\n", - "f_binned = np.load(os.path.join(f_path, 'e1_v1/', 'f_binned.npy'))\n", - "delta_f_binned = np.load(os.path.join(f_path, 'e1_v1/', 'delta_f_binned.npy'))\n", + "grid_e1 = np.load(os.path.join(f_path, \"e1_v1/\", \"grid_e1.npy\"))\n", + "grid_v1 = np.load(os.path.join(f_path, \"e1_v1/\", \"grid_v1.npy\"))\n", + "f_binned = np.load(os.path.join(f_path, \"e1_v1/\", \"f_binned.npy\"))\n", + "delta_f_binned = np.load(os.path.join(f_path, \"e1_v1/\", \"delta_f_binned.npy\"))\n", "\n", "print(grid_e1.shape)\n", "print(grid_v1.shape)\n", @@ -137,20 +139,20 @@ "source": [ "steps = list(np.arange(10))\n", "\n", - "plt.figure(figsize=(12, 5*len(steps)))\n", + "plt.figure(figsize=(12, 5 * len(steps)))\n", "for n, step in enumerate(steps):\n", - " plt.subplot(len(steps), 2, 2*n + 1)\n", - " plt.pcolor(grid_e1, grid_v1, f_binned[step].T, label=f'time = {t_grid[step]}')\n", - " plt.xlabel('$e1$')\n", - " plt.ylabel(r'$v_\\parallel$')\n", - " plt.title('full-f')\n", + " plt.subplot(len(steps), 2, 2 * n + 1)\n", + " plt.pcolor(grid_e1, grid_v1, f_binned[step].T, label=f\"time = {t_grid[step]}\")\n", + " plt.xlabel(\"$e1$\")\n", + " plt.ylabel(r\"$v_\\parallel$\")\n", + " plt.title(\"full-f\")\n", " plt.legend()\n", " plt.colorbar()\n", - " plt.subplot(len(steps), 2, 2*n + 2)\n", - " plt.pcolor(grid_e1, grid_v1, delta_f_binned[step].T, label=f'time = {t_grid[step]}')\n", - " plt.xlabel('$e1$')\n", - " plt.ylabel(r'$v_\\parallel$')\n", - " plt.title(r'$\\delta f$')\n", + " plt.subplot(len(steps), 2, 2 * n + 2)\n", + " plt.pcolor(grid_e1, grid_v1, delta_f_binned[step].T, label=f\"time = {t_grid[step]}\")\n", + " plt.xlabel(\"$e1$\")\n", + " plt.ylabel(r\"$v_\\parallel$\")\n", + " plt.title(r\"$\\delta f$\")\n", " plt.legend()\n", " plt.colorbar()" ] @@ -161,7 +163,7 @@ "metadata": {}, "outputs": [], "source": [ - "fields_path = os.path.join(data_path, 'fields_data')\n", + "fields_path = os.path.join(data_path, \"fields_data\")\n", "\n", "print(os.listdir(fields_path))" ] @@ -174,7 +176,7 @@ "source": [ "import pickle\n", "\n", - "with open(os.path.join(fields_path, 'grids_phy.bin'), 'rb') as file:\n", + "with open(os.path.join(fields_path, \"grids_phy.bin\"), \"rb\") as file:\n", " x_grid, y_grid, z_grid = pickle.load(file)\n", "\n", "print(type(x_grid))\n", @@ -187,7 +189,7 @@ "metadata": {}, "outputs": [], "source": [ - "with open(os.path.join(fields_path, 'em_fields', 'phi_phy.bin'), 'rb') as file:\n", + "with open(os.path.join(fields_path, \"em_fields\", \"phi_phy.bin\"), \"rb\") as file:\n", " phi = pickle.load(file)\n", "\n", "plt.figure(figsize=(12, 12))\n", @@ -197,9 +199,9 @@ " t = t_grid[step]\n", " print(phi[t][0].shape)\n", " plt.subplot(2, 2, n + 1)\n", - " plt.plot(x_grid[:, 0, 0], phi[t][0][:, 0, 0], label=f'time = {t}')\n", - " plt.xlabel('x')\n", - " plt.ylabel(r'$\\phi$(x)')\n", + " plt.plot(x_grid[:, 0, 0], phi[t][0][:, 0, 0], label=f\"time = {t}\")\n", + " plt.xlabel(\"x\")\n", + " plt.ylabel(r\"$\\phi$(x)\")\n", " plt.legend()" ] }, diff --git a/src/struphy/models/hybrid.py b/src/struphy/models/hybrid.py index bcc9f6492..c1952f59c 100644 --- a/src/struphy/models/hybrid.py +++ b/src/struphy/models/hybrid.py @@ -319,15 +319,15 @@ def __init__(self): class Propagators: def __init__(self, turn_off: tuple[str, ...] = (None,)): - if not "PushEtaPC" in turn_off: + if "PushEtaPC" not in turn_off: self.push_eta_pc = propagators_markers.PushEtaPC() - if not "PushVxB" in turn_off: + if "PushVxB" not in turn_off: self.push_vxb = propagators_markers.PushVxB() - if not "PressureCoupling6D" in turn_off: + if "PressureCoupling6D" not in turn_off: self.pc6d = propagators_coupling.PressureCoupling6D() - if not "ShearAlfven" in turn_off: + if "ShearAlfven" not in turn_off: self.shearalfven = propagators_fields.ShearAlfven() - if not "Magnetosonic" in turn_off: + if "Magnetosonic" not in turn_off: self.magnetosonic = propagators_fields.Magnetosonic() def __init__(self, turn_off: tuple[str, ...] = (None,)): @@ -343,19 +343,19 @@ def __init__(self, turn_off: tuple[str, ...] = (None,)): self.propagators = self.Propagators(turn_off) # 3. assign variables to propagators - if not "ShearAlfven" in turn_off: + if "ShearAlfven" not in turn_off: self.propagators.shearalfven.variables.u = self.mhd.velocity self.propagators.shearalfven.variables.b = self.em_fields.b_field - if not "Magnetosonic" in turn_off: + if "Magnetosonic" not in turn_off: self.propagators.magnetosonic.variables.n = self.mhd.density self.propagators.magnetosonic.variables.u = self.mhd.velocity self.propagators.magnetosonic.variables.p = self.mhd.pressure - if not "PressureCoupling6D" in turn_off: + if "PressureCoupling6D" not in turn_off: self.propagators.pc6d.variables.u = self.mhd.velocity self.propagators.pc6d.variables.energetic_ions = self.energetic_ions.var - if not "PushEtaPC" in turn_off: + if "PushEtaPC" not in turn_off: self.propagators.push_eta_pc.variables.var = self.energetic_ions.var - if not "PushVxB" in turn_off: + if "PushVxB" not in turn_off: self.propagators.push_vxb.variables.ions = self.energetic_ions.var # define scalars for update_scalar_quantities @@ -584,19 +584,19 @@ def __init__(self): class Propagators: def __init__(self, turn_off: tuple[str, ...] = (None,)): - if not "PushGuidingCenterBxEstar" in turn_off: + if "PushGuidingCenterBxEstar" not in turn_off: self.push_bxe = propagators_markers.PushGuidingCenterBxEstar() - if not "PushGuidingCenterParallel" in turn_off: + if "PushGuidingCenterParallel" not in turn_off: self.push_parallel = propagators_markers.PushGuidingCenterParallel() - if not "ShearAlfvenCurrentCoupling5D" in turn_off: + if "ShearAlfvenCurrentCoupling5D" not in turn_off: self.shearalfen_cc5d = propagators_fields.ShearAlfvenCurrentCoupling5D() - if not "Magnetosonic" in turn_off: + if "Magnetosonic" not in turn_off: self.magnetosonic = propagators_fields.Magnetosonic() - if not "CurrentCoupling5DDensity" in turn_off: + if "CurrentCoupling5DDensity" not in turn_off: self.cc5d_density = propagators_fields.CurrentCoupling5DDensity() - if not "CurrentCoupling5DGradB" in turn_off: + if "CurrentCoupling5DGradB" not in turn_off: self.cc5d_gradb = propagators_coupling.CurrentCoupling5DGradB() - if not "CurrentCoupling5DCurlb" in turn_off: + if "CurrentCoupling5DCurlb" not in turn_off: self.cc5d_curlb = propagators_coupling.CurrentCoupling5DCurlb() def __init__(self, turn_off: tuple[str, ...] = (None,)): @@ -612,24 +612,24 @@ def __init__(self, turn_off: tuple[str, ...] = (None,)): self.propagators = self.Propagators(turn_off) # 3. assign variables to propagators - if not "ShearAlfvenCurrentCoupling5D" in turn_off: + if "ShearAlfvenCurrentCoupling5D" not in turn_off: self.propagators.shearalfen_cc5d.variables.u = self.mhd.velocity self.propagators.shearalfen_cc5d.variables.b = self.em_fields.b_field - if not "Magnetosonic" in turn_off: + if "Magnetosonic" not in turn_off: self.propagators.magnetosonic.variables.n = self.mhd.density self.propagators.magnetosonic.variables.u = self.mhd.velocity self.propagators.magnetosonic.variables.p = self.mhd.pressure - if not "CurrentCoupling5DDensity" in turn_off: + if "CurrentCoupling5DDensity" not in turn_off: self.propagators.cc5d_density.variables.u = self.mhd.velocity - if not "CurrentCoupling5DGradB" in turn_off: + if "CurrentCoupling5DGradB" not in turn_off: self.propagators.cc5d_gradb.variables.u = self.mhd.velocity self.propagators.cc5d_gradb.variables.energetic_ions = self.energetic_ions.var - if not "CurrentCoupling5DCurlb" in turn_off: + if "CurrentCoupling5DCurlb" not in turn_off: self.propagators.cc5d_curlb.variables.u = self.mhd.velocity self.propagators.cc5d_curlb.variables.energetic_ions = self.energetic_ions.var - if not "PushGuidingCenterBxEstar" in turn_off: + if "PushGuidingCenterBxEstar" not in turn_off: self.propagators.push_bxe.variables.ions = self.energetic_ions.var - if not "PushGuidingCenterParallel" in turn_off: + if "PushGuidingCenterParallel" not in turn_off: self.propagators.push_parallel.variables.ions = self.energetic_ions.var # define scalars for update_scalar_quantities diff --git a/tutorial_07_data_structures.ipynb b/tutorial_07_data_structures.ipynb index 62727c733..dc21d7332 100644 --- a/tutorial_07_data_structures.ipynb +++ b/tutorial_07_data_structures.ipynb @@ -29,9 +29,10 @@ "metadata": {}, "outputs": [], "source": [ + "import numpy as np\n", "from psydac.linalg.stencil import StencilVector\n", + "\n", "from struphy.feec.psydac_derham import Derham\n", - "import numpy as np\n", "\n", "Nel = [8, 8, 12] # number of elements\n", "p = [2, 3, 4] # spline degrees\n", @@ -42,9 +43,9 @@ "dr_serial = Derham(Nel, p, spl_kind)\n", "\n", "# element of V0_h\n", - "x0 = StencilVector(dr_serial.Vh['0'])\n", + "x0 = StencilVector(dr_serial.Vh[\"0\"])\n", "\n", - "assert np.all(x0[:] == 0.)" + "assert np.all(x0[:] == 0.0)" ] }, { @@ -60,10 +61,10 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{type(x0) = }')\n", - "print(f'{type(x0[:]) = }')\n", - "print(f'{type(x0[:, :, :]) = }')\n", - "print(f'{type(x0[:2, 1:2:7, :-1]) = }')" + "print(f\"{type(x0) = }\")\n", + "print(f\"{type(x0[:]) = }\")\n", + "print(f\"{type(x0[:, :, :]) = }\")\n", + "print(f\"{type(x0[:2, 1:2:7, :-1]) = }\")" ] }, { @@ -79,9 +80,9 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{x0[3, 2, 1] = }')\n", - "x0[3, 2, 1] = 99.\n", - "print(f'{x0[3, 2, 1] = }')" + "print(f\"{x0[3, 2, 1] = }\")\n", + "x0[3, 2, 1] = 99.0\n", + "print(f\"{x0[3, 2, 1] = }\")" ] }, { @@ -115,8 +116,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{x0.starts = }')\n", - "print(f'{x0.ends = }')" + "print(f\"{x0.starts = }\")\n", + "print(f\"{x0.ends = }\")" ] }, { @@ -132,9 +133,9 @@ "metadata": {}, "outputs": [], "source": [ - "dims = [Ni + pi*(not spi) for Ni, pi, spi in zip(Nel, p, spl_kind)]\n", - "print(f'{dims = }')\n", - "print(f'{dims[0]*dims[1]*dims[2] = }' + ' = total dimension of vector space')" + "dims = [Ni + pi * (not spi) for Ni, pi, spi in zip(Nel, p, spl_kind)]\n", + "print(f\"{dims = }\")\n", + "print(f\"{dims[0]*dims[1]*dims[2] = }\" + \" = total dimension of vector space\")" ] }, { @@ -152,9 +153,9 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{type(x0[:]) = }, {np.shape(x0[:]) = }')\n", - "print(f'{type(x0[:, :, :]) = }, {np.shape(x0[:, :, :]) = }')\n", - "print(f'{type(x0._data) = }, {np.shape(x0._data) = }')" + "print(f\"{type(x0[:]) = }, {np.shape(x0[:]) = }\")\n", + "print(f\"{type(x0[:, :, :]) = }, {np.shape(x0[:, :, :]) = }\")\n", + "print(f\"{type(x0._data) = }, {np.shape(x0._data) = }\")" ] }, { @@ -170,9 +171,9 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{id(x0[:]) = }')\n", - "print(f'{id(x0[:, :, :]) = }')\n", - "print(f'{id(x0._data) = }')" + "print(f\"{id(x0[:]) = }\")\n", + "print(f\"{id(x0[:, :, :]) = }\")\n", + "print(f\"{id(x0._data) = }\")" ] }, { @@ -188,8 +189,8 @@ "metadata": {}, "outputs": [], "source": [ - "shape = [dim + 2*pi for dim, pi in zip(dims, p)]\n", - "print(f'{shape = }')" + "shape = [dim + 2 * pi for dim, pi in zip(dims, p)]\n", + "print(f\"{shape = }\")" ] }, { @@ -206,14 +207,14 @@ "outputs": [], "source": [ "a = np.arange(dims[0] * dims[1] * dims[2]).reshape(*dims)\n", - "x0[:] = 99.\n", + "x0[:] = 99.0\n", "\n", "s = x0.starts\n", "e = x0.ends\n", - "x0[s[0]: e[0] + 1, s[1]: e[1] + 1, s[2]: e[2] + 1] = a\n", - "print(f'{x0[0, 0, :4] = }')\n", - "print(f'{x0[0, :4, 0] = }')\n", - "print(f'{x0[:4, 0, 0] = }')" + "x0[s[0] : e[0] + 1, s[1] : e[1] + 1, s[2] : e[2] + 1] = a\n", + "print(f\"{x0[0, 0, :4] = }\")\n", + "print(f\"{x0[0, :4, 0] = }\")\n", + "print(f\"{x0[:4, 0, 0] = }\")" ] }, { @@ -230,8 +231,8 @@ "outputs": [], "source": [ "x0[0, 0, -1] = 11\n", - "print(f'{x0[0, 0, :4] = }')\n", - "print(f'{x0[0, 0, 0:4] = }')" + "print(f\"{x0[0, 0, :4] = }\")\n", + "print(f\"{x0[0, 0, 0:4] = }\")" ] }, { @@ -247,16 +248,16 @@ "metadata": {}, "outputs": [], "source": [ - "x0[0, 0, -1] = 99.\n", - "y0 = StencilVector(dr_serial.Vh['0'])\n", - "y0[:] = 99.\n", + "x0[0, 0, -1] = 99.0\n", + "y0 = StencilVector(dr_serial.Vh[\"0\"])\n", + "y0[:] = 99.0\n", "\n", "pd = y0.pads\n", - "print(f'{pd = }')\n", - "y0._data[pd[0]: -pd[0], pd[1]: -pd[1], pd[2]: -pd[2]] = a\n", - "print(f'{y0[0, 0, :4] = }')\n", - "print(f'{y0[0, :4, 0] = }')\n", - "print(f'{y0[:4, 0, 0] = }')\n", + "print(f\"{pd = }\")\n", + "y0._data[pd[0] : -pd[0], pd[1] : -pd[1], pd[2] : -pd[2]] = a\n", + "print(f\"{y0[0, 0, :4] = }\")\n", + "print(f\"{y0[0, :4, 0] = }\")\n", + "print(f\"{y0[:4, 0, 0] = }\")\n", "\n", "assert np.all(x0[:] == y0[:])" ] @@ -274,10 +275,10 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{x0.shape = }')\n", - "print(f'{dims[0]*dims[1]*dims[2] = }')\n", - "print(f'{type(x0.toarray()) = }')\n", - "print(f'{x0.toarray().shape = }')" + "print(f\"{x0.shape = }\")\n", + "print(f\"{dims[0]*dims[1]*dims[2] = }\")\n", + "print(f\"{type(x0.toarray()) = }\")\n", + "print(f\"{x0.toarray().shape = }\")" ] }, { @@ -293,10 +294,10 @@ "metadata": {}, "outputs": [], "source": [ - "flat_data = x0[s[0]: e[0] + 1, s[1]: e[1] + 1, s[2]: e[2] + 1].flatten()\n", + "flat_data = x0[s[0] : e[0] + 1, s[1] : e[1] + 1, s[2] : e[2] + 1].flatten()\n", "assert np.all(x0.toarray() == flat_data)\n", - "print(f'{x0.toarray()[:4] = }')\n", - "print(f'{x0.toarray()[-4:] = }')" + "print(f\"{x0.toarray()[:4] = }\")\n", + "print(f\"{x0.toarray()[-4:] = }\")" ] }, { @@ -316,9 +317,9 @@ "source": [ "from psydac.linalg.stencil import StencilMatrix\n", "\n", - "A0 = StencilMatrix(dr_serial.Vh['0'], dr_serial.Vh['0'])\n", + "A0 = StencilMatrix(dr_serial.Vh[\"0\"], dr_serial.Vh[\"0\"])\n", "\n", - "assert np.all(A0[:, :] == 0.)" + "assert np.all(A0[:, :] == 0.0)" ] }, { @@ -334,10 +335,10 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{type(A0) = }')\n", - "print(f'{type(A0[:, :]) = }')\n", - "print(f'{type(A0[:, :, :, :, :, :]) = }')\n", - "print(f'{type(A0[:2, 1:2:7, :-1, :, 2, :]) = }')" + "print(f\"{type(A0) = }\")\n", + "print(f\"{type(A0[:, :]) = }\")\n", + "print(f\"{type(A0[:, :, :, :, :, :]) = }\")\n", + "print(f\"{type(A0[:2, 1:2:7, :-1, :, 2, :]) = }\")" ] }, { @@ -353,9 +354,9 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{A0[3, 2, 1, 0, 0, 0] = }')\n", - "A0[3, 2, 1, 0, 0, 0] = 99.\n", - "print(f'{A0[3, 2, 1, 0, 0, 0] = }')" + "print(f\"{A0[3, 2, 1, 0, 0, 0] = }\")\n", + "A0[3, 2, 1, 0, 0, 0] = 99.0\n", + "print(f\"{A0[3, 2, 1, 0, 0, 0] = }\")" ] }, { @@ -380,8 +381,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{A0.codomain.starts = }')\n", - "print(f'{A0.codomain.ends = }')" + "print(f\"{A0.codomain.starts = }\")\n", + "print(f\"{A0.codomain.ends = }\")" ] }, { @@ -397,8 +398,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{[2*pi + 1 for pi in p] = }')\n", - "print(f'{A0[0, 0, 0, :, :, :].shape = }')" + "print(f\"{[2*pi + 1 for pi in p] = }\")\n", + "print(f\"{A0[0, 0, 0, :, :, :].shape = }\")" ] }, { @@ -417,10 +418,10 @@ "s = A0.codomain.starts\n", "e = A0.codomain.ends\n", "\n", - "for n in range(- p[2], p[2] + 1):\n", - " A0[0, 0, s[2]: e[2] + 1, :, :, n] = n*10\n", + "for n in range(-p[2], p[2] + 1):\n", + " A0[0, 0, s[2] : e[2] + 1, :, :, n] = n * 10\n", "\n", - "print('A0[0, 0, :, 0, 0, :] = ')\n", + "print(\"A0[0, 0, :, 0, 0, :] = \")\n", "print(A0[0, 0, :, 0, 0, :])" ] }, @@ -439,19 +440,19 @@ "metadata": {}, "outputs": [], "source": [ - "vector_space_1d = dr_serial.Vh_fem['0'].spaces[2].coeff_space\n", + "vector_space_1d = dr_serial.Vh_fem[\"0\"].spaces[2].coeff_space\n", "A0_1d = StencilMatrix(vector_space_1d, vector_space_1d)\n", "\n", "s = A0_1d.codomain.starts\n", "e = A0_1d.codomain.ends\n", "\n", - "for n in range(- p[2], p[2] + 1):\n", - " A0_1d[s[0]: e[0] + 1, n] = n*10\n", + "for n in range(-p[2], p[2] + 1):\n", + " A0_1d[s[0] : e[0] + 1, n] = n * 10\n", "\n", - "print('A0_1d[0, 0, :, 0, 0, :] = ')\n", + "print(\"A0_1d[0, 0, :, 0, 0, :] = \")\n", "print(A0_1d[:, :])\n", "\n", - "print('\\nA0_1d.toarray() = ')\n", + "print(\"\\nA0_1d.toarray() = \")\n", "print(A0_1d.toarray())" ] }, @@ -470,23 +471,23 @@ "source": [ "a = np.arange(dims[0] * dims[1] * dims[2]).reshape(*dims)\n", "\n", - "A0[:, :] = 99.\n", + "A0[:, :] = 99.0\n", "\n", "s = A0.codomain.starts\n", "e = A0.codomain.ends\n", "pd = A0.pads\n", "\n", - "A0[s[0]: e[0] + 1, s[1]: e[1] + 1, s[2]: e[2] + 1, 0, 0, 0] = a\n", + "A0[s[0] : e[0] + 1, s[1] : e[1] + 1, s[2] : e[2] + 1, 0, 0, 0] = a\n", "\n", - "B0 = StencilMatrix(dr_serial.Vh['0'], dr_serial.Vh['0'])\n", + "B0 = StencilMatrix(dr_serial.Vh[\"0\"], dr_serial.Vh[\"0\"])\n", "\n", - "B0[:, :] = 99.\n", + "B0[:, :] = 99.0\n", "\n", "s = B0.codomain.starts\n", "e = B0.codomain.ends\n", "pd = B0.pads\n", "\n", - "B0._data[pd[0]: -pd[0], pd[1]: -pd[1], pd[2]: -pd[2], p[0], p[1], p[2]] = a\n", + "B0._data[pd[0] : -pd[0], pd[1] : -pd[1], pd[2] : -pd[2], p[0], p[1], p[2]] = a\n", "\n", "assert np.all(A0[:, :] == B0[:, :])" ] @@ -504,10 +505,10 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{A0.shape = }')\n", - "print(f'{dims[0]*dims[1]*dims[2] = }')\n", - "print(f'{type(A0.toarray()) = }')\n", - "print(f'{A0.toarray().shape = }')" + "print(f\"{A0.shape = }\")\n", + "print(f\"{dims[0]*dims[1]*dims[2] = }\")\n", + "print(f\"{type(A0.toarray()) = }\")\n", + "print(f\"{A0.toarray().shape = }\")" ] }, { @@ -540,11 +541,11 @@ "\n", "\n", "def stencil_vec_shape():\n", + " import numpy as np\n", + " from psydac.ddm.mpi import mpi as MPI\n", + " from psydac.linalg.stencil import StencilVector\n", "\n", " from struphy.feec.psydac_derham import Derham\n", - " from psydac.linalg.stencil import StencilVector\n", - " from psydac.ddm.mpi import mpi as MPI\n", - " import numpy as np\n", "\n", " comm = MPI.COMM_WORLD\n", " rank = comm.Get_rank()\n", @@ -556,16 +557,16 @@ "\n", " dr = Derham(Nel, p, spl_kind, comm=comm)\n", "\n", - " x0 = StencilVector(dr.Vh['0'])\n", + " x0 = StencilVector(dr.Vh[\"0\"])\n", "\n", - " assert np.all(x0[:] == 0.)\n", + " assert np.all(x0[:] == 0.0)\n", "\n", - " out = f'{rank = }, {x0.starts = }, {x0.ends = }, {x0.pads = }, {np.shape(x0[:]) = }:'\n", + " out = f\"{rank = }, {x0.starts = }, {x0.ends = }, {x0.pads = }, {np.shape(x0[:]) = }:\"\n", "\n", " return out\n", "\n", "\n", - "with ipp.Cluster(engines='mpi', n=2) as rc:\n", + "with ipp.Cluster(engines=\"mpi\", n=2) as rc:\n", " view = rc.broadcast_view()\n", " r = view.apply_sync(stencil_vec_shape)\n", " print(\"\\n\".join(r))" @@ -589,11 +590,11 @@ "outputs": [], "source": [ "def stencil_vec_ghost():\n", + " import numpy as np\n", + " from psydac.ddm.mpi import mpi as MPI\n", + " from psydac.linalg.stencil import StencilVector\n", "\n", " from struphy.feec.psydac_derham import Derham\n", - " from psydac.linalg.stencil import StencilVector\n", - " from psydac.ddm.mpi import mpi as MPI\n", - " import numpy as np\n", "\n", " comm = MPI.COMM_WORLD\n", " rank = comm.Get_rank()\n", @@ -605,26 +606,26 @@ "\n", " dr = Derham(Nel, p, spl_kind, comm=comm)\n", "\n", - " x0 = StencilVector(dr.Vh['0'])\n", + " x0 = StencilVector(dr.Vh[\"0\"])\n", " s = x0.starts\n", " e = x0.ends\n", " pd = x0.pads\n", "\n", - " assert np.all(x0[:] == 0.)\n", + " assert np.all(x0[:] == 0.0)\n", "\n", - " x0[:] = -99.\n", - " x0[s[0], s[1], s[2]: e[2] + 1] = np.arange(e[2] + 1 - s[2])*10**rank\n", + " x0[:] = -99.0\n", + " x0[s[0], s[1], s[2] : e[2] + 1] = np.arange(e[2] + 1 - s[2]) * 10**rank\n", "\n", - " out = f'{rank = }, before update: {x0[s[0], s[1], :] = }:'\n", + " out = f\"{rank = }, before update: {x0[s[0], s[1], :] = }:\"\n", "\n", " x0.update_ghost_regions()\n", "\n", - " out += f'\\n{rank = }, after update: {x0[s[0], s[1], :] = }:'\n", + " out += f\"\\n{rank = }, after update: {x0[s[0], s[1], :] = }:\"\n", "\n", " return out\n", "\n", "\n", - "with ipp.Cluster(engines='mpi', n=3) as rc:\n", + "with ipp.Cluster(engines=\"mpi\", n=3) as rc:\n", " view = rc.broadcast_view()\n", " r = view.apply_sync(stencil_vec_ghost)\n", " print(\"\\n\".join(r))" @@ -646,11 +647,11 @@ "outputs": [], "source": [ "def stencil_vec_toarray():\n", + " import numpy as np\n", + " from psydac.ddm.mpi import mpi as MPI\n", + " from psydac.linalg.stencil import StencilVector\n", "\n", " from struphy.feec.psydac_derham import Derham\n", - " from psydac.linalg.stencil import StencilVector\n", - " from psydac.ddm.mpi import mpi as MPI\n", - " import numpy as np\n", "\n", " comm = MPI.COMM_WORLD\n", " rank = comm.Get_rank()\n", @@ -662,16 +663,16 @@ "\n", " dr = Derham(Nel, p, spl_kind, comm=comm)\n", "\n", - " x0 = StencilVector(dr.Vh['0'])\n", + " x0 = StencilVector(dr.Vh[\"0\"])\n", "\n", - " assert np.all(x0[:] == 0.)\n", + " assert np.all(x0[:] == 0.0)\n", "\n", - " out = f'{rank = }, {np.shape(x0.toarray()) = }, {np.shape(x0.toarray_local()) = }'\n", + " out = f\"{rank = }, {np.shape(x0.toarray()) = }, {np.shape(x0.toarray_local()) = }\"\n", "\n", " return out\n", "\n", "\n", - "with ipp.Cluster(engines='mpi', n=2) as rc:\n", + "with ipp.Cluster(engines=\"mpi\", n=2) as rc:\n", " view = rc.broadcast_view()\n", " r = view.apply_sync(stencil_vec_toarray)\n", " print(\"\\n\".join(r))" @@ -706,11 +707,11 @@ "outputs": [], "source": [ "def stencil_mat_shape():\n", + " import numpy as np\n", + " from psydac.ddm.mpi import mpi as MPI\n", + " from psydac.linalg.stencil import StencilMatrix\n", "\n", " from struphy.feec.psydac_derham import Derham\n", - " from psydac.linalg.stencil import StencilMatrix\n", - " from psydac.ddm.mpi import mpi as MPI\n", - " import numpy as np\n", "\n", " comm = MPI.COMM_WORLD\n", " rank = comm.Get_rank()\n", @@ -722,16 +723,16 @@ "\n", " dr = Derham(Nel, p, spl_kind, comm=comm)\n", "\n", - " A0 = StencilMatrix(dr.Vh['0'], dr.Vh['0'])\n", + " A0 = StencilMatrix(dr.Vh[\"0\"], dr.Vh[\"0\"])\n", "\n", - " assert np.all(A0[:, :] == 0.)\n", + " assert np.all(A0[:, :] == 0.0)\n", "\n", - " out = f'{rank = }, {A0.codomain.starts = }, {A0.codomain.ends = }, {A0.pads = }, {np.shape(A0[:, :]) = }:'\n", + " out = f\"{rank = }, {A0.codomain.starts = }, {A0.codomain.ends = }, {A0.pads = }, {np.shape(A0[:, :]) = }:\"\n", "\n", " return out\n", "\n", "\n", - "with ipp.Cluster(engines='mpi', n=2) as rc:\n", + "with ipp.Cluster(engines=\"mpi\", n=2) as rc:\n", " view = rc.broadcast_view()\n", " r = view.apply_sync(stencil_mat_shape)\n", " print(\"\\n\".join(r))" @@ -753,11 +754,11 @@ "outputs": [], "source": [ "def stencil_mat_ghost():\n", + " import numpy as np\n", + " from psydac.ddm.mpi import mpi as MPI\n", + " from psydac.linalg.stencil import StencilMatrix\n", "\n", " from struphy.feec.psydac_derham import Derham\n", - " from psydac.linalg.stencil import StencilMatrix\n", - " from psydac.ddm.mpi import mpi as MPI\n", - " import numpy as np\n", "\n", " comm = MPI.COMM_WORLD\n", " rank = comm.Get_rank()\n", @@ -769,27 +770,28 @@ "\n", " dr = Derham(Nel, p, spl_kind, comm=comm)\n", "\n", - " A0 = StencilMatrix(dr.Vh['0'], dr.Vh['0'])\n", + " A0 = StencilMatrix(dr.Vh[\"0\"], dr.Vh[\"0\"])\n", " s = A0.codomain.starts\n", " e = A0.codomain.ends\n", " pd = A0.pads\n", "\n", - " assert np.all(A0[:, :] == 0.)\n", + " assert np.all(A0[:, :] == 0.0)\n", "\n", - " A0[:, :] = -99.\n", - " A0[s[0], s[1], s[2]: e[2] + 1, 0, 0, -pd[2]: pd[2] + 1] = np.arange(\n", - " (e[2] + 1 - s[2])*(2*pd[2] + 1)).reshape(e[2] + 1 - s[2], 2*pd[2] + 1)*10**rank\n", + " A0[:, :] = -99.0\n", + " A0[s[0], s[1], s[2] : e[2] + 1, 0, 0, -pd[2] : pd[2] + 1] = (\n", + " np.arange((e[2] + 1 - s[2]) * (2 * pd[2] + 1)).reshape(e[2] + 1 - s[2], 2 * pd[2] + 1) * 10**rank\n", + " )\n", "\n", - " out = f'{rank = }, before update: A0[s[0], s[1], :, 0, 0, :] = \\n{A0[s[0], s[1], :, 0, 0, :]}:'\n", + " out = f\"{rank = }, before update: A0[s[0], s[1], :, 0, 0, :] = \\n{A0[s[0], s[1], :, 0, 0, :]}:\"\n", "\n", " A0.update_ghost_regions()\n", "\n", - " out += f'\\n{rank = }, after update: A0[s[0], s[1], :, 0, 0, :] = \\n{A0[s[0], s[1], :, 0, 0, :]}:'\n", + " out += f\"\\n{rank = }, after update: A0[s[0], s[1], :, 0, 0, :] = \\n{A0[s[0], s[1], :, 0, 0, :]}:\"\n", "\n", " return out\n", "\n", "\n", - "with ipp.Cluster(engines='mpi', n=2) as rc:\n", + "with ipp.Cluster(engines=\"mpi\", n=2) as rc:\n", " view = rc.broadcast_view()\n", " r = view.apply_sync(stencil_mat_ghost)\n", " print(\"\\n\".join(r))" @@ -811,11 +813,11 @@ "outputs": [], "source": [ "def stencil_mat_toarray():\n", + " import numpy as np\n", + " from psydac.ddm.mpi import mpi as MPI\n", + " from psydac.linalg.stencil import StencilMatrix\n", "\n", " from struphy.feec.psydac_derham import Derham\n", - " from psydac.linalg.stencil import StencilMatrix\n", - " from psydac.ddm.mpi import mpi as MPI\n", - " import numpy as np\n", "\n", " comm = MPI.COMM_WORLD\n", " rank = comm.Get_rank()\n", @@ -827,16 +829,16 @@ "\n", " dr = Derham(Nel, p, spl_kind, comm=comm)\n", "\n", - " A0 = StencilMatrix(dr.Vh['0'], dr.Vh['0'])\n", + " A0 = StencilMatrix(dr.Vh[\"0\"], dr.Vh[\"0\"])\n", "\n", - " assert np.all(A0[:, :] == 0.)\n", + " assert np.all(A0[:, :] == 0.0)\n", "\n", - " out = f'{rank = }, {np.shape(A0.toarray()) = }'\n", + " out = f\"{rank = }, {np.shape(A0.toarray()) = }\"\n", "\n", " return out\n", "\n", "\n", - "with ipp.Cluster(engines='mpi', n=2) as rc:\n", + "with ipp.Cluster(engines=\"mpi\", n=2) as rc:\n", " view = rc.broadcast_view()\n", " r = view.apply_sync(stencil_mat_toarray)\n", " print(\"\\n\".join(r))" @@ -870,7 +872,9 @@ "metadata": {}, "outputs": [], "source": [ - "import sys, inspect\n", + "import inspect\n", + "import sys\n", + "\n", "from struphy.pic import particles\n", "\n", "for name, obj in inspect.getmembers(particles):\n", @@ -913,9 +917,9 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{particles.Np = }')\n", - "print(f'{particles.markers.shape = }')\n", - "print(f'{particles.markers_wo_holes.shape = }')" + "print(f\"{particles.Np = }\")\n", + "print(f\"{particles.markers.shape = }\")\n", + "print(f\"{particles.markers_wo_holes.shape = }\")" ] }, { @@ -931,13 +935,13 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{particles.positions[:5] = }\\n')\n", - "print(f'{particles.velocities[:5] = }\\n')\n", - "print(f'{particles.phasespace_coords[:5] = }\\n')\n", - "print(f'{particles.weights[:5] = }\\n')\n", - "print(f'{particles.sampling_density[:5] = }\\n')\n", - "print(f'{particles.weights0[:5] = }\\n')\n", - "print(f'{particles.marker_ids[:5] = }')" + "print(f\"{particles.positions[:5] = }\\n\")\n", + "print(f\"{particles.velocities[:5] = }\\n\")\n", + "print(f\"{particles.phasespace_coords[:5] = }\\n\")\n", + "print(f\"{particles.weights[:5] = }\\n\")\n", + "print(f\"{particles.sampling_density[:5] = }\\n\")\n", + "print(f\"{particles.weights0[:5] = }\\n\")\n", + "print(f\"{particles.marker_ids[:5] = }\")" ] } ], diff --git a/tutorials/tutorial_01_parameter_files.ipynb b/tutorials/tutorial_01_parameter_files.ipynb index 7621ed6b1..a382368fa 100644 --- a/tutorials/tutorial_01_parameter_files.ipynb +++ b/tutorials/tutorial_01_parameter_files.ipynb @@ -49,24 +49,23 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", - "from struphy.geometry import domains\n", + "from struphy import main\n", "from struphy.fields_background import equils\n", - "from struphy.topology import grids\n", - "from struphy.io.options import DerhamOptions\n", - "from struphy.io.options import FieldsBackground\n", + "from struphy.geometry import domains\n", "from struphy.initial import perturbations\n", + "from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Time\n", "from struphy.kinetic_background import maxwellians\n", - "from struphy.pic.utilities import (LoadingParameters, \n", - " WeightsParameters, \n", - " BoundaryParameters,\n", - " BinningPlot,\n", - " KernelDensityPlot,\n", - " )\n", - "from struphy import main\n", "\n", "# import model, set verbosity\n", - "from struphy.models.toy import Vlasov" + "from struphy.models.toy import Vlasov\n", + "from struphy.pic.utilities import (\n", + " BinningPlot,\n", + " BoundaryParameters,\n", + " KernelDensityPlot,\n", + " LoadingParameters,\n", + " WeightsParameters,\n", + ")\n", + "from struphy.topology import grids" ] }, { @@ -172,10 +171,10 @@ "source": [ "loading_params = LoadingParameters(Np=15)\n", "weights_params = WeightsParameters()\n", - "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", - "model.kinetic_ions.set_markers(loading_params=loading_params, \n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params)\n", + "boundary_params = BoundaryParameters(bc=(\"reflect\", \"reflect\", \"periodic\"))\n", + "model.kinetic_ions.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", + ")\n", "\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)" @@ -248,17 +247,18 @@ "source": [ "verbose = True\n", "\n", - "main.run(model, \n", - " params_path=None, \n", - " env=env, \n", - " base_units=base_units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -279,6 +279,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "path = os.path.join(os.getcwd(), \"sim_1\")\n", "\n", "main.pproc(path)" @@ -363,31 +364,31 @@ "metadata": {}, "outputs": [], "source": [ - "from matplotlib import pyplot as plt\n", "import numpy as np\n", + "from matplotlib import pyplot as plt\n", "\n", "fig = plt.figure()\n", "ax = fig.gca()\n", "\n", - "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", "\n", "# create alpha for color scaling\n", "Tend = time_opts.Tend\n", - "alpha = np.linspace(1., 0., Nt + 1)\n", + "alpha = np.linspace(1.0, 0.0, Nt + 1)\n", "\n", "# loop through particles, plot all time steps\n", "for i in range(Np):\n", " ax.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], alpha=alpha)\n", - " \n", - "ax.plot([l1, l1], [l2, r2], 'k')\n", - "ax.plot([r1, r1], [l2, r2], 'k')\n", - "ax.plot([l1, r1], [l2, l2], 'k')\n", - "ax.plot([l1, r1], [r2, r2], 'k')\n", - "ax.set_xlabel('x')\n", - "ax.set_ylabel('y')\n", + "\n", + "ax.plot([l1, l1], [l2, r2], \"k\")\n", + "ax.plot([r1, r1], [l2, r2], \"k\")\n", + "ax.plot([l1, r1], [l2, l2], \"k\")\n", + "ax.plot([l1, r1], [r2, r2], \"k\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", "ax.set_xlim(-6.5, 6.5)\n", "ax.set_ylim(-9, 9)\n", - "ax.set_title(f'{int(Nt - 1)} time steps (full color at t=0)');" + "ax.set_title(f\"{int(Nt - 1)} time steps (full color at t=0)\");" ] } ], diff --git a/tutorials/tutorial_02_test_particles.ipynb b/tutorials/tutorial_02_test_particles.ipynb index e95b6df81..7aac7f7e8 100644 --- a/tutorials/tutorial_02_test_particles.ipynb +++ b/tutorials/tutorial_02_test_particles.ipynb @@ -37,24 +37,23 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", - "from struphy.geometry import domains\n", + "from struphy import main\n", "from struphy.fields_background import equils\n", - "from struphy.topology import grids\n", - "from struphy.io.options import DerhamOptions\n", - "from struphy.io.options import FieldsBackground\n", + "from struphy.geometry import domains\n", "from struphy.initial import perturbations\n", + "from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Time\n", "from struphy.kinetic_background import maxwellians\n", - "from struphy.pic.utilities import (LoadingParameters, \n", - " WeightsParameters, \n", - " BoundaryParameters,\n", - " BinningPlot,\n", - " KernelDensityPlot,\n", - " )\n", - "from struphy import main\n", "\n", "# import model, set verbosity\n", - "from struphy.models.toy import Vlasov" + "from struphy.models.toy import Vlasov\n", + "from struphy.pic.utilities import (\n", + " BinningPlot,\n", + " BoundaryParameters,\n", + " KernelDensityPlot,\n", + " LoadingParameters,\n", + " WeightsParameters,\n", + ")\n", + "from struphy.topology import grids" ] }, { @@ -99,9 +98,9 @@ "time_opts = Time(dt=0.2, Tend=0.2)\n", "\n", "# geometry\n", - "a1 = 0.\n", - "a2 = 5.\n", - "Lz = 20.\n", + "a1 = 0.0\n", + "a2 = 5.0\n", + "Lz = 20.0\n", "domain = domains.HollowCylinder(a1=a1, a2=a2, Lz=Lz)" ] }, @@ -193,12 +192,12 @@ "weights_params = WeightsParameters()\n", "boundary_params = BoundaryParameters()\n", "\n", - "model.kinetic_ions.set_markers(loading_params=loading_params, \n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params)\n", - "model_2.kinetic_ions.set_markers(loading_params=loading_params_2, \n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params)\n", + "model.kinetic_ions.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", + ")\n", + "model_2.kinetic_ions.set_markers(\n", + " loading_params=loading_params_2, weights_params=weights_params, boundary_params=boundary_params\n", + ")\n", "\n", "model.kinetic_ions.set_sorting_boxes()\n", "model_2.kinetic_ions.set_sorting_boxes()\n", @@ -262,17 +261,18 @@ "source": [ "verbose = False\n", "\n", - "main.run(model, \n", - " params_path=None, \n", - " env=env, \n", - " base_units=base_units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -290,17 +290,18 @@ "metadata": {}, "outputs": [], "source": [ - "main.run(model_2, \n", - " params_path=None, \n", - " env=env_2, \n", - " base_units=base_units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model_2,\n", + " params_path=None,\n", + " env=env_2,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -319,6 +320,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "path = os.path.join(os.getcwd(), \"sim_1\")\n", "path_2 = os.path.join(os.getcwd(), \"sim_2\")\n", "\n", @@ -346,7 +348,7 @@ "source": [ "from matplotlib import pyplot as plt\n", "\n", - "fig = plt.figure(figsize=(10, 6)) \n", + "fig = plt.figure(figsize=(10, 6))\n", "\n", "orbits = simdata.orbits[\"kinetic_ions\"]\n", "orbits_uni = simdata_2.orbits[\"kinetic_ions\"]\n", @@ -355,24 +357,24 @@ "# orbits_uni = simdata_2.pic_species[\"kinetic_ions\"][\"orbits\"]\n", "\n", "plt.subplot(1, 2, 1)\n", - "plt.scatter(orbits[0, :, 0], orbits[0, :, 1], s=2.)\n", - "circle1 = plt.Circle((0, 0), a2, color='k', fill=False)\n", + "plt.scatter(orbits[0, :, 0], orbits[0, :, 1], s=2.0)\n", + "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "ax = plt.gca()\n", "ax.add_patch(circle1)\n", - "ax.set_aspect('equal')\n", - "plt.xlabel('x')\n", - "plt.ylabel('y')\n", - "plt.title('sim_1: draw uniform in logical space')\n", + "ax.set_aspect(\"equal\")\n", + "plt.xlabel(\"x\")\n", + "plt.ylabel(\"y\")\n", + "plt.title(\"sim_1: draw uniform in logical space\")\n", "\n", "plt.subplot(1, 2, 2)\n", - "plt.scatter(orbits_uni[0, :, 0], orbits_uni[0, :, 1], s=2.)\n", - "circle2 = plt.Circle((0, 0), a2, color='k', fill=False)\n", + "plt.scatter(orbits_uni[0, :, 0], orbits_uni[0, :, 1], s=2.0)\n", + "circle2 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "ax = plt.gca()\n", "ax.add_patch(circle2)\n", - "ax.set_aspect('equal')\n", - "plt.xlabel('x')\n", - "plt.ylabel('y')\n", - "plt.title('sim_2: draw uniform on disc');" + "ax.set_aspect(\"equal\")\n", + "plt.xlabel(\"x\")\n", + "plt.ylabel(\"y\")\n", + "plt.title(\"sim_2: draw uniform on disc\");" ] }, { @@ -395,7 +397,7 @@ "source": [ "time_opts = Time(dt=0.2, Tend=10.0)\n", "loading_params = LoadingParameters(Np=15, spatial=\"disc\")\n", - "boundary_params = BoundaryParameters(bc=('reflect', 'periodic', 'periodic'))" + "boundary_params = BoundaryParameters(bc=(\"reflect\", \"periodic\", \"periodic\"))" ] }, { @@ -411,9 +413,9 @@ "# species parameters\n", "model.kinetic_ions.set_phys_params()\n", "\n", - "model.kinetic_ions.set_markers(loading_params=loading_params, \n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params)\n", + "model.kinetic_ions.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", + ")\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)" ] @@ -461,17 +463,18 @@ "source": [ "verbose = False\n", "\n", - "main.run(model, \n", - " params_path=None, \n", - " env=env, \n", - " base_units=base_units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -529,23 +532,23 @@ "fig = plt.figure()\n", "ax = fig.gca()\n", "\n", - "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", "\n", "# create alpha for color scaling\n", "Tend = time_opts.Tend\n", - "alpha = np.linspace(1., 0., Nt + 1)\n", + "alpha = np.linspace(1.0, 0.0, Nt + 1)\n", "\n", "# loop through particles, plot all time steps\n", "for i in range(Np):\n", " ax.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], alpha=alpha)\n", - " \n", - "circle1 = plt.Circle((0, 0), a2, color='k', fill=False)\n", + "\n", + "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "\n", "ax.add_patch(circle1)\n", - "ax.set_aspect('equal')\n", - "ax.set_xlabel('x')\n", - "ax.set_ylabel('y')\n", - "ax.set_title(f'{Nt - 1} time steps (full color at t=0)');" + "ax.set_aspect(\"equal\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "ax.set_title(f\"{Nt - 1} time steps (full color at t=0)\");" ] }, { @@ -578,9 +581,9 @@ "metadata": {}, "outputs": [], "source": [ - "B0x = 0.\n", - "B0y = 0.\n", - "B0z = 1.\n", + "B0x = 0.0\n", + "B0y = 0.0\n", + "B0z = 1.0\n", "equil = equils.HomogenSlab(B0x=B0x, B0y=B0y, B0z=B0z)" ] }, @@ -625,10 +628,10 @@ "model.kinetic_ions.set_phys_params()\n", "\n", "loading_params = LoadingParameters(Np=20)\n", - "boundary_params = BoundaryParameters(bc=('remove', 'periodic', 'periodic'))\n", - "model.kinetic_ions.set_markers(loading_params=loading_params, \n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params)\n", + "boundary_params = BoundaryParameters(bc=(\"remove\", \"periodic\", \"periodic\"))\n", + "model.kinetic_ions.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", + ")\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)\n", "\n", @@ -661,17 +664,18 @@ "# run\n", "verbose = False\n", "\n", - "main.run(model, \n", - " params_path=None, \n", - " env=env, \n", - " base_units=base_units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -710,23 +714,23 @@ "fig = plt.figure()\n", "ax = fig.gca()\n", "\n", - "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", "\n", "# create alpha for color scaling\n", "Tend = time_opts.Tend\n", - "alpha = np.linspace(1., 0., Nt + 1)\n", + "alpha = np.linspace(1.0, 0.0, Nt + 1)\n", "\n", "# loop through particles, plot all time steps\n", "for i in range(Np):\n", " ax.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], alpha=alpha)\n", - " \n", - "circle1 = plt.Circle((0, 0), a2, color='k', fill=False)\n", + "\n", + "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "\n", "ax.add_patch(circle1)\n", - "ax.set_aspect('equal')\n", - "ax.set_xlabel('x')\n", - "ax.set_ylabel('y')\n", - "ax.set_title(f'{int(Nt - 1)} time steps (full color at t=0)');" + "ax.set_aspect(\"equal\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "ax.set_title(f\"{int(Nt - 1)} time steps (full color at t=0)\");" ] }, { @@ -746,9 +750,9 @@ "metadata": {}, "outputs": [], "source": [ - "n1 = 0.\n", - "n2 = 0.\n", - "na = 1.\n", + "n1 = 0.0\n", + "n2 = 0.0\n", + "na = 1.0\n", "equil = equils.EQDSKequilibrium(n1=n1, n2=n2, na=na)" ] }, @@ -770,12 +774,8 @@ "Nel = (28, 72)\n", "p = (3, 3)\n", "psi_power = 0.6\n", - "psi_shifts = (1e-6, 1.)\n", - "domain = domains.Tokamak(equilibrium=equil, \n", - " Nel=Nel,\n", - " p=p,\n", - " psi_power=psi_power,\n", - " psi_shifts=psi_shifts)" + "psi_shifts = (1e-6, 1.0)\n", + "domain = domains.Tokamak(equilibrium=equil, Nel=Nel, p=p, psi_power=psi_power, psi_shifts=psi_shifts)" ] }, { @@ -838,9 +838,9 @@ "import numpy as np\n", "\n", "# logical grid on the unit cube\n", - "e1 = np.linspace(0., 1., 101)\n", - "e2 = np.linspace(0., 1., 101)\n", - "e3 = np.linspace(0., 1., 101)\n", + "e1 = np.linspace(0.0, 1.0, 101)\n", + "e2 = np.linspace(0.0, 1.0, 101)\n", + "e3 = np.linspace(0.0, 1.0, 101)\n", "\n", "# move away from the singular point r = 0\n", "e1[0] += 1e-5" @@ -854,11 +854,11 @@ "outputs": [], "source": [ "# logical coordinates of the poloidal plane at phi = 0\n", - "eta_poloidal = (e1, e2, 0.)\n", + "eta_poloidal = (e1, e2, 0.0)\n", "# logical coordinates of the top view at theta = 0\n", - "eta_topview_1 = (e1, 0., e3)\n", + "eta_topview_1 = (e1, 0.0, e3)\n", "# logical coordinates of the top view at theta = pi\n", - "eta_topview_2 = (e1, .5, e3)" + "eta_topview_2 = (e1, 0.5, e3)" ] }, { @@ -873,9 +873,9 @@ "x_top1, y_top1, z_top1 = domain(*eta_topview_1, squeeze_out=True)\n", "x_top2, y_top2, z_top2 = domain(*eta_topview_2, squeeze_out=True)\n", "\n", - "print(f'{x_pol.shape = }')\n", - "print(f'{x_top1.shape = }')\n", - "print(f'{x_top2.shape = }')" + "print(f\"{x_pol.shape = }\")\n", + "print(f\"{x_top1.shape = }\")\n", + "print(f\"{x_top2.shape = }\")" ] }, { @@ -904,36 +904,35 @@ "ax_top.contourf(x_top2, y_top2, equil.absB0(*eta_topview_2, squeeze_out=True), levels=levels)\n", "\n", "# last closed flux surface, poloidal\n", - "ax.plot(x_pol[-1], z_pol[-1], color='k')\n", + "ax.plot(x_pol[-1], z_pol[-1], color=\"k\")\n", "\n", "# last closed flux surface, toroidal\n", - "ax_top.plot(x_top1[-1], y_top1[-1], color='k')\n", - "ax_top.plot(x_top2[-1], y_top2[-1], color='k')\n", + "ax_top.plot(x_top1[-1], y_top1[-1], color=\"k\")\n", + "ax_top.plot(x_top2[-1], y_top2[-1], color=\"k\")\n", "\n", "# limiter, poloidal\n", - "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, 'tab:orange')\n", - "ax.axis('equal')\n", - "ax.set_xlabel('R')\n", - "ax.set_ylabel('Z')\n", - "ax.set_title('abs(B) at $\\phi=0$')\n", - "fig.colorbar(im);\n", - "\n", + "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, \"tab:orange\")\n", + "ax.axis(\"equal\")\n", + "ax.set_xlabel(\"R\")\n", + "ax.set_ylabel(\"Z\")\n", + "ax.set_title(\"abs(B) at $\\phi=0$\")\n", + "fig.colorbar(im)\n", "# limiter, toroidal\n", "limiter_Rmax = np.max(equil.limiter_pts_R)\n", "limiter_Rmin = np.min(equil.limiter_pts_R)\n", "\n", - "thetas = 2*np.pi*e2\n", + "thetas = 2 * np.pi * e2\n", "limiter_x_max = limiter_Rmax * np.cos(thetas)\n", - "limiter_y_max = - limiter_Rmax * np.sin(thetas)\n", + "limiter_y_max = -limiter_Rmax * np.sin(thetas)\n", "limiter_x_min = limiter_Rmin * np.cos(thetas)\n", - "limiter_y_min = - limiter_Rmin * np.sin(thetas)\n", - "\n", - "ax_top.plot(limiter_x_max, limiter_y_max, 'tab:orange')\n", - "ax_top.plot(limiter_x_min, limiter_y_min, 'tab:orange')\n", - "ax_top.axis('equal')\n", - "ax_top.set_xlabel('x')\n", - "ax_top.set_ylabel('y')\n", - "ax_top.set_title('abs(B) at $Z=0$')\n", + "limiter_y_min = -limiter_Rmin * np.sin(thetas)\n", + "\n", + "ax_top.plot(limiter_x_max, limiter_y_max, \"tab:orange\")\n", + "ax_top.plot(limiter_x_min, limiter_y_min, \"tab:orange\")\n", + "ax_top.axis(\"equal\")\n", + "ax_top.set_xlabel(\"x\")\n", + "ax_top.set_ylabel(\"y\")\n", + "ax_top.set_title(\"abs(B) at $Z=0$\")\n", "fig.colorbar(im_top);" ] }, @@ -958,17 +957,18 @@ "# species parameters\n", "model.kinetic_ions.set_phys_params()\n", "\n", - "initial = ((.501, 0.001, 0.001, 0., 0.0450, -0.04), # co-passing particle\n", - " (.511, 0.001, 0.001, 0., -0.0450, -0.04), # counter passing particle\n", - " (.521, 0.001, 0.001, 0., 0.0105, -0.04), # co-trapped particle\n", - " (.531, 0.001, 0.001, 0., -0.0155, -0.04))\n", + "initial = (\n", + " (0.501, 0.001, 0.001, 0.0, 0.0450, -0.04), # co-passing particle\n", + " (0.511, 0.001, 0.001, 0.0, -0.0450, -0.04), # counter passing particle\n", + " (0.521, 0.001, 0.001, 0.0, 0.0105, -0.04), # co-trapped particle\n", + " (0.531, 0.001, 0.001, 0.0, -0.0155, -0.04),\n", + ")\n", "\n", "loading_params = LoadingParameters(Np=4, seed=1608, specific_markers=initial)\n", - "boundary_params = BoundaryParameters(bc=('remove', 'periodic', 'periodic'))\n", - "model.kinetic_ions.set_markers(loading_params=loading_params, \n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params,\n", - " bufsize=2.)\n", + "boundary_params = BoundaryParameters(bc=(\"remove\", \"periodic\", \"periodic\"))\n", + "model.kinetic_ions.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params, bufsize=2.0\n", + ")\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)\n", "\n", @@ -1025,17 +1025,18 @@ "\n", "verbose = False\n", "\n", - "main.run(model, \n", - " params_path=None, \n", - " env=env, \n", - " base_units=base_units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -1046,6 +1047,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "from struphy import main\n", "\n", "path = os.path.join(os.getcwd(), \"sim_1\")\n", @@ -1076,21 +1078,20 @@ "source": [ "import math\n", "\n", - "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", "\n", "dt = time_opts.dt\n", "Tend = time_opts.Tend\n", "\n", "for i in range(Np):\n", - " r = np.sqrt(orbits[:, i, 0]**2 + orbits[:, i, 1]**2)\n", - " # poloidal \n", + " r = np.sqrt(orbits[:, i, 0] ** 2 + orbits[:, i, 1] ** 2)\n", + " # poloidal\n", " ax.scatter(r, orbits[:, i, 2], c=colors[i % 4], s=1)\n", " # top view\n", " ax_top.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], s=1)\n", - " \n", - "ax.set_title(f'{math.ceil(Tend/dt)} time steps')\n", - "ax_top.set_title(f'{math.ceil(Tend/dt)} time steps');\n", "\n", + "ax.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", + "ax_top.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", "fig" ] }, @@ -1119,17 +1120,18 @@ "# species parameters\n", "model.kinetic_ions.set_phys_params()\n", "\n", - "initial = ((.501, 0.001, 0.001, -1.935 , 1.72), # co-passing particle\n", - " (.501, 0.001, 0.001, 1.935 , 1.72), # couner-passing particle\n", - " (.501, 0.001, 0.001, -0.6665, 1.72), # co-trapped particle\n", - " (.501, 0.001, 0.001, 0.4515, 1.72)) # counter-trapped particl\n", + "initial = (\n", + " (0.501, 0.001, 0.001, -1.935, 1.72), # co-passing particle\n", + " (0.501, 0.001, 0.001, 1.935, 1.72), # couner-passing particle\n", + " (0.501, 0.001, 0.001, -0.6665, 1.72), # co-trapped particle\n", + " (0.501, 0.001, 0.001, 0.4515, 1.72),\n", + ") # counter-trapped particl\n", "\n", "loading_params = LoadingParameters(Np=4, seed=1608, specific_markers=initial)\n", - "boundary_params = BoundaryParameters(bc=('remove', 'periodic', 'periodic'))\n", - "model.kinetic_ions.set_markers(loading_params=loading_params, \n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params,\n", - " bufsize=2.)\n", + "boundary_params = BoundaryParameters(bc=(\"remove\", \"periodic\", \"periodic\"))\n", + "model.kinetic_ions.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params, bufsize=2.0\n", + ")\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)\n", "\n", @@ -1170,36 +1172,35 @@ "ax_top.contourf(x_top2, y_top2, equil.absB0(*eta_topview_2, squeeze_out=True), levels=levels)\n", "\n", "# last closed flux surface, poloidal\n", - "ax.plot(x_pol[-1], z_pol[-1], color='k')\n", + "ax.plot(x_pol[-1], z_pol[-1], color=\"k\")\n", "\n", "# last closed flux surface, toroidal\n", - "ax_top.plot(x_top1[-1], y_top1[-1], color='k')\n", - "ax_top.plot(x_top2[-1], y_top2[-1], color='k')\n", + "ax_top.plot(x_top1[-1], y_top1[-1], color=\"k\")\n", + "ax_top.plot(x_top2[-1], y_top2[-1], color=\"k\")\n", "\n", "# limiter, poloidal\n", - "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, 'tab:orange')\n", - "ax.axis('equal')\n", - "ax.set_xlabel('R')\n", - "ax.set_ylabel('Z')\n", - "ax.set_title('abs(B) at $\\phi=0$')\n", - "fig.colorbar(im);\n", - "\n", + "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, \"tab:orange\")\n", + "ax.axis(\"equal\")\n", + "ax.set_xlabel(\"R\")\n", + "ax.set_ylabel(\"Z\")\n", + "ax.set_title(\"abs(B) at $\\phi=0$\")\n", + "fig.colorbar(im)\n", "# limiter, toroidal\n", "limiter_Rmax = np.max(equil.limiter_pts_R)\n", "limiter_Rmin = np.min(equil.limiter_pts_R)\n", "\n", - "thetas = 2*np.pi*e2\n", + "thetas = 2 * np.pi * e2\n", "limiter_x_max = limiter_Rmax * np.cos(thetas)\n", - "limiter_y_max = - limiter_Rmax * np.sin(thetas)\n", + "limiter_y_max = -limiter_Rmax * np.sin(thetas)\n", "limiter_x_min = limiter_Rmin * np.cos(thetas)\n", - "limiter_y_min = - limiter_Rmin * np.sin(thetas)\n", - "\n", - "ax_top.plot(limiter_x_max, limiter_y_max, 'tab:orange')\n", - "ax_top.plot(limiter_x_min, limiter_y_min, 'tab:orange')\n", - "ax_top.axis('equal')\n", - "ax_top.set_xlabel('x')\n", - "ax_top.set_ylabel('y')\n", - "ax_top.set_title('abs(B) at $Z=0$')\n", + "limiter_y_min = -limiter_Rmin * np.sin(thetas)\n", + "\n", + "ax_top.plot(limiter_x_max, limiter_y_max, \"tab:orange\")\n", + "ax_top.plot(limiter_x_min, limiter_y_min, \"tab:orange\")\n", + "ax_top.axis(\"equal\")\n", + "ax_top.set_xlabel(\"x\")\n", + "ax_top.set_ylabel(\"y\")\n", + "ax_top.set_title(\"abs(B) at $Z=0$\")\n", "fig.colorbar(im_top);" ] }, @@ -1214,17 +1215,18 @@ "\n", "verbose = False\n", "\n", - "main.run(model, \n", - " params_path=None, \n", - " env=env, \n", - " base_units=base_units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -1235,6 +1237,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "from struphy import main\n", "\n", "path = os.path.join(os.getcwd(), \"sim_1\")\n", @@ -1265,21 +1268,20 @@ "source": [ "import math\n", "\n", - "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", "\n", "dt = time_opts.dt\n", "Tend = time_opts.Tend\n", "\n", "for i in range(Np):\n", - " r = np.sqrt(orbits[:, i, 0]**2 + orbits[:, i, 1]**2)\n", - " # poloidal \n", + " r = np.sqrt(orbits[:, i, 0] ** 2 + orbits[:, i, 1] ** 2)\n", + " # poloidal\n", " ax.scatter(r, orbits[:, i, 2], c=colors[i % 4], s=1)\n", " # top view\n", " ax_top.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], s=1)\n", - " \n", - "ax.set_title(f'{math.ceil(Tend/dt)} time steps')\n", - "ax_top.set_title(f'{math.ceil(Tend/dt)} time steps');\n", "\n", + "ax.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", + "ax_top.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", "fig" ] } diff --git a/tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb b/tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb index f50bfd8a7..c56d1fe27 100644 --- a/tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb +++ b/tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb @@ -50,24 +50,23 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", - "from struphy.geometry import domains\n", + "from struphy import main\n", "from struphy.fields_background import equils\n", - "from struphy.topology import grids\n", - "from struphy.io.options import DerhamOptions\n", - "from struphy.io.options import FieldsBackground\n", + "from struphy.geometry import domains\n", "from struphy.initial import perturbations\n", + "from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Time\n", "from struphy.kinetic_background import maxwellians\n", - "from struphy.pic.utilities import (LoadingParameters,\n", - " WeightsParameters,\n", - " BoundaryParameters,\n", - " BinningPlot,\n", - " KernelDensityPlot,\n", - " )\n", - "from struphy import main\n", "\n", "# import model, set verbosity\n", - "from struphy.models.toy import PressureLessSPH" + "from struphy.models.toy import PressureLessSPH\n", + "from struphy.pic.utilities import (\n", + " BinningPlot,\n", + " BoundaryParameters,\n", + " KernelDensityPlot,\n", + " LoadingParameters,\n", + " WeightsParameters,\n", + ")\n", + "from struphy.topology import grids" ] }, { @@ -95,12 +94,12 @@ "time_opts = Time(dt=0.02, Tend=4, split_algo=\"Strang\")\n", "\n", "# geometry\n", - "l1 = -.5\n", - "r1 = .5\n", - "l2 = -.5\n", - "r2 = .5\n", - "l3 = 0.\n", - "r3 = 1.\n", + "l1 = -0.5\n", + "r1 = 0.5\n", + "l2 = -0.5\n", + "r2 = 0.5\n", + "l3 = 0.0\n", + "r3 = 1.0\n", "domain = domains.Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" ] }, @@ -122,17 +121,20 @@ "# construct Beltrami flow\n", "import numpy as np\n", "\n", + "\n", "def u_fun(x, y, z):\n", - " ux = -np.cos(np.pi*x)*np.sin(np.pi*y)\n", - " uy = np.sin(np.pi*x)*np.cos(np.pi*y)\n", - " uz = 0 * x \n", + " ux = -np.cos(np.pi * x) * np.sin(np.pi * y)\n", + " uy = np.sin(np.pi * x) * np.cos(np.pi * y)\n", + " uz = 0 * x\n", " return ux, uy, uz\n", "\n", - "p_fun = lambda x, y, z: 0.5*(np.sin(np.pi*x)**2 + np.sin(np.pi*y)**2)\n", - "n_fun = lambda x, y, z: 1. + 0*x\n", + "\n", + "p_fun = lambda x, y, z: 0.5 * (np.sin(np.pi * x) ** 2 + np.sin(np.pi * y) ** 2)\n", + "n_fun = lambda x, y, z: 1.0 + 0 * x\n", "\n", "# put the functions in a generic equilibirum container\n", "from struphy.fields_background.generic import GenericCartesianFluidEquilibrium\n", + "\n", "bel_flow = GenericCartesianFluidEquilibrium(u_xyz=u_fun, p_xyz=p_fun, n_xyz=n_fun)" ] }, @@ -184,11 +186,12 @@ "\n", "loading_params = LoadingParameters(Np=1000)\n", "weights_params = WeightsParameters()\n", - "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", - "model.cold_fluid.set_markers(loading_params=loading_params,\n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params,\n", - " )\n", + "boundary_params = BoundaryParameters(bc=(\"reflect\", \"reflect\", \"periodic\"))\n", + "model.cold_fluid.set_markers(\n", + " loading_params=loading_params,\n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params,\n", + ")\n", "model.cold_fluid.set_sorting_boxes(boxes_per_dim=(1, 1, 1))\n", "model.cold_fluid.set_save_data(n_markers=1.0)" ] @@ -210,6 +213,7 @@ "source": [ "# propagator options\n", "from struphy.ode.utils import ButcherTableau\n", + "\n", "butcher = ButcherTableau(algo=\"forward_euler\")\n", "model.propagators.push_eta.options = model.propagators.push_eta.Options(butcher=butcher)\n", "\n", @@ -253,17 +257,18 @@ "source": [ "verbose = False\n", "\n", - "main.run(model,\n", - " params_path=None,\n", - " env=env,\n", - " base_units=base_units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -274,6 +279,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "path = os.path.join(os.getcwd(), \"sim_1\")\n", "\n", "main.pproc(path)" @@ -297,32 +303,32 @@ "outputs": [], "source": [ "from matplotlib import pyplot as plt\n", + "\n", "plt.figure(figsize=(12, 28))\n", "\n", "orbits = simdata.orbits[\"cold_fluid\"]\n", "\n", - "coloring = np.select([orbits[0, :, 0]<=-0.2, \n", - " np.abs(orbits[0, :, 0]) < +0.2, \n", - " orbits[0, :, 0] >= 0.2],\n", - " [-1.0, 0.0, +1.0])\n", + "coloring = np.select(\n", + " [orbits[0, :, 0] <= -0.2, np.abs(orbits[0, :, 0]) < +0.2, orbits[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0]\n", + ")\n", "\n", "dt = time_opts.dt\n", "Nt = simdata.t_grid.size - 1\n", - "interval = Nt/20\n", + "interval = Nt / 20\n", "plot_ct = 0\n", "for i in range(Nt):\n", " if i % interval == 0:\n", - " print(f'{i = }')\n", + " print(f\"{i = }\")\n", " plot_ct += 1\n", " plt.subplot(5, 2, plot_ct)\n", - " ax = plt.gca() \n", + " ax = plt.gca()\n", " plt.scatter(orbits[i, :, 0], orbits[i, :, 1], c=coloring)\n", - " plt.axis('square')\n", - " plt.title('n0_scatter')\n", + " plt.axis(\"square\")\n", + " plt.title(\"n0_scatter\")\n", " plt.xlim(l1, r1)\n", " plt.ylim(l2, r2)\n", " plt.colorbar()\n", - " plt.title(f'Gas at t={i*dt}')\n", + " plt.title(f\"Gas at t={i * dt}\")\n", " if plot_ct == 10:\n", " break" ] @@ -369,12 +375,10 @@ "\n", "loading_params = LoadingParameters(ppb=4, loading=\"tesselation\")\n", "weights_params = WeightsParameters()\n", - "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", - "model.cold_fluid.set_markers(loading_params=loading_params,\n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params,\n", - " bufsize=0.5\n", - " )\n", + "boundary_params = BoundaryParameters(bc=(\"reflect\", \"reflect\", \"periodic\"))\n", + "model.cold_fluid.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params, bufsize=0.5\n", + ")\n", "model.cold_fluid.set_sorting_boxes(boxes_per_dim=(16, 16, 1))\n", "model.cold_fluid.set_save_data(n_markers=1.0)" ] @@ -388,6 +392,7 @@ "source": [ "# propagator options\n", "from struphy.ode.utils import ButcherTableau\n", + "\n", "butcher = ButcherTableau(algo=\"forward_euler\")\n", "model.propagators.push_eta.options = model.propagators.push_eta.Options(butcher=butcher)\n", "\n", @@ -413,17 +418,18 @@ "metadata": {}, "outputs": [], "source": [ - "main.run(model,\n", - " params_path=None,\n", - " env=env,\n", - " base_units=base_units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -434,6 +440,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "path = os.path.join(os.getcwd(), \"sim_2\")\n", "\n", "main.pproc(path)" @@ -457,32 +464,32 @@ "outputs": [], "source": [ "from matplotlib import pyplot as plt\n", + "\n", "plt.figure(figsize=(12, 28))\n", "\n", "orbits = simdata.orbits[\"cold_fluid\"]\n", "\n", - "coloring = np.select([orbits[0, :, 0]<=-0.2, \n", - " np.abs(orbits[0, :, 0]) < +0.2, \n", - " orbits[0, :, 0] >= 0.2],\n", - " [-1.0, 0.0, +1.0])\n", + "coloring = np.select(\n", + " [orbits[0, :, 0] <= -0.2, np.abs(orbits[0, :, 0]) < +0.2, orbits[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0]\n", + ")\n", "\n", "dt = time_opts.dt\n", "Nt = simdata.t_grid.size - 1\n", - "interval = Nt/20\n", + "interval = Nt / 20\n", "plot_ct = 0\n", "for i in range(Nt):\n", " if i % interval == 0:\n", - " print(f'{i = }')\n", + " print(f\"{i = }\")\n", " plot_ct += 1\n", " plt.subplot(5, 2, plot_ct)\n", - " ax = plt.gca() \n", + " ax = plt.gca()\n", " plt.scatter(orbits[i, :, 0], orbits[i, :, 1], c=coloring)\n", - " plt.axis('square')\n", - " plt.title('n0_scatter')\n", + " plt.axis(\"square\")\n", + " plt.title(\"n0_scatter\")\n", " plt.xlim(l1, r1)\n", " plt.ylim(l2, r2)\n", " plt.colorbar()\n", - " plt.title(f'Gas at t={i*dt}')\n", + " plt.title(f\"Gas at t={i * dt}\")\n", " if plot_ct == 10:\n", " break" ] @@ -541,7 +548,7 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.pic.sph_smoothing_kernels import linear_uni, trigonometric_uni, gaussian_uni\n", + "from struphy.pic.sph_smoothing_kernels import gaussian_uni, linear_uni, trigonometric_uni\n", "\n", "x = np.linspace(-1, 1, 200)\n", "out1 = np.zeros_like(x)\n", @@ -549,13 +556,13 @@ "out3 = np.zeros_like(x)\n", "\n", "for i, xi in enumerate(x):\n", - " out1[i] = trigonometric_uni(xi, 1.)\n", - " out2[i] = gaussian_uni(xi, 1.)\n", - " out3[i] = linear_uni(xi, 1.)\n", + " out1[i] = trigonometric_uni(xi, 1.0)\n", + " out2[i] = gaussian_uni(xi, 1.0)\n", + " out3[i] = linear_uni(xi, 1.0)\n", "plt.plot(x, out1, label=\"trigonometric\")\n", "plt.plot(x, out2, label=\"gaussian\")\n", - "plt.plot(x, out3, label = \"linear\")\n", - "plt.title('Some smoothing kernels')\n", + "plt.plot(x, out3, label=\"linear\")\n", + "plt.title(\"Some smoothing kernels\")\n", "plt.legend()" ] }, @@ -574,24 +581,23 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", - "from struphy.geometry import domains\n", + "from struphy import main\n", "from struphy.fields_background import equils\n", - "from struphy.topology import grids\n", - "from struphy.io.options import DerhamOptions\n", - "from struphy.io.options import FieldsBackground\n", + "from struphy.geometry import domains\n", "from struphy.initial import perturbations\n", + "from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Time\n", "from struphy.kinetic_background import maxwellians\n", - "from struphy.pic.utilities import (LoadingParameters,\n", - " WeightsParameters,\n", - " BoundaryParameters,\n", - " BinningPlot,\n", - " KernelDensityPlot,\n", - " )\n", - "from struphy import main\n", "\n", "# import model, set verbosity\n", - "from struphy.models.fluid import EulerSPH" + "from struphy.models.fluid import EulerSPH\n", + "from struphy.pic.utilities import (\n", + " BinningPlot,\n", + " BoundaryParameters,\n", + " KernelDensityPlot,\n", + " LoadingParameters,\n", + " WeightsParameters,\n", + ")\n", + "from struphy.topology import grids" ] }, { @@ -623,8 +629,8 @@ "r1 = 3.0\n", "l2 = -3.0\n", "r2 = 3.0\n", - "l3 = 0.\n", - "r3 = 1.\n", + "l3 = 0.0\n", + "r3 = 1.0\n", "domain = domains.Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" ] }, @@ -644,11 +650,13 @@ "outputs": [], "source": [ "# gaussian initial blob\n", - "from struphy.fields_background.generic import GenericCartesianFluidEquilibrium\n", "import numpy as np\n", + "\n", + "from struphy.fields_background.generic import GenericCartesianFluidEquilibrium\n", + "\n", "T_h = 0.2\n", - "gamma = 5/3\n", - "n_fun = lambda x, y, z: np.exp(-(x**2 + y**2)/T_h) / 35\n", + "gamma = 5 / 3\n", + "n_fun = lambda x, y, z: np.exp(-(x**2 + y**2) / T_h) / 35\n", "\n", "blob = GenericCartesianFluidEquilibrium(n_xyz=n_fun)" ] @@ -702,10 +710,11 @@ "loading_params = LoadingParameters(ppb=400)\n", "weights_params = WeightsParameters(reject_weights=True, threshold=3e-3)\n", "boundary_params = BoundaryParameters()\n", - "model.euler_fluid.set_markers(loading_params=loading_params,\n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params,\n", - " )\n", + "model.euler_fluid.set_markers(\n", + " loading_params=loading_params,\n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params,\n", + ")\n", "nx = 16\n", "ny = 16\n", "model.euler_fluid.set_sorting_boxes(boxes_per_dim=(nx, ny, 1))" @@ -726,18 +735,20 @@ "metadata": {}, "outputs": [], "source": [ - "bin_plot = BinningPlot(slice=\"e1_e2\", \n", - " n_bins=(64, 64), \n", - " ranges=((0.0, 1.0), (0.0, 1.0)), \n", - " divide_by_jac=False,\n", - " )\n", + "bin_plot = BinningPlot(\n", + " slice=\"e1_e2\",\n", + " n_bins=(64, 64),\n", + " ranges=((0.0, 1.0), (0.0, 1.0)),\n", + " divide_by_jac=False,\n", + ")\n", "pts_e1 = 100\n", "pts_e2 = 90\n", "kd_plot = KernelDensityPlot(pts_e1=pts_e1, pts_e2=pts_e2, pts_e3=1)\n", - "model.euler_fluid.set_save_data(n_markers=1.0,\n", - " binning_plots=(bin_plot,),\n", - " kernel_density_plots=(kd_plot,),\n", - " )" + "model.euler_fluid.set_save_data(\n", + " n_markers=1.0,\n", + " binning_plots=(bin_plot,),\n", + " kernel_density_plots=(kd_plot,),\n", + ")" ] }, { @@ -757,6 +768,7 @@ "source": [ "# propagator options\n", "from struphy.ode.utils import ButcherTableau\n", + "\n", "butcher = ButcherTableau(algo=\"forward_euler\")\n", "model.propagators.push_eta.options = model.propagators.push_eta.Options(butcher=butcher)\n", "\n", @@ -791,17 +803,18 @@ "source": [ "verbose = True\n", "\n", - "main.run(model,\n", - " params_path=None,\n", - " env=env,\n", - " base_units=base_units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -850,7 +863,7 @@ "x = np.linspace(l1, r1, pts_e1)\n", "y = np.linspace(l2, r2, pts_e2)\n", "xx, yy = np.meshgrid(x, y, indexing=\"ij\")\n", - "ee1, ee2, ee3 = simdata.n_sph[\"euler_fluid\"][\"view_0\"][\"grid_n_sph\"]\n", + "ee1, ee2, ee3 = simdata.n_sph[\"euler_fluid\"][\"view_0\"][\"grid_n_sph\"]\n", "eta1 = ee1[:, 0, 0]\n", "eta2 = ee2[0, :, 0]\n", "bc_x = simdata.f[\"euler_fluid\"][\"e1_e2\"][\"grid_e1\"]\n", @@ -873,20 +886,21 @@ "metadata": {}, "outputs": [], "source": [ - "import matplotlib.pyplot as plt \n", + "import matplotlib.pyplot as plt\n", + "\n", "plt.figure(figsize=(12, 15))\n", "\n", "# plots\n", "plt.subplot(3, 2, 1)\n", "plt.pcolor(xx, yy, n_fun(xx, yy, 0))\n", - "plt.axis('square')\n", - "plt.title('n_xyz initial')\n", + "plt.axis(\"square\")\n", + "plt.title(\"n_xyz initial\")\n", "plt.colorbar()\n", "\n", "plt.subplot(3, 2, 2)\n", "plt.pcolor(eta1, eta2, n3(eta1, eta2, 0, squeeze_out=True).T)\n", - "plt.axis('square')\n", - "plt.title('$\\hat{n}^{\\t{vol}}$ initial (volume form)')\n", + "plt.axis(\"square\")\n", + "plt.title(\"$\\hat{n}^{\\t{vol}}$ initial (volume form)\")\n", "plt.colorbar()\n", "\n", "make_scatter = True\n", @@ -895,12 +909,12 @@ " ax = plt.gca()\n", " ax.set_xticks(np.linspace(l1, r1, nx + 1))\n", " ax.set_yticks(np.linspace(l2, r2, ny + 1))\n", - " plt.tick_params(labelbottom = False) \n", + " plt.tick_params(labelbottom=False)\n", " coloring = weights\n", - " plt.scatter(positions[:, 0], positions[:, 1], c=coloring, s=.25)\n", - " plt.grid(c='k')\n", - " plt.axis('square')\n", - " plt.title('$\\hat{n}^{\\t{vol}}$ initial scatter (random)')\n", + " plt.scatter(positions[:, 0], positions[:, 1], c=coloring, s=0.25)\n", + " plt.grid(c=\"k\")\n", + " plt.axis(\"square\")\n", + " plt.title(\"$\\hat{n}^{\\t{vol}}$ initial scatter (random)\")\n", " plt.xlim(l1, r1)\n", " plt.ylim(l2, r2)\n", " plt.colorbar()\n", @@ -908,19 +922,19 @@ "plt.subplot(3, 2, 4)\n", "ax = plt.gca()\n", "ax.set_xticks(np.linspace(0, 1, nx + 1))\n", - "ax.set_yticks(np.linspace(0, 1., ny + 1))\n", - "plt.tick_params(labelbottom = False) \n", - "plt.pcolor(ee1[:,:,0], ee2[:,:,0], n_sph[:,:,0])\n", + "ax.set_yticks(np.linspace(0, 1.0, ny + 1))\n", + "plt.tick_params(labelbottom=False)\n", + "plt.pcolor(ee1[:, :, 0], ee2[:, :, 0], n_sph[:, :, 0])\n", "plt.grid()\n", - "plt.axis('square')\n", - "plt.title('n_sph initial (random)')\n", + "plt.axis(\"square\")\n", + "plt.title(\"n_sph initial (random)\")\n", "plt.colorbar()\n", "\n", "plt.subplot(3, 2, 5)\n", "ax = plt.gca()\n", "plt.pcolor(bc_x, bc_y, f_bin)\n", - "plt.axis('square')\n", - "plt.title('n_binned initial (random)')\n", + "plt.axis(\"square\")\n", + "plt.title(\"n_binned initial (random)\")\n", "plt.colorbar()" ] }, @@ -936,24 +950,24 @@ "\n", "positions = orbits[:, :, :3]\n", "\n", - "interval = Nt/10\n", + "interval = Nt / 10\n", "plot_ct = 0\n", "\n", "plt.figure(figsize=(12, 24))\n", "for i in range(Nt):\n", " if i % interval == 0:\n", - " print(f'{i = }')\n", + " print(f\"{i = }\")\n", " plot_ct += 1\n", " plt.subplot(4, 2, plot_ct)\n", - " ax = plt.gca() \n", + " ax = plt.gca()\n", " coloring = weights\n", - " plt.scatter(positions[i, :, 0], positions[i, :, 1], c=coloring, s=.25)\n", - " plt.axis('square')\n", - " plt.title('n0_scatter')\n", + " plt.scatter(positions[i, :, 0], positions[i, :, 1], c=coloring, s=0.25)\n", + " plt.axis(\"square\")\n", + " plt.title(\"n0_scatter\")\n", " plt.xlim(l1, r1)\n", " plt.ylim(l2, r2)\n", " plt.colorbar()\n", - " plt.title(f'Gas at t={i*dt}')\n", + " plt.title(f\"Gas at t={i * dt}\")\n", " if plot_ct == 8:\n", " break" ] diff --git a/tutorials/tutorial_04_vlasov_maxwell.ipynb b/tutorials/tutorial_04_vlasov_maxwell.ipynb index f35f0443f..7fa3795b0 100644 --- a/tutorials/tutorial_04_vlasov_maxwell.ipynb +++ b/tutorials/tutorial_04_vlasov_maxwell.ipynb @@ -32,18 +32,15 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", - "from struphy.geometry import domains\n", + "from struphy import main\n", "from struphy.fields_background import equils\n", - "from struphy.topology import grids\n", - "from struphy.io.options import DerhamOptions\n", - "from struphy.io.options import FieldsBackground\n", + "from struphy.geometry import domains\n", "from struphy.initial import perturbations\n", + "from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Time\n", "from struphy.kinetic_background import maxwellians\n", - "from struphy.pic.utilities import LoadingParameters, WeightsParameters, BoundaryParameters, BinningPlot\n", - "from struphy import main\n", - "\n", - "from struphy.models.kinetic import VlasovAmpereOneSpecies" + "from struphy.models.kinetic import VlasovAmpereOneSpecies\n", + "from struphy.pic.utilities import BinningPlot, BoundaryParameters, LoadingParameters, WeightsParameters\n", + "from struphy.topology import grids" ] }, { @@ -66,7 +63,7 @@ "base_units = BaseUnits()\n", "\n", "# time stepping\n", - "time_opts = Time(dt = 0.05, Tend = 0.5)#, Tend = 3.5\n", + "time_opts = Time(dt=0.05, Tend=0.5) # , Tend = 3.5\n", "\n", "# geometry\n", "r1 = 12.56\n", @@ -116,7 +113,9 @@ "loading_params = LoadingParameters(ppc=10000)\n", "weights_params = WeightsParameters(control_variate=True)\n", "boundary_params = BoundaryParameters()\n", - "model.kinetic_ions.set_markers(loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params)\n", + "model.kinetic_ions.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", + ")\n", "model.kinetic_ions.set_sorting_boxes()" ] }, @@ -215,17 +214,18 @@ "source": [ "verbose = True\n", "\n", - "main.run(model, \n", - " params_path=None, \n", - " env=env, \n", - " base_units=base_units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -262,8 +262,8 @@ "f_v1_init = simdata.f[\"kinetic_ions\"][\"v1\"][\"f_binned\"][0]\n", "\n", "plt.plot(v1_bins, f_v1_init)\n", - "plt.xlabel('vx')\n", - "plt.title('Initial Maxwellian');" + "plt.xlabel(\"vx\")\n", + "plt.title(\"Initial Maxwellian\");" ] }, { @@ -278,8 +278,8 @@ "df_e1_init = simdata.f[\"kinetic_ions\"][\"e1\"][\"delta_f_binned\"][0]\n", "\n", "plt.plot(e1_bins, df_e1_init)\n", - "plt.xlabel('$\\eta_1$')\n", - "plt.title('Initial spatial perturbation');" + "plt.xlabel(\"$\\eta_1$\")\n", + "plt.title(\"Initial spatial perturbation\");" ] }, { @@ -301,30 +301,30 @@ "\n", "plt.subplot(2, 2, 1)\n", "plt.pcolor(e1_bins, v1_bins, f_init.T)\n", - "plt.xlabel('$\\eta_1$')\n", - "plt.ylabel('$v_x$')\n", - "plt.title('Initial Maxwellian')\n", + "plt.xlabel(\"$\\eta_1$\")\n", + "plt.ylabel(\"$v_x$\")\n", + "plt.title(\"Initial Maxwellian\")\n", "plt.colorbar()\n", "\n", "plt.subplot(2, 2, 2)\n", "plt.pcolor(e1_bins, v1_bins, df_init.T)\n", - "plt.xlabel('$\\eta_1$')\n", - "plt.ylabel('$v_x$')\n", - "plt.title('Initial perturbation')\n", + "plt.xlabel(\"$\\eta_1$\")\n", + "plt.ylabel(\"$v_x$\")\n", + "plt.title(\"Initial perturbation\")\n", "plt.colorbar()\n", "\n", "plt.subplot(2, 2, 3)\n", "plt.pcolor(e1_bins, v1_bins, f_end.T)\n", - "plt.xlabel('$\\eta_1$')\n", - "plt.ylabel('$v_x$')\n", - "plt.title('Final Maxwellian')\n", + "plt.xlabel(\"$\\eta_1$\")\n", + "plt.ylabel(\"$v_x$\")\n", + "plt.title(\"Final Maxwellian\")\n", "plt.colorbar()\n", "\n", "plt.subplot(2, 2, 4)\n", "plt.pcolor(e1_bins, v1_bins, df_end.T)\n", - "plt.xlabel('$\\eta_1$')\n", - "plt.ylabel('$v_x$')\n", - "plt.title('Final perturbation')\n", + "plt.xlabel(\"$\\eta_1$\")\n", + "plt.ylabel(\"$v_x$\")\n", + "plt.title(\"Final perturbation\")\n", "plt.colorbar();" ] }, @@ -336,12 +336,12 @@ "source": [ "# electric field\n", "\n", - "e1, e2, e3 = simdata.grids_log \n", + "e1, e2, e3 = simdata.grids_log\n", "e_vals = simdata.spline_values[\"em_fields\"][\"e_field_log\"][0][0]\n", "\n", - "plt.plot(e1, e_vals[:, 0, 0], label='E')\n", - "plt.xlabel('$\\eta_1$')\n", - "plt.title('Initial electric field')\n", + "plt.plot(e1, e_vals[:, 0, 0], label=\"E\")\n", + "plt.xlabel(\"$\\eta_1$\")\n", + "plt.title(\"Initial electric field\")\n", "plt.legend();" ] } diff --git a/tutorials/tutorial_05_mapped_domains.ipynb b/tutorials/tutorial_05_mapped_domains.ipynb index f26724fdf..898cecde7 100644 --- a/tutorials/tutorial_05_mapped_domains.ipynb +++ b/tutorials/tutorial_05_mapped_domains.ipynb @@ -46,7 +46,7 @@ "outputs": [], "source": [ "for key, val in domain.params.items():\n", - " print(key, '=', val)" + " print(key, \"=\", val)" ] }, { @@ -98,7 +98,7 @@ "outputs": [], "source": [ "for attr in dir(domain):\n", - " if callable(getattr(domain, attr)) and '__' not in attr and attr[0] != '_':\n", + " if callable(getattr(domain, attr)) and \"__\" not in attr and attr[0] != \"_\":\n", " print(attr)" ] }, @@ -131,7 +131,7 @@ "metadata": {}, "outputs": [], "source": [ - "domain = domains.HollowCylinder(a1=.05)\n", + "domain = domains.HollowCylinder(a1=0.05)\n", "domain.show()" ] }, @@ -148,7 +148,7 @@ "metadata": {}, "outputs": [], "source": [ - "domain = domains.HollowCylinder(a1=.0)\n", + "domain = domains.HollowCylinder(a1=0.0)\n", "domain.show()" ] }, @@ -214,7 +214,7 @@ "outputs": [], "source": [ "for key, val in domain.params.items():\n", - " print(key, '=', val)" + " print(key, \"=\", val)" ] }, { @@ -230,7 +230,7 @@ "metadata": {}, "outputs": [], "source": [ - "domain = domains.HollowTorus(a1=.05, sfl=True, tor_period=1)\n", + "domain = domains.HollowTorus(a1=0.05, sfl=True, tor_period=1)\n", "domain.show()" ] }, @@ -269,8 +269,8 @@ "outputs": [], "source": [ "for key, val in domain.params.items():\n", - " if 'cx' not in key and 'cy' not in key:\n", - " print(key, '=', val)" + " if \"cx\" not in key and \"cy\" not in key:\n", + " print(key, \"=\", val)" ] }, { @@ -307,7 +307,7 @@ "metadata": {}, "outputs": [], "source": [ - "domain = domains.Tokamak(equilibrium=mhd_eq, psi_shifts=[.2, 2])\n", + "domain = domains.Tokamak(equilibrium=mhd_eq, psi_shifts=[0.2, 2])\n", "domain.show()" ] }, @@ -353,8 +353,8 @@ "outputs": [], "source": [ "for key, val in domain.params.items():\n", - " if 'cx' not in key and 'cy' not in key and 'cz' not in key:\n", - " print(key, '=', val)" + " if \"cx\" not in key and \"cy\" not in key and \"cz\" not in key:\n", + " print(key, \"=\", val)" ] }, { @@ -372,7 +372,7 @@ "source": [ "from struphy.fields_background.equils import GVECequilibrium\n", "\n", - "gvec_equil = GVECequilibrium(rmin=.1, use_nfp=False)\n", + "gvec_equil = GVECequilibrium(rmin=0.1, use_nfp=False)\n", "domain = domains.GVECunit(gvec_equil)\n", "domain.show()" ] diff --git a/tutorials/tutorial_06_mhd_equilibria.ipynb b/tutorials/tutorial_06_mhd_equilibria.ipynb index 176374284..c72d237b0 100644 --- a/tutorials/tutorial_06_mhd_equilibria.ipynb +++ b/tutorials/tutorial_06_mhd_equilibria.ipynb @@ -25,9 +25,10 @@ "metadata": {}, "outputs": [], "source": [ + "import numpy as np\n", + "\n", "from struphy.fields_background import equils\n", - "from struphy.geometry import domains\n", - "import numpy as np" + "from struphy.geometry import domains" ] }, { @@ -43,8 +44,8 @@ "metadata": {}, "outputs": [], "source": [ - "mhd_equil = equils.ScrewPinch(R0=1.)\n", - "mhd_equil.domain = domains.HollowCylinder(a1=1e-8, a2=1, Lz=2*np.pi)\n", + "mhd_equil = equils.ScrewPinch(R0=1.0)\n", + "mhd_equil.domain = domains.HollowCylinder(a1=1e-8, a2=1, Lz=2 * np.pi)\n", "mhd_equil.show()" ] }, @@ -98,7 +99,7 @@ "outputs": [], "source": [ "mhd_equil = equils.GVECequilibrium(use_nfp=False)\n", - "mhd_equil.show() " + "mhd_equil.show()" ] }, { diff --git a/tutorials_old/tutorial_01_kinetic_particles.ipynb b/tutorials_old/tutorial_01_kinetic_particles.ipynb index fa48d4aba..4ed692ed9 100644 --- a/tutorials_old/tutorial_01_kinetic_particles.ipynb +++ b/tutorials_old/tutorial_01_kinetic_particles.ipynb @@ -45,11 +45,11 @@ "from struphy.geometry.domains import Cuboid\n", "\n", "l1 = -5\n", - "r1 = 5.\n", + "r1 = 5.0\n", "l2 = -7\n", - "r2 = 7.\n", - "l3 = -1.\n", - "r3 = 1.\n", + "r2 = 7.0\n", + "l3 = -1.0\n", + "r3 = 1.0\n", "domain = Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" ] }, @@ -62,14 +62,11 @@ "from struphy.pic.particles import Particles6D\n", "\n", "Np = 15\n", - "bc = ['reflect', 'reflect', 'periodic']\n", - "loading_params = {'seed': None}\n", + "bc = [\"reflect\", \"reflect\", \"periodic\"]\n", + "loading_params = {\"seed\": None}\n", "\n", "# instantiate Particle object\n", - "particles = Particles6D(Np=Np, \n", - " bc=bc, \n", - " domain=domain,\n", - " loading_params=loading_params)" + "particles = Particles6D(Np=Np, bc=bc, domain=domain, loading_params=loading_params)" ] }, { @@ -118,22 +115,24 @@ "source": [ "from matplotlib import pyplot as plt\n", "\n", - "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", "\n", "fig = plt.figure()\n", "ax = fig.gca()\n", "\n", "for i, pos in enumerate(pushed_pos):\n", " ax.scatter(pos[0], pos[1], c=colors[i % 4])\n", - " ax.arrow(pos[0], pos[1], particles.velocities[i, 0], particles.velocities[i, 1], color=colors[i % 4], head_width=.2)\n", - "\n", - "ax.plot([l1, l1], [l2, r2], 'k')\n", - "ax.plot([r1, r1], [l2, r2], 'k')\n", - "ax.plot([l1, r1], [l2, l2], 'k')\n", - "ax.plot([l1, r1], [r2, r2], 'k')\n", + " ax.arrow(\n", + " pos[0], pos[1], particles.velocities[i, 0], particles.velocities[i, 1], color=colors[i % 4], head_width=0.2\n", + " )\n", + "\n", + "ax.plot([l1, l1], [l2, r2], \"k\")\n", + "ax.plot([r1, r1], [l2, r2], \"k\")\n", + "ax.plot([l1, r1], [l2, l2], \"k\")\n", + "ax.plot([l1, r1], [r2, r2], \"k\")\n", "ax.set_xlim(-6.5, 6.5)\n", "ax.set_ylim(-9, 9)\n", - "ax.set_title('Initial conditions');" + "ax.set_title(\"Initial conditions\");" ] }, { @@ -175,12 +174,13 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", "import math\n", "\n", + "import numpy as np\n", + "\n", "# time stepping\n", - "Tend = 10. \n", - "dt = .2\n", + "Tend = 10.0\n", + "dt = 0.2\n", "Nt = int(Tend / dt)\n", "\n", "pos = np.zeros((Nt + 1, Np, 3), dtype=float)\n", @@ -188,20 +188,20 @@ "\n", "pos[0] = pushed_pos\n", "\n", - "time = 0.\n", + "time = 0.0\n", "n = 0\n", "while time < (Tend - dt):\n", " time += dt\n", " n += 1\n", - " \n", + "\n", " # advance in time\n", " prop_eta(dt)\n", - " \n", + "\n", " # positions on the physical domain Omega\n", " pos[n] = domain(particles.positions).T\n", - " \n", + "\n", " # scaling for plotting\n", - " alpha[n] = (Tend - time)/Tend" + " alpha[n] = (Tend - time) / Tend" ] }, { @@ -213,16 +213,15 @@ "for i in range(Np):\n", " ax.scatter(pos[:, i, 0], pos[:, i, 1], c=colors[i % 4], alpha=alpha)\n", "\n", - "ax.plot([l1, l1], [l2, r2], 'k')\n", - "ax.plot([r1, r1], [l2, r2], 'k')\n", - "ax.plot([l1, r1], [l2, l2], 'k')\n", - "ax.plot([l1, r1], [r2, r2], 'k')\n", - "ax.set_xlabel('x')\n", - "ax.set_ylabel('y')\n", + "ax.plot([l1, l1], [l2, r2], \"k\")\n", + "ax.plot([r1, r1], [l2, r2], \"k\")\n", + "ax.plot([l1, r1], [l2, l2], \"k\")\n", + "ax.plot([l1, r1], [r2, r2], \"k\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", "ax.set_xlim(-6.5, 6.5)\n", "ax.set_ylim(-9, 9)\n", - "ax.set_title(f'{math.ceil(Tend/dt)} time steps (full color at t=0)');\n", - "\n", + "ax.set_title(f\"{math.ceil(Tend / dt)} time steps (full color at t=0)\")\n", "fig" ] }, @@ -246,9 +245,9 @@ "source": [ "from struphy.geometry.domains import HollowCylinder\n", "\n", - "a1 = 0.\n", - "a2 = 5.\n", - "Lz = 1.\n", + "a1 = 0.0\n", + "a2 = 5.0\n", + "Lz = 1.0\n", "domain = HollowCylinder(a1=a1, a2=a2, Lz=Lz)" ] }, @@ -260,19 +259,15 @@ "source": [ "# instantiate Particle object\n", "Np = 1000\n", - "bc = ['remove', 'periodic', 'periodic']\n", - "loading_params = {'seed': None}\n", + "bc = [\"remove\", \"periodic\", \"periodic\"]\n", + "loading_params = {\"seed\": None}\n", "\n", - "particles = Particles6D(Np=Np, \n", - " bc=bc, \n", - " loading_params=loading_params)\n", + "particles = Particles6D(Np=Np, bc=bc, loading_params=loading_params)\n", "\n", "# instantiate another Particle object\n", - "name = 'test_uni'\n", - "loading_params = {'seed': None, 'spatial': 'disc'}\n", - "particles_uni = Particles6D(Np=Np, \n", - " bc=bc, \n", - " loading_params=loading_params)" + "name = \"test_uni\"\n", + "loading_params = {\"seed\": None, \"spatial\": \"disc\"}\n", + "particles_uni = Particles6D(Np=Np, bc=bc, loading_params=loading_params)" ] }, { @@ -302,27 +297,27 @@ "metadata": {}, "outputs": [], "source": [ - "fig = plt.figure(figsize=(10, 6)) \n", + "fig = plt.figure(figsize=(10, 6))\n", "\n", "plt.subplot(1, 2, 1)\n", - "plt.scatter(pushed_pos[:, 0], pushed_pos[:, 1], s=2.)\n", - "circle1 = plt.Circle((0, 0), a2, color='k', fill=False)\n", + "plt.scatter(pushed_pos[:, 0], pushed_pos[:, 1], s=2.0)\n", + "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "ax = plt.gca()\n", "ax.add_patch(circle1)\n", - "ax.set_aspect('equal')\n", - "plt.xlabel('x')\n", - "plt.ylabel('y')\n", - "plt.title('Draw uniform in logical space')\n", + "ax.set_aspect(\"equal\")\n", + "plt.xlabel(\"x\")\n", + "plt.ylabel(\"y\")\n", + "plt.title(\"Draw uniform in logical space\")\n", "\n", "plt.subplot(1, 2, 2)\n", - "plt.scatter(pushed_pos_uni[:, 0], pushed_pos_uni[:, 1], s=2.)\n", - "circle2 = plt.Circle((0, 0), a2, color='k', fill=False)\n", + "plt.scatter(pushed_pos_uni[:, 0], pushed_pos_uni[:, 1], s=2.0)\n", + "circle2 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "ax = plt.gca()\n", "ax.add_patch(circle2)\n", - "ax.set_aspect('equal')\n", - "plt.xlabel('x')\n", - "plt.ylabel('y')\n", - "plt.title('Draw uniform on disc');" + "ax.set_aspect(\"equal\")\n", + "plt.xlabel(\"x\")\n", + "plt.ylabel(\"y\")\n", + "plt.title(\"Draw uniform on disc\");" ] }, { @@ -333,13 +328,10 @@ "source": [ "# instantiate Particle object\n", "Np = 15\n", - "bc = ['reflect', 'periodic', 'periodic']\n", - "loading_params = {'seed': None}\n", + "bc = [\"reflect\", \"periodic\", \"periodic\"]\n", + "loading_params = {\"seed\": None}\n", "\n", - "particles = Particles6D(Np=Np, \n", - " bc=bc, \n", - " domain=domain,\n", - " loading_params=loading_params)" + "particles = Particles6D(Np=Np, bc=bc, domain=domain, loading_params=loading_params)" ] }, { @@ -368,20 +360,22 @@ "metadata": {}, "outputs": [], "source": [ - "fig = plt.figure() \n", + "fig = plt.figure()\n", "ax = fig.gca()\n", "\n", "for n, pos in enumerate(pushed_pos):\n", " ax.scatter(pos[0], pos[1], c=colors[n % 4])\n", - " ax.arrow(pos[0], pos[1], particles.velocities[n, 0], particles.velocities[n, 1], color=colors[n % 4], head_width=.2)\n", + " ax.arrow(\n", + " pos[0], pos[1], particles.velocities[n, 0], particles.velocities[n, 1], color=colors[n % 4], head_width=0.2\n", + " )\n", "\n", - "circle1 = plt.Circle((0, 0), a2, color='k', fill=False)\n", + "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "\n", "ax.add_patch(circle1)\n", - "ax.set_aspect('equal')\n", - "ax.set_xlabel('x')\n", - "ax.set_ylabel('y')\n", - "ax.set_title('Initial conditions');" + "ax.set_aspect(\"equal\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "ax.set_title(\"Initial conditions\");" ] }, { @@ -411,8 +405,8 @@ "outputs": [], "source": [ "# time stepping\n", - "Tend = 10. \n", - "dt = .2\n", + "Tend = 10.0\n", + "dt = 0.2\n", "Nt = int(Tend / dt)\n", "\n", "pos = np.zeros((Nt + 1, Np, 3), dtype=float)\n", @@ -420,20 +414,20 @@ "\n", "pos[0] = pushed_pos\n", "\n", - "time = 0.\n", + "time = 0.0\n", "n = 0\n", - "while time < (Tend -dt):\n", + "while time < (Tend - dt):\n", " time += dt\n", " n += 1\n", - " \n", + "\n", " # advance in time\n", " prop_eta(dt)\n", - " \n", + "\n", " # positions on the physical domain Omega\n", " pos[n] = domain(particles.positions).T\n", - " \n", + "\n", " # scaling for plotting\n", - " alpha[n] = (Tend - time)/Tend" + " alpha[n] = (Tend - time) / Tend" ] }, { @@ -446,14 +440,13 @@ "for i in range(Np):\n", " ax.scatter(pos[:, i, 0], pos[:, i, 1], c=colors[i % 4], alpha=alpha)\n", "\n", - "circle1 = plt.Circle((0, 0), a2, color='k', fill=False)\n", + "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "\n", "ax.add_patch(circle1)\n", - "ax.set_aspect('equal')\n", - "ax.set_xlabel('x')\n", - "ax.set_ylabel('y')\n", - "ax.set_title(f'{math.ceil(Tend/dt)} time steps (full color at t=0)');\n", - "\n", + "ax.set_aspect(\"equal\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "ax.set_title(f\"{math.ceil(Tend / dt)} time steps (full color at t=0)\")\n", "fig" ] }, @@ -485,9 +478,9 @@ "source": [ "from struphy.geometry.domains import HollowCylinder\n", "\n", - "a1 = 0.\n", - "a2 = 5.\n", - "Lz = 1.\n", + "a1 = 0.0\n", + "a2 = 5.0\n", + "Lz = 1.0\n", "domain = HollowCylinder(a1=a1, a2=a2, Lz=Lz)" ] }, @@ -499,12 +492,10 @@ "source": [ "# instantiate Particle object\n", "Np = 20\n", - "bc = ['remove', 'periodic', 'periodic']\n", - "loading_params = {'seed': None}\n", + "bc = [\"remove\", \"periodic\", \"periodic\"]\n", + "loading_params = {\"seed\": None}\n", "\n", - "particles = Particles6D(Np=Np, \n", - " bc=bc, \n", - " loading_params=loading_params)" + "particles = Particles6D(Np=Np, bc=bc, loading_params=loading_params)" ] }, { @@ -533,20 +524,22 @@ "metadata": {}, "outputs": [], "source": [ - "fig = plt.figure() \n", + "fig = plt.figure()\n", "ax = fig.gca()\n", "\n", "for n, pos in enumerate(pushed_pos):\n", " ax.scatter(pos[0], pos[1], c=colors[n % 4])\n", - " ax.arrow(pos[0], pos[1], particles.velocities[n, 0], particles.velocities[n, 1], color=colors[n % 4], head_width=.2)\n", + " ax.arrow(\n", + " pos[0], pos[1], particles.velocities[n, 0], particles.velocities[n, 1], color=colors[n % 4], head_width=0.2\n", + " )\n", "\n", - "circle1 = plt.Circle((0, 0), a2, color='k', fill=False)\n", + "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "\n", "ax.add_patch(circle1)\n", - "ax.set_aspect('equal')\n", - "ax.set_xlabel('x')\n", - "ax.set_ylabel('y')\n", - "ax.set_title('Initial conditions');" + "ax.set_aspect(\"equal\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "ax.set_title(\"Initial conditions\");" ] }, { @@ -570,9 +563,9 @@ "source": [ "from struphy.fields_background.equils import HomogenSlab\n", "\n", - "B0x = 0.\n", - "B0y = 0.\n", - "B0z = 1.\n", + "B0x = 0.0\n", + "B0y = 0.0\n", + "B0z = 1.0\n", "equil = HomogenSlab(B0x=B0x, B0y=B0y, B0z=B0z)" ] }, @@ -592,8 +585,8 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.fields_background.projected_equils import ProjectedMHDequilibrium\n", "from struphy.feec.psydac_derham import Derham\n", + "from struphy.fields_background.projected_equils import ProjectedMHDequilibrium\n", "\n", "# instantiate Derham object\n", "Nel = [16, 16, 32]\n", @@ -635,8 +628,8 @@ "outputs": [], "source": [ "# time stepping\n", - "Tend = 10. - 1e-6\n", - "dt = .2\n", + "Tend = 10.0 - 1e-6\n", + "dt = 0.2\n", "Nt = int(Tend / dt)\n", "\n", "pos = []\n", @@ -648,25 +641,25 @@ " marker_col[m_id] = colors[int(m_id) % 4]\n", "ids_wo_holes = []\n", "\n", - "time = 0.\n", + "time = 0.0\n", "n = 0\n", "while time < (Tend - dt):\n", " time += dt\n", " n += 1\n", - " \n", + "\n", " # advance in time\n", - " prop_vxB(dt/2)\n", + " prop_vxB(dt / 2)\n", " prop_eta(dt)\n", - " prop_vxB(dt/2)\n", - " \n", + " prop_vxB(dt / 2)\n", + "\n", " # positions on the physical domain Omega (can change shape when particles are lost)\n", " pos += [domain(particles.positions).T]\n", "\n", " # id's of non-holes\n", " ids_wo_holes += [np.int64(particles.markers_wo_holes[:, -1])]\n", - " \n", + "\n", " # scaling for plotting\n", - " alpha[n] = (Tend - time)/Tend" + " alpha[n] = (Tend - time) / Tend" ] }, { @@ -682,14 +675,13 @@ " cs += [marker_col[ii]]\n", " ax.scatter(po[:, 0], po[:, 1], c=cs, alpha=alph)\n", "\n", - "circle1 = plt.Circle((0, 0), a2, color='k', fill=False)\n", + "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "\n", "ax.add_patch(circle1)\n", - "ax.set_aspect('equal')\n", - "ax.set_xlabel('x')\n", - "ax.set_ylabel('y')\n", - "ax.set_title(f'{math.ceil(Tend/dt)} time steps (full color at t=0)');\n", - "\n", + "ax.set_aspect(\"equal\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "ax.set_title(f\"{math.ceil(Tend / dt)} time steps (full color at t=0)\")\n", "fig" ] }, @@ -712,9 +704,9 @@ "source": [ "from struphy.fields_background.equils import EQDSKequilibrium\n", "\n", - "n1 = 0.\n", - "n2 = 0.\n", - "na = 1.\n", + "n1 = 0.0\n", + "n2 = 0.0\n", + "na = 1.0\n", "equil = EQDSKequilibrium(n1=n1, n2=n2, na=na)\n", "equil.params" ] @@ -737,12 +729,8 @@ "Nel = (28, 72)\n", "p = (3, 3)\n", "psi_power = 0.6\n", - "psi_shifts = (1e-6, 1.)\n", - "domain = Tokamak(equilibrium=equil, \n", - " Nel=Nel,\n", - " p=p,\n", - " psi_power=psi_power,\n", - " psi_shifts=psi_shifts)" + "psi_shifts = (1e-6, 1.0)\n", + "domain = Tokamak(equilibrium=equil, Nel=Nel, p=p, psi_power=psi_power, psi_shifts=psi_shifts)" ] }, { @@ -812,9 +800,9 @@ "import numpy as np\n", "\n", "# logical grid on the unit cube\n", - "e1 = np.linspace(0., 1., 101)\n", - "e2 = np.linspace(0., 1., 101)\n", - "e3 = np.linspace(0., 1., 101)\n", + "e1 = np.linspace(0.0, 1.0, 101)\n", + "e2 = np.linspace(0.0, 1.0, 101)\n", + "e3 = np.linspace(0.0, 1.0, 101)\n", "\n", "# move away from the singular point r = 0\n", "e1[0] += 1e-5" @@ -827,11 +815,11 @@ "outputs": [], "source": [ "# logical coordinates of the poloidal plane at phi = 0\n", - "eta_poloidal = (e1, e2, 0.)\n", + "eta_poloidal = (e1, e2, 0.0)\n", "# logical coordinates of the top view at theta = 0\n", - "eta_topview_1 = (e1, 0., e3)\n", + "eta_topview_1 = (e1, 0.0, e3)\n", "# logical coordinates of the top view at theta = pi\n", - "eta_topview_2 = (e1, .5, e3)" + "eta_topview_2 = (e1, 0.5, e3)" ] }, { @@ -845,9 +833,9 @@ "x_top1, y_top1, z_top1 = domain(*eta_topview_1, squeeze_out=True)\n", "x_top2, y_top2, z_top2 = domain(*eta_topview_2, squeeze_out=True)\n", "\n", - "print(f'{x_pol.shape = }')\n", - "print(f'{x_top1.shape = }')\n", - "print(f'{x_top2.shape = }')" + "print(f\"{x_pol.shape = }\")\n", + "print(f\"{x_top1.shape = }\")\n", + "print(f\"{x_top2.shape = }\")" ] }, { @@ -874,36 +862,35 @@ "ax_top.contourf(x_top2, y_top2, equil.absB0(*eta_topview_2, squeeze_out=True), levels=levels)\n", "\n", "# last closed flux surface, poloidal\n", - "ax.plot(x_pol[-1], z_pol[-1], color='k')\n", + "ax.plot(x_pol[-1], z_pol[-1], color=\"k\")\n", "\n", "# last closed flux surface, toroidal\n", - "ax_top.plot(x_top1[-1], y_top1[-1], color='k')\n", - "ax_top.plot(x_top2[-1], y_top2[-1], color='k')\n", + "ax_top.plot(x_top1[-1], y_top1[-1], color=\"k\")\n", + "ax_top.plot(x_top2[-1], y_top2[-1], color=\"k\")\n", "\n", "# limiter, poloidal\n", - "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, 'tab:orange')\n", - "ax.axis('equal')\n", - "ax.set_xlabel('R')\n", - "ax.set_ylabel('Z')\n", - "ax.set_title('abs(B) at $\\phi=0$')\n", - "fig.colorbar(im);\n", - "\n", + "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, \"tab:orange\")\n", + "ax.axis(\"equal\")\n", + "ax.set_xlabel(\"R\")\n", + "ax.set_ylabel(\"Z\")\n", + "ax.set_title(\"abs(B) at $\\phi=0$\")\n", + "fig.colorbar(im)\n", "# limiter, toroidal\n", "limiter_Rmax = np.max(equil.limiter_pts_R)\n", "limiter_Rmin = np.min(equil.limiter_pts_R)\n", "\n", - "thetas = 2*np.pi*e2\n", + "thetas = 2 * np.pi * e2\n", "limiter_x_max = limiter_Rmax * np.cos(thetas)\n", - "limiter_y_max = - limiter_Rmax * np.sin(thetas)\n", + "limiter_y_max = -limiter_Rmax * np.sin(thetas)\n", "limiter_x_min = limiter_Rmin * np.cos(thetas)\n", - "limiter_y_min = - limiter_Rmin * np.sin(thetas)\n", - "\n", - "ax_top.plot(limiter_x_max, limiter_y_max, 'tab:orange')\n", - "ax_top.plot(limiter_x_min, limiter_y_min, 'tab:orange')\n", - "ax_top.axis('equal')\n", - "ax_top.set_xlabel('x')\n", - "ax_top.set_ylabel('y')\n", - "ax_top.set_title('abs(B) at $Z=0$')\n", + "limiter_y_min = -limiter_Rmin * np.sin(thetas)\n", + "\n", + "ax_top.plot(limiter_x_max, limiter_y_max, \"tab:orange\")\n", + "ax_top.plot(limiter_x_min, limiter_y_min, \"tab:orange\")\n", + "ax_top.axis(\"equal\")\n", + "ax_top.set_xlabel(\"x\")\n", + "ax_top.set_ylabel(\"y\")\n", + "ax_top.set_title(\"abs(B) at $Z=0$\")\n", "fig.colorbar(im_top);" ] }, @@ -915,21 +902,19 @@ "source": [ "# instantiate Particle object\n", "Np = 4\n", - "bc = ['remove', 'periodic', 'periodic']\n", - "bufsize = 2.\n", + "bc = [\"remove\", \"periodic\", \"periodic\"]\n", + "bufsize = 2.0\n", "\n", - "initial = [[.501, 0.001, 0.001, 0., 0.0450, -0.04], # co-passing particle\n", - " [.511, 0.001, 0.001, 0., -0.0450, -0.04], # counter passing particle\n", - " [.521, 0.001, 0.001, 0., 0.0105, -0.04], # co-trapped particle\n", - " [.531, 0.001, 0.001, 0., -0.0155, -0.04]]\n", + "initial = [\n", + " [0.501, 0.001, 0.001, 0.0, 0.0450, -0.04], # co-passing particle\n", + " [0.511, 0.001, 0.001, 0.0, -0.0450, -0.04], # counter passing particle\n", + " [0.521, 0.001, 0.001, 0.0, 0.0105, -0.04], # co-trapped particle\n", + " [0.531, 0.001, 0.001, 0.0, -0.0155, -0.04],\n", + "]\n", "\n", - "loading_params = {'seed': 1608,\n", - " 'initial' : initial}\n", + "loading_params = {\"seed\": 1608, \"initial\": initial}\n", "\n", - "particles = Particles6D(Np=Np, \n", - " bc=bc, \n", - " loading_params=loading_params,\n", - " bufsize=bufsize)" + "particles = Particles6D(Np=Np, bc=bc, loading_params=loading_params, bufsize=bufsize)" ] }, { @@ -959,7 +944,7 @@ "outputs": [], "source": [ "# compute R-coordinate\n", - "pushed_r = np.sqrt(pushed_pos[:, 0]**2 + pushed_pos[:, 1]**2)" + "pushed_r = np.sqrt(pushed_pos[:, 0] ** 2 + pushed_pos[:, 1] ** 2)" ] }, { @@ -968,30 +953,34 @@ "metadata": {}, "outputs": [], "source": [ - "labels = ['co-passing',\n", - " 'counter passing',\n", - " 'co_trapped',\n", - " 'counter-trapped']\n", + "labels = [\"co-passing\", \"counter passing\", \"co_trapped\", \"counter-trapped\"]\n", "\n", "for n, (r, pos) in enumerate(zip(pushed_r, pushed_pos)):\n", - " # poloidal \n", + " # poloidal\n", " ax.scatter(r, pos[2], c=colors[n % 4], label=labels[n])\n", - " ax.arrow(r, pos[2], particles.velocities[n, 0], particles.velocities[n, 2]*10, color=colors[n % 4], head_width=.05)\n", + " ax.arrow(\n", + " r, pos[2], particles.velocities[n, 0], particles.velocities[n, 2] * 10, color=colors[n % 4], head_width=0.05\n", + " )\n", " # topview\n", " ax_top.scatter(pos[0], pos[1], c=colors[n % 4], label=labels[n])\n", - " ax_top.arrow(pos[0], pos[1], particles.velocities[n, 0], particles.velocities[n, 1]*10, color=colors[n % 4], head_width=.05)\n", - "\n", - "ax.set_xlabel('R')\n", - "ax.set_ylabel('Z')\n", - "ax.set_title('Initial conditions')\n", - "ax.legend();\n", - "\n", - "ax_top.set_xlabel('x')\n", - "ax_top.set_ylabel('y')\n", - "ax_top.set_title('Initial conditions')\n", - "ax_top.legend();\n", - "\n", - "fig " + " ax_top.arrow(\n", + " pos[0],\n", + " pos[1],\n", + " particles.velocities[n, 0],\n", + " particles.velocities[n, 1] * 10,\n", + " color=colors[n % 4],\n", + " head_width=0.05,\n", + " )\n", + "\n", + "ax.set_xlabel(\"R\")\n", + "ax.set_ylabel(\"Z\")\n", + "ax.set_title(\"Initial conditions\")\n", + "ax.legend()\n", + "ax_top.set_xlabel(\"x\")\n", + "ax_top.set_ylabel(\"y\")\n", + "ax_top.set_title(\"Initial conditions\")\n", + "ax_top.legend()\n", + "fig" ] }, { @@ -1040,34 +1029,33 @@ "outputs": [], "source": [ "# time stepping\n", - "Tend = 3000. - 1e-6\n", - "dt = .2\n", + "Tend = 3000.0 - 1e-6\n", + "dt = 0.2\n", "Nt = int(Tend / dt)\n", "\n", "pos = np.zeros((Nt + 2, Np, 3), dtype=float)\n", "r = np.zeros((Nt + 2, Np), dtype=float)\n", "\n", "pos[0] = pushed_pos\n", - "r[0] = np.sqrt(pushed_pos[:, 0]**2 + pushed_pos[:, 1]**2)\n", + "r[0] = np.sqrt(pushed_pos[:, 0] ** 2 + pushed_pos[:, 1] ** 2)\n", "\n", - "time = 0.\n", + "time = 0.0\n", "n = 0\n", "while time < Tend:\n", " time += dt\n", " n += 1\n", - " \n", + "\n", " # advance in time\n", - " prop_vxB(dt/2)\n", + " prop_vxB(dt / 2)\n", " prop_eta(dt)\n", - " prop_vxB(dt/2)\n", - " \n", + " prop_vxB(dt / 2)\n", + "\n", " # positions on the physical domain Omega\n", " pushed_pos = domain(particles.positions).T\n", - " \n", + "\n", " # compute R-ccordinate\n", " pos[n] = pushed_pos\n", - " r[n] = np.sqrt(pushed_pos[:, 0]**2 + pushed_pos[:, 1]**2)\n", - " " + " r[n] = np.sqrt(pushed_pos[:, 0] ** 2 + pushed_pos[:, 1] ** 2)" ] }, { @@ -1076,16 +1064,15 @@ "metadata": {}, "outputs": [], "source": [ - "# make scatter plot for each particle \n", + "# make scatter plot for each particle\n", "for i in range(pos.shape[1]):\n", - " # poloidal \n", + " # poloidal\n", " ax.scatter(r[:, i], pos[:, i, 2], c=colors[i % 4], s=1)\n", " # top view\n", " ax_top.scatter(pos[:, i, 0], pos[:, i, 1], c=colors[i % 4], s=1)\n", "\n", - "ax.set_title(f'{math.ceil(Tend/dt)} time steps')\n", - "ax_top.set_title(f'{math.ceil(Tend/dt)} time steps');\n", - "\n", + "ax.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", + "ax_top.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", "fig" ] }, @@ -1110,22 +1097,19 @@ "\n", "# instantiate Particle object\n", "Np = 4\n", - "bc = ['remove', 'periodic', 'periodic']\n", - "bufsize = 2.\n", + "bc = [\"remove\", \"periodic\", \"periodic\"]\n", + "bufsize = 2.0\n", "\n", - "initial = [[.501, 0.001, 0.001, -1.935 , 1.72], # co-passing particle\n", - " [.501, 0.001, 0.001, 1.935 , 1.72], # couner-passing particle\n", - " [.501, 0.001, 0.001, -0.6665, 1.72], # co-trapped particle\n", - " [.501, 0.001, 0.001, 0.4515, 1.72]] # counter-trapped particle\n", + "initial = [\n", + " [0.501, 0.001, 0.001, -1.935, 1.72], # co-passing particle\n", + " [0.501, 0.001, 0.001, 1.935, 1.72], # couner-passing particle\n", + " [0.501, 0.001, 0.001, -0.6665, 1.72], # co-trapped particle\n", + " [0.501, 0.001, 0.001, 0.4515, 1.72],\n", + "] # counter-trapped particle\n", "\n", - "loading_params = {'seed': 1608,\n", - " 'initial' : initial}\n", + "loading_params = {\"seed\": 1608, \"initial\": initial}\n", "\n", - "particles = Particles5D(proj_equil,\n", - " Np=Np, \n", - " bc=bc, \n", - " loading_params=loading_params,\n", - " bufsize=bufsize)" + "particles = Particles5D(proj_equil, Np=Np, bc=bc, loading_params=loading_params, bufsize=bufsize)" ] }, { @@ -1155,7 +1139,7 @@ "outputs": [], "source": [ "# compute R-coordinate\n", - "pushed_r = np.sqrt(pushed_pos[:, 0]**2 + pushed_pos[:, 1]**2)" + "pushed_r = np.sqrt(pushed_pos[:, 0] ** 2 + pushed_pos[:, 1] ** 2)" ] }, { @@ -1191,36 +1175,35 @@ "ax_top.contourf(x_top2, y_top2, equil.absB0(*eta_topview_2, squeeze_out=True), levels=levels)\n", "\n", "# last closed flux surface, poloidal\n", - "ax.plot(x_pol[-1], z_pol[-1], color='k')\n", + "ax.plot(x_pol[-1], z_pol[-1], color=\"k\")\n", "\n", "# last closed flux surface, toroidal\n", - "ax_top.plot(x_top1[-1], y_top1[-1], color='k')\n", - "ax_top.plot(x_top2[-1], y_top2[-1], color='k')\n", + "ax_top.plot(x_top1[-1], y_top1[-1], color=\"k\")\n", + "ax_top.plot(x_top2[-1], y_top2[-1], color=\"k\")\n", "\n", "# limiter, poloidal\n", - "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, 'tab:orange')\n", - "ax.axis('equal')\n", - "ax.set_xlabel('R')\n", - "ax.set_ylabel('Z')\n", - "ax.set_title('abs(B) at $\\phi=0$')\n", - "fig.colorbar(im);\n", - "\n", + "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, \"tab:orange\")\n", + "ax.axis(\"equal\")\n", + "ax.set_xlabel(\"R\")\n", + "ax.set_ylabel(\"Z\")\n", + "ax.set_title(\"abs(B) at $\\phi=0$\")\n", + "fig.colorbar(im)\n", "# limiter, toroidal\n", "limiter_Rmax = np.max(equil.limiter_pts_R)\n", "limiter_Rmin = np.min(equil.limiter_pts_R)\n", "\n", - "thetas = 2*np.pi*e2\n", + "thetas = 2 * np.pi * e2\n", "limiter_x_max = limiter_Rmax * np.cos(thetas)\n", - "limiter_y_max = - limiter_Rmax * np.sin(thetas)\n", + "limiter_y_max = -limiter_Rmax * np.sin(thetas)\n", "limiter_x_min = limiter_Rmin * np.cos(thetas)\n", - "limiter_y_min = - limiter_Rmin * np.sin(thetas)\n", - "\n", - "ax_top.plot(limiter_x_max, limiter_y_max, 'tab:orange')\n", - "ax_top.plot(limiter_x_min, limiter_y_min, 'tab:orange')\n", - "ax_top.axis('equal')\n", - "ax_top.set_xlabel('x')\n", - "ax_top.set_ylabel('y')\n", - "ax_top.set_title('abs(B) at $Z=0$')\n", + "limiter_y_min = -limiter_Rmin * np.sin(thetas)\n", + "\n", + "ax_top.plot(limiter_x_max, limiter_y_max, \"tab:orange\")\n", + "ax_top.plot(limiter_x_min, limiter_y_min, \"tab:orange\")\n", + "ax_top.axis(\"equal\")\n", + "ax_top.set_xlabel(\"x\")\n", + "ax_top.set_ylabel(\"y\")\n", + "ax_top.set_title(\"abs(B) at $Z=0$\")\n", "fig.colorbar(im_top);" ] }, @@ -1230,29 +1213,24 @@ "metadata": {}, "outputs": [], "source": [ - "labels = ['co-passing',\n", - " 'counter passing',\n", - " 'co_trapped',\n", - " 'counter-trapped']\n", + "labels = [\"co-passing\", \"counter passing\", \"co_trapped\", \"counter-trapped\"]\n", "\n", "for n, (r, pos) in enumerate(zip(pushed_r, pushed_pos)):\n", - " # poloidal \n", + " # poloidal\n", " ax.scatter(r, pos[2], c=colors[n % 4], label=labels[n])\n", " # topview\n", " ax_top.scatter(pos[0], pos[1], c=colors[n % 4], label=labels[n])\n", - " ax_top.arrow(pos[0], pos[1], 0., particles.velocities[n, 0]/5, color=colors[n % 4], head_width=.05)\n", - "\n", - "ax.set_xlabel('R')\n", - "ax.set_ylabel('Z')\n", - "ax.set_title('Initial conditions')\n", - "ax.legend();\n", - "\n", - "ax_top.set_xlabel('x')\n", - "ax_top.set_ylabel('y')\n", - "ax_top.set_title('Initial conditions')\n", - "ax_top.legend();\n", - "\n", - "fig " + " ax_top.arrow(pos[0], pos[1], 0.0, particles.velocities[n, 0] / 5, color=colors[n % 4], head_width=0.05)\n", + "\n", + "ax.set_xlabel(\"R\")\n", + "ax.set_ylabel(\"Z\")\n", + "ax.set_title(\"Initial conditions\")\n", + "ax.legend()\n", + "ax_top.set_xlabel(\"x\")\n", + "ax_top.set_ylabel(\"y\")\n", + "ax_top.set_title(\"Initial conditions\")\n", + "ax_top.legend()\n", + "fig" ] }, { @@ -1300,24 +1278,24 @@ "mu0 = 1.25663706212e-6 # magnetic constant (N/A^2)\n", "\n", "# epsilon equation parameter\n", - "A = 1. # mass number in units of proton mass\n", - "Z = 1 # signed charge number in units of elementary charge\n", - "unit_x = 1. # length scale unit in m\n", - "unit_B = 1. # magnetic field unit in T\n", - "unit_n = 1e20 # number density unit in m^(-3)\n", - "unit_v = unit_B / np.sqrt(unit_n * A * mH * mu0) # Alfvén velocity unit\n", - "unit_t = unit_x / unit_v # time unit\n", + "A = 1.0 # mass number in units of proton mass\n", + "Z = 1 # signed charge number in units of elementary charge\n", + "unit_x = 1.0 # length scale unit in m\n", + "unit_B = 1.0 # magnetic field unit in T\n", + "unit_n = 1e20 # number density unit in m^(-3)\n", + "unit_v = unit_B / np.sqrt(unit_n * A * mH * mu0) # Alfvén velocity unit\n", + "unit_t = unit_x / unit_v # time unit\n", "\n", "# cyclotron frequency and epsilon parameter\n", - "om_c = Z*e * unit_B / (A*mH)\n", - "epsilon = 1./(om_c * unit_t)\n", + "om_c = Z * e * unit_B / (A * mH)\n", + "epsilon = 1.0 / (om_c * unit_t)\n", "\n", - "print(f'{unit_x = }')\n", - "print(f'{unit_B = }')\n", - "print(f'{unit_n = }')\n", - "print(f'{unit_v = }')\n", - "print(f'{unit_t = }')\n", - "print(f'{epsilon = }')" + "print(f\"{unit_x = }\")\n", + "print(f\"{unit_B = }\")\n", + "print(f\"{unit_n = }\")\n", + "print(f\"{unit_v = }\")\n", + "print(f\"{unit_t = }\")\n", + "print(f\"{epsilon = }\")" ] }, { @@ -1327,10 +1305,10 @@ "outputs": [], "source": [ "# instantiate Propagator object\n", - "opts_BxE['algo']['tol'] = 1e-5\n", - "opts_para['algo']['tol'] = 1e-5\n", - "prop_BxE = PushGuidingCenterBxEstar(particles, epsilon=epsilon, algo=opts_BxE['algo'])\n", - "prop_para = PushGuidingCenterParallel(particles, epsilon=epsilon, algo=opts_para['algo'])" + "opts_BxE[\"algo\"][\"tol\"] = 1e-5\n", + "opts_para[\"algo\"][\"tol\"] = 1e-5\n", + "prop_BxE = PushGuidingCenterBxEstar(particles, epsilon=epsilon, algo=opts_BxE[\"algo\"])\n", + "prop_para = PushGuidingCenterParallel(particles, epsilon=epsilon, algo=opts_para[\"algo\"])" ] }, { @@ -1340,34 +1318,33 @@ "outputs": [], "source": [ "# time stepping\n", - "Tend = 100. - 1e-6\n", - "dt = .1\n", + "Tend = 100.0 - 1e-6\n", + "dt = 0.1\n", "Nt = int(Tend / dt)\n", "\n", "pos = np.zeros((Nt + 2, Np, 3), dtype=float)\n", "r = np.zeros((Nt + 2, Np), dtype=float)\n", "\n", "pos[0] = pushed_pos\n", - "r[0] = np.sqrt(pushed_pos[:, 0]**2 + pushed_pos[:, 1]**2)\n", + "r[0] = np.sqrt(pushed_pos[:, 0] ** 2 + pushed_pos[:, 1] ** 2)\n", "\n", - "time = 0.\n", + "time = 0.0\n", "n = 0\n", "while time < Tend:\n", " time += dt\n", " n += 1\n", "\n", " # advance in time\n", - " prop_BxE(dt/2)\n", + " prop_BxE(dt / 2)\n", " prop_para(dt)\n", - " prop_BxE(dt/2)\n", - " \n", + " prop_BxE(dt / 2)\n", + "\n", " # positions on the physical domain Omega\n", " pushed_pos = domain(particles.positions).T\n", - " \n", + "\n", " # compute R-coordinate\n", " pos[n] = pushed_pos\n", - " r[n] = np.sqrt(pushed_pos[:, 0]**2 + pushed_pos[:, 1]**2)\n", - " " + " r[n] = np.sqrt(pushed_pos[:, 0] ** 2 + pushed_pos[:, 1] ** 2)" ] }, { @@ -1378,14 +1355,13 @@ "source": [ "# make scatter plot for each particle in xy-plane\n", "for i in range(pos.shape[1]):\n", - " # poloidal \n", + " # poloidal\n", " ax.scatter(r[:, i], pos[:, i, 2], c=colors[i % 4], s=1)\n", " # top view\n", " ax_top.scatter(pos[:, i, 0], pos[:, i, 1], c=colors[i % 4], s=1)\n", "\n", - "ax.set_title(f'{math.ceil(Tend/dt)} time steps')\n", - "ax_top.set_title(f'{math.ceil(Tend/dt)} time steps');\n", - "\n", + "ax.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", + "ax_top.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", "fig" ] } diff --git a/tutorials_old/tutorial_01_parameter_files.ipynb b/tutorials_old/tutorial_01_parameter_files.ipynb index e803e3746..8c2ce8ced 100644 --- a/tutorials_old/tutorial_01_parameter_files.ipynb +++ b/tutorials_old/tutorial_01_parameter_files.ipynb @@ -44,19 +44,18 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.io.options import EnvironmentOptions, Units, Time\n", - "from struphy.geometry import domains\n", + "from struphy import main\n", "from struphy.fields_background import equils\n", - "from struphy.topology import grids\n", - "from struphy.io.options import DerhamOptions\n", - "from struphy.io.options import FieldsBackground\n", + "from struphy.geometry import domains\n", "from struphy.initial import perturbations\n", + "from struphy.io.options import DerhamOptions, EnvironmentOptions, FieldsBackground, Time, Units\n", "from struphy.kinetic_background import maxwellians\n", - "from struphy.pic.utilities import LoadingParameters, WeightsParameters, BoundaryParameters\n", - "from struphy import main\n", "\n", "# import model, set verbosity\n", "from struphy.models.toy import Vlasov as Model\n", + "from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters\n", + "from struphy.topology import grids\n", + "\n", "verbose = True" ] }, @@ -151,10 +150,10 @@ "\n", "loading_params = LoadingParameters(Np=15)\n", "weights_params = WeightsParameters()\n", - "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", - "model.kinetic_ions.set_markers(loading_params=loading_params, \n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params)\n", + "boundary_params = BoundaryParameters(bc=(\"reflect\", \"reflect\", \"periodic\"))\n", + "model.kinetic_ions.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", + ")\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)" ] @@ -222,17 +221,18 @@ "metadata": {}, "outputs": [], "source": [ - "main.run(model, \n", - " params_path=None, \n", - " env=env, \n", - " units=units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " units=units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -253,6 +253,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "path = os.path.join(os.getcwd(), \"sim_1\")\n", "\n", "main.pproc(path, physical=True)" @@ -330,27 +331,27 @@ "fig = plt.figure()\n", "ax = fig.gca()\n", "\n", - "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", "\n", - "time = 0.\n", + "time = 0.0\n", "dt = time_opts.dt\n", "Tend = time_opts.Tend\n", "for k, v in simdata.pic_species[\"kinetic_ions\"][\"orbits\"].items():\n", " # print(f\"{v[0] = }\")\n", - " alpha = (Tend - time)/Tend\n", + " alpha = (Tend - time) / Tend\n", " for i, particle in enumerate(v):\n", " ax.scatter(particle[0], particle[1], c=colors[i % 4], alpha=alpha)\n", " time += dt\n", - " \n", - "ax.plot([l1, l1], [l2, r2], 'k')\n", - "ax.plot([r1, r1], [l2, r2], 'k')\n", - "ax.plot([l1, r1], [l2, l2], 'k')\n", - "ax.plot([l1, r1], [r2, r2], 'k')\n", - "ax.set_xlabel('x')\n", - "ax.set_ylabel('y')\n", + "\n", + "ax.plot([l1, l1], [l2, r2], \"k\")\n", + "ax.plot([r1, r1], [l2, r2], \"k\")\n", + "ax.plot([l1, r1], [l2, l2], \"k\")\n", + "ax.plot([l1, r1], [r2, r2], \"k\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", "ax.set_xlim(-6.5, 6.5)\n", "ax.set_ylim(-9, 9)\n", - "ax.set_title(f'{int(Tend/dt)} time steps (full color at t=0)');" + "ax.set_title(f\"{int(Tend / dt)} time steps (full color at t=0)\");" ] } ], diff --git a/tutorials_old/tutorial_01_particles.ipynb b/tutorials_old/tutorial_01_particles.ipynb index dc4bc05b5..b72b1ce94 100644 --- a/tutorials_old/tutorial_01_particles.ipynb +++ b/tutorials_old/tutorial_01_particles.ipynb @@ -43,19 +43,18 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.io.options import EnvironmentOptions, Units, Time\n", - "from struphy.geometry import domains\n", + "from struphy import main\n", "from struphy.fields_background import equils\n", - "from struphy.topology import grids\n", - "from struphy.io.options import DerhamOptions\n", - "from struphy.io.options import FieldsBackground\n", + "from struphy.geometry import domains\n", "from struphy.initial import perturbations\n", + "from struphy.io.options import DerhamOptions, EnvironmentOptions, FieldsBackground, Time, Units\n", "from struphy.kinetic_background import maxwellians\n", - "from struphy.pic.utilities import LoadingParameters, WeightsParameters, BoundaryParameters\n", - "from struphy import main\n", "\n", "# import model, set verbosity\n", "from struphy.models.toy import Vlasov as Model\n", + "from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters\n", + "from struphy.topology import grids\n", + "\n", "verbose = True" ] }, @@ -109,10 +108,10 @@ "\n", "loading_params = LoadingParameters(Np=15)\n", "weights_params = WeightsParameters()\n", - "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", - "model.kinetic_ions.set_markers(loading_params=loading_params, \n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params)\n", + "boundary_params = BoundaryParameters(bc=(\"reflect\", \"reflect\", \"periodic\"))\n", + "model.kinetic_ions.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", + ")\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)" ] @@ -150,17 +149,18 @@ "metadata": {}, "outputs": [], "source": [ - "main.run(model, \n", - " params_path=None, \n", - " env=env, \n", - " units=units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " units=units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -225,27 +225,27 @@ "fig = plt.figure()\n", "ax = fig.gca()\n", "\n", - "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", "\n", - "time = 0.\n", + "time = 0.0\n", "dt = time_opts.dt\n", "Tend = time_opts.Tend\n", "for k, v in simdata.pic_species[\"kinetic_ions\"][\"orbits\"].items():\n", " # print(k, v)\n", - " alpha = (Tend - time)/Tend\n", + " alpha = (Tend - time) / Tend\n", " for i, particle in enumerate(v):\n", " ax.scatter(particle[1], particle[2], c=colors[i % 4], alpha=alpha)\n", " time += dt\n", - " \n", - "ax.plot([l1, l1], [l2, r2], 'k')\n", - "ax.plot([r1, r1], [l2, r2], 'k')\n", - "ax.plot([l1, r1], [l2, l2], 'k')\n", - "ax.plot([l1, r1], [r2, r2], 'k')\n", - "ax.set_xlabel('x')\n", - "ax.set_ylabel('y')\n", + "\n", + "ax.plot([l1, l1], [l2, r2], \"k\")\n", + "ax.plot([r1, r1], [l2, r2], \"k\")\n", + "ax.plot([l1, r1], [l2, l2], \"k\")\n", + "ax.plot([l1, r1], [r2, r2], \"k\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", "ax.set_xlim(-6.5, 6.5)\n", "ax.set_ylim(-9, 9)\n", - "ax.set_title(f'{int(Tend/dt)} time steps (full color at t=0)');" + "ax.set_title(f\"{int(Tend / dt)} time steps (full color at t=0)\");" ] } ], diff --git a/tutorials_old/tutorial_02_fluid_particles.ipynb b/tutorials_old/tutorial_02_fluid_particles.ipynb index 36ab63d1b..7c22c1195 100644 --- a/tutorials_old/tutorial_02_fluid_particles.ipynb +++ b/tutorials_old/tutorial_02_fluid_particles.ipynb @@ -43,12 +43,12 @@ "source": [ "from struphy.geometry.domains import Cuboid\n", "\n", - "l1 = -.5\n", - "r1 = .5\n", - "l2 = -.5\n", - "r2 = .5\n", - "l3 = 0.\n", - "r3 = 1.\n", + "l1 = -0.5\n", + "r1 = 0.5\n", + "l2 = -0.5\n", + "r2 = 0.5\n", + "l3 = 0.0\n", + "r3 = 1.0\n", "domain = Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" ] }, @@ -60,17 +60,20 @@ "source": [ "# define the initial flow\n", "\n", - "from struphy.fields_background.generic import GenericCartesianFluidEquilibrium\n", "import numpy as np\n", "\n", + "from struphy.fields_background.generic import GenericCartesianFluidEquilibrium\n", + "\n", + "\n", "def u_fun(x, y, z):\n", - " ux = -np.cos(np.pi*x)*np.sin(np.pi*y)\n", - " uy = np.sin(np.pi*x)*np.cos(np.pi*y)\n", - " uz = 0 * x \n", + " ux = -np.cos(np.pi * x) * np.sin(np.pi * y)\n", + " uy = np.sin(np.pi * x) * np.cos(np.pi * y)\n", + " uz = 0 * x\n", " return ux, uy, uz\n", "\n", - "p_fun = lambda x, y, z: 0.5*(np.sin(np.pi*x)**2 + np.sin(np.pi*y)**2)\n", - "n_fun = lambda x, y, z: 1. + 0*x\n", + "\n", + "p_fun = lambda x, y, z: 0.5 * (np.sin(np.pi * x) ** 2 + np.sin(np.pi * y) ** 2)\n", + "n_fun = lambda x, y, z: 1.0 + 0 * x\n", "\n", "bel_flow = GenericCartesianFluidEquilibrium(u_xyz=u_fun, p_xyz=p_fun, n_xyz=n_fun)\n", "bel_flow.domain = domain\n", @@ -87,17 +90,17 @@ "from struphy.pic.particles import ParticlesSPH\n", "\n", "# particle boundary conditions\n", - "bc = ['reflect', 'reflect', 'periodic']\n", + "bc = [\"reflect\", \"reflect\", \"periodic\"]\n", "\n", "# instantiate Particle object (for random drawing of markers)\n", "Np = 1000\n", "\n", "particles_1 = ParticlesSPH(\n", - " bc=bc,\n", - " domain=domain,\n", - " bckgr_params=bel_flow,\n", - " Np=Np,\n", - " )\n", + " bc=bc,\n", + " domain=domain,\n", + " bckgr_params=bel_flow,\n", + " Np=Np,\n", + ")\n", "\n", "# instantiate Particle object (for regular tesselation drawing of markers)\n", "ppb = 4\n", @@ -107,15 +110,15 @@ "bufsize = 0.5\n", "\n", "particles_2 = ParticlesSPH(\n", - " bc=bc,\n", - " domain=domain,\n", - " bckgr_params=bel_flow,\n", - " ppb=ppb,\n", - " boxes_per_dim=boxes_per_dim,\n", - " loading=loading,\n", - " loading_params=loading_params,\n", - " bufsize=bufsize,\n", - " )" + " bc=bc,\n", + " domain=domain,\n", + " bckgr_params=bel_flow,\n", + " ppb=ppb,\n", + " boxes_per_dim=boxes_per_dim,\n", + " loading=loading,\n", + " loading_params=loading_params,\n", + " bufsize=bufsize,\n", + ")" ] }, { @@ -137,8 +140,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{particles_1.positions.shape = }')\n", - "print(f'{particles_2.positions.shape = }')" + "print(f\"{particles_1.positions.shape = }\")\n", + "print(f\"{particles_2.positions.shape = }\")" ] }, { @@ -148,8 +151,8 @@ "outputs": [], "source": [ "# positions on the physical domain Omega\n", - "print(f'random: \\n{domain(particles_1.positions).T[:10]}')\n", - "print(f'\\ntesselation: \\n{domain(particles_2.positions).T[:10]}')" + "print(f\"random: \\n{domain(particles_1.positions).T[:10]}\")\n", + "print(f\"\\ntesselation: \\n{domain(particles_2.positions).T[:10]}\")" ] }, { @@ -182,8 +185,8 @@ "outputs": [], "source": [ "# instantiate Propagator object\n", - "prop_eta_1 = PushEta(particles_1, algo = \"forward_euler\")\n", - "prop_eta_2 = PushEta(particles_2, algo = \"forward_euler\")" + "prop_eta_1 = PushEta(particles_1, algo=\"forward_euler\")\n", + "prop_eta_2 = PushEta(particles_2, algo=\"forward_euler\")" ] }, { @@ -196,7 +199,7 @@ "\n", "Nel = [64, 64, 1] # Number of grid cells\n", "p = [3, 3, 1] # spline degrees\n", - "spl_kind = [False, False, True] # spline types (clamped vs. periodic)\n", + "spl_kind = [False, False, True] # spline types (clamped vs. periodic)\n", "\n", "derham = Derham(Nel, p, spl_kind)" ] @@ -230,7 +233,7 @@ "metadata": {}, "outputs": [], "source": [ - "p_h = derham.create_spline_function('pressure', 'H1', coeffs=p_coeffs)" + "p_h = derham.create_spline_function(\"pressure\", \"H1\", coeffs=p_coeffs)" ] }, { @@ -240,39 +243,38 @@ "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", - "import numpy as np\n", "\n", "plt.figure(figsize=(12, 12))\n", - "x = np.linspace(-.5, .5, 100)\n", - "y = np.linspace(-.5, .5, 90)\n", + "x = np.linspace(-0.5, 0.5, 100)\n", + "y = np.linspace(-0.5, 0.5, 90)\n", "xx, yy = np.meshgrid(x, y)\n", "eta1 = np.linspace(0, 1, 100)\n", "eta2 = np.linspace(0, 1, 90)\n", "\n", "plt.subplot(2, 2, 1)\n", "plt.pcolor(xx, yy, p_xyz(xx, yy, 0))\n", - "plt.axis('square')\n", - "plt.title('p_xyz')\n", + "plt.axis(\"square\")\n", + "plt.title(\"p_xyz\")\n", "plt.colorbar()\n", "\n", "plt.subplot(2, 2, 2)\n", "p_vals = p0(eta1, eta2, 0, squeeze_out=True).T\n", "plt.pcolor(eta1, eta2, p_vals)\n", - "plt.axis('square')\n", - "plt.title('p logical')\n", + "plt.axis(\"square\")\n", + "plt.title(\"p logical\")\n", "plt.colorbar()\n", "\n", "plt.subplot(2, 2, 3)\n", "p_h_vals = p_h(eta1, eta2, 0, squeeze_out=True).T\n", "plt.pcolor(eta1, eta2, p_h_vals)\n", - "plt.axis('square')\n", - "plt.title('p_h (logical)')\n", + "plt.axis(\"square\")\n", + "plt.title(\"p_h (logical)\")\n", "plt.colorbar()\n", "\n", "plt.subplot(2, 2, 4)\n", "plt.pcolor(eta1, eta2, np.abs(p_vals - p_h_vals))\n", - "plt.axis('square')\n", - "plt.title('difference')\n", + "plt.axis(\"square\")\n", + "plt.title(\"difference\")\n", "plt.colorbar()" ] }, @@ -283,8 +285,8 @@ "outputs": [], "source": [ "grad_p = derham.grad.dot(p_coeffs)\n", - "grad_p.update_ghost_regions() # very important, we will move it inside grad\n", - "grad_p *= -1.\n", + "grad_p.update_ghost_regions() # very important, we will move it inside grad\n", + "grad_p *= -1.0\n", "prop_v_1 = PushVinEfield(particles_1, e_field=grad_p)\n", "prop_v_2 = PushVinEfield(particles_2, e_field=grad_p)" ] @@ -303,7 +305,7 @@ "\n", "ax2 = fig.add_subplot(1, 2, 2, projection=\"3d\")\n", "pos_2 = domain(particles_2.positions).T\n", - "ax2.scatter(pos_2[:, 0],pos_2[:, 1],pos_2[:, 2])\n", + "ax2.scatter(pos_2[:, 0], pos_2[:, 1], pos_2[:, 2])\n", "ax2.set_title(\"starting positions from tesselation\")" ] }, @@ -329,26 +331,26 @@ "\n", "pos_1[0] = domain(particles_1.positions).T\n", "velo_1[0] = particles_1.velocities\n", - "energy_1[0] = .5*(velo_1[0, : , 0]**2 + velo_1[0, : , 1]**2) + p_h(particles_1.positions)\n", + "energy_1[0] = 0.5 * (velo_1[0, :, 0] ** 2 + velo_1[0, :, 1] ** 2) + p_h(particles_1.positions)\n", "\n", - "time = 0.\n", + "time = 0.0\n", "time_vec = np.zeros(Nt + 1, dtype=float)\n", "n = 0\n", "while n < Nt:\n", " time += dt\n", " n += 1\n", " time_vec[n] = time\n", - " \n", + "\n", " # advance in time\n", - " prop_eta_1(dt/2)\n", + " prop_eta_1(dt / 2)\n", " prop_v_1(dt)\n", - " prop_eta_1(dt/2)\n", - " \n", + " prop_eta_1(dt / 2)\n", + "\n", " # positions on the physical domain Omega\n", " pos_1[n] = domain(particles_1.positions).T\n", " velo_1[n] = particles_1.velocities\n", - " \n", - " energy_1[n] = .5*(velo_1[n, : , 0]**2 + velo_1[n, : , 1]**2) + p_h(particles_1.positions)" + "\n", + " energy_1[n] = 0.5 * (velo_1[n, :, 0] ** 2 + velo_1[n, :, 1] ** 2) + p_h(particles_1.positions)" ] }, { @@ -358,31 +360,31 @@ "outputs": [], "source": [ "# energy plots (random)\n", - "fig = plt.figure(figsize = (13, 6))\n", + "fig = plt.figure(figsize=(13, 6))\n", "\n", "plt.subplot(2, 2, 1)\n", "plt.plot(time_vec, energy_1[:, 0])\n", - "plt.title('particle 1')\n", - "plt.xlabel('time')\n", - "plt.ylabel('energy')\n", + "plt.title(\"particle 1\")\n", + "plt.xlabel(\"time\")\n", + "plt.ylabel(\"energy\")\n", "\n", "plt.subplot(2, 2, 2)\n", "plt.plot(time_vec, energy_1[:, 1])\n", - "plt.title('particle 2')\n", - "plt.xlabel('time')\n", - "plt.ylabel('energy')\n", + "plt.title(\"particle 2\")\n", + "plt.xlabel(\"time\")\n", + "plt.ylabel(\"energy\")\n", "\n", "plt.subplot(2, 2, 3)\n", "plt.plot(time_vec, energy_1[:, 2])\n", - "plt.title('particle 3')\n", - "plt.xlabel('time')\n", - "plt.ylabel('energy')\n", + "plt.title(\"particle 3\")\n", + "plt.xlabel(\"time\")\n", + "plt.ylabel(\"energy\")\n", "\n", "plt.subplot(2, 2, 4)\n", "plt.plot(time_vec, energy_1[:, 3])\n", - "plt.title('particle 4')\n", - "plt.xlabel('time')\n", - "plt.ylabel('energy')" + "plt.title(\"particle 4\")\n", + "plt.xlabel(\"time\")\n", + "plt.ylabel(\"energy\")" ] }, { @@ -393,24 +395,23 @@ "source": [ "plt.figure(figsize=(12, 28))\n", "\n", - "coloring = np.select([pos_1[0,:,0]<=-0.2, np.abs(pos_1[0,:,0]) < +0.2, pos_1[0,:,0] >= 0.2],\n", - " [-1.0, 0.0, +1.0])\n", + "coloring = np.select([pos_1[0, :, 0] <= -0.2, np.abs(pos_1[0, :, 0]) < +0.2, pos_1[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0])\n", "\n", - "interval = Nt/20\n", + "interval = Nt / 20\n", "plot_ct = 0\n", "for i in range(Nt):\n", " if i % interval == 0:\n", - " print(f'{i = }')\n", + " print(f\"{i = }\")\n", " plot_ct += 1\n", " plt.subplot(5, 2, plot_ct)\n", - " ax = plt.gca() \n", + " ax = plt.gca()\n", " plt.scatter(pos_1[i, :, 0], pos_1[i, :, 1], c=coloring)\n", - " plt.axis('square')\n", - " plt.title('n0_scatter')\n", + " plt.axis(\"square\")\n", + " plt.title(\"n0_scatter\")\n", " plt.xlim(l1, r1)\n", " plt.ylim(l2, r2)\n", " plt.colorbar()\n", - " plt.title(f'Gas at t={i*dt}')\n", + " plt.title(f\"Gas at t={i * dt}\")\n", " if plot_ct == 10:\n", " break" ] @@ -432,26 +433,26 @@ "\n", "pos_2[0] = domain(particles_2.positions).T\n", "velo_2[0] = particles_2.velocities\n", - "energy_2[0] = .5*(velo_2[0, : , 0]**2 + velo_2[0, : , 1]**2) + p_h(particles_2.positions)\n", + "energy_2[0] = 0.5 * (velo_2[0, :, 0] ** 2 + velo_2[0, :, 1] ** 2) + p_h(particles_2.positions)\n", "\n", - "time = 0.\n", + "time = 0.0\n", "time_vec = np.zeros(Nt + 1, dtype=float)\n", "n = 0\n", "while n < Nt:\n", " time += dt\n", " n += 1\n", " time_vec[n] = time\n", - " \n", + "\n", " # advance in time\n", - " prop_eta_2(dt/2)\n", + " prop_eta_2(dt / 2)\n", " prop_v_2(dt)\n", - " prop_eta_2(dt/2)\n", - " \n", + " prop_eta_2(dt / 2)\n", + "\n", " # positions on the physical domain Omega\n", " pos_2[n] = domain(particles_2.positions).T\n", " velo_2[n] = particles_2.velocities\n", - " \n", - " energy_2[n] = .5*(velo_2[n, : , 0]**2 + velo_2[n, : , 1]**2) + p_h(particles_2.positions)" + "\n", + " energy_2[n] = 0.5 * (velo_2[n, :, 0] ** 2 + velo_2[n, :, 1] ** 2) + p_h(particles_2.positions)" ] }, { @@ -461,31 +462,31 @@ "outputs": [], "source": [ "# energy plots (tesselation)\n", - "fig = plt.figure(figsize = (13, 6))\n", + "fig = plt.figure(figsize=(13, 6))\n", "\n", "plt.subplot(2, 2, 1)\n", "plt.plot(time_vec, energy_2[:, 0])\n", - "plt.title('particle 1')\n", - "plt.xlabel('time')\n", - "plt.ylabel('energy')\n", + "plt.title(\"particle 1\")\n", + "plt.xlabel(\"time\")\n", + "plt.ylabel(\"energy\")\n", "\n", "plt.subplot(2, 2, 2)\n", "plt.plot(time_vec, energy_2[:, 1])\n", - "plt.title('particle 2')\n", - "plt.xlabel('time')\n", - "plt.ylabel('energy')\n", + "plt.title(\"particle 2\")\n", + "plt.xlabel(\"time\")\n", + "plt.ylabel(\"energy\")\n", "\n", "plt.subplot(2, 2, 3)\n", "plt.plot(time_vec, energy_2[:, 2])\n", - "plt.title('particle 3')\n", - "plt.xlabel('time')\n", - "plt.ylabel('energy')\n", + "plt.title(\"particle 3\")\n", + "plt.xlabel(\"time\")\n", + "plt.ylabel(\"energy\")\n", "\n", "plt.subplot(2, 2, 4)\n", "plt.plot(time_vec, energy_2[:, 3])\n", - "plt.title('particle 4')\n", - "plt.xlabel('time')\n", - "plt.ylabel('energy')" + "plt.title(\"particle 4\")\n", + "plt.xlabel(\"time\")\n", + "plt.ylabel(\"energy\")" ] }, { @@ -496,24 +497,23 @@ "source": [ "plt.figure(figsize=(12, 28))\n", "\n", - "coloring = np.select([pos_2[0,:,0]<=-0.2, np.abs(pos_2[0,:,0]) < +0.2, pos_2[0,:,0] >= 0.2],\n", - " [-1.0, 0.0, +1.0])\n", + "coloring = np.select([pos_2[0, :, 0] <= -0.2, np.abs(pos_2[0, :, 0]) < +0.2, pos_2[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0])\n", "\n", - "interval = Nt/20\n", + "interval = Nt / 20\n", "plot_ct = 0\n", "for i in range(Nt):\n", " if i % interval == 0:\n", - " print(f'{i = }')\n", + " print(f\"{i = }\")\n", " plot_ct += 1\n", " plt.subplot(5, 2, plot_ct)\n", - " ax = plt.gca() \n", + " ax = plt.gca()\n", " plt.scatter(pos_2[i, :, 0], pos_2[i, :, 1], c=coloring)\n", - " plt.axis('square')\n", - " plt.title('n0_scatter')\n", + " plt.axis(\"square\")\n", + " plt.title(\"n0_scatter\")\n", " plt.xlim(l1, r1)\n", " plt.ylim(l2, r2)\n", " plt.colorbar()\n", - " plt.title(f'Gas at t={i*dt}')\n", + " plt.title(f\"Gas at t={i * dt}\")\n", " if plot_ct == 10:\n", " break" ] @@ -527,38 +527,41 @@ "make_movie = False\n", "if make_movie:\n", " import matplotlib.animation as animation\n", + "\n", " n_frame = Nt\n", " fig, axs = plt.subplots(1, 2, figsize=(12, 8))\n", "\n", - " coloring_1 = np.select([pos_1[0,:,0]<=-0.2, np.abs(pos_1[0,:,0]) < +0.2, pos_1[0,:,0] >= 0.2],\n", - " [-1.0, 0.0, +1.0])\n", - " scat_1 = axs[0].scatter(pos_1[0,:,0], pos_1[0,:,1], c=coloring_1)\n", - " axs[0].set_xlim([-0.5,0.5])\n", - " axs[0].set_ylim([-0.5,0.5])\n", - " axs[0].set_aspect('equal')\n", - " \n", - " coloring_2 = np.select([pos_2[0,:,0]<=-0.2, np.abs(pos_2[0,:,0]) < +0.2, pos_2[0,:,0] >= 0.2],\n", - " [-1.0, 0.0, +1.0])\n", - " scat_2 = axs[1].scatter(pos_2[0,:,0], pos_2[0,:,1], c=coloring_2)\n", - " axs[1].set_xlim([-0.5,0.5])\n", - " axs[1].set_ylim([-0.5,0.5])\n", - " axs[1].set_aspect('equal')\n", - "\n", - " f = lambda x, y: np.cos(np.pi*x)*np.cos(np.pi*y)\n", + " coloring_1 = np.select(\n", + " [pos_1[0, :, 0] <= -0.2, np.abs(pos_1[0, :, 0]) < +0.2, pos_1[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0]\n", + " )\n", + " scat_1 = axs[0].scatter(pos_1[0, :, 0], pos_1[0, :, 1], c=coloring_1)\n", + " axs[0].set_xlim([-0.5, 0.5])\n", + " axs[0].set_ylim([-0.5, 0.5])\n", + " axs[0].set_aspect(\"equal\")\n", + "\n", + " coloring_2 = np.select(\n", + " [pos_2[0, :, 0] <= -0.2, np.abs(pos_2[0, :, 0]) < +0.2, pos_2[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0]\n", + " )\n", + " scat_2 = axs[1].scatter(pos_2[0, :, 0], pos_2[0, :, 1], c=coloring_2)\n", + " axs[1].set_xlim([-0.5, 0.5])\n", + " axs[1].set_ylim([-0.5, 0.5])\n", + " axs[1].set_aspect(\"equal\")\n", + "\n", + " f = lambda x, y: np.cos(np.pi * x) * np.cos(np.pi * y)\n", " axs[0].contour(xx, yy, f(xx, yy))\n", - " axs[0].set_title(f'time = {time_vec[0]:4.2f}')\n", + " axs[0].set_title(f\"time = {time_vec[0]:4.2f}\")\n", " axs[1].contour(xx, yy, f(xx, yy))\n", - " axs[1].set_title(f'time = {time_vec[0]:4.2f}')\n", + " axs[1].set_title(f\"time = {time_vec[0]:4.2f}\")\n", "\n", " def update_frame(frame):\n", - " scat_1.set_offsets(pos_1[frame,:,:2])\n", - " axs[0].set_title(f'time = {time_vec[frame]:4.2f}')\n", - " \n", - " scat_2.set_offsets(pos_2[frame,:,:2])\n", - " axs[1].set_title(f'time = {time_vec[frame]:4.2f}')\n", + " scat_1.set_offsets(pos_1[frame, :, :2])\n", + " axs[0].set_title(f\"time = {time_vec[frame]:4.2f}\")\n", + "\n", + " scat_2.set_offsets(pos_2[frame, :, :2])\n", + " axs[1].set_title(f\"time = {time_vec[frame]:4.2f}\")\n", " return scat_1, scat_2\n", "\n", - " ani = animation.FuncAnimation(fig=fig, func=update_frame, frames = n_frame)\n", + " ani = animation.FuncAnimation(fig=fig, func=update_frame, frames=n_frame)\n", " ani.save(\"tutorial_02_movie.gif\")" ] }, @@ -621,9 +624,9 @@ "l1 = 0\n", "r1 = 2.5\n", "l2 = 0\n", - "r2 = 1.\n", - "l3 = 0.\n", - "r3 = 1.\n", + "r2 = 1.0\n", + "l3 = 0.0\n", + "r3 = 1.0\n", "domain = Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" ] }, @@ -633,15 +636,10 @@ "metadata": {}, "outputs": [], "source": [ - "cst_vel = {\"ux\": 0., \n", - " \"uy\": 0.,\n", - " \"uz\": 0.,\n", - " \"density_profile\": \"constant\"}\n", + "cst_vel = {\"ux\": 0.0, \"uy\": 0.0, \"uz\": 0.0, \"density_profile\": \"constant\"}\n", "bckgr_params = {\"ConstantVelocity\": cst_vel}\n", "\n", - "mode_params = {\"given_in_basis\": \"0\",\n", - " \"ls\": [1],\n", - " \"amps\": [1e-2]}\n", + "mode_params = {\"given_in_basis\": \"0\", \"ls\": [1], \"amps\": [1e-2]}\n", "modes = {\"ModesSin\": mode_params}\n", "pert_params = {\"n\": modes}" ] @@ -652,7 +650,7 @@ "metadata": {}, "outputs": [], "source": [ - "#particle initialization \n", + "# particle initialization\n", "from struphy.pic.particles import ParticlesSPH\n", "\n", "# marker parameters\n", @@ -661,24 +659,24 @@ "ny = 1\n", "nz = 1\n", "boxes_per_dim = (nx, ny, nz)\n", - "bc = ['periodic']*3\n", + "bc = [\"periodic\"] * 3\n", "loading = \"tesselation\"\n", "loading_params = {\"n_quad\": 1}\n", "\n", "# instantiate Particle object\n", "particles = ParticlesSPH(\n", - " ppb=ppb,\n", - " boxes_per_dim=boxes_per_dim,\n", - " bc=bc,\n", - " domain=domain,\n", - " bckgr_params=bckgr_params,\n", - " pert_params=pert_params,\n", - " loading=loading,\n", - " loading_params=loading_params,\n", - " verbose=False,\n", - " bufsize=0.5,\n", - " n_cols_aux=3,\n", - " )" + " ppb=ppb,\n", + " boxes_per_dim=boxes_per_dim,\n", + " bc=bc,\n", + " domain=domain,\n", + " bckgr_params=bckgr_params,\n", + " pert_params=pert_params,\n", + " loading=loading,\n", + " loading_params=loading_params,\n", + " verbose=False,\n", + " bufsize=0.5,\n", + " n_cols_aux=3,\n", + ")" ] }, { @@ -717,7 +715,7 @@ "source": [ "import numpy as np\n", "\n", - "np.set_printoptions(suppress=True,linewidth=300,threshold=300,formatter=dict(float=lambda x: \"%.5f\" % x))\n", + "np.set_printoptions(suppress=True, linewidth=300, threshold=300, formatter=dict(float=lambda x: \"%.5f\" % x))\n", "\n", "plot_pts = 32\n", "\n", @@ -739,7 +737,7 @@ "eta1 = np.linspace(0, 1, plot_pts)\n", "eta2 = np.linspace(0, 1, 1)\n", "eta3 = np.linspace(0, 1, 1)\n", - "ee1, ee2, ee3 = np.meshgrid(eta1, eta2, eta3, indexing='ij')" + "ee1, ee2, ee3 = np.meshgrid(eta1, eta2, eta3, indexing=\"ij\")" ] }, { @@ -748,12 +746,21 @@ "metadata": {}, "outputs": [], "source": [ - "kernel_type = \"gaussian_1d\" \n", - "h1 = 1/nx\n", - "h2 = 1/ny\n", - "h3 = 1/nz\n", - "\n", - "n_sph_init = particles.eval_density(ee1, ee2, ee3, h1=h1, h2=h2, h3=h3, kernel_type=kernel_type, fast=True,)\n", + "kernel_type = \"gaussian_1d\"\n", + "h1 = 1 / nx\n", + "h2 = 1 / ny\n", + "h3 = 1 / nz\n", + "\n", + "n_sph_init = particles.eval_density(\n", + " ee1,\n", + " ee2,\n", + " ee3,\n", + " h1=h1,\n", + " h2=h2,\n", + " h3=h3,\n", + " kernel_type=kernel_type,\n", + " fast=True,\n", + ")\n", "n_sph_init.shape" ] }, @@ -765,8 +772,8 @@ "source": [ "logpos = particles.positions\n", "weights = particles.weights\n", - "print(f'{logpos.shape = }')\n", - "print(f'{weights.shape = }')" + "print(f\"{logpos.shape = }\")\n", + "print(f\"{weights.shape = }\")" ] }, { @@ -775,25 +782,26 @@ "metadata": {}, "outputs": [], "source": [ - "import matplotlib.pyplot as plt \n", + "import matplotlib.pyplot as plt\n", + "\n", "plt.figure(figsize=(10, 10))\n", "\n", "n0 = particles.f_init\n", "\n", "plt.subplot(2, 2, 1)\n", "plt.plot(eta1, np.squeeze(n0(eta1, eta2, eta3).T))\n", - "plt.title('$n/\\sqrt{g}$ (0-form)')\n", + "plt.title(\"$n/\\sqrt{g}$ (0-form)\")\n", "\n", "plt.subplot(2, 2, 2)\n", "ax = plt.gca()\n", "ax.set_xticks(np.linspace(0, 1, nx + 1))\n", "ax.set_yticks(np.linspace(0, 1, ny + 1))\n", - "plt.tick_params(labelbottom = False) \n", + "plt.tick_params(labelbottom=False)\n", "coloring = weights\n", - "plt.scatter(logpos[:, 0], logpos[:, 1], c=coloring, s=.25)\n", - "plt.grid(c='k')\n", - "plt.axis('square')\n", - "plt.title('n0_scatter')\n", + "plt.scatter(logpos[:, 0], logpos[:, 1], c=coloring, s=0.25)\n", + "plt.grid(c=\"k\")\n", + "plt.axis(\"square\")\n", + "plt.title(\"n0_scatter\")\n", "plt.xlim(0, 1)\n", "plt.ylim(0, 1)\n", "plt.colorbar()\n", @@ -801,21 +809,21 @@ "plt.subplot(2, 2, 3)\n", "ax = plt.gca()\n", "ax.set_xticks(np.linspace(0, 1, nx + 1))\n", - "#ax.set_yticks(np.linspace(0, 1., ny + 1))\n", - "plt.tick_params(labelbottom = False) \n", + "# ax.set_yticks(np.linspace(0, 1., ny + 1))\n", + "plt.tick_params(labelbottom=False)\n", "plt.plot(eta1, n_sph_init[:, 0, 0])\n", "plt.grid()\n", - "plt.title('n_sph_init')\n", + "plt.title(\"n_sph_init\")\n", "\n", "plt.subplot(2, 2, 4)\n", "ax = plt.gca()\n", "ax.set_xticks(np.linspace(0, 1, nx + 1))\n", - "#ax.set_yticks(np.linspace(0, 1., ny + 1))\n", - "plt.tick_params(labelbottom = False) \n", - "bc_x = (be_x[:-1] + be_x[1:]) / 2. # centers of binning cells\n", + "# ax.set_yticks(np.linspace(0, 1., ny + 1))\n", + "plt.tick_params(labelbottom=False)\n", + "bc_x = (be_x[:-1] + be_x[1:]) / 2.0 # centers of binning cells\n", "plt.plot(bc_x, df_bin.T)\n", - "#plt.grid()\n", - "plt.title('n_binned')" + "# plt.grid()\n", + "plt.title(\"n_binned\")" ] }, { @@ -824,7 +832,7 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.pic.sph_smoothing_kernels import linear_uni, trigonometric_uni, gaussian_uni\n", + "from struphy.pic.sph_smoothing_kernels import gaussian_uni, linear_uni, trigonometric_uni\n", "\n", "x = np.linspace(-1, 1, 200)\n", "out1 = np.zeros_like(x)\n", @@ -832,13 +840,13 @@ "out3 = np.zeros_like(x)\n", "\n", "for i, xi in enumerate(x):\n", - " out1[i] = trigonometric_uni(xi, 1.)\n", - " out2[i] = gaussian_uni(xi, 1.)\n", - " out3[i] = linear_uni(xi, 1.)\n", + " out1[i] = trigonometric_uni(xi, 1.0)\n", + " out2[i] = gaussian_uni(xi, 1.0)\n", + " out3[i] = linear_uni(xi, 1.0)\n", "plt.plot(x, out1, label=\"trigonometric\")\n", "plt.plot(x, out2, label=\"gaussian\")\n", - "plt.plot(x, out3, label = \"linear\")\n", - "plt.title('Some smoothing kernels')\n", + "plt.plot(x, out3, label=\"linear\")\n", + "plt.title(\"Some smoothing kernels\")\n", "plt.legend()" ] }, @@ -851,15 +859,12 @@ "from struphy.propagators.propagators_markers import PushEta, PushVinSPHpressure\n", "\n", "PushEta.domain = domain\n", - "prop_eta = PushEta(particles, algo = \"forward_euler\")\n", + "prop_eta = PushEta(particles, algo=\"forward_euler\")\n", "\n", "PushVinSPHpressure.domain = domain\n", "algo = \"forward_euler\"\n", "kernel_width = (h1, h2, h3)\n", - "prop_v = PushVinSPHpressure(particles,\n", - " kernel_type = kernel_type,\n", - " kernel_width = kernel_width, \n", - " algo = algo)" + "prop_v = PushVinSPHpressure(particles, kernel_type=kernel_type, kernel_width=kernel_width, algo=algo)" ] }, { @@ -871,9 +876,9 @@ "import numpy as np\n", "\n", "# time stepping\n", - "end_time = (r1 - l1) # so that the waves traverse the domain once (c_s = 1)\n", - "dt = 0.05*(8/nx) * end_time\n", - "Nt = int(end_time/dt)\n", + "end_time = r1 - l1 # so that the waves traverse the domain once (c_s = 1)\n", + "dt = 0.05 * (8 / nx) * end_time\n", + "Nt = int(end_time / dt)\n", "\n", "Np = particles.positions.shape[0]\n", "\n", @@ -885,7 +890,7 @@ "weights[0] = particles.weights\n", "n_sph[0] = n_sph_init\n", "\n", - "time = 0.\n", + "time = 0.0\n", "time_vec = np.zeros(Nt + 1, dtype=float)\n", "n = 0\n", "\n", @@ -894,18 +899,27 @@ " time += dt\n", " n += 1\n", " time_vec[n] = time\n", - " \n", + "\n", " # advance in time\n", - " prop_eta(dt/2)\n", + " prop_eta(dt / 2)\n", " prop_v(dt)\n", - " prop_eta(dt/2)\n", - " \n", + " prop_eta(dt / 2)\n", + "\n", " # positions on the physical domain Omega\n", " pos[n] = domain(particles.positions).T\n", " weights[n] = particles.weights\n", - " n_sph[n] = particles.eval_density(ee1, ee2, ee3, h1=h1, h2=h2, h3=h3, kernel_type=kernel_type, fast=True,)\n", + " n_sph[n] = particles.eval_density(\n", + " ee1,\n", + " ee2,\n", + " ee3,\n", + " h1=h1,\n", + " h2=h2,\n", + " h3=h3,\n", + " kernel_type=kernel_type,\n", + " fast=True,\n", + " )\n", "\n", - " print(f'{n} time steps done.')" + " print(f\"{n} time steps done.\")" ] }, { @@ -919,28 +933,28 @@ "x, y, z = domain(eta1, eta2, eta3, squeeze_out=True)\n", "\n", "plt.figure(figsize=(10, 8))\n", - "interval = Nt/10\n", + "interval = Nt / 10\n", "plot_ct = 0\n", "for i in range(0, Nt + 1):\n", " if i % interval == 0:\n", - " print(f'{i = }')\n", + " print(f\"{i = }\")\n", " plot_ct += 1\n", - " ax = plt.gca() \n", - " \n", + " ax = plt.gca()\n", + "\n", " if plot_ct <= 6:\n", - " style = '-'\n", + " style = \"-\"\n", " else:\n", - " style = '.'\n", - " plt.plot(x, n_sph[i, :, 0, 0], style, label=f'time={i*dt:4.2f}')\n", + " style = \".\"\n", + " plt.plot(x, n_sph[i, :, 0, 0], style, label=f\"time={i * dt:4.2f}\")\n", " plt.xlim(l1, r1)\n", " plt.legend()\n", " ax.set_xticks(np.linspace(l1, r1, nx + 1))\n", - " ax.xaxis.set_major_formatter(FormatStrFormatter('%.2f'))\n", - " plt.grid(c='k')\n", + " ax.xaxis.set_major_formatter(FormatStrFormatter(\"%.2f\"))\n", + " plt.grid(c=\"k\")\n", " plt.xlabel(\"x\")\n", " plt.ylabel(r\"$\\rho$\")\n", - " \n", - " plt.title(f'standing sound wave ($c_s = 1$) for {nx = } and {ppb = }')\n", + "\n", + " plt.title(f\"standing sound wave ($c_s = 1$) for {nx = } and {ppb = }\")\n", " if plot_ct == 11:\n", " break" ] @@ -966,8 +980,8 @@ "r1 = 3\n", "l2 = -3\n", "r2 = 3\n", - "l3 = 0.\n", - "r3 = 1.\n", + "l3 = 0.0\n", + "r3 = 1.0\n", "domain = Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" ] }, @@ -977,11 +991,13 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.fields_background.generic import GenericCartesianFluidEquilibrium\n", "import numpy as np\n", + "\n", + "from struphy.fields_background.generic import GenericCartesianFluidEquilibrium\n", + "\n", "T_h = 0.2\n", - "gamma = 5/3\n", - "n_fun = lambda x, y, z: np.exp(-(x**2 + y**2)/T_h) / 35\n", + "gamma = 5 / 3\n", + "n_fun = lambda x, y, z: np.exp(-(x**2 + y**2) / T_h) / 35\n", "\n", "bckgr = GenericCartesianFluidEquilibrium(n_xyz=n_fun)\n", "bckgr.domain = domain" @@ -993,7 +1009,7 @@ "metadata": {}, "outputs": [], "source": [ - "#particle initialization \n", + "# particle initialization\n", "from struphy.pic.particles import ParticlesSPH\n", "\n", "# marker parameters\n", @@ -1002,16 +1018,16 @@ "ny = 16\n", "nz = 1\n", "boxes_per_dim = (nx, ny, nz)\n", - "bc = ['periodic']*3\n", + "bc = [\"periodic\"] * 3\n", "\n", "# instantiate Particle object (for random drawing of markers)\n", "particles_1 = ParticlesSPH(\n", - " bc=bc,\n", - " domain=domain,\n", - " bckgr_params=bckgr,\n", - " ppb=ppb,\n", - " boxes_per_dim=boxes_per_dim,\n", - " )\n", + " bc=bc,\n", + " domain=domain,\n", + " bckgr_params=bckgr,\n", + " ppb=ppb,\n", + " boxes_per_dim=boxes_per_dim,\n", + ")\n", "\n", "# instantiate Particle object (for regular tesselation drawing of markers)\n", "loading = \"tesselation\"\n", @@ -1019,15 +1035,15 @@ "bufsize = 0.5\n", "\n", "particles_2 = ParticlesSPH(\n", - " bc=bc,\n", - " domain=domain,\n", - " bckgr_params=bckgr,\n", - " ppb=ppb,\n", - " boxes_per_dim=boxes_per_dim,\n", - " loading=loading,\n", - " loading_params=loading_params,\n", - " bufsize=bufsize,\n", - " )" + " bc=bc,\n", + " domain=domain,\n", + " bckgr_params=bckgr,\n", + " ppb=ppb,\n", + " boxes_per_dim=boxes_per_dim,\n", + " loading=loading,\n", + " loading_params=loading_params,\n", + " bufsize=bufsize,\n", + ")" ] }, { @@ -1068,8 +1084,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{particles_1.markers.shape = }')\n", - "print(f'{particles_2.markers.shape = }')" + "print(f\"{particles_1.markers.shape = }\")\n", + "print(f\"{particles_2.markers.shape = }\")" ] }, { @@ -1078,8 +1094,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{particles_1.sorting_boxes.boxes.shape = }')\n", - "print(f'{particles_2.sorting_boxes.boxes.shape = }')" + "print(f\"{particles_1.sorting_boxes.boxes.shape = }\")\n", + "print(f\"{particles_2.sorting_boxes.boxes.shape = }\")" ] }, { @@ -1110,7 +1126,7 @@ "xx, yy = np.meshgrid(x, y, indexing=\"ij\")\n", "eta1 = np.linspace(0, 1, 100)\n", "eta2 = np.linspace(0, 1, 90)\n", - "eta3 = np.linspace(0,1,1)\n", + "eta3 = np.linspace(0, 1, 1)\n", "ee1, ee2, ee3 = np.meshgrid(eta1, eta2, eta3, indexing=\"ij\")" ] }, @@ -1120,13 +1136,31 @@ "metadata": {}, "outputs": [], "source": [ - "kernel_type = \"gaussian_2d\" \n", - "h1 = 1/nx\n", - "h2 = 1/ny\n", - "h3 = 1/nz\n", - "\n", - "n_sph_1 = particles_1.eval_density(ee1, ee2, ee3, h1=h1, h2=h2, h3=h3, kernel_type=kernel_type, fast=True,)\n", - "n_sph_2 = particles_2.eval_density(ee1, ee2, ee3, h1=h1, h2=h2, h3=h3, kernel_type=kernel_type, fast=True,)" + "kernel_type = \"gaussian_2d\"\n", + "h1 = 1 / nx\n", + "h2 = 1 / ny\n", + "h3 = 1 / nz\n", + "\n", + "n_sph_1 = particles_1.eval_density(\n", + " ee1,\n", + " ee2,\n", + " ee3,\n", + " h1=h1,\n", + " h2=h2,\n", + " h3=h3,\n", + " kernel_type=kernel_type,\n", + " fast=True,\n", + ")\n", + "n_sph_2 = particles_2.eval_density(\n", + " ee1,\n", + " ee2,\n", + " ee3,\n", + " h1=h1,\n", + " h2=h2,\n", + " h3=h3,\n", + " kernel_type=kernel_type,\n", + " fast=True,\n", + ")" ] }, { @@ -1141,10 +1175,10 @@ "weights_1 = particles_1.weights\n", "weights_2 = particles_2.weights\n", "\n", - "print(f'{logpos_1.shape = }')\n", - "print(f'{logpos_2.shape = }')\n", - "print(f'{weights_1.shape = }')\n", - "print(f'{weights_2.shape = }')" + "print(f\"{logpos_1.shape = }\")\n", + "print(f\"{logpos_2.shape = }\")\n", + "print(f\"{weights_1.shape = }\")\n", + "print(f\"{weights_2.shape = }\")" ] }, { @@ -1153,7 +1187,8 @@ "metadata": {}, "outputs": [], "source": [ - "import matplotlib.pyplot as plt \n", + "import matplotlib.pyplot as plt\n", + "\n", "plt.figure(figsize=(12, 22))\n", "\n", "n_xyz = bckgr.n_xyz\n", @@ -1161,14 +1196,14 @@ "\n", "plt.subplot(4, 2, 1)\n", "plt.pcolor(xx, yy, n_fun(xx, yy, 0))\n", - "plt.axis('square')\n", - "plt.title('n_xyz')\n", + "plt.axis(\"square\")\n", + "plt.title(\"n_xyz\")\n", "plt.colorbar()\n", "\n", "plt.subplot(4, 2, 2)\n", - "plt.pcolor(eta1, eta2, n3(eta1,eta2,0, squeeze_out=True).T)\n", - "plt.axis('square')\n", - "plt.title('$\\hat{n}^{\\t{vol}}$ (volume form)')\n", + "plt.pcolor(eta1, eta2, n3(eta1, eta2, 0, squeeze_out=True).T)\n", + "plt.axis(\"square\")\n", + "plt.title(\"$\\hat{n}^{\\t{vol}}$ (volume form)\")\n", "plt.colorbar()\n", "\n", "make_scatter = True\n", @@ -1176,27 +1211,27 @@ " plt.subplot(4, 2, 3)\n", " ax = plt.gca()\n", " ax.set_xticks(np.linspace(0, 1, nx + 1))\n", - " ax.set_yticks(np.linspace(0, 1., ny + 1))\n", - " plt.tick_params(labelbottom = False) \n", + " ax.set_yticks(np.linspace(0, 1.0, ny + 1))\n", + " plt.tick_params(labelbottom=False)\n", " coloring = weights_1\n", - " plt.scatter(logpos_1[:, 0], logpos_1[:, 1], c=coloring, s=.25)\n", - " plt.grid(c='k')\n", - " plt.axis('square')\n", - " plt.title('$\\hat{n}^{\\t{vol}}$ scatter (random)')\n", + " plt.scatter(logpos_1[:, 0], logpos_1[:, 1], c=coloring, s=0.25)\n", + " plt.grid(c=\"k\")\n", + " plt.axis(\"square\")\n", + " plt.title(\"$\\hat{n}^{\\t{vol}}$ scatter (random)\")\n", " plt.xlim(0, 1)\n", " plt.ylim(0, 1)\n", " plt.colorbar()\n", - " \n", + "\n", " plt.subplot(4, 2, 4)\n", " ax = plt.gca()\n", " ax.set_xticks(np.linspace(0, 1, nx + 1))\n", - " ax.set_yticks(np.linspace(0, 1., ny + 1))\n", - " plt.tick_params(labelbottom = False) \n", + " ax.set_yticks(np.linspace(0, 1.0, ny + 1))\n", + " plt.tick_params(labelbottom=False)\n", " coloring = weights_2\n", - " plt.scatter(logpos_2[:, 0], logpos_2[:, 1], c=coloring, s=.25)\n", - " plt.grid(c='k')\n", - " plt.axis('square')\n", - " plt.title('$\\hat{n}^{\\t{vol}}$ scatter (tesselation)')\n", + " plt.scatter(logpos_2[:, 0], logpos_2[:, 1], c=coloring, s=0.25)\n", + " plt.grid(c=\"k\")\n", + " plt.axis(\"square\")\n", + " plt.title(\"$\\hat{n}^{\\t{vol}}$ scatter (tesselation)\")\n", " plt.xlim(0, 1)\n", " plt.ylim(0, 1)\n", " plt.colorbar()\n", @@ -1204,49 +1239,49 @@ "plt.subplot(4, 2, 5)\n", "ax = plt.gca()\n", "ax.set_xticks(np.linspace(0, 1, nx + 1))\n", - "ax.set_yticks(np.linspace(0, 1., ny + 1))\n", - "plt.tick_params(labelbottom = False) \n", - "plt.pcolor(ee1[:,:,0], ee2[:,:,0], n_sph_1[:,:,0])\n", + "ax.set_yticks(np.linspace(0, 1.0, ny + 1))\n", + "plt.tick_params(labelbottom=False)\n", + "plt.pcolor(ee1[:, :, 0], ee2[:, :, 0], n_sph_1[:, :, 0])\n", "plt.grid()\n", - "plt.axis('square')\n", - "plt.title('n_sph (random)')\n", + "plt.axis(\"square\")\n", + "plt.title(\"n_sph (random)\")\n", "plt.colorbar()\n", "\n", "plt.subplot(4, 2, 6)\n", "ax = plt.gca()\n", "ax.set_xticks(np.linspace(0, 1, nx + 1))\n", - "ax.set_yticks(np.linspace(0, 1., ny + 1))\n", - "plt.tick_params(labelbottom = False) \n", - "plt.pcolor(ee1[:,:,0], ee2[:,:,0], n_sph_2[:,:,0])\n", + "ax.set_yticks(np.linspace(0, 1.0, ny + 1))\n", + "plt.tick_params(labelbottom=False)\n", + "plt.pcolor(ee1[:, :, 0], ee2[:, :, 0], n_sph_2[:, :, 0])\n", "plt.grid()\n", - "plt.axis('square')\n", - "plt.title('n_sph (tesselation)')\n", + "plt.axis(\"square\")\n", + "plt.title(\"n_sph (tesselation)\")\n", "plt.colorbar()\n", "\n", "plt.subplot(4, 2, 7)\n", "ax = plt.gca()\n", "# ax.set_xticks(np.linspace(0, 1, nx + 1))\n", "# ax.set_yticks(np.linspace(0, 1., ny + 1))\n", - "# plt.tick_params(labelbottom = False) \n", - "bc_x = (be_x[:-1] + be_x[1:]) / 2. # centers of binning cells\n", - "bc_y = (be_y[:-1] + be_y[1:]) / 2.\n", + "# plt.tick_params(labelbottom = False)\n", + "bc_x = (be_x[:-1] + be_x[1:]) / 2.0 # centers of binning cells\n", + "bc_y = (be_y[:-1] + be_y[1:]) / 2.0\n", "plt.pcolor(bc_x, bc_y, f_bin_1)\n", - "#plt.grid()\n", - "plt.axis('square')\n", - "plt.title('n_binned (random)')\n", + "# plt.grid()\n", + "plt.axis(\"square\")\n", + "plt.title(\"n_binned (random)\")\n", "plt.colorbar()\n", "\n", "plt.subplot(4, 2, 8)\n", "ax = plt.gca()\n", "# ax.set_xticks(np.linspace(0, 1, nx + 1))\n", "# ax.set_yticks(np.linspace(0, 1., ny + 1))\n", - "# plt.tick_params(labelbottom = False) \n", - "bc_x = (be_x[:-1] + be_x[1:]) / 2. # centers of binning cells\n", - "bc_y = (be_y[:-1] + be_y[1:]) / 2.\n", + "# plt.tick_params(labelbottom = False)\n", + "bc_x = (be_x[:-1] + be_x[1:]) / 2.0 # centers of binning cells\n", + "bc_y = (be_y[:-1] + be_y[1:]) / 2.0\n", "plt.pcolor(bc_x, bc_y, f_bin_2)\n", - "#plt.grid()\n", - "plt.axis('square')\n", - "plt.title('n_binned (tesselation)')\n", + "# plt.grid()\n", + "plt.axis(\"square\")\n", + "plt.title(\"n_binned (tesselation)\")\n", "plt.colorbar()" ] }, @@ -1256,7 +1291,7 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.pic.sph_smoothing_kernels import linear_uni, trigonometric_uni, gaussian_uni\n", + "from struphy.pic.sph_smoothing_kernels import gaussian_uni, linear_uni, trigonometric_uni\n", "\n", "x = np.linspace(-1, 1, 200)\n", "out1 = np.zeros_like(x)\n", @@ -1264,13 +1299,13 @@ "out3 = np.zeros_like(x)\n", "\n", "for i, xi in enumerate(x):\n", - " out1[i] = trigonometric_uni(xi, 1.)\n", - " out2[i] = gaussian_uni(xi, 1.)\n", - " out3[i] = linear_uni(xi, 1.)\n", + " out1[i] = trigonometric_uni(xi, 1.0)\n", + " out2[i] = gaussian_uni(xi, 1.0)\n", + " out3[i] = linear_uni(xi, 1.0)\n", "plt.plot(x, out1, label=\"trigonometric\")\n", "plt.plot(x, out2, label=\"gaussian\")\n", - "plt.plot(x, out3, label = \"linear\")\n", - "plt.title('Some smoothing kernels')\n", + "plt.plot(x, out3, label=\"linear\")\n", + "plt.title(\"Some smoothing kernels\")\n", "plt.legend()" ] }, @@ -1304,8 +1339,8 @@ "outputs": [], "source": [ "# instantiate Propagator object\n", - "prop_eta_1 = PushEta(particles_1, algo = \"forward_euler\")\n", - "prop_eta_2 = PushEta(particles_2, algo = \"forward_euler\")" + "prop_eta_1 = PushEta(particles_1, algo=\"forward_euler\")\n", + "prop_eta_2 = PushEta(particles_2, algo=\"forward_euler\")" ] }, { @@ -1341,15 +1376,9 @@ "algo = \"forward_euler\"\n", "kernel_width = (h1, h2, h3)\n", "\n", - "prop_v_1 = PushVinSPHpressure(particles_1,\n", - " kernel_type = kernel_type,\n", - " kernel_width = kernel_width, \n", - " algo = algo)\n", + "prop_v_1 = PushVinSPHpressure(particles_1, kernel_type=kernel_type, kernel_width=kernel_width, algo=algo)\n", "\n", - "prop_v_2 = PushVinSPHpressure(particles_2,\n", - " kernel_type = kernel_type,\n", - " kernel_width = kernel_width, \n", - " algo = algo)" + "prop_v_2 = PushVinSPHpressure(particles_2, kernel_type=kernel_type, kernel_width=kernel_width, algo=algo)" ] }, { @@ -1372,24 +1401,24 @@ "pos_1[0] = domain(particles_1.positions).T\n", "velo_1[0] = particles_1.velocities\n", "\n", - "time = 0.\n", + "time = 0.0\n", "time_vec = np.zeros(Nt + 1, dtype=float)\n", "n = 0\n", "while n < Nt:\n", " time += dt\n", " n += 1\n", " time_vec[n] = time\n", - " \n", + "\n", " # advance in time\n", - " prop_eta_1(dt/2)\n", + " prop_eta_1(dt / 2)\n", " prop_v_1(dt)\n", - " prop_eta_1(dt/2)\n", - " \n", + " prop_eta_1(dt / 2)\n", + "\n", " # positions on the physical domain Omega\n", " pos_1[n] = domain(particles_1.positions).T\n", " velo_1[n] = particles_1.velocities\n", - " \n", - " print(f'{n} time steps done.')" + "\n", + " print(f\"{n} time steps done.\")" ] }, { @@ -1399,22 +1428,22 @@ "outputs": [], "source": [ "plt.figure(figsize=(12, 24))\n", - "interval = Nt/10\n", + "interval = Nt / 10\n", "plot_ct = 0\n", "for i in range(Nt):\n", " if i % interval == 0:\n", - " print(f'{i = }')\n", + " print(f\"{i = }\")\n", " plot_ct += 1\n", " plt.subplot(4, 2, plot_ct)\n", - " ax = plt.gca() \n", + " ax = plt.gca()\n", " coloring = weights_1\n", - " plt.scatter(pos_1[i, :, 0], pos_1[i, :, 1], c=coloring, s=.25)\n", - " plt.axis('square')\n", - " plt.title('n0_scatter')\n", + " plt.scatter(pos_1[i, :, 0], pos_1[i, :, 1], c=coloring, s=0.25)\n", + " plt.axis(\"square\")\n", + " plt.title(\"n0_scatter\")\n", " plt.xlim(l1, r1)\n", " plt.ylim(l2, r2)\n", " plt.colorbar()\n", - " plt.title(f'Gas at t={i*dt}')\n", + " plt.title(f\"Gas at t={i * dt}\")\n", " if plot_ct == 8:\n", " break" ] @@ -1433,24 +1462,24 @@ "pos_2[0] = domain(particles_2.positions).T\n", "velo_2[0] = particles_2.velocities\n", "\n", - "time = 0.\n", + "time = 0.0\n", "time_vec = np.zeros(Nt + 1, dtype=float)\n", "n = 0\n", "while n < Nt:\n", " time += dt\n", " n += 1\n", " time_vec[n] = time\n", - " \n", + "\n", " # advance in time\n", - " prop_eta_2(dt/2)\n", + " prop_eta_2(dt / 2)\n", " prop_v_2(dt)\n", - " prop_eta_2(dt/2)\n", - " \n", + " prop_eta_2(dt / 2)\n", + "\n", " # positions on the physical domain Omega\n", " pos_2[n] = domain(particles_2.positions).T\n", " velo_2[n] = particles_2.velocities\n", - " \n", - " print(f'{n} time steps done.')" + "\n", + " print(f\"{n} time steps done.\")" ] }, { @@ -1460,22 +1489,22 @@ "outputs": [], "source": [ "plt.figure(figsize=(12, 24))\n", - "interval = Nt/10\n", + "interval = Nt / 10\n", "plot_ct = 0\n", "for i in range(Nt):\n", " if i % interval == 0:\n", - " print(f'{i = }')\n", + " print(f\"{i = }\")\n", " plot_ct += 1\n", " plt.subplot(4, 2, plot_ct)\n", - " ax = plt.gca() \n", + " ax = plt.gca()\n", " coloring = weights_2\n", - " plt.scatter(pos_2[i, :, 0], pos_2[i, :, 1], c=coloring, s=.25)\n", - " plt.axis('square')\n", - " plt.title('n0_scatter')\n", + " plt.scatter(pos_2[i, :, 0], pos_2[i, :, 1], c=coloring, s=0.25)\n", + " plt.axis(\"square\")\n", + " plt.title(\"n0_scatter\")\n", " plt.xlim(l1, r1)\n", " plt.ylim(l2, r2)\n", " plt.colorbar()\n", - " plt.title(f'Gas at t={i*dt}')\n", + " plt.title(f\"Gas at t={i * dt}\")\n", " if plot_ct == 8:\n", " break" ] diff --git a/tutorials_old/tutorial_03_discrete_derham.ipynb b/tutorials_old/tutorial_03_discrete_derham.ipynb index 4c02dba9e..f2e8d8ab2 100644 --- a/tutorials_old/tutorial_03_discrete_derham.ipynb +++ b/tutorials_old/tutorial_03_discrete_derham.ipynb @@ -22,11 +22,12 @@ "outputs": [], "source": [ "from psydac.ddm.mpi import mpi as MPI\n", + "\n", "from struphy.feec.psydac_derham import Derham\n", "\n", "Nel = [9, 9, 10] # Number of grid cells\n", "p = [1, 2, 3] # spline degrees\n", - "spl_kind = [False, True, True] # spline types (clamped vs. periodic)\n", + "spl_kind = [False, True, True] # spline types (clamped vs. periodic)\n", "\n", "comm = MPI.COMM_WORLD\n", "derham = Derham(Nel, p, spl_kind, comm=comm)" @@ -45,9 +46,9 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{derham.grad = }')\n", - "print(f'{derham.curl = }')\n", - "print(f'{derham.div = }')" + "print(f\"{derham.grad = }\")\n", + "print(f\"{derham.curl = }\")\n", + "print(f\"{derham.div = }\")" ] }, { @@ -68,7 +69,7 @@ "source": [ "# commuting projectors\n", "for key, val in derham.P.items():\n", - " print(f'{key = }, {val = }')" + " print(f\"{key = }, {val = }\")" ] }, { @@ -79,7 +80,7 @@ "source": [ "# Vector spaces for FE coefficients\n", "for key, val in derham.Vh.items():\n", - " print(f'{key = }, {val = }')" + " print(f\"{key = }, {val = }\")" ] }, { @@ -90,7 +91,7 @@ "source": [ "# Polar spaces\n", "for key, val in derham.Vh_pol.items():\n", - " print(f'{key = }, {val = }')" + " print(f\"{key = }, {val = }\")" ] }, { @@ -118,10 +119,10 @@ "metadata": {}, "outputs": [], "source": [ - "p0 = derham.create_spline_function('pressure', 'H1')\n", - "e1 = derham.create_spline_function('e_field', 'Hcurl')\n", - "b2 = derham.create_spline_function('b_field', 'Hdiv')\n", - "n3 = derham.create_spline_function('density', 'L2')" + "p0 = derham.create_spline_function(\"pressure\", \"H1\")\n", + "e1 = derham.create_spline_function(\"e_field\", \"Hcurl\")\n", + "b2 = derham.create_spline_function(\"b_field\", \"Hdiv\")\n", + "n3 = derham.create_spline_function(\"density\", \"L2\")" ] }, { @@ -142,29 +143,36 @@ "metadata": {}, "outputs": [], "source": [ - "pp_pressure = {'ModesSin': {'given_in_basis': '0',\n", - " 'ns': [2],\n", - " 'amps': [.5],\n", - " }}\n", - "\n", - "pp_e_field = {'ModesSin': {'given_in_basis': ['v', None, None],\n", - " 'ns': [[2], None, None],\n", - " 'amps': [[.5], None, None],\n", - " },\n", - " 'ModesCos': {'given_in_basis': [None, None, 'v'],\n", - " 'ms': [None, None, [1, 2]],\n", - " 'amps': [None, None, [.75, .5]],\n", - " }}\n", - "\n", - "pp_b_field = {'ModesCos': {'given_in_basis': [None, 'v', None],\n", - " 'ms': [None, [1, 2], None],\n", - " 'amps': [None,[.75, .5], None],\n", - " }}\n", - "\n", - "pp_density = {'noise': {'comps': [True],\n", - " 'direction': 'e3',\n", - " 'amp': 0.001,\n", - " 'seed': 3456546}}" + "pp_pressure = {\n", + " \"ModesSin\": {\n", + " \"given_in_basis\": \"0\",\n", + " \"ns\": [2],\n", + " \"amps\": [0.5],\n", + " }\n", + "}\n", + "\n", + "pp_e_field = {\n", + " \"ModesSin\": {\n", + " \"given_in_basis\": [\"v\", None, None],\n", + " \"ns\": [[2], None, None],\n", + " \"amps\": [[0.5], None, None],\n", + " },\n", + " \"ModesCos\": {\n", + " \"given_in_basis\": [None, None, \"v\"],\n", + " \"ms\": [None, None, [1, 2]],\n", + " \"amps\": [None, None, [0.75, 0.5]],\n", + " },\n", + "}\n", + "\n", + "pp_b_field = {\n", + " \"ModesCos\": {\n", + " \"given_in_basis\": [None, \"v\", None],\n", + " \"ms\": [None, [1, 2], None],\n", + " \"amps\": [None, [0.75, 0.5], None],\n", + " }\n", + "}\n", + "\n", + "pp_density = {\"noise\": {\"comps\": [True], \"direction\": \"e3\", \"amp\": 0.001, \"seed\": 3456546}}" ] }, { @@ -201,24 +209,24 @@ "\n", "# evaluation points\n", "eta1 = 0\n", - "eta2 = np.linspace(0., 1., 50)\n", - "eta3 = np.linspace(0., 1., 70)\n", + "eta2 = np.linspace(0.0, 1.0, 50)\n", + "eta3 = np.linspace(0.0, 1.0, 70)\n", "\n", "# evaluate 0-form\n", "p0_vals = p0(eta1, eta2, eta3, squeeze_out=True)\n", - "print(f'{type(p0_vals) = }, {p0_vals.shape = }')\n", + "print(f\"{type(p0_vals) = }, {p0_vals.shape = }\")\n", "\n", "# evaluate 1-form\n", "e1_vals = e1(eta1, eta2, eta3, squeeze_out=True)\n", - "print(f'{type(e1_vals) = }, {type(e1_vals[0]) = }, {e1_vals[0].shape = }')\n", + "print(f\"{type(e1_vals) = }, {type(e1_vals[0]) = }, {e1_vals[0].shape = }\")\n", "\n", "# evaluate 2-form\n", "b2_vals = b2(eta1, eta2, eta3, squeeze_out=True)\n", - "print(f'{type(b2_vals) = }, {type(b2_vals[0]) = }, {b2_vals[0].shape = }')\n", + "print(f\"{type(b2_vals) = }, {type(b2_vals[0]) = }, {b2_vals[0].shape = }\")\n", "\n", "# evaluate 3-form\n", "n3_vals = n3(eta1, eta2, eta3, squeeze_out=True)\n", - "print(f'{type(n3_vals) = }, {n3_vals.shape = }')" + "print(f\"{type(n3_vals) = }, {n3_vals.shape = }\")" ] }, { @@ -227,43 +235,42 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "# plotting\n", "plt.figure(figsize=(12, 14))\n", "plt.subplot(4, 3, 1)\n", "plt.plot(eta3, p0_vals[0, :], label=p0.name)\n", - "plt.xlabel('$\\eta_3$')\n", + "plt.xlabel(\"$\\eta_3$\")\n", "plt.legend()\n", "\n", "plt.subplot(4, 3, 4)\n", - "plt.plot(eta3, e1_vals[0][0, :], label=(e1.name + '_1'))\n", - "plt.xlabel('$\\eta_3$')\n", + "plt.plot(eta3, e1_vals[0][0, :], label=(e1.name + \"_1\"))\n", + "plt.xlabel(\"$\\eta_3$\")\n", "plt.legend()\n", "plt.subplot(4, 3, 5)\n", - "plt.plot(eta3, e1_vals[1][0, :], label=(e1.name + '_2'))\n", - "plt.xlabel('$\\eta_3$')\n", + "plt.plot(eta3, e1_vals[1][0, :], label=(e1.name + \"_2\"))\n", + "plt.xlabel(\"$\\eta_3$\")\n", "plt.legend()\n", "plt.subplot(4, 3, 6)\n", - "plt.plot(eta2, e1_vals[2][:, 0], label=(e1.name + '_3'))\n", - "plt.xlabel('$\\eta_2$')\n", + "plt.plot(eta2, e1_vals[2][:, 0], label=(e1.name + \"_3\"))\n", + "plt.xlabel(\"$\\eta_2$\")\n", "plt.legend()\n", "\n", "plt.subplot(4, 3, 7)\n", - "plt.plot(eta2, b2_vals[0][:, 0], label=(b2.name + '_1'))\n", - "plt.xlabel('$\\eta_2$')\n", + "plt.plot(eta2, b2_vals[0][:, 0], label=(b2.name + \"_1\"))\n", + "plt.xlabel(\"$\\eta_2$\")\n", "plt.legend()\n", "plt.subplot(4, 3, 8)\n", - "plt.plot(eta2, b2_vals[1][:, 0], label=(b2.name + '_2'))\n", - "plt.xlabel('$\\eta_2$')\n", + "plt.plot(eta2, b2_vals[1][:, 0], label=(b2.name + \"_2\"))\n", + "plt.xlabel(\"$\\eta_2$\")\n", "plt.legend()\n", "plt.subplot(4, 3, 9)\n", - "plt.plot(eta2, b2_vals[2][:, 0], label=(b2.name + '_3'))\n", - "plt.xlabel('$\\eta_2$')\n", + "plt.plot(eta2, b2_vals[2][:, 0], label=(b2.name + \"_3\"))\n", + "plt.xlabel(\"$\\eta_2$\")\n", "plt.legend()\n", "\n", "plt.subplot(4, 3, 10)\n", "plt.plot(eta3, n3_vals[0, :], label=n3.name)\n", - "plt.xlabel('$\\eta_3$')\n", + "plt.xlabel(\"$\\eta_3$\")\n", "plt.legend()" ] }, @@ -282,17 +289,28 @@ "metadata": {}, "outputs": [], "source": [ - "def fun(x, y, z): return .5*np.sin(2*2*np.pi*z)\n", + "def fun(x, y, z):\n", + " return 0.5 * np.sin(2 * 2 * np.pi * z)\n", + "\n", + "\n", + "fun_h = derham.P[\"0\"](fun)\n", + "print(f\"{type(fun_h) = }\")\n", + "\n", + "\n", + "def dx_fun(x, y, z):\n", + " return 0 * z\n", + "\n", + "\n", + "def dy_fun(x, y, z):\n", + " return 0 * z\n", + "\n", "\n", - "fun_h = derham.P['0'](fun)\n", - "print(f'{type(fun_h) = }')\n", + "def dz_fun(x, y, z):\n", + " return 2 * 2 * np.pi * 0.5 * np.cos(2 * 2 * np.pi * z)\n", "\n", - "def dx_fun(x, y, z): return 0*z\n", - "def dy_fun(x, y, z): return 0*z\n", - "def dz_fun(x, y, z): return 2*2*np.pi*.5*np.cos(2*2*np.pi*z)\n", "\n", - "dfun_h = derham.P['1']((dx_fun, dy_fun, dz_fun))\n", - "print(f'{type(dfun_h) = }')" + "dfun_h = derham.P[\"1\"]((dx_fun, dy_fun, dz_fun))\n", + "print(f\"{type(dfun_h) = }\")" ] }, { @@ -308,9 +326,9 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{type(derham.grad) = }')\n", + "print(f\"{type(derham.grad) = }\")\n", "gradfun_h = derham.grad.dot(fun_h)\n", - "print(f'{type(gradfun_h) = }')\n", + "print(f\"{type(gradfun_h) = }\")\n", "\n", "assert np.allclose(dfun_h[0].toarray(), gradfun_h[0].toarray())\n", "assert np.allclose(dfun_h[1].toarray(), gradfun_h[1].toarray())\n", diff --git a/tutorials_old/tutorial_06_poisson.ipynb b/tutorials_old/tutorial_06_poisson.ipynb index f8d402fd8..eeaf8c5ce 100644 --- a/tutorials_old/tutorial_06_poisson.ipynb +++ b/tutorials_old/tutorial_06_poisson.ipynb @@ -68,11 +68,12 @@ "outputs": [], "source": [ "# set up domain Omega\n", - "from struphy.geometry.domains import Cuboid\n", "import numpy as np\n", "\n", - "l1 = -2*np.pi\n", - "r1 = 2*np.pi\n", + "from struphy.geometry.domains import Cuboid\n", + "\n", + "l1 = -2 * np.pi\n", + "r1 = 2 * np.pi\n", "domain = Cuboid(l1=l1, r1=r1)" ] }, @@ -106,8 +107,8 @@ "metadata": {}, "outputs": [], "source": [ - "# create solution field in Vh_0 subset H1 \n", - "phi = derham.create_spline_function('my solution', 'H1')\n", + "# create solution field in Vh_0 subset H1\n", + "phi = derham.create_spline_function(\"my solution\", \"H1\")\n", "phi" ] }, @@ -128,10 +129,10 @@ "source": [ "# manufactured solution, defined on Omega\n", "k = 2\n", - "f_xyz = lambda x, y, z: np.sin(k*x)\n", - "rhs_xyz = lambda x, y, z: k**2 * np.sin(k*x)\n", + "f_xyz = lambda x, y, z: np.sin(k * x)\n", + "rhs_xyz = lambda x, y, z: k**2 * np.sin(k * x)\n", "\n", - "# pullback to the logical unit cube \n", + "# pullback to the logical unit cube\n", "rhs = lambda e1, e2, e3: domain.pull(rhs_xyz, e1, e2, e3)" ] }, @@ -141,10 +142,10 @@ "metadata": {}, "outputs": [], "source": [ - "# compute rhs vector in Vh_0 subset H1 \n", + "# compute rhs vector in Vh_0 subset H1\n", "from struphy.feec.projectors import L2Projector\n", "\n", - "l2proj = L2Projector('H1', mass_ops)\n", + "l2proj = L2Projector(\"H1\", mass_ops)\n", "\n", "rho = l2proj.get_dofs(rhs)" ] @@ -169,7 +170,7 @@ "outputs": [], "source": [ "# solve (call with arbitrary dt)\n", - "poisson(1.)" + "poisson(1.0)" ] }, { @@ -180,8 +181,8 @@ "source": [ "# evalaute at logical coordinates\n", "e1 = np.linspace(0, 1, 100)\n", - "e2 = .5\n", - "e3 = .5\n", + "e2 = 0.5\n", + "e3 = 0.5\n", "\n", "funval = phi(e1, e2, e3)" ] @@ -205,12 +206,12 @@ "metadata": {}, "outputs": [], "source": [ - "# plot solution \n", + "# plot solution\n", "from matplotlib import pyplot as plt\n", "\n", - "plt.plot(x, f_xyz(x, 0., 0.), label='exact')\n", - "plt.plot(x, fh_xyz, '--r', label='numeric')\n", - "plt.xlabel('x')\n", + "plt.plot(x, f_xyz(x, 0.0, 0.0), label=\"exact\")\n", + "plt.plot(x, fh_xyz, \"--r\", label=\"numeric\")\n", + "plt.xlabel(\"x\")\n", "plt.legend();" ] }, diff --git a/tutorials_old/tutorial_07_heat_equation.ipynb b/tutorials_old/tutorial_07_heat_equation.ipynb index 5a82e73e2..dca4102d8 100644 --- a/tutorials_old/tutorial_07_heat_equation.ipynb +++ b/tutorials_old/tutorial_07_heat_equation.ipynb @@ -58,7 +58,7 @@ "Nel = [32, 1, 1]\n", "p = [1, 1, 1]\n", "spl_kind = [False, True, True]\n", - "dirichlet_bc = [[True]*2, [False]*2, [False]*2]\n", + "dirichlet_bc = [[True] * 2, [False] * 2, [False] * 2]\n", "derham = Derham(Nel, p, spl_kind, dirichlet_bc=dirichlet_bc)" ] }, @@ -71,8 +71,8 @@ "# set up domain Omega\n", "from struphy.geometry.domains import Cuboid\n", "\n", - "l1 = 0.\n", - "r1 = 10.\n", + "l1 = 0.0\n", + "r1 = 10.0\n", "domain = Cuboid(l1=l1, r1=r1)" ] }, @@ -109,7 +109,7 @@ "# initial condition\n", "import numpy as np\n", "\n", - "phi0_xyz = lambda x, y, z: np.exp(-(x - 5.)**2 / 0.3)" + "phi0_xyz = lambda x, y, z: np.exp(-((x - 5.0) ** 2) / 0.3)" ] }, { @@ -118,7 +118,7 @@ "metadata": {}, "outputs": [], "source": [ - "# pullback to the logical unit cube \n", + "# pullback to the logical unit cube\n", "phi0_logical = lambda e1, e2, e3: domain.pull(phi0_xyz, e1, e2, e3)" ] }, @@ -129,7 +129,7 @@ "outputs": [], "source": [ "# compute initial FE coeffs by projection\n", - "coeffs = derham.P['0'](phi0_logical)" + "coeffs = derham.P[\"0\"](phi0_logical)" ] }, { @@ -138,8 +138,8 @@ "metadata": {}, "outputs": [], "source": [ - "# solution field in Vh_0 subset H1 \n", - "phi = derham.create_spline_function('my solution', 'H1', coeffs=coeffs)" + "# solution field in Vh_0 subset H1\n", + "phi = derham.create_spline_function(\"my solution\", \"H1\", coeffs=coeffs)" ] }, { @@ -149,21 +149,18 @@ "outputs": [], "source": [ "# propagator parameters for heat equation\n", - "sigma_1 = 1.\n", - "sigma_2 = 1.\n", - "sigma_3 = 0.\n", + "sigma_1 = 1.0\n", + "sigma_2 = 1.0\n", + "sigma_3 = 0.0\n", "\n", "# solver options\n", - "solver = opts['solver']\n", - "solver['recycle'] = True\n", + "solver = opts[\"solver\"]\n", + "solver[\"recycle\"] = True\n", "\n", "# instantiate Propagator for the above quation, pass data structure (vector) of FemField\n", - "prop_heat_eq = ImplicitDiffusion(phi.vector, \n", - " sigma_1=sigma_1,\n", - " sigma_2=sigma_2,\n", - " sigma_3=sigma_3,\n", - " divide_by_dt=True,\n", - " solver=solver)" + "prop_heat_eq = ImplicitDiffusion(\n", + " phi.vector, sigma_1=sigma_1, sigma_2=sigma_2, sigma_3=sigma_3, divide_by_dt=True, solver=solver\n", + ")" ] }, { @@ -174,23 +171,23 @@ "source": [ "# evalaute at logical coordinates\n", "e1 = np.linspace(0, 1, 100)\n", - "e2 = .5\n", - "e3 = .5\n", + "e2 = 0.5\n", + "e3 = 0.5\n", "\n", "# time stepping\n", - "Tend = 2. - 1e-6\n", - "dt = .1\n", + "Tend = 2.0 - 1e-6\n", + "dt = 0.1\n", "\n", "phi_of_t = []\n", - "time = 0.\n", + "time = 0.0\n", "n = 0\n", "while time < Tend:\n", " n += 1\n", - " \n", + "\n", " # advance in time\n", " prop_heat_eq(dt)\n", " time += dt\n", - " \n", + "\n", " # evaluate solution and push to Omega\n", " phi_of_t += [phi(e1, e2, e3)]" ] @@ -211,8 +208,8 @@ "\n", " # plot\n", " plt.plot(x, fh_xyz)\n", - " plt.xlabel('x')\n", - " plt.title(f'{n} time steps');" + " plt.xlabel(\"x\")\n", + " plt.title(f\"{n} time steps\")" ] }, { @@ -259,9 +256,9 @@ "# set up domain Omega\n", "from struphy.geometry.domains import HollowCylinder\n", "\n", - "a1 = .1\n", - "a2 = 4.\n", - "Lz = 1.\n", + "a1 = 0.1\n", + "a2 = 4.0\n", + "Lz = 1.0\n", "domain = HollowCylinder(a1=a1, a2=a2, Lz=Lz)" ] }, @@ -293,8 +290,8 @@ "metadata": {}, "outputs": [], "source": [ - "# solution field in Vh_0 subset H1 \n", - "phi = derham.create_spline_function('my solution', 'H1')" + "# solution field in Vh_0 subset H1\n", + "phi = derham.create_spline_function(\"my solution\", \"H1\")" ] }, { @@ -304,7 +301,7 @@ "outputs": [], "source": [ "# initial condition\n", - "phi0_xyz = lambda x, y, z: np.exp(-(x - 2.)**2 / 0.3) * np.exp(-(y)**2 / 0.3)" + "phi0_xyz = lambda x, y, z: np.exp(-((x - 2.0) ** 2) / 0.3) * np.exp(-((y) ** 2) / 0.3)" ] }, { @@ -313,7 +310,7 @@ "metadata": {}, "outputs": [], "source": [ - "# pullback to the logical unit cube \n", + "# pullback to the logical unit cube\n", "phi0_logical = lambda e1, e2, e3: domain.pull(phi0_xyz, e1, e2, e3)" ] }, @@ -326,7 +323,7 @@ "# evaluate initial condition in logical space\n", "e1 = np.linspace(0, 1, 101)\n", "e2 = np.linspace(0, 1, 101)\n", - "e3 = .5\n", + "e3 = 0.5\n", "\n", "funvals = phi0_logical(e1, e2, e3)" ] @@ -339,10 +336,10 @@ "source": [ "# push to Omega\n", "fh_xyz = domain.push(funvals, e1, e2, e3, squeeze_out=True)\n", - "print(f'{fh_xyz.shape = }')\n", + "print(f\"{fh_xyz.shape = }\")\n", "\n", "x, y, z = domain(e1, e2, e3, squeeze_out=True)\n", - "print(f'{x.shape = }')" + "print(f\"{x.shape = }\")" ] }, { @@ -356,15 +353,15 @@ "ax = axs[0]\n", "\n", "ax.contourf(x, y, fh_xyz, levels=51)\n", - "ax.axis('equal')\n", - "ax.set_title('Initial condition')\n", - "ax.set_xlabel('x')\n", - "ax.set_ylabel('y')\n", + "ax.axis(\"equal\")\n", + "ax.set_title(\"Initial condition\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", "\n", "# add isolines of r-coordinate\n", "for i in range(x.shape[0]):\n", " if i % 5 == 0:\n", - " ax.plot(x[i], y[i], c='tab:blue', alpha=.4, linewidth=.5);" + " ax.plot(x[i], y[i], c=\"tab:blue\", alpha=0.4, linewidth=0.5)" ] }, { @@ -374,12 +371,12 @@ "outputs": [], "source": [ "# create diffusion matrix\n", - "bx = lambda x, y, z: y/np.sqrt(x**2 + y**2)\n", - "by = lambda x, y, z: -x/np.sqrt(x**2 + y**2)\n", - "bz = lambda x, y, z: 0.*x\n", + "bx = lambda x, y, z: y / np.sqrt(x**2 + y**2)\n", + "by = lambda x, y, z: -x / np.sqrt(x**2 + y**2)\n", + "bz = lambda x, y, z: 0.0 * x\n", "\n", "# vector-field pullback\n", - "bv = lambda e1, e2, e3: domain.pull((bx, by, bz), e1, e2, e3, kind='v')" + "bv = lambda e1, e2, e3: domain.pull((bx, by, bz), e1, e2, e3, kind=\"v\")" ] }, { @@ -391,12 +388,12 @@ "# creation of callable Kronecker matrix\n", "def Dmat_call(e1, e2, e3):\n", " bv_vals = bv(e1, e2, e3)\n", - " \n", + "\n", " # array from 2d list gives 3x3 array is in the first two indices\n", " tmp = np.array([[bi * bj for bj in bv_vals] for bi in bv_vals])\n", - " \n", + "\n", " # numpy operates on the last two indices with @\n", - " return np.transpose(tmp, axes=(2, 3, 4, 0, 1)) " + " return np.transpose(tmp, axes=(2, 3, 4, 0, 1))" ] }, { @@ -406,7 +403,7 @@ "outputs": [], "source": [ "# create and assembla mass matrix\n", - "Dmat = mass_ops.create_weighted_mass('Hcurl', 'Hcurl', name='bb', weights=[Dmat_call, 'sqrt_g'], assemble=True)" + "Dmat = mass_ops.create_weighted_mass(\"Hcurl\", \"Hcurl\", name=\"bb\", weights=[Dmat_call, \"sqrt_g\"], assemble=True)" ] }, { @@ -416,7 +413,7 @@ "outputs": [], "source": [ "# compute initial FE coeffs by projection\n", - "phi.vector = derham.P['0'](phi0_logical)" + "phi.vector = derham.P[\"0\"](phi0_logical)" ] }, { @@ -426,22 +423,18 @@ "outputs": [], "source": [ "# propagator parameters for heat equation\n", - "sigma_1 = 1.\n", - "sigma_2 = 1.\n", - "sigma_3 = 0.\n", + "sigma_1 = 1.0\n", + "sigma_2 = 1.0\n", + "sigma_3 = 0.0\n", "\n", "# solver options\n", - "solver = opts['solver']\n", - "solver['recycle'] = True\n", + "solver = opts[\"solver\"]\n", + "solver[\"recycle\"] = True\n", "\n", "# instantiate Propagator for the above quation, pass data structure (vector) of FemField\n", - "prop_heat_eq = ImplicitDiffusion(phi.vector, \n", - " sigma_1=sigma_1,\n", - " sigma_2=sigma_2,\n", - " sigma_3=sigma_3,\n", - " diffusion_mat=Dmat,\n", - " divide_by_dt=True,\n", - " solver=solver)" + "prop_heat_eq = ImplicitDiffusion(\n", + " phi.vector, sigma_1=sigma_1, sigma_2=sigma_2, sigma_3=sigma_3, diffusion_mat=Dmat, divide_by_dt=True, solver=solver\n", + ")" ] }, { @@ -451,19 +444,19 @@ "outputs": [], "source": [ "# time stepping\n", - "Tend = 6. - 1e-6\n", - "dt = .1\n", + "Tend = 6.0 - 1e-6\n", + "dt = 0.1\n", "\n", "phi_of_t = []\n", - "time = 0.\n", + "time = 0.0\n", "n = 0\n", "while time < Tend:\n", " n += 1\n", - " \n", + "\n", " # advance in time\n", " prop_heat_eq(dt)\n", " time += dt\n", - " \n", + "\n", " # evaluate solution and push to Omega\n", " phi_of_t += [phi(e1, e2, e3)]" ] @@ -480,16 +473,16 @@ "# plot\n", "ax_t = axs[1]\n", "ax_t.contourf(x, y, fh_xyz, levels=51)\n", - "ax_t.axis('equal')\n", - "ax_t.set_title(f'{n} time steps')\n", - "ax_t.set_xlabel('x')\n", - "ax_t.set_ylabel('y')\n", + "ax_t.axis(\"equal\")\n", + "ax_t.set_title(f\"{n} time steps\")\n", + "ax_t.set_xlabel(\"x\")\n", + "ax_t.set_ylabel(\"y\")\n", "\n", "# add isolines of r-coordinate\n", "for i in range(x.shape[0]):\n", " if i % 5 == 0:\n", - " ax_t.plot(x[i], y[i], c='tab:blue', alpha=.4, linewidth=.5);\n", - " \n", + " ax_t.plot(x[i], y[i], c=\"tab:blue\", alpha=0.4, linewidth=0.5)\n", + "\n", "fig" ] } diff --git a/tutorials_old/tutorial_08_maxwell.ipynb b/tutorials_old/tutorial_08_maxwell.ipynb index 316ea0a29..a00bd6e33 100644 --- a/tutorials_old/tutorial_08_maxwell.ipynb +++ b/tutorials_old/tutorial_08_maxwell.ipynb @@ -24,12 +24,12 @@ "# set up domain Omega\n", "from struphy.geometry.domains import Cuboid\n", "\n", - "l1 = 0.\n", - "r1 = 1.\n", - "l2 = 0.\n", - "r2 = 1.\n", - "l3 = 0.\n", - "r3 = 20.\n", + "l1 = 0.0\n", + "r1 = 1.0\n", + "l2 = 0.0\n", + "r2 = 1.0\n", + "l3 = 0.0\n", + "r3 = 20.0\n", "domain = Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" ] }, @@ -66,11 +66,11 @@ "metadata": {}, "outputs": [], "source": [ - "# create solution field E in Vh_1 subset H(curl) \n", - "e_field = derham.create_spline_function('electric field', 'Hcurl')\n", + "# create solution field E in Vh_1 subset H(curl)\n", + "e_field = derham.create_spline_function(\"electric field\", \"Hcurl\")\n", "\n", - "# create solution field B in Vh_2 subset H(div) \n", - "b_field = derham.create_spline_function('magnetic field', 'Hdiv')" + "# create solution field B in Vh_2 subset H(div)\n", + "b_field = derham.create_spline_function(\"magnetic field\", \"Hdiv\")" ] }, { @@ -80,15 +80,23 @@ "outputs": [], "source": [ "# initial perturbations\n", - "pert_params_e = {\"noise\": {\"comps\": [True, True, False],\n", - " 'direction' : 'e3',\n", - " 'amp' : 0.1, \n", - " 'seed' : None,}}\n", + "pert_params_e = {\n", + " \"noise\": {\n", + " \"comps\": [True, True, False],\n", + " \"direction\": \"e3\",\n", + " \"amp\": 0.1,\n", + " \"seed\": None,\n", + " }\n", + "}\n", "\n", - "pert_params_b = {\"noise\": {\"comps\": [False, False, False],\n", - " 'direction' : 'e3',\n", - " 'amp' : 0.1, \n", - " 'seed' : None,}}" + "pert_params_b = {\n", + " \"noise\": {\n", + " \"comps\": [False, False, False],\n", + " \"direction\": \"e3\",\n", + " \"amp\": 0.1,\n", + " \"seed\": None,\n", + " }\n", + "}" ] }, { @@ -110,8 +118,8 @@ "# evalaute at logical coordinates\n", "import numpy as np\n", "\n", - "e1 = .5\n", - "e2 = .5\n", + "e1 = 0.5\n", + "e2 = 0.5\n", "e3 = np.linspace(0, 1, 100)\n", "\n", "e_vals = e_field(e1, e2, e3, squeeze_out=True)\n", @@ -159,7 +167,7 @@ "outputs": [], "source": [ "prop_implicit = Maxwell(e_field.vector, b_field.vector)\n", - "prop_rk4 = Maxwell(e_field.vector, b_field.vector, algo='rk4')" + "prop_rk4 = Maxwell(e_field.vector, b_field.vector, algo=\"rk4\")" ] }, { @@ -168,8 +176,8 @@ "metadata": {}, "outputs": [], "source": [ - "Tend = 100. - 1e-6\n", - "dt = .05" + "Tend = 100.0 - 1e-6\n", + "dt = 0.05" ] }, { @@ -180,20 +188,20 @@ "source": [ "# implicit time stepping\n", "Ex_of_t_implicit = {}\n", - "time = 0.\n", + "time = 0.0\n", "n = 0\n", "while time < Tend:\n", " n += 1\n", - " \n", + "\n", " # advance in time\n", " prop_implicit(dt)\n", " time += dt\n", - " \n", + "\n", " # evaluate solution and push to Omega\n", " Ex_of_t_implicit[time] = e_field(e1, e2, e3)\n", - " \n", + "\n", " if n % 100 == 0:\n", - " print(f'{n}/{int(np.ceil(Tend/dt))} steps completed with {prop_implicit._algo =}.')" + " print(f\"{n}/{int(np.ceil(Tend / dt))} steps completed with {prop_implicit._algo =}.\")" ] }, { @@ -215,20 +223,20 @@ "source": [ "# rk4 time stepping\n", "Ex_of_t_rk4 = {}\n", - "time = 0.\n", + "time = 0.0\n", "n = 0\n", "while time < Tend:\n", " n += 1\n", - " \n", + "\n", " # advance in time\n", " prop_rk4(dt)\n", " time += dt\n", - " \n", + "\n", " # evaluate solution and push to Omega\n", " Ex_of_t_rk4[time] = e_field(e1, e2, e3)\n", - " \n", + "\n", " if n % 100 == 0:\n", - " print(f'{n}/{int(np.ceil(Tend/dt))} steps completed with {prop_rk4._algo = }.')" + " print(f\"{n}/{int(np.ceil(Tend / dt))} steps completed with {prop_rk4._algo = }.\")" ] }, { @@ -238,18 +246,21 @@ "outputs": [], "source": [ "from struphy.diagnostics.diagn_tools import power_spectrum_2d\n", + "\n", "x, y, z = domain(e1, e2, e3)\n", "\n", "# fft in (t, z) of first component of e_field on physical grid\n", - "power_spectrum_2d(Ex_of_t_implicit,\n", - " 'e1',\n", - " 'Maxwell',\n", - " grids=[e1, e2, e3],\n", - " grids_mapped=[x, y, z],\n", - " component=0,\n", - " slice_at=[0, 0, None],\n", - " do_plot=True,\n", - " disp_name='Maxwell1D')" + "power_spectrum_2d(\n", + " Ex_of_t_implicit,\n", + " \"e1\",\n", + " \"Maxwell\",\n", + " grids=[e1, e2, e3],\n", + " grids_mapped=[x, y, z],\n", + " component=0,\n", + " slice_at=[0, 0, None],\n", + " do_plot=True,\n", + " disp_name=\"Maxwell1D\",\n", + ")" ] }, { @@ -259,15 +270,17 @@ "outputs": [], "source": [ "# fft in (t, z) of first component of e_field on physical grid\n", - "power_spectrum_2d(Ex_of_t_rk4,\n", - " 'e1',\n", - " 'Maxwell',\n", - " grids=[e1, e2, e3],\n", - " grids_mapped=[x, y, z],\n", - " component=0,\n", - " slice_at=[0, 0, None],\n", - " do_plot=True,\n", - " disp_name='Maxwell1D')" + "power_spectrum_2d(\n", + " Ex_of_t_rk4,\n", + " \"e1\",\n", + " \"Maxwell\",\n", + " grids=[e1, e2, e3],\n", + " grids_mapped=[x, y, z],\n", + " component=0,\n", + " slice_at=[0, 0, None],\n", + " do_plot=True,\n", + " disp_name=\"Maxwell1D\",\n", + ")" ] }, { diff --git a/tutorials_old/tutorial_09_vlasov_maxwell.ipynb b/tutorials_old/tutorial_09_vlasov_maxwell.ipynb index 7628f8f20..c7728e7e0 100644 --- a/tutorials_old/tutorial_09_vlasov_maxwell.ipynb +++ b/tutorials_old/tutorial_09_vlasov_maxwell.ipynb @@ -27,15 +27,16 @@ "outputs": [], "source": [ "# set up domain Omega\n", - "from struphy.geometry.domains import Cuboid\n", "import numpy as np\n", "\n", - "l1 = 0.\n", + "from struphy.geometry.domains import Cuboid\n", + "\n", + "l1 = 0.0\n", "r1 = 12.56\n", - "l2 = 0.\n", - "r2 = 1.\n", - "l3 = 0.\n", - "r3 = 1.\n", + "l2 = 0.0\n", + "r2 = 1.0\n", + "l3 = 0.0\n", + "r3 = 1.0\n", "domain = Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" ] }, @@ -78,18 +79,19 @@ "ppc = 10000\n", "domain_array = derham.domain_array\n", "nprocs = derham.domain_decomposition.nprocs\n", - "bc = ['periodic', 'periodic', 'periodic']\n", - "loading_params = {'seed': None}\n", + "bc = [\"periodic\", \"periodic\", \"periodic\"]\n", + "loading_params = {\"seed\": None}\n", "control_variate = True\n", "\n", "# instantiate Particle object\n", - "particles = Particles6D(ppc=ppc,\n", - " domain_decomp=(domain_array, nprocs),\n", - " bc=bc,\n", - " loading_params=loading_params,\n", - " control_variate=control_variate,\n", - " domain=domain,\n", - " )" + "particles = Particles6D(\n", + " ppc=ppc,\n", + " domain_decomp=(domain_array, nprocs),\n", + " bc=bc,\n", + " loading_params=loading_params,\n", + " control_variate=control_variate,\n", + " domain=domain,\n", + ")" ] }, { @@ -108,14 +110,16 @@ "outputs": [], "source": [ "# kinetic equilibrium\n", - "bckgr_params = {'Maxwellian3D': {'n': 1.}}\n", + "bckgr_params = {\"Maxwellian3D\": {\"n\": 1.0}}\n", "\n", "# density perturbation for weak Landau damping\n", "pert_params = {}\n", "pert_params[\"n\"] = {}\n", - "pert_params[\"n\"][\"ModesCos\"] = {'given_in_basis': '0',\n", - " 'ls': [1],\n", - " 'amps':[0.001],}\n", + "pert_params[\"n\"][\"ModesCos\"] = {\n", + " \"given_in_basis\": \"0\",\n", + " \"ls\": [1],\n", + " \"amps\": [0.001],\n", + "}\n", "\n", "particles.initialize_weights(bckgr_params=bckgr_params, pert_params=pert_params)" ] @@ -127,17 +131,17 @@ "outputs": [], "source": [ "# particle binning in v1\n", - "components = [False]*6\n", + "components = [False] * 6\n", "components[3] = True\n", "\n", - "vmin = -5.\n", - "vmax = 5.\n", + "vmin = -5.0\n", + "vmax = 5.0\n", "n_bins = 128\n", "bin_edges_v = np.linspace(vmin, vmax, n_bins + 1)\n", "\n", "f_v1, df_v1 = particles.binning(components=components, bin_edges=[bin_edges_v])\n", - "print(f'{f_v1.shape = }')\n", - "print(f'{df_v1.shape = }')" + "print(f\"{f_v1.shape = }\")\n", + "print(f\"{df_v1.shape = }\")" ] }, { @@ -149,10 +153,10 @@ "# plot in v1\n", "from matplotlib import pyplot as plt\n", "\n", - "v1_bins = bin_edges_v[:-1] + (vmax - vmin)/n_bins/2\n", + "v1_bins = bin_edges_v[:-1] + (vmax - vmin) / n_bins / 2\n", "plt.plot(v1_bins, f_v1)\n", - "plt.xlabel('vx')\n", - "plt.title('Initial Maxwellian');" + "plt.xlabel(\"vx\")\n", + "plt.title(\"Initial Maxwellian\");" ] }, { @@ -162,16 +166,16 @@ "outputs": [], "source": [ "# particle binning in e1\n", - "components = [False]*6\n", + "components = [False] * 6\n", "components[0] = True\n", "\n", - "emin = 0.\n", - "emax = 1.\n", + "emin = 0.0\n", + "emax = 1.0\n", "bin_edges_e = np.linspace(emin, emax, n_bins + 1)\n", "\n", "f_e1, df_e1 = particles.binning(components=components, bin_edges=[bin_edges_e])\n", - "print(f'{f_e1.shape = }')\n", - "print(f'{df_e1.shape = }')" + "print(f\"{f_e1.shape = }\")\n", + "print(f\"{df_e1.shape = }\")" ] }, { @@ -181,10 +185,10 @@ "outputs": [], "source": [ "# plot in e1\n", - "e1_bins = bin_edges_e[:-1] + (emax - emin)/n_bins/2\n", + "e1_bins = bin_edges_e[:-1] + (emax - emin) / n_bins / 2\n", "plt.plot(e1_bins, df_e1)\n", - "plt.xlabel('$\\eta_1$')\n", - "plt.title('Initial spatial perturbation');" + "plt.xlabel(\"$\\eta_1$\")\n", + "plt.title(\"Initial spatial perturbation\");" ] }, { @@ -194,13 +198,13 @@ "outputs": [], "source": [ "# particle binning in e1-v1\n", - "components = [False]*6\n", + "components = [False] * 6\n", "components[0] = True\n", "components[3] = True\n", "\n", "f_e1v1, df_e1v1 = particles.binning(components=components, bin_edges=[bin_edges_e, bin_edges_v])\n", - "print(f'{f_e1v1.shape = }')\n", - "print(f'{df_e1v1.shape = }')" + "print(f\"{f_e1v1.shape = }\")\n", + "print(f\"{df_e1v1.shape = }\")" ] }, { @@ -209,22 +213,22 @@ "metadata": {}, "outputs": [], "source": [ - "e1_bins = bin_edges_e[:-1] + (emax - emin)/n_bins/2\n", + "e1_bins = bin_edges_e[:-1] + (emax - emin) / n_bins / 2\n", "\n", "plt.figure(figsize=(7, 10))\n", "\n", "plt.subplot(2, 1, 1)\n", "plt.pcolor(e1_bins, v1_bins, f_e1v1.T)\n", - "plt.xlabel('$\\eta_1$')\n", - "plt.ylabel('$v_x$')\n", - "plt.title('Initial Maxwellian')\n", + "plt.xlabel(\"$\\eta_1$\")\n", + "plt.ylabel(\"$v_x$\")\n", + "plt.title(\"Initial Maxwellian\")\n", "plt.colorbar()\n", "\n", "plt.subplot(2, 1, 2)\n", "plt.pcolor(e1_bins, v1_bins, df_e1v1.T)\n", - "plt.xlabel('$\\eta_1$')\n", - "plt.ylabel('$v_x$')\n", - "plt.title('Initial perturbation')\n", + "plt.xlabel(\"$\\eta_1$\")\n", + "plt.ylabel(\"$v_x$\")\n", + "plt.title(\"Initial perturbation\")\n", "plt.colorbar();" ] }, @@ -242,14 +246,14 @@ "outputs": [], "source": [ "# accumulate charge density\n", - "from struphy.pic.accumulation.particles_to_grid import AccumulatorVector\n", "from struphy.pic.accumulation.accum_kernels import charge_density_0form\n", + "from struphy.pic.accumulation.particles_to_grid import AccumulatorVector\n", "from struphy.utils.pyccel import Pyccelkernel\n", "\n", "# instantiate\n", "charge_accum = AccumulatorVector(\n", " particles=particles,\n", - " space_id='H1',\n", + " space_id=\"H1\",\n", " kernel=Pyccelkernel(charge_density_0form),\n", " mass_ops=mass_ops,\n", " args_domain=domain.args_domain,\n", @@ -271,7 +275,7 @@ "# use L2-projection to get density\n", "from struphy.feec.projectors import L2Projector\n", "\n", - "l2_proj = L2Projector(space_id='H1', mass_ops=mass_ops)\n", + "l2_proj = L2Projector(space_id=\"H1\", mass_ops=mass_ops)\n", "\n", "rho_coeffs = l2_proj.solve(rho_vec)" ] @@ -283,7 +287,7 @@ "outputs": [], "source": [ "# fit rho coeffs into a callable field\n", - "rho = derham.create_spline_function(name='charge density', space_id='H1', coeffs=rho_coeffs)" + "rho = derham.create_spline_function(name=\"charge density\", space_id=\"H1\", coeffs=rho_coeffs)" ] }, { @@ -294,8 +298,8 @@ "source": [ "# evaluate at logical coordinates\n", "e1 = np.linspace(0, 1, 100)\n", - "e2 = .5\n", - "e3 = .5\n", + "e2 = 0.5\n", + "e3 = 0.5\n", "\n", "funval = rho(e1, e2, e3, squeeze_out=True)" ] @@ -307,10 +311,10 @@ "outputs": [], "source": [ "# plot rho in logical space\n", - "plt.plot(e1, 1e-3*np.cos(2*np.pi*e1), label='exact')\n", - "plt.plot(e1, funval, '--r', label='L2 projection of charge deposition')\n", - "plt.xlabel('$\\eta_1$')\n", - "plt.title('Charge density for Poisson solver')\n", + "plt.plot(e1, 1e-3 * np.cos(2 * np.pi * e1), label=\"exact\")\n", + "plt.plot(e1, funval, \"--r\", label=\"L2 projection of charge deposition\")\n", + "plt.xlabel(\"$\\eta_1$\")\n", + "plt.title(\"Charge density for Poisson solver\")\n", "plt.legend();" ] }, @@ -345,11 +349,11 @@ "metadata": {}, "outputs": [], "source": [ - "# create solution field in Vh_0 subset H1 \n", - "phi = derham.create_spline_function('my solution', 'H1')\n", + "# create solution field in Vh_0 subset H1\n", + "phi = derham.create_spline_function(\"my solution\", \"H1\")\n", "\n", - "# create solution field E in Vh_1 subset H(curl) \n", - "e_field = derham.create_spline_function('electric field', 'Hcurl')" + "# create solution field E in Vh_1 subset H(curl)\n", + "e_field = derham.create_spline_function(\"electric field\", \"Hcurl\")" ] }, { @@ -390,7 +394,7 @@ "outputs": [], "source": [ "# solve (call with arbitrary dt)\n", - "poisson(1.)" + "poisson(1.0)" ] }, { @@ -400,7 +404,7 @@ "outputs": [], "source": [ "# compute initial E field\n", - "e_field.vector = - derham.grad.dot(phi.vector)" + "e_field.vector = -derham.grad.dot(phi.vector)" ] }, { @@ -411,8 +415,8 @@ "source": [ "# evalaute at logical coordinates\n", "e1 = np.linspace(0, 1, 100)\n", - "e2 = .5\n", - "e3 = .5\n", + "e2 = 0.5\n", + "e3 = 0.5\n", "\n", "e_vals = e_field(e1, e2, e3, squeeze_out=True)" ] @@ -432,12 +436,12 @@ "metadata": {}, "outputs": [], "source": [ - "# plot solution \n", + "# plot solution\n", "from matplotlib import pyplot as plt\n", "\n", - "plt.plot(e1, e_vals[0], label='E')\n", - "plt.xlabel('$\\eta_1$')\n", - "plt.title('Initial electric field')\n", + "plt.plot(e1, e_vals[0], label=\"E\")\n", + "plt.xlabel(\"$\\eta_1$\")\n", + "plt.title(\"Initial electric field\")\n", "plt.legend();" ] }, @@ -521,40 +525,41 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", "from time import time\n", "\n", + "import numpy as np\n", + "\n", "# diagnostics\n", "time_vec = []\n", "energy_E = []\n", "\n", "# initial values\n", - "time_vec += [0.]\n", + "time_vec += [0.0]\n", "energy_E += [0.5 * mass_ops.M1.dot_inner(e_field.vector, e_field.vector)]\n", "\n", "# time stepping\n", - "Tend = 3.5 \n", - "dt = .05\n", + "Tend = 3.5\n", + "dt = 0.05\n", "Nt = int(Tend / dt)\n", "\n", - "t = 0.\n", + "t = 0.0\n", "n = 0\n", "while t < (Tend - dt):\n", " t += dt\n", " n += 1\n", - " \n", + "\n", " t0 = time()\n", " # advance in time\n", " prop_eta(dt)\n", " t1 = time()\n", - " print(f'Time for PushEta = {t1 - t0}')\n", - " \n", + " print(f\"Time for PushEta = {t1 - t0}\")\n", + "\n", " prop_coupling(dt)\n", " t2 = time()\n", - " print(f'Time for VlasovAmpere = {t2 - t1}')\n", - " \n", - " print(f'Time step {n} done in {t2 - t0} sec\\n')\n", - " \n", + " print(f\"Time for VlasovAmpere = {t2 - t1}\")\n", + "\n", + " print(f\"Time step {n} done in {t2 - t0} sec\\n\")\n", + "\n", " # diagnostics\n", " time_vec += [t]\n", " energy_E += [0.5 * mass_ops.M1.dot_inner(e_field.vector, e_field.vector)]" diff --git a/tutorials_old/tutorial_10_linear_mhd.ipynb b/tutorials_old/tutorial_10_linear_mhd.ipynb index 06201dfc7..9e801ae80 100644 --- a/tutorials_old/tutorial_10_linear_mhd.ipynb +++ b/tutorials_old/tutorial_10_linear_mhd.ipynb @@ -43,12 +43,12 @@ "# set up domain Omega\n", "from struphy.geometry.domains import Cuboid\n", "\n", - "xL = 0.\n", - "xR = 1.\n", - "yL = 0.\n", - "yR = 1.\n", - "zL = 0.\n", - "zR = 60.\n", + "xL = 0.0\n", + "xR = 1.0\n", + "yL = 0.0\n", + "yR = 1.0\n", + "zL = 0.0\n", + "zR = 60.0\n", "domain = Cuboid(l1=xL, r1=xR, l2=yL, r2=yR, l3=zL, r3=zR)" ] }, @@ -61,11 +61,11 @@ "# set up MHD equilibrium\n", "from struphy.fields_background.equils import HomogenSlab\n", "\n", - "B0x = 0.\n", - "B0y = 1.\n", - "B0z = 1.\n", - "beta = 1.\n", - "n0 = 1.\n", + "B0x = 0.0\n", + "B0y = 1.0\n", + "B0z = 1.0\n", + "beta = 1.0\n", + "n0 = 1.0\n", "mhd_equil = HomogenSlab(B0x=B0x, B0y=B0y, B0z=B0z, beta=beta, n0=n0)\n", "\n", "# must set domain of Cartesian MHD equilibirum\n", @@ -93,16 +93,16 @@ "metadata": {}, "outputs": [], "source": [ - "# create solution field u in Vh_2 subset H(div) \n", - "u_space = 'Hdiv' # choose 'H1vec' for comparison\n", - "mhd_u = derham.create_spline_function('velocity', u_space)\n", + "# create solution field u in Vh_2 subset H(div)\n", + "u_space = \"Hdiv\" # choose 'H1vec' for comparison\n", + "mhd_u = derham.create_spline_function(\"velocity\", u_space)\n", "\n", - "# create solution field B in Vh_2 subset H(div) \n", - "b_field = derham.create_spline_function('magnetic field', 'Hdiv')\n", + "# create solution field B in Vh_2 subset H(div)\n", + "b_field = derham.create_spline_function(\"magnetic field\", \"Hdiv\")\n", "\n", - "# create solution fields rho and p in Vh_3 subset L2 \n", - "mhd_rho = derham.create_spline_function('mass density', 'L2')\n", - "mhd_p = derham.create_spline_function('pressure', 'L2')" + "# create solution fields rho and p in Vh_3 subset L2\n", + "mhd_rho = derham.create_spline_function(\"mass density\", \"L2\")\n", + "mhd_p = derham.create_spline_function(\"pressure\", \"L2\")" ] }, { @@ -112,10 +112,14 @@ "outputs": [], "source": [ "# initial perturbations\n", - "pert_params_u = {\"noise\": {'comps' : [True, True, True],\n", - " 'direction' : 'e3',\n", - " 'amp' : 0.1, \n", - " 'seed' : None,}}" + "pert_params_u = {\n", + " \"noise\": {\n", + " \"comps\": [True, True, True],\n", + " \"direction\": \"e3\",\n", + " \"amp\": 0.1,\n", + " \"seed\": None,\n", + " }\n", + "}" ] }, { @@ -139,8 +143,8 @@ "# evalaute at logical coordinates\n", "import numpy as np\n", "\n", - "e1 = .5\n", - "e2 = .5\n", + "e1 = 0.5\n", + "e2 = 0.5\n", "e3 = np.linspace(0, 1, 100)\n", "\n", "u_vals = mhd_u(e1, e2, e3, squeeze_out=True)\n", @@ -161,17 +165,17 @@ "for i in range(3):\n", " plt.subplot(2, 3, i + 1)\n", " plt.plot(e3, u_vals[i])\n", - " plt.title(f'$\\hat u^{2 if u_space == \"Hdiv2\" else \" \"}_{i + 1}$')\n", - " plt.xlabel('$\\eta_3$')\n", + " plt.title(f\"$\\hat u^{2 if u_space == 'Hdiv2' else ' '}_{i + 1}$\")\n", + " plt.xlabel(\"$\\eta_3$\")\n", " if i == 0:\n", - " plt.ylabel('a.u.')\n", - " \n", + " plt.ylabel(\"a.u.\")\n", + "\n", " plt.subplot(2, 3, i + 4)\n", " plt.plot(e3, b_vals[i])\n", - " plt.title(f'$\\hat b^2_{i + 1}$')\n", - " plt.xlabel('$\\eta_3$')\n", + " plt.title(f\"$\\hat b^2_{i + 1}$\")\n", + " plt.xlabel(\"$\\eta_3$\")\n", " if i == 0:\n", - " plt.ylabel('a.u.')" + " plt.ylabel(\"a.u.\")" ] }, { @@ -219,7 +223,7 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.propagators.propagators_fields import ShearAlfven, Magnetosonic\n", + "from struphy.propagators.propagators_fields import Magnetosonic, ShearAlfven\n", "\n", "# default parameters of Propagator\n", "opts = ShearAlfven.options(default=True)\n", @@ -253,11 +257,7 @@ "metadata": {}, "outputs": [], "source": [ - "prop_2 = Magnetosonic(mhd_rho.vector,\n", - " mhd_u.vector,\n", - " mhd_p.vector,\n", - " u_space=u_space,\n", - " b=b_field.vector)" + "prop_2 = Magnetosonic(mhd_rho.vector, mhd_u.vector, mhd_p.vector, u_space=u_space, b=b_field.vector)" ] }, { @@ -267,27 +267,27 @@ "outputs": [], "source": [ "# time stepping, with both propagators\n", - "Tend = 180. - 1e-6\n", - "dt = .15\n", + "Tend = 180.0 - 1e-6\n", + "dt = 0.15\n", "\n", "u_of_t = {}\n", "p_of_t = {}\n", - "time = 0.\n", + "time = 0.0\n", "n = 0\n", "while time < Tend:\n", " n += 1\n", - " \n", + "\n", " # advance in time\n", " prop_1(dt)\n", " prop_2(dt)\n", " time += dt\n", - " \n", - " # evaluate solution \n", + "\n", + " # evaluate solution\n", " u_of_t[time] = mhd_u(e1, e2, e3)\n", " p_of_t[time] = [mhd_p(e1, e2, e3)]\n", - " \n", + "\n", " if n % 100 == 0:\n", - " print(f'{n}/{int(np.ceil(Tend/dt))} steps completed.')" + " print(f\"{n}/{int(np.ceil(Tend / dt))} steps completed.\")" ] }, { @@ -308,27 +308,27 @@ "outputs": [], "source": [ "# time stepping, with both propagators\n", - "Tend = 180. - 1e-6\n", - "dt = .15\n", + "Tend = 180.0 - 1e-6\n", + "dt = 0.15\n", "\n", "u_of_t_ex = {}\n", "p_of_t_ex = {}\n", - "time = 0.\n", + "time = 0.0\n", "n = 0\n", "while time < Tend:\n", " n += 1\n", - " \n", + "\n", " # advance in time\n", " prop_1_explicit(dt)\n", " prop_2(dt)\n", " time += dt\n", - " \n", - " # evaluate solution \n", + "\n", + " # evaluate solution\n", " u_of_t_ex[time] = mhd_u(e1, e2, e3)\n", " p_of_t_ex[time] = [mhd_p(e1, e2, e3)]\n", - " \n", + "\n", " if n % 100 == 0:\n", - " print(f'{n}/{int(np.ceil(Tend/dt))} steps completed.')" + " print(f\"{n}/{int(np.ceil(Tend / dt))} steps completed.\")" ] }, { @@ -344,24 +344,21 @@ "# equilibrium pressure\n", "p0 = beta * (B0x**2 + B0y**2 + B0z**2) / 2\n", "\n", - "disp_params = {'B0x': B0x,\n", - " 'B0y': B0y,\n", - " 'B0z': B0z,\n", - " 'p0': p0,\n", - " 'n0': n0,\n", - " 'gamma': 5/3}\n", + "disp_params = {\"B0x\": B0x, \"B0y\": B0y, \"B0z\": B0z, \"p0\": p0, \"n0\": n0, \"gamma\": 5 / 3}\n", "\n", "# fft in (t, z) of first component of e_field on physical grid\n", - "power_spectrum_2d(u_of_t,\n", - " 'mhd_u',\n", - " 'notebook tutorial',\n", - " grids=[e1, e2, e3],\n", - " grids_mapped=[x, y, z],\n", - " component=0,\n", - " slice_at=[0, 0, None],\n", - " do_plot=True,\n", - " disp_name='MHDhomogenSlab',\n", - " disp_params=disp_params)" + "power_spectrum_2d(\n", + " u_of_t,\n", + " \"mhd_u\",\n", + " \"notebook tutorial\",\n", + " grids=[e1, e2, e3],\n", + " grids_mapped=[x, y, z],\n", + " component=0,\n", + " slice_at=[0, 0, None],\n", + " do_plot=True,\n", + " disp_name=\"MHDhomogenSlab\",\n", + " disp_params=disp_params,\n", + ")" ] }, { @@ -377,24 +374,21 @@ "# equilibrium pressure\n", "p0 = beta * (B0x**2 + B0y**2 + B0z**2) / 2\n", "\n", - "disp_params = {'B0x': B0x,\n", - " 'B0y': B0y,\n", - " 'B0z': B0z,\n", - " 'p0': p0,\n", - " 'n0': n0,\n", - " 'gamma': 5/3}\n", + "disp_params = {\"B0x\": B0x, \"B0y\": B0y, \"B0z\": B0z, \"p0\": p0, \"n0\": n0, \"gamma\": 5 / 3}\n", "\n", "# fft in (t, z) of first component of e_field on physical grid\n", - "power_spectrum_2d(u_of_t_ex,\n", - " 'mhd_u',\n", - " 'notebook tutorial',\n", - " grids=[e1, e2, e3],\n", - " grids_mapped=[x, y, z],\n", - " component=0,\n", - " slice_at=[0, 0, None],\n", - " do_plot=True,\n", - " disp_name='MHDhomogenSlab',\n", - " disp_params=disp_params)" + "power_spectrum_2d(\n", + " u_of_t_ex,\n", + " \"mhd_u\",\n", + " \"notebook tutorial\",\n", + " grids=[e1, e2, e3],\n", + " grids_mapped=[x, y, z],\n", + " component=0,\n", + " slice_at=[0, 0, None],\n", + " do_plot=True,\n", + " disp_name=\"MHDhomogenSlab\",\n", + " disp_params=disp_params,\n", + ")" ] }, { @@ -403,16 +397,18 @@ "metadata": {}, "outputs": [], "source": [ - "power_spectrum_2d(p_of_t,\n", - " 'mhd_p',\n", - " 'notebook tutorial',\n", - " grids=[e1, e2, e3],\n", - " grids_mapped=[x, y, z],\n", - " component=0,\n", - " slice_at=[0, 0, None],\n", - " do_plot=True,\n", - " disp_name='MHDhomogenSlab',\n", - " disp_params=disp_params)" + "power_spectrum_2d(\n", + " p_of_t,\n", + " \"mhd_p\",\n", + " \"notebook tutorial\",\n", + " grids=[e1, e2, e3],\n", + " grids_mapped=[x, y, z],\n", + " component=0,\n", + " slice_at=[0, 0, None],\n", + " do_plot=True,\n", + " disp_name=\"MHDhomogenSlab\",\n", + " disp_params=disp_params,\n", + ")" ] }, { @@ -421,16 +417,18 @@ "metadata": {}, "outputs": [], "source": [ - "power_spectrum_2d(p_of_t_ex,\n", - " 'mhd_p',\n", - " 'notebook tutorial',\n", - " grids=[e1, e2, e3],\n", - " grids_mapped=[x, y, z],\n", - " component=0,\n", - " slice_at=[0, 0, None],\n", - " do_plot=True,\n", - " disp_name='MHDhomogenSlab',\n", - " disp_params=disp_params)" + "power_spectrum_2d(\n", + " p_of_t_ex,\n", + " \"mhd_p\",\n", + " \"notebook tutorial\",\n", + " grids=[e1, e2, e3],\n", + " grids_mapped=[x, y, z],\n", + " component=0,\n", + " slice_at=[0, 0, None],\n", + " do_plot=True,\n", + " disp_name=\"MHDhomogenSlab\",\n", + " disp_params=disp_params,\n", + ")" ] }, { @@ -470,9 +468,9 @@ "a = 1\n", "R0 = 3\n", "\n", - "a1 = 0. + 1e-6\n", + "a1 = 0.0 + 1e-6\n", "a2 = a\n", - "Lz = 2*np.pi*R0\n", + "Lz = 2 * np.pi * R0\n", "domain = HollowCylinder(a1=a1, a2=a2, Lz=Lz)" ] }, @@ -538,10 +536,10 @@ "\n", "# noise_params = {\n", "# 'comps' : {\n", - "# 'velocity' : [True, True, True], \n", + "# 'velocity' : [True, True, True],\n", "# },\n", "# 'direction' : 'e3',\n", - "# 'amp' : 0.1, \n", + "# 'amp' : 0.1,\n", "# 'seed' : None,\n", "# }\n", "\n", @@ -555,14 +553,14 @@ "metadata": {}, "outputs": [], "source": [ - "# # create solution field u in Vh_2 subset H(div) \n", + "# # create solution field u in Vh_2 subset H(div)\n", "# u_space = 'Hdiv' # choose 'H1vec' for comparison\n", "# mhd_u = derham.create_spline_function('velocity', u_space, pert_params=pert_params)\n", "\n", - "# # create solution field B in Vh_2 subset H(div) \n", + "# # create solution field B in Vh_2 subset H(div)\n", "# b_field = derham.create_spline_function('magnetic field', 'Hdiv', pert_params=pert_params)\n", "\n", - "# # create solution fields rho and p in Vh_3 subset L2 \n", + "# # create solution fields rho and p in Vh_3 subset L2\n", "# mhd_rho = derham.create_spline_function('mass density', 'L2', pert_params=pert_params)\n", "# mhd_p = derham.create_spline_function('pressure', 'L2', pert_params=pert_params)" ] @@ -614,7 +612,7 @@ "# plt.xlabel('$\\eta_3$')\n", "# if i == 0:\n", "# plt.ylabel('a.u.')\n", - " \n", + "\n", "# plt.subplot(2, 3, i + 4)\n", "# plt.plot(e3, b_vals[i])\n", "# plt.title(f'$\\hat b^2_{i + 1}$')\n", diff --git a/tutorials_old/tutorial_12_struphy_data_pproc.ipynb b/tutorials_old/tutorial_12_struphy_data_pproc.ipynb index 9004f01b0..c05ee2535 100644 --- a/tutorials_old/tutorial_12_struphy_data_pproc.ipynb +++ b/tutorials_old/tutorial_12_struphy_data_pproc.ipynb @@ -50,9 +50,10 @@ "outputs": [], "source": [ "import os\n", + "\n", "import struphy\n", "\n", - "path_out = os.path.join(struphy.__path__[0], 'io/out', 'tutorial_02')\n", + "path_out = os.path.join(struphy.__path__[0], \"io/out\", \"tutorial_02\")\n", "\n", "print(path_out)\n", "os.listdir(path_out)" @@ -72,7 +73,7 @@ "metadata": {}, "outputs": [], "source": [ - "with open(os.path.join(path_out, 'meta.txt')) as file:\n", + "with open(os.path.join(path_out, \"meta.txt\")) as file:\n", " print(file.read())" ] }, @@ -90,7 +91,7 @@ "metadata": {}, "outputs": [], "source": [ - "path_data = os.path.join(path_out, 'data/')\n", + "path_data = os.path.join(path_out, \"data/\")\n", "\n", "os.listdir(path_data)" ] @@ -111,11 +112,11 @@ "source": [ "import h5py\n", "\n", - "with h5py.File(os.path.join(path_data, 'data_proc0.hdf5'), \"r\") as f:\n", + "with h5py.File(os.path.join(path_data, \"data_proc0.hdf5\"), \"r\") as f:\n", " for key in f.keys():\n", - " print(key + '/')\n", + " print(key + \"/\")\n", " for subkey in f[key].keys():\n", - " print(' ' + subkey + '/')" + " print(\" \" + subkey + \"/\")" ] }, { @@ -147,6 +148,7 @@ "outputs": [], "source": [ "from struphy.post_processing.pproc_struphy import main\n", + "\n", "help(main)" ] }, @@ -204,7 +206,7 @@ "metadata": {}, "outputs": [], "source": [ - "data_path = os.path.join(path_out, 'post_processing')\n", + "data_path = os.path.join(path_out, \"post_processing\")\n", "os.listdir(data_path)" ] }, @@ -224,7 +226,7 @@ "source": [ "import numpy as np\n", "\n", - "t_grid = np.load(os.path.join(data_path, 't_grid.npy'))\n", + "t_grid = np.load(os.path.join(data_path, \"t_grid.npy\"))\n", "t_grid" ] }, @@ -243,7 +245,7 @@ "metadata": {}, "outputs": [], "source": [ - "kinetic_path = os.path.join(data_path, 'kinetic_data')\n", + "kinetic_path = os.path.join(data_path, \"kinetic_data\")\n", "\n", "print(os.listdir(kinetic_path))" ] @@ -262,7 +264,7 @@ "metadata": {}, "outputs": [], "source": [ - "ep_path = os.path.join(kinetic_path, 'energetic_ions')\n", + "ep_path = os.path.join(kinetic_path, \"energetic_ions\")\n", "\n", "os.listdir(ep_path)" ] @@ -287,7 +289,7 @@ "metadata": {}, "outputs": [], "source": [ - "f_path = os.path.join(ep_path, 'distribution_function')\n", + "f_path = os.path.join(ep_path, \"distribution_function\")\n", "\n", "print(os.listdir(f_path))" ] @@ -315,8 +317,8 @@ "metadata": {}, "outputs": [], "source": [ - "grid_v1 = np.load(os.path.join(f_path, 'v1/', 'grid_v1.npy'))\n", - "f_binned = np.load(os.path.join(f_path, 'v1/', 'f_binned.npy'))\n", + "grid_v1 = np.load(os.path.join(f_path, \"v1/\", \"grid_v1.npy\"))\n", + "f_binned = np.load(os.path.join(f_path, \"v1/\", \"f_binned.npy\"))\n", "\n", "print(grid_v1.shape)\n", "print(f_binned.shape)" @@ -342,9 +344,9 @@ "steps = [0, 1, 2, -1]\n", "for n, step in enumerate(steps):\n", " plt.subplot(2, 2, n + 1)\n", - " plt.plot(grid_v1, f_binned[step], label=f'time = {t_grid[step]}')\n", - " plt.xlabel('v1')\n", - " plt.ylabel('fvol(v1)')\n", + " plt.plot(grid_v1, f_binned[step], label=f\"time = {t_grid[step]}\")\n", + " plt.xlabel(\"v1\")\n", + " plt.ylabel(\"fvol(v1)\")\n", " plt.legend()" ] }, @@ -361,9 +363,9 @@ "metadata": {}, "outputs": [], "source": [ - "grid_e1 = np.load(os.path.join(f_path, 'e1_v1/', 'grid_e1.npy'))\n", - "grid_v1 = np.load(os.path.join(f_path, 'e1_v1/', 'grid_v1.npy'))\n", - "f_binned = np.load(os.path.join(f_path, 'e1_v1/', 'f_binned.npy'))\n", + "grid_e1 = np.load(os.path.join(f_path, \"e1_v1/\", \"grid_e1.npy\"))\n", + "grid_v1 = np.load(os.path.join(f_path, \"e1_v1/\", \"grid_v1.npy\"))\n", + "f_binned = np.load(os.path.join(f_path, \"e1_v1/\", \"f_binned.npy\"))\n", "\n", "print(grid_e1.shape)\n", "print(grid_v1.shape)\n", @@ -388,10 +390,10 @@ "steps = [0, 1, 2, -1]\n", "for n, step in enumerate(steps):\n", " plt.subplot(2, 2, n + 1)\n", - " plt.pcolor(grid_e1, grid_v1, f_binned[step].T, label=f'time = {t_grid[step]}')\n", - " plt.xlabel('e1')\n", - " plt.ylabel('v1')\n", - " plt.title('fvol(e1, v1)')\n", + " plt.pcolor(grid_e1, grid_v1, f_binned[step].T, label=f\"time = {t_grid[step]}\")\n", + " plt.xlabel(\"e1\")\n", + " plt.ylabel(\"v1\")\n", + " plt.title(\"fvol(e1, v1)\")\n", " plt.legend()" ] }, @@ -410,7 +412,7 @@ "metadata": {}, "outputs": [], "source": [ - "orbits_path = os.path.join(ep_path, 'orbits')\n", + "orbits_path = os.path.join(ep_path, \"orbits\")\n", "\n", "print(len(os.listdir(orbits_path)))\n", "for el in sorted(os.listdir(orbits_path)):\n", @@ -430,12 +432,12 @@ "metadata": {}, "outputs": [], "source": [ - "markers = np.load(os.path.join(orbits_path, 'energetic_ions_00.npy'))\n", + "markers = np.load(os.path.join(orbits_path, \"energetic_ions_00.npy\"))\n", "\n", - "with open (os.path.join(orbits_path, 'energetic_ions_00.txt')) as file:\n", + "with open(os.path.join(orbits_path, \"energetic_ions_00.txt\")) as file:\n", " orbit_str = file.read()\n", - " \n", - "markers_txt = orbit_str.split('\\n')\n", + "\n", + "markers_txt = orbit_str.split(\"\\n\")\n", "markers_txt[:6]" ] }, @@ -470,8 +472,8 @@ "outputs": [], "source": [ "plt.scatter(markers[:, 1], markers[:, 2])\n", - "plt.xlabel('x')\n", - "plt.ylabel('y')" + "plt.xlabel(\"x\")\n", + "plt.ylabel(\"y\")" ] }, { @@ -490,7 +492,7 @@ "metadata": {}, "outputs": [], "source": [ - "fluid_path = os.path.join(data_path, 'fields_data')\n", + "fluid_path = os.path.join(data_path, \"fields_data\")\n", "\n", "print(os.listdir(fluid_path))" ] @@ -514,7 +516,7 @@ "metadata": {}, "outputs": [], "source": [ - "os.listdir(os.path.join(fluid_path, 'em_fields'))" + "os.listdir(os.path.join(fluid_path, \"em_fields\"))" ] }, { @@ -523,7 +525,7 @@ "metadata": {}, "outputs": [], "source": [ - "os.listdir(os.path.join(fluid_path, 'mhd'))" + "os.listdir(os.path.join(fluid_path, \"mhd\"))" ] }, { @@ -541,9 +543,9 @@ "source": [ "import pickle\n", "\n", - "with open(os.path.join(fluid_path, 'grids_phy.bin'), 'rb') as file:\n", + "with open(os.path.join(fluid_path, \"grids_phy.bin\"), \"rb\") as file:\n", " x_grid, y_grid, z_grid = pickle.load(file)\n", - " \n", + "\n", "print(type(x_grid))\n", "print(x_grid.shape)" ] @@ -561,9 +563,9 @@ "metadata": {}, "outputs": [], "source": [ - "with open(os.path.join(fluid_path, 'em_fields', 'b_field_phy.bin'), 'rb') as file:\n", + "with open(os.path.join(fluid_path, \"em_fields\", \"b_field_phy.bin\"), \"rb\") as file:\n", " b2 = pickle.load(file)\n", - " \n", + "\n", "print(type(b2))\n", "print(len(b2))" ] @@ -609,9 +611,9 @@ "for n, step in enumerate(steps):\n", " t = t_grid[step]\n", " plt.subplot(4, 2, n + 1)\n", - " plt.plot(x_grid[:, 0, 0], b2[t][2][:, 0, 0], label=f'time = {t}')\n", - " plt.xlabel('x')\n", - " plt.ylabel('$B_z$(x)')\n", + " plt.plot(x_grid[:, 0, 0], b2[t][2][:, 0, 0], label=f\"time = {t}\")\n", + " plt.xlabel(\"x\")\n", + " plt.ylabel(\"$B_z$(x)\")\n", " plt.legend()" ] } diff --git a/utils/set_release_dependencies.py b/utils/set_release_dependencies.py index 96e29343a..08a54bba5 100644 --- a/utils/set_release_dependencies.py +++ b/utils/set_release_dependencies.py @@ -2,8 +2,6 @@ import re import tomllib -import tomli_w - def get_min_bound(entry): match = re.search(r"(>=|==|~=|>|>)\s*([\w\.\-]+)", entry) @@ -42,7 +40,11 @@ def update_dependencies(dependencies): try: installed_version = importlib.metadata.version(package_name) - package_deps = {"installed": installed_version, "min": get_min_bound(entry), "max": get_max_bound(entry)} + package_deps = { + "installed": installed_version, + "min": get_min_bound(entry), + "max": get_max_bound(entry), + } if package_deps["installed"]: dependencies[i] = generate_updated_entry(package_name, package_deps) @@ -59,6 +61,8 @@ def update_dependencies(dependencies): def main(): with open("pyproject.toml", "rb") as f: + import tomllib + pyproject_data = tomllib.load(f) mandatory_dependencies = pyproject_data["project"]["dependencies"] @@ -69,6 +73,8 @@ def main(): update_dependencies(group_deps) with open("pyproject.toml", "wb") as f: + import tomli_w + tomli_w.dump(pyproject_data, f) From 601917f51ef7538586fc408c1be7e050840aa07e Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 30 Oct 2025 17:45:13 +0100 Subject: [PATCH 12/83] Use the GitHub container registry to store images (#74) Redo of https://github.com/struphy-hub/struphy/pull/66, and a few lingering changed which were not included previously. --------- Co-authored-by: Stefan Possanner Co-authored-by: Byung Kyu Na Co-authored-by: Stefan Possanner <86720346+spossann@users.noreply.github.com> --- .../install/struphy_in_container/action.yml | 26 ++ .github/workflows/reusable-testing.yml | 95 ++++ .github/workflows/static_analysis.yml | 9 +- .github/workflows/test-PR-models.yml | 72 +++ .github/workflows/test-PR-unit.yml | 68 +++ .github/workflows/ubuntu-latest.yml | 19 +- docker/almalinux-latest.dockerfile | 14 +- docker/fedora-latest.dockerfile | 14 +- .../mpcdf-gcc-openmpi-with-struphy.dockerfile | 56 --- docker/opensuse-latest.dockerfile | 14 +- docker/ubuntu-latest-with-struphy.dockerfile | 53 ++- docker/ubuntu-latest.dockerfile | 41 +- pyproject.toml | 10 + src/struphy/console/test.py | 4 +- src/struphy/feec/tests/test_l2_projectors.py | 8 +- tutorial_07_data_structures.ipynb | 20 +- tutorials/tutorial_01_parameter_files.ipynb | 79 ++-- tutorials/tutorial_02_test_particles.ipynb | 434 +++++++++--------- ...l_03_smoothed_particle_hydrodynamics.ipynb | 314 ++++++------- tutorials/tutorial_04_vlasov_maxwell.ipynb | 84 ++-- .../tutorial_01_parameter_files.ipynb | 67 ++- tutorials_old/tutorial_01_particles.ipynb | 66 +-- .../tutorial_02_fluid_particles.ipynb | 26 +- utils/set_release_dependencies.py | 2 + 24 files changed, 895 insertions(+), 700 deletions(-) create mode 100644 .github/actions/install/struphy_in_container/action.yml create mode 100644 .github/workflows/reusable-testing.yml create mode 100644 .github/workflows/test-PR-models.yml create mode 100644 .github/workflows/test-PR-unit.yml delete mode 100644 docker/mpcdf-gcc-openmpi-with-struphy.dockerfile diff --git a/.github/actions/install/struphy_in_container/action.yml b/.github/actions/install/struphy_in_container/action.yml new file mode 100644 index 000000000..0bfaf30c7 --- /dev/null +++ b/.github/actions/install/struphy_in_container/action.yml @@ -0,0 +1,26 @@ +name: "Install Struphy in Container" + +runs: + using: composite + steps: + - name: Git branch name + id: git-branch-name + uses: EthanSK/git-branch-name-action@v1 + - name: Echo the branch name + shell: bash + run: echo "Branch name ${GIT_BRANCH_NAME}" + - name: Install struphy + shell: bash + run: | + ls / -a + which python3 + cd /struphy_c_ + git status + git fetch origin + echo ${GIT_BRANCH_NAME} + git checkout ${GIT_BRANCH_NAME} + git pull + source env_c_/bin/activate + which python3 + struphy -p + pip install -e ".[phys,mpi,doc]" \ No newline at end of file diff --git a/.github/workflows/reusable-testing.yml b/.github/workflows/reusable-testing.yml new file mode 100644 index 000000000..c27af6914 --- /dev/null +++ b/.github/workflows/reusable-testing.yml @@ -0,0 +1,95 @@ +name: Testing + +on: + workflow_call: + inputs: + os: + required: true + type: string + +jobs: + test: + runs-on: ${{ inputs.os }} + env: + OMPI_MCA_rmaps_base_oversubscribe: 1 # Linux + PRRTE_MCA_rmaps_base_oversubscribe: 1 # MacOS + strategy: + fail-fast: false + matrix: + python-version: ["3.12"] + compile-language: ["fortran", "c"] + test-type: ["unit", "model", "quickstart", "tutorials"] + + steps: + # Checkout the repository + - name: Checkout code + uses: actions/checkout@v5 + + # https://docs.github.com/en/actions/tutorials/build-and-test-code/python + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + # You can test your matrix by printing the current Python version + - name: Display Python version + run: python -c "import sys; print(sys.version)" + + # Cache pip dependencies + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-pip- + + # Install prereqs + # I don't think it's possible to use a single action for this because + # we can't use ${inputs.os} in an if statement, so we have to use two different actions. + - name: Install prerequisites (Ubuntu) + if: inputs.os == 'ubuntu-latest' + uses: ./.github/actions/install/ubuntu-latest + + - name: Install prerequisites (macOS) + if: inputs.os == 'macos-latest' + uses: ./.github/actions/install/macos-latest + + # Check that mpirun oversubscribing works, doesn't work unless OMPI_MCA_rmaps_base_oversubscribe==1 + - name: Test mpirun + run: | + echo $OMPI_MCA_rmaps_base_oversubscribe + echo $PRRTE_MCA_rmaps_base_oversubscribe + pip install mpi4py -U + which mpirun + mpirun --version + mpirun --oversubscribe --report-bindings -n 4 python -c "from mpi4py import MPI; comm=MPI.COMM_WORLD; print(f'Hello from rank {comm.Get_rank()} of {comm.Get_size()}'); assert comm.Get_size()==4" + + # Clone struphy-ci-testing + - name: Install struphy + uses: ./.github/actions/install/install-struphy + env: + FC: ${{ env.FC }} + CC: ${{ env.CC }} + CXX: ${{ env.CXX }} + + # Compile + - name: Compile kernels + uses: ./.github/actions/compile + + # Run tests + - name: Run unit tests + if: matrix.test-type == 'unit' + uses: ./.github/actions/tests/unit + + - name: Run model tests + if: matrix.test-type == 'model' + uses: ./.github/actions/tests/models + + - name: Run quickstart tests + if: matrix.test-type == 'quickstart' + uses: ./.github/actions/tests/quickstart + + - name: Run tutorials + if: matrix.test-type == 'tutorials' + uses: ./.github/actions/tests/tutorials diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 3022a70c2..2fd4c2051 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -121,15 +121,10 @@ jobs: - name: Checkout the code uses: actions/checkout@v4 - # TODO: Remove --select I once all errors are fixed - - name: ruff check --select I + - name: Linting with ruff run: | pip install ruff - ruff check --select I - - - name: ruff format --check - run: | - ruff format --check + ruff check --select I src/**/*.py # pylint: # runs-on: ubuntu-latest diff --git a/.github/workflows/test-PR-models.yml b/.github/workflows/test-PR-models.yml new file mode 100644 index 000000000..9dfeb3ebc --- /dev/null +++ b/.github/workflows/test-PR-models.yml @@ -0,0 +1,72 @@ +name: PR - model tests in Container + +on: + pull_request: + branches: + - main + - devel + workflow_dispatch: + +defaults: + run: + shell: bash + +permissions: + contents: read + +# concurrency: +# group: "pages" +# cancel-in-progress: false + +jobs: + model-tests-in-container-with-struphy: + runs-on: ubuntu-latest + container: + image: ghcr.io/struphy-hub/struphy/ubuntu-with-struphy:latest + credentials: + username: spossann + password: ${{ secrets.GHCR_TOKEN }} + steps: + + - name: Check for dockerenv file + run: (ls /.dockerenv && echo Found dockerenv) || (echo No dockerenv) + + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Install Struphy in Container + uses: ./.github/actions/install/struphy_in_container + + - name: Compile Struphy + run: | + which python3 + source /struphy_c_/env_c_/bin/activate + which python3 + struphy compile --status + struphy compile + + - name: Model tests + shell: bash + run: | + which python3 + source /struphy_c_/env_c_/bin/activate + which python3 + struphy compile --status + struphy test LinearMHD + struphy test toy + struphy test models + struphy test verification + + - name: Model tests with MPI + shell: bash + run: | + which python3 + source /struphy_c_/env_c_/bin/activate + which python3 + struphy compile --status + struphy test models + struphy test models --mpi 2 + struphy test verification --mpi 1 + struphy test verification --mpi 4 + struphy test verification --mpi 4 --nclones 2 + struphy test VlasovAmpereOneSpecies --mpi 2 --nclones 2 diff --git a/.github/workflows/test-PR-unit.yml b/.github/workflows/test-PR-unit.yml new file mode 100644 index 000000000..1f6df8cf4 --- /dev/null +++ b/.github/workflows/test-PR-unit.yml @@ -0,0 +1,68 @@ +name: PR - unit tests in Container + +on: + pull_request: + branches: + - main + - devel + workflow_dispatch: + +defaults: + run: + shell: bash + +permissions: + contents: read + +# concurrency: +# group: "pages" +# cancel-in-progress: false + +jobs: + unit-tests-in-container-with-struphy: + runs-on: ubuntu-latest + container: + image: ghcr.io/struphy-hub/struphy/ubuntu-with-struphy:latest + credentials: + username: spossann + password: ${{ secrets.GHCR_TOKEN }} + steps: + + - name: Check for dockerenv file + run: (ls /.dockerenv && echo Found dockerenv) || (echo No dockerenv) + + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Install Struphy in Container + uses: ./.github/actions/install/struphy_in_container + + - name: Compile Struphy + run: | + which python3 + source /struphy_c_/env_c_/bin/activate + which python3 + struphy compile --status + struphy compile + + - name: Run unit tests with MPI + shell: bash + run: | + which python3 + source /struphy_c_/env_c_/bin/activate + which python3 + struphy compile --status + struphy --refresh-models + struphy test unit --mpi 2 + + - name: Run unit tests + shell: bash + run: | + which python3 + source /struphy_c_/env_c_/bin/activate + which python3 + struphy compile --status + struphy --refresh-models + pip show mpi4py + pip uninstall -y mpi4py + struphy test unit diff --git a/.github/workflows/ubuntu-latest.yml b/.github/workflows/ubuntu-latest.yml index e66aeb3b3..8fd73272e 100644 --- a/.github/workflows/ubuntu-latest.yml +++ b/.github/workflows/ubuntu-latest.yml @@ -1,20 +1,11 @@ -name: Ubuntu +name: Ubuntu latest - cronjob on: - push: - branches: - - main - - devel - pull_request: - branches: - - main - - devel - -# concurrency: -# group: ${{ github.ref }} -# cancel-in-progress: true + schedule: + # run at 1 a.m. on Sunday + - cron: "0 1 * * 0" jobs: ubuntu-latest-build: - uses: ./.github/workflows/testing.yml + uses: ./.github/workflows/reusable-testing.yml with: os: ubuntu-latest \ No newline at end of file diff --git a/docker/almalinux-latest.dockerfile b/docker/almalinux-latest.dockerfile index acb1824fd..8d84d5e45 100644 --- a/docker/almalinux-latest.dockerfile +++ b/docker/almalinux-latest.dockerfile @@ -1,12 +1,13 @@ -# Here is how to build the image and upload it to the mpcdf gitlab registry: +# Here is how to build the image and upload it to the Github package registry: # # We suppose you are in the struphy repo directory. -# Start the docker engine and run "docker login" with the following token: +# Start the docker engine and login to the Github package registry using a github personal acces token (classic): # -# TOKEN=gldt-CgMRBMtePbSwdWTxKw4Q; echo "$TOKEN" | docker login gitlab-registry.mpcdf.mpg.de -u gitlab+deploy-token-162 --password-stdin +# export CR_PAT=YOUR_TOKEN +# echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin # docker info -# docker build -t gitlab-registry.mpcdf.mpg.de/struphy/struphy/almalinux-latest --provenance=false -f docker/almalinux-latest.dockerfile . -# docker push gitlab-registry.mpcdf.mpg.de/struphy/struphy/almalinux-latest +# docker build -t ghcr.io/struphy-hub/struphy/almalinux-with-reqs:latest --provenance=false -f docker/almalinux-latest.dockerfile . +# docker push ghcr.io/struphy-hub/struphy/almalinux-with-reqs:latest FROM almalinux:latest @@ -42,9 +43,6 @@ RUN echo "Installing additional tools..." \ && export CC=`which gcc` \ && export CXX=`which g++` -# create new working dir -WORKDIR /install_struphy_here/ - # allow mpirun as root ENV OMPI_ALLOW_RUN_AS_ROOT=1 ENV OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1 diff --git a/docker/fedora-latest.dockerfile b/docker/fedora-latest.dockerfile index 9cf384454..79c3ed6a7 100644 --- a/docker/fedora-latest.dockerfile +++ b/docker/fedora-latest.dockerfile @@ -1,12 +1,13 @@ -# Here is how to build the image and upload it to the mpcdf gitlab registry: +# Here is how to build the image and upload it to the Github package registry: # # We suppose you are in the struphy repo directory. -# Start the docker engine and run "docker login" with the following token: +# Start the docker engine and login to the Github package registry using a github personal acces token (classic): # -# TOKEN=gldt-CgMRBMtePbSwdWTxKw4Q; echo "$TOKEN" | docker login gitlab-registry.mpcdf.mpg.de -u gitlab+deploy-token-162 --password-stdin +# export CR_PAT=YOUR_TOKEN +# echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin # docker info -# docker build -t gitlab-registry.mpcdf.mpg.de/struphy/struphy/fedora-latest --provenance=false -f docker/fedora-latest.dockerfile . -# docker push gitlab-registry.mpcdf.mpg.de/struphy/struphy/fedora-latest +# docker build -t ghcr.io/struphy-hub/struphy/fedora-with-reqs:latest --provenance=false -f docker/fedora-latest.dockerfile . +# docker push ghcr.io/struphy-hub/struphy/fedora-with-reqs:latest FROM fedora:latest @@ -34,9 +35,6 @@ RUN echo "Installing additional tools..." \ && export CC=`which gcc` \ && export CXX=`which g++` - # create new working dir -WORKDIR /install_struphy_here/ - # allow mpirun as root ENV OMPI_ALLOW_RUN_AS_ROOT=1 ENV OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1 diff --git a/docker/mpcdf-gcc-openmpi-with-struphy.dockerfile b/docker/mpcdf-gcc-openmpi-with-struphy.dockerfile deleted file mode 100644 index 1b64254f9..000000000 --- a/docker/mpcdf-gcc-openmpi-with-struphy.dockerfile +++ /dev/null @@ -1,56 +0,0 @@ -# Here is how to build the image and upload it to the mpcdf gitlab registry: -# -# We suppose you are in the struphy repo directory. -# Start the docker engine and run "docker login" with the following token: -# -# TOKEN=gldt-CgMRBMtePbSwdWTxKw4Q; echo "$TOKEN" | docker login gitlab-registry.mpcdf.mpg.de -u gitlab+deploy-token-162 --password-stdin -# docker info -# docker build -t gitlab-registry.mpcdf.mpg.de/struphy/struphy/mpcdf-gcc-openmpi-with-struphy --provenance=false -f docker/mpcdf-gcc-openmpi-with-struphy.dockerfile . -# docker push gitlab-registry.mpcdf.mpg.de/struphy/struphy/mpcdf-gcc-openmpi-with-struphy - -FROM gitlab-registry.mpcdf.mpg.de/mpcdf/ci-module-image/gcc_14-openmpi_5_0:latest - -RUN source ./mpcdf/soft/SLE_15/packages/x86_64/Modules/5.4.0/etc/profile.d/modules.sh \ - && module load gcc/14 openmpi/5.0 python-waterboa/2024.06 git graphviz/8 \ - && module load cmake netcdf-serial mkl hdf5-serial \ - && export FC=`which gfortran` \ - && export CC=`which gcc` \ - && export CXX=`which g++` \ - && git clone https://gitlab.mpcdf.mpg.de/struphy/struphy.git struphy_c_ \ - && cd struphy_c_ \ - && python3 -m venv env_c_ \ - && source env_c_/bin/activate \ - && pip install -U pip \ - && pip install -e .[phys] --no-cache-dir --no-binary mpi4py \ - && struphy compile \ - && deactivate - -RUN source ./mpcdf/soft/SLE_15/packages/x86_64/Modules/5.4.0/etc/profile.d/modules.sh \ - && module load gcc/14 openmpi/5.0 python-waterboa/2024.06 git graphviz/8 \ - && module load cmake netcdf-serial mkl hdf5-serial \ - && export FC=`which gfortran` \ - && export CC=`which gcc` \ - && export CXX=`which g++` \ - && git clone https://gitlab.mpcdf.mpg.de/struphy/struphy.git struphy_fortran_\ - && cd struphy_fortran_ \ - && python3 -m venv env_fortran_ \ - && source env_fortran_/bin/activate \ - && pip install -U pip \ - && pip install -e .[phys] --no-cache-dir --no-binary mpi4py \ - && struphy compile --language fortran -y \ - && deactivate - -RUN source ./mpcdf/soft/SLE_15/packages/x86_64/Modules/5.4.0/etc/profile.d/modules.sh \ - && module load gcc/14 openmpi/5.0 python-waterboa/2024.06 git graphviz/8 \ - && module load cmake netcdf-serial mkl hdf5-serial \ - && export FC=`which gfortran` \ - && export CC=`which gcc` \ - && export CXX=`which g++` \ - && git clone https://gitlab.mpcdf.mpg.de/struphy/struphy.git struphy_fortran_--omp-pic\ - && cd struphy_fortran_--omp-pic \ - && python3 -m venv env_fortran_--omp-pic \ - && source env_fortran_--omp-pic/bin/activate \ - && pip install -U pip \ - && pip install -e .[phys] --no-cache-dir --no-binary mpi4py \ - && struphy compile --language fortran --omp-pic -y \ - && deactivate \ No newline at end of file diff --git a/docker/opensuse-latest.dockerfile b/docker/opensuse-latest.dockerfile index 04ecff4e4..ef7fc47f4 100644 --- a/docker/opensuse-latest.dockerfile +++ b/docker/opensuse-latest.dockerfile @@ -1,12 +1,13 @@ -# Here is how to build the image and upload it to the mpcdf gitlab registry: +# Here is how to build the image and upload it to the Github package registry: # # We suppose you are in the struphy repo directory. -# Start the docker engine and run "docker login" with the following token: +# Start the docker engine and login to the Github package registry using a github personal acces token (classic): # -# TOKEN=gldt-CgMRBMtePbSwdWTxKw4Q; echo "$TOKEN" | docker login gitlab-registry.mpcdf.mpg.de -u gitlab+deploy-token-162 --password-stdin +# export CR_PAT=YOUR_TOKEN +# echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin # docker info -# docker build -t gitlab-registry.mpcdf.mpg.de/struphy/struphy/opensuse-latest --provenance=false -f docker/opensuse-latest.dockerfile . -# docker push gitlab-registry.mpcdf.mpg.de/struphy/struphy/opensuse-latest +# docker build -t ghcr.io/struphy-hub/struphy/opensuse-with-reqs:latest --provenance=false -f docker/opensuse-latest.dockerfile . +# docker push ghcr.io/struphy-hub/struphy/opensuse-with-reqs:latest FROM opensuse/tumbleweed:latest @@ -42,9 +43,6 @@ RUN echo "Installing additional tools..." \ && export CXX=`which g++` \ && zypper clean --all -# Create a new working directory -WORKDIR /install_struphy_here/ - # Allow mpirun to run as root (for OpenMPI) ENV OMPI_ALLOW_RUN_AS_ROOT=1 ENV OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1 diff --git a/docker/ubuntu-latest-with-struphy.dockerfile b/docker/ubuntu-latest-with-struphy.dockerfile index c7fea9c9b..5b8d8c0fd 100644 --- a/docker/ubuntu-latest-with-struphy.dockerfile +++ b/docker/ubuntu-latest-with-struphy.dockerfile @@ -1,12 +1,13 @@ -# Here is how to build the image and upload it to the mpcdf gitlab registry: +# Here is how to build the image and upload it to the Github package registry: # # We suppose you are in the struphy repo directory. -# Start the docker engine and run "docker login" with the following token: +# Start the docker engine and login to the Github package registry using a github personal acces token (classic): # -# TOKEN=gldt-CgMRBMtePbSwdWTxKw4Q; echo "$TOKEN" | docker login gitlab-registry.mpcdf.mpg.de -u gitlab+deploy-token-162 --password-stdin +# export CR_PAT=YOUR_TOKEN +# echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdinn # docker info -# docker build -t gitlab-registry.mpcdf.mpg.de/struphy/struphy/ubuntu-latest-with-struphy --provenance=false -f docker/ubuntu-latest-with-struphy.dockerfile . -# docker push gitlab-registry.mpcdf.mpg.de/struphy/struphy/ubuntu-latest-with-struphy +# docker build -t ghcr.io/struphy-hub/struphy/ubuntu-with-struphy:latest --provenance=false -f docker/ubuntu-latest-with-struphy.dockerfile . +# docker push ghcr.io/struphy-hub/struphy/ubuntu-with-struphy:latest FROM ubuntu:latest @@ -16,49 +17,57 @@ ARG DEBIAN_FRONTEND=noninteractive RUN apt update -y && apt clean \ && apt install -y software-properties-common \ && add-apt-repository -y ppa:deadsnakes/ppa \ - && apt update -y \ - && apt install -y python3 \ + && apt update -y + +RUN apt install -y python3 \ && apt install -y python3-dev \ && apt install -y python3-pip \ - && apt install -y python3-venv \ - && apt install -y gfortran gcc \ - && apt install -y liblapack-dev libblas-dev \ - && apt install -y libopenmpi-dev openmpi-bin \ - && apt install -y libomp-dev libomp5 \ - && apt install -y git \ + && apt install -y python3-venv + +RUN apt install -y gfortran gcc \ + && apt install -y liblapack-dev libblas-dev + +RUN apt install -y libopenmpi-dev openmpi-bin \ + && apt install -y libomp-dev libomp5 + +RUN apt install -y git \ && apt install -y pandoc graphviz \ - && bash -c "source ~/.bashrc" \ - # for gvec - && apt install -y g++ liblapack3 cmake cmake-curses-gui zlib1g-dev libnetcdf-dev libnetcdff-dev \ + && bash -c "source ~/.bashrc" + +# for gvec +RUN apt install -y g++ liblapack3 cmake cmake-curses-gui zlib1g-dev libnetcdf-dev libnetcdff-dev \ && export FC=`which gfortran` \ && export CC=`which gcc` \ && export CXX=`which g++` # install three versions of struphy -RUN git clone https://gitlab.mpcdf.mpg.de/struphy/struphy.git struphy_c_ \ +RUN git clone https://github.com/struphy-hub/struphy.git struphy_c_ \ && cd struphy_c_ \ && python3 -m venv env_c_ \ && . env_c_/bin/activate \ && pip install -U pip \ - && pip install -e .[phys] --no-cache-dir \ + && pip install -e .[phys,mpi,doc] --no-cache-dir \ + && struphy compile --status \ && struphy compile \ && deactivate -RUN git clone https://gitlab.mpcdf.mpg.de/struphy/struphy.git struphy_fortran_\ +RUN git clone https://github.com/struphy-hub/struphy.git struphy_fortran_\ && cd struphy_fortran_ \ && python3 -m venv env_fortran_ \ && . env_fortran_/bin/activate \ && pip install -U pip \ - && pip install -e .[phys] --no-cache-dir \ + && pip install -e .[phys,mpi,doc] --no-cache-dir \ + && struphy compile --status \ && struphy compile --language fortran -y \ && deactivate -RUN git clone https://gitlab.mpcdf.mpg.de/struphy/struphy.git struphy_fortran_--omp-pic\ +RUN git clone https://github.com/struphy-hub/struphy.git struphy_fortran_--omp-pic\ && cd struphy_fortran_--omp-pic \ && python3 -m venv env_fortran_--omp-pic \ && . env_fortran_--omp-pic/bin/activate \ && pip install -U pip \ - && pip install -e .[phys] --no-cache-dir \ + && pip install -e .[phys,mpi,doc] --no-cache-dir \ + && struphy compile --status \ && struphy compile --language fortran --omp-pic -y \ && deactivate diff --git a/docker/ubuntu-latest.dockerfile b/docker/ubuntu-latest.dockerfile index adcf65609..386426c29 100644 --- a/docker/ubuntu-latest.dockerfile +++ b/docker/ubuntu-latest.dockerfile @@ -1,12 +1,13 @@ -# Here is how to build the image and upload it to the mpcdf gitlab registry: +# Here is how to build the image and upload it to the Github package registry: # # We suppose you are in the struphy repo directory. -# Start the docker engine and run "docker login" with the following token: +# Start the docker engine and login to the Github package registry using a github personal acces token (classic): # -# TOKEN=gldt-CgMRBMtePbSwdWTxKw4Q; echo "$TOKEN" | docker login gitlab-registry.mpcdf.mpg.de -u gitlab+deploy-token-162 --password-stdin +# export CR_PAT=YOUR_TOKEN +# echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin # docker info -# docker build -t gitlab-registry.mpcdf.mpg.de/struphy/struphy/ubuntu-latest --provenance=false -f docker/ubuntu-latest.dockerfile . -# docker push gitlab-registry.mpcdf.mpg.de/struphy/struphy/ubuntu-latest +# docker build -t ghcr.io/struphy-hub/struphy/ubuntu-with-reqs:latest --provenance=false -f docker/ubuntu-latest.dockerfile . +# docker push ghcr.io/struphy-hub/struphy/ubuntu-with-reqs:latest FROM ubuntu:latest @@ -16,27 +17,29 @@ ARG DEBIAN_FRONTEND=noninteractive RUN apt update -y && apt clean \ && apt install -y software-properties-common \ && add-apt-repository -y ppa:deadsnakes/ppa \ - && apt update -y \ - && apt install -y python3 \ + && apt update -y + +RUN apt install -y python3 \ && apt install -y python3-dev \ && apt install -y python3-pip \ - && apt install -y python3-venv \ - && apt install -y gfortran gcc \ - && apt install -y liblapack-dev libblas-dev \ - && apt install -y libopenmpi-dev openmpi-bin \ - && apt install -y libomp-dev libomp5 \ - && apt install -y git \ + && apt install -y python3-venv + +RUN apt install -y gfortran gcc \ + && apt install -y liblapack-dev libblas-dev + +RUN apt install -y libopenmpi-dev openmpi-bin \ + && apt install -y libomp-dev libomp5 + +RUN apt install -y git \ && apt install -y pandoc graphviz \ - && bash -c "source ~/.bashrc" \ - # for gvec - && apt install -y g++ liblapack3 cmake cmake-curses-gui zlib1g-dev libnetcdf-dev libnetcdff-dev \ + && bash -c "source ~/.bashrc" + +# for gvec +RUN apt install -y g++ liblapack3 cmake cmake-curses-gui zlib1g-dev libnetcdf-dev libnetcdff-dev \ && export FC=`which gfortran` \ && export CC=`which gcc` \ && export CXX=`which g++` -# Create a new working directory -WORKDIR /install_struphy_here/ - # allow mpirun as root ENV OMPI_ALLOW_RUN_AS_ROOT=1 ENV OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1 diff --git a/pyproject.toml b/pyproject.toml index 7e82a720e..4c4badf6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -194,3 +194,13 @@ markers = [ "hybrid", "single", ] + +[tool.pytest.ini_options] +markers = [ + "models", + "toy", + "fluid", + "kinetic", + "hybrid", + "single", +] diff --git a/src/struphy/console/test.py b/src/struphy/console/test.py index ebcff34d4..ab64707e3 100644 --- a/src/struphy/console/test.py +++ b/src/struphy/console/test.py @@ -42,14 +42,14 @@ def struphy_test( str(mpi), "pytest", "-k", - "not _models and not _tutorial and not pproc", + "not _models and not _tutorial and not pproc and not _verif_", "--with-mpi", ] else: cmd = [ "pytest", "-k", - "not _models and not _tutorial and not pproc", + "not _models and not _tutorial and not pproc and not _verif_", ] if with_desc: diff --git a/src/struphy/feec/tests/test_l2_projectors.py b/src/struphy/feec/tests/test_l2_projectors.py index 7da42eff4..2e9f611eb 100644 --- a/src/struphy/feec/tests/test_l2_projectors.py +++ b/src/struphy/feec/tests/test_l2_projectors.py @@ -15,7 +15,7 @@ @pytest.mark.parametrize("p", [[2, 1, 1], [3, 2, 1]]) @pytest.mark.parametrize("spl_kind", [[False, True, True]]) @pytest.mark.parametrize("array_input", [False, True]) -def test_l2_projectors_mappings(Nel, p, spl_kind, array_input, with_desc, do_plot=False): +def test_l2_projectors_mappings(Nel, p, spl_kind, array_input, with_gvec=False, with_desc=False, do_plot=False): """Tests the L2-projectors for all available mappings. Both callable and array inputs to the projectors are tested. @@ -50,6 +50,10 @@ def test_l2_projectors_mappings(Nel, p, spl_kind, array_input, with_desc, do_plo print(f"Testing {dom_class =}") print("#" * 80) + if "GVEC" in dom_type and not with_gvec: + print(f"Attention: {with_gvec =}, GVEC not tested here !!") + continue + if "DESC" in dom_type and not with_desc: print(f"Attention: {with_desc =}, DESC not tested here !!") continue @@ -260,5 +264,5 @@ def f(x, y, z): spl_kind = [False, True, True] array_input = True test_l2_projectors_mappings(Nel, p, spl_kind, array_input, do_plot=False, with_desc=False) - # test_l2_projectors_convergence(0, 1, True, do_plot=True) + test_l2_projectors_convergence(0, 1, True, do_plot=False) # test_l2_projectors_convergence(1, 1, False, do_plot=True) diff --git a/tutorial_07_data_structures.ipynb b/tutorial_07_data_structures.ipynb index dc21d7332..bde0b0c23 100644 --- a/tutorial_07_data_structures.ipynb +++ b/tutorial_07_data_structures.ipynb @@ -917,9 +917,9 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"{particles.Np = }\")\n", - "print(f\"{particles.markers.shape = }\")\n", - "print(f\"{particles.markers_wo_holes.shape = }\")" + "print(f'{particles.Np = }')\n", + "print(f'{particles.markers.shape = }')\n", + "print(f'{particles.markers_wo_holes.shape = }')" ] }, { @@ -935,13 +935,13 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"{particles.positions[:5] = }\\n\")\n", - "print(f\"{particles.velocities[:5] = }\\n\")\n", - "print(f\"{particles.phasespace_coords[:5] = }\\n\")\n", - "print(f\"{particles.weights[:5] = }\\n\")\n", - "print(f\"{particles.sampling_density[:5] = }\\n\")\n", - "print(f\"{particles.weights0[:5] = }\\n\")\n", - "print(f\"{particles.marker_ids[:5] = }\")" + "print(f'{particles.positions[:5] = }\\n')\n", + "print(f'{particles.velocities[:5] = }\\n')\n", + "print(f'{particles.phasespace_coords[:5] = }\\n')\n", + "print(f'{particles.weights[:5] = }\\n')\n", + "print(f'{particles.sampling_density[:5] = }\\n')\n", + "print(f'{particles.weights0[:5] = }\\n')\n", + "print(f'{particles.marker_ids[:5] = }')" ] } ], diff --git a/tutorials/tutorial_01_parameter_files.ipynb b/tutorials/tutorial_01_parameter_files.ipynb index a382368fa..7621ed6b1 100644 --- a/tutorials/tutorial_01_parameter_files.ipynb +++ b/tutorials/tutorial_01_parameter_files.ipynb @@ -49,23 +49,24 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy import main\n", - "from struphy.fields_background import equils\n", + "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", "from struphy.geometry import domains\n", + "from struphy.fields_background import equils\n", + "from struphy.topology import grids\n", + "from struphy.io.options import DerhamOptions\n", + "from struphy.io.options import FieldsBackground\n", "from struphy.initial import perturbations\n", - "from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Time\n", "from struphy.kinetic_background import maxwellians\n", + "from struphy.pic.utilities import (LoadingParameters, \n", + " WeightsParameters, \n", + " BoundaryParameters,\n", + " BinningPlot,\n", + " KernelDensityPlot,\n", + " )\n", + "from struphy import main\n", "\n", "# import model, set verbosity\n", - "from struphy.models.toy import Vlasov\n", - "from struphy.pic.utilities import (\n", - " BinningPlot,\n", - " BoundaryParameters,\n", - " KernelDensityPlot,\n", - " LoadingParameters,\n", - " WeightsParameters,\n", - ")\n", - "from struphy.topology import grids" + "from struphy.models.toy import Vlasov" ] }, { @@ -171,10 +172,10 @@ "source": [ "loading_params = LoadingParameters(Np=15)\n", "weights_params = WeightsParameters()\n", - "boundary_params = BoundaryParameters(bc=(\"reflect\", \"reflect\", \"periodic\"))\n", - "model.kinetic_ions.set_markers(\n", - " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", - ")\n", + "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", + "model.kinetic_ions.set_markers(loading_params=loading_params, \n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params)\n", "\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)" @@ -247,18 +248,17 @@ "source": [ "verbose = True\n", "\n", - "main.run(\n", - " model,\n", - " params_path=None,\n", - " env=env,\n", - " base_units=base_units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - ")" + "main.run(model, \n", + " params_path=None, \n", + " env=env, \n", + " base_units=base_units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" ] }, { @@ -279,7 +279,6 @@ "outputs": [], "source": [ "import os\n", - "\n", "path = os.path.join(os.getcwd(), \"sim_1\")\n", "\n", "main.pproc(path)" @@ -364,31 +363,31 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", "from matplotlib import pyplot as plt\n", + "import numpy as np\n", "\n", "fig = plt.figure()\n", "ax = fig.gca()\n", "\n", - "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", + "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", "\n", "# create alpha for color scaling\n", "Tend = time_opts.Tend\n", - "alpha = np.linspace(1.0, 0.0, Nt + 1)\n", + "alpha = np.linspace(1., 0., Nt + 1)\n", "\n", "# loop through particles, plot all time steps\n", "for i in range(Np):\n", " ax.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], alpha=alpha)\n", - "\n", - "ax.plot([l1, l1], [l2, r2], \"k\")\n", - "ax.plot([r1, r1], [l2, r2], \"k\")\n", - "ax.plot([l1, r1], [l2, l2], \"k\")\n", - "ax.plot([l1, r1], [r2, r2], \"k\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", + " \n", + "ax.plot([l1, l1], [l2, r2], 'k')\n", + "ax.plot([r1, r1], [l2, r2], 'k')\n", + "ax.plot([l1, r1], [l2, l2], 'k')\n", + "ax.plot([l1, r1], [r2, r2], 'k')\n", + "ax.set_xlabel('x')\n", + "ax.set_ylabel('y')\n", "ax.set_xlim(-6.5, 6.5)\n", "ax.set_ylim(-9, 9)\n", - "ax.set_title(f\"{int(Nt - 1)} time steps (full color at t=0)\");" + "ax.set_title(f'{int(Nt - 1)} time steps (full color at t=0)');" ] } ], diff --git a/tutorials/tutorial_02_test_particles.ipynb b/tutorials/tutorial_02_test_particles.ipynb index 7aac7f7e8..e95b6df81 100644 --- a/tutorials/tutorial_02_test_particles.ipynb +++ b/tutorials/tutorial_02_test_particles.ipynb @@ -37,23 +37,24 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy import main\n", - "from struphy.fields_background import equils\n", + "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", "from struphy.geometry import domains\n", + "from struphy.fields_background import equils\n", + "from struphy.topology import grids\n", + "from struphy.io.options import DerhamOptions\n", + "from struphy.io.options import FieldsBackground\n", "from struphy.initial import perturbations\n", - "from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Time\n", "from struphy.kinetic_background import maxwellians\n", + "from struphy.pic.utilities import (LoadingParameters, \n", + " WeightsParameters, \n", + " BoundaryParameters,\n", + " BinningPlot,\n", + " KernelDensityPlot,\n", + " )\n", + "from struphy import main\n", "\n", "# import model, set verbosity\n", - "from struphy.models.toy import Vlasov\n", - "from struphy.pic.utilities import (\n", - " BinningPlot,\n", - " BoundaryParameters,\n", - " KernelDensityPlot,\n", - " LoadingParameters,\n", - " WeightsParameters,\n", - ")\n", - "from struphy.topology import grids" + "from struphy.models.toy import Vlasov" ] }, { @@ -98,9 +99,9 @@ "time_opts = Time(dt=0.2, Tend=0.2)\n", "\n", "# geometry\n", - "a1 = 0.0\n", - "a2 = 5.0\n", - "Lz = 20.0\n", + "a1 = 0.\n", + "a2 = 5.\n", + "Lz = 20.\n", "domain = domains.HollowCylinder(a1=a1, a2=a2, Lz=Lz)" ] }, @@ -192,12 +193,12 @@ "weights_params = WeightsParameters()\n", "boundary_params = BoundaryParameters()\n", "\n", - "model.kinetic_ions.set_markers(\n", - " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", - ")\n", - "model_2.kinetic_ions.set_markers(\n", - " loading_params=loading_params_2, weights_params=weights_params, boundary_params=boundary_params\n", - ")\n", + "model.kinetic_ions.set_markers(loading_params=loading_params, \n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params)\n", + "model_2.kinetic_ions.set_markers(loading_params=loading_params_2, \n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params)\n", "\n", "model.kinetic_ions.set_sorting_boxes()\n", "model_2.kinetic_ions.set_sorting_boxes()\n", @@ -261,18 +262,17 @@ "source": [ "verbose = False\n", "\n", - "main.run(\n", - " model,\n", - " params_path=None,\n", - " env=env,\n", - " base_units=base_units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - ")" + "main.run(model, \n", + " params_path=None, \n", + " env=env, \n", + " base_units=base_units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" ] }, { @@ -290,18 +290,17 @@ "metadata": {}, "outputs": [], "source": [ - "main.run(\n", - " model_2,\n", - " params_path=None,\n", - " env=env_2,\n", - " base_units=base_units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - ")" + "main.run(model_2, \n", + " params_path=None, \n", + " env=env_2, \n", + " base_units=base_units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" ] }, { @@ -320,7 +319,6 @@ "outputs": [], "source": [ "import os\n", - "\n", "path = os.path.join(os.getcwd(), \"sim_1\")\n", "path_2 = os.path.join(os.getcwd(), \"sim_2\")\n", "\n", @@ -348,7 +346,7 @@ "source": [ "from matplotlib import pyplot as plt\n", "\n", - "fig = plt.figure(figsize=(10, 6))\n", + "fig = plt.figure(figsize=(10, 6)) \n", "\n", "orbits = simdata.orbits[\"kinetic_ions\"]\n", "orbits_uni = simdata_2.orbits[\"kinetic_ions\"]\n", @@ -357,24 +355,24 @@ "# orbits_uni = simdata_2.pic_species[\"kinetic_ions\"][\"orbits\"]\n", "\n", "plt.subplot(1, 2, 1)\n", - "plt.scatter(orbits[0, :, 0], orbits[0, :, 1], s=2.0)\n", - "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", + "plt.scatter(orbits[0, :, 0], orbits[0, :, 1], s=2.)\n", + "circle1 = plt.Circle((0, 0), a2, color='k', fill=False)\n", "ax = plt.gca()\n", "ax.add_patch(circle1)\n", - "ax.set_aspect(\"equal\")\n", - "plt.xlabel(\"x\")\n", - "plt.ylabel(\"y\")\n", - "plt.title(\"sim_1: draw uniform in logical space\")\n", + "ax.set_aspect('equal')\n", + "plt.xlabel('x')\n", + "plt.ylabel('y')\n", + "plt.title('sim_1: draw uniform in logical space')\n", "\n", "plt.subplot(1, 2, 2)\n", - "plt.scatter(orbits_uni[0, :, 0], orbits_uni[0, :, 1], s=2.0)\n", - "circle2 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", + "plt.scatter(orbits_uni[0, :, 0], orbits_uni[0, :, 1], s=2.)\n", + "circle2 = plt.Circle((0, 0), a2, color='k', fill=False)\n", "ax = plt.gca()\n", "ax.add_patch(circle2)\n", - "ax.set_aspect(\"equal\")\n", - "plt.xlabel(\"x\")\n", - "plt.ylabel(\"y\")\n", - "plt.title(\"sim_2: draw uniform on disc\");" + "ax.set_aspect('equal')\n", + "plt.xlabel('x')\n", + "plt.ylabel('y')\n", + "plt.title('sim_2: draw uniform on disc');" ] }, { @@ -397,7 +395,7 @@ "source": [ "time_opts = Time(dt=0.2, Tend=10.0)\n", "loading_params = LoadingParameters(Np=15, spatial=\"disc\")\n", - "boundary_params = BoundaryParameters(bc=(\"reflect\", \"periodic\", \"periodic\"))" + "boundary_params = BoundaryParameters(bc=('reflect', 'periodic', 'periodic'))" ] }, { @@ -413,9 +411,9 @@ "# species parameters\n", "model.kinetic_ions.set_phys_params()\n", "\n", - "model.kinetic_ions.set_markers(\n", - " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", - ")\n", + "model.kinetic_ions.set_markers(loading_params=loading_params, \n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params)\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)" ] @@ -463,18 +461,17 @@ "source": [ "verbose = False\n", "\n", - "main.run(\n", - " model,\n", - " params_path=None,\n", - " env=env,\n", - " base_units=base_units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - ")" + "main.run(model, \n", + " params_path=None, \n", + " env=env, \n", + " base_units=base_units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" ] }, { @@ -532,23 +529,23 @@ "fig = plt.figure()\n", "ax = fig.gca()\n", "\n", - "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", + "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", "\n", "# create alpha for color scaling\n", "Tend = time_opts.Tend\n", - "alpha = np.linspace(1.0, 0.0, Nt + 1)\n", + "alpha = np.linspace(1., 0., Nt + 1)\n", "\n", "# loop through particles, plot all time steps\n", "for i in range(Np):\n", " ax.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], alpha=alpha)\n", - "\n", - "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", + " \n", + "circle1 = plt.Circle((0, 0), a2, color='k', fill=False)\n", "\n", "ax.add_patch(circle1)\n", - "ax.set_aspect(\"equal\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_title(f\"{Nt - 1} time steps (full color at t=0)\");" + "ax.set_aspect('equal')\n", + "ax.set_xlabel('x')\n", + "ax.set_ylabel('y')\n", + "ax.set_title(f'{Nt - 1} time steps (full color at t=0)');" ] }, { @@ -581,9 +578,9 @@ "metadata": {}, "outputs": [], "source": [ - "B0x = 0.0\n", - "B0y = 0.0\n", - "B0z = 1.0\n", + "B0x = 0.\n", + "B0y = 0.\n", + "B0z = 1.\n", "equil = equils.HomogenSlab(B0x=B0x, B0y=B0y, B0z=B0z)" ] }, @@ -628,10 +625,10 @@ "model.kinetic_ions.set_phys_params()\n", "\n", "loading_params = LoadingParameters(Np=20)\n", - "boundary_params = BoundaryParameters(bc=(\"remove\", \"periodic\", \"periodic\"))\n", - "model.kinetic_ions.set_markers(\n", - " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", - ")\n", + "boundary_params = BoundaryParameters(bc=('remove', 'periodic', 'periodic'))\n", + "model.kinetic_ions.set_markers(loading_params=loading_params, \n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params)\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)\n", "\n", @@ -664,18 +661,17 @@ "# run\n", "verbose = False\n", "\n", - "main.run(\n", - " model,\n", - " params_path=None,\n", - " env=env,\n", - " base_units=base_units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - ")" + "main.run(model, \n", + " params_path=None, \n", + " env=env, \n", + " base_units=base_units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" ] }, { @@ -714,23 +710,23 @@ "fig = plt.figure()\n", "ax = fig.gca()\n", "\n", - "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", + "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", "\n", "# create alpha for color scaling\n", "Tend = time_opts.Tend\n", - "alpha = np.linspace(1.0, 0.0, Nt + 1)\n", + "alpha = np.linspace(1., 0., Nt + 1)\n", "\n", "# loop through particles, plot all time steps\n", "for i in range(Np):\n", " ax.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], alpha=alpha)\n", - "\n", - "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", + " \n", + "circle1 = plt.Circle((0, 0), a2, color='k', fill=False)\n", "\n", "ax.add_patch(circle1)\n", - "ax.set_aspect(\"equal\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_title(f\"{int(Nt - 1)} time steps (full color at t=0)\");" + "ax.set_aspect('equal')\n", + "ax.set_xlabel('x')\n", + "ax.set_ylabel('y')\n", + "ax.set_title(f'{int(Nt - 1)} time steps (full color at t=0)');" ] }, { @@ -750,9 +746,9 @@ "metadata": {}, "outputs": [], "source": [ - "n1 = 0.0\n", - "n2 = 0.0\n", - "na = 1.0\n", + "n1 = 0.\n", + "n2 = 0.\n", + "na = 1.\n", "equil = equils.EQDSKequilibrium(n1=n1, n2=n2, na=na)" ] }, @@ -774,8 +770,12 @@ "Nel = (28, 72)\n", "p = (3, 3)\n", "psi_power = 0.6\n", - "psi_shifts = (1e-6, 1.0)\n", - "domain = domains.Tokamak(equilibrium=equil, Nel=Nel, p=p, psi_power=psi_power, psi_shifts=psi_shifts)" + "psi_shifts = (1e-6, 1.)\n", + "domain = domains.Tokamak(equilibrium=equil, \n", + " Nel=Nel,\n", + " p=p,\n", + " psi_power=psi_power,\n", + " psi_shifts=psi_shifts)" ] }, { @@ -838,9 +838,9 @@ "import numpy as np\n", "\n", "# logical grid on the unit cube\n", - "e1 = np.linspace(0.0, 1.0, 101)\n", - "e2 = np.linspace(0.0, 1.0, 101)\n", - "e3 = np.linspace(0.0, 1.0, 101)\n", + "e1 = np.linspace(0., 1., 101)\n", + "e2 = np.linspace(0., 1., 101)\n", + "e3 = np.linspace(0., 1., 101)\n", "\n", "# move away from the singular point r = 0\n", "e1[0] += 1e-5" @@ -854,11 +854,11 @@ "outputs": [], "source": [ "# logical coordinates of the poloidal plane at phi = 0\n", - "eta_poloidal = (e1, e2, 0.0)\n", + "eta_poloidal = (e1, e2, 0.)\n", "# logical coordinates of the top view at theta = 0\n", - "eta_topview_1 = (e1, 0.0, e3)\n", + "eta_topview_1 = (e1, 0., e3)\n", "# logical coordinates of the top view at theta = pi\n", - "eta_topview_2 = (e1, 0.5, e3)" + "eta_topview_2 = (e1, .5, e3)" ] }, { @@ -873,9 +873,9 @@ "x_top1, y_top1, z_top1 = domain(*eta_topview_1, squeeze_out=True)\n", "x_top2, y_top2, z_top2 = domain(*eta_topview_2, squeeze_out=True)\n", "\n", - "print(f\"{x_pol.shape = }\")\n", - "print(f\"{x_top1.shape = }\")\n", - "print(f\"{x_top2.shape = }\")" + "print(f'{x_pol.shape = }')\n", + "print(f'{x_top1.shape = }')\n", + "print(f'{x_top2.shape = }')" ] }, { @@ -904,35 +904,36 @@ "ax_top.contourf(x_top2, y_top2, equil.absB0(*eta_topview_2, squeeze_out=True), levels=levels)\n", "\n", "# last closed flux surface, poloidal\n", - "ax.plot(x_pol[-1], z_pol[-1], color=\"k\")\n", + "ax.plot(x_pol[-1], z_pol[-1], color='k')\n", "\n", "# last closed flux surface, toroidal\n", - "ax_top.plot(x_top1[-1], y_top1[-1], color=\"k\")\n", - "ax_top.plot(x_top2[-1], y_top2[-1], color=\"k\")\n", + "ax_top.plot(x_top1[-1], y_top1[-1], color='k')\n", + "ax_top.plot(x_top2[-1], y_top2[-1], color='k')\n", "\n", "# limiter, poloidal\n", - "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, \"tab:orange\")\n", - "ax.axis(\"equal\")\n", - "ax.set_xlabel(\"R\")\n", - "ax.set_ylabel(\"Z\")\n", - "ax.set_title(\"abs(B) at $\\phi=0$\")\n", - "fig.colorbar(im)\n", + "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, 'tab:orange')\n", + "ax.axis('equal')\n", + "ax.set_xlabel('R')\n", + "ax.set_ylabel('Z')\n", + "ax.set_title('abs(B) at $\\phi=0$')\n", + "fig.colorbar(im);\n", + "\n", "# limiter, toroidal\n", "limiter_Rmax = np.max(equil.limiter_pts_R)\n", "limiter_Rmin = np.min(equil.limiter_pts_R)\n", "\n", - "thetas = 2 * np.pi * e2\n", + "thetas = 2*np.pi*e2\n", "limiter_x_max = limiter_Rmax * np.cos(thetas)\n", - "limiter_y_max = -limiter_Rmax * np.sin(thetas)\n", + "limiter_y_max = - limiter_Rmax * np.sin(thetas)\n", "limiter_x_min = limiter_Rmin * np.cos(thetas)\n", - "limiter_y_min = -limiter_Rmin * np.sin(thetas)\n", - "\n", - "ax_top.plot(limiter_x_max, limiter_y_max, \"tab:orange\")\n", - "ax_top.plot(limiter_x_min, limiter_y_min, \"tab:orange\")\n", - "ax_top.axis(\"equal\")\n", - "ax_top.set_xlabel(\"x\")\n", - "ax_top.set_ylabel(\"y\")\n", - "ax_top.set_title(\"abs(B) at $Z=0$\")\n", + "limiter_y_min = - limiter_Rmin * np.sin(thetas)\n", + "\n", + "ax_top.plot(limiter_x_max, limiter_y_max, 'tab:orange')\n", + "ax_top.plot(limiter_x_min, limiter_y_min, 'tab:orange')\n", + "ax_top.axis('equal')\n", + "ax_top.set_xlabel('x')\n", + "ax_top.set_ylabel('y')\n", + "ax_top.set_title('abs(B) at $Z=0$')\n", "fig.colorbar(im_top);" ] }, @@ -957,18 +958,17 @@ "# species parameters\n", "model.kinetic_ions.set_phys_params()\n", "\n", - "initial = (\n", - " (0.501, 0.001, 0.001, 0.0, 0.0450, -0.04), # co-passing particle\n", - " (0.511, 0.001, 0.001, 0.0, -0.0450, -0.04), # counter passing particle\n", - " (0.521, 0.001, 0.001, 0.0, 0.0105, -0.04), # co-trapped particle\n", - " (0.531, 0.001, 0.001, 0.0, -0.0155, -0.04),\n", - ")\n", + "initial = ((.501, 0.001, 0.001, 0., 0.0450, -0.04), # co-passing particle\n", + " (.511, 0.001, 0.001, 0., -0.0450, -0.04), # counter passing particle\n", + " (.521, 0.001, 0.001, 0., 0.0105, -0.04), # co-trapped particle\n", + " (.531, 0.001, 0.001, 0., -0.0155, -0.04))\n", "\n", "loading_params = LoadingParameters(Np=4, seed=1608, specific_markers=initial)\n", - "boundary_params = BoundaryParameters(bc=(\"remove\", \"periodic\", \"periodic\"))\n", - "model.kinetic_ions.set_markers(\n", - " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params, bufsize=2.0\n", - ")\n", + "boundary_params = BoundaryParameters(bc=('remove', 'periodic', 'periodic'))\n", + "model.kinetic_ions.set_markers(loading_params=loading_params, \n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params,\n", + " bufsize=2.)\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)\n", "\n", @@ -1025,18 +1025,17 @@ "\n", "verbose = False\n", "\n", - "main.run(\n", - " model,\n", - " params_path=None,\n", - " env=env,\n", - " base_units=base_units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - ")" + "main.run(model, \n", + " params_path=None, \n", + " env=env, \n", + " base_units=base_units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" ] }, { @@ -1047,7 +1046,6 @@ "outputs": [], "source": [ "import os\n", - "\n", "from struphy import main\n", "\n", "path = os.path.join(os.getcwd(), \"sim_1\")\n", @@ -1078,20 +1076,21 @@ "source": [ "import math\n", "\n", - "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", + "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", "\n", "dt = time_opts.dt\n", "Tend = time_opts.Tend\n", "\n", "for i in range(Np):\n", - " r = np.sqrt(orbits[:, i, 0] ** 2 + orbits[:, i, 1] ** 2)\n", - " # poloidal\n", + " r = np.sqrt(orbits[:, i, 0]**2 + orbits[:, i, 1]**2)\n", + " # poloidal \n", " ax.scatter(r, orbits[:, i, 2], c=colors[i % 4], s=1)\n", " # top view\n", " ax_top.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], s=1)\n", + " \n", + "ax.set_title(f'{math.ceil(Tend/dt)} time steps')\n", + "ax_top.set_title(f'{math.ceil(Tend/dt)} time steps');\n", "\n", - "ax.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", - "ax_top.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", "fig" ] }, @@ -1120,18 +1119,17 @@ "# species parameters\n", "model.kinetic_ions.set_phys_params()\n", "\n", - "initial = (\n", - " (0.501, 0.001, 0.001, -1.935, 1.72), # co-passing particle\n", - " (0.501, 0.001, 0.001, 1.935, 1.72), # couner-passing particle\n", - " (0.501, 0.001, 0.001, -0.6665, 1.72), # co-trapped particle\n", - " (0.501, 0.001, 0.001, 0.4515, 1.72),\n", - ") # counter-trapped particl\n", + "initial = ((.501, 0.001, 0.001, -1.935 , 1.72), # co-passing particle\n", + " (.501, 0.001, 0.001, 1.935 , 1.72), # couner-passing particle\n", + " (.501, 0.001, 0.001, -0.6665, 1.72), # co-trapped particle\n", + " (.501, 0.001, 0.001, 0.4515, 1.72)) # counter-trapped particl\n", "\n", "loading_params = LoadingParameters(Np=4, seed=1608, specific_markers=initial)\n", - "boundary_params = BoundaryParameters(bc=(\"remove\", \"periodic\", \"periodic\"))\n", - "model.kinetic_ions.set_markers(\n", - " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params, bufsize=2.0\n", - ")\n", + "boundary_params = BoundaryParameters(bc=('remove', 'periodic', 'periodic'))\n", + "model.kinetic_ions.set_markers(loading_params=loading_params, \n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params,\n", + " bufsize=2.)\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)\n", "\n", @@ -1172,35 +1170,36 @@ "ax_top.contourf(x_top2, y_top2, equil.absB0(*eta_topview_2, squeeze_out=True), levels=levels)\n", "\n", "# last closed flux surface, poloidal\n", - "ax.plot(x_pol[-1], z_pol[-1], color=\"k\")\n", + "ax.plot(x_pol[-1], z_pol[-1], color='k')\n", "\n", "# last closed flux surface, toroidal\n", - "ax_top.plot(x_top1[-1], y_top1[-1], color=\"k\")\n", - "ax_top.plot(x_top2[-1], y_top2[-1], color=\"k\")\n", + "ax_top.plot(x_top1[-1], y_top1[-1], color='k')\n", + "ax_top.plot(x_top2[-1], y_top2[-1], color='k')\n", "\n", "# limiter, poloidal\n", - "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, \"tab:orange\")\n", - "ax.axis(\"equal\")\n", - "ax.set_xlabel(\"R\")\n", - "ax.set_ylabel(\"Z\")\n", - "ax.set_title(\"abs(B) at $\\phi=0$\")\n", - "fig.colorbar(im)\n", + "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, 'tab:orange')\n", + "ax.axis('equal')\n", + "ax.set_xlabel('R')\n", + "ax.set_ylabel('Z')\n", + "ax.set_title('abs(B) at $\\phi=0$')\n", + "fig.colorbar(im);\n", + "\n", "# limiter, toroidal\n", "limiter_Rmax = np.max(equil.limiter_pts_R)\n", "limiter_Rmin = np.min(equil.limiter_pts_R)\n", "\n", - "thetas = 2 * np.pi * e2\n", + "thetas = 2*np.pi*e2\n", "limiter_x_max = limiter_Rmax * np.cos(thetas)\n", - "limiter_y_max = -limiter_Rmax * np.sin(thetas)\n", + "limiter_y_max = - limiter_Rmax * np.sin(thetas)\n", "limiter_x_min = limiter_Rmin * np.cos(thetas)\n", - "limiter_y_min = -limiter_Rmin * np.sin(thetas)\n", - "\n", - "ax_top.plot(limiter_x_max, limiter_y_max, \"tab:orange\")\n", - "ax_top.plot(limiter_x_min, limiter_y_min, \"tab:orange\")\n", - "ax_top.axis(\"equal\")\n", - "ax_top.set_xlabel(\"x\")\n", - "ax_top.set_ylabel(\"y\")\n", - "ax_top.set_title(\"abs(B) at $Z=0$\")\n", + "limiter_y_min = - limiter_Rmin * np.sin(thetas)\n", + "\n", + "ax_top.plot(limiter_x_max, limiter_y_max, 'tab:orange')\n", + "ax_top.plot(limiter_x_min, limiter_y_min, 'tab:orange')\n", + "ax_top.axis('equal')\n", + "ax_top.set_xlabel('x')\n", + "ax_top.set_ylabel('y')\n", + "ax_top.set_title('abs(B) at $Z=0$')\n", "fig.colorbar(im_top);" ] }, @@ -1215,18 +1214,17 @@ "\n", "verbose = False\n", "\n", - "main.run(\n", - " model,\n", - " params_path=None,\n", - " env=env,\n", - " base_units=base_units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - ")" + "main.run(model, \n", + " params_path=None, \n", + " env=env, \n", + " base_units=base_units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" ] }, { @@ -1237,7 +1235,6 @@ "outputs": [], "source": [ "import os\n", - "\n", "from struphy import main\n", "\n", "path = os.path.join(os.getcwd(), \"sim_1\")\n", @@ -1268,20 +1265,21 @@ "source": [ "import math\n", "\n", - "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", + "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", "\n", "dt = time_opts.dt\n", "Tend = time_opts.Tend\n", "\n", "for i in range(Np):\n", - " r = np.sqrt(orbits[:, i, 0] ** 2 + orbits[:, i, 1] ** 2)\n", - " # poloidal\n", + " r = np.sqrt(orbits[:, i, 0]**2 + orbits[:, i, 1]**2)\n", + " # poloidal \n", " ax.scatter(r, orbits[:, i, 2], c=colors[i % 4], s=1)\n", " # top view\n", " ax_top.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], s=1)\n", + " \n", + "ax.set_title(f'{math.ceil(Tend/dt)} time steps')\n", + "ax_top.set_title(f'{math.ceil(Tend/dt)} time steps');\n", "\n", - "ax.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", - "ax_top.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", "fig" ] } diff --git a/tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb b/tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb index c56d1fe27..f50bfd8a7 100644 --- a/tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb +++ b/tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb @@ -50,23 +50,24 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy import main\n", - "from struphy.fields_background import equils\n", + "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", "from struphy.geometry import domains\n", + "from struphy.fields_background import equils\n", + "from struphy.topology import grids\n", + "from struphy.io.options import DerhamOptions\n", + "from struphy.io.options import FieldsBackground\n", "from struphy.initial import perturbations\n", - "from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Time\n", "from struphy.kinetic_background import maxwellians\n", + "from struphy.pic.utilities import (LoadingParameters,\n", + " WeightsParameters,\n", + " BoundaryParameters,\n", + " BinningPlot,\n", + " KernelDensityPlot,\n", + " )\n", + "from struphy import main\n", "\n", "# import model, set verbosity\n", - "from struphy.models.toy import PressureLessSPH\n", - "from struphy.pic.utilities import (\n", - " BinningPlot,\n", - " BoundaryParameters,\n", - " KernelDensityPlot,\n", - " LoadingParameters,\n", - " WeightsParameters,\n", - ")\n", - "from struphy.topology import grids" + "from struphy.models.toy import PressureLessSPH" ] }, { @@ -94,12 +95,12 @@ "time_opts = Time(dt=0.02, Tend=4, split_algo=\"Strang\")\n", "\n", "# geometry\n", - "l1 = -0.5\n", - "r1 = 0.5\n", - "l2 = -0.5\n", - "r2 = 0.5\n", - "l3 = 0.0\n", - "r3 = 1.0\n", + "l1 = -.5\n", + "r1 = .5\n", + "l2 = -.5\n", + "r2 = .5\n", + "l3 = 0.\n", + "r3 = 1.\n", "domain = domains.Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" ] }, @@ -121,20 +122,17 @@ "# construct Beltrami flow\n", "import numpy as np\n", "\n", - "\n", "def u_fun(x, y, z):\n", - " ux = -np.cos(np.pi * x) * np.sin(np.pi * y)\n", - " uy = np.sin(np.pi * x) * np.cos(np.pi * y)\n", - " uz = 0 * x\n", + " ux = -np.cos(np.pi*x)*np.sin(np.pi*y)\n", + " uy = np.sin(np.pi*x)*np.cos(np.pi*y)\n", + " uz = 0 * x \n", " return ux, uy, uz\n", "\n", - "\n", - "p_fun = lambda x, y, z: 0.5 * (np.sin(np.pi * x) ** 2 + np.sin(np.pi * y) ** 2)\n", - "n_fun = lambda x, y, z: 1.0 + 0 * x\n", + "p_fun = lambda x, y, z: 0.5*(np.sin(np.pi*x)**2 + np.sin(np.pi*y)**2)\n", + "n_fun = lambda x, y, z: 1. + 0*x\n", "\n", "# put the functions in a generic equilibirum container\n", "from struphy.fields_background.generic import GenericCartesianFluidEquilibrium\n", - "\n", "bel_flow = GenericCartesianFluidEquilibrium(u_xyz=u_fun, p_xyz=p_fun, n_xyz=n_fun)" ] }, @@ -186,12 +184,11 @@ "\n", "loading_params = LoadingParameters(Np=1000)\n", "weights_params = WeightsParameters()\n", - "boundary_params = BoundaryParameters(bc=(\"reflect\", \"reflect\", \"periodic\"))\n", - "model.cold_fluid.set_markers(\n", - " loading_params=loading_params,\n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params,\n", - ")\n", + "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", + "model.cold_fluid.set_markers(loading_params=loading_params,\n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params,\n", + " )\n", "model.cold_fluid.set_sorting_boxes(boxes_per_dim=(1, 1, 1))\n", "model.cold_fluid.set_save_data(n_markers=1.0)" ] @@ -213,7 +210,6 @@ "source": [ "# propagator options\n", "from struphy.ode.utils import ButcherTableau\n", - "\n", "butcher = ButcherTableau(algo=\"forward_euler\")\n", "model.propagators.push_eta.options = model.propagators.push_eta.Options(butcher=butcher)\n", "\n", @@ -257,18 +253,17 @@ "source": [ "verbose = False\n", "\n", - "main.run(\n", - " model,\n", - " params_path=None,\n", - " env=env,\n", - " base_units=base_units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - ")" + "main.run(model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + " )" ] }, { @@ -279,7 +274,6 @@ "outputs": [], "source": [ "import os\n", - "\n", "path = os.path.join(os.getcwd(), \"sim_1\")\n", "\n", "main.pproc(path)" @@ -303,32 +297,32 @@ "outputs": [], "source": [ "from matplotlib import pyplot as plt\n", - "\n", "plt.figure(figsize=(12, 28))\n", "\n", "orbits = simdata.orbits[\"cold_fluid\"]\n", "\n", - "coloring = np.select(\n", - " [orbits[0, :, 0] <= -0.2, np.abs(orbits[0, :, 0]) < +0.2, orbits[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0]\n", - ")\n", + "coloring = np.select([orbits[0, :, 0]<=-0.2, \n", + " np.abs(orbits[0, :, 0]) < +0.2, \n", + " orbits[0, :, 0] >= 0.2],\n", + " [-1.0, 0.0, +1.0])\n", "\n", "dt = time_opts.dt\n", "Nt = simdata.t_grid.size - 1\n", - "interval = Nt / 20\n", + "interval = Nt/20\n", "plot_ct = 0\n", "for i in range(Nt):\n", " if i % interval == 0:\n", - " print(f\"{i = }\")\n", + " print(f'{i = }')\n", " plot_ct += 1\n", " plt.subplot(5, 2, plot_ct)\n", - " ax = plt.gca()\n", + " ax = plt.gca() \n", " plt.scatter(orbits[i, :, 0], orbits[i, :, 1], c=coloring)\n", - " plt.axis(\"square\")\n", - " plt.title(\"n0_scatter\")\n", + " plt.axis('square')\n", + " plt.title('n0_scatter')\n", " plt.xlim(l1, r1)\n", " plt.ylim(l2, r2)\n", " plt.colorbar()\n", - " plt.title(f\"Gas at t={i * dt}\")\n", + " plt.title(f'Gas at t={i*dt}')\n", " if plot_ct == 10:\n", " break" ] @@ -375,10 +369,12 @@ "\n", "loading_params = LoadingParameters(ppb=4, loading=\"tesselation\")\n", "weights_params = WeightsParameters()\n", - "boundary_params = BoundaryParameters(bc=(\"reflect\", \"reflect\", \"periodic\"))\n", - "model.cold_fluid.set_markers(\n", - " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params, bufsize=0.5\n", - ")\n", + "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", + "model.cold_fluid.set_markers(loading_params=loading_params,\n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params,\n", + " bufsize=0.5\n", + " )\n", "model.cold_fluid.set_sorting_boxes(boxes_per_dim=(16, 16, 1))\n", "model.cold_fluid.set_save_data(n_markers=1.0)" ] @@ -392,7 +388,6 @@ "source": [ "# propagator options\n", "from struphy.ode.utils import ButcherTableau\n", - "\n", "butcher = ButcherTableau(algo=\"forward_euler\")\n", "model.propagators.push_eta.options = model.propagators.push_eta.Options(butcher=butcher)\n", "\n", @@ -418,18 +413,17 @@ "metadata": {}, "outputs": [], "source": [ - "main.run(\n", - " model,\n", - " params_path=None,\n", - " env=env,\n", - " base_units=base_units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - ")" + "main.run(model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + " )" ] }, { @@ -440,7 +434,6 @@ "outputs": [], "source": [ "import os\n", - "\n", "path = os.path.join(os.getcwd(), \"sim_2\")\n", "\n", "main.pproc(path)" @@ -464,32 +457,32 @@ "outputs": [], "source": [ "from matplotlib import pyplot as plt\n", - "\n", "plt.figure(figsize=(12, 28))\n", "\n", "orbits = simdata.orbits[\"cold_fluid\"]\n", "\n", - "coloring = np.select(\n", - " [orbits[0, :, 0] <= -0.2, np.abs(orbits[0, :, 0]) < +0.2, orbits[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0]\n", - ")\n", + "coloring = np.select([orbits[0, :, 0]<=-0.2, \n", + " np.abs(orbits[0, :, 0]) < +0.2, \n", + " orbits[0, :, 0] >= 0.2],\n", + " [-1.0, 0.0, +1.0])\n", "\n", "dt = time_opts.dt\n", "Nt = simdata.t_grid.size - 1\n", - "interval = Nt / 20\n", + "interval = Nt/20\n", "plot_ct = 0\n", "for i in range(Nt):\n", " if i % interval == 0:\n", - " print(f\"{i = }\")\n", + " print(f'{i = }')\n", " plot_ct += 1\n", " plt.subplot(5, 2, plot_ct)\n", - " ax = plt.gca()\n", + " ax = plt.gca() \n", " plt.scatter(orbits[i, :, 0], orbits[i, :, 1], c=coloring)\n", - " plt.axis(\"square\")\n", - " plt.title(\"n0_scatter\")\n", + " plt.axis('square')\n", + " plt.title('n0_scatter')\n", " plt.xlim(l1, r1)\n", " plt.ylim(l2, r2)\n", " plt.colorbar()\n", - " plt.title(f\"Gas at t={i * dt}\")\n", + " plt.title(f'Gas at t={i*dt}')\n", " if plot_ct == 10:\n", " break" ] @@ -548,7 +541,7 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.pic.sph_smoothing_kernels import gaussian_uni, linear_uni, trigonometric_uni\n", + "from struphy.pic.sph_smoothing_kernels import linear_uni, trigonometric_uni, gaussian_uni\n", "\n", "x = np.linspace(-1, 1, 200)\n", "out1 = np.zeros_like(x)\n", @@ -556,13 +549,13 @@ "out3 = np.zeros_like(x)\n", "\n", "for i, xi in enumerate(x):\n", - " out1[i] = trigonometric_uni(xi, 1.0)\n", - " out2[i] = gaussian_uni(xi, 1.0)\n", - " out3[i] = linear_uni(xi, 1.0)\n", + " out1[i] = trigonometric_uni(xi, 1.)\n", + " out2[i] = gaussian_uni(xi, 1.)\n", + " out3[i] = linear_uni(xi, 1.)\n", "plt.plot(x, out1, label=\"trigonometric\")\n", "plt.plot(x, out2, label=\"gaussian\")\n", - "plt.plot(x, out3, label=\"linear\")\n", - "plt.title(\"Some smoothing kernels\")\n", + "plt.plot(x, out3, label = \"linear\")\n", + "plt.title('Some smoothing kernels')\n", "plt.legend()" ] }, @@ -581,23 +574,24 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy import main\n", - "from struphy.fields_background import equils\n", + "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", "from struphy.geometry import domains\n", + "from struphy.fields_background import equils\n", + "from struphy.topology import grids\n", + "from struphy.io.options import DerhamOptions\n", + "from struphy.io.options import FieldsBackground\n", "from struphy.initial import perturbations\n", - "from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Time\n", "from struphy.kinetic_background import maxwellians\n", + "from struphy.pic.utilities import (LoadingParameters,\n", + " WeightsParameters,\n", + " BoundaryParameters,\n", + " BinningPlot,\n", + " KernelDensityPlot,\n", + " )\n", + "from struphy import main\n", "\n", "# import model, set verbosity\n", - "from struphy.models.fluid import EulerSPH\n", - "from struphy.pic.utilities import (\n", - " BinningPlot,\n", - " BoundaryParameters,\n", - " KernelDensityPlot,\n", - " LoadingParameters,\n", - " WeightsParameters,\n", - ")\n", - "from struphy.topology import grids" + "from struphy.models.fluid import EulerSPH" ] }, { @@ -629,8 +623,8 @@ "r1 = 3.0\n", "l2 = -3.0\n", "r2 = 3.0\n", - "l3 = 0.0\n", - "r3 = 1.0\n", + "l3 = 0.\n", + "r3 = 1.\n", "domain = domains.Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" ] }, @@ -650,13 +644,11 @@ "outputs": [], "source": [ "# gaussian initial blob\n", - "import numpy as np\n", - "\n", "from struphy.fields_background.generic import GenericCartesianFluidEquilibrium\n", - "\n", + "import numpy as np\n", "T_h = 0.2\n", - "gamma = 5 / 3\n", - "n_fun = lambda x, y, z: np.exp(-(x**2 + y**2) / T_h) / 35\n", + "gamma = 5/3\n", + "n_fun = lambda x, y, z: np.exp(-(x**2 + y**2)/T_h) / 35\n", "\n", "blob = GenericCartesianFluidEquilibrium(n_xyz=n_fun)" ] @@ -710,11 +702,10 @@ "loading_params = LoadingParameters(ppb=400)\n", "weights_params = WeightsParameters(reject_weights=True, threshold=3e-3)\n", "boundary_params = BoundaryParameters()\n", - "model.euler_fluid.set_markers(\n", - " loading_params=loading_params,\n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params,\n", - ")\n", + "model.euler_fluid.set_markers(loading_params=loading_params,\n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params,\n", + " )\n", "nx = 16\n", "ny = 16\n", "model.euler_fluid.set_sorting_boxes(boxes_per_dim=(nx, ny, 1))" @@ -735,20 +726,18 @@ "metadata": {}, "outputs": [], "source": [ - "bin_plot = BinningPlot(\n", - " slice=\"e1_e2\",\n", - " n_bins=(64, 64),\n", - " ranges=((0.0, 1.0), (0.0, 1.0)),\n", - " divide_by_jac=False,\n", - ")\n", + "bin_plot = BinningPlot(slice=\"e1_e2\", \n", + " n_bins=(64, 64), \n", + " ranges=((0.0, 1.0), (0.0, 1.0)), \n", + " divide_by_jac=False,\n", + " )\n", "pts_e1 = 100\n", "pts_e2 = 90\n", "kd_plot = KernelDensityPlot(pts_e1=pts_e1, pts_e2=pts_e2, pts_e3=1)\n", - "model.euler_fluid.set_save_data(\n", - " n_markers=1.0,\n", - " binning_plots=(bin_plot,),\n", - " kernel_density_plots=(kd_plot,),\n", - ")" + "model.euler_fluid.set_save_data(n_markers=1.0,\n", + " binning_plots=(bin_plot,),\n", + " kernel_density_plots=(kd_plot,),\n", + " )" ] }, { @@ -768,7 +757,6 @@ "source": [ "# propagator options\n", "from struphy.ode.utils import ButcherTableau\n", - "\n", "butcher = ButcherTableau(algo=\"forward_euler\")\n", "model.propagators.push_eta.options = model.propagators.push_eta.Options(butcher=butcher)\n", "\n", @@ -803,18 +791,17 @@ "source": [ "verbose = True\n", "\n", - "main.run(\n", - " model,\n", - " params_path=None,\n", - " env=env,\n", - " base_units=base_units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - ")" + "main.run(model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + " )" ] }, { @@ -863,7 +850,7 @@ "x = np.linspace(l1, r1, pts_e1)\n", "y = np.linspace(l2, r2, pts_e2)\n", "xx, yy = np.meshgrid(x, y, indexing=\"ij\")\n", - "ee1, ee2, ee3 = simdata.n_sph[\"euler_fluid\"][\"view_0\"][\"grid_n_sph\"]\n", + "ee1, ee2, ee3 = simdata.n_sph[\"euler_fluid\"][\"view_0\"][\"grid_n_sph\"]\n", "eta1 = ee1[:, 0, 0]\n", "eta2 = ee2[0, :, 0]\n", "bc_x = simdata.f[\"euler_fluid\"][\"e1_e2\"][\"grid_e1\"]\n", @@ -886,21 +873,20 @@ "metadata": {}, "outputs": [], "source": [ - "import matplotlib.pyplot as plt\n", - "\n", + "import matplotlib.pyplot as plt \n", "plt.figure(figsize=(12, 15))\n", "\n", "# plots\n", "plt.subplot(3, 2, 1)\n", "plt.pcolor(xx, yy, n_fun(xx, yy, 0))\n", - "plt.axis(\"square\")\n", - "plt.title(\"n_xyz initial\")\n", + "plt.axis('square')\n", + "plt.title('n_xyz initial')\n", "plt.colorbar()\n", "\n", "plt.subplot(3, 2, 2)\n", "plt.pcolor(eta1, eta2, n3(eta1, eta2, 0, squeeze_out=True).T)\n", - "plt.axis(\"square\")\n", - "plt.title(\"$\\hat{n}^{\\t{vol}}$ initial (volume form)\")\n", + "plt.axis('square')\n", + "plt.title('$\\hat{n}^{\\t{vol}}$ initial (volume form)')\n", "plt.colorbar()\n", "\n", "make_scatter = True\n", @@ -909,12 +895,12 @@ " ax = plt.gca()\n", " ax.set_xticks(np.linspace(l1, r1, nx + 1))\n", " ax.set_yticks(np.linspace(l2, r2, ny + 1))\n", - " plt.tick_params(labelbottom=False)\n", + " plt.tick_params(labelbottom = False) \n", " coloring = weights\n", - " plt.scatter(positions[:, 0], positions[:, 1], c=coloring, s=0.25)\n", - " plt.grid(c=\"k\")\n", - " plt.axis(\"square\")\n", - " plt.title(\"$\\hat{n}^{\\t{vol}}$ initial scatter (random)\")\n", + " plt.scatter(positions[:, 0], positions[:, 1], c=coloring, s=.25)\n", + " plt.grid(c='k')\n", + " plt.axis('square')\n", + " plt.title('$\\hat{n}^{\\t{vol}}$ initial scatter (random)')\n", " plt.xlim(l1, r1)\n", " plt.ylim(l2, r2)\n", " plt.colorbar()\n", @@ -922,19 +908,19 @@ "plt.subplot(3, 2, 4)\n", "ax = plt.gca()\n", "ax.set_xticks(np.linspace(0, 1, nx + 1))\n", - "ax.set_yticks(np.linspace(0, 1.0, ny + 1))\n", - "plt.tick_params(labelbottom=False)\n", - "plt.pcolor(ee1[:, :, 0], ee2[:, :, 0], n_sph[:, :, 0])\n", + "ax.set_yticks(np.linspace(0, 1., ny + 1))\n", + "plt.tick_params(labelbottom = False) \n", + "plt.pcolor(ee1[:,:,0], ee2[:,:,0], n_sph[:,:,0])\n", "plt.grid()\n", - "plt.axis(\"square\")\n", - "plt.title(\"n_sph initial (random)\")\n", + "plt.axis('square')\n", + "plt.title('n_sph initial (random)')\n", "plt.colorbar()\n", "\n", "plt.subplot(3, 2, 5)\n", "ax = plt.gca()\n", "plt.pcolor(bc_x, bc_y, f_bin)\n", - "plt.axis(\"square\")\n", - "plt.title(\"n_binned initial (random)\")\n", + "plt.axis('square')\n", + "plt.title('n_binned initial (random)')\n", "plt.colorbar()" ] }, @@ -950,24 +936,24 @@ "\n", "positions = orbits[:, :, :3]\n", "\n", - "interval = Nt / 10\n", + "interval = Nt/10\n", "plot_ct = 0\n", "\n", "plt.figure(figsize=(12, 24))\n", "for i in range(Nt):\n", " if i % interval == 0:\n", - " print(f\"{i = }\")\n", + " print(f'{i = }')\n", " plot_ct += 1\n", " plt.subplot(4, 2, plot_ct)\n", - " ax = plt.gca()\n", + " ax = plt.gca() \n", " coloring = weights\n", - " plt.scatter(positions[i, :, 0], positions[i, :, 1], c=coloring, s=0.25)\n", - " plt.axis(\"square\")\n", - " plt.title(\"n0_scatter\")\n", + " plt.scatter(positions[i, :, 0], positions[i, :, 1], c=coloring, s=.25)\n", + " plt.axis('square')\n", + " plt.title('n0_scatter')\n", " plt.xlim(l1, r1)\n", " plt.ylim(l2, r2)\n", " plt.colorbar()\n", - " plt.title(f\"Gas at t={i * dt}\")\n", + " plt.title(f'Gas at t={i*dt}')\n", " if plot_ct == 8:\n", " break" ] diff --git a/tutorials/tutorial_04_vlasov_maxwell.ipynb b/tutorials/tutorial_04_vlasov_maxwell.ipynb index 7fa3795b0..f35f0443f 100644 --- a/tutorials/tutorial_04_vlasov_maxwell.ipynb +++ b/tutorials/tutorial_04_vlasov_maxwell.ipynb @@ -32,15 +32,18 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy import main\n", - "from struphy.fields_background import equils\n", + "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", "from struphy.geometry import domains\n", + "from struphy.fields_background import equils\n", + "from struphy.topology import grids\n", + "from struphy.io.options import DerhamOptions\n", + "from struphy.io.options import FieldsBackground\n", "from struphy.initial import perturbations\n", - "from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Time\n", "from struphy.kinetic_background import maxwellians\n", - "from struphy.models.kinetic import VlasovAmpereOneSpecies\n", - "from struphy.pic.utilities import BinningPlot, BoundaryParameters, LoadingParameters, WeightsParameters\n", - "from struphy.topology import grids" + "from struphy.pic.utilities import LoadingParameters, WeightsParameters, BoundaryParameters, BinningPlot\n", + "from struphy import main\n", + "\n", + "from struphy.models.kinetic import VlasovAmpereOneSpecies" ] }, { @@ -63,7 +66,7 @@ "base_units = BaseUnits()\n", "\n", "# time stepping\n", - "time_opts = Time(dt=0.05, Tend=0.5) # , Tend = 3.5\n", + "time_opts = Time(dt = 0.05, Tend = 0.5)#, Tend = 3.5\n", "\n", "# geometry\n", "r1 = 12.56\n", @@ -113,9 +116,7 @@ "loading_params = LoadingParameters(ppc=10000)\n", "weights_params = WeightsParameters(control_variate=True)\n", "boundary_params = BoundaryParameters()\n", - "model.kinetic_ions.set_markers(\n", - " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", - ")\n", + "model.kinetic_ions.set_markers(loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params)\n", "model.kinetic_ions.set_sorting_boxes()" ] }, @@ -214,18 +215,17 @@ "source": [ "verbose = True\n", "\n", - "main.run(\n", - " model,\n", - " params_path=None,\n", - " env=env,\n", - " base_units=base_units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - ")" + "main.run(model, \n", + " params_path=None, \n", + " env=env, \n", + " base_units=base_units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" ] }, { @@ -262,8 +262,8 @@ "f_v1_init = simdata.f[\"kinetic_ions\"][\"v1\"][\"f_binned\"][0]\n", "\n", "plt.plot(v1_bins, f_v1_init)\n", - "plt.xlabel(\"vx\")\n", - "plt.title(\"Initial Maxwellian\");" + "plt.xlabel('vx')\n", + "plt.title('Initial Maxwellian');" ] }, { @@ -278,8 +278,8 @@ "df_e1_init = simdata.f[\"kinetic_ions\"][\"e1\"][\"delta_f_binned\"][0]\n", "\n", "plt.plot(e1_bins, df_e1_init)\n", - "plt.xlabel(\"$\\eta_1$\")\n", - "plt.title(\"Initial spatial perturbation\");" + "plt.xlabel('$\\eta_1$')\n", + "plt.title('Initial spatial perturbation');" ] }, { @@ -301,30 +301,30 @@ "\n", "plt.subplot(2, 2, 1)\n", "plt.pcolor(e1_bins, v1_bins, f_init.T)\n", - "plt.xlabel(\"$\\eta_1$\")\n", - "plt.ylabel(\"$v_x$\")\n", - "plt.title(\"Initial Maxwellian\")\n", + "plt.xlabel('$\\eta_1$')\n", + "plt.ylabel('$v_x$')\n", + "plt.title('Initial Maxwellian')\n", "plt.colorbar()\n", "\n", "plt.subplot(2, 2, 2)\n", "plt.pcolor(e1_bins, v1_bins, df_init.T)\n", - "plt.xlabel(\"$\\eta_1$\")\n", - "plt.ylabel(\"$v_x$\")\n", - "plt.title(\"Initial perturbation\")\n", + "plt.xlabel('$\\eta_1$')\n", + "plt.ylabel('$v_x$')\n", + "plt.title('Initial perturbation')\n", "plt.colorbar()\n", "\n", "plt.subplot(2, 2, 3)\n", "plt.pcolor(e1_bins, v1_bins, f_end.T)\n", - "plt.xlabel(\"$\\eta_1$\")\n", - "plt.ylabel(\"$v_x$\")\n", - "plt.title(\"Final Maxwellian\")\n", + "plt.xlabel('$\\eta_1$')\n", + "plt.ylabel('$v_x$')\n", + "plt.title('Final Maxwellian')\n", "plt.colorbar()\n", "\n", "plt.subplot(2, 2, 4)\n", "plt.pcolor(e1_bins, v1_bins, df_end.T)\n", - "plt.xlabel(\"$\\eta_1$\")\n", - "plt.ylabel(\"$v_x$\")\n", - "plt.title(\"Final perturbation\")\n", + "plt.xlabel('$\\eta_1$')\n", + "plt.ylabel('$v_x$')\n", + "plt.title('Final perturbation')\n", "plt.colorbar();" ] }, @@ -336,12 +336,12 @@ "source": [ "# electric field\n", "\n", - "e1, e2, e3 = simdata.grids_log\n", + "e1, e2, e3 = simdata.grids_log \n", "e_vals = simdata.spline_values[\"em_fields\"][\"e_field_log\"][0][0]\n", "\n", - "plt.plot(e1, e_vals[:, 0, 0], label=\"E\")\n", - "plt.xlabel(\"$\\eta_1$\")\n", - "plt.title(\"Initial electric field\")\n", + "plt.plot(e1, e_vals[:, 0, 0], label='E')\n", + "plt.xlabel('$\\eta_1$')\n", + "plt.title('Initial electric field')\n", "plt.legend();" ] } diff --git a/tutorials_old/tutorial_01_parameter_files.ipynb b/tutorials_old/tutorial_01_parameter_files.ipynb index 8c2ce8ced..e803e3746 100644 --- a/tutorials_old/tutorial_01_parameter_files.ipynb +++ b/tutorials_old/tutorial_01_parameter_files.ipynb @@ -44,18 +44,19 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy import main\n", - "from struphy.fields_background import equils\n", + "from struphy.io.options import EnvironmentOptions, Units, Time\n", "from struphy.geometry import domains\n", + "from struphy.fields_background import equils\n", + "from struphy.topology import grids\n", + "from struphy.io.options import DerhamOptions\n", + "from struphy.io.options import FieldsBackground\n", "from struphy.initial import perturbations\n", - "from struphy.io.options import DerhamOptions, EnvironmentOptions, FieldsBackground, Time, Units\n", "from struphy.kinetic_background import maxwellians\n", + "from struphy.pic.utilities import LoadingParameters, WeightsParameters, BoundaryParameters\n", + "from struphy import main\n", "\n", "# import model, set verbosity\n", "from struphy.models.toy import Vlasov as Model\n", - "from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters\n", - "from struphy.topology import grids\n", - "\n", "verbose = True" ] }, @@ -150,10 +151,10 @@ "\n", "loading_params = LoadingParameters(Np=15)\n", "weights_params = WeightsParameters()\n", - "boundary_params = BoundaryParameters(bc=(\"reflect\", \"reflect\", \"periodic\"))\n", - "model.kinetic_ions.set_markers(\n", - " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", - ")\n", + "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", + "model.kinetic_ions.set_markers(loading_params=loading_params, \n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params)\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)" ] @@ -221,18 +222,17 @@ "metadata": {}, "outputs": [], "source": [ - "main.run(\n", - " model,\n", - " params_path=None,\n", - " env=env,\n", - " units=units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - ")" + "main.run(model, \n", + " params_path=None, \n", + " env=env, \n", + " units=units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" ] }, { @@ -253,7 +253,6 @@ "outputs": [], "source": [ "import os\n", - "\n", "path = os.path.join(os.getcwd(), \"sim_1\")\n", "\n", "main.pproc(path, physical=True)" @@ -331,27 +330,27 @@ "fig = plt.figure()\n", "ax = fig.gca()\n", "\n", - "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", + "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", "\n", - "time = 0.0\n", + "time = 0.\n", "dt = time_opts.dt\n", "Tend = time_opts.Tend\n", "for k, v in simdata.pic_species[\"kinetic_ions\"][\"orbits\"].items():\n", " # print(f\"{v[0] = }\")\n", - " alpha = (Tend - time) / Tend\n", + " alpha = (Tend - time)/Tend\n", " for i, particle in enumerate(v):\n", " ax.scatter(particle[0], particle[1], c=colors[i % 4], alpha=alpha)\n", " time += dt\n", - "\n", - "ax.plot([l1, l1], [l2, r2], \"k\")\n", - "ax.plot([r1, r1], [l2, r2], \"k\")\n", - "ax.plot([l1, r1], [l2, l2], \"k\")\n", - "ax.plot([l1, r1], [r2, r2], \"k\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", + " \n", + "ax.plot([l1, l1], [l2, r2], 'k')\n", + "ax.plot([r1, r1], [l2, r2], 'k')\n", + "ax.plot([l1, r1], [l2, l2], 'k')\n", + "ax.plot([l1, r1], [r2, r2], 'k')\n", + "ax.set_xlabel('x')\n", + "ax.set_ylabel('y')\n", "ax.set_xlim(-6.5, 6.5)\n", "ax.set_ylim(-9, 9)\n", - "ax.set_title(f\"{int(Tend / dt)} time steps (full color at t=0)\");" + "ax.set_title(f'{int(Tend/dt)} time steps (full color at t=0)');" ] } ], diff --git a/tutorials_old/tutorial_01_particles.ipynb b/tutorials_old/tutorial_01_particles.ipynb index b72b1ce94..dc4bc05b5 100644 --- a/tutorials_old/tutorial_01_particles.ipynb +++ b/tutorials_old/tutorial_01_particles.ipynb @@ -43,18 +43,19 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy import main\n", - "from struphy.fields_background import equils\n", + "from struphy.io.options import EnvironmentOptions, Units, Time\n", "from struphy.geometry import domains\n", + "from struphy.fields_background import equils\n", + "from struphy.topology import grids\n", + "from struphy.io.options import DerhamOptions\n", + "from struphy.io.options import FieldsBackground\n", "from struphy.initial import perturbations\n", - "from struphy.io.options import DerhamOptions, EnvironmentOptions, FieldsBackground, Time, Units\n", "from struphy.kinetic_background import maxwellians\n", + "from struphy.pic.utilities import LoadingParameters, WeightsParameters, BoundaryParameters\n", + "from struphy import main\n", "\n", "# import model, set verbosity\n", "from struphy.models.toy import Vlasov as Model\n", - "from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters\n", - "from struphy.topology import grids\n", - "\n", "verbose = True" ] }, @@ -108,10 +109,10 @@ "\n", "loading_params = LoadingParameters(Np=15)\n", "weights_params = WeightsParameters()\n", - "boundary_params = BoundaryParameters(bc=(\"reflect\", \"reflect\", \"periodic\"))\n", - "model.kinetic_ions.set_markers(\n", - " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", - ")\n", + "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", + "model.kinetic_ions.set_markers(loading_params=loading_params, \n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params)\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)" ] @@ -149,18 +150,17 @@ "metadata": {}, "outputs": [], "source": [ - "main.run(\n", - " model,\n", - " params_path=None,\n", - " env=env,\n", - " units=units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - ")" + "main.run(model, \n", + " params_path=None, \n", + " env=env, \n", + " units=units, \n", + " time_opts=time_opts, \n", + " domain=domain, \n", + " equil=equil, \n", + " grid=grid, \n", + " derham_opts=derham_opts, \n", + " verbose=verbose, \n", + " )" ] }, { @@ -225,27 +225,27 @@ "fig = plt.figure()\n", "ax = fig.gca()\n", "\n", - "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", + "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", "\n", - "time = 0.0\n", + "time = 0.\n", "dt = time_opts.dt\n", "Tend = time_opts.Tend\n", "for k, v in simdata.pic_species[\"kinetic_ions\"][\"orbits\"].items():\n", " # print(k, v)\n", - " alpha = (Tend - time) / Tend\n", + " alpha = (Tend - time)/Tend\n", " for i, particle in enumerate(v):\n", " ax.scatter(particle[1], particle[2], c=colors[i % 4], alpha=alpha)\n", " time += dt\n", - "\n", - "ax.plot([l1, l1], [l2, r2], \"k\")\n", - "ax.plot([r1, r1], [l2, r2], \"k\")\n", - "ax.plot([l1, r1], [l2, l2], \"k\")\n", - "ax.plot([l1, r1], [r2, r2], \"k\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", + " \n", + "ax.plot([l1, l1], [l2, r2], 'k')\n", + "ax.plot([r1, r1], [l2, r2], 'k')\n", + "ax.plot([l1, r1], [l2, l2], 'k')\n", + "ax.plot([l1, r1], [r2, r2], 'k')\n", + "ax.set_xlabel('x')\n", + "ax.set_ylabel('y')\n", "ax.set_xlim(-6.5, 6.5)\n", "ax.set_ylim(-9, 9)\n", - "ax.set_title(f\"{int(Tend / dt)} time steps (full color at t=0)\");" + "ax.set_title(f'{int(Tend/dt)} time steps (full color at t=0)');" ] } ], diff --git a/tutorials_old/tutorial_02_fluid_particles.ipynb b/tutorials_old/tutorial_02_fluid_particles.ipynb index 7c22c1195..ea5831daa 100644 --- a/tutorials_old/tutorial_02_fluid_particles.ipynb +++ b/tutorials_old/tutorial_02_fluid_particles.ipynb @@ -813,7 +813,7 @@ "plt.tick_params(labelbottom=False)\n", "plt.plot(eta1, n_sph_init[:, 0, 0])\n", "plt.grid()\n", - "plt.title(\"n_sph_init\")\n", + "plt.title('n_sph_init')\n", "\n", "plt.subplot(2, 2, 4)\n", "ax = plt.gca()\n", @@ -822,8 +822,8 @@ "plt.tick_params(labelbottom=False)\n", "bc_x = (be_x[:-1] + be_x[1:]) / 2.0 # centers of binning cells\n", "plt.plot(bc_x, df_bin.T)\n", - "# plt.grid()\n", - "plt.title(\"n_binned\")" + "#plt.grid()\n", + "plt.title('n_binned')" ] }, { @@ -1243,8 +1243,8 @@ "plt.tick_params(labelbottom=False)\n", "plt.pcolor(ee1[:, :, 0], ee2[:, :, 0], n_sph_1[:, :, 0])\n", "plt.grid()\n", - "plt.axis(\"square\")\n", - "plt.title(\"n_sph (random)\")\n", + "plt.axis('square')\n", + "plt.title('n_sph (random)')\n", "plt.colorbar()\n", "\n", "plt.subplot(4, 2, 6)\n", @@ -1254,8 +1254,8 @@ "plt.tick_params(labelbottom=False)\n", "plt.pcolor(ee1[:, :, 0], ee2[:, :, 0], n_sph_2[:, :, 0])\n", "plt.grid()\n", - "plt.axis(\"square\")\n", - "plt.title(\"n_sph (tesselation)\")\n", + "plt.axis('square')\n", + "plt.title('n_sph (tesselation)')\n", "plt.colorbar()\n", "\n", "plt.subplot(4, 2, 7)\n", @@ -1266,9 +1266,9 @@ "bc_x = (be_x[:-1] + be_x[1:]) / 2.0 # centers of binning cells\n", "bc_y = (be_y[:-1] + be_y[1:]) / 2.0\n", "plt.pcolor(bc_x, bc_y, f_bin_1)\n", - "# plt.grid()\n", - "plt.axis(\"square\")\n", - "plt.title(\"n_binned (random)\")\n", + "#plt.grid()\n", + "plt.axis('square')\n", + "plt.title('n_binned (random)')\n", "plt.colorbar()\n", "\n", "plt.subplot(4, 2, 8)\n", @@ -1279,9 +1279,9 @@ "bc_x = (be_x[:-1] + be_x[1:]) / 2.0 # centers of binning cells\n", "bc_y = (be_y[:-1] + be_y[1:]) / 2.0\n", "plt.pcolor(bc_x, bc_y, f_bin_2)\n", - "# plt.grid()\n", - "plt.axis(\"square\")\n", - "plt.title(\"n_binned (tesselation)\")\n", + "#plt.grid()\n", + "plt.axis('square')\n", + "plt.title('n_binned (tesselation)')\n", "plt.colorbar()" ] }, diff --git a/utils/set_release_dependencies.py b/utils/set_release_dependencies.py index 08a54bba5..fb6556547 100644 --- a/utils/set_release_dependencies.py +++ b/utils/set_release_dependencies.py @@ -2,6 +2,8 @@ import re import tomllib +import tomli_w + def get_min_bound(entry): match = re.search(r"(>=|==|~=|>|>)\s*([\w\.\-]+)", entry) From af4a0d9f59a08844450a89050836da016221adf6 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 30 Oct 2025 17:47:22 +0100 Subject: [PATCH 13/83] Fixed redefinition of variables (#75) Redo of https://github.com/struphy-hub/struphy/pull/65 **Solves the following issue(s):** Closes #51 I deleted variables that were unused before redefinition according to `ruff check --select F811`. https://docs.astral.sh/ruff/rules/redefined-while-unused/ The biggest change was the class `PushVinViscousPotential `, which seems to be written twice. I removed the first one, but please can someone check that the version that remains is correct? --------- Co-authored-by: Stefan Possanner Co-authored-by: Byung Kyu Na Co-authored-by: Stefan Possanner <86720346+spossann@users.noreply.github.com> --- .github/workflows/static_analysis.yml | 9 +- .github/workflows/ubuntu-latest.yml | 19 +- pyproject.toml | 20 - src/struphy/console/tests/test_console.py | 1 - src/struphy/feec/preconditioner.py | 8 - .../feec/tests/test_local_projectors.py | 47 -- .../tests/test_mhd_equils.py | 1 - src/struphy/models/base.py | 1 - src/struphy/models/fluid.py | 3 - src/struphy/pic/tests/test_sorting.py | 8 +- .../likwid/plot_time_traces.py | 48 +- src/struphy/propagators/__init__.py | 6 +- .../propagators/propagators_markers.py | 4 +- tutorial_07_data_structures.ipynb | 20 +- tutorials/tutorial_01_parameter_files.ipynb | 79 ++-- tutorials/tutorial_02_test_particles.ipynb | 434 +++++++++--------- ...l_03_smoothed_particle_hydrodynamics.ipynb | 313 +++++++------ tutorials/tutorial_04_vlasov_maxwell.ipynb | 84 ++-- .../tutorial_01_parameter_files.ipynb | 67 +-- tutorials_old/tutorial_01_particles.ipynb | 66 +-- .../tutorial_02_fluid_particles.ipynb | 26 +- utils/set_release_dependencies.py | 2 - 22 files changed, 605 insertions(+), 661 deletions(-) diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 2fd4c2051..3022a70c2 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -121,10 +121,15 @@ jobs: - name: Checkout the code uses: actions/checkout@v4 - - name: Linting with ruff + # TODO: Remove --select I once all errors are fixed + - name: ruff check --select I run: | pip install ruff - ruff check --select I src/**/*.py + ruff check --select I + + - name: ruff format --check + run: | + ruff format --check # pylint: # runs-on: ubuntu-latest diff --git a/.github/workflows/ubuntu-latest.yml b/.github/workflows/ubuntu-latest.yml index 8fd73272e..e66aeb3b3 100644 --- a/.github/workflows/ubuntu-latest.yml +++ b/.github/workflows/ubuntu-latest.yml @@ -1,11 +1,20 @@ -name: Ubuntu latest - cronjob +name: Ubuntu on: - schedule: - # run at 1 a.m. on Sunday - - cron: "0 1 * * 0" + push: + branches: + - main + - devel + pull_request: + branches: + - main + - devel + +# concurrency: +# group: ${{ github.ref }} +# cancel-in-progress: true jobs: ubuntu-latest-build: - uses: ./.github/workflows/reusable-testing.yml + uses: ./.github/workflows/testing.yml with: os: ubuntu-latest \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4c4badf6c..9ee050079 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -184,23 +184,3 @@ markers = [ "hybrid", "single", ] - -[tool.pytest.ini_options] -markers = [ - "models", - "toy", - "fluid", - "kinetic", - "hybrid", - "single", -] - -[tool.pytest.ini_options] -markers = [ - "models", - "toy", - "fluid", - "kinetic", - "hybrid", - "single", -] diff --git a/src/struphy/console/tests/test_console.py b/src/struphy/console/tests/test_console.py index 5855e7cc3..c94e41eb2 100644 --- a/src/struphy/console/tests/test_console.py +++ b/src/struphy/console/tests/test_console.py @@ -7,7 +7,6 @@ import pytest # from psydac.ddm.mpi import mpi as MPI -import struphy import struphy as struphy_lib from struphy.console.compile import struphy_compile from struphy.console.main import struphy diff --git a/src/struphy/feec/preconditioner.py b/src/struphy/feec/preconditioner.py index 87b7e89fb..dfa00df4c 100644 --- a/src/struphy/feec/preconditioner.py +++ b/src/struphy/feec/preconditioner.py @@ -318,11 +318,6 @@ def solver(self): """KroneckerLinearSolver or BlockDiagonalSolver for exactly inverting the approximate mass matrix self.matrix.""" return self._solver - @property - def domain(self): - """The domain of the linear operator - an element of Vectorspace""" - return self._space - @property def codomain(self): """The codomain of the linear operator - an element of Vectorspace""" @@ -704,9 +699,6 @@ def matrix(self): def solver(self): """KroneckerLinearSolver or BlockDiagonalSolver for exactly inverting the approximate mass matrix self.matrix.""" return self._solver - - @property - def domain(self): """The domain of the linear operator - an element of Vectorspace""" return self._space diff --git a/src/struphy/feec/tests/test_local_projectors.py b/src/struphy/feec/tests/test_local_projectors.py index 4cf2d401c..f51177a6a 100644 --- a/src/struphy/feec/tests/test_local_projectors.py +++ b/src/struphy/feec/tests/test_local_projectors.py @@ -15,53 +15,6 @@ from struphy.feec.utilities_local_projectors import get_one_spline, get_span_and_basis, get_values_and_indices_splines -def get_span_and_basis(pts, space): - """Compute the knot span index and the values of p + 1 basis function at each point in pts. - - Parameters - ---------- - pts : xp.array - 2d array of points (ii, iq) = (interval, quadrature point). - - space : SplineSpace - Psydac object, the 1d spline space to be projected. - - Returns - ------- - span : xp.array - 2d array indexed by (n, nq), where n is the interval and nq is the quadrature point in the interval. - - basis : xp.array - 3d array of values of basis functions indexed by (n, nq, basis function). - """ - - import psydac.core.bsplines as bsp - - # Extract knot vectors, degree and kind of basis - T = space.knots - p = space.degree - - span = xp.zeros(pts.shape, dtype=int) - basis = xp.zeros((*pts.shape, p + 1), dtype=float) - - for n in range(pts.shape[0]): - for nq in range(pts.shape[1]): - # avoid 1. --> 0. for clamped interpolation - x = pts[n, nq] % (1.0 + 1e-14) - span_tmp = bsp.find_span(T, p, x) - basis[n, nq, :] = bsp.basis_funs_all_ders( - T, - p, - x, - span_tmp, - 0, - normalization=space.basis, - ) - span[n, nq] = span_tmp # % space.nbasis - - return span, basis - - @pytest.mark.parametrize("Nel", [[14, 16, 18]]) @pytest.mark.parametrize("p", [[5, 4, 3]]) @pytest.mark.parametrize("spl_kind", [[True, False, False], [False, True, False], [False, False, True]]) diff --git a/src/struphy/fields_background/tests/test_mhd_equils.py b/src/struphy/fields_background/tests/test_mhd_equils.py index 494d707b3..f363ddbe3 100644 --- a/src/struphy/fields_background/tests/test_mhd_equils.py +++ b/src/struphy/fields_background/tests/test_mhd_equils.py @@ -136,7 +136,6 @@ def test_equils(equil_domain_pair): Test field evaluations of all implemented MHD equilbria with default parameters. """ - from struphy.fields_background import equils from struphy.fields_background.base import CartesianMHDequilibrium, NumericalMHDequilibrium from struphy.geometry import domains diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index eb133cb45..b484397a0 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -1233,7 +1233,6 @@ def write_parameters_to_file(cls, parameters=None, file=None, save=True, prompt= import yaml - import struphy import struphy.utils.utils as utils # Read struphy state file diff --git a/src/struphy/models/fluid.py b/src/struphy/models/fluid.py index a4916e39d..405610b7b 100644 --- a/src/struphy/models/fluid.py +++ b/src/struphy/models/fluid.py @@ -2388,9 +2388,6 @@ def allocate_helpers(self): self._rho: StencilVector = self.derham.Vh["0"].zeros() self.update_rho() - def update_scalar_quantities(self): - pass - def update_rho(self): omega = self.plasma.vorticity.spline.vector self._rho = self.mass_ops.M0.dot(omega, out=self._rho) diff --git a/src/struphy/pic/tests/test_sorting.py b/src/struphy/pic/tests/test_sorting.py index a11c9600e..0daf8f4c9 100644 --- a/src/struphy/pic/tests/test_sorting.py +++ b/src/struphy/pic/tests/test_sorting.py @@ -14,7 +14,7 @@ @pytest.mark.parametrize("ny", [16, 80]) @pytest.mark.parametrize("nz", [32, 90]) @pytest.mark.parametrize("algo", ["fortran_ordering", "c_ordering"]) -def test_flattening(nx, ny, nz, algo): +def test_flattening_1(nx, ny, nz, algo): from struphy.pic.sorting_kernels import flatten_index, unflatten_index n1s = xp.array(xp.random.rand(10) * (nx + 1), dtype=int) @@ -34,7 +34,7 @@ def test_flattening(nx, ny, nz, algo): @pytest.mark.parametrize("ny", [16, 80]) @pytest.mark.parametrize("nz", [32, 90]) @pytest.mark.parametrize("algo", ["fortran_ordering", "c_ordering"]) -def test_flattening(nx, ny, nz, algo): +def test_flattening_2(nx, ny, nz, algo): from struphy.pic.sorting_kernels import flatten_index, unflatten_index n1s = xp.array(xp.random.rand(10) * (nx + 1), dtype=int) @@ -54,7 +54,7 @@ def test_flattening(nx, ny, nz, algo): @pytest.mark.parametrize("ny", [16, 80]) @pytest.mark.parametrize("nz", [32, 90]) @pytest.mark.parametrize("algo", ["fortran_ordering", "c_ordering"]) -def test_flattening(nx, ny, nz, algo): +def test_flattening_3(nx, ny, nz, algo): from struphy.pic.sorting_kernels import flatten_index, unflatten_index n1s = xp.array(xp.random.rand(10) * (nx + 1), dtype=int) @@ -136,7 +136,7 @@ def test_sorting(Nel, p, spl_kind, mapping, Np, verbose=False): if __name__ == "__main__": - test_flattening(8, 8, 8, "c_orderwding") + test_flattening_1(8, 8, 8, "c_orderwding") # test_sorting( # [8, 9, 10], # [2, 3, 4], diff --git a/src/struphy/post_processing/likwid/plot_time_traces.py b/src/struphy/post_processing/likwid/plot_time_traces.py index f97681ffa..7451833cb 100644 --- a/src/struphy/post_processing/likwid/plot_time_traces.py +++ b/src/struphy/post_processing/likwid/plot_time_traces.py @@ -4,6 +4,7 @@ import cunumpy as xp import matplotlib.pyplot as plt +import plotly.graph_objects as go import plotly.io as pio # pio.kaleido.scope.mathjax = None @@ -16,19 +17,31 @@ def glob_to_regex(pat: str) -> str: return "^" + esc.replace(r"\*", ".*").replace(r"\?", ".") + "$" +# def plot_region(region_name, groups_include=["*"], groups_skip=[]): +# # skips first +# for pat in groups_skip: +# rx = glob_to_regex(pat) +# if re.fullmatch(rx, region_name): +# return False + +# # includes next +# for pat in groups_include: +# rx = glob_to_regex(pat) +# if re.fullmatch(rx, region_name): +# return True + +# return False + + def plot_region(region_name, groups_include=["*"], groups_skip=[]): - # skips first - for pat in groups_skip: - rx = glob_to_regex(pat) - if re.fullmatch(rx, region_name): - return False + from fnmatch import fnmatch - # includes next - for pat in groups_include: - rx = glob_to_regex(pat) - if re.fullmatch(rx, region_name): + for pattern in groups_skip: + if fnmatch(region_name, pattern): + return False + for pattern in groups_include: + if fnmatch(region_name, pattern): return True - return False @@ -146,21 +159,6 @@ def plot_avg_duration_bar_chart( print(f"Saved average duration bar chart to: {figure_path}") -import plotly.graph_objects as go - - -def plot_region(region_name, groups_include=["*"], groups_skip=[]): - from fnmatch import fnmatch - - for pattern in groups_skip: - if fnmatch(region_name, pattern): - return False - for pattern in groups_include: - if fnmatch(region_name, pattern): - return True - return False - - def plot_gantt_chart_plotly( path: str, output_path: str, diff --git a/src/struphy/propagators/__init__.py b/src/struphy/propagators/__init__.py index 04418745c..72067e021 100644 --- a/src/struphy/propagators/__init__.py +++ b/src/struphy/propagators/__init__.py @@ -44,7 +44,8 @@ # PushRandomDiffusion, # PushVinEfield, # PushVinSPHpressure, -# PushVinViscousPotential, +# PushVinViscousPotential2D, +# PushVinViscousPotential3D, # PushVxB, # StepStaticEfield, # ) @@ -92,5 +93,6 @@ # "PushDeterministicDiffusion", # "PushRandomDiffusion", # "PushVinSPHpressure", -# "PushVinViscousPotential", +# "PushVinViscousPotential2D", +# "PushVinViscousPotential3D", # ] diff --git a/src/struphy/propagators/propagators_markers.py b/src/struphy/propagators/propagators_markers.py index f1dbbe5f6..0360f39de 100644 --- a/src/struphy/propagators/propagators_markers.py +++ b/src/struphy/propagators/propagators_markers.py @@ -1778,7 +1778,7 @@ def __call__(self, dt): self._pusher(dt) -class PushVinViscousPotential(Propagator): +class PushVinViscousPotential2D(Propagator): r"""For each marker :math:`p`, solves .. math:: @@ -1909,7 +1909,7 @@ def __call__(self, dt): self._pusher(dt) -class PushVinViscousPotential(Propagator): +class PushVinViscousPotential3D(Propagator): r"""For each marker :math:`p`, solves .. math:: diff --git a/tutorial_07_data_structures.ipynb b/tutorial_07_data_structures.ipynb index bde0b0c23..dc21d7332 100644 --- a/tutorial_07_data_structures.ipynb +++ b/tutorial_07_data_structures.ipynb @@ -917,9 +917,9 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{particles.Np = }')\n", - "print(f'{particles.markers.shape = }')\n", - "print(f'{particles.markers_wo_holes.shape = }')" + "print(f\"{particles.Np = }\")\n", + "print(f\"{particles.markers.shape = }\")\n", + "print(f\"{particles.markers_wo_holes.shape = }\")" ] }, { @@ -935,13 +935,13 @@ "metadata": {}, "outputs": [], "source": [ - "print(f'{particles.positions[:5] = }\\n')\n", - "print(f'{particles.velocities[:5] = }\\n')\n", - "print(f'{particles.phasespace_coords[:5] = }\\n')\n", - "print(f'{particles.weights[:5] = }\\n')\n", - "print(f'{particles.sampling_density[:5] = }\\n')\n", - "print(f'{particles.weights0[:5] = }\\n')\n", - "print(f'{particles.marker_ids[:5] = }')" + "print(f\"{particles.positions[:5] = }\\n\")\n", + "print(f\"{particles.velocities[:5] = }\\n\")\n", + "print(f\"{particles.phasespace_coords[:5] = }\\n\")\n", + "print(f\"{particles.weights[:5] = }\\n\")\n", + "print(f\"{particles.sampling_density[:5] = }\\n\")\n", + "print(f\"{particles.weights0[:5] = }\\n\")\n", + "print(f\"{particles.marker_ids[:5] = }\")" ] } ], diff --git a/tutorials/tutorial_01_parameter_files.ipynb b/tutorials/tutorial_01_parameter_files.ipynb index 7621ed6b1..a382368fa 100644 --- a/tutorials/tutorial_01_parameter_files.ipynb +++ b/tutorials/tutorial_01_parameter_files.ipynb @@ -49,24 +49,23 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", - "from struphy.geometry import domains\n", + "from struphy import main\n", "from struphy.fields_background import equils\n", - "from struphy.topology import grids\n", - "from struphy.io.options import DerhamOptions\n", - "from struphy.io.options import FieldsBackground\n", + "from struphy.geometry import domains\n", "from struphy.initial import perturbations\n", + "from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Time\n", "from struphy.kinetic_background import maxwellians\n", - "from struphy.pic.utilities import (LoadingParameters, \n", - " WeightsParameters, \n", - " BoundaryParameters,\n", - " BinningPlot,\n", - " KernelDensityPlot,\n", - " )\n", - "from struphy import main\n", "\n", "# import model, set verbosity\n", - "from struphy.models.toy import Vlasov" + "from struphy.models.toy import Vlasov\n", + "from struphy.pic.utilities import (\n", + " BinningPlot,\n", + " BoundaryParameters,\n", + " KernelDensityPlot,\n", + " LoadingParameters,\n", + " WeightsParameters,\n", + ")\n", + "from struphy.topology import grids" ] }, { @@ -172,10 +171,10 @@ "source": [ "loading_params = LoadingParameters(Np=15)\n", "weights_params = WeightsParameters()\n", - "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", - "model.kinetic_ions.set_markers(loading_params=loading_params, \n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params)\n", + "boundary_params = BoundaryParameters(bc=(\"reflect\", \"reflect\", \"periodic\"))\n", + "model.kinetic_ions.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", + ")\n", "\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)" @@ -248,17 +247,18 @@ "source": [ "verbose = True\n", "\n", - "main.run(model, \n", - " params_path=None, \n", - " env=env, \n", - " base_units=base_units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -279,6 +279,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "path = os.path.join(os.getcwd(), \"sim_1\")\n", "\n", "main.pproc(path)" @@ -363,31 +364,31 @@ "metadata": {}, "outputs": [], "source": [ - "from matplotlib import pyplot as plt\n", "import numpy as np\n", + "from matplotlib import pyplot as plt\n", "\n", "fig = plt.figure()\n", "ax = fig.gca()\n", "\n", - "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", "\n", "# create alpha for color scaling\n", "Tend = time_opts.Tend\n", - "alpha = np.linspace(1., 0., Nt + 1)\n", + "alpha = np.linspace(1.0, 0.0, Nt + 1)\n", "\n", "# loop through particles, plot all time steps\n", "for i in range(Np):\n", " ax.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], alpha=alpha)\n", - " \n", - "ax.plot([l1, l1], [l2, r2], 'k')\n", - "ax.plot([r1, r1], [l2, r2], 'k')\n", - "ax.plot([l1, r1], [l2, l2], 'k')\n", - "ax.plot([l1, r1], [r2, r2], 'k')\n", - "ax.set_xlabel('x')\n", - "ax.set_ylabel('y')\n", + "\n", + "ax.plot([l1, l1], [l2, r2], \"k\")\n", + "ax.plot([r1, r1], [l2, r2], \"k\")\n", + "ax.plot([l1, r1], [l2, l2], \"k\")\n", + "ax.plot([l1, r1], [r2, r2], \"k\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", "ax.set_xlim(-6.5, 6.5)\n", "ax.set_ylim(-9, 9)\n", - "ax.set_title(f'{int(Nt - 1)} time steps (full color at t=0)');" + "ax.set_title(f\"{int(Nt - 1)} time steps (full color at t=0)\");" ] } ], diff --git a/tutorials/tutorial_02_test_particles.ipynb b/tutorials/tutorial_02_test_particles.ipynb index e95b6df81..7aac7f7e8 100644 --- a/tutorials/tutorial_02_test_particles.ipynb +++ b/tutorials/tutorial_02_test_particles.ipynb @@ -37,24 +37,23 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", - "from struphy.geometry import domains\n", + "from struphy import main\n", "from struphy.fields_background import equils\n", - "from struphy.topology import grids\n", - "from struphy.io.options import DerhamOptions\n", - "from struphy.io.options import FieldsBackground\n", + "from struphy.geometry import domains\n", "from struphy.initial import perturbations\n", + "from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Time\n", "from struphy.kinetic_background import maxwellians\n", - "from struphy.pic.utilities import (LoadingParameters, \n", - " WeightsParameters, \n", - " BoundaryParameters,\n", - " BinningPlot,\n", - " KernelDensityPlot,\n", - " )\n", - "from struphy import main\n", "\n", "# import model, set verbosity\n", - "from struphy.models.toy import Vlasov" + "from struphy.models.toy import Vlasov\n", + "from struphy.pic.utilities import (\n", + " BinningPlot,\n", + " BoundaryParameters,\n", + " KernelDensityPlot,\n", + " LoadingParameters,\n", + " WeightsParameters,\n", + ")\n", + "from struphy.topology import grids" ] }, { @@ -99,9 +98,9 @@ "time_opts = Time(dt=0.2, Tend=0.2)\n", "\n", "# geometry\n", - "a1 = 0.\n", - "a2 = 5.\n", - "Lz = 20.\n", + "a1 = 0.0\n", + "a2 = 5.0\n", + "Lz = 20.0\n", "domain = domains.HollowCylinder(a1=a1, a2=a2, Lz=Lz)" ] }, @@ -193,12 +192,12 @@ "weights_params = WeightsParameters()\n", "boundary_params = BoundaryParameters()\n", "\n", - "model.kinetic_ions.set_markers(loading_params=loading_params, \n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params)\n", - "model_2.kinetic_ions.set_markers(loading_params=loading_params_2, \n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params)\n", + "model.kinetic_ions.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", + ")\n", + "model_2.kinetic_ions.set_markers(\n", + " loading_params=loading_params_2, weights_params=weights_params, boundary_params=boundary_params\n", + ")\n", "\n", "model.kinetic_ions.set_sorting_boxes()\n", "model_2.kinetic_ions.set_sorting_boxes()\n", @@ -262,17 +261,18 @@ "source": [ "verbose = False\n", "\n", - "main.run(model, \n", - " params_path=None, \n", - " env=env, \n", - " base_units=base_units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -290,17 +290,18 @@ "metadata": {}, "outputs": [], "source": [ - "main.run(model_2, \n", - " params_path=None, \n", - " env=env_2, \n", - " base_units=base_units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model_2,\n", + " params_path=None,\n", + " env=env_2,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -319,6 +320,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "path = os.path.join(os.getcwd(), \"sim_1\")\n", "path_2 = os.path.join(os.getcwd(), \"sim_2\")\n", "\n", @@ -346,7 +348,7 @@ "source": [ "from matplotlib import pyplot as plt\n", "\n", - "fig = plt.figure(figsize=(10, 6)) \n", + "fig = plt.figure(figsize=(10, 6))\n", "\n", "orbits = simdata.orbits[\"kinetic_ions\"]\n", "orbits_uni = simdata_2.orbits[\"kinetic_ions\"]\n", @@ -355,24 +357,24 @@ "# orbits_uni = simdata_2.pic_species[\"kinetic_ions\"][\"orbits\"]\n", "\n", "plt.subplot(1, 2, 1)\n", - "plt.scatter(orbits[0, :, 0], orbits[0, :, 1], s=2.)\n", - "circle1 = plt.Circle((0, 0), a2, color='k', fill=False)\n", + "plt.scatter(orbits[0, :, 0], orbits[0, :, 1], s=2.0)\n", + "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "ax = plt.gca()\n", "ax.add_patch(circle1)\n", - "ax.set_aspect('equal')\n", - "plt.xlabel('x')\n", - "plt.ylabel('y')\n", - "plt.title('sim_1: draw uniform in logical space')\n", + "ax.set_aspect(\"equal\")\n", + "plt.xlabel(\"x\")\n", + "plt.ylabel(\"y\")\n", + "plt.title(\"sim_1: draw uniform in logical space\")\n", "\n", "plt.subplot(1, 2, 2)\n", - "plt.scatter(orbits_uni[0, :, 0], orbits_uni[0, :, 1], s=2.)\n", - "circle2 = plt.Circle((0, 0), a2, color='k', fill=False)\n", + "plt.scatter(orbits_uni[0, :, 0], orbits_uni[0, :, 1], s=2.0)\n", + "circle2 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "ax = plt.gca()\n", "ax.add_patch(circle2)\n", - "ax.set_aspect('equal')\n", - "plt.xlabel('x')\n", - "plt.ylabel('y')\n", - "plt.title('sim_2: draw uniform on disc');" + "ax.set_aspect(\"equal\")\n", + "plt.xlabel(\"x\")\n", + "plt.ylabel(\"y\")\n", + "plt.title(\"sim_2: draw uniform on disc\");" ] }, { @@ -395,7 +397,7 @@ "source": [ "time_opts = Time(dt=0.2, Tend=10.0)\n", "loading_params = LoadingParameters(Np=15, spatial=\"disc\")\n", - "boundary_params = BoundaryParameters(bc=('reflect', 'periodic', 'periodic'))" + "boundary_params = BoundaryParameters(bc=(\"reflect\", \"periodic\", \"periodic\"))" ] }, { @@ -411,9 +413,9 @@ "# species parameters\n", "model.kinetic_ions.set_phys_params()\n", "\n", - "model.kinetic_ions.set_markers(loading_params=loading_params, \n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params)\n", + "model.kinetic_ions.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", + ")\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)" ] @@ -461,17 +463,18 @@ "source": [ "verbose = False\n", "\n", - "main.run(model, \n", - " params_path=None, \n", - " env=env, \n", - " base_units=base_units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -529,23 +532,23 @@ "fig = plt.figure()\n", "ax = fig.gca()\n", "\n", - "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", "\n", "# create alpha for color scaling\n", "Tend = time_opts.Tend\n", - "alpha = np.linspace(1., 0., Nt + 1)\n", + "alpha = np.linspace(1.0, 0.0, Nt + 1)\n", "\n", "# loop through particles, plot all time steps\n", "for i in range(Np):\n", " ax.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], alpha=alpha)\n", - " \n", - "circle1 = plt.Circle((0, 0), a2, color='k', fill=False)\n", + "\n", + "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "\n", "ax.add_patch(circle1)\n", - "ax.set_aspect('equal')\n", - "ax.set_xlabel('x')\n", - "ax.set_ylabel('y')\n", - "ax.set_title(f'{Nt - 1} time steps (full color at t=0)');" + "ax.set_aspect(\"equal\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "ax.set_title(f\"{Nt - 1} time steps (full color at t=0)\");" ] }, { @@ -578,9 +581,9 @@ "metadata": {}, "outputs": [], "source": [ - "B0x = 0.\n", - "B0y = 0.\n", - "B0z = 1.\n", + "B0x = 0.0\n", + "B0y = 0.0\n", + "B0z = 1.0\n", "equil = equils.HomogenSlab(B0x=B0x, B0y=B0y, B0z=B0z)" ] }, @@ -625,10 +628,10 @@ "model.kinetic_ions.set_phys_params()\n", "\n", "loading_params = LoadingParameters(Np=20)\n", - "boundary_params = BoundaryParameters(bc=('remove', 'periodic', 'periodic'))\n", - "model.kinetic_ions.set_markers(loading_params=loading_params, \n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params)\n", + "boundary_params = BoundaryParameters(bc=(\"remove\", \"periodic\", \"periodic\"))\n", + "model.kinetic_ions.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", + ")\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)\n", "\n", @@ -661,17 +664,18 @@ "# run\n", "verbose = False\n", "\n", - "main.run(model, \n", - " params_path=None, \n", - " env=env, \n", - " base_units=base_units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -710,23 +714,23 @@ "fig = plt.figure()\n", "ax = fig.gca()\n", "\n", - "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", "\n", "# create alpha for color scaling\n", "Tend = time_opts.Tend\n", - "alpha = np.linspace(1., 0., Nt + 1)\n", + "alpha = np.linspace(1.0, 0.0, Nt + 1)\n", "\n", "# loop through particles, plot all time steps\n", "for i in range(Np):\n", " ax.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], alpha=alpha)\n", - " \n", - "circle1 = plt.Circle((0, 0), a2, color='k', fill=False)\n", + "\n", + "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", "\n", "ax.add_patch(circle1)\n", - "ax.set_aspect('equal')\n", - "ax.set_xlabel('x')\n", - "ax.set_ylabel('y')\n", - "ax.set_title(f'{int(Nt - 1)} time steps (full color at t=0)');" + "ax.set_aspect(\"equal\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "ax.set_title(f\"{int(Nt - 1)} time steps (full color at t=0)\");" ] }, { @@ -746,9 +750,9 @@ "metadata": {}, "outputs": [], "source": [ - "n1 = 0.\n", - "n2 = 0.\n", - "na = 1.\n", + "n1 = 0.0\n", + "n2 = 0.0\n", + "na = 1.0\n", "equil = equils.EQDSKequilibrium(n1=n1, n2=n2, na=na)" ] }, @@ -770,12 +774,8 @@ "Nel = (28, 72)\n", "p = (3, 3)\n", "psi_power = 0.6\n", - "psi_shifts = (1e-6, 1.)\n", - "domain = domains.Tokamak(equilibrium=equil, \n", - " Nel=Nel,\n", - " p=p,\n", - " psi_power=psi_power,\n", - " psi_shifts=psi_shifts)" + "psi_shifts = (1e-6, 1.0)\n", + "domain = domains.Tokamak(equilibrium=equil, Nel=Nel, p=p, psi_power=psi_power, psi_shifts=psi_shifts)" ] }, { @@ -838,9 +838,9 @@ "import numpy as np\n", "\n", "# logical grid on the unit cube\n", - "e1 = np.linspace(0., 1., 101)\n", - "e2 = np.linspace(0., 1., 101)\n", - "e3 = np.linspace(0., 1., 101)\n", + "e1 = np.linspace(0.0, 1.0, 101)\n", + "e2 = np.linspace(0.0, 1.0, 101)\n", + "e3 = np.linspace(0.0, 1.0, 101)\n", "\n", "# move away from the singular point r = 0\n", "e1[0] += 1e-5" @@ -854,11 +854,11 @@ "outputs": [], "source": [ "# logical coordinates of the poloidal plane at phi = 0\n", - "eta_poloidal = (e1, e2, 0.)\n", + "eta_poloidal = (e1, e2, 0.0)\n", "# logical coordinates of the top view at theta = 0\n", - "eta_topview_1 = (e1, 0., e3)\n", + "eta_topview_1 = (e1, 0.0, e3)\n", "# logical coordinates of the top view at theta = pi\n", - "eta_topview_2 = (e1, .5, e3)" + "eta_topview_2 = (e1, 0.5, e3)" ] }, { @@ -873,9 +873,9 @@ "x_top1, y_top1, z_top1 = domain(*eta_topview_1, squeeze_out=True)\n", "x_top2, y_top2, z_top2 = domain(*eta_topview_2, squeeze_out=True)\n", "\n", - "print(f'{x_pol.shape = }')\n", - "print(f'{x_top1.shape = }')\n", - "print(f'{x_top2.shape = }')" + "print(f\"{x_pol.shape = }\")\n", + "print(f\"{x_top1.shape = }\")\n", + "print(f\"{x_top2.shape = }\")" ] }, { @@ -904,36 +904,35 @@ "ax_top.contourf(x_top2, y_top2, equil.absB0(*eta_topview_2, squeeze_out=True), levels=levels)\n", "\n", "# last closed flux surface, poloidal\n", - "ax.plot(x_pol[-1], z_pol[-1], color='k')\n", + "ax.plot(x_pol[-1], z_pol[-1], color=\"k\")\n", "\n", "# last closed flux surface, toroidal\n", - "ax_top.plot(x_top1[-1], y_top1[-1], color='k')\n", - "ax_top.plot(x_top2[-1], y_top2[-1], color='k')\n", + "ax_top.plot(x_top1[-1], y_top1[-1], color=\"k\")\n", + "ax_top.plot(x_top2[-1], y_top2[-1], color=\"k\")\n", "\n", "# limiter, poloidal\n", - "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, 'tab:orange')\n", - "ax.axis('equal')\n", - "ax.set_xlabel('R')\n", - "ax.set_ylabel('Z')\n", - "ax.set_title('abs(B) at $\\phi=0$')\n", - "fig.colorbar(im);\n", - "\n", + "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, \"tab:orange\")\n", + "ax.axis(\"equal\")\n", + "ax.set_xlabel(\"R\")\n", + "ax.set_ylabel(\"Z\")\n", + "ax.set_title(\"abs(B) at $\\phi=0$\")\n", + "fig.colorbar(im)\n", "# limiter, toroidal\n", "limiter_Rmax = np.max(equil.limiter_pts_R)\n", "limiter_Rmin = np.min(equil.limiter_pts_R)\n", "\n", - "thetas = 2*np.pi*e2\n", + "thetas = 2 * np.pi * e2\n", "limiter_x_max = limiter_Rmax * np.cos(thetas)\n", - "limiter_y_max = - limiter_Rmax * np.sin(thetas)\n", + "limiter_y_max = -limiter_Rmax * np.sin(thetas)\n", "limiter_x_min = limiter_Rmin * np.cos(thetas)\n", - "limiter_y_min = - limiter_Rmin * np.sin(thetas)\n", - "\n", - "ax_top.plot(limiter_x_max, limiter_y_max, 'tab:orange')\n", - "ax_top.plot(limiter_x_min, limiter_y_min, 'tab:orange')\n", - "ax_top.axis('equal')\n", - "ax_top.set_xlabel('x')\n", - "ax_top.set_ylabel('y')\n", - "ax_top.set_title('abs(B) at $Z=0$')\n", + "limiter_y_min = -limiter_Rmin * np.sin(thetas)\n", + "\n", + "ax_top.plot(limiter_x_max, limiter_y_max, \"tab:orange\")\n", + "ax_top.plot(limiter_x_min, limiter_y_min, \"tab:orange\")\n", + "ax_top.axis(\"equal\")\n", + "ax_top.set_xlabel(\"x\")\n", + "ax_top.set_ylabel(\"y\")\n", + "ax_top.set_title(\"abs(B) at $Z=0$\")\n", "fig.colorbar(im_top);" ] }, @@ -958,17 +957,18 @@ "# species parameters\n", "model.kinetic_ions.set_phys_params()\n", "\n", - "initial = ((.501, 0.001, 0.001, 0., 0.0450, -0.04), # co-passing particle\n", - " (.511, 0.001, 0.001, 0., -0.0450, -0.04), # counter passing particle\n", - " (.521, 0.001, 0.001, 0., 0.0105, -0.04), # co-trapped particle\n", - " (.531, 0.001, 0.001, 0., -0.0155, -0.04))\n", + "initial = (\n", + " (0.501, 0.001, 0.001, 0.0, 0.0450, -0.04), # co-passing particle\n", + " (0.511, 0.001, 0.001, 0.0, -0.0450, -0.04), # counter passing particle\n", + " (0.521, 0.001, 0.001, 0.0, 0.0105, -0.04), # co-trapped particle\n", + " (0.531, 0.001, 0.001, 0.0, -0.0155, -0.04),\n", + ")\n", "\n", "loading_params = LoadingParameters(Np=4, seed=1608, specific_markers=initial)\n", - "boundary_params = BoundaryParameters(bc=('remove', 'periodic', 'periodic'))\n", - "model.kinetic_ions.set_markers(loading_params=loading_params, \n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params,\n", - " bufsize=2.)\n", + "boundary_params = BoundaryParameters(bc=(\"remove\", \"periodic\", \"periodic\"))\n", + "model.kinetic_ions.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params, bufsize=2.0\n", + ")\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)\n", "\n", @@ -1025,17 +1025,18 @@ "\n", "verbose = False\n", "\n", - "main.run(model, \n", - " params_path=None, \n", - " env=env, \n", - " base_units=base_units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -1046,6 +1047,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "from struphy import main\n", "\n", "path = os.path.join(os.getcwd(), \"sim_1\")\n", @@ -1076,21 +1078,20 @@ "source": [ "import math\n", "\n", - "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", "\n", "dt = time_opts.dt\n", "Tend = time_opts.Tend\n", "\n", "for i in range(Np):\n", - " r = np.sqrt(orbits[:, i, 0]**2 + orbits[:, i, 1]**2)\n", - " # poloidal \n", + " r = np.sqrt(orbits[:, i, 0] ** 2 + orbits[:, i, 1] ** 2)\n", + " # poloidal\n", " ax.scatter(r, orbits[:, i, 2], c=colors[i % 4], s=1)\n", " # top view\n", " ax_top.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], s=1)\n", - " \n", - "ax.set_title(f'{math.ceil(Tend/dt)} time steps')\n", - "ax_top.set_title(f'{math.ceil(Tend/dt)} time steps');\n", "\n", + "ax.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", + "ax_top.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", "fig" ] }, @@ -1119,17 +1120,18 @@ "# species parameters\n", "model.kinetic_ions.set_phys_params()\n", "\n", - "initial = ((.501, 0.001, 0.001, -1.935 , 1.72), # co-passing particle\n", - " (.501, 0.001, 0.001, 1.935 , 1.72), # couner-passing particle\n", - " (.501, 0.001, 0.001, -0.6665, 1.72), # co-trapped particle\n", - " (.501, 0.001, 0.001, 0.4515, 1.72)) # counter-trapped particl\n", + "initial = (\n", + " (0.501, 0.001, 0.001, -1.935, 1.72), # co-passing particle\n", + " (0.501, 0.001, 0.001, 1.935, 1.72), # couner-passing particle\n", + " (0.501, 0.001, 0.001, -0.6665, 1.72), # co-trapped particle\n", + " (0.501, 0.001, 0.001, 0.4515, 1.72),\n", + ") # counter-trapped particl\n", "\n", "loading_params = LoadingParameters(Np=4, seed=1608, specific_markers=initial)\n", - "boundary_params = BoundaryParameters(bc=('remove', 'periodic', 'periodic'))\n", - "model.kinetic_ions.set_markers(loading_params=loading_params, \n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params,\n", - " bufsize=2.)\n", + "boundary_params = BoundaryParameters(bc=(\"remove\", \"periodic\", \"periodic\"))\n", + "model.kinetic_ions.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params, bufsize=2.0\n", + ")\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)\n", "\n", @@ -1170,36 +1172,35 @@ "ax_top.contourf(x_top2, y_top2, equil.absB0(*eta_topview_2, squeeze_out=True), levels=levels)\n", "\n", "# last closed flux surface, poloidal\n", - "ax.plot(x_pol[-1], z_pol[-1], color='k')\n", + "ax.plot(x_pol[-1], z_pol[-1], color=\"k\")\n", "\n", "# last closed flux surface, toroidal\n", - "ax_top.plot(x_top1[-1], y_top1[-1], color='k')\n", - "ax_top.plot(x_top2[-1], y_top2[-1], color='k')\n", + "ax_top.plot(x_top1[-1], y_top1[-1], color=\"k\")\n", + "ax_top.plot(x_top2[-1], y_top2[-1], color=\"k\")\n", "\n", "# limiter, poloidal\n", - "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, 'tab:orange')\n", - "ax.axis('equal')\n", - "ax.set_xlabel('R')\n", - "ax.set_ylabel('Z')\n", - "ax.set_title('abs(B) at $\\phi=0$')\n", - "fig.colorbar(im);\n", - "\n", + "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, \"tab:orange\")\n", + "ax.axis(\"equal\")\n", + "ax.set_xlabel(\"R\")\n", + "ax.set_ylabel(\"Z\")\n", + "ax.set_title(\"abs(B) at $\\phi=0$\")\n", + "fig.colorbar(im)\n", "# limiter, toroidal\n", "limiter_Rmax = np.max(equil.limiter_pts_R)\n", "limiter_Rmin = np.min(equil.limiter_pts_R)\n", "\n", - "thetas = 2*np.pi*e2\n", + "thetas = 2 * np.pi * e2\n", "limiter_x_max = limiter_Rmax * np.cos(thetas)\n", - "limiter_y_max = - limiter_Rmax * np.sin(thetas)\n", + "limiter_y_max = -limiter_Rmax * np.sin(thetas)\n", "limiter_x_min = limiter_Rmin * np.cos(thetas)\n", - "limiter_y_min = - limiter_Rmin * np.sin(thetas)\n", - "\n", - "ax_top.plot(limiter_x_max, limiter_y_max, 'tab:orange')\n", - "ax_top.plot(limiter_x_min, limiter_y_min, 'tab:orange')\n", - "ax_top.axis('equal')\n", - "ax_top.set_xlabel('x')\n", - "ax_top.set_ylabel('y')\n", - "ax_top.set_title('abs(B) at $Z=0$')\n", + "limiter_y_min = -limiter_Rmin * np.sin(thetas)\n", + "\n", + "ax_top.plot(limiter_x_max, limiter_y_max, \"tab:orange\")\n", + "ax_top.plot(limiter_x_min, limiter_y_min, \"tab:orange\")\n", + "ax_top.axis(\"equal\")\n", + "ax_top.set_xlabel(\"x\")\n", + "ax_top.set_ylabel(\"y\")\n", + "ax_top.set_title(\"abs(B) at $Z=0$\")\n", "fig.colorbar(im_top);" ] }, @@ -1214,17 +1215,18 @@ "\n", "verbose = False\n", "\n", - "main.run(model, \n", - " params_path=None, \n", - " env=env, \n", - " base_units=base_units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -1235,6 +1237,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "from struphy import main\n", "\n", "path = os.path.join(os.getcwd(), \"sim_1\")\n", @@ -1265,21 +1268,20 @@ "source": [ "import math\n", "\n", - "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", "\n", "dt = time_opts.dt\n", "Tend = time_opts.Tend\n", "\n", "for i in range(Np):\n", - " r = np.sqrt(orbits[:, i, 0]**2 + orbits[:, i, 1]**2)\n", - " # poloidal \n", + " r = np.sqrt(orbits[:, i, 0] ** 2 + orbits[:, i, 1] ** 2)\n", + " # poloidal\n", " ax.scatter(r, orbits[:, i, 2], c=colors[i % 4], s=1)\n", " # top view\n", " ax_top.scatter(orbits[:, i, 0], orbits[:, i, 1], c=colors[i % 4], s=1)\n", - " \n", - "ax.set_title(f'{math.ceil(Tend/dt)} time steps')\n", - "ax_top.set_title(f'{math.ceil(Tend/dt)} time steps');\n", "\n", + "ax.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", + "ax_top.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", "fig" ] } diff --git a/tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb b/tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb index f50bfd8a7..05c465e08 100644 --- a/tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb +++ b/tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb @@ -50,24 +50,23 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", - "from struphy.geometry import domains\n", + "from struphy import main\n", "from struphy.fields_background import equils\n", - "from struphy.topology import grids\n", - "from struphy.io.options import DerhamOptions\n", - "from struphy.io.options import FieldsBackground\n", + "from struphy.geometry import domains\n", "from struphy.initial import perturbations\n", + "from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Time\n", "from struphy.kinetic_background import maxwellians\n", - "from struphy.pic.utilities import (LoadingParameters,\n", - " WeightsParameters,\n", - " BoundaryParameters,\n", - " BinningPlot,\n", - " KernelDensityPlot,\n", - " )\n", - "from struphy import main\n", "\n", "# import model, set verbosity\n", - "from struphy.models.toy import PressureLessSPH" + "from struphy.models.toy import PressureLessSPH\n", + "from struphy.pic.utilities import (\n", + " BinningPlot,\n", + " BoundaryParameters,\n", + " KernelDensityPlot,\n", + " LoadingParameters,\n", + " WeightsParameters,\n", + ")\n", + "from struphy.topology import grids" ] }, { @@ -95,12 +94,12 @@ "time_opts = Time(dt=0.02, Tend=4, split_algo=\"Strang\")\n", "\n", "# geometry\n", - "l1 = -.5\n", - "r1 = .5\n", - "l2 = -.5\n", - "r2 = .5\n", - "l3 = 0.\n", - "r3 = 1.\n", + "l1 = -0.5\n", + "r1 = 0.5\n", + "l2 = -0.5\n", + "r2 = 0.5\n", + "l3 = 0.0\n", + "r3 = 1.0\n", "domain = domains.Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" ] }, @@ -122,17 +121,20 @@ "# construct Beltrami flow\n", "import numpy as np\n", "\n", + "\n", "def u_fun(x, y, z):\n", - " ux = -np.cos(np.pi*x)*np.sin(np.pi*y)\n", - " uy = np.sin(np.pi*x)*np.cos(np.pi*y)\n", - " uz = 0 * x \n", + " ux = -np.cos(np.pi * x) * np.sin(np.pi * y)\n", + " uy = np.sin(np.pi * x) * np.cos(np.pi * y)\n", + " uz = 0 * x\n", " return ux, uy, uz\n", "\n", - "p_fun = lambda x, y, z: 0.5*(np.sin(np.pi*x)**2 + np.sin(np.pi*y)**2)\n", - "n_fun = lambda x, y, z: 1. + 0*x\n", + "\n", + "p_fun = lambda x, y, z: 0.5 * (np.sin(np.pi * x) ** 2 + np.sin(np.pi * y) ** 2)\n", + "n_fun = lambda x, y, z: 1.0 + 0 * x\n", "\n", "# put the functions in a generic equilibirum container\n", "from struphy.fields_background.generic import GenericCartesianFluidEquilibrium\n", + "\n", "bel_flow = GenericCartesianFluidEquilibrium(u_xyz=u_fun, p_xyz=p_fun, n_xyz=n_fun)" ] }, @@ -184,11 +186,12 @@ "\n", "loading_params = LoadingParameters(Np=1000)\n", "weights_params = WeightsParameters()\n", - "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", - "model.cold_fluid.set_markers(loading_params=loading_params,\n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params,\n", - " )\n", + "boundary_params = BoundaryParameters(bc=(\"reflect\", \"reflect\", \"periodic\"))\n", + "model.cold_fluid.set_markers(\n", + " loading_params=loading_params,\n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params,\n", + ")\n", "model.cold_fluid.set_sorting_boxes(boxes_per_dim=(1, 1, 1))\n", "model.cold_fluid.set_save_data(n_markers=1.0)" ] @@ -210,6 +213,7 @@ "source": [ "# propagator options\n", "from struphy.ode.utils import ButcherTableau\n", + "\n", "butcher = ButcherTableau(algo=\"forward_euler\")\n", "model.propagators.push_eta.options = model.propagators.push_eta.Options(butcher=butcher)\n", "\n", @@ -253,17 +257,18 @@ "source": [ "verbose = False\n", "\n", - "main.run(model,\n", - " params_path=None,\n", - " env=env,\n", - " base_units=base_units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -274,6 +279,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "path = os.path.join(os.getcwd(), \"sim_1\")\n", "\n", "main.pproc(path)" @@ -297,32 +303,32 @@ "outputs": [], "source": [ "from matplotlib import pyplot as plt\n", + "\n", "plt.figure(figsize=(12, 28))\n", "\n", "orbits = simdata.orbits[\"cold_fluid\"]\n", "\n", - "coloring = np.select([orbits[0, :, 0]<=-0.2, \n", - " np.abs(orbits[0, :, 0]) < +0.2, \n", - " orbits[0, :, 0] >= 0.2],\n", - " [-1.0, 0.0, +1.0])\n", + "coloring = np.select(\n", + " [orbits[0, :, 0] <= -0.2, np.abs(orbits[0, :, 0]) < +0.2, orbits[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0]\n", + ")\n", "\n", "dt = time_opts.dt\n", "Nt = simdata.t_grid.size - 1\n", - "interval = Nt/20\n", + "interval = Nt / 20\n", "plot_ct = 0\n", "for i in range(Nt):\n", " if i % interval == 0:\n", - " print(f'{i = }')\n", + " print(f\"{i = }\")\n", " plot_ct += 1\n", " plt.subplot(5, 2, plot_ct)\n", - " ax = plt.gca() \n", + " ax = plt.gca()\n", " plt.scatter(orbits[i, :, 0], orbits[i, :, 1], c=coloring)\n", - " plt.axis('square')\n", - " plt.title('n0_scatter')\n", + " plt.axis(\"square\")\n", + " plt.title(\"n0_scatter\")\n", " plt.xlim(l1, r1)\n", " plt.ylim(l2, r2)\n", " plt.colorbar()\n", - " plt.title(f'Gas at t={i*dt}')\n", + " plt.title(f\"Gas at t={i * dt}\")\n", " if plot_ct == 10:\n", " break" ] @@ -369,12 +375,10 @@ "\n", "loading_params = LoadingParameters(ppb=4, loading=\"tesselation\")\n", "weights_params = WeightsParameters()\n", - "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", - "model.cold_fluid.set_markers(loading_params=loading_params,\n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params,\n", - " bufsize=0.5\n", - " )\n", + "boundary_params = BoundaryParameters(bc=(\"reflect\", \"reflect\", \"periodic\"))\n", + "model.cold_fluid.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params, bufsize=0.5\n", + ")\n", "model.cold_fluid.set_sorting_boxes(boxes_per_dim=(16, 16, 1))\n", "model.cold_fluid.set_save_data(n_markers=1.0)" ] @@ -388,6 +392,7 @@ "source": [ "# propagator options\n", "from struphy.ode.utils import ButcherTableau\n", + "\n", "butcher = ButcherTableau(algo=\"forward_euler\")\n", "model.propagators.push_eta.options = model.propagators.push_eta.Options(butcher=butcher)\n", "\n", @@ -413,17 +418,18 @@ "metadata": {}, "outputs": [], "source": [ - "main.run(model,\n", - " params_path=None,\n", - " env=env,\n", - " base_units=base_units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -434,6 +440,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "path = os.path.join(os.getcwd(), \"sim_2\")\n", "\n", "main.pproc(path)" @@ -457,32 +464,32 @@ "outputs": [], "source": [ "from matplotlib import pyplot as plt\n", + "\n", "plt.figure(figsize=(12, 28))\n", "\n", "orbits = simdata.orbits[\"cold_fluid\"]\n", "\n", - "coloring = np.select([orbits[0, :, 0]<=-0.2, \n", - " np.abs(orbits[0, :, 0]) < +0.2, \n", - " orbits[0, :, 0] >= 0.2],\n", - " [-1.0, 0.0, +1.0])\n", + "coloring = np.select(\n", + " [orbits[0, :, 0] <= -0.2, np.abs(orbits[0, :, 0]) < +0.2, orbits[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0]\n", + ")\n", "\n", "dt = time_opts.dt\n", "Nt = simdata.t_grid.size - 1\n", - "interval = Nt/20\n", + "interval = Nt / 20\n", "plot_ct = 0\n", "for i in range(Nt):\n", " if i % interval == 0:\n", - " print(f'{i = }')\n", + " print(f\"{i = }\")\n", " plot_ct += 1\n", " plt.subplot(5, 2, plot_ct)\n", - " ax = plt.gca() \n", + " ax = plt.gca()\n", " plt.scatter(orbits[i, :, 0], orbits[i, :, 1], c=coloring)\n", - " plt.axis('square')\n", - " plt.title('n0_scatter')\n", + " plt.axis(\"square\")\n", + " plt.title(\"n0_scatter\")\n", " plt.xlim(l1, r1)\n", " plt.ylim(l2, r2)\n", " plt.colorbar()\n", - " plt.title(f'Gas at t={i*dt}')\n", + " plt.title(f\"Gas at t={i * dt}\")\n", " if plot_ct == 10:\n", " break" ] @@ -541,7 +548,7 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.pic.sph_smoothing_kernels import linear_uni, trigonometric_uni, gaussian_uni\n", + "from struphy.pic.sph_smoothing_kernels import gaussian_uni, linear_uni, trigonometric_uni\n", "\n", "x = np.linspace(-1, 1, 200)\n", "out1 = np.zeros_like(x)\n", @@ -549,13 +556,13 @@ "out3 = np.zeros_like(x)\n", "\n", "for i, xi in enumerate(x):\n", - " out1[i] = trigonometric_uni(xi, 1.)\n", - " out2[i] = gaussian_uni(xi, 1.)\n", - " out3[i] = linear_uni(xi, 1.)\n", + " out1[i] = trigonometric_uni(xi, 1.0)\n", + " out2[i] = gaussian_uni(xi, 1.0)\n", + " out3[i] = linear_uni(xi, 1.0)\n", "plt.plot(x, out1, label=\"trigonometric\")\n", "plt.plot(x, out2, label=\"gaussian\")\n", - "plt.plot(x, out3, label = \"linear\")\n", - "plt.title('Some smoothing kernels')\n", + "plt.plot(x, out3, label=\"linear\")\n", + "plt.title(\"Some smoothing kernels\")\n", "plt.legend()" ] }, @@ -574,24 +581,18 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", - "from struphy.geometry import domains\n", - "from struphy.fields_background import equils\n", - "from struphy.topology import grids\n", - "from struphy.io.options import DerhamOptions\n", - "from struphy.io.options import FieldsBackground\n", - "from struphy.initial import perturbations\n", - "from struphy.kinetic_background import maxwellians\n", - "from struphy.pic.utilities import (LoadingParameters,\n", - " WeightsParameters,\n", - " BoundaryParameters,\n", - " BinningPlot,\n", - " KernelDensityPlot,\n", - " )\n", "from struphy import main\n", + "from struphy.geometry import domains\n", + "from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, Time\n", "\n", "# import model, set verbosity\n", - "from struphy.models.fluid import EulerSPH" + "from struphy.models.fluid import EulerSPH\n", + "from struphy.pic.utilities import (\n", + " BoundaryParameters,\n", + " LoadingParameters,\n", + " WeightsParameters,\n", + ")\n", + "from struphy.topology import grids" ] }, { @@ -623,8 +624,8 @@ "r1 = 3.0\n", "l2 = -3.0\n", "r2 = 3.0\n", - "l3 = 0.\n", - "r3 = 1.\n", + "l3 = 0.0\n", + "r3 = 1.0\n", "domain = domains.Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" ] }, @@ -644,11 +645,13 @@ "outputs": [], "source": [ "# gaussian initial blob\n", - "from struphy.fields_background.generic import GenericCartesianFluidEquilibrium\n", "import numpy as np\n", + "\n", + "from struphy.fields_background.generic import GenericCartesianFluidEquilibrium\n", + "\n", "T_h = 0.2\n", - "gamma = 5/3\n", - "n_fun = lambda x, y, z: np.exp(-(x**2 + y**2)/T_h) / 35\n", + "gamma = 5 / 3\n", + "n_fun = lambda x, y, z: np.exp(-(x**2 + y**2) / T_h) / 35\n", "\n", "blob = GenericCartesianFluidEquilibrium(n_xyz=n_fun)" ] @@ -702,10 +705,11 @@ "loading_params = LoadingParameters(ppb=400)\n", "weights_params = WeightsParameters(reject_weights=True, threshold=3e-3)\n", "boundary_params = BoundaryParameters()\n", - "model.euler_fluid.set_markers(loading_params=loading_params,\n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params,\n", - " )\n", + "model.euler_fluid.set_markers(\n", + " loading_params=loading_params,\n", + " weights_params=weights_params,\n", + " boundary_params=boundary_params,\n", + ")\n", "nx = 16\n", "ny = 16\n", "model.euler_fluid.set_sorting_boxes(boxes_per_dim=(nx, ny, 1))" @@ -726,18 +730,20 @@ "metadata": {}, "outputs": [], "source": [ - "bin_plot = BinningPlot(slice=\"e1_e2\", \n", - " n_bins=(64, 64), \n", - " ranges=((0.0, 1.0), (0.0, 1.0)), \n", - " divide_by_jac=False,\n", - " )\n", + "bin_plot = BinningPlot(\n", + " slice=\"e1_e2\",\n", + " n_bins=(64, 64),\n", + " ranges=((0.0, 1.0), (0.0, 1.0)),\n", + " divide_by_jac=False,\n", + ")\n", "pts_e1 = 100\n", "pts_e2 = 90\n", "kd_plot = KernelDensityPlot(pts_e1=pts_e1, pts_e2=pts_e2, pts_e3=1)\n", - "model.euler_fluid.set_save_data(n_markers=1.0,\n", - " binning_plots=(bin_plot,),\n", - " kernel_density_plots=(kd_plot,),\n", - " )" + "model.euler_fluid.set_save_data(\n", + " n_markers=1.0,\n", + " binning_plots=(bin_plot,),\n", + " kernel_density_plots=(kd_plot,),\n", + ")" ] }, { @@ -757,6 +763,7 @@ "source": [ "# propagator options\n", "from struphy.ode.utils import ButcherTableau\n", + "\n", "butcher = ButcherTableau(algo=\"forward_euler\")\n", "model.propagators.push_eta.options = model.propagators.push_eta.Options(butcher=butcher)\n", "\n", @@ -791,17 +798,18 @@ "source": [ "verbose = True\n", "\n", - "main.run(model,\n", - " params_path=None,\n", - " env=env,\n", - " base_units=base_units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -850,7 +858,7 @@ "x = np.linspace(l1, r1, pts_e1)\n", "y = np.linspace(l2, r2, pts_e2)\n", "xx, yy = np.meshgrid(x, y, indexing=\"ij\")\n", - "ee1, ee2, ee3 = simdata.n_sph[\"euler_fluid\"][\"view_0\"][\"grid_n_sph\"]\n", + "ee1, ee2, ee3 = simdata.n_sph[\"euler_fluid\"][\"view_0\"][\"grid_n_sph\"]\n", "eta1 = ee1[:, 0, 0]\n", "eta2 = ee2[0, :, 0]\n", "bc_x = simdata.f[\"euler_fluid\"][\"e1_e2\"][\"grid_e1\"]\n", @@ -873,20 +881,21 @@ "metadata": {}, "outputs": [], "source": [ - "import matplotlib.pyplot as plt \n", + "import matplotlib.pyplot as plt\n", + "\n", "plt.figure(figsize=(12, 15))\n", "\n", "# plots\n", "plt.subplot(3, 2, 1)\n", "plt.pcolor(xx, yy, n_fun(xx, yy, 0))\n", - "plt.axis('square')\n", - "plt.title('n_xyz initial')\n", + "plt.axis(\"square\")\n", + "plt.title(\"n_xyz initial\")\n", "plt.colorbar()\n", "\n", "plt.subplot(3, 2, 2)\n", "plt.pcolor(eta1, eta2, n3(eta1, eta2, 0, squeeze_out=True).T)\n", - "plt.axis('square')\n", - "plt.title('$\\hat{n}^{\\t{vol}}$ initial (volume form)')\n", + "plt.axis(\"square\")\n", + "plt.title(\"$\\hat{n}^{\\t{vol}}$ initial (volume form)\")\n", "plt.colorbar()\n", "\n", "make_scatter = True\n", @@ -895,12 +904,12 @@ " ax = plt.gca()\n", " ax.set_xticks(np.linspace(l1, r1, nx + 1))\n", " ax.set_yticks(np.linspace(l2, r2, ny + 1))\n", - " plt.tick_params(labelbottom = False) \n", + " plt.tick_params(labelbottom=False)\n", " coloring = weights\n", - " plt.scatter(positions[:, 0], positions[:, 1], c=coloring, s=.25)\n", - " plt.grid(c='k')\n", - " plt.axis('square')\n", - " plt.title('$\\hat{n}^{\\t{vol}}$ initial scatter (random)')\n", + " plt.scatter(positions[:, 0], positions[:, 1], c=coloring, s=0.25)\n", + " plt.grid(c=\"k\")\n", + " plt.axis(\"square\")\n", + " plt.title(\"$\\hat{n}^{\\t{vol}}$ initial scatter (random)\")\n", " plt.xlim(l1, r1)\n", " plt.ylim(l2, r2)\n", " plt.colorbar()\n", @@ -908,19 +917,19 @@ "plt.subplot(3, 2, 4)\n", "ax = plt.gca()\n", "ax.set_xticks(np.linspace(0, 1, nx + 1))\n", - "ax.set_yticks(np.linspace(0, 1., ny + 1))\n", - "plt.tick_params(labelbottom = False) \n", - "plt.pcolor(ee1[:,:,0], ee2[:,:,0], n_sph[:,:,0])\n", + "ax.set_yticks(np.linspace(0, 1.0, ny + 1))\n", + "plt.tick_params(labelbottom=False)\n", + "plt.pcolor(ee1[:, :, 0], ee2[:, :, 0], n_sph[:, :, 0])\n", "plt.grid()\n", - "plt.axis('square')\n", - "plt.title('n_sph initial (random)')\n", + "plt.axis(\"square\")\n", + "plt.title(\"n_sph initial (random)\")\n", "plt.colorbar()\n", "\n", "plt.subplot(3, 2, 5)\n", "ax = plt.gca()\n", "plt.pcolor(bc_x, bc_y, f_bin)\n", - "plt.axis('square')\n", - "plt.title('n_binned initial (random)')\n", + "plt.axis(\"square\")\n", + "plt.title(\"n_binned initial (random)\")\n", "plt.colorbar()" ] }, @@ -936,24 +945,24 @@ "\n", "positions = orbits[:, :, :3]\n", "\n", - "interval = Nt/10\n", + "interval = Nt / 10\n", "plot_ct = 0\n", "\n", "plt.figure(figsize=(12, 24))\n", "for i in range(Nt):\n", " if i % interval == 0:\n", - " print(f'{i = }')\n", + " print(f\"{i = }\")\n", " plot_ct += 1\n", " plt.subplot(4, 2, plot_ct)\n", - " ax = plt.gca() \n", + " ax = plt.gca()\n", " coloring = weights\n", - " plt.scatter(positions[i, :, 0], positions[i, :, 1], c=coloring, s=.25)\n", - " plt.axis('square')\n", - " plt.title('n0_scatter')\n", + " plt.scatter(positions[i, :, 0], positions[i, :, 1], c=coloring, s=0.25)\n", + " plt.axis(\"square\")\n", + " plt.title(\"n0_scatter\")\n", " plt.xlim(l1, r1)\n", " plt.ylim(l2, r2)\n", " plt.colorbar()\n", - " plt.title(f'Gas at t={i*dt}')\n", + " plt.title(f\"Gas at t={i * dt}\")\n", " if plot_ct == 8:\n", " break" ] diff --git a/tutorials/tutorial_04_vlasov_maxwell.ipynb b/tutorials/tutorial_04_vlasov_maxwell.ipynb index f35f0443f..7fa3795b0 100644 --- a/tutorials/tutorial_04_vlasov_maxwell.ipynb +++ b/tutorials/tutorial_04_vlasov_maxwell.ipynb @@ -32,18 +32,15 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.io.options import EnvironmentOptions, BaseUnits, Time\n", - "from struphy.geometry import domains\n", + "from struphy import main\n", "from struphy.fields_background import equils\n", - "from struphy.topology import grids\n", - "from struphy.io.options import DerhamOptions\n", - "from struphy.io.options import FieldsBackground\n", + "from struphy.geometry import domains\n", "from struphy.initial import perturbations\n", + "from struphy.io.options import BaseUnits, DerhamOptions, EnvironmentOptions, FieldsBackground, Time\n", "from struphy.kinetic_background import maxwellians\n", - "from struphy.pic.utilities import LoadingParameters, WeightsParameters, BoundaryParameters, BinningPlot\n", - "from struphy import main\n", - "\n", - "from struphy.models.kinetic import VlasovAmpereOneSpecies" + "from struphy.models.kinetic import VlasovAmpereOneSpecies\n", + "from struphy.pic.utilities import BinningPlot, BoundaryParameters, LoadingParameters, WeightsParameters\n", + "from struphy.topology import grids" ] }, { @@ -66,7 +63,7 @@ "base_units = BaseUnits()\n", "\n", "# time stepping\n", - "time_opts = Time(dt = 0.05, Tend = 0.5)#, Tend = 3.5\n", + "time_opts = Time(dt=0.05, Tend=0.5) # , Tend = 3.5\n", "\n", "# geometry\n", "r1 = 12.56\n", @@ -116,7 +113,9 @@ "loading_params = LoadingParameters(ppc=10000)\n", "weights_params = WeightsParameters(control_variate=True)\n", "boundary_params = BoundaryParameters()\n", - "model.kinetic_ions.set_markers(loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params)\n", + "model.kinetic_ions.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", + ")\n", "model.kinetic_ions.set_sorting_boxes()" ] }, @@ -215,17 +214,18 @@ "source": [ "verbose = True\n", "\n", - "main.run(model, \n", - " params_path=None, \n", - " env=env, \n", - " base_units=base_units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " base_units=base_units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -262,8 +262,8 @@ "f_v1_init = simdata.f[\"kinetic_ions\"][\"v1\"][\"f_binned\"][0]\n", "\n", "plt.plot(v1_bins, f_v1_init)\n", - "plt.xlabel('vx')\n", - "plt.title('Initial Maxwellian');" + "plt.xlabel(\"vx\")\n", + "plt.title(\"Initial Maxwellian\");" ] }, { @@ -278,8 +278,8 @@ "df_e1_init = simdata.f[\"kinetic_ions\"][\"e1\"][\"delta_f_binned\"][0]\n", "\n", "plt.plot(e1_bins, df_e1_init)\n", - "plt.xlabel('$\\eta_1$')\n", - "plt.title('Initial spatial perturbation');" + "plt.xlabel(\"$\\eta_1$\")\n", + "plt.title(\"Initial spatial perturbation\");" ] }, { @@ -301,30 +301,30 @@ "\n", "plt.subplot(2, 2, 1)\n", "plt.pcolor(e1_bins, v1_bins, f_init.T)\n", - "plt.xlabel('$\\eta_1$')\n", - "plt.ylabel('$v_x$')\n", - "plt.title('Initial Maxwellian')\n", + "plt.xlabel(\"$\\eta_1$\")\n", + "plt.ylabel(\"$v_x$\")\n", + "plt.title(\"Initial Maxwellian\")\n", "plt.colorbar()\n", "\n", "plt.subplot(2, 2, 2)\n", "plt.pcolor(e1_bins, v1_bins, df_init.T)\n", - "plt.xlabel('$\\eta_1$')\n", - "plt.ylabel('$v_x$')\n", - "plt.title('Initial perturbation')\n", + "plt.xlabel(\"$\\eta_1$\")\n", + "plt.ylabel(\"$v_x$\")\n", + "plt.title(\"Initial perturbation\")\n", "plt.colorbar()\n", "\n", "plt.subplot(2, 2, 3)\n", "plt.pcolor(e1_bins, v1_bins, f_end.T)\n", - "plt.xlabel('$\\eta_1$')\n", - "plt.ylabel('$v_x$')\n", - "plt.title('Final Maxwellian')\n", + "plt.xlabel(\"$\\eta_1$\")\n", + "plt.ylabel(\"$v_x$\")\n", + "plt.title(\"Final Maxwellian\")\n", "plt.colorbar()\n", "\n", "plt.subplot(2, 2, 4)\n", "plt.pcolor(e1_bins, v1_bins, df_end.T)\n", - "plt.xlabel('$\\eta_1$')\n", - "plt.ylabel('$v_x$')\n", - "plt.title('Final perturbation')\n", + "plt.xlabel(\"$\\eta_1$\")\n", + "plt.ylabel(\"$v_x$\")\n", + "plt.title(\"Final perturbation\")\n", "plt.colorbar();" ] }, @@ -336,12 +336,12 @@ "source": [ "# electric field\n", "\n", - "e1, e2, e3 = simdata.grids_log \n", + "e1, e2, e3 = simdata.grids_log\n", "e_vals = simdata.spline_values[\"em_fields\"][\"e_field_log\"][0][0]\n", "\n", - "plt.plot(e1, e_vals[:, 0, 0], label='E')\n", - "plt.xlabel('$\\eta_1$')\n", - "plt.title('Initial electric field')\n", + "plt.plot(e1, e_vals[:, 0, 0], label=\"E\")\n", + "plt.xlabel(\"$\\eta_1$\")\n", + "plt.title(\"Initial electric field\")\n", "plt.legend();" ] } diff --git a/tutorials_old/tutorial_01_parameter_files.ipynb b/tutorials_old/tutorial_01_parameter_files.ipynb index e803e3746..8c2ce8ced 100644 --- a/tutorials_old/tutorial_01_parameter_files.ipynb +++ b/tutorials_old/tutorial_01_parameter_files.ipynb @@ -44,19 +44,18 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.io.options import EnvironmentOptions, Units, Time\n", - "from struphy.geometry import domains\n", + "from struphy import main\n", "from struphy.fields_background import equils\n", - "from struphy.topology import grids\n", - "from struphy.io.options import DerhamOptions\n", - "from struphy.io.options import FieldsBackground\n", + "from struphy.geometry import domains\n", "from struphy.initial import perturbations\n", + "from struphy.io.options import DerhamOptions, EnvironmentOptions, FieldsBackground, Time, Units\n", "from struphy.kinetic_background import maxwellians\n", - "from struphy.pic.utilities import LoadingParameters, WeightsParameters, BoundaryParameters\n", - "from struphy import main\n", "\n", "# import model, set verbosity\n", "from struphy.models.toy import Vlasov as Model\n", + "from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters\n", + "from struphy.topology import grids\n", + "\n", "verbose = True" ] }, @@ -151,10 +150,10 @@ "\n", "loading_params = LoadingParameters(Np=15)\n", "weights_params = WeightsParameters()\n", - "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", - "model.kinetic_ions.set_markers(loading_params=loading_params, \n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params)\n", + "boundary_params = BoundaryParameters(bc=(\"reflect\", \"reflect\", \"periodic\"))\n", + "model.kinetic_ions.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", + ")\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)" ] @@ -222,17 +221,18 @@ "metadata": {}, "outputs": [], "source": [ - "main.run(model, \n", - " params_path=None, \n", - " env=env, \n", - " units=units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " units=units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -253,6 +253,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "path = os.path.join(os.getcwd(), \"sim_1\")\n", "\n", "main.pproc(path, physical=True)" @@ -330,27 +331,27 @@ "fig = plt.figure()\n", "ax = fig.gca()\n", "\n", - "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", "\n", - "time = 0.\n", + "time = 0.0\n", "dt = time_opts.dt\n", "Tend = time_opts.Tend\n", "for k, v in simdata.pic_species[\"kinetic_ions\"][\"orbits\"].items():\n", " # print(f\"{v[0] = }\")\n", - " alpha = (Tend - time)/Tend\n", + " alpha = (Tend - time) / Tend\n", " for i, particle in enumerate(v):\n", " ax.scatter(particle[0], particle[1], c=colors[i % 4], alpha=alpha)\n", " time += dt\n", - " \n", - "ax.plot([l1, l1], [l2, r2], 'k')\n", - "ax.plot([r1, r1], [l2, r2], 'k')\n", - "ax.plot([l1, r1], [l2, l2], 'k')\n", - "ax.plot([l1, r1], [r2, r2], 'k')\n", - "ax.set_xlabel('x')\n", - "ax.set_ylabel('y')\n", + "\n", + "ax.plot([l1, l1], [l2, r2], \"k\")\n", + "ax.plot([r1, r1], [l2, r2], \"k\")\n", + "ax.plot([l1, r1], [l2, l2], \"k\")\n", + "ax.plot([l1, r1], [r2, r2], \"k\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", "ax.set_xlim(-6.5, 6.5)\n", "ax.set_ylim(-9, 9)\n", - "ax.set_title(f'{int(Tend/dt)} time steps (full color at t=0)');" + "ax.set_title(f\"{int(Tend / dt)} time steps (full color at t=0)\");" ] } ], diff --git a/tutorials_old/tutorial_01_particles.ipynb b/tutorials_old/tutorial_01_particles.ipynb index dc4bc05b5..b72b1ce94 100644 --- a/tutorials_old/tutorial_01_particles.ipynb +++ b/tutorials_old/tutorial_01_particles.ipynb @@ -43,19 +43,18 @@ "metadata": {}, "outputs": [], "source": [ - "from struphy.io.options import EnvironmentOptions, Units, Time\n", - "from struphy.geometry import domains\n", + "from struphy import main\n", "from struphy.fields_background import equils\n", - "from struphy.topology import grids\n", - "from struphy.io.options import DerhamOptions\n", - "from struphy.io.options import FieldsBackground\n", + "from struphy.geometry import domains\n", "from struphy.initial import perturbations\n", + "from struphy.io.options import DerhamOptions, EnvironmentOptions, FieldsBackground, Time, Units\n", "from struphy.kinetic_background import maxwellians\n", - "from struphy.pic.utilities import LoadingParameters, WeightsParameters, BoundaryParameters\n", - "from struphy import main\n", "\n", "# import model, set verbosity\n", "from struphy.models.toy import Vlasov as Model\n", + "from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters\n", + "from struphy.topology import grids\n", + "\n", "verbose = True" ] }, @@ -109,10 +108,10 @@ "\n", "loading_params = LoadingParameters(Np=15)\n", "weights_params = WeightsParameters()\n", - "boundary_params = BoundaryParameters(bc=('reflect', 'reflect', 'periodic'))\n", - "model.kinetic_ions.set_markers(loading_params=loading_params, \n", - " weights_params=weights_params,\n", - " boundary_params=boundary_params)\n", + "boundary_params = BoundaryParameters(bc=(\"reflect\", \"reflect\", \"periodic\"))\n", + "model.kinetic_ions.set_markers(\n", + " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", + ")\n", "model.kinetic_ions.set_sorting_boxes()\n", "model.kinetic_ions.set_save_data(n_markers=1.0)" ] @@ -150,17 +149,18 @@ "metadata": {}, "outputs": [], "source": [ - "main.run(model, \n", - " params_path=None, \n", - " env=env, \n", - " units=units, \n", - " time_opts=time_opts, \n", - " domain=domain, \n", - " equil=equil, \n", - " grid=grid, \n", - " derham_opts=derham_opts, \n", - " verbose=verbose, \n", - " )" + "main.run(\n", + " model,\n", + " params_path=None,\n", + " env=env,\n", + " units=units,\n", + " time_opts=time_opts,\n", + " domain=domain,\n", + " equil=equil,\n", + " grid=grid,\n", + " derham_opts=derham_opts,\n", + " verbose=verbose,\n", + ")" ] }, { @@ -225,27 +225,27 @@ "fig = plt.figure()\n", "ax = fig.gca()\n", "\n", - "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", "\n", - "time = 0.\n", + "time = 0.0\n", "dt = time_opts.dt\n", "Tend = time_opts.Tend\n", "for k, v in simdata.pic_species[\"kinetic_ions\"][\"orbits\"].items():\n", " # print(k, v)\n", - " alpha = (Tend - time)/Tend\n", + " alpha = (Tend - time) / Tend\n", " for i, particle in enumerate(v):\n", " ax.scatter(particle[1], particle[2], c=colors[i % 4], alpha=alpha)\n", " time += dt\n", - " \n", - "ax.plot([l1, l1], [l2, r2], 'k')\n", - "ax.plot([r1, r1], [l2, r2], 'k')\n", - "ax.plot([l1, r1], [l2, l2], 'k')\n", - "ax.plot([l1, r1], [r2, r2], 'k')\n", - "ax.set_xlabel('x')\n", - "ax.set_ylabel('y')\n", + "\n", + "ax.plot([l1, l1], [l2, r2], \"k\")\n", + "ax.plot([r1, r1], [l2, r2], \"k\")\n", + "ax.plot([l1, r1], [l2, l2], \"k\")\n", + "ax.plot([l1, r1], [r2, r2], \"k\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", "ax.set_xlim(-6.5, 6.5)\n", "ax.set_ylim(-9, 9)\n", - "ax.set_title(f'{int(Tend/dt)} time steps (full color at t=0)');" + "ax.set_title(f\"{int(Tend / dt)} time steps (full color at t=0)\");" ] } ], diff --git a/tutorials_old/tutorial_02_fluid_particles.ipynb b/tutorials_old/tutorial_02_fluid_particles.ipynb index ea5831daa..7c22c1195 100644 --- a/tutorials_old/tutorial_02_fluid_particles.ipynb +++ b/tutorials_old/tutorial_02_fluid_particles.ipynb @@ -813,7 +813,7 @@ "plt.tick_params(labelbottom=False)\n", "plt.plot(eta1, n_sph_init[:, 0, 0])\n", "plt.grid()\n", - "plt.title('n_sph_init')\n", + "plt.title(\"n_sph_init\")\n", "\n", "plt.subplot(2, 2, 4)\n", "ax = plt.gca()\n", @@ -822,8 +822,8 @@ "plt.tick_params(labelbottom=False)\n", "bc_x = (be_x[:-1] + be_x[1:]) / 2.0 # centers of binning cells\n", "plt.plot(bc_x, df_bin.T)\n", - "#plt.grid()\n", - "plt.title('n_binned')" + "# plt.grid()\n", + "plt.title(\"n_binned\")" ] }, { @@ -1243,8 +1243,8 @@ "plt.tick_params(labelbottom=False)\n", "plt.pcolor(ee1[:, :, 0], ee2[:, :, 0], n_sph_1[:, :, 0])\n", "plt.grid()\n", - "plt.axis('square')\n", - "plt.title('n_sph (random)')\n", + "plt.axis(\"square\")\n", + "plt.title(\"n_sph (random)\")\n", "plt.colorbar()\n", "\n", "plt.subplot(4, 2, 6)\n", @@ -1254,8 +1254,8 @@ "plt.tick_params(labelbottom=False)\n", "plt.pcolor(ee1[:, :, 0], ee2[:, :, 0], n_sph_2[:, :, 0])\n", "plt.grid()\n", - "plt.axis('square')\n", - "plt.title('n_sph (tesselation)')\n", + "plt.axis(\"square\")\n", + "plt.title(\"n_sph (tesselation)\")\n", "plt.colorbar()\n", "\n", "plt.subplot(4, 2, 7)\n", @@ -1266,9 +1266,9 @@ "bc_x = (be_x[:-1] + be_x[1:]) / 2.0 # centers of binning cells\n", "bc_y = (be_y[:-1] + be_y[1:]) / 2.0\n", "plt.pcolor(bc_x, bc_y, f_bin_1)\n", - "#plt.grid()\n", - "plt.axis('square')\n", - "plt.title('n_binned (random)')\n", + "# plt.grid()\n", + "plt.axis(\"square\")\n", + "plt.title(\"n_binned (random)\")\n", "plt.colorbar()\n", "\n", "plt.subplot(4, 2, 8)\n", @@ -1279,9 +1279,9 @@ "bc_x = (be_x[:-1] + be_x[1:]) / 2.0 # centers of binning cells\n", "bc_y = (be_y[:-1] + be_y[1:]) / 2.0\n", "plt.pcolor(bc_x, bc_y, f_bin_2)\n", - "#plt.grid()\n", - "plt.axis('square')\n", - "plt.title('n_binned (tesselation)')\n", + "# plt.grid()\n", + "plt.axis(\"square\")\n", + "plt.title(\"n_binned (tesselation)\")\n", "plt.colorbar()" ] }, diff --git a/utils/set_release_dependencies.py b/utils/set_release_dependencies.py index fb6556547..08a54bba5 100644 --- a/utils/set_release_dependencies.py +++ b/utils/set_release_dependencies.py @@ -2,8 +2,6 @@ import re import tomllib -import tomli_w - def get_min_bound(entry): match = re.search(r"(>=|==|~=|>|>)\s*([\w\.\-]+)", entry) From 3bdceb60146b57419701afc2adec0ccf94dbf474 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 30 Oct 2025 17:51:10 +0100 Subject: [PATCH 14/83] Update `devel-clean` to `devel` state (#87) These were the changes that remained after redoing all the PRs --- .github/workflows/ubuntu-latest.yml | 19 +++++-------------- utils/set_release_dependencies.py | 1 - 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ubuntu-latest.yml b/.github/workflows/ubuntu-latest.yml index e66aeb3b3..8fd73272e 100644 --- a/.github/workflows/ubuntu-latest.yml +++ b/.github/workflows/ubuntu-latest.yml @@ -1,20 +1,11 @@ -name: Ubuntu +name: Ubuntu latest - cronjob on: - push: - branches: - - main - - devel - pull_request: - branches: - - main - - devel - -# concurrency: -# group: ${{ github.ref }} -# cancel-in-progress: true + schedule: + # run at 1 a.m. on Sunday + - cron: "0 1 * * 0" jobs: ubuntu-latest-build: - uses: ./.github/workflows/testing.yml + uses: ./.github/workflows/reusable-testing.yml with: os: ubuntu-latest \ No newline at end of file diff --git a/utils/set_release_dependencies.py b/utils/set_release_dependencies.py index 08a54bba5..914410dfb 100644 --- a/utils/set_release_dependencies.py +++ b/utils/set_release_dependencies.py @@ -1,6 +1,5 @@ import importlib.metadata import re -import tomllib def get_min_bound(entry): From ca97e0ac8ac0bbcd5e7a539bc92daca4ccd874f4 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 30 Oct 2025 17:55:18 +0100 Subject: [PATCH 15/83] Removed `.github/workflows/testing.yml` (#88) --- .github/workflows/testing.yml | 95 ----------------------------------- 1 file changed, 95 deletions(-) delete mode 100644 .github/workflows/testing.yml diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml deleted file mode 100644 index c27af6914..000000000 --- a/.github/workflows/testing.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: Testing - -on: - workflow_call: - inputs: - os: - required: true - type: string - -jobs: - test: - runs-on: ${{ inputs.os }} - env: - OMPI_MCA_rmaps_base_oversubscribe: 1 # Linux - PRRTE_MCA_rmaps_base_oversubscribe: 1 # MacOS - strategy: - fail-fast: false - matrix: - python-version: ["3.12"] - compile-language: ["fortran", "c"] - test-type: ["unit", "model", "quickstart", "tutorials"] - - steps: - # Checkout the repository - - name: Checkout code - uses: actions/checkout@v5 - - # https://docs.github.com/en/actions/tutorials/build-and-test-code/python - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - # You can test your matrix by printing the current Python version - - name: Display Python version - run: python -c "import sys; print(sys.version)" - - # Cache pip dependencies - - name: Cache pip - uses: actions/cache@v3 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }} - restore-keys: | - ${{ runner.os }}-pip- - - # Install prereqs - # I don't think it's possible to use a single action for this because - # we can't use ${inputs.os} in an if statement, so we have to use two different actions. - - name: Install prerequisites (Ubuntu) - if: inputs.os == 'ubuntu-latest' - uses: ./.github/actions/install/ubuntu-latest - - - name: Install prerequisites (macOS) - if: inputs.os == 'macos-latest' - uses: ./.github/actions/install/macos-latest - - # Check that mpirun oversubscribing works, doesn't work unless OMPI_MCA_rmaps_base_oversubscribe==1 - - name: Test mpirun - run: | - echo $OMPI_MCA_rmaps_base_oversubscribe - echo $PRRTE_MCA_rmaps_base_oversubscribe - pip install mpi4py -U - which mpirun - mpirun --version - mpirun --oversubscribe --report-bindings -n 4 python -c "from mpi4py import MPI; comm=MPI.COMM_WORLD; print(f'Hello from rank {comm.Get_rank()} of {comm.Get_size()}'); assert comm.Get_size()==4" - - # Clone struphy-ci-testing - - name: Install struphy - uses: ./.github/actions/install/install-struphy - env: - FC: ${{ env.FC }} - CC: ${{ env.CC }} - CXX: ${{ env.CXX }} - - # Compile - - name: Compile kernels - uses: ./.github/actions/compile - - # Run tests - - name: Run unit tests - if: matrix.test-type == 'unit' - uses: ./.github/actions/tests/unit - - - name: Run model tests - if: matrix.test-type == 'model' - uses: ./.github/actions/tests/models - - - name: Run quickstart tests - if: matrix.test-type == 'quickstart' - uses: ./.github/actions/tests/quickstart - - - name: Run tutorials - if: matrix.test-type == 'tutorials' - uses: ./.github/actions/tests/tutorials From 77dd95de529d9efec52d270256a228d3fe160638 Mon Sep 17 00:00:00 2001 From: Stefan Possanner <86720346+spossann@users.noreply.github.com> Date: Wed, 5 Nov 2025 10:44:53 +0100 Subject: [PATCH 16/83] Clean: Deploy doc to Github pages (#92) This PR replaces #52 Closes https://github.com/struphy-hub/struphy/issues/32 and closes #15 --- .../install/install-struphy/action.yml | 7 +- .github/workflows/docs.yml | 49 +- .github/workflows/hello_world_reusable_wf.yml | 13 + .github/workflows/macos-latest.yml | 34 +- .github/workflows/publish.yml | 2 +- .github/workflows/reusable-testing.yml | 2 +- .github/workflows/static_analysis.yml | 1 - .github/workflows/test-PR-models.yml | 2 +- .github/workflows/test-PR-unit.yml | 15 +- README.md | 102 +- apt.txt | 4 + doc/api/1d_matrices.ipynb | 359 ----- doc/api/disp_rels.ipynb | 122 -- doc/api/stability.ipynb | 103 -- doc/conf.py | 30 +- doc/gallery/gallery_frontpage_bk.png | Bin 0 -> 1472832 bytes doc/gallery/gallery_step_1496.png | Bin 0 -> 102882 bytes doc/gallery/gallery_struphy_heat.png | Bin 0 -> 205996 bytes doc/gallery/gallery_struphy_tracer6D.png | Bin 0 -> 238347 bytes doc/index.rst | 49 +- doc/pics/MPI_PP_Logo_Wide_E_green_rgb.png | Bin 0 -> 30340 bytes doc/pics/struphy_header_with_subs.png | Bin 0 -> 31169 bytes doc/sections/abstract.rst | 35 +- doc/sections/install.rst | 353 +---- pyproject.toml | 1 + src/struphy/geometry/tests/test_domain.py | 2 +- .../tests/test_maxwellians.py | 14 +- tutorials/tutorial_01_parameter_files.ipynb | 416 ------ tutorials/tutorial_02_test_particles.ipynb | 1310 ----------------- ...l_03_smoothed_particle_hydrodynamics.ipynb | 992 ------------- tutorials/tutorial_04_vlasov_maxwell.ipynb | 370 ----- tutorials/tutorial_05_mapped_domains.ipynb | 434 ------ tutorials/tutorial_06_mhd_equilibria.ipynb | 153 -- 33 files changed, 278 insertions(+), 4696 deletions(-) create mode 100644 .github/workflows/hello_world_reusable_wf.yml create mode 100644 apt.txt delete mode 100644 doc/api/1d_matrices.ipynb delete mode 100644 doc/api/disp_rels.ipynb delete mode 100644 doc/api/stability.ipynb create mode 100644 doc/gallery/gallery_frontpage_bk.png create mode 100644 doc/gallery/gallery_step_1496.png create mode 100644 doc/gallery/gallery_struphy_heat.png create mode 100644 doc/gallery/gallery_struphy_tracer6D.png create mode 100644 doc/pics/MPI_PP_Logo_Wide_E_green_rgb.png create mode 100644 doc/pics/struphy_header_with_subs.png delete mode 100644 tutorials/tutorial_01_parameter_files.ipynb delete mode 100644 tutorials/tutorial_02_test_particles.ipynb delete mode 100644 tutorials/tutorial_03_smoothed_particle_hydrodynamics.ipynb delete mode 100644 tutorials/tutorial_04_vlasov_maxwell.ipynb delete mode 100644 tutorials/tutorial_05_mapped_domains.ipynb delete mode 100644 tutorials/tutorial_06_mhd_equilibria.ipynb diff --git a/.github/actions/install/install-struphy/action.yml b/.github/actions/install/install-struphy/action.yml index a9589c4c4..319fff78a 100644 --- a/.github/actions/install/install-struphy/action.yml +++ b/.github/actions/install/install-struphy/action.yml @@ -1,5 +1,10 @@ name: "Clone and install struphy" +inputs: + optional-deps: + required: true + default: 'dev' + runs: using: composite steps: @@ -8,7 +13,7 @@ runs: shell: bash run: | pip install --upgrade pip - pip install ".[phys,mpi,doc]" + pip install ".[${{ inputs.optional-deps }}]" pip list struphy -h struphy --refresh-models diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b4827d372..62830810d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,7 +2,10 @@ name: Deploy docs to GitHub Pages on: push: - branches: ["devel", "main"] + branches: + - main + - devel + workflow_dispatch: defaults: @@ -13,6 +16,7 @@ permissions: contents: read pages: write id-token: write + packages: read concurrency: group: "pages" @@ -21,17 +25,15 @@ concurrency: jobs: build-and-deploy: runs-on: ubuntu-latest - container: - image: texlive/texlive:latest environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" @@ -45,31 +47,30 @@ jobs: - name: Install pandoc run: | - apt-get update - apt-get install -y pandoc + sudo apt update -y + sudo apt install -y pandoc - # - name: Install TeX Live with pdflatex - # run: | - # sudo apt-get update - # sudo apt-get install -y texlive-full - # sudo apt-get install -y texlive texlive-latex-base texlive-latex-extra texlive-fonts-recommended texlive-science + - name: Install prerequisites (Ubuntu) + uses: ./.github/actions/install/ubuntu-latest + env: + FC: ${{ env.FC }} + CC: ${{ env.CC }} + CXX: ${{ env.CXX }} - - name: Check if pdflatex is available - run: | - which pdflatex - pdflatex --version + - name: Install Struphy (dev-doc) + uses: ./.github/actions/install/install-struphy + with: + optional-deps: 'dev,doc' - - name: Set up virtual environment and install project + - name: Compile Struphy + shell: bash run: | - python -m venv env - source env/bin/activate - pip install --upgrade pip - pip install ".[test, dev, docs]" + struphy compile --status + struphy compile -y - name: Build Sphinx docs run: | - source env/bin/activate - cd docs + cd doc make html - name: Setup Pages @@ -78,7 +79,7 @@ jobs: - name: Upload built docs uses: actions/upload-pages-artifact@v3 with: - path: docs/build/html/ + path: doc/_build/html/ - name: Deploy to GitHub Pages id: deployment diff --git a/.github/workflows/hello_world_reusable_wf.yml b/.github/workflows/hello_world_reusable_wf.yml new file mode 100644 index 000000000..bd0b92d7b --- /dev/null +++ b/.github/workflows/hello_world_reusable_wf.yml @@ -0,0 +1,13 @@ +on: + workflow_call: + inputs: + os: + required: true + type: string + +jobs: + hello_world: + runs-on: ${{ inputs.os }} + steps: + - name: Say hello + run: echo "Hello world!" \ No newline at end of file diff --git a/.github/workflows/macos-latest.yml b/.github/workflows/macos-latest.yml index 2b5a1b0e5..9031e5bdc 100644 --- a/.github/workflows/macos-latest.yml +++ b/.github/workflows/macos-latest.yml @@ -1,20 +1,20 @@ # name: MacOS -# on: -# push: -# branches: -# - main -# - devel -# pull_request: -# branches: -# - main -# - devel +on: + push: + branches: + - main + - devel + pull_request: + branches: + - main + - devel -# # concurrency: -# # group: ${{ github.ref }} -# # cancel-in-progress: true +# concurrency: +# group: ${{ github.ref }} +# cancel-in-progress: true -# jobs: -# macos-latest-build: -# uses: ./.github/workflows/testing.yml -# with: -# os: macos-latest \ No newline at end of file +jobs: + macos-latest-build: + uses: ./.github/workflows/hello_world_reusable_wf.yml + with: + os: macos-latest \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 02f9fc5ea..7b7b8166e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,4 @@ -name: Publish Python Package +name: Publish Struphy to PyPI on: push: diff --git a/.github/workflows/reusable-testing.yml b/.github/workflows/reusable-testing.yml index c27af6914..37cd5209f 100644 --- a/.github/workflows/reusable-testing.yml +++ b/.github/workflows/reusable-testing.yml @@ -1,4 +1,4 @@ -name: Testing +name: Reusable workflow for testing on: workflow_call: diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 3022a70c2..fbe56d20c 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -89,7 +89,6 @@ jobs: run: | pip install isort isort --check src/ - isort --check tutorials/ # mypy: # runs-on: ubuntu-latest diff --git a/.github/workflows/test-PR-models.yml b/.github/workflows/test-PR-models.yml index 9dfeb3ebc..148da8f59 100644 --- a/.github/workflows/test-PR-models.yml +++ b/.github/workflows/test-PR-models.yml @@ -57,7 +57,7 @@ jobs: struphy test models struphy test verification - - name: Model tests with MPI + - name: Model tests with MPI shell: bash run: | which python3 diff --git a/.github/workflows/test-PR-unit.yml b/.github/workflows/test-PR-unit.yml index 1f6df8cf4..9a58a421b 100644 --- a/.github/workflows/test-PR-unit.yml +++ b/.github/workflows/test-PR-unit.yml @@ -45,7 +45,7 @@ jobs: struphy compile --status struphy compile - - name: Run unit tests with MPI + - name: Run unit tests shell: bash run: | which python3 @@ -53,9 +53,11 @@ jobs: which python3 struphy compile --status struphy --refresh-models - struphy test unit --mpi 2 + pip show mpi4py + pip uninstall -y mpi4py + struphy test unit - - name: Run unit tests + - name: Run unit tests with MPI shell: bash run: | which python3 @@ -63,6 +65,7 @@ jobs: which python3 struphy compile --status struphy --refresh-models - pip show mpi4py - pip uninstall -y mpi4py - struphy test unit + pip install -U mpi4py + struphy test unit --mpi 2 + + diff --git a/README.md b/README.md index 62263ed54..7d70bdd55 100755 --- a/README.md +++ b/README.md @@ -13,56 +13,100 @@ https://pypi.org/project/struphy/) # Welcome! -Struphy is a Python package for plasma physics PDEs. +**This is a Python package for solving partial differential equations (PDEs) mainly - but not exclusively - for plasma physics.** -Join the [Struphy mailing list](https://listserv.gwdg.de/mailman/listinfo/struphy) and stay informed on updates. +**STRUPHY** stands for **STRU**cture-**P**reserving **HY**brid code (or **STRU**cture-preserving **PHY**sics code). The package provides off-the-shelf models for plasma physics problems, such as + +* Maxwell's equations +* Magneto-hydrodynamics (MHD) +* Vlasov-Poisson and Vlasov-Maxwell kinetic models +* Drift-kinetic models for strongly magnetized plasma +* MHD-kinetic hybrid models + +All models can be run on multiple cores through MPI (distributed memory) and OpenMP (shared memory). The compute-intensive parts of the code are translated and compiled ("transpiled") using [pyccel](https://github.com/pyccel/pyccel), giving you the speed of Fortran or C while working within the familiar Python environment. + +Particles in a Tokamak
(model "Vlasov") | Toroidal Alfvén eigenmode
(model "LinearMHDDriftKineticCC") +:-------------------------:|:-------------------------: +![](/doc/gallery/gallery_struphy_tracer6D.png) | ![](/doc/gallery/gallery_frontpage_bk.png) +**Strong Landau damping
(model "VlasovAmpereOneSpecies")** | **Anisotropic diffusion
(propagator "ImplicitDiffusion")** +![](/doc/gallery/gallery_step_1496.png) | ![](/doc/gallery/gallery_struphy_heat.png) + + +The code is freely available under an [MIT license](https://github.com/struphy-hub/struphy/blob/devel/LICENSE) - Copyright (c) 2019-2025, Struphy developers, Max Planck Institute for Plasma Physics. + +

+ +

+ +## Tutorials + +Get familiar with Struphy right away on [mybinder](https://mybinder.org/v2/gh/struphy-hub/struphy-tutorials/main) - no installation needed. -## Documentation -See the [Struphy pages](https://struphy.pages.mpcdf.de/struphy/index.html) for details regarding installation, tutorials, use, and development. ## Quick install -Use a virtual environment: +Quick install on your computer (using a virtual environment): + +``` +python -m venv struphy_env +source struphy_env/bin/activate +pip install -U pip +pip install -U struphy +struphy compile --status +struphy compile +``` - python3 -m pip install --upgrade virtualenv - python3 -m venv struphy_env - source struphy_env/bin/activate +In case you face troubles with install/compile: -Install latest release: +1. check the [prerequisites](https://struphy-hub.github.io/struphy/sections/install.html#requirements) +2. visit [trouble shooting](https://struphy-hub.github.io/struphy/sections/install.html#trouble-shooting) - pip install --no-cache-dir --upgrade struphy -Compile kernels: +## Quick run - struphy compile +As an example, let's say we want to solve Maxwell's equations. We can use the CLI and generate a default launch file via -Quick help: +``` +struphy params Maxwell +``` +Hit yes when prompted - this will create the file `params_Maxwell.py` in your current working directory (cwd). You can open the file and - if you feel like it already - change some parameters, then run - struphy -h +``` +python params_Maxwell.py +``` -In case of problems visit [Trouble shooting](https://struphy.pages.mpcdf.de/struphy/sections/install.html#trouble-shooting). +The default output is in `sim_1/` in your cwd. You can change the output path via the class `EnvironmentOptions` in the parameter file. -## Run tests from the command-line +Parallel simulations are run for example with -Run available verification tests for [Struphy models](https://struphy.pages.mpcdf.de/struphy/sections/models.html): +``` +pip install -U mpi4py +mpirun -n 4 python params_Maxwell.py +``` - struphy test models --verification --fast --show-plots +You can also put the run command in a batch script. -The corresponding parameter files are in [struphy/io/inp/verification/](https://gitlab.mpcdf.mpg.de/struphy/struphy/-/tree/devel/src/struphy/io/inp/verification). -The corresponding diagnostics functions are in [struphy/models/tests/verification.py](https://gitlab.mpcdf.mpg.de/struphy/struphy/-/blob/devel/src/struphy/models/tests/verification.py). You can repeat the verification run of a single `` by typing - struphy test --verification --fast --show-plots +## Documentation -## Tutorial notebooks +The doc is on [Github pages](https://struphy-hub.github.io/struphy/index.html), we recommend in particular to visit: -Struphy tutorials are available in the form of [Jupyter notebooks](https://gitlab.mpcdf.mpg.de/struphy/struphy/-/tree/devel/doc/tutorials). +* [Install](https://struphy-hub.github.io/struphy/sections/install.html) +* [Userguide](https://struphy-hub.github.io/struphy/sections/userguide.html) +* [Available models](https://struphy-hub.github.io/struphy/sections/models.html) +* [Numerical methods](https://struphy-hub.github.io/struphy/sections/numerics.html) -## Reference paper -* S. Possanner, F. Holderied, Y. Li, B.-K. Na, D. Bell, S. Hadjout and Y. Güçlü, [**High-Order Structure-Preserving Algorithms for Plasma Hybrid Models**](https://link.springer.com/chapter/10.1007/978-3-031-38299-4_28), International Conference on Geometric Science of Information 2023, 263-271, Springer Nature Switzerland. +## Get in touch -## Contact +* [Issues](https://github.com/struphy-hub/struphy/issues) +* [Discussions](https://github.com/struphy-hub/struphy/discussions) +* @spossann [stefan.possanner@ipp.mpg.de](mailto:spossann@ipp.mpg.de) (Maintainer) +* @max-models [Max.Lindqvist@ipp.mpg.de](mailto:Max.Lindqvist@ipp.mpg.de) (Maintainer) +* [LinkedIn profile](https://www.linkedin.com/company/struphy/) + + +## Citing Struphy + +* S. Possanner, F. Holderied, Y. Li, B.-K. Na, D. Bell, S. Hadjout and Y. Güçlü, [**High-Order Structure-Preserving Algorithms for Plasma Hybrid Models**](https://link.springer.com/chapter/10.1007/978-3-031-38299-4_28), International Conference on Geometric Science of Information 2023, 263-271, Springer Nature Switzerland. -* Stefan Possanner [stefan.possanner@ipp.mpg.de](mailto:spossann@ipp.mpg.de) -* Eric Sonnendrücker [eric.sonnendruecker@ipp.mpg.de](mailto:eric.sonnendruecker@ipp.mpg.de) -* Xin Wang [xin.wang@ipp.mpg.de](mailto:xin.wang@ipp.mpg.de) diff --git a/apt.txt b/apt.txt new file mode 100644 index 000000000..7abccc7e4 --- /dev/null +++ b/apt.txt @@ -0,0 +1,4 @@ +gfortran +gcc +liblapack-dev +libblas-dev \ No newline at end of file diff --git a/doc/api/1d_matrices.ipynb b/doc/api/1d_matrices.ipynb deleted file mode 100644 index f81f5fde0..000000000 --- a/doc/api/1d_matrices.ipynb +++ /dev/null @@ -1,359 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 1d FEM spaces: mass-, inter- and histopolation matrices" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "from psydac.core.bsplines import collocation_matrix, histopolation_matrix\n", - "from psydac.ddm.cart import DomainDecomposition\n", - "from psydac.fem.tensor import TensorFemSpace\n", - "\n", - "from struphy.feec.mass import WeightedMassOperator\n", - "from struphy.feec.psydac_derham import Derham" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# instance of Derham\n", - "Nel = [12, 12, 12] # Number of grid cells\n", - "p = [3, 4, 5] # spline degrees\n", - "spl_kind = [True, True, True] # Spline types (clamped vs. periodic)\n", - "\n", - "derham = Derham(Nel, p, spl_kind)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 1d spline spaces: attributes and point sets" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# 1d fem spaces\n", - "\n", - "V0_fem = derham.Vh_fem[\"0\"].spaces\n", - "V3_fem = derham.Vh_fem[\"3\"].spaces\n", - "\n", - "for l, (V0_1d, V3_1d) in enumerate(zip(V0_fem, V3_fem)):\n", - " print(f\"Direction {l + 1}\")\n", - "\n", - " print(\"\\nH1 p: \", V0_1d.degree)\n", - " print(\"L2 p: \", V3_1d.degree)\n", - "\n", - " print(\"\\nH1 knots: \", V0_1d.knots)\n", - " print(\"L2 knots: \", V3_1d.knots)\n", - "\n", - " print(\"\\nH1 basis: \", V0_1d.basis)\n", - " print(\"L2 basis: \", V3_1d.basis)\n", - "\n", - " print(\"\\nH1 nbasis: \", V0_1d.nbasis)\n", - " print(\"L2 nbasis: \", V3_1d.nbasis)\n", - "\n", - " print(\"\\nH1 breaks: \", V0_1d.breaks)\n", - " print(\"L2 breaks: \", V3_1d.breaks)\n", - "\n", - " print(\"\\nH1 greville: \", V0_1d.greville)\n", - " print(\"L2 greville: \", V3_1d.greville)\n", - "\n", - " print(\"\\nH1 ext_greville: \", V0_1d.ext_greville)\n", - " print(\"L2 ext_greville: \", V3_1d.ext_greville)\n", - "\n", - " print(\"\\n---------------------------------------\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 1d mass matrices\n", - "\n", - "$$\n", - "\\mathbb M^0_{ij} := \\int_{\\hat \\Omega} \\Lambda^0_i\\, \\Lambda^0_j \\sqrt g\\,\\textnormal d\\eta\n", - "$$\n", - "\n", - "$$\n", - "\\mathbb M^1_{ij} := \\int_{\\hat \\Omega} \\Lambda^1_i\\, \\Lambda^1_j \\frac{1}{\\sqrt g}\\,\\textnormal d\\eta\n", - "$$\n", - "\n", - "$$\n", - "\\mathbb M^{1\\times 0}_{a^0, ij} := \\int_{\\hat \\Omega} a^0 \\Lambda^1_i \\Lambda^0_j\\,\\textnormal d \\eta\n", - "$$\n", - "\n", - "For the moment, below all weights are equal to 1." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# 1d mass matrices in H1 (no weight)\n", - "mass_H1_1d = []\n", - "for femspace_1d in V0_fem:\n", - " domain_decompos_1d = DomainDecomposition([femspace_1d.ncells], [femspace_1d.periodic])\n", - " femspace_1d_tensor = TensorFemSpace(domain_decompos_1d, femspace_1d)\n", - "\n", - " M = WeightedMassOperator(derham, femspace_1d_tensor, femspace_1d_tensor, nquads=[femspace_1d.degree])\n", - " M.assemble(verbose=False)\n", - " M.matrix.exchange_assembly_data()\n", - "\n", - " mass_H1_1d += [M.matrix.toarray()]\n", - "\n", - "# 1d mass matrices in L2 (no weight)\n", - "mass_L2_1d = []\n", - "for femspace_1d in V3_fem:\n", - " domain_decompos_1d = DomainDecomposition([femspace_1d.ncells], [femspace_1d.periodic])\n", - " femspace_1d_tensor = TensorFemSpace(domain_decompos_1d, femspace_1d)\n", - "\n", - " M = WeightedMassOperator(derham, femspace_1d_tensor, femspace_1d_tensor, nquads=[femspace_1d.degree])\n", - " M.assemble(verbose=False)\n", - " M.matrix.exchange_assembly_data()\n", - "\n", - " mass_L2_1d += [M.matrix.toarray()]\n", - "\n", - "# 1d mixed mass matrices: V0 -> V3\n", - "mass_mixed_1d = []\n", - "for V0_1d, V3_1d in zip(V0_fem, V3_fem):\n", - " domain_decompos_1d = DomainDecomposition([V0_1d.ncells], [V0_1d.periodic])\n", - " V0_femspace = TensorFemSpace(domain_decompos_1d, V0_1d)\n", - " V3_femspace = TensorFemSpace(domain_decompos_1d, V3_1d)\n", - "\n", - " M = WeightedMassOperator(derham, V0_femspace, V3_femspace, nquads=[V0_1d.degree])\n", - " M.assemble(verbose=False)\n", - " M.matrix.exchange_assembly_data()\n", - "\n", - " mass_mixed_1d += [M.matrix.toarray()]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(\"Sorted eigenvalues of H1 mass matrices in 1d:\")\n", - "for deg, M in zip(p, mass_H1_1d):\n", - " print(f\"\\np={deg}:\\n\", np.sort(np.linalg.eigvals(M)))\n", - "\n", - "print(\"\\nSorted eigenvalues of L2 mass matrices in 1d:\")\n", - "for deg, M in zip(p, mass_L2_1d):\n", - " print(f\"\\np={deg}:\\n\", np.sort(np.linalg.eigvals(M)))\n", - "\n", - "print(\"\\nSorted eigenvalues (abs) of mixed mass matrices in 1d:\")\n", - "for deg, M in zip(p, mass_mixed_1d):\n", - " print(f\"\\np={deg}:\\n\", np.sort(np.abs(np.linalg.eigvals(M))))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(\"First row of circulant H1 mass matrices in 1d:\")\n", - "for deg, M in zip(p, mass_H1_1d):\n", - " print(f\"\\np={deg}:\\n\", M[0])\n", - "\n", - "print(\"\\nFirst row of circulant L2 mass matrices in 1d:\")\n", - "for deg, M in zip(p, mass_L2_1d):\n", - " print(f\"\\np={deg}:\\n\", M[0])\n", - "\n", - "print(\"\\nFirst row of circulant mixed mass matrices in 1d:\")\n", - "for deg, M in zip(p, mass_mixed_1d):\n", - " print(f\"\\np={deg}:\\n\", M[0])" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Standard collocation and histopolation matrices\n", - "\n", - "$$\n", - "\\mathcal C^0_{ij} := \\sigma^0_i(\\Lambda^0_j)\\,, \\qquad \\mathcal H^1_{ij} := \\sigma^1_i(\\Lambda^1_j)\\,.\n", - "$$" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# 1d Inter-/histopolation matrices\n", - "\n", - "# Commuting projectors\n", - "P0 = derham.P[\"0\"]\n", - "P3 = derham.P[\"3\"]\n", - "\n", - "# 1d collocation matrices\n", - "colloc_H1_1d = []\n", - "for mat in P0._imat.mats:\n", - " colloc_H1_1d += [mat.toarray()]\n", - "\n", - "# 1d histopolation matrices\n", - "histop_L2_1d = []\n", - "for mat in P3._imat.mats:\n", - " histop_L2_1d += [mat.toarray()]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(\"Sorted eigenvalues of H1 collocation matrices in 1d:\")\n", - "for deg, M in zip(p, colloc_H1_1d):\n", - " print(f\"\\np={deg}:\\n\", np.sort(np.abs(np.linalg.eigvals(M))))\n", - "\n", - "print(\"\\nSorted eigenvalues of L2 histopolation matrices in 1d:\")\n", - "for deg, M in zip(p, histop_L2_1d):\n", - " print(f\"\\np={deg}:\\n\", np.sort(np.abs(np.linalg.eigvals(M))))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(\"First row of circulant H1 collocation matrices in 1d:\")\n", - "for deg, M in zip(p, colloc_H1_1d):\n", - " print(f\"\\np={deg}:\\n\", M[0])\n", - "\n", - "print(\"\\nFirst row of circulant L2 histopolation matrices in 1d:\")\n", - "for deg, M in zip(p, histop_L2_1d):\n", - " print(f\"\\np={deg}:\\n\", M[0])" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Opposite space collocation and histopolation matrices\n", - "\n", - "$$\n", - "\\mathcal C^1_{ij} := \\sigma^0_i(\\Lambda^1_j)\\,, \\qquad \\mathcal H^0_{ij} := \\sigma^1_i(\\Lambda^0_j)\\,.\n", - "$$" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# 1d Inter-/histopolation matrices of opposite spaces\n", - "\n", - "# histopolation in H1\n", - "histop_H1_1d = []\n", - "for femspace_1d in V0_fem:\n", - " hmat = histopolation_matrix(\n", - " knots=femspace_1d.knots,\n", - " degree=femspace_1d.degree,\n", - " periodic=femspace_1d.periodic,\n", - " normalization=femspace_1d.basis,\n", - " xgrid=femspace_1d.greville,\n", - " )\n", - "\n", - " histop_H1_1d += [hmat]\n", - "\n", - "# interpolation in L2\n", - "colloc_L2_1d = []\n", - "for femspace_1d in V3_fem:\n", - " imat = collocation_matrix(\n", - " knots=femspace_1d.knots,\n", - " degree=femspace_1d.degree,\n", - " periodic=femspace_1d.periodic,\n", - " normalization=femspace_1d.basis,\n", - " xgrid=femspace_1d.ext_greville,\n", - " )\n", - "\n", - " colloc_L2_1d += [imat]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(\"Sorted eigenvalues of H1 histopolation matrices in 1d:\")\n", - "for deg, M in zip(p, histop_H1_1d):\n", - " print(f\"\\np={deg}:\\n\", np.sort(np.abs(np.linalg.eigvals(M))))\n", - "\n", - "print(\"\\nSorted eigenvalues of L2 collocation matrices in 1d:\")\n", - "for deg, M in zip(p, colloc_L2_1d):\n", - " print(f\"\\np={deg}:\\n\", np.sort(np.abs(np.linalg.eigvals(M))))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(\"First row of circulant H1 histopolation matrices in 1d:\")\n", - "for deg, M in zip(p, histop_H1_1d):\n", - " print(f\"\\np={deg}:\\n\", M[0])\n", - "\n", - "print(\"\\nFirst row of circulant L2 collocation matrices in 1d:\")\n", - "for deg, M in zip(p, colloc_L2_1d):\n", - " print(f\"\\np={deg}:\\n\", M[0])" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - }, - "vscode": { - "interpreter": { - "hash": "9db6cd2b68760e57326699bbcbb4001f1217ba4632a3f0b45cb7a85cd53c814d" - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/doc/api/disp_rels.ipynb b/doc/api/disp_rels.ipynb deleted file mode 100644 index a1987e86e..000000000 --- a/doc/api/disp_rels.ipynb +++ /dev/null @@ -1,122 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# API: Dispersion relations\n", - "\n", - "This API provides access to 1D dispersion relations available in Struphy." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "from struphy.dispersion_relations import analytic\n", - "\n", - "k = np.linspace(0, 0.3, 100)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Maxwell" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "disp_rel = analytic.Maxwell1D(c=2.0)\n", - "disp_rel.plot(k)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## MHD homogeneous slab" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "disp_rel = analytic.MHDhomogenSlab()\n", - "disp_rel.plot(k)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Hall MHD homogeneous slab" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "disp_rel = analytic.ExtendedMHDhomogenSlab(eps=1.0)\n", - "disp_rel.plot(k)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Fluid ITG in slab" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "disp_rel = analytic.FluidSlabITG(vstar=0.1)\n", - "disp_rel.plot(k)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "env", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/doc/api/stability.ipynb b/doc/api/stability.ipynb deleted file mode 100644 index 9df11aad2..000000000 --- a/doc/api/stability.ipynb +++ /dev/null @@ -1,103 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "from matplotlib import pyplot as plt" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The stability function is defined as a rational function $R(z) = p(z)/q(z)$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# add polynomial coeffs of methods here\n", - "methods = {}\n", - "methods[\"explicit Euler\"] = {\"p\": [1, 1], \"q\": [0, 1]}\n", - "methods[\"explicit RK 2\"] = {\"p\": [1 / 2, 1, 1], \"q\": [0, 0, 1]}\n", - "methods[\"explicit RK 3\"] = {\"p\": [1 / 6, 1 / 2, 1, 1], \"q\": [0, 0, 0, 1]}\n", - "methods[\"explicit RK 4\"] = {\"p\": [1 / 24, 1 / 6, 1 / 2, 1, 1], \"q\": [0, 0, 0, 0, 1]}\n", - "methods[\"implicit Euler\"] = {\"p\": [0, 1], \"q\": [-1, 1]}\n", - "methods[\"Crank Nicolson\"] = {\"p\": [1 / 2, 1], \"q\": [-1 / 2, 1]}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# plotting parameters\n", - "N = 400\n", - "bound = 3.0\n", - "x = np.linspace(-bound, bound, N)\n", - "y = np.linspace(-bound, bound, N)\n", - "xx, yy = np.meshgrid(x, y, indexing=\"ij\")\n", - "zz = xx + yy * 1j" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# plot stability region for all methods\n", - "for key, val in methods.items():\n", - " p = np.poly1d(val[\"p\"])\n", - " q = np.poly1d(val[\"q\"])\n", - "\n", - " rr = np.abs(p(zz) / q(zz))\n", - " rr[rr > 1.0] = -1\n", - "\n", - " fig = plt.figure(figsize=(5, 5))\n", - " plt.contourf(xx, yy, rr, [0, 1], colors=\"b\")\n", - " plt.plot([-bound, bound], [0, 0], \"--k\")\n", - " plt.plot([0, 0], [-bound, bound], \"--k\")\n", - " plt.title(key)\n", - " plt.xlabel(\"Re($z$)\")\n", - " plt.ylabel(\"Im($z$)\")\n", - " plt.show()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3.8.10 ('env': venv)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - }, - "orig_nbformat": 4, - "vscode": { - "interpreter": { - "hash": "9db6cd2b68760e57326699bbcbb4001f1217ba4632a3f0b45cb7a85cd53c814d" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/doc/conf.py b/doc/conf.py index d5e8a3265..b99285648 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -9,28 +9,6 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -# -import os -import shutil - -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -def copy_tutorials(app): - src = os.path.abspath("../tutorials") - dst = os.path.abspath("source/tutorials") - - # Remove existing target directory if it exists - if os.path.exists(dst): - shutil.rmtree(dst) - - shutil.copytree(src, dst) - - -def setup(app): - app.connect("builder-inited", copy_tutorials) - with open("../src/struphy/console/main.py") as f: exec(f.read()) @@ -62,6 +40,7 @@ def setup(app): "sphinx.ext.graphviz", "myst_parser", "sphinx_design", + "sphinx_copybutton", ] nbsphinx_execute = "auto" @@ -98,9 +77,12 @@ def setup(app): "header_links_before_dropdown": 8, "primary_sidebar_end": ["sidebar-ethical-ads"], "external_links": [ - {"name": "Struphy repo", "url": "https://gitlab.mpcdf.mpg.de/struphy/struphy"}, + {"name": "Struphy repo", "url": "https://github.com/struphy-hub/struphy"}, {"name": "Struphy LinkedIn", "url": "https://www.linkedin.com/company/struphy/"}, - {"name": "Struphy RocketChat", "url": "https://chat.gwdg.de/channel/struphy-developers"}, + { + "name": "Struphy MatrixChat", + "url": "https://matrix.to/#/!wqjcJpsUvAbTPOUXen:mpg.de?via=mpg.de&via=academiccloud.de", + }, ], } diff --git a/doc/gallery/gallery_frontpage_bk.png b/doc/gallery/gallery_frontpage_bk.png new file mode 100644 index 0000000000000000000000000000000000000000..e20c4ce41202cd4dd4826fe73c15059a4e1d4a2b GIT binary patch literal 1472832 zcmZU(1y~$Svo^eJAS@8v-2(&*?yxun3liL&;4JPC+%32T4T0c+-~=bQTae%oB#S%W z@I3E1=e_=auWOiQW~yhpx~uNHsygzmvMf3(2`UH#LYJ45QUigWM5L_3u+WA)bJRWYx zG6F^gxEf!8&fMEIGC#ZrCom5t_affb_0iMU$>jus(gP9B0(p(7(p(A(x)4~N9j*?w zWPwy0=EzcX9}ga@q!k#G&_K`vDrMZCNKIIjh#*HCISVfkR&fGfUM9o4^%<-`1Nk4Y zw3wX3Q2CggGf|E9>LR0mOWYxXY6bJyhd|Im(vu2b@#oW@x~+qfr5ID82go%l4oBwH zJuvEqAYLcsX0^PWix0|utK3u1d9Lqb#4DnTs}i2|xY%F>+&pH)JsP;c@Sef6yT9Zz zp+&0py*Ry~^KnVXDVMwxPa?oZso@1NvJqS?FmrIJW8+lS%Gx8kuniOI+XTiX?)048 zToX}?QN1vgjF_Sq!2W&D8@jhnRg6ANsrzztqgT8F+4-X!1<{ftOoCUy(s(6nkCUx5 zFJLt7{5pU7v1A}_=kpoIp(1vb5haXeAVwi19n~4y&QA?CMUSA}T&OX*=Ve05_!WUb znn5E&9F5e&nMgNpR2=`YWGLA5=FnsNC$ee3vU!vj_Wjj+LreXLS6cJ92pwYqL};_D zNlxp?u2Lt?zn|4Wq!EI?%sbc0;ZT;ZJB!%l2E@E)abOsU+;8l^mlpW(X63~?znA|( z=lq@UcY${|o{8M-uavhK40C%iW7V!d93%Y>L<57+7>$*lC5sbaHM-Au{1AGlg0=RH zcmPDRHa_+uM(*HGVy7(&BPc+E4t(;%`N6M^CIR!hN>ZDifnrlb3prtBwNzv9xeE0t zk_i>^3iC}0-3JP~K!ybD$8K8D3NVo*M#ayEfHBPKRbmYDu3O^X&KQ=PKzfb@BIpb1 z=h*hn&we3ac6I-r!PK^uInYwx|Y<7bSI&MzH6WKM+qn8=r*TrCo*OUI3z%oWs&0nuyW`1_58{Hkw%zK){o zwcM3M7eb#C$G4~q7>@rrSO3EohU^nII(AQeD={C2{;h|=1i2bEFMj}@4VigH;7(@S zsasw6M)>?HPr72M_tQP2!??Ue=&Sz zutGw{v7Q&ir&Fayh^)s{V^h*$p~#vh(9z)!Lg^yF8Ii-XZxTMr3DhE$$y`u}CngUd z6`?nM5S9{5oEu=klblf{pN4#skS(xF`4INWc^8rUSxzuPKH7M(HL-5wk;I?z>DAFNnUG81fUBp)` zZdfbWky0-wGIufZdg)9FoS!?RI6rg7gd^6(UZu}V(j=bAU(h+K(M78Kq{UC1O>|F; zORh{3l%pJYZ;o5bB@mIVn3{k#SiG6DX|`FuX|*|qrxZosn~WomtyZF*s@kAR!j#5D zo-(Gv`h{mEi$r#$V6-q*)way)^W|s9>FMd=Y2oRDnV$EsnV-`aUvjj3K3}LB=q)Sf zl;;*yzBT&NE_okvLw?T3O)?U-EvrznHIX{`#irC6)0&c-h$N0g82wWpSuAR-U~D-F z`yO61eU4zTQv0^u2iRY)6Mzk z`dLNx#pIt^rw#Ml-rc5Lm_6@%ZRJD^?(^;A?IV^6$58Fp4KGbDRxha2D}7h0r=>S1 z%_r`W=@5FPy6pb4uio?|U0~Q^_QB{p)m(>TAn81@6saQ5ytPwbzU}u>r>ftN? z&s6tT_Ycf7&da^8AKgt+P2-rFALAeL{08UPP{s|5jE|f*|2F8HET-Meg25dAu1|1e zpO=Rlo#S1)1BX4&y`7~!fup8%L;pd$z)ZG%u0#7`>q6^NSC45$uVOSw^ePKes@;&{ z(6`hbmQ_6+J$hZ%`Y(;%4qvTC>e#9)Yuc?O>)_uqOx0~*%c<5w4I>T9#y-6m-(cT{ z>dUKJ?BW+&7NQUN<{M+FV*V0Q6RQfWr+#RD)=$0qsqgnE+9{4-HRCla@GZIM#6eoE z7-7DXtmJa_xGCP!mcn}3(70D*GyMDC>T>Ejo4T%VtQU9lCtxysG5%Yo$22YryDm!~ ze+$mKeX_0lqs8aJm!bDnue2r9=gY14rJfzRJDv~9Z7cGR?p3om+2np%6}~ayvHRQr zF9A;rpTje6(LV1O>){5D__mSO>lMA$B|mM^GSMu*ML#9K>-LZB@pr@bA^uDExz2V1 zI@t!^XmEPlWN)D?!RmQ_SNjpkJpscZrD6*)xmz8y@IQXHaQ4`+U&n`!pqz!IgzSf0 zMj}Kq<1Y2blJVL8DyI8D=d32l$s|DS<>NAT)d{!l^C-=2+gQY8F$4`4{pGfz zuwbNUq}U$x6*Uiso7Fy{KV3)CYI2`~UfwM08tN>nA>=-!JT!;KapKKHo$K-0$GtJG zRxV8oSIZmAR7>~Brgay1c|}5nlFo4*ZJmeto*wL&d@DFdlWE^m|4 zdv?~O1{HXAt>v3W7R3gGxB-R%(i7LUc>m}w8F=!#NxO=_^h0NqjG!^BVe4EQLD4-zFnW)u|?6{^Mu<~uy9gDHsZ{uZ7 z7QdISBTq>QiCEKH*5E2X#Afhp@CKtCSkH97+hDiLwSIPk@Am#72w)~4Fk*3j#l(7D zD^;V{I@#=w@w($UoAAwBcv>qjJJ$|{nUFAhAmZt(p^+m~OP z-tMb4+pxNQHMRHA@zm)SjCLEE^J5xg=}`S`RAbfl`=N7NZzpt#cAK_;_a~Oi?+)(GjP~2JZo4n|D2*vUi&%R(-Bb5{S{E#G z@HX)9v-IESMYZx0I$4{{^VZNy0tZ}(pYc_3$7I#j;&c6ba@Gkgc-Z$7k zlAL**OX9JAv$s3-)v9YWTa488Vp?FdX=A5tjL+b2tJ4+Dt?4~|qkNI z#$et4&gfW{;0M8~JC29=IKoN7yq2hk=^NO+LRK2`D#v5&q-Kl4KQ%ACT=ZpQ+A zcFP{7qA~Wtqv=?*gZ&}@@zXx-?M9PIWL~ac?oH@f@j?0FT%V)K>e2c3#l@RzPXE;^ z(DM}8ELV`>F(@z<5%esVv~1(uPIr6o=h1pU$Q?t4pyUNUAL#vWF+W$ryOt)T&J}x* zwz!`+DoB+Zv@Q%1^gc5t=N~%H5&9Sk8GD}ftfOSBBq%^#!)lW9j$#p9`~{6@@c|9{ z>H~r`WmX0dT|`-E%UdcbfnEV^2nZ2@1cU^%5P(Azf%JddG6;;IXa8IWgFxXnAjJPX zqYRv%ehI+wB=hh2SyC7X8TbzeI6U*g|MT>7y!>bX(?((e?t#SBCFSLTv$~m!g@psm z+R;_uko5$(fZ`;l0|S8w=${S*c{RGfK>zbL8rrVfN{WJJj`nOO=8mQoY@YT`PyK*| zJq3ZLy@jg@m8ZR(15D6Ugyx?o1cCO`Wp)~>e;#qQ6`|2qdP^ng=wd;|%l4Y>HH|1L z6&01Ri@Bwsnv~3c#DV`rXslgbodnt0Jv={NLj$lTx^_NZ5$n_p87R0b#!wTp`m$t(f|Jb{Z0!{oBw;01MEM; z0tU$bbcdaT?KS)V_6>*%KV21kYvXBQrz2%!59kbdhbRXZC%5oF0{_2T|M$xO64m}M zQ63)N{}%nPTmN6tcQ6YVNk@C&rLLm?H(~z~{_mUr5fo;B8vB3g;$KDoa~06ED5@~~ z|IV5yYT%!TWMCr6Y@}2)fHSbno_^p|;1A=!XP}M3QjD5j5&!~0LGn`K8lDIT%X>+5 z3(n-mYAv6U(vNgrlVLp%j@}bfD!B}JF(~zh4&jBR{aYdWHz@({{Jb4xWi>4TviN6CP{?;PLrIfQ(KyO#QbUqaPJG^1v+^3P@dD@^<-; zH3;s;Y?^BU+|{tbtAGyjkw+EyDn1_%m|Z-ui@p+||c(AaNp~R*Ng~_H*OojgE*n={vme04!1w||ONp0{LyZ-w<_g|y z26_Y#k55sZZTU`wn}qkpOqz($uyQ!V^#lw4--m5huJsZb<9J9ldIf)kpBxTP-F&!e zpshCv0@6$e+k+~H(Wya=bUNKEkG(DkY}%EgPNgG9x^P(XHffD1y_%*Btr+}iZynMT zZvFG_%lv%30b)|&sK{^i{_MwTcxfE@tT7_a*ɉ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�c3e&fimQ6Lp>+PHZ(7Q2KWV=J?|#+0jfia( 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 z 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%0AMcc1c^ySmIh{ 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 =PBb7FutGU-AnSg!0_wpIK#nrket$> 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 z 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

O+Q=0V;#%cE*vv?8ya`-Gl0k{-{)sG( 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:WgPO@oDD+ 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


+[![Release](https://img.shields.io/github/v/release/struphy-hub/struphy?label=Release)](https://github.com/struphy-hub/struphy/releases) +[![License](https://img.shields.io/badge/License-MIT-violet)](https://github.com/struphy-hub/struphy/blob/devel/LICENSE) +[![badge](https://img.shields.io/badge/launch-tutorials-579ACA.svg?logo=data:image/png;base64,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)](https://mybinder.org/v2/gh/struphy-hub/struphy-tutorials/main) [![Ubuntu latest](https://github.com/struphy-hub/struphy/actions/workflows/ubuntu-latest.yml/badge.svg)](https://github.com/struphy-hub/struphy/actions/workflows/ubuntu-latest.yml) [![MacOS latest](https://github.com/struphy-hub/struphy/actions/workflows/macos-latest.yml/badge.svg)](https://github.com/struphy-hub/struphy/actions/workflows/macos-latest.yml) [![isort and ruff](https://github.com/struphy-hub/struphy/actions/workflows/static_analysis.yml/badge.svg)](https://github.com/struphy-hub/struphy/actions/workflows/static_analysis.yml) [![PyPI](https://img.shields.io/pypi/v/struphy?label=PyPI)](https://pypi.org/project/struphy/) [![PyPI Downloads](https://img.shields.io/pypi/dm/struphy.svg?label=PyPI%20downloads)]( https://pypi.org/project/struphy/) -[![Release](https://img.shields.io/github/v/release/struphy-hub/struphy?label=Release)](https://github.com/struphy-hub/struphy/releases) -[![License](https://img.shields.io/badge/License-MIT-violet)](https://github.com/struphy-hub/struphy/blob/devel/LICENSE) + + # Welcome! @@ -27,9 +30,9 @@ All models can be run on multiple cores through MPI (distributed memory) and Ope Particles in a Tokamak
(model "Vlasov") | Toroidal Alfvén eigenmode
(model "LinearMHDDriftKineticCC") :-------------------------:|:-------------------------: -![](/doc/gallery/gallery_struphy_tracer6D.png) | ![](/doc/gallery/gallery_frontpage_bk.png) +![](https://raw.githubusercontent.com/struphy-hub/.github/refs/heads/main/profile/gallery_struphy_tracer6D.png) | ![](https://raw.githubusercontent.com/struphy-hub/.github/refs/heads/main/profile/gallery_frontpage_bk.png) **Strong Landau damping
(model "VlasovAmpereOneSpecies")** | **Anisotropic diffusion
(propagator "ImplicitDiffusion")** -![](/doc/gallery/gallery_step_1496.png) | ![](/doc/gallery/gallery_struphy_heat.png) +![](https://raw.githubusercontent.com/struphy-hub/.github/refs/heads/main/profile/gallery_step_1496.png) | ![](https://raw.githubusercontent.com/struphy-hub/.github/refs/heads/main/profile/gallery_struphy_heat.png) The code is freely available under an [MIT license](https://github.com/struphy-hub/struphy/blob/devel/LICENSE) - Copyright (c) 2019-2025, Struphy developers, Max Planck Institute for Plasma Physics. diff --git a/doc/index.rst b/doc/index.rst index 07719c72f..84aefc0c7 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -30,12 +30,14 @@ The code is freely available under an `MIT license 0`) are admitted. :maxdepth: 2 :caption: Contents: - subsections/domains_avail - subsections/domains_base - subsections/domains_kernels - subsections/domains_utils + subsections/domains-avail + subsections/domains-base + subsections/domains-kernels + subsections/domains-utils diff --git a/doc/sections/equilibria.rst b/doc/sections/fluid-equils.rst similarity index 57% rename from doc/sections/equilibria.rst rename to doc/sections/fluid-equils.rst index cac73838d..57fc3d245 100644 --- a/doc/sections/equilibria.rst +++ b/doc/sections/fluid-equils.rst @@ -1,9 +1,9 @@ .. _equilibria: -Equilibria -========== +Fluid equilibrium +================= -Fluid/kinetic equilibria (or backgrounds) are often the starting point of dynamical plasma simulations. +Fluid equilibria (or backgrounds) are often the starting point of dynamical plasma simulations. In Struphy they can be used for setting :ref:`initial_conditions`, or for providing the background in ":math:`\delta f`-models", where solutions are computed w.r.t a given background. @@ -11,5 +11,5 @@ In Struphy they can be used for setting :ref:`initial_conditions`, or for provid :maxdepth: 2 :caption: Contents: - subsections/mhd_equils - subsections/kinetic_backgrounds + subsections/equils-avail + subsections/equils-base \ No newline at end of file diff --git a/doc/sections/kinetic-equils.rst b/doc/sections/kinetic-equils.rst new file mode 100644 index 000000000..2bb742402 --- /dev/null +++ b/doc/sections/kinetic-equils.rst @@ -0,0 +1,15 @@ +.. _equilibria: + +Kinetic Equilibrium +=================== + +Kinetic equilibria (or backgrounds) are often the starting point of dynamical plasma simulations. +In Struphy they can be used for setting :ref:`initial_conditions`, or for providing the background in +":math:`\delta f`-models", where solutions are computed w.r.t a given background. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + subsections/kinetic-bckgr-avail + subsections/kinetic-bckgr-base \ No newline at end of file diff --git a/doc/sections/models.rst b/doc/sections/models.rst index 6002461ab..fe97e944d 100644 --- a/doc/sections/models.rst +++ b/doc/sections/models.rst @@ -15,12 +15,12 @@ To add a new model, please visit :ref:`add_model`. :maxdepth: 2 :caption: Contents: - subsections/normalization - subsections/models_fluid - subsections/models_kinetic - subsections/models_hybrid - subsections/models_toy - subsections/model_base_class + subsections/models-normalization + subsections/models-fluid + subsections/models-kinetic + subsections/models-hybrid + subsections/models-toy + subsections/models-base diff --git a/doc/sections/numerics.rst b/doc/sections/numerics.rst index 65c79bc1c..4fa641834 100644 --- a/doc/sections/numerics.rst +++ b/doc/sections/numerics.rst @@ -23,9 +23,10 @@ we detail the discretization of the Vlasov-Maxwell system implemented in Struphy :maxdepth: 1 :caption: Contents: - subsections/pic - subsections/geomFE - subsections/time_discret + subsections/numerics-pic + subsections/numerics-geomFE + subsections/numerics-sph + subsections/numerics-time-discrete ../markdown/vlasov-maxwell diff --git a/doc/sections/perturbations.rst b/doc/sections/perturbations.rst new file mode 100644 index 000000000..21c5565c9 --- /dev/null +++ b/doc/sections/perturbations.rst @@ -0,0 +1,8 @@ +.. _perturbations: + +Perturbation functions +---------------------- + +.. automodule:: struphy.initial.perturbations + :members: + :show-inheritance: \ No newline at end of file diff --git a/doc/sections/propagators.rst b/doc/sections/propagators.rst index d34910232..506b033d9 100644 --- a/doc/sections/propagators.rst +++ b/doc/sections/propagators.rst @@ -1,11 +1,3 @@ -.. _Tutorial 1 - Kinetic particles: ../tutorials/tutorial_01_kinetic_particles.ipynb -.. _Tutorial 2 - Fluid particles: ../tutorials/tutorial_02_fluid_particles.ipynb -.. _Tutorial 6 - Poisson: ../tutorials/tutorial_06_poisson.ipynb -.. _Tutorial 7 - Heat equation: ../tutorials/tutorial_07_heat_equation.ipynb -.. _Tutorial 8 - Maxwell equations: ../tutorials/tutorial_08_maxwell.ipynb -.. _Tutorial 9 - Vlasov-Maxwell: ../tutorials/tutorial_09_vlasov_maxwell.ipynb -.. _Tutorial 10 - Linear MHD equations: ../tutorials/tutorial_10_linear_mhd.ipynb - .. _propagators: Propagators @@ -15,24 +7,14 @@ Propagators are the main building blocks of :ref:`models`, as they define the time splitting scheme of every algorithm. A propagator is used to advance a subset of a model's variables by one time step, :math:`t \to t + \Delta t`. -Check out the following tutorials for how to use propagators in Struphy: - -* `Tutorial 1 - Kinetic particles`_ -* `Tutorial 2 - Fluid particles`_ -* `Tutorial 6 - Poisson`_ -* `Tutorial 7 - Heat equation`_ -* `Tutorial 8 - Maxwell equations`_ -* `Tutorial 9 - Vlasov-Maxwell`_ -* `Tutorial 10 - Linear MHD equations`_ - .. toctree:: :maxdepth: 1 :caption: Contents: - subsections/propagators_fields - subsections/propagators_markers - subsections/propagators_coupling - subsections/propagator_base_class + subsections/propagators-fields + subsections/propagators-markers + subsections/propagators-coupling + subsections/propagators-base_class diff --git a/doc/sections/quickstart.rst b/doc/sections/quickstart.rst index 3d5ad984d..f6b6e266d 100644 --- a/doc/sections/quickstart.rst +++ b/doc/sections/quickstart.rst @@ -3,7 +3,8 @@ Quickstart ========== -Get familiar with Struphy objects using the notebook :ref:`tutorials`. +Get familiar with Struphy right away through the tutorials on `mybinder `_ - no installation needed. + What follows is an introduction to the CLI (command line interface) of Struphy. For a more in-depth manual please go to :ref:`userguide`. @@ -15,84 +16,61 @@ Check if kernels are compiled:: struphy compile -Check the current I/O paths:: - - struphy -p - -Set the I/O paths to the current working directory:: - - struphy --set-i . - struphy --set-o . - -Get a list of available Struphy models:: +Display available kinetic models:: - struphy run -h + struphy --kinetic -Let us generate default parameters for the model :class:`~struphy.models.kinetic.VlasovMaxwellOneSpecies`:: +Generate default parameters for the model :class:`~struphy.models.kinetic.VlasovMaxwellOneSpecies`:: struphy params VlasovMaxwellOneSpecies -After hitting ``enter`` on prompt, the parameter file ``params_VlasovMaxwellOneSpecies.yml`` is created -in the current input path (cwd). Let us rename it for convenience:: - - mv params_VlasovMaxwellOneSpecies.yml test.yml - -We can now run a simulation with these parameters and save the data to ``my_first_sim/``:: - - struphy run VlasovMaxwellOneSpecies -i test.yml -o my_first_sim - -The produced data is in the expected folder in the current output path (cwd):: - - ls my_first_sim/ - -Let us post-process the raw simulation data:: - - struphy pproc my_first_sim - -The results of post-processing are stored under ``my_first_sim/post_processing/``. In particular, -the data of the FEEC-fields is stored under:: - - ls my_first_sim/post_processing/fields_data/ - -and the data of the kinetic particles is stored under:: - - ls my_first_sim/post_processing/kinetic_data/ - -Check out Tutorial 08 in :ref:`tutorials` -for a deeper discussion on Struphy data and post processing. - -Our first simulation ran for just three time steps. Let us change the end-time of the simulation by opening the parameter file:: +After hitting enter on prompt, the default launch file ``params_VlasovMaxwellOneSpecies.py`` is created +in the current working directory (cwd). Let us rename it for convenience:: - vi test.yml + mv params_VlasovMaxwellOneSpecies.py test_struphy.py -and setting ``time/Tend`` to ``0.1``. Save, quit and run again, but this time on 2 MPI processes, -and saving to a different folder:: +The file ``test_struphy.py`` contains all information for a simulation with the above model. +We can change the parameters therein to our liking. +Then, we can run a simulation simply with:: - struphy run VlasovMaxwellOneSpecies -i test.yml -o another_sim --mpi 2 + python test_struphy.py -This time we ran for 20 time steps. The physical time unit of the run can be known via:: +By default, the produced data is in ``sim_1`` in the cwd:: - struphy units VlasovMaxwellOneSpecies -i test.yml + ls sim_1/ -For completeness, let us post-process the data of the second run:: +The data can be accessed through the Struphy API. If ``ipython`` is installed, type:: - struphy pproc another_sim + ipython + +and then:: -Let us now double the number of markers used in the simulation:: + from struphy.main import pproc, load_data + import os + path = os.path.join(os.getcwd(), "sim_1") + pproc(path) + simdata = load_data(path) - vi test.yml +The variable ``simdata`` is of type :class:`~struphy.main.SimData` and holds grid and orbit information. +You can deduce the kind of info held from the screen output. For instance, you have access several ``grids`` +as well as to, for instance:: -by changing ``kinetic/electrons/markers/ppc`` from 10 to 20, and then running:: + print(simdata.spline_values["em_fields"]["e_field_log"].keys()) + print(simdata.orbits["kinetic_ions"].shape) + print(simdata.f["kinetic_ions"]["e1"].keys()) - struphy run VlasovMaxwellOneSpecies -i test.yml -o sim_20 --mpi 2 +Under ``simdata.spline_values`` you find dictionaries holding splines values at the pre-defined ``simdata.grids_log`` +(or the physical grid); the keys are the time points of evaluation. -Finally, each Struphy model has some specific options to it, which in the case of ``VlasovMaxwellOneSpecies`` can be inspected via:: +Under ``simdata.orbits`` you find numpy arrays holding orbit data, indexed by ``[time, particle, attribute]``. - struphy params VlasovMaxwellOneSpecies --options +Under ``simdata.f`` you find binning data, in this case a 1d binning plot in the first logical coordinate :math:`\eta_1`-direction +(see :ref:`binning` for details). + +Parallel simulations can invoked from the same launch file for instance by:: -These options can be set in the parameter file. They usually refer to different types of solvers or solution methods. + mpirun -n 4 struphy_test.py -If you want to learn more about using Struphy, please check out the :ref:`userguide` -as well as the :ref:`tutorials`. +If you want to learn more please check the :ref:`userguide`. diff --git a/doc/sections/subsections/adding_model.rst b/doc/sections/subsections-old/adding_model.rst similarity index 100% rename from doc/sections/subsections/adding_model.rst rename to doc/sections/subsections-old/adding_model.rst diff --git a/doc/sections/subsections/boundary_conditions.rst b/doc/sections/subsections-old/boundary_conditions.rst similarity index 100% rename from doc/sections/subsections/boundary_conditions.rst rename to doc/sections/subsections-old/boundary_conditions.rst diff --git a/doc/sections/subsections/braginskii_equils.rst b/doc/sections/subsections-old/braginskii_equils.rst similarity index 86% rename from doc/sections/subsections/braginskii_equils.rst rename to doc/sections/subsections-old/braginskii_equils.rst index 030110ce6..2e221d273 100644 --- a/doc/sections/subsections/braginskii_equils.rst +++ b/doc/sections/subsections-old/braginskii_equils.rst @@ -11,9 +11,6 @@ References: * `Possanner, Negulescu 2016 `_ * `Possanner et al. 2017 `_ -.. inheritance-diagram:: struphy.fields_background.braginskii_equil.equils - :parts: 1 - .. toctree:: :maxdepth: 2 :caption: Contents: diff --git a/doc/sections/subsections/braginskii_equils_sub.rst b/doc/sections/subsections-old/braginskii_equils_sub.rst similarity index 100% rename from doc/sections/subsections/braginskii_equils_sub.rst rename to doc/sections/subsections-old/braginskii_equils_sub.rst diff --git a/doc/sections/subsections/bsplines.rst b/doc/sections/subsections-old/bsplines.rst similarity index 100% rename from doc/sections/subsections/bsplines.rst rename to doc/sections/subsections-old/bsplines.rst diff --git a/doc/sections/subsections/change_doc.rst b/doc/sections/subsections-old/change_doc.rst similarity index 100% rename from doc/sections/subsections/change_doc.rst rename to doc/sections/subsections-old/change_doc.rst diff --git a/doc/sections/subsections/data_structures.rst b/doc/sections/subsections-old/data_structures.rst similarity index 100% rename from doc/sections/subsections/data_structures.rst rename to doc/sections/subsections-old/data_structures.rst diff --git a/doc/sections/subsections/diagnostics.rst b/doc/sections/subsections-old/diagnostics.rst similarity index 100% rename from doc/sections/subsections/diagnostics.rst rename to doc/sections/subsections-old/diagnostics.rst diff --git a/doc/sections/subsections/dispersions.rst b/doc/sections/subsections-old/dispersions.rst similarity index 84% rename from doc/sections/subsections/dispersions.rst rename to doc/sections/subsections-old/dispersions.rst index 280969821..72d8ef62d 100644 --- a/doc/sections/subsections/dispersions.rst +++ b/doc/sections/subsections-old/dispersions.rst @@ -18,9 +18,6 @@ Base classes Available dispersion relations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. inheritance-diagram:: struphy.dispersion_relations.analytic - :parts: 1 - .. automodule:: struphy.dispersion_relations.analytic :members: :undoc-members: diff --git a/doc/sections/subsections/feec_basisops.rst b/doc/sections/subsections-old/feec_basisops.rst similarity index 100% rename from doc/sections/subsections/feec_basisops.rst rename to doc/sections/subsections-old/feec_basisops.rst diff --git a/doc/sections/subsections/feec_derham.rst b/doc/sections/subsections-old/feec_derham.rst similarity index 100% rename from doc/sections/subsections/feec_derham.rst rename to doc/sections/subsections-old/feec_derham.rst diff --git a/doc/sections/subsections/feec_linalg.rst b/doc/sections/subsections-old/feec_linalg.rst similarity index 100% rename from doc/sections/subsections/feec_linalg.rst rename to doc/sections/subsections-old/feec_linalg.rst diff --git a/doc/sections/subsections/feec_projected_mhd.rst b/doc/sections/subsections-old/feec_projected_mhd.rst similarity index 100% rename from doc/sections/subsections/feec_projected_mhd.rst rename to doc/sections/subsections-old/feec_projected_mhd.rst diff --git a/doc/sections/subsections/feec_projectors.rst b/doc/sections/subsections-old/feec_projectors.rst similarity index 100% rename from doc/sections/subsections/feec_projectors.rst rename to doc/sections/subsections-old/feec_projectors.rst diff --git a/doc/sections/subsections/feec_weightedmass.rst b/doc/sections/subsections-old/feec_weightedmass.rst similarity index 100% rename from doc/sections/subsections/feec_weightedmass.rst rename to doc/sections/subsections-old/feec_weightedmass.rst diff --git a/doc/sections/subsections/fluid_equils.rst b/doc/sections/subsections-old/fluid_equils.rst similarity index 71% rename from doc/sections/subsections/fluid_equils.rst rename to doc/sections/subsections-old/fluid_equils.rst index d2b79a9db..1833e89e1 100644 --- a/doc/sections/subsections/fluid_equils.rst +++ b/doc/sections/subsections-old/fluid_equils.rst @@ -5,9 +5,6 @@ Fluid equilibria The following inheritance diagram shows the fluid equilibria available in Struphy: -.. inheritance-diagram:: struphy.fields_background.fluid_equil.equils - :parts: 1 - .. toctree:: :maxdepth: 2 :caption: Contents: diff --git a/doc/sections/subsections/fluid_equils_sub.rst b/doc/sections/subsections-old/fluid_equils_sub.rst similarity index 100% rename from doc/sections/subsections/fluid_equils_sub.rst rename to doc/sections/subsections-old/fluid_equils_sub.rst diff --git a/doc/sections/subsections/git_workflow.rst b/doc/sections/subsections-old/git_workflow.rst similarity index 100% rename from doc/sections/subsections/git_workflow.rst rename to doc/sections/subsections-old/git_workflow.rst diff --git a/doc/sections/subsections/initial_conditions.rst b/doc/sections/subsections-old/initial_conditions.rst similarity index 100% rename from doc/sections/subsections/initial_conditions.rst rename to doc/sections/subsections-old/initial_conditions.rst diff --git a/doc/sections/subsections/inits.rst b/doc/sections/subsections-old/inits.rst similarity index 100% rename from doc/sections/subsections/inits.rst rename to doc/sections/subsections-old/inits.rst diff --git a/doc/sections/subsections/inits_sub.rst b/doc/sections/subsections-old/inits_sub.rst similarity index 100% rename from doc/sections/subsections/inits_sub.rst rename to doc/sections/subsections-old/inits_sub.rst diff --git a/doc/sections/subsections/io.rst b/doc/sections/subsections-old/io.rst similarity index 100% rename from doc/sections/subsections/io.rst rename to doc/sections/subsections-old/io.rst diff --git a/doc/sections/subsections/linear_algebra.rst b/doc/sections/subsections-old/linear_algebra.rst similarity index 100% rename from doc/sections/subsections/linear_algebra.rst rename to doc/sections/subsections-old/linear_algebra.rst diff --git a/doc/sections/subsections/mhd_equils.rst b/doc/sections/subsections-old/mhd_equils.rst similarity index 74% rename from doc/sections/subsections/mhd_equils.rst rename to doc/sections/subsections-old/mhd_equils.rst index 31377ab07..6377203aa 100644 --- a/doc/sections/subsections/mhd_equils.rst +++ b/doc/sections/subsections-old/mhd_equils.rst @@ -5,9 +5,6 @@ Fluid backgrounds The following inheritance diagram shows the fluid backgrounds available in Struphy: -.. inheritance-diagram:: struphy.fields_background.equils - :parts: 1 - .. toctree:: :maxdepth: 1 :caption: Contents: diff --git a/doc/sections/subsections/parameters.rst b/doc/sections/subsections-old/parameters.rst similarity index 100% rename from doc/sections/subsections/parameters.rst rename to doc/sections/subsections-old/parameters.rst diff --git a/doc/sections/subsections/paraview.rst b/doc/sections/subsections-old/paraview.rst similarity index 100% rename from doc/sections/subsections/paraview.rst rename to doc/sections/subsections-old/paraview.rst diff --git a/doc/sections/subsections/performance_tests.rst b/doc/sections/subsections-old/performance_tests.rst similarity index 100% rename from doc/sections/subsections/performance_tests.rst rename to doc/sections/subsections-old/performance_tests.rst diff --git a/doc/sections/subsections/pic_accumulation.rst b/doc/sections/subsections-old/pic_accumulation.rst similarity index 100% rename from doc/sections/subsections/pic_accumulation.rst rename to doc/sections/subsections-old/pic_accumulation.rst diff --git a/doc/sections/subsections/pic_base.rst b/doc/sections/subsections-old/pic_base.rst similarity index 93% rename from doc/sections/subsections/pic_base.rst rename to doc/sections/subsections-old/pic_base.rst index 5799ffe45..c3c48ee2f 100644 --- a/doc/sections/subsections/pic_base.rst +++ b/doc/sections/subsections-old/pic_base.rst @@ -3,9 +3,6 @@ Base modules ------------ -.. inheritance-diagram:: struphy.pic.particles - :parts: 1 - .. automodule:: struphy.pic.base :members: :undoc-members: diff --git a/doc/sections/subsections/pic_pushers.rst b/doc/sections/subsections-old/pic_pushers.rst similarity index 100% rename from doc/sections/subsections/pic_pushers.rst rename to doc/sections/subsections-old/pic_pushers.rst diff --git a/doc/sections/subsections/pic_sorting_sph.rst b/doc/sections/subsections-old/pic_sorting_sph.rst similarity index 100% rename from doc/sections/subsections/pic_sorting_sph.rst rename to doc/sections/subsections-old/pic_sorting_sph.rst diff --git a/doc/sections/subsections/pic_utilities.rst b/doc/sections/subsections-old/pic_utilities.rst similarity index 100% rename from doc/sections/subsections/pic_utilities.rst rename to doc/sections/subsections-old/pic_utilities.rst diff --git a/doc/sections/subsections/polar.rst b/doc/sections/subsections-old/polar.rst similarity index 100% rename from doc/sections/subsections/polar.rst rename to doc/sections/subsections-old/polar.rst diff --git a/doc/sections/subsections/post_processing.rst b/doc/sections/subsections-old/post_processing.rst similarity index 100% rename from doc/sections/subsections/post_processing.rst rename to doc/sections/subsections-old/post_processing.rst diff --git a/doc/sections/subsections/pproc_tools.rst b/doc/sections/subsections-old/pproc_tools.rst similarity index 100% rename from doc/sections/subsections/pproc_tools.rst rename to doc/sections/subsections-old/pproc_tools.rst diff --git a/doc/sections/subsections/profiling.rst b/doc/sections/subsections-old/profiling.rst similarity index 100% rename from doc/sections/subsections/profiling.rst rename to doc/sections/subsections-old/profiling.rst diff --git a/doc/sections/subsections/struphy_cli.rst b/doc/sections/subsections-old/struphy_cli.rst similarity index 100% rename from doc/sections/subsections/struphy_cli.rst rename to doc/sections/subsections-old/struphy_cli.rst diff --git a/doc/sections/subsections/write_prop.rst b/doc/sections/subsections-old/write_prop.rst similarity index 100% rename from doc/sections/subsections/write_prop.rst rename to doc/sections/subsections-old/write_prop.rst diff --git a/doc/sections/subsections/domains_avail.rst b/doc/sections/subsections/domains-avail.rst similarity index 76% rename from doc/sections/subsections/domains_avail.rst rename to doc/sections/subsections/domains-avail.rst index 6fcade7e2..e1d4b5037 100644 --- a/doc/sections/subsections/domains_avail.rst +++ b/doc/sections/subsections/domains-avail.rst @@ -3,9 +3,6 @@ Available domains ----------------- -.. inheritance-diagram:: struphy.geometry.domains - :parts: 1 - .. automodule:: struphy.geometry.domains :members: :exclude-members: kind_map, params, params_numpy, pole, periodic_eta3 diff --git a/doc/sections/subsections/domains_base.rst b/doc/sections/subsections/domains-base.rst similarity index 100% rename from doc/sections/subsections/domains_base.rst rename to doc/sections/subsections/domains-base.rst diff --git a/doc/sections/subsections/domains_kernels.rst b/doc/sections/subsections/domains-kernels.rst similarity index 100% rename from doc/sections/subsections/domains_kernels.rst rename to doc/sections/subsections/domains-kernels.rst diff --git a/doc/sections/subsections/domains_utils.rst b/doc/sections/subsections/domains-utils.rst similarity index 100% rename from doc/sections/subsections/domains_utils.rst rename to doc/sections/subsections/domains-utils.rst diff --git a/doc/sections/subsections/mhd_equils_sub.rst b/doc/sections/subsections/equils-avail.rst similarity index 64% rename from doc/sections/subsections/mhd_equils_sub.rst rename to doc/sections/subsections/equils-avail.rst index f6ca8ecb3..b2e39bdaf 100644 --- a/doc/sections/subsections/mhd_equils_sub.rst +++ b/doc/sections/subsections/equils-avail.rst @@ -3,17 +3,6 @@ Available fluid equilibria ^^^^^^^^^^^^^^^^^^^^^^^^^^ -Aside form the classes listed below, the fluid background ``LogicalConst`` -is available for simple testing; it has the following input structure:: - - LogicalConst : - values : 1.3 - -or, for vector-valued variables:: - - LogicalConst : - values : [.3, .15, null] - .. automodule:: struphy.fields_background.equils :members: :undoc-members: @@ -26,7 +15,7 @@ or, for vector-valued variables:: Projected fluid equilibria ^^^^^^^^^^^^^^^^^^^^^^^^^^ -These classes provide discrete representations of fluid equilibria in De Rham spaces. +These classes provide discrete representations of fluid equilibria in DeRham spaces. .. automodule:: struphy.fields_background.projected_equils :members: @@ -47,16 +36,4 @@ These classes can be used to quickly pass callables to Struphy objects :members: :undoc-members: :exclude-members: set_defaults - :show-inheritance: - - -.. _mhd_base: - -Base classes -^^^^^^^^^^^^ - -.. automodule:: struphy.fields_background.base - :members: - :undoc-members: - :exclude-members: :show-inheritance: \ No newline at end of file diff --git a/doc/sections/subsections/equils-base.rst b/doc/sections/subsections/equils-base.rst new file mode 100644 index 000000000..3848c7d5c --- /dev/null +++ b/doc/sections/subsections/equils-base.rst @@ -0,0 +1,10 @@ +.. _mhd_base: + +Base classes +^^^^^^^^^^^^ + +.. automodule:: struphy.fields_background.base + :members: + :undoc-members: + :exclude-members: + :show-inheritance: \ No newline at end of file diff --git a/doc/sections/subsections/kinetic-bckgr-avail.rst b/doc/sections/subsections/kinetic-bckgr-avail.rst new file mode 100644 index 000000000..d2e6779d0 --- /dev/null +++ b/doc/sections/subsections/kinetic-bckgr-avail.rst @@ -0,0 +1,7 @@ +Available Maxwellians +^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: struphy.kinetic_background.maxwellians + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/doc/sections/subsections/kinetic-bckgr-base.rst b/doc/sections/subsections/kinetic-bckgr-base.rst new file mode 100644 index 000000000..1736959cd --- /dev/null +++ b/doc/sections/subsections/kinetic-bckgr-base.rst @@ -0,0 +1,7 @@ +Base classes +^^^^^^^^^^^^ + +.. automodule:: struphy.kinetic_background.base + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/doc/sections/subsections/kinetic_backgrounds.rst b/doc/sections/subsections/kinetic_backgrounds.rst deleted file mode 100644 index 1e3ac362d..000000000 --- a/doc/sections/subsections/kinetic_backgrounds.rst +++ /dev/null @@ -1,16 +0,0 @@ -.. _kinetic_backgrounds: - -Kinetic backgrounds -------------------- - -Kinetic backgrounds are often thermal equilibria like Maxwellian distributions. - -.. inheritance-diagram:: struphy.kinetic_background.maxwellians - :parts: 1 - -.. toctree:: - :maxdepth: 1 - :caption: Contents: - - kinetic_backgrounds_sub - diff --git a/doc/sections/subsections/kinetic_backgrounds_sub.rst b/doc/sections/subsections/kinetic_backgrounds_sub.rst deleted file mode 100644 index a8333cb37..000000000 --- a/doc/sections/subsections/kinetic_backgrounds_sub.rst +++ /dev/null @@ -1,27 +0,0 @@ -Available Maxwellians -^^^^^^^^^^^^^^^^^^^^^ - -.. automodule:: struphy.kinetic_background.maxwellians - :members: - :undoc-members: - :show-inheritance: - - -.. _moment_functions: - -Moment functions -^^^^^^^^^^^^^^^^ - -.. automodule:: struphy.kinetic_background.moment_functions - :members: - :undoc-members: - :show-inheritance: - - -Base classes -^^^^^^^^^^^^ - -.. automodule:: struphy.kinetic_background.base - :members: - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/doc/sections/subsections/model_base_class.rst b/doc/sections/subsections/models-base.rst similarity index 100% rename from doc/sections/subsections/model_base_class.rst rename to doc/sections/subsections/models-base.rst diff --git a/doc/sections/subsections/models_fluid.rst b/doc/sections/subsections/models-fluid.rst similarity index 89% rename from doc/sections/subsections/models_fluid.rst rename to doc/sections/subsections/models-fluid.rst index b402425d7..fba081aec 100644 --- a/doc/sections/subsections/models_fluid.rst +++ b/doc/sections/subsections/models-fluid.rst @@ -5,9 +5,6 @@ Fluid models Pure fluid models where all plasma species are considered in local thermal equilibirum. -.. inheritance-diagram:: struphy.models.fluid - :parts: 1 - .. automodule:: struphy.models.fluid :members: :undoc-members: diff --git a/doc/sections/subsections/models_hybrid.rst b/doc/sections/subsections/models-hybrid.rst similarity index 90% rename from doc/sections/subsections/models_hybrid.rst rename to doc/sections/subsections/models-hybrid.rst index 83b1167d7..4323287bf 100644 --- a/doc/sections/subsections/models_hybrid.rst +++ b/doc/sections/subsections/models-hybrid.rst @@ -5,9 +5,6 @@ Fluid-kinetic hybrid models The bulk plasma is fluid, but there is also at least one kinetic species (e.g. energetic particles). -.. inheritance-diagram:: struphy.models.hybrid - :parts: 1 - .. automodule:: struphy.models.hybrid :members: :undoc-members: diff --git a/doc/sections/subsections/models_kinetic.rst b/doc/sections/subsections/models-kinetic.rst similarity index 89% rename from doc/sections/subsections/models_kinetic.rst rename to doc/sections/subsections/models-kinetic.rst index 76ac8c2a8..332e0f1ad 100644 --- a/doc/sections/subsections/models_kinetic.rst +++ b/doc/sections/subsections/models-kinetic.rst @@ -5,9 +5,6 @@ Kinetic models The bulk plasma is kinetic, out of thermal equilibirum; there can be fluid components too (not bulk). -.. inheritance-diagram:: struphy.models.kinetic - :parts: 1 - .. automodule:: struphy.models.kinetic :members: :undoc-members: diff --git a/doc/sections/subsections/normalization.rst b/doc/sections/subsections/models-normalization.rst similarity index 64% rename from doc/sections/subsections/normalization.rst rename to doc/sections/subsections/models-normalization.rst index 627fdf664..ba1af491d 100644 --- a/doc/sections/subsections/normalization.rst +++ b/doc/sections/subsections/models-normalization.rst @@ -17,8 +17,8 @@ expressed as :math:`a = 2 \cdot 1\, \textrm{meter}` or as of length was chosen to be 0.5 meter. The units :math:`\hat X` for a Struphy model -can be influenced by the user through the :code:`parameter.yml` file. -In particular, in the section :ref:`units` the user can set +can be influenced by the user through :class:`~struphy.io.options.BaseUnits` in the launch file, +where the user can set * the unit of **length** :math:`\hat x`, expressed in **Meter**, @@ -58,7 +58,7 @@ There are four possibilities: \hat v = \sqrt{\frac{k_\textnormal{B} \hat T}{m_\textnormal{bulk}}}\,. Several additional units are derived internally from the above basic units, -in the function :func:`~struphy.io.setup.derive_units`. In particular, +in the class :class:`~struphy.io.options.Units`. In particular, * the **time** unit in **Seconds**: @@ -86,9 +86,39 @@ which is equal to :math:`\hat B^2/\mu_0` if the velocity scale is ``Alfvén``, \hat \jmath = q_\textnormal{bulk} \hat n \hat v\,. -The numerical values of these units, for any :code:`MODEL` with a parameter file :code:`FILE`, -can be inspected via:: +We refer to :ref:`disc_example` for an example of how to derive a normalization for a physics model. - struphy units MODEL -i FILE -We refer to :ref:`disc_example` for an example of how to derive a normalization for a physics model. \ No newline at end of file +Units class +----------- + +.. autoclass:: struphy.io.options.Units + :members: + :undoc-members: + + +.. _equation_params: + +Equation parameters +------------------- + +In Struphy models, the following equation parameters appear: + +.. math:: + + \alpha_\textrm{s} = \frac{\hat \Omega_\textnormal{ps}}{\hat \Omega_\textnormal{cs}}\,,\qquad \varepsilon_\textrm{s} = \frac{1}{\hat \Omega_\textnormal{cs} \hat t} \,, + +featuring the plasma- and cyclotron frequency of species :math:`\textrm{s}`, respectively, + +.. math:: + + \hat\Omega_\textnormal{ps} = \sqrt{\frac{\hat n (Z_\textrm{s}e)^2}{\epsilon_0 (A_\textrm{s} m_\textnormal{H})}} \,,\qquad \hat \Omega_{\textnormal{cs}} = \frac{(Z_\textrm{s}e) \hat B}{(A_\textrm{s} m_\textnormal{H})}\,, + +where :math:`Z_\textrm{s}` and :math:`A_\textrm{s}` stand for the species' charge and mass number, respectively. +These equation parameters are defined in :class:`~struphy.models.species.Species.EquationParameters` and can be overridden +in the launch file via :func:`~struphy.models.species.Species.set_phys_params`. + +.. autoclass:: struphy.models.species.Species.EquationParameters + :members: + :undoc-members: + diff --git a/doc/sections/subsections/models_toy.rst b/doc/sections/subsections/models-toy.rst similarity index 88% rename from doc/sections/subsections/models_toy.rst rename to doc/sections/subsections/models-toy.rst index 5f9f605ee..b118e0a9a 100644 --- a/doc/sections/subsections/models_toy.rst +++ b/doc/sections/subsections/models-toy.rst @@ -5,9 +5,6 @@ Toy models Simple toy models for testing. -.. inheritance-diagram:: struphy.models.toy - :parts: 1 - .. automodule:: struphy.models.toy :members: :undoc-members: diff --git a/doc/sections/subsections/geomFE.rst b/doc/sections/subsections/numerics-geomFE.rst similarity index 100% rename from doc/sections/subsections/geomFE.rst rename to doc/sections/subsections/numerics-geomFE.rst diff --git a/doc/sections/subsections/pic.rst b/doc/sections/subsections/numerics-pic.rst similarity index 100% rename from doc/sections/subsections/pic.rst rename to doc/sections/subsections/numerics-pic.rst diff --git a/doc/sections/subsections/numerics-sph.rst b/doc/sections/subsections/numerics-sph.rst new file mode 100644 index 000000000..2d2a43842 --- /dev/null +++ b/doc/sections/subsections/numerics-sph.rst @@ -0,0 +1,9 @@ +.. _sph_method: + +Smoothed particle hydrodynamics (SPH) +------------------------------------- + +Basics +^^^^^^ + +Coming soon! \ No newline at end of file diff --git a/doc/sections/subsections/time_discret.rst b/doc/sections/subsections/numerics-time-discrete.rst similarity index 100% rename from doc/sections/subsections/time_discret.rst rename to doc/sections/subsections/numerics-time-discrete.rst diff --git a/doc/sections/subsections/propagator_base_class.rst b/doc/sections/subsections/propagators-base.rst similarity index 100% rename from doc/sections/subsections/propagator_base_class.rst rename to doc/sections/subsections/propagators-base.rst diff --git a/doc/sections/subsections/propagators_coupling.rst b/doc/sections/subsections/propagators-coupling.rst similarity index 75% rename from doc/sections/subsections/propagators_coupling.rst rename to doc/sections/subsections/propagators-coupling.rst index 247810c15..788985e95 100644 --- a/doc/sections/subsections/propagators_coupling.rst +++ b/doc/sections/subsections/propagators-coupling.rst @@ -3,9 +3,6 @@ Particle-field coupling propagators ----------------------------------- -.. inheritance-diagram:: struphy.propagators.propagators_coupling - :parts: 1 - .. automodule:: struphy.propagators.propagators_coupling :members: :undoc-members: diff --git a/doc/sections/subsections/propagators_fields.rst b/doc/sections/subsections/propagators-fields.rst similarity index 72% rename from doc/sections/subsections/propagators_fields.rst rename to doc/sections/subsections/propagators-fields.rst index a5d58373f..32563955d 100644 --- a/doc/sections/subsections/propagators_fields.rst +++ b/doc/sections/subsections/propagators-fields.rst @@ -3,9 +3,6 @@ Field propagators ----------------- -.. inheritance-diagram:: struphy.propagators.propagators_fields - :parts: 1 - .. automodule:: struphy.propagators.propagators_fields :members: :undoc-members: diff --git a/doc/sections/subsections/propagators_markers.rst b/doc/sections/subsections/propagators-markers.rst similarity index 72% rename from doc/sections/subsections/propagators_markers.rst rename to doc/sections/subsections/propagators-markers.rst index 9d71be030..eaaa785db 100644 --- a/doc/sections/subsections/propagators_markers.rst +++ b/doc/sections/subsections/propagators-markers.rst @@ -3,9 +3,6 @@ Particle propagators -------------------- -.. inheritance-diagram:: struphy.propagators.propagators_markers - :parts: 1 - .. automodule:: struphy.propagators.propagators_markers :members: :undoc-members: diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index 2f58f33ce..709d1b5cc 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -3,47 +3,144 @@ Userguide ========= -There are two basic modes of how Struphy can be used: +This guide takes you through the classes used in Struphy launch files generated by:: -1. in Python programs, as for instance described in the notebook :ref:`tutorials` -2. via the Struphy CLI (command line interface) + struphy params MODEL -In the CLI the overall help is displayed by typing:: +for a valid ``MODEL`` from the list of :ref:`models`. +All information for running simulations are contained and can be altered in such launch (parameter) files. +It is recommended to generate such a file and inspect it with an editor like VScode (using a type checker like "mypy"). - struphy -h +:ref:`pproc` is discussed at the end of this section. -To get more information on the sub-commands:: - struphy COMMAND -h +Basic options +------------- -The installed version is obtained by:: +.. autoclass:: struphy.io.options.EnvironmentOptions - struphy -v +.. autoclass:: struphy.io.options.BaseUnits -.. toctree:: - :maxdepth: 1 - :caption: Contents: +.. autoclass:: struphy.io.options.Time - subsections/struphy_cli - subsections/parameters - subsections/initial_conditions - subsections/boundary_conditions - subsections/profiling - subsections/post_processing - subsections/paraview +Simulation domains +------------------ +See :ref:`avail_mappings`. +Fluid backgrounds +----------------- +See :ref:`equils_avail`. +Grids +----- - +.. autoclass:: struphy.topology.grids.TensorProductGrid + :show-inheritance: +Derham complex +-------------- +.. autoclass:: struphy.io.options.DerhamOptions +Models +------ +See :ref:`models`. + +Species types +^^^^^^^^^^^^^ + +Each Struphy model is a collection of species of one of the following types: + +.. autoclass:: struphy.models.species.FieldSpecies + +.. autoclass:: struphy.models.species.FluidSpecies + +.. autoclass:: struphy.models.species.ParticleSpecies + +.. automethod:: struphy.models.species.Species.set_phys_params + + +Variable types +^^^^^^^^^^^^^^ + +Each species can contain multiple variables. + +.. autoclass:: struphy.models.variables.PICVariable + +.. autoclass:: struphy.models.variables.FEECVariable + +.. autoclass:: struphy.models.variables.SPHVariable + + +Setting particle parameters +--------------------------- + +.. automethod:: struphy.models.species.ParticlesSpecies.set_markers + +.. automethod:: struphy.models.species.ParticlesSpecies.set_sorting_boxes + +.. automethod:: struphy.models.species.ParticlesSpecies.set_save_data + +.. autoclass:: struphy.pic.utilities.LoadingParameters + +.. autoclass:: struphy.pic.utilities.WeightsParameters + +.. autoclass:: struphy.pic.utilities.BoundaryParameters + +.. autoclass:: struphy.pic.utilities.BinningPlot + +.. autoclass:: struphy.pic.utilities.KernelDensityPlot + + +Setting backgrounds +------------------- + +.. automethod:: struphy.models.variables.FEECVariable.add_background + +.. automethod:: struphy.models.variables.PICVariable.add_background + +.. automethod:: struphy.models.variables.SPHVariable.add_background + +.. autoclass:: struphy.io.options.FieldsBackground + + +Adding perturbations +-------------------- + +.. automethod:: struphy.models.variables.FEECVariable.add_perturbation + +.. automethod:: struphy.models.variables.SPHVariable.add_perturbation + + +.. autoclass:: struphy.initial.base.Perturbation + +See available :ref:`perturbations`. + + +Setting initial conditions +-------------------------- + +.. automethod:: struphy.models.variables.PICVariable.add_initial_condition + + +For :class:`~struphy.models.variables.FEECVariable` and :class:`~struphy.models.variables.SPHVariable` the initial condition is automatically +created as the sum of background + perturbation. + + +.. _pproc: + +Post-processing +--------------- + +.. automodule:: struphy.main + :members: + :exclude-members: run \ No newline at end of file diff --git a/src/struphy/io/options.py b/src/struphy/io/options.py index cecd4b72c..9be8b3249 100644 --- a/src/struphy/io/options.py +++ b/src/struphy/io/options.py @@ -95,7 +95,7 @@ def __post_init__(self): @dataclass class BaseUnits: """ - Base units are passed to __init__, other units derive from these. + Base units from which other units are derived. See :ref:`normalization`. Parameters ---------- @@ -121,7 +121,7 @@ class BaseUnits: class Units: """ - Colllects base units and derives other units from these. + Colllects base units and derives other units from these. See :ref:`normalization`. """ def __init__(self, base: BaseUnits = None): @@ -245,7 +245,7 @@ def derive_units(self, velocity_scale: str = "light", A_bulk: int = None, Z_bulk @dataclass class DerhamOptions: - """Options for the Derham spaces. + """Options for the Derham spaces. See :ref:`geomFE`. Parameters ---------- @@ -297,7 +297,7 @@ class FieldsBackground: Can be length 1 for scalar functions; must be length 3 for vector-valued functions. variable : str - Name of the function in FluidEquilibrium that should be the background. + Name of the method in :class:`~struphy.fields_background.base.FluidEquilibrium` that should be the background. """ type: BackgroundTypes = "LogicalConst" @@ -316,16 +316,16 @@ class EnvironmentOptions: Parameters ---------- out_folders : str - The directory where all sim_folders are stored. + Absolute path to directory for ``sim_folder``. sim_folder : str - Folder in 'out_folders/' for the current simulation (default='sim_1'). + Folder in ``out_folders/`` for the current simulation (default= ``sim_1/`` ). Will create the folder if it does not exist OR cleans the folder for new runs. restart : bool Whether to restart a run (default=False). - max_runtime : int, + max_runtime : int Maximum run time of simulation in minutes. Will finish the time integration once this limit is reached (default=300). save_step : int diff --git a/src/struphy/models/species.py b/src/struphy/models/species.py index 8a9f11008..122d40486 100644 --- a/src/struphy/models/species.py +++ b/src/struphy/models/species.py @@ -54,7 +54,8 @@ def set_phys_params( epsilon: float = None, kappa: float = None, ): - """Set charge- and mass number. Set equation parameters (alpha, epsilon, ...) to override units.""" + """Set charge- and mass number of species. + Optional: Set equation parameters (alpha, epsilon, kappa) to override units.""" self._charge_number = charge_number self._mass_number = mass_number self.alpha = alpha diff --git a/src/struphy/models/variables.py b/src/struphy/models/variables.py index e1c310db0..3f47f1efe 100644 --- a/src/struphy/models/variables.py +++ b/src/struphy/models/variables.py @@ -72,7 +72,8 @@ def __name__(self): return self._name def add_background(self, background, verbose=True): - """Type inference of added background done in sub class.""" + """Add a static background for this variable. + Multiple backgrounds can be added up.""" if not hasattr(self, "_backgrounds") or self.backgrounds is None: self._backgrounds = background else: @@ -89,6 +90,8 @@ def add_background(self, background, verbose=True): class FEECVariable(Variable): + """Variable discretized with :ref:`geomFE`.""" + def __init__(self, space: OptsFEECSpace = "H1"): check_option(space, OptsFEECSpace) self._space = space @@ -111,6 +114,8 @@ def add_background(self, background: FieldsBackground, verbose=True): super().add_background(background, verbose=verbose) def add_perturbation(self, perturbation: Perturbation, verbose=True): + """Add an initial :class:`~struphy.initial.base.Perturbation` for this variable. + Multiple perturbations can be added up.""" if not hasattr(self, "_perturbations") or self.perturbations is None: self._perturbations = perturbation else: @@ -142,6 +147,8 @@ def allocate( class PICVariable(Variable): + """Variable discretized with :ref:`particle_discrete`.""" + def __init__(self, space: OptsPICSpace = "Particles6D"): check_option(space, OptsPICSpace) self._space = space @@ -275,6 +282,8 @@ def saved_markers(self) -> xp.ndarray: class SPHVariable(Variable): + """Variable discretized with :ref:`sph_method`.""" + def __init__(self): self._space = "ParticlesSPH" self._n_as_volume_form = True @@ -314,6 +323,7 @@ def add_perturbation( del_u3: Perturbation = None, verbose=True, ): + """Add an initial :class:`~struphy.initial.base.Perturbation` for the fluid density and/or velocity.""" self._perturbations = {} self._perturbations["n"] = del_n self._perturbations["u1"] = del_u1 From 703ecd46e4e00ce1619becffc5a4fcb752368f4f Mon Sep 17 00:00:00 2001 From: Stefan Possanner <86720346+spossann@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:33:17 +0100 Subject: [PATCH 19/83] 108 psydac change renaming of globalpojector (#109) **Solves the following issue(s):** Closes #108 --- .../subsections-old/feec_projectors.rst | 2 +- src/struphy/feec/projectors.py | 10 +++---- src/struphy/feec/psydac_derham.py | 30 +++++++++++-------- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/doc/sections/subsections-old/feec_projectors.rst b/doc/sections/subsections-old/feec_projectors.rst index 25c55d2a8..8c19b77ff 100644 --- a/doc/sections/subsections-old/feec_projectors.rst +++ b/doc/sections/subsections-old/feec_projectors.rst @@ -8,7 +8,7 @@ Projections into Derham :undoc-members: :show-inheritance: -.. autoclass:: psydac.feec.global_projectors.GlobalProjector +.. autoclass:: psydac.feec.global_geometric_projectors.GlobalGeometricProjector :members: :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/src/struphy/feec/projectors.py b/src/struphy/feec/projectors.py index be56cc722..21b8f77b4 100644 --- a/src/struphy/feec/projectors.py +++ b/src/struphy/feec/projectors.py @@ -1,7 +1,7 @@ import cunumpy as xp from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL from psydac.ddm.mpi import mpi as MPI -from psydac.feec.global_projectors import GlobalProjector +from psydac.feec.global_geometric_projectors import GlobalGeometricProjector from psydac.fem.basic import FemSpace from psydac.fem.tensor import TensorFemSpace from psydac.fem.vector import VectorFemSpace @@ -58,15 +58,15 @@ class CommutingProjector: * :math:`\mathbb B`: :class:`~struphy.feec.linear_operators.BoundaryOperator`, * :math:`\mathbb P`: :class:`~struphy.polar.linear_operators.PolarExtractionOperator` for degrees of freedom, - * :math:`\mathcal I`: Kronecker product inter-/histopolation matrix, from :class:`~psydac.feec.global_projectors.GlobalProjector` + * :math:`\mathcal I`: Kronecker product inter-/histopolation matrix, from :class:`~psydac.feec.global_geometric_projectors.GlobalGeometricProjector` * :math:`\mathbb E`: :class:`~struphy.polar.linear_operators.PolarExtractionOperator` for FE coefficients. :math:`\mathbb P` and :math:`\mathbb E` (and :math:`\mathbb B` in case of no boundary conditions) can be identity operators, - which gives the pure tensor-product Psydac :class:`~psydac.feec.global_projectors.GlobalProjector`. + which gives the pure tensor-product Psydac :class:`~psydac.feec.global_geometric_projectors.GlobalGeometricProjector`. Parameters ---------- - projector_tensor : GlobalProjector + projector_tensor : GlobalGeometricProjector The pure tensor product projector. dofs_extraction_op : PolarExtractionOperator, optional @@ -81,7 +81,7 @@ class CommutingProjector: def __init__( self, - projector_tensor: GlobalProjector, + projector_tensor: GlobalGeometricProjector, dofs_extraction_op=None, base_extraction_op=None, boundary_op=None, diff --git a/src/struphy/feec/psydac_derham.py b/src/struphy/feec/psydac_derham.py index e5a8cde32..972be4509 100644 --- a/src/struphy/feec/psydac_derham.py +++ b/src/struphy/feec/psydac_derham.py @@ -6,8 +6,14 @@ from psydac.ddm.cart import DomainDecomposition from psydac.ddm.mpi import MockComm, MockMPI from psydac.ddm.mpi import mpi as MPI -from psydac.feec.derivatives import Curl_3D, Divergence_3D, Gradient_3D -from psydac.feec.global_projectors import Projector_H1, Projector_H1vec, Projector_Hcurl, Projector_Hdiv, Projector_L2 +from psydac.feec.derivatives import Curl3D, Divergence3D, Gradient3D +from psydac.feec.global_geometric_projectors import ( + GlobalGeometricProjectorH1, + GlobalGeometricProjectorH1vec, + GlobalGeometricProjectorHcurl, + GlobalGeometricProjectorHdiv, + GlobalGeometricProjectorL2, +) from psydac.fem.grid import FemAssemblyGrid from psydac.fem.partitioning import create_cart from psydac.fem.splines import SplineSpace @@ -204,7 +210,7 @@ def __init__( if "dev" in psydac_ver: _h1vec_space.symbolic_space = "H1vec" self._Vh_fem[sp_form] = _h1vec_space - self._P[sp_form] = Projector_H1vec(self.Vh_fem[sp_form]) + self._P[sp_form] = GlobalGeometricProjectorH1vec(self.Vh_fem[sp_form]) else: self._Vh_fem[sp_form] = getattr(_derham, "V" + str(i)) self._P[sp_form] = _projectors[i] @@ -2535,9 +2541,9 @@ def __init__(self, *spaces): self._spaces = spaces self._dim = 3 - D0 = Gradient_3D(spaces[0], spaces[1]) - D1 = Curl_3D(spaces[1], spaces[2]) - D2 = Divergence_3D(spaces[2], spaces[3]) + D0 = Gradient3D(spaces[0], spaces[1]) + D1 = Curl3D(spaces[1], spaces[2]) + D2 = Divergence3D(spaces[2], spaces[3]) spaces[0].diff = spaces[0].grad = D0 spaces[1].diff = spaces[1].curl = D1 @@ -2582,7 +2588,7 @@ def spaces(self): @property def derivatives_as_matrices(self): """Differential operators of the De Rham sequence as LinearOperator objects.""" - return tuple(V.diff.matrix for V in self.spaces[:-1]) + return tuple(V.diff.linop for V in self.spaces[:-1]) @property def derivatives(self): @@ -2604,7 +2610,7 @@ def projectors(self, *, kind="global", nquads=None): kind : str Type of the projection : at the moment, only global is accepted and returns geometric commuting projectors based on interpolation/histopolation - for the De Rham sequence (GlobalProjector objects). + for the De Rham sequence (GlobalGeometricProjector objects). nquads : list(int) | tuple(int) Number of quadrature points along each direction, to be used in Gauss @@ -2632,10 +2638,10 @@ def projectors(self, *, kind="global", nquads=None): assert all(isinstance(nq, int) for nq in nquads) assert all(nq >= 1 for nq in nquads) - P0 = Projector_H1(self.V0) - P1 = Projector_Hcurl(self.V1, nquads) - P2 = Projector_Hdiv(self.V2, nquads) - P3 = Projector_L2(self.V3, nquads) + P0 = GlobalGeometricProjectorH1(self.V0) + P1 = GlobalGeometricProjectorHcurl(self.V1, nquads) + P2 = GlobalGeometricProjectorHdiv(self.V2, nquads) + P3 = GlobalGeometricProjectorL2(self.V3, nquads) return P0, P1, P2, P3 From 2b513df089be8dd18b5ebe2db669be867e046da2 Mon Sep 17 00:00:00 2001 From: Stefan Possanner <86720346+spossann@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:52:02 +0100 Subject: [PATCH 20/83] Workflow for publishing to Docker hub and GHCR (#104) **Solves the following issue(s):** Closes #67 - [x] publish to docker hub - [x] publish to GHCR The Docker hub images are published here: https://hub.docker.com/u/spossann The GHCR images are here: https://github.com/orgs/struphy-hub/packages?repo_name=struphy Both are updated automatically on push to `main.` --- .github/workflows/docker.yml | 97 +++++++++++++++++++++++++++++++ .github/workflows/ghcr.yml | 108 +++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/ghcr.yml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..d2a4959c6 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,97 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# GitHub recommends pinning actions to a commit SHA. +# To get a newer version, you will need to update the SHA. +# You can also reference a tag or branch, but the action may change without warning. + +name: Publish to Docker Hub + +on: + push: + branches: ['main'] + +jobs: + push_to_ubuntu-for-struphy: + name: Push ubuntu-for-struphy to Docker Hub + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + attestations: write + id-token: write + steps: + - name: Check out the repo + uses: actions/checkout@v5 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: spossann/ubuntu-for-struphy + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v6 + with: + context: . + file: docker/ubuntu-latest.dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v3 + with: + subject-name: index.docker.io/spossann/ubuntu-for-struphy + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true + + push_to_struphy: + name: Push struphy to Docker Hub + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + attestations: write + id-token: write + steps: + - name: Check out the repo + uses: actions/checkout@v5 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: spossann/struphy + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v6 + with: + context: . + file: docker/ubuntu-latest-with-struphy.dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v3 + with: + subject-name: index.docker.io/spossann/struphy + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/ghcr.yml b/.github/workflows/ghcr.yml new file mode 100644 index 000000000..3371889c3 --- /dev/null +++ b/.github/workflows/ghcr.yml @@ -0,0 +1,108 @@ +# +name: Publish to GHCR + +# Configures this workflow to run every time a change is pushed to the branch called `release`. +on: + push: + branches: ['devel'] + +# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. +env: + REGISTRY: ghcr.io + IMAGE_NAME_1: ${{ github.repository }}/ubuntu-with-reqs + IMAGE_NAME_2: ${{ github.repository }}/ubuntu-with-struphy + +# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. +jobs: + build-and-push-ubuntu-with-reqs: + runs-on: ubuntu-latest + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. + permissions: + contents: read + packages: write + attestations: write + id-token: write + # + steps: + - name: Checkout repository + uses: actions/checkout@v5 + # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_1 }} + # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. + # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see [Usage](https://github.com/docker/build-push-action#usage) in the README of the `docker/build-push-action` repository. + # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v6 + with: + context: . + file: docker/ubuntu-latest.dockerfile + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_1 }}:latest + labels: ${{ steps.meta.outputs.labels }} + + # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see [Using artifact attestations to establish provenance for builds](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds). + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v3 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_1}} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true + + build-and-push-ubuntu-with-struphy: + runs-on: ubuntu-latest + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. + permissions: + contents: read + packages: write + attestations: write + id-token: write + # + steps: + - name: Checkout repository + uses: actions/checkout@v5 + # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_2 }} + # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. + # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see [Usage](https://github.com/docker/build-push-action#usage) in the README of the `docker/build-push-action` repository. + # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v6 + with: + context: . + file: docker/ubuntu-latest-with-struphy.dockerfile + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_2 }}:latest + labels: ${{ steps.meta.outputs.labels }} + + # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see [Using artifact attestations to establish provenance for builds](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds). + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v3 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_2}} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true + From ef293be999a66def0f8d13e2a2bf3efedba3cf16 Mon Sep 17 00:00:00 2001 From: Stefan Possanner <86720346+spossann@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:52:46 +0100 Subject: [PATCH 21/83] Perform deletions to clean front page (#112) **Solves the following issue(s):** Closes #111 --- .dockerignore | 11 - .gitlab/issue_templates/struphy_issue.md | 13 - .../struphy_merge_request.md | 15 - apt.txt | 4 - tutorial_07_data_structures.ipynb | 969 ----------- .../tutorial_01_kinetic_particles.ipynb | 1390 --------------- .../tutorial_01_parameter_files.ipynb | 379 ---- tutorials_old/tutorial_01_particles.ipynb | 273 --- .../tutorial_02_fluid_particles.ipynb | 1541 ----------------- .../tutorial_03_discrete_derham.ipynb | 367 ---- tutorials_old/tutorial_06_poisson.ipynb | 249 --- tutorials_old/tutorial_07_heat_equation.ipynb | 511 ------ tutorials_old/tutorial_08_maxwell.ipynb | 317 ---- .../tutorial_09_vlasov_maxwell.ipynb | 599 ------- tutorials_old/tutorial_10_linear_mhd.ipynb | 646 ------- .../tutorial_12_struphy_data_pproc.ipynb | 642 ------- 16 files changed, 7926 deletions(-) delete mode 100644 .dockerignore delete mode 100644 .gitlab/issue_templates/struphy_issue.md delete mode 100644 .gitlab/merge_request_templates/struphy_merge_request.md delete mode 100644 apt.txt delete mode 100644 tutorial_07_data_structures.ipynb delete mode 100644 tutorials_old/tutorial_01_kinetic_particles.ipynb delete mode 100644 tutorials_old/tutorial_01_parameter_files.ipynb delete mode 100644 tutorials_old/tutorial_01_particles.ipynb delete mode 100644 tutorials_old/tutorial_02_fluid_particles.ipynb delete mode 100644 tutorials_old/tutorial_03_discrete_derham.ipynb delete mode 100644 tutorials_old/tutorial_06_poisson.ipynb delete mode 100644 tutorials_old/tutorial_07_heat_equation.ipynb delete mode 100644 tutorials_old/tutorial_08_maxwell.ipynb delete mode 100644 tutorials_old/tutorial_09_vlasov_maxwell.ipynb delete mode 100644 tutorials_old/tutorial_10_linear_mhd.ipynb delete mode 100644 tutorials_old/tutorial_12_struphy_data_pproc.ipynb diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 42bf2b522..000000000 --- a/.dockerignore +++ /dev/null @@ -1,11 +0,0 @@ -Dockerfile -*.dockerfile* -*.md -LICENSE -env/ -doc/ -**/*.so -**/__gpyccel__* -**/__psydac__* -**/__pycache__* -**/.git \ No newline at end of file diff --git a/.gitlab/issue_templates/struphy_issue.md b/.gitlab/issue_templates/struphy_issue.md deleted file mode 100644 index b2c4eb7bc..000000000 --- a/.gitlab/issue_templates/struphy_issue.md +++ /dev/null @@ -1,13 +0,0 @@ -**Bug description / feature request:** - -... - -**Expected behavior:** - -... - -**Proposed solution:** - -... - - diff --git a/.gitlab/merge_request_templates/struphy_merge_request.md b/.gitlab/merge_request_templates/struphy_merge_request.md deleted file mode 100644 index 87f9ff3cc..000000000 --- a/.gitlab/merge_request_templates/struphy_merge_request.md +++ /dev/null @@ -1,15 +0,0 @@ -**Solves the following issue(s):** - -Closes #... - -**Core changes:** - -None - -**Model-specific changes:** - -None - -**Documentation changes:** - -None \ No newline at end of file diff --git a/apt.txt b/apt.txt deleted file mode 100644 index 7abccc7e4..000000000 --- a/apt.txt +++ /dev/null @@ -1,4 +0,0 @@ -gfortran -gcc -liblapack-dev -libblas-dev \ No newline at end of file diff --git a/tutorial_07_data_structures.ipynb b/tutorial_07_data_structures.ipynb deleted file mode 100644 index dc21d7332..000000000 --- a/tutorial_07_data_structures.ipynb +++ /dev/null @@ -1,969 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 7 - Struphy data structures\n", - "\n", - "In this tutorial we will learn about the data structures used in Struphy to store FEEC and particle variables. \n", - "\n", - "For FEEC variables, Struphy relies on the distributed data structures provided by [Psydac](https://github.com/pyccel/psydac). In particular we need to understand the objects [StencilVector](https://github.com/pyccel/psydac/blob/652b29fdb813dce9836b34c0fd789c99cf779f17/psydac/linalg/stencil.py#L379) and [StencilMatrix](https://github.com/pyccel/psydac/blob/652b29fdb813dce9836b34c0fd789c99cf779f17/psydac/linalg/stencil.py#L859).\n", - "\n", - "Regarding particle (PIC) variables, we will look at Struphy's [markers attribute](https://struphy.pages.mpcdf.de/struphy/sections/developers.html#struphy.pic.base.Particles.markers) of the [Particle class](https://struphy.pages.mpcdf.de/struphy/sections/developers.html#particle-base-class), which is just a static 2D numpy array. The communication of particles from one process to anther is done via the method [mpi_sort_markers](https://struphy.pages.mpcdf.de/struphy/sections/developers.html#struphy.pic.base.Particles.mpi_sort_markers).\n", - "\n", - "## FEEC data structures\n", - "\n", - "The [3D Derham sequence](https://struphy.pages.mpcdf.de/struphy/sections/discretization.html#id3) has two scalar-valued spaces, namely $H^1$ and $L^2$, and two vector-valued spaces, namely $H$(curl) and $H$(div). In the discrete case, members of $V^0_h\\subset H^1$ and of $V^3_h \\subset L^2$ are characterized by their FE coefficients in $\\mathbb R^{N_0}$ and $\\mathbb R^{N_3}$, respectively, and members $V^1_h\\subset H$(curl) and of $V^2_h \\subset H$(div) are characterized by their FE coefficients in $\\mathbb R^{N_{1, 1} + N_{1,2} + N_{1,3}}$ and $\\mathbb R^{N_{2, 1} + N_{2,2} + N_{2,3}}$. The scalar-valued coefficients are stored in [StencilVector](https://github.com/pyccel/psydac/blob/652b29fdb813dce9836b34c0fd789c99cf779f17/psydac/linalg/stencil.py#L379) format, whereas the vector-valued coefficients are stored in [BlockVector](https://github.com/pyccel/psydac/blob/652b29fdb813dce9836b34c0fd789c99cf779f17/psydac/linalg/block.py#L153) format. A `BlockVector` is nothing else than a 3-list of `StencilVectors`, so it is enough to understand the `StencilVector` object.\n", - "\n", - "`StencilVectors` are elements of a [StencilVectorSpace](https://github.com/pyccel/psydac/blob/652b29fdb813dce9836b34c0fd789c99cf779f17/psydac/linalg/stencil.py#L84). A linear mapping between two `StencilVectorSpaces` can be represented by a [StencilMatrix](https://github.com/pyccel/psydac/blob/652b29fdb813dce9836b34c0fd789c99cf779f17/psydac/linalg/stencil.py#L859), which is a special kind of [LinearOperator](https://github.com/pyccel/psydac/blob/652b29fdb813dce9836b34c0fd789c99cf779f17/psydac/linalg/basic.py#L193). For the vector-valued equivalent [BlockVectorSpace](https://github.com/pyccel/psydac/blob/652b29fdb813dce9836b34c0fd789c99cf779f17/psydac/linalg/block.py#L18) the corresponding matrix is a [BlockLinearOperator](https://github.com/pyccel/psydac/blob/652b29fdb813dce9836b34c0fd789c99cf779f17/psydac/linalg/block.py#L462), which is nothing else than a nested list of `StencilMatrices`. Thus, it is enough to understand the `StencilMatrix` object.\n", - "\n", - "### StencilVector - serial case\n", - "\n", - "`StencilVectors` are initialized from a [StencilVectorSpace](https://github.com/pyccel/psydac/blob/652b29fdb813dce9836b34c0fd789c99cf779f17/psydac/linalg/stencil.py#L84). In Struphy, all necessary `StencilVectorSpaces` can be read out from the [Derham](https://struphy.pages.mpcdf.de/struphy/sections/developers.html?highlight=derham#derham-sequence-3d) object (for an in-dpeth view on the `Derham` class please go to the API [discrete de Rham sequence](https://struphy.pages.mpcdf.de/struphy/api/discrete_derham.html#API:-Discrete-de-Rham-sequence))." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "from psydac.linalg.stencil import StencilVector\n", - "\n", - "from struphy.feec.psydac_derham import Derham\n", - "\n", - "Nel = [8, 8, 12] # number of elements\n", - "p = [2, 3, 4] # spline degrees\n", - "# spline boundary conditions (periodic=True, clamped=False)\n", - "spl_kind = [False, False, True]\n", - "\n", - "# Psydac discrete Derham sequence\n", - "dr_serial = Derham(Nel, p, spl_kind)\n", - "\n", - "# element of V0_h\n", - "x0 = StencilVector(dr_serial.Vh[\"0\"])\n", - "\n", - "assert np.all(x0[:] == 0.0)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can see that a `StencilVector` is initialized with zeros. Moreover, it can be [indexed and sliced](https://numpy.org/doc/stable/user/basics.indexing.html) like a numpy array:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"{type(x0) = }\")\n", - "print(f\"{type(x0[:]) = }\")\n", - "print(f\"{type(x0[:, :, :]) = }\")\n", - "print(f\"{type(x0[:2, 1:2:7, :-1]) = }\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "With this, we can set specific entries in the `StencilVector`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"{x0[3, 2, 1] = }\")\n", - "x0[3, 2, 1] = 99.0\n", - "print(f\"{x0[3, 2, 1] = }\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "When indexing a `StencilVector`, it important to note that **global indices (!)** have to be used. These are the indices of the full (global) 3D array, one could call them \"mathematical indices\". This will become important when we run in parallel below. The global indices available on the current process can be obtained from the attributes of the `StencilVector`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.utils.utils import print_all_attr\n", - "\n", - "print_all_attr(x0)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The global start and end indices in each direction are:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"{x0.starts = }\")\n", - "print(f\"{x0.ends = }\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us explain why these values make sense. The dimension of a 1D-periodic spline space is `Nel`, and of a 1D-clamped spline space it is `Nel + p`. In our case:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "dims = [Ni + pi * (not spi) for Ni, pi, spi in zip(Nel, p, spl_kind)]\n", - "print(f\"{dims = }\")\n", - "print(f\"{dims[0]*dims[1]*dims[2] = }\" + \" = total dimension of vector space\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Because we are running on just one process at the moment (no domain decomposition), the start index is zero and the end index is `dims - 1` in each direction, exactly what is is given by `x0.starts` and `x0.ends`.\n", - "\n", - "The data of a `StencilVector` is stored in a `numpy` array. This array can be accessed in different ways:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"{type(x0[:]) = }, {np.shape(x0[:]) = }\")\n", - "print(f\"{type(x0[:, :, :]) = }, {np.shape(x0[:, :, :]) = }\")\n", - "print(f\"{type(x0._data) = }, {np.shape(x0._data) = }\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Actually, these are not the same arrays in memory, but copies of each other:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"{id(x0[:]) = }\")\n", - "print(f\"{id(x0[:, :, :]) = }\")\n", - "print(f\"{id(x0._data) = }\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us try to understand the `shape` of these numpy arrays (printed above). Indeed, these shapes do not correspond to `dims`, as one could have expected. This is because the numpy arrays also contain the `ghost regions` needed for MPI communication. In each of the three directions, there is a ghost region of size `p` attached to the left and to the right of the actual domain. Thus, we expect the array size to be `dim + 2*p` in each direction. Let's check: " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "shape = [dim + 2 * pi for dim, pi in zip(dims, p)]\n", - "print(f\"{shape = }\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The presence of ghost regions needs to be taken into account when `indexing` a `StencilVector`. For example, if we want to write a vector `a` into our `StencilVector`, we have to be careful with the `slicing`: " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "a = np.arange(dims[0] * dims[1] * dims[2]).reshape(*dims)\n", - "x0[:] = 99.0\n", - "\n", - "s = x0.starts\n", - "e = x0.ends\n", - "x0[s[0] : e[0] + 1, s[1] : e[1] + 1, s[2] : e[2] + 1] = a\n", - "print(f\"{x0[0, 0, :4] = }\")\n", - "print(f\"{x0[0, :4, 0] = }\")\n", - "print(f\"{x0[:4, 0, 0] = }\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note that the output after slicing `:4` is an array of size 8, which contains the ghost regions. This is because the ghost regions are addressed with negative indices. Let us verify:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "x0[0, 0, -1] = 11\n", - "print(f\"{x0[0, 0, :4] = }\")\n", - "print(f\"{x0[0, 0, 0:4] = }\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There is also a way to use **local indices** for addressing a `StencilVector`, namely by writing directly into the `_data` attribute. In this case the usual numpy indexing of the array applies, **and the ghost regions have to be taken into account explicitly (!)** via the `pads`. The `pads` attribute holds the size of the ghost regions, which is always `p`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "x0[0, 0, -1] = 99.0\n", - "y0 = StencilVector(dr_serial.Vh[\"0\"])\n", - "y0[:] = 99.0\n", - "\n", - "pd = y0.pads\n", - "print(f\"{pd = }\")\n", - "y0._data[pd[0] : -pd[0], pd[1] : -pd[1], pd[2] : -pd[2]] = a\n", - "print(f\"{y0[0, 0, :4] = }\")\n", - "print(f\"{y0[0, :4, 0] = }\")\n", - "print(f\"{y0[:4, 0, 0] = }\")\n", - "\n", - "assert np.all(x0[:] == y0[:])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, let us inspect the `to_array()` method of a `StencilVector`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"{x0.shape = }\")\n", - "print(f\"{dims[0]*dims[1]*dims[2] = }\")\n", - "print(f\"{type(x0.toarray()) = }\")\n", - "print(f\"{x0.toarray().shape = }\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The result of `toarray()` is obviously a flattened version of the 3D numpy array holding the data, **without ghost regions (!)**. The ordering is `row-major` (C-style):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "flat_data = x0[s[0] : e[0] + 1, s[1] : e[1] + 1, s[2] : e[2] + 1].flatten()\n", - "assert np.all(x0.toarray() == flat_data)\n", - "print(f\"{x0.toarray()[:4] = }\")\n", - "print(f\"{x0.toarray()[-4:] = }\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### StencilMatrix - serial case\n", - "\n", - "The initialization is similar to a `StencilVector`, but this time we have to state the `domain` and `codomain` of the linear mapping:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from psydac.linalg.stencil import StencilMatrix\n", - "\n", - "A0 = StencilMatrix(dr_serial.Vh[\"0\"], dr_serial.Vh[\"0\"])\n", - "\n", - "assert np.all(A0[:, :] == 0.0)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can see that a `StencilMatrix` is initialized with zeros. Moreover, it can be [indexed and sliced](https://numpy.org/doc/stable/user/basics.indexing.html) like a numpy array:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"{type(A0) = }\")\n", - "print(f\"{type(A0[:, :]) = }\")\n", - "print(f\"{type(A0[:, :, :, :, :, :]) = }\")\n", - "print(f\"{type(A0[:2, 1:2:7, :-1, :, 2, :]) = }\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "With this, we can set specific entries in the `StencilMatrix`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"{A0[3, 2, 1, 0, 0, 0] = }\")\n", - "A0[3, 2, 1, 0, 0, 0] = 99.0\n", - "print(f\"{A0[3, 2, 1, 0, 0, 0] = }\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "When indexing a `StencilMatrix`, it important to note that **global indices (!)** have to be used **for the row indices (!)**. These are the indices of the rows of the full (global) 3D array, one could call them \"mathematical row indices\". This will become important when we run in parallel below. The global row-indices available on the current process can be obtained from the attribute `codomain` of the `StencilMatrix`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print_all_attr(A0)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"{A0.codomain.starts = }\")\n", - "print(f\"{A0.codomain.ends = }\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The `row-indexing` works pretty much as for the `StencilVector` described above. The characteristic feature of a `StencilMatrix` is the column storage. Namely, it stores only `2*p + 1` (off-)diagonals for each direction:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"{[2*pi + 1 for pi in p] = }\")\n", - "print(f\"{A0[0, 0, 0, :, :, :].shape = }\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The column index runs between `[-p, p]` and the diagonal is at `0` (!), which means that lower diagonals are addressed with negativ indices:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "s = A0.codomain.starts\n", - "e = A0.codomain.ends\n", - "\n", - "for n in range(-p[2], p[2] + 1):\n", - " A0[0, 0, s[2] : e[2] + 1, :, :, n] = n * 10\n", - "\n", - "print(\"A0[0, 0, :, 0, 0, :] = \")\n", - "print(A0[0, 0, :, 0, 0, :])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note the ghost regions in the row index, as in the case of the `StencilVector`. \n", - "\n", - "Beware that **the above output is not the actual matrix (!)**, but merely the array in which the (off-)diagonals are stored. The actual matrix can be obtained from `toarray()`. For simplicity, we shall show this in the 1D case:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "vector_space_1d = dr_serial.Vh_fem[\"0\"].spaces[2].coeff_space\n", - "A0_1d = StencilMatrix(vector_space_1d, vector_space_1d)\n", - "\n", - "s = A0_1d.codomain.starts\n", - "e = A0_1d.codomain.ends\n", - "\n", - "for n in range(-p[2], p[2] + 1):\n", - " A0_1d[s[0] : e[0] + 1, n] = n * 10\n", - "\n", - "print(\"A0_1d[0, 0, :, 0, 0, :] = \")\n", - "print(A0_1d[:, :])\n", - "\n", - "print(\"\\nA0_1d.toarray() = \")\n", - "print(A0_1d.toarray())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Much like for the `StencilVector`, there is also a way to use **local indices** for addressing a `StencilMatrix`, namely by writing directly into the `_data` attribute. In this case the usual numpy indexing of the array applies, **and the ghost regions have to be taken into account explicitly (!)** via the `pads`. In contrast to the global indexing, the columns are addressed with indices between `[0, 2*p]`, where the diagonal is at `p` (and not at `0`):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "a = np.arange(dims[0] * dims[1] * dims[2]).reshape(*dims)\n", - "\n", - "A0[:, :] = 99.0\n", - "\n", - "s = A0.codomain.starts\n", - "e = A0.codomain.ends\n", - "pd = A0.pads\n", - "\n", - "A0[s[0] : e[0] + 1, s[1] : e[1] + 1, s[2] : e[2] + 1, 0, 0, 0] = a\n", - "\n", - "B0 = StencilMatrix(dr_serial.Vh[\"0\"], dr_serial.Vh[\"0\"])\n", - "\n", - "B0[:, :] = 99.0\n", - "\n", - "s = B0.codomain.starts\n", - "e = B0.codomain.ends\n", - "pd = B0.pads\n", - "\n", - "B0._data[pd[0] : -pd[0], pd[1] : -pd[1], pd[2] : -pd[2], p[0], p[1], p[2]] = a\n", - "\n", - "assert np.all(A0[:, :] == B0[:, :])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, let us inspect the `to_array()` method of a `StencilMatrix`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"{A0.shape = }\")\n", - "print(f\"{dims[0]*dims[1]*dims[2] = }\")\n", - "print(f\"{type(A0.toarray()) = }\")\n", - "print(f\"{A0.toarray().shape = }\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The result of `toarray()` is the full matrix, with the domain and codomain flattened in `row-major` (C-style), and **without ghost regions (!)**." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### StencilVector - parallel case\n", - "\n", - "The distributed `StencilVector` can be understood via the following sketch:\n", - "\n", - "![StencilVector](../pics/stencil_vec.jpg)\n", - "\n", - "If we want to show the distributed character of the `StencilVector` in an example, we must run with `ipyparallel`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import ipyparallel as ipp\n", - "\n", - "\n", - "def stencil_vec_shape():\n", - " import numpy as np\n", - " from psydac.ddm.mpi import mpi as MPI\n", - " from psydac.linalg.stencil import StencilVector\n", - "\n", - " from struphy.feec.psydac_derham import Derham\n", - "\n", - " comm = MPI.COMM_WORLD\n", - " rank = comm.Get_rank()\n", - "\n", - " Nel = [8, 8, 12] # number of elements\n", - " p = [2, 3, 4] # spline degrees\n", - " # spline boundary conditions (periodic=True, clamped=False)\n", - " spl_kind = [False, False, True]\n", - "\n", - " dr = Derham(Nel, p, spl_kind, comm=comm)\n", - "\n", - " x0 = StencilVector(dr.Vh[\"0\"])\n", - "\n", - " assert np.all(x0[:] == 0.0)\n", - "\n", - " out = f\"{rank = }, {x0.starts = }, {x0.ends = }, {x0.pads = }, {np.shape(x0[:]) = }:\"\n", - "\n", - " return out\n", - "\n", - "\n", - "with ipp.Cluster(engines=\"mpi\", n=2) as rc:\n", - " view = rc.broadcast_view()\n", - " r = view.apply_sync(stencil_vec_shape)\n", - " print(\"\\n\".join(r))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here, we can clearly see the parallelization (domain decomposition) in the third direction, manifest in the different `starts` and `ends` on each process. These are the global, or \"mathematical\" indices. The `shape` of 14 in the third direction can be understood as follows:\n", - "\n", - "The total dimension in the third direction is 12. This has been split into two equal parts by the domain decomposition, meaning that coefficients with indices `[0, 5]` are stored on the first process, and coefficients with indices `[6, 11]` are stored on the second process. The size of the ghost regions (`pads`) is four in both cases, yielding 4 + 6 + 4 = 14. \n", - "\n", - "Next, let us look at the MPI communication via `update_ghost_regions()`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def stencil_vec_ghost():\n", - " import numpy as np\n", - " from psydac.ddm.mpi import mpi as MPI\n", - " from psydac.linalg.stencil import StencilVector\n", - "\n", - " from struphy.feec.psydac_derham import Derham\n", - "\n", - " comm = MPI.COMM_WORLD\n", - " rank = comm.Get_rank()\n", - "\n", - " Nel = [4, 4, 12] # number of elements\n", - " p = [2, 2, 2] # spline degrees\n", - " # spline boundary conditions (periodic=True, clamped=False)\n", - " spl_kind = [False, False, True]\n", - "\n", - " dr = Derham(Nel, p, spl_kind, comm=comm)\n", - "\n", - " x0 = StencilVector(dr.Vh[\"0\"])\n", - " s = x0.starts\n", - " e = x0.ends\n", - " pd = x0.pads\n", - "\n", - " assert np.all(x0[:] == 0.0)\n", - "\n", - " x0[:] = -99.0\n", - " x0[s[0], s[1], s[2] : e[2] + 1] = np.arange(e[2] + 1 - s[2]) * 10**rank\n", - "\n", - " out = f\"{rank = }, before update: {x0[s[0], s[1], :] = }:\"\n", - "\n", - " x0.update_ghost_regions()\n", - "\n", - " out += f\"\\n{rank = }, after update: {x0[s[0], s[1], :] = }:\"\n", - "\n", - " return out\n", - "\n", - "\n", - "with ipp.Cluster(engines=\"mpi\", n=3) as rc:\n", - " view = rc.broadcast_view()\n", - " r = view.apply_sync(stencil_vec_ghost)\n", - " print(\"\\n\".join(r))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As we can see from the different ranks, the logic is exactly the one displayed in the picture above. The ghost regions accept the incoming data from the neighboring processes. Ghost regions do not send out data.\n", - "\n", - "Finally, let us check the `toarray()` function in the parallel case:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def stencil_vec_toarray():\n", - " import numpy as np\n", - " from psydac.ddm.mpi import mpi as MPI\n", - " from psydac.linalg.stencil import StencilVector\n", - "\n", - " from struphy.feec.psydac_derham import Derham\n", - "\n", - " comm = MPI.COMM_WORLD\n", - " rank = comm.Get_rank()\n", - "\n", - " Nel = [8, 8, 12] # number of elements\n", - " p = [2, 3, 4] # spline degrees\n", - " # spline boundary conditions (periodic=True, clamped=False)\n", - " spl_kind = [False, False, True]\n", - "\n", - " dr = Derham(Nel, p, spl_kind, comm=comm)\n", - "\n", - " x0 = StencilVector(dr.Vh[\"0\"])\n", - "\n", - " assert np.all(x0[:] == 0.0)\n", - "\n", - " out = f\"{rank = }, {np.shape(x0.toarray()) = }, {np.shape(x0.toarray_local()) = }\"\n", - "\n", - " return out\n", - "\n", - "\n", - "with ipp.Cluster(engines=\"mpi\", n=2) as rc:\n", - " view = rc.broadcast_view()\n", - " r = view.apply_sync(stencil_vec_toarray)\n", - " print(\"\\n\".join(r))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The `toarray_local()` gives only the (flattened) local data, whereas `toarray()` yields global size. In the latter case, data points that are not available on the current process are filled with zeros." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### StencilMatrix - parallel case\n", - "\n", - "The distributed `StencilMatrix` can be understood via the following sketch:\n", - "\n", - "![StencilMatrix](../pics/stencil_matrix.jpg)\n", - "\n", - "It is important to note that only the row indices are distributed, and that they are distributed much like a `StencilVector`. The `2+p + 1` (off-)diagonals are stored in a numpy array. The indexing has been explained above.\n", - "\n", - "If we want to show the distributed character of the `StencilMatrix` in an example, we must run with `ipyparallel`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def stencil_mat_shape():\n", - " import numpy as np\n", - " from psydac.ddm.mpi import mpi as MPI\n", - " from psydac.linalg.stencil import StencilMatrix\n", - "\n", - " from struphy.feec.psydac_derham import Derham\n", - "\n", - " comm = MPI.COMM_WORLD\n", - " rank = comm.Get_rank()\n", - "\n", - " Nel = [8, 8, 12] # number of elements\n", - " p = [2, 3, 4] # spline degrees\n", - " # spline boundary conditions (periodic=True, clamped=False)\n", - " spl_kind = [False, False, True]\n", - "\n", - " dr = Derham(Nel, p, spl_kind, comm=comm)\n", - "\n", - " A0 = StencilMatrix(dr.Vh[\"0\"], dr.Vh[\"0\"])\n", - "\n", - " assert np.all(A0[:, :] == 0.0)\n", - "\n", - " out = f\"{rank = }, {A0.codomain.starts = }, {A0.codomain.ends = }, {A0.pads = }, {np.shape(A0[:, :]) = }:\"\n", - "\n", - " return out\n", - "\n", - "\n", - "with ipp.Cluster(engines=\"mpi\", n=2) as rc:\n", - " view = rc.broadcast_view()\n", - " r = view.apply_sync(stencil_mat_shape)\n", - " print(\"\\n\".join(r))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here, we can clearly see the parallelization (domain decomposition) of the row indices in the third direction, manifest in the different `starts` and `ends` on each process. These are the global, or \"mathematical\" indices. The `shape` of 14 in the third direction has been explained above. The column dimensions are `2*p + 1`. \n", - "\n", - "Next, let us look at the MPI communication via `update_ghost_regions()`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def stencil_mat_ghost():\n", - " import numpy as np\n", - " from psydac.ddm.mpi import mpi as MPI\n", - " from psydac.linalg.stencil import StencilMatrix\n", - "\n", - " from struphy.feec.psydac_derham import Derham\n", - "\n", - " comm = MPI.COMM_WORLD\n", - " rank = comm.Get_rank()\n", - "\n", - " Nel = [4, 4, 12] # number of elements\n", - " p = [2, 2, 2] # spline degrees\n", - " # spline boundary conditions (periodic=True, clamped=False)\n", - " spl_kind = [False, False, True]\n", - "\n", - " dr = Derham(Nel, p, spl_kind, comm=comm)\n", - "\n", - " A0 = StencilMatrix(dr.Vh[\"0\"], dr.Vh[\"0\"])\n", - " s = A0.codomain.starts\n", - " e = A0.codomain.ends\n", - " pd = A0.pads\n", - "\n", - " assert np.all(A0[:, :] == 0.0)\n", - "\n", - " A0[:, :] = -99.0\n", - " A0[s[0], s[1], s[2] : e[2] + 1, 0, 0, -pd[2] : pd[2] + 1] = (\n", - " np.arange((e[2] + 1 - s[2]) * (2 * pd[2] + 1)).reshape(e[2] + 1 - s[2], 2 * pd[2] + 1) * 10**rank\n", - " )\n", - "\n", - " out = f\"{rank = }, before update: A0[s[0], s[1], :, 0, 0, :] = \\n{A0[s[0], s[1], :, 0, 0, :]}:\"\n", - "\n", - " A0.update_ghost_regions()\n", - "\n", - " out += f\"\\n{rank = }, after update: A0[s[0], s[1], :, 0, 0, :] = \\n{A0[s[0], s[1], :, 0, 0, :]}:\"\n", - "\n", - " return out\n", - "\n", - "\n", - "with ipp.Cluster(engines=\"mpi\", n=2) as rc:\n", - " view = rc.broadcast_view()\n", - " r = view.apply_sync(stencil_mat_ghost)\n", - " print(\"\\n\".join(r))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As we can see from the different ranks, the logic for the row indices is exactly the one displayed for the `StencilVector`. The ghost regions accept the incoming data from the neighboring processes. Ghost regions do not send out data.\n", - "\n", - "Finally, let us check the `toarray()` function in the parallel case:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def stencil_mat_toarray():\n", - " import numpy as np\n", - " from psydac.ddm.mpi import mpi as MPI\n", - " from psydac.linalg.stencil import StencilMatrix\n", - "\n", - " from struphy.feec.psydac_derham import Derham\n", - "\n", - " comm = MPI.COMM_WORLD\n", - " rank = comm.Get_rank()\n", - "\n", - " Nel = [8, 8, 12] # number of elements\n", - " p = [2, 3, 4] # spline degrees\n", - " # spline boundary conditions (periodic=True, clamped=False)\n", - " spl_kind = [False, False, True]\n", - "\n", - " dr = Derham(Nel, p, spl_kind, comm=comm)\n", - "\n", - " A0 = StencilMatrix(dr.Vh[\"0\"], dr.Vh[\"0\"])\n", - "\n", - " assert np.all(A0[:, :] == 0.0)\n", - "\n", - " out = f\"{rank = }, {np.shape(A0.toarray()) = }\"\n", - "\n", - " return out\n", - "\n", - "\n", - "with ipp.Cluster(engines=\"mpi\", n=2) as rc:\n", - " view = rc.broadcast_view()\n", - " r = view.apply_sync(stencil_mat_toarray)\n", - " print(\"\\n\".join(r))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The `toarray()` method gives the (flattened) global size. Data points that are not available on the current process are filled with zeros." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## PIC data structures\n", - "\n", - "In Struphy all PIC related information is stored in the base class [Particles](https://struphy.pages.mpcdf.de/struphy/sections/developers.html#struphy.pic.base.Particles). In particular, particles are stored in [Particles.markers](https://struphy.pages.mpcdf.de/struphy/sections/developers.html#struphy.pic.base.Particles.markers), which is a simple 2D numpy array. The row index refers to the particles and the column index to the attributes of particles (coordinates, weights, etc.). \n", - "\n", - "The `markers`-array is a static array with more rows than the actual number of particles on a process, because of redistribution of particles when positions change (domain decomposition). Markers sent to another process leave behind a \"hole\", which can then be filled by another incoming marker. The marker communication is accomplished by [Particles.mpi_sort_markers](https://struphy.pages.mpcdf.de/struphy/sections/developers.html#struphy.pic.base.Particles.mpi_sort_markers).\n", - "\n", - "### The markers array\n", - "\n", - "We start by looking at the different particle classes that are available in Struphy:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import inspect\n", - "import sys\n", - "\n", - "from struphy.pic import particles\n", - "\n", - "for name, obj in inspect.getmembers(particles):\n", - " if inspect.isclass(obj) and obj.__module__ == particles.__name__:\n", - " print(obj)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We now create an instance of `Particles6D` with default parameters:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.pic.particles import Particles6D\n", - "from struphy.pic.utilities import LoadingParameters\n", - "\n", - "loading_params = LoadingParameters(Np=120)\n", - "particles = Particles6D(loading_params=loading_params)\n", - "\n", - "particles.draw_markers()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Some important attributes are: the total number of markers, the shape of the markers array, and the shape of the array with \"holes\" removed:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"{particles.Np = }\")\n", - "print(f\"{particles.markers.shape = }\")\n", - "print(f\"{particles.markers_wo_holes.shape = }\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us look at some parameters/coordinates of the first five markers:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"{particles.positions[:5] = }\\n\")\n", - "print(f\"{particles.velocities[:5] = }\\n\")\n", - "print(f\"{particles.phasespace_coords[:5] = }\\n\")\n", - "print(f\"{particles.weights[:5] = }\\n\")\n", - "print(f\"{particles.sampling_density[:5] = }\\n\")\n", - "print(f\"{particles.weights0[:5] = }\\n\")\n", - "print(f\"{particles.marker_ids[:5] = }\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "env", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.3" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/tutorials_old/tutorial_01_kinetic_particles.ipynb b/tutorials_old/tutorial_01_kinetic_particles.ipynb deleted file mode 100644 index 4ed692ed9..000000000 --- a/tutorials_old/tutorial_01_kinetic_particles.ipynb +++ /dev/null @@ -1,1390 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 1 - Kinetic particles\n", - "\n", - "Topics covered in this tutorial:\n", - "\n", - "- basic functionalities of [Particles6D and Particles5D](https://struphy.pages.mpcdf.de/struphy/sections/subsections/pic_base.html#base-modules) classes\n", - "- particle boundary conditions\n", - "- particle drawing on a disc\n", - "- time stepping using some [Particle Propagators](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_markers.html#particle-propagators)\n", - "- instantiating [Cuboid](https://struphy.pages.mpcdf.de/struphy/sections/subsections/domains_avail.html#struphy.geometry.domains.Cuboid), [HollowCylinder](https://struphy.pages.mpcdf.de/struphy/sections/subsections/domains_avail.html#struphy.geometry.domains.HollowCylinder) and [Tokamak](https://struphy.pages.mpcdf.de/struphy/sections/subsections/domains_avail.html#struphy.geometry.domains.Tokamak) mappings\n", - "- instantiation of [ProjectedMHDequilibrium](https://struphy.pages.mpcdf.de/struphy/sections/subsections/feec_projected_mhd.html#module-struphy.fields_background.mhd_equil.projected_equils) object\n", - "- plotting Tokamak coordinates in Struphy\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Particles in a box\n", - "\n", - "Let $\\Omega \\subset \\mathbb R^3$ be a box (cuboid). We search for trajectories $(\\mathbf x_p, \\mathbf v_p): [0,T] \\to \\Omega \\times \\mathbb R^3$, $p = 0, \\ldots, N-1$ that satisfy\n", - "\n", - "$$\n", - "\\begin{align}\n", - " \\dot{\\mathbf x}_p &= \\mathbf v_p\\,,\\qquad && \\mathbf x_p(0) = \\mathbf x_{p0}\\,,\n", - " \\\\[2mm]\n", - " \\dot{\\mathbf v}_p &= 0 \\qquad && \\mathbf v_p(0) = \\mathbf v_{p0}\\,.\n", - " \\end{align}\n", - "$$\n", - "\n", - "In Struphy, the position coordinates are updated in logical space $[0, 1]^3 = F^{-1}(\\Omega)$, for instance with the Propagator [PushEta](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_markers.html#struphy.propagators.propagators_markers.PushEta) which we shall use in what follows." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.geometry.domains import Cuboid\n", - "\n", - "l1 = -5\n", - "r1 = 5.0\n", - "l2 = -7\n", - "r2 = 7.0\n", - "l3 = -1.0\n", - "r3 = 1.0\n", - "domain = Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.pic.particles import Particles6D\n", - "\n", - "Np = 15\n", - "bc = [\"reflect\", \"reflect\", \"periodic\"]\n", - "loading_params = {\"seed\": None}\n", - "\n", - "# instantiate Particle object\n", - "particles = Particles6D(Np=Np, bc=bc, domain=domain, loading_params=loading_params)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "particles.draw_markers()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "particles.positions" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# positions on the physical domain Omega\n", - "pushed_pos = domain(particles.positions).T\n", - "pushed_pos" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "particles.velocities" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from matplotlib import pyplot as plt\n", - "\n", - "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", - "\n", - "fig = plt.figure()\n", - "ax = fig.gca()\n", - "\n", - "for i, pos in enumerate(pushed_pos):\n", - " ax.scatter(pos[0], pos[1], c=colors[i % 4])\n", - " ax.arrow(\n", - " pos[0], pos[1], particles.velocities[i, 0], particles.velocities[i, 1], color=colors[i % 4], head_width=0.2\n", - " )\n", - "\n", - "ax.plot([l1, l1], [l2, r2], \"k\")\n", - "ax.plot([r1, r1], [l2, r2], \"k\")\n", - "ax.plot([l1, r1], [l2, l2], \"k\")\n", - "ax.plot([l1, r1], [r2, r2], \"k\")\n", - "ax.set_xlim(-6.5, 6.5)\n", - "ax.set_ylim(-9, 9)\n", - "ax.set_title(\"Initial conditions\");" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.propagators.propagators_markers import PushEta\n", - "\n", - "# default parameters of Propagator\n", - "opts_eta = PushEta.options(default=True)\n", - "print(opts_eta)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# pass simulation parameters to Propagator class\n", - "PushEta.domain = domain" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# instantiate Propagator object\n", - "prop_eta = PushEta(particles)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import math\n", - "\n", - "import numpy as np\n", - "\n", - "# time stepping\n", - "Tend = 10.0\n", - "dt = 0.2\n", - "Nt = int(Tend / dt)\n", - "\n", - "pos = np.zeros((Nt + 1, Np, 3), dtype=float)\n", - "alpha = np.ones(Nt + 1, dtype=float)\n", - "\n", - "pos[0] = pushed_pos\n", - "\n", - "time = 0.0\n", - "n = 0\n", - "while time < (Tend - dt):\n", - " time += dt\n", - " n += 1\n", - "\n", - " # advance in time\n", - " prop_eta(dt)\n", - "\n", - " # positions on the physical domain Omega\n", - " pos[n] = domain(particles.positions).T\n", - "\n", - " # scaling for plotting\n", - " alpha[n] = (Tend - time) / Tend" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "for i in range(Np):\n", - " ax.scatter(pos[:, i, 0], pos[:, i, 1], c=colors[i % 4], alpha=alpha)\n", - "\n", - "ax.plot([l1, l1], [l2, r2], \"k\")\n", - "ax.plot([r1, r1], [l2, r2], \"k\")\n", - "ax.plot([l1, r1], [l2, l2], \"k\")\n", - "ax.plot([l1, r1], [r2, r2], \"k\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_xlim(-6.5, 6.5)\n", - "ax.set_ylim(-9, 9)\n", - "ax.set_title(f\"{math.ceil(Tend / dt)} time steps (full color at t=0)\")\n", - "fig" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Particles in a cylinder\n", - "\n", - "We use the same setup as before but change the domain $\\Omega$ to a cylinder. We explore two options for drawing markers in posittion space:\n", - "\n", - "- uniform in logical space $[0, 1]^3 = F^{-1}(\\Omega)$\n", - "- uniform on the cylinder $\\Omega$" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.geometry.domains import HollowCylinder\n", - "\n", - "a1 = 0.0\n", - "a2 = 5.0\n", - "Lz = 1.0\n", - "domain = HollowCylinder(a1=a1, a2=a2, Lz=Lz)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# instantiate Particle object\n", - "Np = 1000\n", - "bc = [\"remove\", \"periodic\", \"periodic\"]\n", - "loading_params = {\"seed\": None}\n", - "\n", - "particles = Particles6D(Np=Np, bc=bc, loading_params=loading_params)\n", - "\n", - "# instantiate another Particle object\n", - "name = \"test_uni\"\n", - "loading_params = {\"seed\": None, \"spatial\": \"disc\"}\n", - "particles_uni = Particles6D(Np=Np, bc=bc, loading_params=loading_params)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "particles.draw_markers()\n", - "particles_uni.draw_markers()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# positions on the physical domain Omega\n", - "pushed_pos = domain(particles.positions).T\n", - "pushed_pos_uni = domain(particles_uni.positions).T" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "fig = plt.figure(figsize=(10, 6))\n", - "\n", - "plt.subplot(1, 2, 1)\n", - "plt.scatter(pushed_pos[:, 0], pushed_pos[:, 1], s=2.0)\n", - "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", - "ax = plt.gca()\n", - "ax.add_patch(circle1)\n", - "ax.set_aspect(\"equal\")\n", - "plt.xlabel(\"x\")\n", - "plt.ylabel(\"y\")\n", - "plt.title(\"Draw uniform in logical space\")\n", - "\n", - "plt.subplot(1, 2, 2)\n", - "plt.scatter(pushed_pos_uni[:, 0], pushed_pos_uni[:, 1], s=2.0)\n", - "circle2 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", - "ax = plt.gca()\n", - "ax.add_patch(circle2)\n", - "ax.set_aspect(\"equal\")\n", - "plt.xlabel(\"x\")\n", - "plt.ylabel(\"y\")\n", - "plt.title(\"Draw uniform on disc\");" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# instantiate Particle object\n", - "Np = 15\n", - "bc = [\"reflect\", \"periodic\", \"periodic\"]\n", - "loading_params = {\"seed\": None}\n", - "\n", - "particles = Particles6D(Np=Np, bc=bc, domain=domain, loading_params=loading_params)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "particles.draw_markers()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# positions on the physical domain Omega\n", - "pushed_pos = domain(particles.positions).T\n", - "pushed_pos" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "fig = plt.figure()\n", - "ax = fig.gca()\n", - "\n", - "for n, pos in enumerate(pushed_pos):\n", - " ax.scatter(pos[0], pos[1], c=colors[n % 4])\n", - " ax.arrow(\n", - " pos[0], pos[1], particles.velocities[n, 0], particles.velocities[n, 1], color=colors[n % 4], head_width=0.2\n", - " )\n", - "\n", - "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", - "\n", - "ax.add_patch(circle1)\n", - "ax.set_aspect(\"equal\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_title(\"Initial conditions\");" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# pass simulation parameters to Propagator class\n", - "PushEta.domain = domain" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# instantiate Propagator object\n", - "prop_eta = PushEta(particles)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# time stepping\n", - "Tend = 10.0\n", - "dt = 0.2\n", - "Nt = int(Tend / dt)\n", - "\n", - "pos = np.zeros((Nt + 1, Np, 3), dtype=float)\n", - "alpha = np.ones(Nt + 1, dtype=float)\n", - "\n", - "pos[0] = pushed_pos\n", - "\n", - "time = 0.0\n", - "n = 0\n", - "while time < (Tend - dt):\n", - " time += dt\n", - " n += 1\n", - "\n", - " # advance in time\n", - " prop_eta(dt)\n", - "\n", - " # positions on the physical domain Omega\n", - " pos[n] = domain(particles.positions).T\n", - "\n", - " # scaling for plotting\n", - " alpha[n] = (Tend - time) / Tend" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# make scatter plot for each particle in xy-plane\n", - "for i in range(Np):\n", - " ax.scatter(pos[:, i, 0], pos[:, i, 1], c=colors[i % 4], alpha=alpha)\n", - "\n", - "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", - "\n", - "ax.add_patch(circle1)\n", - "ax.set_aspect(\"equal\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_title(f\"{math.ceil(Tend / dt)} time steps (full color at t=0)\")\n", - "fig" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Particles in a cylinder with a magnetic field\n", - "\n", - "Let $\\Omega \\subset \\mathbb R^3$ be a cylinder as before. Now, we search for trajectories $(\\mathbf x_p, \\mathbf v_p): [0,T] \\to \\Omega \\times \\mathbb R^3$, $p = 0, \\ldots, N-1$ that satisfy\n", - "\n", - "$$\n", - "\\begin{align}\n", - " \\dot{\\mathbf x}_p &= \\mathbf v_p\\,,\\qquad && \\mathbf x_p(0) = \\mathbf x_{p0}\\,,\n", - " \\\\[2mm]\n", - " \\dot{\\mathbf v}_p &= \\mathbf v_p \\times \\mathbf B_0(\\mathbf x_p) \\qquad && \\mathbf v_p(0) = \\mathbf v_{p0}\\,,\n", - " \\end{align}\n", - "$$\n", - "\n", - "where $\\mathbf B_0$ is a given magnetic field from an [MHDequilibrium](https://struphy.pages.mpcdf.de/struphy/sections/subsections/mhd_equils.html#mhd-equilibria). \n", - "In addition to the Propagator [PushEta](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_markers.html#struphy.propagators.propagators_markers.PushEta) for the position update, we shall use [PushVxB](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_markers.html#struphy.propagators.propagators_markers.PushVxB) for the velocity update." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.geometry.domains import HollowCylinder\n", - "\n", - "a1 = 0.0\n", - "a2 = 5.0\n", - "Lz = 1.0\n", - "domain = HollowCylinder(a1=a1, a2=a2, Lz=Lz)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# instantiate Particle object\n", - "Np = 20\n", - "bc = [\"remove\", \"periodic\", \"periodic\"]\n", - "loading_params = {\"seed\": None}\n", - "\n", - "particles = Particles6D(Np=Np, bc=bc, loading_params=loading_params)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "particles.draw_markers()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# positions on the physical domain Omega\n", - "pushed_pos = domain(particles.positions).T\n", - "pushed_pos" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "fig = plt.figure()\n", - "ax = fig.gca()\n", - "\n", - "for n, pos in enumerate(pushed_pos):\n", - " ax.scatter(pos[0], pos[1], c=colors[n % 4])\n", - " ax.arrow(\n", - " pos[0], pos[1], particles.velocities[n, 0], particles.velocities[n, 1], color=colors[n % 4], head_width=0.2\n", - " )\n", - "\n", - "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", - "\n", - "ax.add_patch(circle1)\n", - "ax.set_aspect(\"equal\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_title(\"Initial conditions\");" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.propagators.propagators_markers import PushVxB\n", - "\n", - "# default parameters of Propagator\n", - "opts_vxB = PushVxB.options(default=True)\n", - "print(opts_vxB)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.fields_background.equils import HomogenSlab\n", - "\n", - "B0x = 0.0\n", - "B0y = 0.0\n", - "B0z = 1.0\n", - "equil = HomogenSlab(B0x=B0x, B0y=B0y, B0z=B0z)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set domain for Cartesian MHD equilibrium\n", - "equil.domain = domain" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.feec.psydac_derham import Derham\n", - "from struphy.fields_background.projected_equils import ProjectedMHDequilibrium\n", - "\n", - "# instantiate Derham object\n", - "Nel = [16, 16, 32]\n", - "p = [1, 1, 3]\n", - "spl_kind = [False, True, True]\n", - "derham = Derham(Nel=Nel, p=p, spl_kind=spl_kind)\n", - "\n", - "# instantiate a projected MHD equilibrium object\n", - "proj_equil = ProjectedMHDequilibrium(equil, derham)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# pass simulation parameters to Propagator class\n", - "PushEta.domain = domain\n", - "PushVxB.domain = domain\n", - "PushVxB.derham = derham" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# instantiate Propagator object\n", - "prop_eta = PushEta(particles)\n", - "prop_vxB = PushVxB(particles, b2=proj_equil.b2)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# time stepping\n", - "Tend = 10.0 - 1e-6\n", - "dt = 0.2\n", - "Nt = int(Tend / dt)\n", - "\n", - "pos = []\n", - "alpha = np.ones(Nt + 1, dtype=float)\n", - "\n", - "marker_col = {}\n", - "for marker in particles.markers_wo_holes:\n", - " m_id = int(marker[-1])\n", - " marker_col[m_id] = colors[int(m_id) % 4]\n", - "ids_wo_holes = []\n", - "\n", - "time = 0.0\n", - "n = 0\n", - "while time < (Tend - dt):\n", - " time += dt\n", - " n += 1\n", - "\n", - " # advance in time\n", - " prop_vxB(dt / 2)\n", - " prop_eta(dt)\n", - " prop_vxB(dt / 2)\n", - "\n", - " # positions on the physical domain Omega (can change shape when particles are lost)\n", - " pos += [domain(particles.positions).T]\n", - "\n", - " # id's of non-holes\n", - " ids_wo_holes += [np.int64(particles.markers_wo_holes[:, -1])]\n", - "\n", - " # scaling for plotting\n", - " alpha[n] = (Tend - time) / Tend" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# make scatter plot for each particle in xy-plane\n", - "for po, ids, alph in zip(pos, ids_wo_holes, alpha):\n", - " cs = []\n", - " for ii in ids:\n", - " cs += [marker_col[ii]]\n", - " ax.scatter(po[:, 0], po[:, 1], c=cs, alpha=alph)\n", - "\n", - "circle1 = plt.Circle((0, 0), a2, color=\"k\", fill=False)\n", - "\n", - "ax.add_patch(circle1)\n", - "ax.set_aspect(\"equal\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_title(f\"{math.ceil(Tend / dt)} time steps (full color at t=0)\")\n", - "fig" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Particles in a Tokamak equilibrium\n", - "\n", - "We use the same Propagators from the previous example but load a more complicated [MHDequilibrium](https://struphy.pages.mpcdf.de/struphy/sections/subsections/mhd_equils.html#mhd-equilibria), namely from an ASDEX-Upgrade equilibrium stored in an EQDSK file.\n", - "\n", - "Let us instatiate an [EQDSKequilibrium](https://struphy.pages.mpcdf.de/struphy/sections/subsections/mhd_equils_sub.html#struphy.fields_background.mhd_equil.equils.EQDSKequilibrium) with many of its default parameters, except for the density:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.fields_background.equils import EQDSKequilibrium\n", - "\n", - "n1 = 0.0\n", - "n2 = 0.0\n", - "na = 1.0\n", - "equil = EQDSKequilibrium(n1=n1, n2=n2, na=na)\n", - "equil.params" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Since [EQDSKequilibrium](https://struphy.pages.mpcdf.de/struphy/sections/subsections/mhd_equils_sub.html#struphy.fields_background.mhd_equil.equils.EQDSKequilibrium) is an [AxisymmMHDequilibrium](https://struphy.pages.mpcdf.de/struphy/sections/subsections/mhd_equils_sub.html#struphy.fields_background.mhd_equil.base.AxisymmMHDequilibrium), which in turn is a [CartesianMHDequilibrium](https://struphy.pages.mpcdf.de/struphy/sections/subsections/mhd_equils_sub.html#struphy.fields_background.mhd_equil.base.CartesianMHDequilibrium), we are free to choose any mapping for the simulation (e.g. a Cuboid for Cartesian coordinates). In order to be conforming to the boundary of the equilibrium, we shall choose the [Tokamak](https://struphy.pages.mpcdf.de/struphy/sections/subsections/domains_avail.html#struphy.geometry.domains.Tokamak) mapping:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.geometry.domains import Tokamak\n", - "\n", - "Nel = (28, 72)\n", - "p = (3, 3)\n", - "psi_power = 0.6\n", - "psi_shifts = (1e-6, 1.0)\n", - "domain = Tokamak(equilibrium=equil, Nel=Nel, p=p, psi_power=psi_power, psi_shifts=psi_shifts)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "equil.domain = domain" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The [Tokamak](https://struphy.pages.mpcdf.de/struphy/sections/subsections/domains_avail.html#struphy.geometry.domains.Tokamak) domain is a [PoloidalSplineTorus](https://struphy.pages.mpcdf.de/struphy/sections/subsections/domains_base.html#struphy.geometry.base.PoloidalSplineTorus), hence\n", - "\n", - "$$\n", - " \\begin{align*}\n", - " x &= R \\cos(\\phi)\\,,\n", - " \\\\\n", - " y &= -R \\sin(\\phi)\\,,\n", - " \\\\\n", - " z &= Z\\,,\n", - " \\end{align*}\n", - "$$\n", - "\n", - "between Cartesian $(x, y, z)$- and Tokamak $(R, Z, \\phi)$-coordinates holds, where $(R, Z)$ spans a poloidal plane. Moreover, the Tokamak coordinates are related to general torus coordinates $(r, \\theta, \\phi)$ via a polar mapping in the poloidal plane:\n", - "\n", - "$$\n", - " \\begin{align*}\n", - " R &= R_0 + r \\cos(\\theta)\\,,\n", - " \\\\\n", - " Z &= r \\sin(\\theta)\\,,\n", - " \\\\\n", - " \\phi &= \\phi\\,.\n", - " \\end{align*}\n", - "$$\n", - "\n", - "The torus coordinates are related to Struphy logical coordinates $\\boldsymbol \\eta = (\\eta_1, \\eta_2, \\eta_3) \\in [0, 1]^3$ as \n", - "\n", - "$$\n", - " \\begin{align*}\n", - " r &= a_1 + (a_2 - a_1) \\eta_1\\,,\n", - " \\\\\n", - " \\theta &= 2\\pi \\eta_2\\,,\n", - " \\\\\n", - " \\phi &= 2\\pi \\eta_3\\,,\n", - " \\end{align*}\n", - "$$\n", - "\n", - "where $a_2 > a_1 \\geq 0$ are boundaries in the radial $r$-direction.\n", - "This can be seen for instance in the [HollowTorus](https://struphy.pages.mpcdf.de/struphy/sections/subsections/domains_avail.html#struphy.geometry.domains.HollowTorus) mapping (more complicated angle parametrizations $\\theta(\\eta_1, \\eta_2)$ are also available, but not discussed here).\n", - "\n", - "Let us plot the equilibrium magnetic field strength:\n", - "\n", - "1. in the poloidal plane at $\\phi = 0$\n", - "2. in the top view at $z = 0$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "# logical grid on the unit cube\n", - "e1 = np.linspace(0.0, 1.0, 101)\n", - "e2 = np.linspace(0.0, 1.0, 101)\n", - "e3 = np.linspace(0.0, 1.0, 101)\n", - "\n", - "# move away from the singular point r = 0\n", - "e1[0] += 1e-5" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# logical coordinates of the poloidal plane at phi = 0\n", - "eta_poloidal = (e1, e2, 0.0)\n", - "# logical coordinates of the top view at theta = 0\n", - "eta_topview_1 = (e1, 0.0, e3)\n", - "# logical coordinates of the top view at theta = pi\n", - "eta_topview_2 = (e1, 0.5, e3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Cartesian coordinates (squeezed)\n", - "x_pol, y_pol, z_pol = domain(*eta_poloidal, squeeze_out=True)\n", - "x_top1, y_top1, z_top1 = domain(*eta_topview_1, squeeze_out=True)\n", - "x_top2, y_top2, z_top2 = domain(*eta_topview_2, squeeze_out=True)\n", - "\n", - "print(f\"{x_pol.shape = }\")\n", - "print(f\"{x_top1.shape = }\")\n", - "print(f\"{x_top2.shape = }\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# generate two axes\n", - "fig, axs = plt.subplots(2, 1, figsize=(8, 16))\n", - "ax = axs[0]\n", - "ax_top = axs[1]\n", - "\n", - "# min/max of field strength\n", - "Bmax = np.max(equil.absB0(*eta_topview_2, squeeze_out=True))\n", - "Bmin = np.min(equil.absB0(*eta_topview_1, squeeze_out=True))\n", - "levels = np.linspace(Bmin, Bmax, 51)\n", - "\n", - "# absolute magnetic field at phi = 0\n", - "im = ax.contourf(x_pol, z_pol, equil.absB0(*eta_poloidal, squeeze_out=True), levels=levels)\n", - "\n", - "# absolute magnetic field at Z = 0\n", - "im_top = ax_top.contourf(x_top1, y_top1, equil.absB0(*eta_topview_1, squeeze_out=True), levels=levels)\n", - "ax_top.contourf(x_top2, y_top2, equil.absB0(*eta_topview_2, squeeze_out=True), levels=levels)\n", - "\n", - "# last closed flux surface, poloidal\n", - "ax.plot(x_pol[-1], z_pol[-1], color=\"k\")\n", - "\n", - "# last closed flux surface, toroidal\n", - "ax_top.plot(x_top1[-1], y_top1[-1], color=\"k\")\n", - "ax_top.plot(x_top2[-1], y_top2[-1], color=\"k\")\n", - "\n", - "# limiter, poloidal\n", - "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, \"tab:orange\")\n", - "ax.axis(\"equal\")\n", - "ax.set_xlabel(\"R\")\n", - "ax.set_ylabel(\"Z\")\n", - "ax.set_title(\"abs(B) at $\\phi=0$\")\n", - "fig.colorbar(im)\n", - "# limiter, toroidal\n", - "limiter_Rmax = np.max(equil.limiter_pts_R)\n", - "limiter_Rmin = np.min(equil.limiter_pts_R)\n", - "\n", - "thetas = 2 * np.pi * e2\n", - "limiter_x_max = limiter_Rmax * np.cos(thetas)\n", - "limiter_y_max = -limiter_Rmax * np.sin(thetas)\n", - "limiter_x_min = limiter_Rmin * np.cos(thetas)\n", - "limiter_y_min = -limiter_Rmin * np.sin(thetas)\n", - "\n", - "ax_top.plot(limiter_x_max, limiter_y_max, \"tab:orange\")\n", - "ax_top.plot(limiter_x_min, limiter_y_min, \"tab:orange\")\n", - "ax_top.axis(\"equal\")\n", - "ax_top.set_xlabel(\"x\")\n", - "ax_top.set_ylabel(\"y\")\n", - "ax_top.set_title(\"abs(B) at $Z=0$\")\n", - "fig.colorbar(im_top);" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# instantiate Particle object\n", - "Np = 4\n", - "bc = [\"remove\", \"periodic\", \"periodic\"]\n", - "bufsize = 2.0\n", - "\n", - "initial = [\n", - " [0.501, 0.001, 0.001, 0.0, 0.0450, -0.04], # co-passing particle\n", - " [0.511, 0.001, 0.001, 0.0, -0.0450, -0.04], # counter passing particle\n", - " [0.521, 0.001, 0.001, 0.0, 0.0105, -0.04], # co-trapped particle\n", - " [0.531, 0.001, 0.001, 0.0, -0.0155, -0.04],\n", - "]\n", - "\n", - "loading_params = {\"seed\": 1608, \"initial\": initial}\n", - "\n", - "particles = Particles6D(Np=Np, bc=bc, loading_params=loading_params, bufsize=bufsize)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "particles.draw_markers()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# positions on the physical domain Omega (x, y, z)\n", - "pushed_pos = domain(particles.positions).T\n", - "pushed_pos" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# compute R-coordinate\n", - "pushed_r = np.sqrt(pushed_pos[:, 0] ** 2 + pushed_pos[:, 1] ** 2)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "labels = [\"co-passing\", \"counter passing\", \"co_trapped\", \"counter-trapped\"]\n", - "\n", - "for n, (r, pos) in enumerate(zip(pushed_r, pushed_pos)):\n", - " # poloidal\n", - " ax.scatter(r, pos[2], c=colors[n % 4], label=labels[n])\n", - " ax.arrow(\n", - " r, pos[2], particles.velocities[n, 0], particles.velocities[n, 2] * 10, color=colors[n % 4], head_width=0.05\n", - " )\n", - " # topview\n", - " ax_top.scatter(pos[0], pos[1], c=colors[n % 4], label=labels[n])\n", - " ax_top.arrow(\n", - " pos[0],\n", - " pos[1],\n", - " particles.velocities[n, 0],\n", - " particles.velocities[n, 1] * 10,\n", - " color=colors[n % 4],\n", - " head_width=0.05,\n", - " )\n", - "\n", - "ax.set_xlabel(\"R\")\n", - "ax.set_ylabel(\"Z\")\n", - "ax.set_title(\"Initial conditions\")\n", - "ax.legend()\n", - "ax_top.set_xlabel(\"x\")\n", - "ax_top.set_ylabel(\"y\")\n", - "ax_top.set_title(\"Initial conditions\")\n", - "ax_top.legend()\n", - "fig" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# instantiate Derham object\n", - "Nel = [32, 72, 1]\n", - "p = [3, 3, 1]\n", - "spl_kind = [False, True, True]\n", - "derham = Derham(Nel=Nel, p=p, spl_kind=spl_kind)\n", - "\n", - "# instantiate a projected MHD equilibrium object\n", - "proj_equil = ProjectedMHDequilibrium(equil, derham)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# pass simulation parameters to Propagator class\n", - "PushEta.domain = domain\n", - "PushVxB.domain = domain\n", - "PushVxB.derham = derham" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# instantiate Propagator object\n", - "prop_eta = PushEta(particles)\n", - "prop_vxB = PushVxB(particles, b2=proj_equil.b2)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# time stepping\n", - "Tend = 3000.0 - 1e-6\n", - "dt = 0.2\n", - "Nt = int(Tend / dt)\n", - "\n", - "pos = np.zeros((Nt + 2, Np, 3), dtype=float)\n", - "r = np.zeros((Nt + 2, Np), dtype=float)\n", - "\n", - "pos[0] = pushed_pos\n", - "r[0] = np.sqrt(pushed_pos[:, 0] ** 2 + pushed_pos[:, 1] ** 2)\n", - "\n", - "time = 0.0\n", - "n = 0\n", - "while time < Tend:\n", - " time += dt\n", - " n += 1\n", - "\n", - " # advance in time\n", - " prop_vxB(dt / 2)\n", - " prop_eta(dt)\n", - " prop_vxB(dt / 2)\n", - "\n", - " # positions on the physical domain Omega\n", - " pushed_pos = domain(particles.positions).T\n", - "\n", - " # compute R-ccordinate\n", - " pos[n] = pushed_pos\n", - " r[n] = np.sqrt(pushed_pos[:, 0] ** 2 + pushed_pos[:, 1] ** 2)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# make scatter plot for each particle\n", - "for i in range(pos.shape[1]):\n", - " # poloidal\n", - " ax.scatter(r[:, i], pos[:, i, 2], c=colors[i % 4], s=1)\n", - " # top view\n", - " ax_top.scatter(pos[:, i, 0], pos[:, i, 1], c=colors[i % 4], s=1)\n", - "\n", - "ax.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", - "ax_top.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", - "fig" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Guiding-centers in a Tokamak equilibrium\n", - "\n", - "We now use the Propagators []() and []() in ASDEX-Upgrade equilibrium from the previous example.\n", - "\n", - "For this we need to instantiate the [Particles5D]() class." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.pic.particles import Particles5D\n", - "\n", - "# instantiate Particle object\n", - "Np = 4\n", - "bc = [\"remove\", \"periodic\", \"periodic\"]\n", - "bufsize = 2.0\n", - "\n", - "initial = [\n", - " [0.501, 0.001, 0.001, -1.935, 1.72], # co-passing particle\n", - " [0.501, 0.001, 0.001, 1.935, 1.72], # couner-passing particle\n", - " [0.501, 0.001, 0.001, -0.6665, 1.72], # co-trapped particle\n", - " [0.501, 0.001, 0.001, 0.4515, 1.72],\n", - "] # counter-trapped particle\n", - "\n", - "loading_params = {\"seed\": 1608, \"initial\": initial}\n", - "\n", - "particles = Particles5D(proj_equil, Np=Np, bc=bc, loading_params=loading_params, bufsize=bufsize)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "particles.draw_markers()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# positions on the physical domain Omega (x, y, z)\n", - "pushed_pos = domain(particles.positions).T\n", - "pushed_pos" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# compute R-coordinate\n", - "pushed_r = np.sqrt(pushed_pos[:, 0] ** 2 + pushed_pos[:, 1] ** 2)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "particles.velocities" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# generate two axes\n", - "fig, axs = plt.subplots(2, 1, figsize=(8, 16))\n", - "ax = axs[0]\n", - "ax_top = axs[1]\n", - "\n", - "# min/max of field strength\n", - "Bmax = np.max(equil.absB0(*eta_topview_2, squeeze_out=True))\n", - "Bmin = np.min(equil.absB0(*eta_topview_1, squeeze_out=True))\n", - "levels = np.linspace(Bmin, Bmax, 51)\n", - "\n", - "# absolute magnetic field at phi = 0\n", - "im = ax.contourf(x_pol, z_pol, equil.absB0(*eta_poloidal, squeeze_out=True), levels=levels)\n", - "\n", - "# absolute magnetic field at Z = 0\n", - "im_top = ax_top.contourf(x_top1, y_top1, equil.absB0(*eta_topview_1, squeeze_out=True), levels=levels)\n", - "ax_top.contourf(x_top2, y_top2, equil.absB0(*eta_topview_2, squeeze_out=True), levels=levels)\n", - "\n", - "# last closed flux surface, poloidal\n", - "ax.plot(x_pol[-1], z_pol[-1], color=\"k\")\n", - "\n", - "# last closed flux surface, toroidal\n", - "ax_top.plot(x_top1[-1], y_top1[-1], color=\"k\")\n", - "ax_top.plot(x_top2[-1], y_top2[-1], color=\"k\")\n", - "\n", - "# limiter, poloidal\n", - "ax.plot(equil.limiter_pts_R, equil.limiter_pts_Z, \"tab:orange\")\n", - "ax.axis(\"equal\")\n", - "ax.set_xlabel(\"R\")\n", - "ax.set_ylabel(\"Z\")\n", - "ax.set_title(\"abs(B) at $\\phi=0$\")\n", - "fig.colorbar(im)\n", - "# limiter, toroidal\n", - "limiter_Rmax = np.max(equil.limiter_pts_R)\n", - "limiter_Rmin = np.min(equil.limiter_pts_R)\n", - "\n", - "thetas = 2 * np.pi * e2\n", - "limiter_x_max = limiter_Rmax * np.cos(thetas)\n", - "limiter_y_max = -limiter_Rmax * np.sin(thetas)\n", - "limiter_x_min = limiter_Rmin * np.cos(thetas)\n", - "limiter_y_min = -limiter_Rmin * np.sin(thetas)\n", - "\n", - "ax_top.plot(limiter_x_max, limiter_y_max, \"tab:orange\")\n", - "ax_top.plot(limiter_x_min, limiter_y_min, \"tab:orange\")\n", - "ax_top.axis(\"equal\")\n", - "ax_top.set_xlabel(\"x\")\n", - "ax_top.set_ylabel(\"y\")\n", - "ax_top.set_title(\"abs(B) at $Z=0$\")\n", - "fig.colorbar(im_top);" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "labels = [\"co-passing\", \"counter passing\", \"co_trapped\", \"counter-trapped\"]\n", - "\n", - "for n, (r, pos) in enumerate(zip(pushed_r, pushed_pos)):\n", - " # poloidal\n", - " ax.scatter(r, pos[2], c=colors[n % 4], label=labels[n])\n", - " # topview\n", - " ax_top.scatter(pos[0], pos[1], c=colors[n % 4], label=labels[n])\n", - " ax_top.arrow(pos[0], pos[1], 0.0, particles.velocities[n, 0] / 5, color=colors[n % 4], head_width=0.05)\n", - "\n", - "ax.set_xlabel(\"R\")\n", - "ax.set_ylabel(\"Z\")\n", - "ax.set_title(\"Initial conditions\")\n", - "ax.legend()\n", - "ax_top.set_xlabel(\"x\")\n", - "ax_top.set_ylabel(\"y\")\n", - "ax_top.set_title(\"Initial conditions\")\n", - "ax_top.legend()\n", - "fig" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.propagators.propagators_markers import PushGuidingCenterBxEstar, PushGuidingCenterParallel\n", - "\n", - "# default parameters of Propagator\n", - "opts_BxE = PushGuidingCenterBxEstar.options(default=True)\n", - "print(opts_BxE)\n", - "\n", - "opts_para = PushGuidingCenterParallel.options(default=True)\n", - "print(opts_para)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# pass simulation parameters to Propagator class\n", - "PushGuidingCenterBxEstar.domain = domain\n", - "PushGuidingCenterParallel.domain = domain\n", - "\n", - "PushGuidingCenterBxEstar.derham = derham\n", - "PushGuidingCenterParallel.derham = derham\n", - "\n", - "PushGuidingCenterBxEstar.projected_equil = proj_equil\n", - "PushGuidingCenterParallel.projected_equil = proj_equil" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# natural constants\n", - "mH = 1.67262192369e-27 # proton mass (kg)\n", - "e = 1.602176634e-19 # elementary charge (C)\n", - "mu0 = 1.25663706212e-6 # magnetic constant (N/A^2)\n", - "\n", - "# epsilon equation parameter\n", - "A = 1.0 # mass number in units of proton mass\n", - "Z = 1 # signed charge number in units of elementary charge\n", - "unit_x = 1.0 # length scale unit in m\n", - "unit_B = 1.0 # magnetic field unit in T\n", - "unit_n = 1e20 # number density unit in m^(-3)\n", - "unit_v = unit_B / np.sqrt(unit_n * A * mH * mu0) # Alfvén velocity unit\n", - "unit_t = unit_x / unit_v # time unit\n", - "\n", - "# cyclotron frequency and epsilon parameter\n", - "om_c = Z * e * unit_B / (A * mH)\n", - "epsilon = 1.0 / (om_c * unit_t)\n", - "\n", - "print(f\"{unit_x = }\")\n", - "print(f\"{unit_B = }\")\n", - "print(f\"{unit_n = }\")\n", - "print(f\"{unit_v = }\")\n", - "print(f\"{unit_t = }\")\n", - "print(f\"{epsilon = }\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# instantiate Propagator object\n", - "opts_BxE[\"algo\"][\"tol\"] = 1e-5\n", - "opts_para[\"algo\"][\"tol\"] = 1e-5\n", - "prop_BxE = PushGuidingCenterBxEstar(particles, epsilon=epsilon, algo=opts_BxE[\"algo\"])\n", - "prop_para = PushGuidingCenterParallel(particles, epsilon=epsilon, algo=opts_para[\"algo\"])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# time stepping\n", - "Tend = 100.0 - 1e-6\n", - "dt = 0.1\n", - "Nt = int(Tend / dt)\n", - "\n", - "pos = np.zeros((Nt + 2, Np, 3), dtype=float)\n", - "r = np.zeros((Nt + 2, Np), dtype=float)\n", - "\n", - "pos[0] = pushed_pos\n", - "r[0] = np.sqrt(pushed_pos[:, 0] ** 2 + pushed_pos[:, 1] ** 2)\n", - "\n", - "time = 0.0\n", - "n = 0\n", - "while time < Tend:\n", - " time += dt\n", - " n += 1\n", - "\n", - " # advance in time\n", - " prop_BxE(dt / 2)\n", - " prop_para(dt)\n", - " prop_BxE(dt / 2)\n", - "\n", - " # positions on the physical domain Omega\n", - " pushed_pos = domain(particles.positions).T\n", - "\n", - " # compute R-coordinate\n", - " pos[n] = pushed_pos\n", - " r[n] = np.sqrt(pushed_pos[:, 0] ** 2 + pushed_pos[:, 1] ** 2)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# make scatter plot for each particle in xy-plane\n", - "for i in range(pos.shape[1]):\n", - " # poloidal\n", - " ax.scatter(r[:, i], pos[:, i, 2], c=colors[i % 4], s=1)\n", - " # top view\n", - " ax_top.scatter(pos[:, i, 0], pos[:, i, 1], c=colors[i % 4], s=1)\n", - "\n", - "ax.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", - "ax_top.set_title(f\"{math.ceil(Tend / dt)} time steps\")\n", - "fig" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "env", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/tutorials_old/tutorial_01_parameter_files.ipynb b/tutorials_old/tutorial_01_parameter_files.ipynb deleted file mode 100644 index 8c2ce8ced..000000000 --- a/tutorials_old/tutorial_01_parameter_files.ipynb +++ /dev/null @@ -1,379 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "d34c79c5", - "metadata": {}, - "source": [ - "# Parameter files and `struphy.main`\n", - "\n", - "Struphy parameter files are Python scripts (.py) that can be executed with the Python interpreter.\n", - "For each `MODEL`, the default parameter file can be generated from the console via\n", - "\n", - "```\n", - "struphy params MODEL\n", - "```\n", - "\n", - "This will create a file `params_MODEL.py` in the current working directory. To run the model type\n", - "\n", - "```\n", - "python params_MODEL.py\n", - "```\n", - "\n", - "The user can modify the parameter file to launch a specific simulation.\n", - "As an example, let us discuss the parameter file of the model [Vlasov](https://struphy.pages.mpcdf.de/struphy/sections/subsections/models_toy.html#struphy.models.toy.Vlasov) and run some simple examples. \n", - "The file can be generated from\n", - "\n", - "```\n", - "struphy params Vlasov\n", - "```\n", - "\n", - "To see its contents, open the file in your preferred editor or type\n", - "\n", - "```\n", - "cat params_Vlasov.py\n", - "```\n", - "\n", - "## Parameters part 1: Imports" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3ecab659", - "metadata": {}, - "outputs": [], - "source": [ - "from struphy import main\n", - "from struphy.fields_background import equils\n", - "from struphy.geometry import domains\n", - "from struphy.initial import perturbations\n", - "from struphy.io.options import DerhamOptions, EnvironmentOptions, FieldsBackground, Time, Units\n", - "from struphy.kinetic_background import maxwellians\n", - "\n", - "# import model, set verbosity\n", - "from struphy.models.toy import Vlasov as Model\n", - "from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters\n", - "from struphy.topology import grids\n", - "\n", - "verbose = True" - ] - }, - { - "cell_type": "markdown", - "id": "5cf6d9c7", - "metadata": {}, - "source": [ - "All Struphy parameter files import the modules listed above, even though some of them might not be needed in a specific model. The last import imports the model itself, always under the alias `Model`.\n", - "\n", - "## Parameters part 2: Generic options\n", - "\n", - "The following lines refer to options that can be set for any model. These are:\n", - "\n", - "* Environment options (paths, saving, domain cloning)\n", - "* Model units\n", - "* Time options\n", - "* Problem geometry (mapped domain)\n", - "* Static background (equilibrium)\n", - "* Grid\n", - "* Derham complex\n", - "\n", - "Check the respective classes for possible options." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cc43d2fc", - "metadata": {}, - "outputs": [], - "source": [ - "# environment options\n", - "env = EnvironmentOptions()\n", - "\n", - "# units\n", - "units = Units()\n", - "\n", - "# time stepping\n", - "time_opts = Time(dt=0.2, Tend=10.0)\n", - "\n", - "# geometry\n", - "l1 = -5.0\n", - "r1 = 5.0\n", - "l2 = -7.0\n", - "r2 = 7.0\n", - "l3 = -1.0\n", - "r3 = 1.0\n", - "domain = domains.Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)\n", - "\n", - "# fluid equilibrium (can be used as part of initial conditions)\n", - "equil = None\n", - "\n", - "# grid\n", - "grid = None\n", - "\n", - "# derham options\n", - "derham_opts = None" - ] - }, - { - "cell_type": "markdown", - "id": "74e6f739", - "metadata": {}, - "source": [ - "## Parameters part 3: Model instance\n", - "\n", - "Here, a light-weight instance of the model is created, without allocating memory. The light-weight instance is used to set model-specific parameters for the model's species.\n", - "\n", - "Check the functions \n", - "\n", - "* `Species.set_phys_params()`\n", - "* `KineticSpecies.set_markers()`\n", - "* `KineticSpecies.set_sorting_boxes()`\n", - "* `KineticSpecies.set_save_data()`\n", - "\n", - " for possible options." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6c498fb3", - "metadata": {}, - "outputs": [], - "source": [ - "# light-weight model instance\n", - "model = Model()\n", - "\n", - "# species parameters\n", - "model.kinetic_ions.set_phys_params()\n", - "\n", - "loading_params = LoadingParameters(Np=15)\n", - "weights_params = WeightsParameters()\n", - "boundary_params = BoundaryParameters(bc=(\"reflect\", \"reflect\", \"periodic\"))\n", - "model.kinetic_ions.set_markers(\n", - " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", - ")\n", - "model.kinetic_ions.set_sorting_boxes()\n", - "model.kinetic_ions.set_save_data(n_markers=1.0)" - ] - }, - { - "cell_type": "markdown", - "id": "b0f65b0a", - "metadata": {}, - "source": [ - "## Parameters part 4: Propagator options\n", - "\n", - "Check the method `set_options()` of each propagator for possible options." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "be6875e7", - "metadata": {}, - "outputs": [], - "source": [ - "# propagator options\n", - "model.propagators.push_vxb.set_options()\n", - "model.propagators.push_eta.set_options()" - ] - }, - { - "cell_type": "markdown", - "id": "b1ef8b97", - "metadata": {}, - "source": [ - "## Parameters part 5: Initial conditions\n", - "\n", - "Use the methods `Variable.add_background()` and `Variable.add_perturbation()` to set initial conditions for each variable of a species. Variables that are not specified are intialized as zero." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cc0ac424", - "metadata": {}, - "outputs": [], - "source": [ - "# initial conditions (background + perturbation)\n", - "perturbation = None\n", - "\n", - "background = maxwellians.Maxwellian3D(n=(1.0, perturbation))\n", - "model.kinetic_ions.var.add_background(background)" - ] - }, - { - "cell_type": "markdown", - "id": "879978af", - "metadata": {}, - "source": [ - "## Parameters part 6: `main.run`\n", - "\n", - "In the final part of the parameter file, the `main.run` command is invoked. This command will allocate memory and run the specified simulation. The run command is not executed when the parameter file is imported in another Python script." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "03764138", - "metadata": {}, - "outputs": [], - "source": [ - "main.run(\n", - " model,\n", - " params_path=None,\n", - " env=env,\n", - " units=units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "f9ce5099", - "metadata": {}, - "source": [ - "## Post processing: `main.pproc`\n", - "\n", - "Aside from `run`, the Struphy `main` module has also a `pproc` routine for post-processing raw simulation data:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dd7cfe62", - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "\n", - "path = os.path.join(os.getcwd(), \"sim_1\")\n", - "\n", - "main.pproc(path, physical=True)" - ] - }, - { - "cell_type": "markdown", - "id": "e317f88d", - "metadata": {}, - "source": [ - "## Loading data: `main.load_data`\n", - "\n", - "After post-processing, the generated data can be loaded via `main.load_data`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "78b8cbab", - "metadata": {}, - "outputs": [], - "source": [ - "simdata = main.load_data(path)" - ] - }, - { - "cell_type": "markdown", - "id": "a9468479", - "metadata": {}, - "source": [ - "`main.load_data` returns a `SimData` object, which you can inspect to get further info on possible data to load:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0531679e", - "metadata": {}, - "outputs": [], - "source": [ - "for k, v in simdata.__dict__.items():\n", - " print(k, type(v))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a329eb38", - "metadata": {}, - "outputs": [], - "source": [ - "for k, v in simdata.pic_species[\"kinetic_ions\"][\"orbits\"].items():\n", - " print(f\"{k = }, {type(v) = }\")" - ] - }, - { - "cell_type": "markdown", - "id": "08544a91", - "metadata": {}, - "source": [ - "## Plotting particle orbits\n", - "\n", - "In this example, for the species `kinetic_ions` some particle orbits have been saved. Let us plot them:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7af3facd", - "metadata": {}, - "outputs": [], - "source": [ - "from matplotlib import pyplot as plt\n", - "\n", - "fig = plt.figure()\n", - "ax = fig.gca()\n", - "\n", - "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", - "\n", - "time = 0.0\n", - "dt = time_opts.dt\n", - "Tend = time_opts.Tend\n", - "for k, v in simdata.pic_species[\"kinetic_ions\"][\"orbits\"].items():\n", - " # print(f\"{v[0] = }\")\n", - " alpha = (Tend - time) / Tend\n", - " for i, particle in enumerate(v):\n", - " ax.scatter(particle[0], particle[1], c=colors[i % 4], alpha=alpha)\n", - " time += dt\n", - "\n", - "ax.plot([l1, l1], [l2, r2], \"k\")\n", - "ax.plot([r1, r1], [l2, r2], \"k\")\n", - "ax.plot([l1, r1], [l2, l2], \"k\")\n", - "ax.plot([l1, r1], [r2, r2], \"k\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_xlim(-6.5, 6.5)\n", - "ax.set_ylim(-9, 9)\n", - "ax.set_title(f\"{int(Tend / dt)} time steps (full color at t=0)\");" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "env", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/tutorials_old/tutorial_01_particles.ipynb b/tutorials_old/tutorial_01_particles.ipynb deleted file mode 100644 index b72b1ce94..000000000 --- a/tutorials_old/tutorial_01_particles.ipynb +++ /dev/null @@ -1,273 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "d34c79c5", - "metadata": {}, - "source": [ - "# Parameter files\n", - "\n", - "Struphy parameter files are Python scripts (.py) that can be executed with the Python interpreter.\n", - "For each `MODEL`, the default parameter file can be generated from the console via\n", - "\n", - "```\n", - "struphy params MODEL\n", - "```\n", - "\n", - "This will create a file `params_MODEL.py` in the current working directory. To run the model type\n", - "\n", - "```\n", - "python params_MODEL.py\n", - "```\n", - "\n", - "The user should modify the parameter file to launch a specific simulation.\n", - "As an example, in what follows we discuss the parameter file of the model [Vlasov](https://struphy.pages.mpcdf.de/struphy/sections/subsections/models_toy.html#struphy.models.toy.Vlasov) and run some simple examples. The file can be generated from\n", - "\n", - "```\n", - "struphy params Vlasov\n", - "```\n", - "\n", - "To see its contents, open the file in your preferred editor or type\n", - "\n", - "```\n", - "cat params_Vlasov.py\n", - "```\n", - "\n", - "## Part 1: imports" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3ecab659", - "metadata": {}, - "outputs": [], - "source": [ - "from struphy import main\n", - "from struphy.fields_background import equils\n", - "from struphy.geometry import domains\n", - "from struphy.initial import perturbations\n", - "from struphy.io.options import DerhamOptions, EnvironmentOptions, FieldsBackground, Time, Units\n", - "from struphy.kinetic_background import maxwellians\n", - "\n", - "# import model, set verbosity\n", - "from struphy.models.toy import Vlasov as Model\n", - "from struphy.pic.utilities import BoundaryParameters, LoadingParameters, WeightsParameters\n", - "from struphy.topology import grids\n", - "\n", - "verbose = True" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cc43d2fc", - "metadata": {}, - "outputs": [], - "source": [ - "# environment options\n", - "env = EnvironmentOptions()\n", - "\n", - "# units\n", - "units = Units()\n", - "\n", - "# time stepping\n", - "time_opts = Time(dt=0.2, Tend=10.0)\n", - "\n", - "# geometry\n", - "l1 = -5.0\n", - "r1 = 5.0\n", - "l2 = -7.0\n", - "r2 = 7.0\n", - "l3 = -1.0\n", - "r3 = 1.0\n", - "domain = domains.Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)\n", - "\n", - "# fluid equilibrium (can be used as part of initial conditions)\n", - "equil = None\n", - "\n", - "# grid\n", - "grid = None\n", - "\n", - "# derham options\n", - "derham_opts = None" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6c498fb3", - "metadata": {}, - "outputs": [], - "source": [ - "# light-weight model instance\n", - "model = Model()\n", - "\n", - "# species parameters\n", - "model.kinetic_ions.set_phys_params()\n", - "\n", - "loading_params = LoadingParameters(Np=15)\n", - "weights_params = WeightsParameters()\n", - "boundary_params = BoundaryParameters(bc=(\"reflect\", \"reflect\", \"periodic\"))\n", - "model.kinetic_ions.set_markers(\n", - " loading_params=loading_params, weights_params=weights_params, boundary_params=boundary_params\n", - ")\n", - "model.kinetic_ions.set_sorting_boxes()\n", - "model.kinetic_ions.set_save_data(n_markers=1.0)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "be6875e7", - "metadata": {}, - "outputs": [], - "source": [ - "# propagator options\n", - "model.propagators.push_vxb.set_options()\n", - "model.propagators.push_eta.set_options()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cc0ac424", - "metadata": {}, - "outputs": [], - "source": [ - "# initial conditions (background + perturbation)\n", - "perturbation = None\n", - "\n", - "background = maxwellians.Maxwellian3D(n=(1.0, perturbation))\n", - "model.kinetic_ions.var.add_background(background)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "03764138", - "metadata": {}, - "outputs": [], - "source": [ - "main.run(\n", - " model,\n", - " params_path=None,\n", - " env=env,\n", - " units=units,\n", - " time_opts=time_opts,\n", - " domain=domain,\n", - " equil=equil,\n", - " grid=grid,\n", - " derham_opts=derham_opts,\n", - " verbose=verbose,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "f9ce5099", - "metadata": {}, - "source": [ - "## Post processing" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dd7cfe62", - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "\n", - "path = os.path.join(os.getcwd(), \"sim_1\")\n", - "main.pproc(path, physical=True)" - ] - }, - { - "cell_type": "markdown", - "id": "e317f88d", - "metadata": {}, - "source": [ - "## Viewing particle orbits" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "78b8cbab", - "metadata": {}, - "outputs": [], - "source": [ - "simdata = main.load_data(path)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a329eb38", - "metadata": {}, - "outputs": [], - "source": [ - "for k, v in simdata.pic_species[\"kinetic_ions\"][\"orbits\"].items():\n", - " print(f\"{k = }, {type(v) = }\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7af3facd", - "metadata": {}, - "outputs": [], - "source": [ - "from matplotlib import pyplot as plt\n", - "\n", - "fig = plt.figure()\n", - "ax = fig.gca()\n", - "\n", - "colors = [\"tab:blue\", \"tab:orange\", \"tab:green\", \"tab:red\"]\n", - "\n", - "time = 0.0\n", - "dt = time_opts.dt\n", - "Tend = time_opts.Tend\n", - "for k, v in simdata.pic_species[\"kinetic_ions\"][\"orbits\"].items():\n", - " # print(k, v)\n", - " alpha = (Tend - time) / Tend\n", - " for i, particle in enumerate(v):\n", - " ax.scatter(particle[1], particle[2], c=colors[i % 4], alpha=alpha)\n", - " time += dt\n", - "\n", - "ax.plot([l1, l1], [l2, r2], \"k\")\n", - "ax.plot([r1, r1], [l2, r2], \"k\")\n", - "ax.plot([l1, r1], [l2, l2], \"k\")\n", - "ax.plot([l1, r1], [r2, r2], \"k\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_xlim(-6.5, 6.5)\n", - "ax.set_ylim(-9, 9)\n", - "ax.set_title(f\"{int(Tend / dt)} time steps (full color at t=0)\");" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "env", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/tutorials_old/tutorial_02_fluid_particles.ipynb b/tutorials_old/tutorial_02_fluid_particles.ipynb deleted file mode 100644 index 7c22c1195..000000000 --- a/tutorials_old/tutorial_02_fluid_particles.ipynb +++ /dev/null @@ -1,1541 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 2 - Fluid particles\n", - "\n", - "Topics covered in this tutorial:\n", - "\n", - "- basic functionalities of [ParticlesSPH](https://struphy.pages.mpcdf.de/struphy/sections/subsections/pic_base.html#base-modules) class\n", - "- drawing markers: random vs. regular \"tesselation\"\n", - "- initializing velocities as $\\mathbf v(0) = \\mathbf u(\\mathbf x(0))$ via a [GenericFluidEquilibrium](https://struphy.pages.mpcdf.de/struphy/sections/subsections/mhd_equils_sub.html#generic-fluid-equilibria)\n", - "- velocity push with [PushVinEfield](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_markers.html#struphy.propagators.propagators_markers.PushVinEfield)\n", - "- velocity push with [PushVinSPHpressure](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_markers.html#struphy.propagators.propagators_markers.PushVinSPHpressure)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Fluid flow in external force field\n", - "\n", - "Let $\\Omega \\subset \\mathbb R^3$ be a box (cuboid). We search for trajectories $(\\mathbf x_p, \\mathbf v_p): [0,T] \\to \\Omega \\times \\mathbb R^3$, $p = 0, \\ldots, N-1$ that satisfy\n", - "\n", - "$$\n", - "\\begin{align}\n", - " \\dot{\\mathbf x}_p &= \\mathbf v_p\\,,\\qquad && \\mathbf x_p(0) = \\mathbf x_{p0}\\,,\n", - " \\\\[2mm]\n", - " \\dot{\\mathbf v}_p &= -\\nabla p(\\mathbf x_p) \\qquad && \\mathbf v_p(0) = \\mathbf u(\\mathbf x_p(0))\\,,\n", - " \\end{align}\n", - "$$\n", - "\n", - "where $p \\in H^1(\\Omega)$ is some given function.\n", - "In Struphy, the position coordinates are updated in logical space $[0, 1]^3 = F^{-1}(\\Omega)$, for instance with the Propagator [PushEta](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_markers.html#struphy.propagators.propagators_markers.PushEta) which we shall use in what follows." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.geometry.domains import Cuboid\n", - "\n", - "l1 = -0.5\n", - "r1 = 0.5\n", - "l2 = -0.5\n", - "r2 = 0.5\n", - "l3 = 0.0\n", - "r3 = 1.0\n", - "domain = Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# define the initial flow\n", - "\n", - "import numpy as np\n", - "\n", - "from struphy.fields_background.generic import GenericCartesianFluidEquilibrium\n", - "\n", - "\n", - "def u_fun(x, y, z):\n", - " ux = -np.cos(np.pi * x) * np.sin(np.pi * y)\n", - " uy = np.sin(np.pi * x) * np.cos(np.pi * y)\n", - " uz = 0 * x\n", - " return ux, uy, uz\n", - "\n", - "\n", - "p_fun = lambda x, y, z: 0.5 * (np.sin(np.pi * x) ** 2 + np.sin(np.pi * y) ** 2)\n", - "n_fun = lambda x, y, z: 1.0 + 0 * x\n", - "\n", - "bel_flow = GenericCartesianFluidEquilibrium(u_xyz=u_fun, p_xyz=p_fun, n_xyz=n_fun)\n", - "bel_flow.domain = domain\n", - "p_xyz = bel_flow.p_xyz\n", - "p0 = bel_flow.p0" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.pic.particles import ParticlesSPH\n", - "\n", - "# particle boundary conditions\n", - "bc = [\"reflect\", \"reflect\", \"periodic\"]\n", - "\n", - "# instantiate Particle object (for random drawing of markers)\n", - "Np = 1000\n", - "\n", - "particles_1 = ParticlesSPH(\n", - " bc=bc,\n", - " domain=domain,\n", - " bckgr_params=bel_flow,\n", - " Np=Np,\n", - ")\n", - "\n", - "# instantiate Particle object (for regular tesselation drawing of markers)\n", - "ppb = 4\n", - "boxes_per_dim = (16, 16, 1)\n", - "loading = \"tesselation\"\n", - "loading_params = {\"n_quad\": 1}\n", - "bufsize = 0.5\n", - "\n", - "particles_2 = ParticlesSPH(\n", - " bc=bc,\n", - " domain=domain,\n", - " bckgr_params=bel_flow,\n", - " ppb=ppb,\n", - " boxes_per_dim=boxes_per_dim,\n", - " loading=loading,\n", - " loading_params=loading_params,\n", - " bufsize=bufsize,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "particles_1.draw_markers(sort=False)\n", - "particles_2.draw_markers(sort=False)\n", - "\n", - "particles_1.initialize_weights()\n", - "particles_2.initialize_weights()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"{particles_1.positions.shape = }\")\n", - "print(f\"{particles_2.positions.shape = }\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# positions on the physical domain Omega\n", - "print(f\"random: \\n{domain(particles_1.positions).T[:10]}\")\n", - "print(f\"\\ntesselation: \\n{domain(particles_2.positions).T[:10]}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.propagators.propagators_markers import PushEta\n", - "\n", - "# default parameters of Propagator\n", - "opts_eta = PushEta.options(default=False)\n", - "print(opts_eta)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# pass simulation parameters to Propagator class\n", - "PushEta.domain = domain" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# instantiate Propagator object\n", - "prop_eta_1 = PushEta(particles_1, algo=\"forward_euler\")\n", - "prop_eta_2 = PushEta(particles_2, algo=\"forward_euler\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.feec.psydac_derham import Derham\n", - "\n", - "Nel = [64, 64, 1] # Number of grid cells\n", - "p = [3, 3, 1] # spline degrees\n", - "spl_kind = [False, False, True] # spline types (clamped vs. periodic)\n", - "\n", - "derham = Derham(Nel, p, spl_kind)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "p_coeffs = derham.P[\"0\"](p0)\n", - "p_coeffs" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.propagators.propagators_markers import PushVinEfield\n", - "\n", - "# instantiate Propagator object\n", - "PushVinEfield.domain = domain\n", - "PushVinEfield.derham = derham" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "p_h = derham.create_spline_function(\"pressure\", \"H1\", coeffs=p_coeffs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "plt.figure(figsize=(12, 12))\n", - "x = np.linspace(-0.5, 0.5, 100)\n", - "y = np.linspace(-0.5, 0.5, 90)\n", - "xx, yy = np.meshgrid(x, y)\n", - "eta1 = np.linspace(0, 1, 100)\n", - "eta2 = np.linspace(0, 1, 90)\n", - "\n", - "plt.subplot(2, 2, 1)\n", - "plt.pcolor(xx, yy, p_xyz(xx, yy, 0))\n", - "plt.axis(\"square\")\n", - "plt.title(\"p_xyz\")\n", - "plt.colorbar()\n", - "\n", - "plt.subplot(2, 2, 2)\n", - "p_vals = p0(eta1, eta2, 0, squeeze_out=True).T\n", - "plt.pcolor(eta1, eta2, p_vals)\n", - "plt.axis(\"square\")\n", - "plt.title(\"p logical\")\n", - "plt.colorbar()\n", - "\n", - "plt.subplot(2, 2, 3)\n", - "p_h_vals = p_h(eta1, eta2, 0, squeeze_out=True).T\n", - "plt.pcolor(eta1, eta2, p_h_vals)\n", - "plt.axis(\"square\")\n", - "plt.title(\"p_h (logical)\")\n", - "plt.colorbar()\n", - "\n", - "plt.subplot(2, 2, 4)\n", - "plt.pcolor(eta1, eta2, np.abs(p_vals - p_h_vals))\n", - "plt.axis(\"square\")\n", - "plt.title(\"difference\")\n", - "plt.colorbar()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "grad_p = derham.grad.dot(p_coeffs)\n", - "grad_p.update_ghost_regions() # very important, we will move it inside grad\n", - "grad_p *= -1.0\n", - "prop_v_1 = PushVinEfield(particles_1, e_field=grad_p)\n", - "prop_v_2 = PushVinEfield(particles_2, e_field=grad_p)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "fig = plt.figure(figsize=(15, 8))\n", - "ax1 = fig.add_subplot(1, 2, 1, projection=\"3d\")\n", - "pos_1 = domain(particles_1.positions).T\n", - "ax1.scatter(pos_1[:, 0], pos_1[:, 1], pos_1[:, 2])\n", - "ax1.set_title(\"random starting positions\")\n", - "\n", - "ax2 = fig.add_subplot(1, 2, 2, projection=\"3d\")\n", - "pos_2 = domain(particles_2.positions).T\n", - "ax2.scatter(pos_2[:, 0], pos_2[:, 1], pos_2[:, 2])\n", - "ax2.set_title(\"starting positions from tesselation\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "# time stepping\n", - "dt = 0.02\n", - "Nt = 200\n", - "\n", - "# random particles\n", - "pos_1 = np.zeros((Nt + 1, particles_1.Np, 3), dtype=float)\n", - "velo_1 = np.zeros((Nt + 1, particles_1.Np, 3), dtype=float)\n", - "energy_1 = np.zeros((Nt + 1, particles_1.Np), dtype=float)\n", - "\n", - "# particles_1.draw_markers(sort=False)\n", - "# particles_1.initialize_weights()\n", - "\n", - "pos_1[0] = domain(particles_1.positions).T\n", - "velo_1[0] = particles_1.velocities\n", - "energy_1[0] = 0.5 * (velo_1[0, :, 0] ** 2 + velo_1[0, :, 1] ** 2) + p_h(particles_1.positions)\n", - "\n", - "time = 0.0\n", - "time_vec = np.zeros(Nt + 1, dtype=float)\n", - "n = 0\n", - "while n < Nt:\n", - " time += dt\n", - " n += 1\n", - " time_vec[n] = time\n", - "\n", - " # advance in time\n", - " prop_eta_1(dt / 2)\n", - " prop_v_1(dt)\n", - " prop_eta_1(dt / 2)\n", - "\n", - " # positions on the physical domain Omega\n", - " pos_1[n] = domain(particles_1.positions).T\n", - " velo_1[n] = particles_1.velocities\n", - "\n", - " energy_1[n] = 0.5 * (velo_1[n, :, 0] ** 2 + velo_1[n, :, 1] ** 2) + p_h(particles_1.positions)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# energy plots (random)\n", - "fig = plt.figure(figsize=(13, 6))\n", - "\n", - "plt.subplot(2, 2, 1)\n", - "plt.plot(time_vec, energy_1[:, 0])\n", - "plt.title(\"particle 1\")\n", - "plt.xlabel(\"time\")\n", - "plt.ylabel(\"energy\")\n", - "\n", - "plt.subplot(2, 2, 2)\n", - "plt.plot(time_vec, energy_1[:, 1])\n", - "plt.title(\"particle 2\")\n", - "plt.xlabel(\"time\")\n", - "plt.ylabel(\"energy\")\n", - "\n", - "plt.subplot(2, 2, 3)\n", - "plt.plot(time_vec, energy_1[:, 2])\n", - "plt.title(\"particle 3\")\n", - "plt.xlabel(\"time\")\n", - "plt.ylabel(\"energy\")\n", - "\n", - "plt.subplot(2, 2, 4)\n", - "plt.plot(time_vec, energy_1[:, 3])\n", - "plt.title(\"particle 4\")\n", - "plt.xlabel(\"time\")\n", - "plt.ylabel(\"energy\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "plt.figure(figsize=(12, 28))\n", - "\n", - "coloring = np.select([pos_1[0, :, 0] <= -0.2, np.abs(pos_1[0, :, 0]) < +0.2, pos_1[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0])\n", - "\n", - "interval = Nt / 20\n", - "plot_ct = 0\n", - "for i in range(Nt):\n", - " if i % interval == 0:\n", - " print(f\"{i = }\")\n", - " plot_ct += 1\n", - " plt.subplot(5, 2, plot_ct)\n", - " ax = plt.gca()\n", - " plt.scatter(pos_1[i, :, 0], pos_1[i, :, 1], c=coloring)\n", - " plt.axis(\"square\")\n", - " plt.title(\"n0_scatter\")\n", - " plt.xlim(l1, r1)\n", - " plt.ylim(l2, r2)\n", - " plt.colorbar()\n", - " plt.title(f\"Gas at t={i * dt}\")\n", - " if plot_ct == 10:\n", - " break" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# regular tesselation particles\n", - "\n", - "pos_2 = np.zeros((Nt + 1, particles_2.Np, 3), dtype=float)\n", - "velo_2 = np.zeros((Nt + 1, particles_2.Np, 3), dtype=float)\n", - "energy_2 = np.zeros((Nt + 1, particles_2.Np), dtype=float)\n", - "\n", - "# particles_1.draw_markers(sort=False)\n", - "# particles_1.initialize_weights()\n", - "\n", - "pos_2[0] = domain(particles_2.positions).T\n", - "velo_2[0] = particles_2.velocities\n", - "energy_2[0] = 0.5 * (velo_2[0, :, 0] ** 2 + velo_2[0, :, 1] ** 2) + p_h(particles_2.positions)\n", - "\n", - "time = 0.0\n", - "time_vec = np.zeros(Nt + 1, dtype=float)\n", - "n = 0\n", - "while n < Nt:\n", - " time += dt\n", - " n += 1\n", - " time_vec[n] = time\n", - "\n", - " # advance in time\n", - " prop_eta_2(dt / 2)\n", - " prop_v_2(dt)\n", - " prop_eta_2(dt / 2)\n", - "\n", - " # positions on the physical domain Omega\n", - " pos_2[n] = domain(particles_2.positions).T\n", - " velo_2[n] = particles_2.velocities\n", - "\n", - " energy_2[n] = 0.5 * (velo_2[n, :, 0] ** 2 + velo_2[n, :, 1] ** 2) + p_h(particles_2.positions)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# energy plots (tesselation)\n", - "fig = plt.figure(figsize=(13, 6))\n", - "\n", - "plt.subplot(2, 2, 1)\n", - "plt.plot(time_vec, energy_2[:, 0])\n", - "plt.title(\"particle 1\")\n", - "plt.xlabel(\"time\")\n", - "plt.ylabel(\"energy\")\n", - "\n", - "plt.subplot(2, 2, 2)\n", - "plt.plot(time_vec, energy_2[:, 1])\n", - "plt.title(\"particle 2\")\n", - "plt.xlabel(\"time\")\n", - "plt.ylabel(\"energy\")\n", - "\n", - "plt.subplot(2, 2, 3)\n", - "plt.plot(time_vec, energy_2[:, 2])\n", - "plt.title(\"particle 3\")\n", - "plt.xlabel(\"time\")\n", - "plt.ylabel(\"energy\")\n", - "\n", - "plt.subplot(2, 2, 4)\n", - "plt.plot(time_vec, energy_2[:, 3])\n", - "plt.title(\"particle 4\")\n", - "plt.xlabel(\"time\")\n", - "plt.ylabel(\"energy\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "plt.figure(figsize=(12, 28))\n", - "\n", - "coloring = np.select([pos_2[0, :, 0] <= -0.2, np.abs(pos_2[0, :, 0]) < +0.2, pos_2[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0])\n", - "\n", - "interval = Nt / 20\n", - "plot_ct = 0\n", - "for i in range(Nt):\n", - " if i % interval == 0:\n", - " print(f\"{i = }\")\n", - " plot_ct += 1\n", - " plt.subplot(5, 2, plot_ct)\n", - " ax = plt.gca()\n", - " plt.scatter(pos_2[i, :, 0], pos_2[i, :, 1], c=coloring)\n", - " plt.axis(\"square\")\n", - " plt.title(\"n0_scatter\")\n", - " plt.xlim(l1, r1)\n", - " plt.ylim(l2, r2)\n", - " plt.colorbar()\n", - " plt.title(f\"Gas at t={i * dt}\")\n", - " if plot_ct == 10:\n", - " break" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "make_movie = False\n", - "if make_movie:\n", - " import matplotlib.animation as animation\n", - "\n", - " n_frame = Nt\n", - " fig, axs = plt.subplots(1, 2, figsize=(12, 8))\n", - "\n", - " coloring_1 = np.select(\n", - " [pos_1[0, :, 0] <= -0.2, np.abs(pos_1[0, :, 0]) < +0.2, pos_1[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0]\n", - " )\n", - " scat_1 = axs[0].scatter(pos_1[0, :, 0], pos_1[0, :, 1], c=coloring_1)\n", - " axs[0].set_xlim([-0.5, 0.5])\n", - " axs[0].set_ylim([-0.5, 0.5])\n", - " axs[0].set_aspect(\"equal\")\n", - "\n", - " coloring_2 = np.select(\n", - " [pos_2[0, :, 0] <= -0.2, np.abs(pos_2[0, :, 0]) < +0.2, pos_2[0, :, 0] >= 0.2], [-1.0, 0.0, +1.0]\n", - " )\n", - " scat_2 = axs[1].scatter(pos_2[0, :, 0], pos_2[0, :, 1], c=coloring_2)\n", - " axs[1].set_xlim([-0.5, 0.5])\n", - " axs[1].set_ylim([-0.5, 0.5])\n", - " axs[1].set_aspect(\"equal\")\n", - "\n", - " f = lambda x, y: np.cos(np.pi * x) * np.cos(np.pi * y)\n", - " axs[0].contour(xx, yy, f(xx, yy))\n", - " axs[0].set_title(f\"time = {time_vec[0]:4.2f}\")\n", - " axs[1].contour(xx, yy, f(xx, yy))\n", - " axs[1].set_title(f\"time = {time_vec[0]:4.2f}\")\n", - "\n", - " def update_frame(frame):\n", - " scat_1.set_offsets(pos_1[frame, :, :2])\n", - " axs[0].set_title(f\"time = {time_vec[frame]:4.2f}\")\n", - "\n", - " scat_2.set_offsets(pos_2[frame, :, :2])\n", - " axs[1].set_title(f\"time = {time_vec[frame]:4.2f}\")\n", - " return scat_1, scat_2\n", - "\n", - " ani = animation.FuncAnimation(fig=fig, func=update_frame, frames=n_frame)\n", - " ani.save(\"tutorial_02_movie.gif\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Sound wave\n", - "\n", - "We use SPH to solve Euler's equations\n", - "\n", - "$$\n", - "\\begin{align}\n", - " \\partial_t \\rho + \\nabla \\cdot (\\rho \\mathbf u) &= 0\\,,\n", - " \\\\[2mm]\n", - " \\rho(\\partial_t \\mathbf u + \\mathbf u \\cdot \\nabla \\mathbf u) &= - \\nabla \\left(\\rho^2 \\frac{\\partial \\mathcal U(\\rho, S)}{\\partial \\rho} \\right)\\,,\n", - " \\\\[2mm]\n", - " \\partial_t S + \\mathbf u \\cdot \\nabla S &= 0\\,,\n", - " \\end{align}\n", - "$$\n", - "\n", - "where $S$ denotes the entropy per unit mass and the internal energy per unit mass is \n", - "\n", - "$$\n", - "\\mathcal U(\\rho, S) = \\kappa(S) \\log \\rho\\,.\n", - "$$\n", - "\n", - "The SPH discretization leads to ODEs for $N$ particles indexed by $p$,\n", - "\n", - "$$\n", - "\\begin{align}\n", - " \\dot{\\mathbf x}_p &= \\mathbf v_p\\,,\\qquad && \\mathbf x_p(0) = \\mathbf x_{p0}\\,,\n", - " \\\\[2mm]\n", - " \\dot{\\mathbf v}_p &= -\\kappa_{p}(0) \\sum_{i=1}^N w_i \\left(\\frac{1}{\\rho^{N,h}(\\mathbf x_p)} + \\frac{1}{\\rho^{N,h}(\\mathbf x_i)} \\right) \\nabla W_h(\\mathbf x_p - \\mathbf x_i) \\qquad && \\mathbf v_p(0) = \\mathbf u(\\mathbf x_p(0))\\,,\n", - " \\end{align}\n", - "$$\n", - "\n", - "where the smoothed density reads\n", - "\n", - "$$\n", - " \\rho^{N,h}(\\mathbf x) = \\sum_{j=1}^N w_j W_h(\\mathbf x - \\mathbf x_j)\\,,\n", - "$$\n", - "\n", - "with weights $w_p = const.$ and where $W_h(\\mathbf x)$ is a suitable smoothing kernel.\n", - "The velocity update is performed with the Propagator [PushVinSPHpressure](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_markers.html#struphy.propagators.propagators_markers.PushVinSPHpressure).\n", - "\n", - "We shall compute:\n", - "* a standing sound wave in 1d (linear dynamics)\n", - "* a gas expansion in 2d (nonlinear example)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.geometry.domains import Cuboid\n", - "\n", - "l1 = 0\n", - "r1 = 2.5\n", - "l2 = 0\n", - "r2 = 1.0\n", - "l3 = 0.0\n", - "r3 = 1.0\n", - "domain = Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "cst_vel = {\"ux\": 0.0, \"uy\": 0.0, \"uz\": 0.0, \"density_profile\": \"constant\"}\n", - "bckgr_params = {\"ConstantVelocity\": cst_vel}\n", - "\n", - "mode_params = {\"given_in_basis\": \"0\", \"ls\": [1], \"amps\": [1e-2]}\n", - "modes = {\"ModesSin\": mode_params}\n", - "pert_params = {\"n\": modes}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# particle initialization\n", - "from struphy.pic.particles import ParticlesSPH\n", - "\n", - "# marker parameters\n", - "ppb = 16\n", - "nx = 16\n", - "ny = 1\n", - "nz = 1\n", - "boxes_per_dim = (nx, ny, nz)\n", - "bc = [\"periodic\"] * 3\n", - "loading = \"tesselation\"\n", - "loading_params = {\"n_quad\": 1}\n", - "\n", - "# instantiate Particle object\n", - "particles = ParticlesSPH(\n", - " ppb=ppb,\n", - " boxes_per_dim=boxes_per_dim,\n", - " bc=bc,\n", - " domain=domain,\n", - " bckgr_params=bckgr_params,\n", - " pert_params=pert_params,\n", - " loading=loading,\n", - " loading_params=loading_params,\n", - " verbose=False,\n", - " bufsize=0.5,\n", - " n_cols_aux=3,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "particles.draw_markers(sort=False)\n", - "particles.initialize_weights()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "particles.markers.shape" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "particles.sorting_boxes.boxes.shape" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "np.set_printoptions(suppress=True, linewidth=300, threshold=300, formatter=dict(float=lambda x: \"%.5f\" % x))\n", - "\n", - "plot_pts = 32\n", - "\n", - "components = [True, False, False, False, False, False]\n", - "nx_b = plot_pts\n", - "be_x = np.linspace(0, 1, nx_b + 1)\n", - "bin_edges = [be_x]\n", - "f_bin, df_bin = particles.binning(components, bin_edges, divide_by_jac=False)\n", - "f_bin.shape" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "x = np.linspace(l1, r1, 100)\n", - "eta1 = np.linspace(0, 1, plot_pts)\n", - "eta2 = np.linspace(0, 1, 1)\n", - "eta3 = np.linspace(0, 1, 1)\n", - "ee1, ee2, ee3 = np.meshgrid(eta1, eta2, eta3, indexing=\"ij\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kernel_type = \"gaussian_1d\"\n", - "h1 = 1 / nx\n", - "h2 = 1 / ny\n", - "h3 = 1 / nz\n", - "\n", - "n_sph_init = particles.eval_density(\n", - " ee1,\n", - " ee2,\n", - " ee3,\n", - " h1=h1,\n", - " h2=h2,\n", - " h3=h3,\n", - " kernel_type=kernel_type,\n", - " fast=True,\n", - ")\n", - "n_sph_init.shape" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "logpos = particles.positions\n", - "weights = particles.weights\n", - "print(f\"{logpos.shape = }\")\n", - "print(f\"{weights.shape = }\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "plt.figure(figsize=(10, 10))\n", - "\n", - "n0 = particles.f_init\n", - "\n", - "plt.subplot(2, 2, 1)\n", - "plt.plot(eta1, np.squeeze(n0(eta1, eta2, eta3).T))\n", - "plt.title(\"$n/\\sqrt{g}$ (0-form)\")\n", - "\n", - "plt.subplot(2, 2, 2)\n", - "ax = plt.gca()\n", - "ax.set_xticks(np.linspace(0, 1, nx + 1))\n", - "ax.set_yticks(np.linspace(0, 1, ny + 1))\n", - "plt.tick_params(labelbottom=False)\n", - "coloring = weights\n", - "plt.scatter(logpos[:, 0], logpos[:, 1], c=coloring, s=0.25)\n", - "plt.grid(c=\"k\")\n", - "plt.axis(\"square\")\n", - "plt.title(\"n0_scatter\")\n", - "plt.xlim(0, 1)\n", - "plt.ylim(0, 1)\n", - "plt.colorbar()\n", - "\n", - "plt.subplot(2, 2, 3)\n", - "ax = plt.gca()\n", - "ax.set_xticks(np.linspace(0, 1, nx + 1))\n", - "# ax.set_yticks(np.linspace(0, 1., ny + 1))\n", - "plt.tick_params(labelbottom=False)\n", - "plt.plot(eta1, n_sph_init[:, 0, 0])\n", - "plt.grid()\n", - "plt.title(\"n_sph_init\")\n", - "\n", - "plt.subplot(2, 2, 4)\n", - "ax = plt.gca()\n", - "ax.set_xticks(np.linspace(0, 1, nx + 1))\n", - "# ax.set_yticks(np.linspace(0, 1., ny + 1))\n", - "plt.tick_params(labelbottom=False)\n", - "bc_x = (be_x[:-1] + be_x[1:]) / 2.0 # centers of binning cells\n", - "plt.plot(bc_x, df_bin.T)\n", - "# plt.grid()\n", - "plt.title(\"n_binned\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.pic.sph_smoothing_kernels import gaussian_uni, linear_uni, trigonometric_uni\n", - "\n", - "x = np.linspace(-1, 1, 200)\n", - "out1 = np.zeros_like(x)\n", - "out2 = np.zeros_like(x)\n", - "out3 = np.zeros_like(x)\n", - "\n", - "for i, xi in enumerate(x):\n", - " out1[i] = trigonometric_uni(xi, 1.0)\n", - " out2[i] = gaussian_uni(xi, 1.0)\n", - " out3[i] = linear_uni(xi, 1.0)\n", - "plt.plot(x, out1, label=\"trigonometric\")\n", - "plt.plot(x, out2, label=\"gaussian\")\n", - "plt.plot(x, out3, label=\"linear\")\n", - "plt.title(\"Some smoothing kernels\")\n", - "plt.legend()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.propagators.propagators_markers import PushEta, PushVinSPHpressure\n", - "\n", - "PushEta.domain = domain\n", - "prop_eta = PushEta(particles, algo=\"forward_euler\")\n", - "\n", - "PushVinSPHpressure.domain = domain\n", - "algo = \"forward_euler\"\n", - "kernel_width = (h1, h2, h3)\n", - "prop_v = PushVinSPHpressure(particles, kernel_type=kernel_type, kernel_width=kernel_width, algo=algo)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "# time stepping\n", - "end_time = r1 - l1 # so that the waves traverse the domain once (c_s = 1)\n", - "dt = 0.05 * (8 / nx) * end_time\n", - "Nt = int(end_time / dt)\n", - "\n", - "Np = particles.positions.shape[0]\n", - "\n", - "pos = np.zeros((Nt + 1, Np, 3), dtype=float)\n", - "weights = np.zeros((Nt + 1, Np), dtype=float)\n", - "n_sph = np.zeros((Nt + 1, *ee1.shape), dtype=float)\n", - "\n", - "pos[0] = domain(particles.positions).T\n", - "weights[0] = particles.weights\n", - "n_sph[0] = n_sph_init\n", - "\n", - "time = 0.0\n", - "time_vec = np.zeros(Nt + 1, dtype=float)\n", - "n = 0\n", - "\n", - "if True:\n", - " while n < Nt:\n", - " time += dt\n", - " n += 1\n", - " time_vec[n] = time\n", - "\n", - " # advance in time\n", - " prop_eta(dt / 2)\n", - " prop_v(dt)\n", - " prop_eta(dt / 2)\n", - "\n", - " # positions on the physical domain Omega\n", - " pos[n] = domain(particles.positions).T\n", - " weights[n] = particles.weights\n", - " n_sph[n] = particles.eval_density(\n", - " ee1,\n", - " ee2,\n", - " ee3,\n", - " h1=h1,\n", - " h2=h2,\n", - " h3=h3,\n", - " kernel_type=kernel_type,\n", - " fast=True,\n", - " )\n", - "\n", - " print(f\"{n} time steps done.\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from matplotlib.ticker import FormatStrFormatter\n", - "\n", - "x, y, z = domain(eta1, eta2, eta3, squeeze_out=True)\n", - "\n", - "plt.figure(figsize=(10, 8))\n", - "interval = Nt / 10\n", - "plot_ct = 0\n", - "for i in range(0, Nt + 1):\n", - " if i % interval == 0:\n", - " print(f\"{i = }\")\n", - " plot_ct += 1\n", - " ax = plt.gca()\n", - "\n", - " if plot_ct <= 6:\n", - " style = \"-\"\n", - " else:\n", - " style = \".\"\n", - " plt.plot(x, n_sph[i, :, 0, 0], style, label=f\"time={i * dt:4.2f}\")\n", - " plt.xlim(l1, r1)\n", - " plt.legend()\n", - " ax.set_xticks(np.linspace(l1, r1, nx + 1))\n", - " ax.xaxis.set_major_formatter(FormatStrFormatter(\"%.2f\"))\n", - " plt.grid(c=\"k\")\n", - " plt.xlabel(\"x\")\n", - " plt.ylabel(r\"$\\rho$\")\n", - "\n", - " plt.title(f\"standing sound wave ($c_s = 1$) for {nx = } and {ppb = }\")\n", - " if plot_ct == 11:\n", - " break" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Gas expansion\n", - "\n", - "We use the same SPH discretization of Euler's equations as described above for sound waves. However, now we simulate a nonlinear gas expansion." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.geometry.domains import Cuboid\n", - "\n", - "l1 = -3\n", - "r1 = 3\n", - "l2 = -3\n", - "r2 = 3\n", - "l3 = 0.0\n", - "r3 = 1.0\n", - "domain = Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "from struphy.fields_background.generic import GenericCartesianFluidEquilibrium\n", - "\n", - "T_h = 0.2\n", - "gamma = 5 / 3\n", - "n_fun = lambda x, y, z: np.exp(-(x**2 + y**2) / T_h) / 35\n", - "\n", - "bckgr = GenericCartesianFluidEquilibrium(n_xyz=n_fun)\n", - "bckgr.domain = domain" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# particle initialization\n", - "from struphy.pic.particles import ParticlesSPH\n", - "\n", - "# marker parameters\n", - "ppb = 400\n", - "nx = 16\n", - "ny = 16\n", - "nz = 1\n", - "boxes_per_dim = (nx, ny, nz)\n", - "bc = [\"periodic\"] * 3\n", - "\n", - "# instantiate Particle object (for random drawing of markers)\n", - "particles_1 = ParticlesSPH(\n", - " bc=bc,\n", - " domain=domain,\n", - " bckgr_params=bckgr,\n", - " ppb=ppb,\n", - " boxes_per_dim=boxes_per_dim,\n", - ")\n", - "\n", - "# instantiate Particle object (for regular tesselation drawing of markers)\n", - "loading = \"tesselation\"\n", - "loading_params = {\"n_quad\": 1}\n", - "bufsize = 0.5\n", - "\n", - "particles_2 = ParticlesSPH(\n", - " bc=bc,\n", - " domain=domain,\n", - " bckgr_params=bckgr,\n", - " ppb=ppb,\n", - " boxes_per_dim=boxes_per_dim,\n", - " loading=loading,\n", - " loading_params=loading_params,\n", - " bufsize=bufsize,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "particles_1.draw_markers(sort=False)\n", - "particles_2.draw_markers(sort=False)\n", - "\n", - "threshold = 1e-1\n", - "particles_1.initialize_weights(reject_weights=True, threshold=threshold)\n", - "particles_2.initialize_weights(reject_weights=True, threshold=threshold)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "particles_1.weights[:10]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "particles_2.weights[:10]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"{particles_1.markers.shape = }\")\n", - "print(f\"{particles_2.markers.shape = }\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"{particles_1.sorting_boxes.boxes.shape = }\")\n", - "print(f\"{particles_2.sorting_boxes.boxes.shape = }\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "components = [True, True, False, False, False, False]\n", - "nx_b = 64\n", - "ny_b = 64\n", - "be_x = np.linspace(0, 1, nx_b + 1)\n", - "be_y = np.linspace(0, 1, ny_b + 1)\n", - "bin_edges = [be_x, be_y]\n", - "\n", - "f_bin_1, df_bin_1 = particles_1.binning(components, bin_edges, divide_by_jac=False)\n", - "f_bin_2, df_bin_2 = particles_2.binning(components, bin_edges, divide_by_jac=False)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "x = np.linspace(l1, r1, 100)\n", - "y = np.linspace(l2, r2, 90)\n", - "xx, yy = np.meshgrid(x, y, indexing=\"ij\")\n", - "eta1 = np.linspace(0, 1, 100)\n", - "eta2 = np.linspace(0, 1, 90)\n", - "eta3 = np.linspace(0, 1, 1)\n", - "ee1, ee2, ee3 = np.meshgrid(eta1, eta2, eta3, indexing=\"ij\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kernel_type = \"gaussian_2d\"\n", - "h1 = 1 / nx\n", - "h2 = 1 / ny\n", - "h3 = 1 / nz\n", - "\n", - "n_sph_1 = particles_1.eval_density(\n", - " ee1,\n", - " ee2,\n", - " ee3,\n", - " h1=h1,\n", - " h2=h2,\n", - " h3=h3,\n", - " kernel_type=kernel_type,\n", - " fast=True,\n", - ")\n", - "n_sph_2 = particles_2.eval_density(\n", - " ee1,\n", - " ee2,\n", - " ee3,\n", - " h1=h1,\n", - " h2=h2,\n", - " h3=h3,\n", - " kernel_type=kernel_type,\n", - " fast=True,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "logpos_1 = particles_1.positions\n", - "logpos_2 = particles_2.positions\n", - "\n", - "weights_1 = particles_1.weights\n", - "weights_2 = particles_2.weights\n", - "\n", - "print(f\"{logpos_1.shape = }\")\n", - "print(f\"{logpos_2.shape = }\")\n", - "print(f\"{weights_1.shape = }\")\n", - "print(f\"{weights_2.shape = }\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "plt.figure(figsize=(12, 22))\n", - "\n", - "n_xyz = bckgr.n_xyz\n", - "n3 = bckgr.n3\n", - "\n", - "plt.subplot(4, 2, 1)\n", - "plt.pcolor(xx, yy, n_fun(xx, yy, 0))\n", - "plt.axis(\"square\")\n", - "plt.title(\"n_xyz\")\n", - "plt.colorbar()\n", - "\n", - "plt.subplot(4, 2, 2)\n", - "plt.pcolor(eta1, eta2, n3(eta1, eta2, 0, squeeze_out=True).T)\n", - "plt.axis(\"square\")\n", - "plt.title(\"$\\hat{n}^{\\t{vol}}$ (volume form)\")\n", - "plt.colorbar()\n", - "\n", - "make_scatter = True\n", - "if make_scatter:\n", - " plt.subplot(4, 2, 3)\n", - " ax = plt.gca()\n", - " ax.set_xticks(np.linspace(0, 1, nx + 1))\n", - " ax.set_yticks(np.linspace(0, 1.0, ny + 1))\n", - " plt.tick_params(labelbottom=False)\n", - " coloring = weights_1\n", - " plt.scatter(logpos_1[:, 0], logpos_1[:, 1], c=coloring, s=0.25)\n", - " plt.grid(c=\"k\")\n", - " plt.axis(\"square\")\n", - " plt.title(\"$\\hat{n}^{\\t{vol}}$ scatter (random)\")\n", - " plt.xlim(0, 1)\n", - " plt.ylim(0, 1)\n", - " plt.colorbar()\n", - "\n", - " plt.subplot(4, 2, 4)\n", - " ax = plt.gca()\n", - " ax.set_xticks(np.linspace(0, 1, nx + 1))\n", - " ax.set_yticks(np.linspace(0, 1.0, ny + 1))\n", - " plt.tick_params(labelbottom=False)\n", - " coloring = weights_2\n", - " plt.scatter(logpos_2[:, 0], logpos_2[:, 1], c=coloring, s=0.25)\n", - " plt.grid(c=\"k\")\n", - " plt.axis(\"square\")\n", - " plt.title(\"$\\hat{n}^{\\t{vol}}$ scatter (tesselation)\")\n", - " plt.xlim(0, 1)\n", - " plt.ylim(0, 1)\n", - " plt.colorbar()\n", - "\n", - "plt.subplot(4, 2, 5)\n", - "ax = plt.gca()\n", - "ax.set_xticks(np.linspace(0, 1, nx + 1))\n", - "ax.set_yticks(np.linspace(0, 1.0, ny + 1))\n", - "plt.tick_params(labelbottom=False)\n", - "plt.pcolor(ee1[:, :, 0], ee2[:, :, 0], n_sph_1[:, :, 0])\n", - "plt.grid()\n", - "plt.axis(\"square\")\n", - "plt.title(\"n_sph (random)\")\n", - "plt.colorbar()\n", - "\n", - "plt.subplot(4, 2, 6)\n", - "ax = plt.gca()\n", - "ax.set_xticks(np.linspace(0, 1, nx + 1))\n", - "ax.set_yticks(np.linspace(0, 1.0, ny + 1))\n", - "plt.tick_params(labelbottom=False)\n", - "plt.pcolor(ee1[:, :, 0], ee2[:, :, 0], n_sph_2[:, :, 0])\n", - "plt.grid()\n", - "plt.axis(\"square\")\n", - "plt.title(\"n_sph (tesselation)\")\n", - "plt.colorbar()\n", - "\n", - "plt.subplot(4, 2, 7)\n", - "ax = plt.gca()\n", - "# ax.set_xticks(np.linspace(0, 1, nx + 1))\n", - "# ax.set_yticks(np.linspace(0, 1., ny + 1))\n", - "# plt.tick_params(labelbottom = False)\n", - "bc_x = (be_x[:-1] + be_x[1:]) / 2.0 # centers of binning cells\n", - "bc_y = (be_y[:-1] + be_y[1:]) / 2.0\n", - "plt.pcolor(bc_x, bc_y, f_bin_1)\n", - "# plt.grid()\n", - "plt.axis(\"square\")\n", - "plt.title(\"n_binned (random)\")\n", - "plt.colorbar()\n", - "\n", - "plt.subplot(4, 2, 8)\n", - "ax = plt.gca()\n", - "# ax.set_xticks(np.linspace(0, 1, nx + 1))\n", - "# ax.set_yticks(np.linspace(0, 1., ny + 1))\n", - "# plt.tick_params(labelbottom = False)\n", - "bc_x = (be_x[:-1] + be_x[1:]) / 2.0 # centers of binning cells\n", - "bc_y = (be_y[:-1] + be_y[1:]) / 2.0\n", - "plt.pcolor(bc_x, bc_y, f_bin_2)\n", - "# plt.grid()\n", - "plt.axis(\"square\")\n", - "plt.title(\"n_binned (tesselation)\")\n", - "plt.colorbar()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.pic.sph_smoothing_kernels import gaussian_uni, linear_uni, trigonometric_uni\n", - "\n", - "x = np.linspace(-1, 1, 200)\n", - "out1 = np.zeros_like(x)\n", - "out2 = np.zeros_like(x)\n", - "out3 = np.zeros_like(x)\n", - "\n", - "for i, xi in enumerate(x):\n", - " out1[i] = trigonometric_uni(xi, 1.0)\n", - " out2[i] = gaussian_uni(xi, 1.0)\n", - " out3[i] = linear_uni(xi, 1.0)\n", - "plt.plot(x, out1, label=\"trigonometric\")\n", - "plt.plot(x, out2, label=\"gaussian\")\n", - "plt.plot(x, out3, label=\"linear\")\n", - "plt.title(\"Some smoothing kernels\")\n", - "plt.legend()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.propagators.propagators_markers import PushEta\n", - "\n", - "# default parameters of Propagator\n", - "opts_eta = PushEta.options(default=False)\n", - "print(opts_eta)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# pass simulation parameters to Propagator class\n", - "PushEta.domain = domain" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# instantiate Propagator object\n", - "prop_eta_1 = PushEta(particles_1, algo=\"forward_euler\")\n", - "prop_eta_2 = PushEta(particles_2, algo=\"forward_euler\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.propagators.propagators_markers import PushVinSPHpressure\n", - "\n", - "# default parameters of Propagator\n", - "opts_sph = PushVinSPHpressure.options(default=False)\n", - "print(opts_sph)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# pass simulation parameters to Propagator class\n", - "PushVinSPHpressure.domain = domain" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# instantiate Propagator object\n", - "algo = \"forward_euler\"\n", - "kernel_width = (h1, h2, h3)\n", - "\n", - "prop_v_1 = PushVinSPHpressure(particles_1, kernel_type=kernel_type, kernel_width=kernel_width, algo=algo)\n", - "\n", - "prop_v_2 = PushVinSPHpressure(particles_2, kernel_type=kernel_type, kernel_width=kernel_width, algo=algo)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "# time stepping\n", - "dt = 0.04\n", - "Nt = 40\n", - "\n", - "Np = particles_1.positions.shape[0]\n", - "pos_1 = np.zeros((Nt + 1, Np, 3), dtype=float)\n", - "velo_1 = np.zeros((Nt + 1, Np, 3), dtype=float)\n", - "energy_1 = np.zeros((Nt + 1, Np), dtype=float)\n", - "\n", - "pos_1[0] = domain(particles_1.positions).T\n", - "velo_1[0] = particles_1.velocities\n", - "\n", - "time = 0.0\n", - "time_vec = np.zeros(Nt + 1, dtype=float)\n", - "n = 0\n", - "while n < Nt:\n", - " time += dt\n", - " n += 1\n", - " time_vec[n] = time\n", - "\n", - " # advance in time\n", - " prop_eta_1(dt / 2)\n", - " prop_v_1(dt)\n", - " prop_eta_1(dt / 2)\n", - "\n", - " # positions on the physical domain Omega\n", - " pos_1[n] = domain(particles_1.positions).T\n", - " velo_1[n] = particles_1.velocities\n", - "\n", - " print(f\"{n} time steps done.\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "plt.figure(figsize=(12, 24))\n", - "interval = Nt / 10\n", - "plot_ct = 0\n", - "for i in range(Nt):\n", - " if i % interval == 0:\n", - " print(f\"{i = }\")\n", - " plot_ct += 1\n", - " plt.subplot(4, 2, plot_ct)\n", - " ax = plt.gca()\n", - " coloring = weights_1\n", - " plt.scatter(pos_1[i, :, 0], pos_1[i, :, 1], c=coloring, s=0.25)\n", - " plt.axis(\"square\")\n", - " plt.title(\"n0_scatter\")\n", - " plt.xlim(l1, r1)\n", - " plt.ylim(l2, r2)\n", - " plt.colorbar()\n", - " plt.title(f\"Gas at t={i * dt}\")\n", - " if plot_ct == 8:\n", - " break" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Np = particles_2.positions.shape[0]\n", - "pos_2 = np.zeros((Nt + 1, Np, 3), dtype=float)\n", - "velo_2 = np.zeros((Nt + 1, Np, 3), dtype=float)\n", - "energy_2 = np.zeros((Nt + 1, Np), dtype=float)\n", - "\n", - "pos_2[0] = domain(particles_2.positions).T\n", - "velo_2[0] = particles_2.velocities\n", - "\n", - "time = 0.0\n", - "time_vec = np.zeros(Nt + 1, dtype=float)\n", - "n = 0\n", - "while n < Nt:\n", - " time += dt\n", - " n += 1\n", - " time_vec[n] = time\n", - "\n", - " # advance in time\n", - " prop_eta_2(dt / 2)\n", - " prop_v_2(dt)\n", - " prop_eta_2(dt / 2)\n", - "\n", - " # positions on the physical domain Omega\n", - " pos_2[n] = domain(particles_2.positions).T\n", - " velo_2[n] = particles_2.velocities\n", - "\n", - " print(f\"{n} time steps done.\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "plt.figure(figsize=(12, 24))\n", - "interval = Nt / 10\n", - "plot_ct = 0\n", - "for i in range(Nt):\n", - " if i % interval == 0:\n", - " print(f\"{i = }\")\n", - " plot_ct += 1\n", - " plt.subplot(4, 2, plot_ct)\n", - " ax = plt.gca()\n", - " coloring = weights_2\n", - " plt.scatter(pos_2[i, :, 0], pos_2[i, :, 1], c=coloring, s=0.25)\n", - " plt.axis(\"square\")\n", - " plt.title(\"n0_scatter\")\n", - " plt.xlim(l1, r1)\n", - " plt.ylim(l2, r2)\n", - " plt.colorbar()\n", - " plt.title(f\"Gas at t={i * dt}\")\n", - " if plot_ct == 8:\n", - " break" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "env", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/tutorials_old/tutorial_03_discrete_derham.ipynb b/tutorials_old/tutorial_03_discrete_derham.ipynb deleted file mode 100644 index f2e8d8ab2..000000000 --- a/tutorials_old/tutorial_03_discrete_derham.ipynb +++ /dev/null @@ -1,367 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 3 - Discrete de Rham sequence\n", - "\n", - "This tutorial covers the access to the discrete FE spaces and the use of operators in the deRham diagram (below). The involved data structures have been discussed in [Tutorial 10 - Struphy data structures](https://struphy.pages.mpcdf.de/struphy/tutorials/tutorial_10_data_structures.html?highlight=data%20structures).\n", - "\n", - "The basics of the 3d DeRham diagram are explained in the [struphy documentation](https://struphy.pages.mpcdf.de/struphy/sections/discretization.html#geometric-finite-elements-feec).\n", - "\n", - "![hi](../pics/derham_complex.png)\n", - "\n", - "The discrete complex in the above diagram (lower row) is loaded via the **Derham** class: " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from psydac.ddm.mpi import mpi as MPI\n", - "\n", - "from struphy.feec.psydac_derham import Derham\n", - "\n", - "Nel = [9, 9, 10] # Number of grid cells\n", - "p = [1, 2, 3] # spline degrees\n", - "spl_kind = [False, True, True] # spline types (clamped vs. periodic)\n", - "\n", - "comm = MPI.COMM_WORLD\n", - "derham = Derham(Nel, p, spl_kind, comm=comm)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us inspect some important attributes of `Derham`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"{derham.grad = }\")\n", - "print(f\"{derham.curl = }\")\n", - "print(f\"{derham.div = }\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# space identifiers\n", - "derham.space_to_form" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# commuting projectors\n", - "for key, val in derham.P.items():\n", - " print(f\"{key = }, {val = }\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Vector spaces for FE coefficients\n", - "for key, val in derham.Vh.items():\n", - " print(f\"{key = }, {val = }\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Polar spaces\n", - "for key, val in derham.Vh_pol.items():\n", - " print(f\"{key = }, {val = }\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# 1D spline spaces ('M' is synonym for 'D'-splines)\n", - "derham.spline_types" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Callable FE fields\n", - "\n", - "Let us create callable spline functions for each discrete space $V_h^n$ and assign a name to it:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "p0 = derham.create_spline_function(\"pressure\", \"H1\")\n", - "e1 = derham.create_spline_function(\"e_field\", \"Hcurl\")\n", - "b2 = derham.create_spline_function(\"b_field\", \"Hdiv\")\n", - "n3 = derham.create_spline_function(\"density\", \"L2\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Moreover, let us initialize these fields in the following way:\n", - "\n", - "* The `pressure` and the 1st component of `e_field` are sinusoidal functions of mode number 2 and amplitude 0.5 in the third direction\n", - "* The 3rd component of `e_field` and the 2nd component of `b_field` are superpositions of two cosines with mode numbers 1 and 2 and amplitudes 0.75 and 0.5, , respectively, in the second direction\n", - "* The `density` has noise of amplitude $10^{-3}$ in the third direction\n", - "* all other components are zero" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pp_pressure = {\n", - " \"ModesSin\": {\n", - " \"given_in_basis\": \"0\",\n", - " \"ns\": [2],\n", - " \"amps\": [0.5],\n", - " }\n", - "}\n", - "\n", - "pp_e_field = {\n", - " \"ModesSin\": {\n", - " \"given_in_basis\": [\"v\", None, None],\n", - " \"ns\": [[2], None, None],\n", - " \"amps\": [[0.5], None, None],\n", - " },\n", - " \"ModesCos\": {\n", - " \"given_in_basis\": [None, None, \"v\"],\n", - " \"ms\": [None, None, [1, 2]],\n", - " \"amps\": [None, None, [0.75, 0.5]],\n", - " },\n", - "}\n", - "\n", - "pp_b_field = {\n", - " \"ModesCos\": {\n", - " \"given_in_basis\": [None, \"v\", None],\n", - " \"ms\": [None, [1, 2], None],\n", - " \"amps\": [None, [0.75, 0.5], None],\n", - " }\n", - "}\n", - "\n", - "pp_density = {\"noise\": {\"comps\": [True], \"direction\": \"e3\", \"amp\": 0.001, \"seed\": 3456546}}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.geometry import domains\n", - "\n", - "domain = domains.Cuboid()\n", - "\n", - "p0.initialize_coeffs(domain=domain, pert_params=pp_pressure)\n", - "e1.initialize_coeffs(domain=domain, pert_params=pp_e_field)\n", - "b2.initialize_coeffs(domain=domain, pert_params=pp_b_field)\n", - "n3.initialize_coeffs(domain=domain, pert_params=pp_density)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us evaluate these fields, squeeze the output and plot all components for verification:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "from matplotlib import pyplot as plt\n", - "\n", - "# evaluation points\n", - "eta1 = 0\n", - "eta2 = np.linspace(0.0, 1.0, 50)\n", - "eta3 = np.linspace(0.0, 1.0, 70)\n", - "\n", - "# evaluate 0-form\n", - "p0_vals = p0(eta1, eta2, eta3, squeeze_out=True)\n", - "print(f\"{type(p0_vals) = }, {p0_vals.shape = }\")\n", - "\n", - "# evaluate 1-form\n", - "e1_vals = e1(eta1, eta2, eta3, squeeze_out=True)\n", - "print(f\"{type(e1_vals) = }, {type(e1_vals[0]) = }, {e1_vals[0].shape = }\")\n", - "\n", - "# evaluate 2-form\n", - "b2_vals = b2(eta1, eta2, eta3, squeeze_out=True)\n", - "print(f\"{type(b2_vals) = }, {type(b2_vals[0]) = }, {b2_vals[0].shape = }\")\n", - "\n", - "# evaluate 3-form\n", - "n3_vals = n3(eta1, eta2, eta3, squeeze_out=True)\n", - "print(f\"{type(n3_vals) = }, {n3_vals.shape = }\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# plotting\n", - "plt.figure(figsize=(12, 14))\n", - "plt.subplot(4, 3, 1)\n", - "plt.plot(eta3, p0_vals[0, :], label=p0.name)\n", - "plt.xlabel(\"$\\eta_3$\")\n", - "plt.legend()\n", - "\n", - "plt.subplot(4, 3, 4)\n", - "plt.plot(eta3, e1_vals[0][0, :], label=(e1.name + \"_1\"))\n", - "plt.xlabel(\"$\\eta_3$\")\n", - "plt.legend()\n", - "plt.subplot(4, 3, 5)\n", - "plt.plot(eta3, e1_vals[1][0, :], label=(e1.name + \"_2\"))\n", - "plt.xlabel(\"$\\eta_3$\")\n", - "plt.legend()\n", - "plt.subplot(4, 3, 6)\n", - "plt.plot(eta2, e1_vals[2][:, 0], label=(e1.name + \"_3\"))\n", - "plt.xlabel(\"$\\eta_2$\")\n", - "plt.legend()\n", - "\n", - "plt.subplot(4, 3, 7)\n", - "plt.plot(eta2, b2_vals[0][:, 0], label=(b2.name + \"_1\"))\n", - "plt.xlabel(\"$\\eta_2$\")\n", - "plt.legend()\n", - "plt.subplot(4, 3, 8)\n", - "plt.plot(eta2, b2_vals[1][:, 0], label=(b2.name + \"_2\"))\n", - "plt.xlabel(\"$\\eta_2$\")\n", - "plt.legend()\n", - "plt.subplot(4, 3, 9)\n", - "plt.plot(eta2, b2_vals[2][:, 0], label=(b2.name + \"_3\"))\n", - "plt.xlabel(\"$\\eta_2$\")\n", - "plt.legend()\n", - "\n", - "plt.subplot(4, 3, 10)\n", - "plt.plot(eta3, n3_vals[0, :], label=n3.name)\n", - "plt.xlabel(\"$\\eta_3$\")\n", - "plt.legend()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Commuting projectors\n", - "\n", - "Next, we shall project a sinusoidal function into $V_h^0$ and, moreover, project its gradient into $V_h^1$:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def fun(x, y, z):\n", - " return 0.5 * np.sin(2 * 2 * np.pi * z)\n", - "\n", - "\n", - "fun_h = derham.P[\"0\"](fun)\n", - "print(f\"{type(fun_h) = }\")\n", - "\n", - "\n", - "def dx_fun(x, y, z):\n", - " return 0 * z\n", - "\n", - "\n", - "def dy_fun(x, y, z):\n", - " return 0 * z\n", - "\n", - "\n", - "def dz_fun(x, y, z):\n", - " return 2 * 2 * np.pi * 0.5 * np.cos(2 * 2 * np.pi * z)\n", - "\n", - "\n", - "dfun_h = derham.P[\"1\"]((dx_fun, dy_fun, dz_fun))\n", - "print(f\"{type(dfun_h) = }\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can check the commuting property by applying the discrete gradient operator." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"{type(derham.grad) = }\")\n", - "gradfun_h = derham.grad.dot(fun_h)\n", - "print(f\"{type(gradfun_h) = }\")\n", - "\n", - "assert np.allclose(dfun_h[0].toarray(), gradfun_h[0].toarray())\n", - "assert np.allclose(dfun_h[1].toarray(), gradfun_h[1].toarray())\n", - "assert np.allclose(dfun_h[2].toarray(), gradfun_h[2].toarray())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**All these operations also work in parallel!**" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "env", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/tutorials_old/tutorial_06_poisson.ipynb b/tutorials_old/tutorial_06_poisson.ipynb deleted file mode 100644 index eeaf8c5ce..000000000 --- a/tutorials_old/tutorial_06_poisson.ipynb +++ /dev/null @@ -1,249 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 6 - Poisson equation\n", - "\n", - "Topics covered in this tutorial:\n", - "\n", - "- instantiate [Derham](https://struphy.pages.mpcdf.de/struphy/sections/subsections/feec_derham.html#module-struphy.feec.psydac_derham) objects for different dimensions\n", - "- creating callable FE fields\n", - "- use of [Projections](https://struphy.pages.mpcdf.de/struphy/sections/subsections/feec_projectors.html#module-struphy.feec.projectors) into Derham\n", - "- instantiate [WeigthedMassOperators](https://struphy.pages.mpcdf.de/struphy/sections/subsections/feec_weightedmass.html#module-struphy.feec.mass) object\n", - "- use of [Poisson](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_fields.html#struphy.propagators.propagators_fields.Poisson) propagator\n", - "\n", - "In what follows we present some examples of the following problem:\n", - "let $\\Omega \\subset \\mathbb R^d$ be open. We want to find $\\phi \\in H^1(\\Omega)$ such that\n", - "\n", - "$$\n", - "- \\nabla \\cdot \\,\\nabla \\phi(\\mathbf x) + \\epsilon \\,\\phi(\\mathbf x) = \\rho(\\mathbf x)\\qquad \\mathbf x \\in \\Omega\\,,\n", - "$$\n", - "\n", - "for suitable boundary conditions, where $\\epsilon \\in \\mathbb R$ is a constant and $\\rho: \\Omega \\to \\mathbb R^+$ is a positive function.\n", - "\n", - "For this we can use the Propagator [Poisson](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_fields.html#struphy.propagators.propagators_fields.Poisson)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.propagators.propagators_fields import Poisson\n", - "\n", - "# default parameters of the Propagator\n", - "opts = Poisson.options(default=True)\n", - "opts" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Manufactured solution in 1D" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up Derham complex\n", - "from struphy.feec.psydac_derham import Derham\n", - "\n", - "Nel = [32, 1, 1]\n", - "p = [1, 1, 1]\n", - "spl_kind = [True, True, True]\n", - "derham = Derham(Nel, p, spl_kind)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up domain Omega\n", - "import numpy as np\n", - "\n", - "from struphy.geometry.domains import Cuboid\n", - "\n", - "l1 = -2 * np.pi\n", - "r1 = 2 * np.pi\n", - "domain = Cuboid(l1=l1, r1=r1)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up mass matrices\n", - "from struphy.feec.mass import WeightedMassOperators\n", - "\n", - "mass_ops = WeightedMassOperators(derham, domain)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# pass simulation parameters to Propagator\n", - "Poisson.derham = derham\n", - "Poisson.domain = domain\n", - "Poisson.mass_ops = mass_ops" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# create solution field in Vh_0 subset H1\n", - "phi = derham.create_spline_function(\"my solution\", \"H1\")\n", - "phi" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "phi.vector" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# manufactured solution, defined on Omega\n", - "k = 2\n", - "f_xyz = lambda x, y, z: np.sin(k * x)\n", - "rhs_xyz = lambda x, y, z: k**2 * np.sin(k * x)\n", - "\n", - "# pullback to the logical unit cube\n", - "rhs = lambda e1, e2, e3: domain.pull(rhs_xyz, e1, e2, e3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# compute rhs vector in Vh_0 subset H1\n", - "from struphy.feec.projectors import L2Projector\n", - "\n", - "l2proj = L2Projector(\"H1\", mass_ops)\n", - "\n", - "rho = l2proj.get_dofs(rhs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# equation parameters\n", - "eps = 1e-12\n", - "\n", - "# instantiate Propagator for the above quation, pass data structure (vector) of FemField\n", - "poisson = Poisson(phi.vector, stab_eps=eps, rho=rho)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# solve (call with arbitrary dt)\n", - "poisson(1.0)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# evalaute at logical coordinates\n", - "e1 = np.linspace(0, 1, 100)\n", - "e2 = 0.5\n", - "e3 = 0.5\n", - "\n", - "funval = phi(e1, e2, e3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# push to Omega\n", - "fh_xyz = domain.push(funval, e1, e2, e3, squeeze_out=True)\n", - "\n", - "x, y, z = domain(e1, e2, e3, squeeze_out=True)\n", - "x.shape" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# plot solution\n", - "from matplotlib import pyplot as plt\n", - "\n", - "plt.plot(x, f_xyz(x, 0.0, 0.0), label=\"exact\")\n", - "plt.plot(x, fh_xyz, \"--r\", label=\"numeric\")\n", - "plt.xlabel(\"x\")\n", - "plt.legend();" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Manufactured solution in a Torus\n", - "\n", - "Under construction ..." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "env", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/tutorials_old/tutorial_07_heat_equation.ipynb b/tutorials_old/tutorial_07_heat_equation.ipynb deleted file mode 100644 index dca4102d8..000000000 --- a/tutorials_old/tutorial_07_heat_equation.ipynb +++ /dev/null @@ -1,511 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 7 - Heat equation\n", - "\n", - "Topics covered in this tutorial:\n", - "\n", - "- [Derham](https://struphy.pages.mpcdf.de/struphy/sections/subsections/feec_derham.html#module-struphy.feec.psydac_derham) with homogeneous Dirichlet boundary conditions\n", - "- creation of a [WeightedMassOperator](https://struphy.pages.mpcdf.de/struphy/sections/subsections/feec_weightedmass.html#struphy.feec.mass.WeightedMassOperator) as diffusion matrix\n", - "- time-dependent version of [ImplicitDiffusion](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_fields.html#struphy.propagators.propagators_fields.ImplicitDiffusion) propagator\n", - "\n", - "In what follows we present some examples of the following problem:\n", - "Let $\\Omega \\subset \\mathbb R^d$ be open. We want to find $\\phi(t) \\in H^1(\\Omega)$, $t \\in [0, T]$, such that\n", - "\n", - "$$\n", - "\\begin{aligned}\n", - "\\frac{\\partial \\phi(t, \\mathbf x)}{\\partial t} - \\nabla \\cdot \\big[D(\\mathbf x) \\,\\nabla \\phi(t, \\mathbf x)\\big] &= 0\\qquad \\mathbf x \\in \\Omega\\,,\n", - "\\\\[2mm]\n", - "\\phi(0, \\mathbf x) &= \\phi_0(\\mathbf x)\\,,\n", - "\\end{aligned}\n", - "$$\n", - "\n", - "for suitable boundary conditions, where $D:\\Omega \\to \\mathbb R^{d \\times d}$ is a positive diffusion matrix, and $\\phi_0\\in H^1(\\Omega)$ is the initial condition." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.propagators.propagators_fields import ImplicitDiffusion\n", - "\n", - "# default parameters of the Propagator\n", - "opts = ImplicitDiffusion.options(default=True)\n", - "opts" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 1D Gaussian blob" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up Derham complex\n", - "from struphy.feec.psydac_derham import Derham\n", - "\n", - "Nel = [32, 1, 1]\n", - "p = [1, 1, 1]\n", - "spl_kind = [False, True, True]\n", - "dirichlet_bc = [[True] * 2, [False] * 2, [False] * 2]\n", - "derham = Derham(Nel, p, spl_kind, dirichlet_bc=dirichlet_bc)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up domain Omega\n", - "from struphy.geometry.domains import Cuboid\n", - "\n", - "l1 = 0.0\n", - "r1 = 10.0\n", - "domain = Cuboid(l1=l1, r1=r1)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up mass matrices\n", - "from struphy.feec.mass import WeightedMassOperators\n", - "\n", - "mass_ops = WeightedMassOperators(derham, domain)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# pass simulation parameters to Propagator\n", - "ImplicitDiffusion.derham = derham\n", - "ImplicitDiffusion.domain = domain\n", - "ImplicitDiffusion.mass_ops = mass_ops" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# initial condition\n", - "import numpy as np\n", - "\n", - "phi0_xyz = lambda x, y, z: np.exp(-((x - 5.0) ** 2) / 0.3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# pullback to the logical unit cube\n", - "phi0_logical = lambda e1, e2, e3: domain.pull(phi0_xyz, e1, e2, e3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# compute initial FE coeffs by projection\n", - "coeffs = derham.P[\"0\"](phi0_logical)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# solution field in Vh_0 subset H1\n", - "phi = derham.create_spline_function(\"my solution\", \"H1\", coeffs=coeffs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# propagator parameters for heat equation\n", - "sigma_1 = 1.0\n", - "sigma_2 = 1.0\n", - "sigma_3 = 0.0\n", - "\n", - "# solver options\n", - "solver = opts[\"solver\"]\n", - "solver[\"recycle\"] = True\n", - "\n", - "# instantiate Propagator for the above quation, pass data structure (vector) of FemField\n", - "prop_heat_eq = ImplicitDiffusion(\n", - " phi.vector, sigma_1=sigma_1, sigma_2=sigma_2, sigma_3=sigma_3, divide_by_dt=True, solver=solver\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# evalaute at logical coordinates\n", - "e1 = np.linspace(0, 1, 100)\n", - "e2 = 0.5\n", - "e3 = 0.5\n", - "\n", - "# time stepping\n", - "Tend = 2.0 - 1e-6\n", - "dt = 0.1\n", - "\n", - "phi_of_t = []\n", - "time = 0.0\n", - "n = 0\n", - "while time < Tend:\n", - " n += 1\n", - "\n", - " # advance in time\n", - " prop_heat_eq(dt)\n", - " time += dt\n", - "\n", - " # evaluate solution and push to Omega\n", - " phi_of_t += [phi(e1, e2, e3)]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from matplotlib import pyplot as plt\n", - "\n", - "# push to Omega for plotting\n", - "x, y, z = domain(e1, e2, e3, squeeze_out=True)\n", - "\n", - "for funvals in phi_of_t:\n", - " fh_xyz = domain.push(funvals, e1, e2, e3, squeeze_out=True)\n", - "\n", - " # plot\n", - " plt.plot(x, fh_xyz)\n", - " plt.xlabel(\"x\")\n", - " plt.title(f\"{n} time steps\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Polar coordinates with diffusion matrix \n", - "\n", - "Let $\\Omega \\subset \\mathbb R^2$ to be the unit disc and assume diffusion along the isolines of the radial coordinate,\n", - "\n", - "$$\n", - " \\mathbf b(x, y) = \\frac{1}{\\sqrt{x^2 + y^2}}\n", - " \\begin{pmatrix}\n", - " y \\\\ -x\n", - " \\end{pmatrix}\\,,\n", - "$$\n", - "\n", - "i.e. the diffusion matrix is given by\n", - "\n", - "$$\n", - " D(x, y) = \\mathbf b(x, y) \\otimes \\mathbf b(x, y)\\,.\n", - "$$" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up Derham complex\n", - "Nel = [32, 32, 1]\n", - "p = [1, 1, 1]\n", - "spl_kind = [False, True, True]\n", - "derham = Derham(Nel, p, spl_kind)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up domain Omega\n", - "from struphy.geometry.domains import HollowCylinder\n", - "\n", - "a1 = 0.1\n", - "a2 = 4.0\n", - "Lz = 1.0\n", - "domain = HollowCylinder(a1=a1, a2=a2, Lz=Lz)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up mass matrices\n", - "mass_ops = WeightedMassOperators(derham, domain)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# pass simulation parameters to Propagator\n", - "ImplicitDiffusion.derham = derham\n", - "ImplicitDiffusion.domain = domain\n", - "ImplicitDiffusion.mass_ops = mass_ops" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# solution field in Vh_0 subset H1\n", - "phi = derham.create_spline_function(\"my solution\", \"H1\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# initial condition\n", - "phi0_xyz = lambda x, y, z: np.exp(-((x - 2.0) ** 2) / 0.3) * np.exp(-((y) ** 2) / 0.3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# pullback to the logical unit cube\n", - "phi0_logical = lambda e1, e2, e3: domain.pull(phi0_xyz, e1, e2, e3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# evaluate initial condition in logical space\n", - "e1 = np.linspace(0, 1, 101)\n", - "e2 = np.linspace(0, 1, 101)\n", - "e3 = 0.5\n", - "\n", - "funvals = phi0_logical(e1, e2, e3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# push to Omega\n", - "fh_xyz = domain.push(funvals, e1, e2, e3, squeeze_out=True)\n", - "print(f\"{fh_xyz.shape = }\")\n", - "\n", - "x, y, z = domain(e1, e2, e3, squeeze_out=True)\n", - "print(f\"{x.shape = }\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# plot at z=0.5\n", - "fig, axs = plt.subplots(1, 2, figsize=(10, 4))\n", - "ax = axs[0]\n", - "\n", - "ax.contourf(x, y, fh_xyz, levels=51)\n", - "ax.axis(\"equal\")\n", - "ax.set_title(\"Initial condition\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "\n", - "# add isolines of r-coordinate\n", - "for i in range(x.shape[0]):\n", - " if i % 5 == 0:\n", - " ax.plot(x[i], y[i], c=\"tab:blue\", alpha=0.4, linewidth=0.5)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# create diffusion matrix\n", - "bx = lambda x, y, z: y / np.sqrt(x**2 + y**2)\n", - "by = lambda x, y, z: -x / np.sqrt(x**2 + y**2)\n", - "bz = lambda x, y, z: 0.0 * x\n", - "\n", - "# vector-field pullback\n", - "bv = lambda e1, e2, e3: domain.pull((bx, by, bz), e1, e2, e3, kind=\"v\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# creation of callable Kronecker matrix\n", - "def Dmat_call(e1, e2, e3):\n", - " bv_vals = bv(e1, e2, e3)\n", - "\n", - " # array from 2d list gives 3x3 array is in the first two indices\n", - " tmp = np.array([[bi * bj for bj in bv_vals] for bi in bv_vals])\n", - "\n", - " # numpy operates on the last two indices with @\n", - " return np.transpose(tmp, axes=(2, 3, 4, 0, 1))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# create and assembla mass matrix\n", - "Dmat = mass_ops.create_weighted_mass(\"Hcurl\", \"Hcurl\", name=\"bb\", weights=[Dmat_call, \"sqrt_g\"], assemble=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# compute initial FE coeffs by projection\n", - "phi.vector = derham.P[\"0\"](phi0_logical)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# propagator parameters for heat equation\n", - "sigma_1 = 1.0\n", - "sigma_2 = 1.0\n", - "sigma_3 = 0.0\n", - "\n", - "# solver options\n", - "solver = opts[\"solver\"]\n", - "solver[\"recycle\"] = True\n", - "\n", - "# instantiate Propagator for the above quation, pass data structure (vector) of FemField\n", - "prop_heat_eq = ImplicitDiffusion(\n", - " phi.vector, sigma_1=sigma_1, sigma_2=sigma_2, sigma_3=sigma_3, diffusion_mat=Dmat, divide_by_dt=True, solver=solver\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# time stepping\n", - "Tend = 6.0 - 1e-6\n", - "dt = 0.1\n", - "\n", - "phi_of_t = []\n", - "time = 0.0\n", - "n = 0\n", - "while time < Tend:\n", - " n += 1\n", - "\n", - " # advance in time\n", - " prop_heat_eq(dt)\n", - " time += dt\n", - "\n", - " # evaluate solution and push to Omega\n", - " phi_of_t += [phi(e1, e2, e3)]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "for funvals in phi_of_t:\n", - " fh_xyz = domain.push(funvals, e1, e2, e3, squeeze_out=True)\n", - "\n", - "# plot\n", - "ax_t = axs[1]\n", - "ax_t.contourf(x, y, fh_xyz, levels=51)\n", - "ax_t.axis(\"equal\")\n", - "ax_t.set_title(f\"{n} time steps\")\n", - "ax_t.set_xlabel(\"x\")\n", - "ax_t.set_ylabel(\"y\")\n", - "\n", - "# add isolines of r-coordinate\n", - "for i in range(x.shape[0]):\n", - " if i % 5 == 0:\n", - " ax_t.plot(x[i], y[i], c=\"tab:blue\", alpha=0.4, linewidth=0.5)\n", - "\n", - "fig" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "env", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/tutorials_old/tutorial_08_maxwell.ipynb b/tutorials_old/tutorial_08_maxwell.ipynb deleted file mode 100644 index a00bd6e33..000000000 --- a/tutorials_old/tutorial_08_maxwell.ipynb +++ /dev/null @@ -1,317 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 8 - Maxwell equations \n", - "\n", - "Topics covered in this tutorial:\n", - "\n", - "- instance of [Maxwell](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_fields.html#struphy.propagators.propagators_fields.Maxwell) propagator\n", - "- initialization with noise\n", - "- power spectrum plot\n", - "\n", - "## Light wave dispersion relation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up domain Omega\n", - "from struphy.geometry.domains import Cuboid\n", - "\n", - "l1 = 0.0\n", - "r1 = 1.0\n", - "l2 = 0.0\n", - "r2 = 1.0\n", - "l3 = 0.0\n", - "r3 = 20.0\n", - "domain = Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up Derham complex\n", - "from struphy.feec.psydac_derham import Derham\n", - "\n", - "Nel = [1, 1, 128]\n", - "p = [1, 1, 3]\n", - "spl_kind = [True, True, True]\n", - "derham = Derham(Nel, p, spl_kind)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up mass matrices\n", - "from struphy.feec.mass import WeightedMassOperators\n", - "\n", - "mass_ops = WeightedMassOperators(derham, domain)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# create solution field E in Vh_1 subset H(curl)\n", - "e_field = derham.create_spline_function(\"electric field\", \"Hcurl\")\n", - "\n", - "# create solution field B in Vh_2 subset H(div)\n", - "b_field = derham.create_spline_function(\"magnetic field\", \"Hdiv\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# initial perturbations\n", - "pert_params_e = {\n", - " \"noise\": {\n", - " \"comps\": [True, True, False],\n", - " \"direction\": \"e3\",\n", - " \"amp\": 0.1,\n", - " \"seed\": None,\n", - " }\n", - "}\n", - "\n", - "pert_params_b = {\n", - " \"noise\": {\n", - " \"comps\": [False, False, False],\n", - " \"direction\": \"e3\",\n", - " \"amp\": 0.1,\n", - " \"seed\": None,\n", - " }\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "e_field.initialize_coeffs(pert_params=pert_params_e)\n", - "b_field.initialize_coeffs(pert_params=pert_params_b)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# evalaute at logical coordinates\n", - "import numpy as np\n", - "\n", - "e1 = 0.5\n", - "e2 = 0.5\n", - "e3 = np.linspace(0, 1, 100)\n", - "\n", - "e_vals = e_field(e1, e2, e3, squeeze_out=True)\n", - "b_vals = b_field(e1, e2, e3, squeeze_out=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "e_vals" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.propagators.propagators_fields import Maxwell\n", - "\n", - "# default parameters of the Propagator\n", - "opts = Maxwell.options(default=True)\n", - "opts" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# pass simulation parameters to Propagator\n", - "Maxwell.derham = derham\n", - "Maxwell.domain = domain\n", - "Maxwell.mass_ops = mass_ops" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "prop_implicit = Maxwell(e_field.vector, b_field.vector)\n", - "prop_rk4 = Maxwell(e_field.vector, b_field.vector, algo=\"rk4\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Tend = 100.0 - 1e-6\n", - "dt = 0.05" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# implicit time stepping\n", - "Ex_of_t_implicit = {}\n", - "time = 0.0\n", - "n = 0\n", - "while time < Tend:\n", - " n += 1\n", - "\n", - " # advance in time\n", - " prop_implicit(dt)\n", - " time += dt\n", - "\n", - " # evaluate solution and push to Omega\n", - " Ex_of_t_implicit[time] = e_field(e1, e2, e3)\n", - "\n", - " if n % 100 == 0:\n", - " print(f\"{n}/{int(np.ceil(Tend / dt))} steps completed with {prop_implicit._algo =}.\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# reset initial condition\n", - "e_field.initialize_coeffs(pert_params=pert_params_e)\n", - "b_field.initialize_coeffs(pert_params=pert_params_b)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# rk4 time stepping\n", - "Ex_of_t_rk4 = {}\n", - "time = 0.0\n", - "n = 0\n", - "while time < Tend:\n", - " n += 1\n", - "\n", - " # advance in time\n", - " prop_rk4(dt)\n", - " time += dt\n", - "\n", - " # evaluate solution and push to Omega\n", - " Ex_of_t_rk4[time] = e_field(e1, e2, e3)\n", - "\n", - " if n % 100 == 0:\n", - " print(f\"{n}/{int(np.ceil(Tend / dt))} steps completed with {prop_rk4._algo = }.\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.diagnostics.diagn_tools import power_spectrum_2d\n", - "\n", - "x, y, z = domain(e1, e2, e3)\n", - "\n", - "# fft in (t, z) of first component of e_field on physical grid\n", - "power_spectrum_2d(\n", - " Ex_of_t_implicit,\n", - " \"e1\",\n", - " \"Maxwell\",\n", - " grids=[e1, e2, e3],\n", - " grids_mapped=[x, y, z],\n", - " component=0,\n", - " slice_at=[0, 0, None],\n", - " do_plot=True,\n", - " disp_name=\"Maxwell1D\",\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# fft in (t, z) of first component of e_field on physical grid\n", - "power_spectrum_2d(\n", - " Ex_of_t_rk4,\n", - " \"e1\",\n", - " \"Maxwell\",\n", - " grids=[e1, e2, e3],\n", - " grids_mapped=[x, y, z],\n", - " component=0,\n", - " slice_at=[0, 0, None],\n", - " do_plot=True,\n", - " disp_name=\"Maxwell1D\",\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Cylindrical wave guide\n", - "\n", - "Under construction ..." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "env", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/tutorials_old/tutorial_09_vlasov_maxwell.ipynb b/tutorials_old/tutorial_09_vlasov_maxwell.ipynb deleted file mode 100644 index c7728e7e0..000000000 --- a/tutorials_old/tutorial_09_vlasov_maxwell.ipynb +++ /dev/null @@ -1,599 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 9 - Vlasov-Maxwell equations\n", - "\n", - "Topics covered in this tutorial:\n", - "\n", - "- instance of [Particels6D](https://struphy.pages.mpcdf.de/struphy/sections/subsections/pic_base.html#struphy.pic.particles.Particles6D) class for PIC simulation\n", - "- phase space binning plots\n", - "- charge deposition with [AccumulatorVector](https://struphy.pages.mpcdf.de/struphy/sections/subsections/pic_base.html#struphy.pic.accumulation.particles_to_grid.AccumulatorVector)\n", - "- solution of inital Poisson problem with [ImplicitDiffusion](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_fields.html#struphy.propagators.propagators_fields.ImplicitDiffusion) propagator\n", - "- particle-field coupling through the propagator [VlasovAmpere](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_coupling.html#struphy.propagators.propagators_coupling.VlasovAmpere)\n", - "- example of weak Landau damping\n", - "\n", - "The equations we will solve are described in the model [VlasovAmpereOneSpecies](https://struphy.pages.mpcdf.de/struphy/sections/subsections/models_kinetic.html#struphy.models.kinetic.VlasovAmpereOneSpecies).\n", - "\n", - "## Weak Landau damping" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up domain Omega\n", - "import numpy as np\n", - "\n", - "from struphy.geometry.domains import Cuboid\n", - "\n", - "l1 = 0.0\n", - "r1 = 12.56\n", - "l2 = 0.0\n", - "r2 = 1.0\n", - "l3 = 0.0\n", - "r3 = 1.0\n", - "domain = Cuboid(l1=l1, r1=r1, l2=l2, r2=r2, l3=l3, r3=r3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up Derham complex\n", - "from struphy.feec.psydac_derham import Derham\n", - "\n", - "Nel = [32, 1, 1]\n", - "p = [1, 1, 1]\n", - "spl_kind = [True, True, True]\n", - "derham = Derham(Nel, p, spl_kind)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up mass matrices\n", - "from struphy.feec.mass import WeightedMassOperators\n", - "\n", - "mass_ops = WeightedMassOperators(derham, domain)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# create particles object\n", - "from struphy.pic.particles import Particles6D\n", - "\n", - "ppc = 10000\n", - "domain_array = derham.domain_array\n", - "nprocs = derham.domain_decomposition.nprocs\n", - "bc = [\"periodic\", \"periodic\", \"periodic\"]\n", - "loading_params = {\"seed\": None}\n", - "control_variate = True\n", - "\n", - "# instantiate Particle object\n", - "particles = Particles6D(\n", - " ppc=ppc,\n", - " domain_decomp=(domain_array, nprocs),\n", - " bc=bc,\n", - " loading_params=loading_params,\n", - " control_variate=control_variate,\n", - " domain=domain,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "particles.draw_markers()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# kinetic equilibrium\n", - "bckgr_params = {\"Maxwellian3D\": {\"n\": 1.0}}\n", - "\n", - "# density perturbation for weak Landau damping\n", - "pert_params = {}\n", - "pert_params[\"n\"] = {}\n", - "pert_params[\"n\"][\"ModesCos\"] = {\n", - " \"given_in_basis\": \"0\",\n", - " \"ls\": [1],\n", - " \"amps\": [0.001],\n", - "}\n", - "\n", - "particles.initialize_weights(bckgr_params=bckgr_params, pert_params=pert_params)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# particle binning in v1\n", - "components = [False] * 6\n", - "components[3] = True\n", - "\n", - "vmin = -5.0\n", - "vmax = 5.0\n", - "n_bins = 128\n", - "bin_edges_v = np.linspace(vmin, vmax, n_bins + 1)\n", - "\n", - "f_v1, df_v1 = particles.binning(components=components, bin_edges=[bin_edges_v])\n", - "print(f\"{f_v1.shape = }\")\n", - "print(f\"{df_v1.shape = }\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# plot in v1\n", - "from matplotlib import pyplot as plt\n", - "\n", - "v1_bins = bin_edges_v[:-1] + (vmax - vmin) / n_bins / 2\n", - "plt.plot(v1_bins, f_v1)\n", - "plt.xlabel(\"vx\")\n", - "plt.title(\"Initial Maxwellian\");" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# particle binning in e1\n", - "components = [False] * 6\n", - "components[0] = True\n", - "\n", - "emin = 0.0\n", - "emax = 1.0\n", - "bin_edges_e = np.linspace(emin, emax, n_bins + 1)\n", - "\n", - "f_e1, df_e1 = particles.binning(components=components, bin_edges=[bin_edges_e])\n", - "print(f\"{f_e1.shape = }\")\n", - "print(f\"{df_e1.shape = }\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# plot in e1\n", - "e1_bins = bin_edges_e[:-1] + (emax - emin) / n_bins / 2\n", - "plt.plot(e1_bins, df_e1)\n", - "plt.xlabel(\"$\\eta_1$\")\n", - "plt.title(\"Initial spatial perturbation\");" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# particle binning in e1-v1\n", - "components = [False] * 6\n", - "components[0] = True\n", - "components[3] = True\n", - "\n", - "f_e1v1, df_e1v1 = particles.binning(components=components, bin_edges=[bin_edges_e, bin_edges_v])\n", - "print(f\"{f_e1v1.shape = }\")\n", - "print(f\"{df_e1v1.shape = }\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "e1_bins = bin_edges_e[:-1] + (emax - emin) / n_bins / 2\n", - "\n", - "plt.figure(figsize=(7, 10))\n", - "\n", - "plt.subplot(2, 1, 1)\n", - "plt.pcolor(e1_bins, v1_bins, f_e1v1.T)\n", - "plt.xlabel(\"$\\eta_1$\")\n", - "plt.ylabel(\"$v_x$\")\n", - "plt.title(\"Initial Maxwellian\")\n", - "plt.colorbar()\n", - "\n", - "plt.subplot(2, 1, 2)\n", - "plt.pcolor(e1_bins, v1_bins, df_e1v1.T)\n", - "plt.xlabel(\"$\\eta_1$\")\n", - "plt.ylabel(\"$v_x$\")\n", - "plt.title(\"Initial perturbation\")\n", - "plt.colorbar();" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We need to solve the Poisson equation once to get the correct initial condition for $\\mathbf E$. For this we use the [ImplicitDiffusion](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_fields.html#struphy.propagators.propagators_fields.ImplicitDiffusion) propagator, see Tutorial 02. The first step is to deposit the charge to the FE grid with an [AccumulatorVector](https://struphy.pages.mpcdf.de/struphy/sections/subsections/pic_base.html#struphy.pic.accumulation.particles_to_grid.AccumulatorVector) object." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# accumulate charge density\n", - "from struphy.pic.accumulation.accum_kernels import charge_density_0form\n", - "from struphy.pic.accumulation.particles_to_grid import AccumulatorVector\n", - "from struphy.utils.pyccel import Pyccelkernel\n", - "\n", - "# instantiate\n", - "charge_accum = AccumulatorVector(\n", - " particles=particles,\n", - " space_id=\"H1\",\n", - " kernel=Pyccelkernel(charge_density_0form),\n", - " mass_ops=mass_ops,\n", - " args_domain=domain.args_domain,\n", - ")\n", - "\n", - "# accumulate\n", - "charge_accum(particles.vdim)\n", - "\n", - "# get result\n", - "rho_vec = charge_accum.vectors[0]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# use L2-projection to get density\n", - "from struphy.feec.projectors import L2Projector\n", - "\n", - "l2_proj = L2Projector(space_id=\"H1\", mass_ops=mass_ops)\n", - "\n", - "rho_coeffs = l2_proj.solve(rho_vec)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# fit rho coeffs into a callable field\n", - "rho = derham.create_spline_function(name=\"charge density\", space_id=\"H1\", coeffs=rho_coeffs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# evaluate at logical coordinates\n", - "e1 = np.linspace(0, 1, 100)\n", - "e2 = 0.5\n", - "e3 = 0.5\n", - "\n", - "funval = rho(e1, e2, e3, squeeze_out=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# plot rho in logical space\n", - "plt.plot(e1, 1e-3 * np.cos(2 * np.pi * e1), label=\"exact\")\n", - "plt.plot(e1, funval, \"--r\", label=\"L2 projection of charge deposition\")\n", - "plt.xlabel(\"$\\eta_1$\")\n", - "plt.title(\"Charge density for Poisson solver\")\n", - "plt.legend();" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.propagators.propagators_fields import Poisson\n", - "\n", - "# default parameters of the Propagator\n", - "opts = Poisson.options(default=True)\n", - "opts" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# pass simulation parameters to Propagator\n", - "Poisson.derham = derham\n", - "Poisson.domain = domain\n", - "Poisson.mass_ops = mass_ops" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# create solution field in Vh_0 subset H1\n", - "phi = derham.create_spline_function(\"my solution\", \"H1\")\n", - "\n", - "# create solution field E in Vh_1 subset H(curl)\n", - "e_field = derham.create_spline_function(\"electric field\", \"Hcurl\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "phi.vector" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "e_field.vector" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# equation parameters\n", - "eps = 1e-12\n", - "\n", - "# instantiate Propagator for the above quation, pass data structure (vector) of FemField\n", - "poisson = Poisson(phi.vector, stab_eps=eps, rho=rho.vector)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# solve (call with arbitrary dt)\n", - "poisson(1.0)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# compute initial E field\n", - "e_field.vector = -derham.grad.dot(phi.vector)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# evalaute at logical coordinates\n", - "e1 = np.linspace(0, 1, 100)\n", - "e2 = 0.5\n", - "e3 = 0.5\n", - "\n", - "e_vals = e_field(e1, e2, e3, squeeze_out=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "e_vals" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# plot solution\n", - "from matplotlib import pyplot as plt\n", - "\n", - "plt.plot(e1, e_vals[0], label=\"E\")\n", - "plt.xlabel(\"$\\eta_1$\")\n", - "plt.title(\"Initial electric field\")\n", - "plt.legend();" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We now create instances of the propagators [PushEta](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_markers.html#struphy.propagators.propagators_markers.PushEta) and [VlasovAmpere](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_coupling.html#struphy.propagators.propagators_coupling.VlasovAmpere), which together build the model [VlasovAmpereOneSpecies](https://struphy.pages.mpcdf.de/struphy/sections/subsections/models_kinetic.html#struphy.models.kinetic.VlasovAmpereOneSpecies)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.propagators.propagators_markers import PushEta\n", - "\n", - "# default parameters of Propagator\n", - "opts_eta = PushEta.options(default=True)\n", - "print(opts_eta)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# pass simulation parameters to Propagator class\n", - "PushEta.domain = domain" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# instantiate Propagator object\n", - "prop_eta = PushEta(particles)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.propagators.propagators_coupling import VlasovAmpere\n", - "\n", - "# default parameters of Propagator\n", - "opts_coupling = VlasovAmpere.options(default=True)\n", - "print(opts_coupling)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# pass simulation parameters to Propagator class\n", - "VlasovAmpere.domain = domain\n", - "VlasovAmpere.derham = derham\n", - "VlasovAmpere.mass_ops = mass_ops" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "prop_coupling = VlasovAmpere(e_field.vector, particles)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from time import time\n", - "\n", - "import numpy as np\n", - "\n", - "# diagnostics\n", - "time_vec = []\n", - "energy_E = []\n", - "\n", - "# initial values\n", - "time_vec += [0.0]\n", - "energy_E += [0.5 * mass_ops.M1.dot_inner(e_field.vector, e_field.vector)]\n", - "\n", - "# time stepping\n", - "Tend = 3.5\n", - "dt = 0.05\n", - "Nt = int(Tend / dt)\n", - "\n", - "t = 0.0\n", - "n = 0\n", - "while t < (Tend - dt):\n", - " t += dt\n", - " n += 1\n", - "\n", - " t0 = time()\n", - " # advance in time\n", - " prop_eta(dt)\n", - " t1 = time()\n", - " print(f\"Time for PushEta = {t1 - t0}\")\n", - "\n", - " prop_coupling(dt)\n", - " t2 = time()\n", - " print(f\"Time for VlasovAmpere = {t2 - t1}\")\n", - "\n", - " print(f\"Time step {n} done in {t2 - t0} sec\\n\")\n", - "\n", - " # diagnostics\n", - " time_vec += [t]\n", - " energy_E += [0.5 * mass_ops.M1.dot_inner(e_field.vector, e_field.vector)]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "plt.plot(time_vec, np.log(energy_E))" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "env", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.3" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/tutorials_old/tutorial_10_linear_mhd.ipynb b/tutorials_old/tutorial_10_linear_mhd.ipynb deleted file mode 100644 index 9e801ae80..000000000 --- a/tutorials_old/tutorial_10_linear_mhd.ipynb +++ /dev/null @@ -1,646 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 10 - Linear MHD equations\n", - "\n", - "Topics covered in this tutorial:\n", - "\n", - "- instance of [ShearAlfven](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_fields.html#struphy.propagators.propagators_fields.ShearAlfven) propagator\n", - "- instance of [Magnetosonic](https://struphy.pages.mpcdf.de/struphy/sections/subsections/propagators_fields.html#struphy.propagators.propagators_fields.Magnetosonic) propagator\n", - "- initialization with noise\n", - "- power spectrum plot\n", - "- $\\theta$-pinch and $Z$-pinch configurations\n", - "\n", - "We are concerned with the solution of the ideal, linearized MHD equations coded in the model [LinearMHD](https://struphy.pages.mpcdf.de/struphy/sections/subsections/models_fluid.html#struphy.models.fluid.LinearMHD):\n", - "\n", - "$$\n", - "\\begin{align}\n", - " &\\frac{\\partial \\tilde \\rho}{\\partial t}+\\nabla\\cdot(\\rho_0 \\tilde{\\mathbf{U}})=0\\,, \n", - " \\\\[2mm]\n", - " \\rho_0&\\frac{\\partial \\tilde{\\mathbf{U}}}{\\partial t} + \\nabla \\tilde p\n", - " = (\\nabla \\times \\tilde{\\mathbf{B}})\\times \\mathbf{B}_0 + (\\nabla\\times\\mathbf{B}_0)\\times \\tilde{\\mathbf{B}} \\,,\n", - " \\\\[2mm]\n", - " &\\frac{\\partial \\tilde p}{\\partial t} + \\nabla\\cdot(p_0 \\tilde{\\mathbf{U}}) \n", - " + \\frac{2}{3}\\,p_0\\nabla\\cdot \\tilde{\\mathbf{U}}=0\\,,\n", - " \\\\[2mm]\n", - " &\\frac{\\partial \\tilde{\\mathbf{B}}}{\\partial t} - \\nabla\\times(\\tilde{\\mathbf{U}} \\times \\mathbf{B}_0)\n", - " = 0\\,.\n", - "\\end{align}\n", - "$$\n", - "\n", - "## MHD dispersion relation in a slab" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up domain Omega\n", - "from struphy.geometry.domains import Cuboid\n", - "\n", - "xL = 0.0\n", - "xR = 1.0\n", - "yL = 0.0\n", - "yR = 1.0\n", - "zL = 0.0\n", - "zR = 60.0\n", - "domain = Cuboid(l1=xL, r1=xR, l2=yL, r2=yR, l3=zL, r3=zR)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up MHD equilibrium\n", - "from struphy.fields_background.equils import HomogenSlab\n", - "\n", - "B0x = 0.0\n", - "B0y = 1.0\n", - "B0z = 1.0\n", - "beta = 1.0\n", - "n0 = 1.0\n", - "mhd_equil = HomogenSlab(B0x=B0x, B0y=B0y, B0z=B0z, beta=beta, n0=n0)\n", - "\n", - "# must set domain of Cartesian MHD equilibirum\n", - "mhd_equil.domain = domain" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up Derham complex\n", - "from struphy.feec.psydac_derham import Derham\n", - "\n", - "Nel = [1, 1, 64]\n", - "p = [1, 1, 3]\n", - "spl_kind = [True, True, True]\n", - "derham = Derham(Nel, p, spl_kind)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# create solution field u in Vh_2 subset H(div)\n", - "u_space = \"Hdiv\" # choose 'H1vec' for comparison\n", - "mhd_u = derham.create_spline_function(\"velocity\", u_space)\n", - "\n", - "# create solution field B in Vh_2 subset H(div)\n", - "b_field = derham.create_spline_function(\"magnetic field\", \"Hdiv\")\n", - "\n", - "# create solution fields rho and p in Vh_3 subset L2\n", - "mhd_rho = derham.create_spline_function(\"mass density\", \"L2\")\n", - "mhd_p = derham.create_spline_function(\"pressure\", \"L2\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# initial perturbations\n", - "pert_params_u = {\n", - " \"noise\": {\n", - " \"comps\": [True, True, True],\n", - " \"direction\": \"e3\",\n", - " \"amp\": 0.1,\n", - " \"seed\": None,\n", - " }\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "mhd_u.initialize_coeffs(pert_params=pert_params_u)\n", - "b_field.initialize_coeffs()\n", - "mhd_rho.initialize_coeffs()\n", - "mhd_p.initialize_coeffs()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# evalaute at logical coordinates\n", - "import numpy as np\n", - "\n", - "e1 = 0.5\n", - "e2 = 0.5\n", - "e3 = np.linspace(0, 1, 100)\n", - "\n", - "u_vals = mhd_u(e1, e2, e3, squeeze_out=True)\n", - "b_vals = b_field(e1, e2, e3, squeeze_out=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# plot inital conditions\n", - "\n", - "from matplotlib import pyplot as plt\n", - "\n", - "plt.figure(figsize=(10, 6))\n", - "for i in range(3):\n", - " plt.subplot(2, 3, i + 1)\n", - " plt.plot(e3, u_vals[i])\n", - " plt.title(f\"$\\hat u^{2 if u_space == 'Hdiv2' else ' '}_{i + 1}$\")\n", - " plt.xlabel(\"$\\eta_3$\")\n", - " if i == 0:\n", - " plt.ylabel(\"a.u.\")\n", - "\n", - " plt.subplot(2, 3, i + 4)\n", - " plt.plot(e3, b_vals[i])\n", - " plt.title(f\"$\\hat b^2_{i + 1}$\")\n", - " plt.xlabel(\"$\\eta_3$\")\n", - " if i == 0:\n", - " plt.ylabel(\"a.u.\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up mass matrices\n", - "from struphy.feec.mass import WeightedMassOperators\n", - "\n", - "mass_ops = WeightedMassOperators(derham, domain, eq_mhd=mhd_equil)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up basis projection operators\n", - "from struphy.feec.basis_projection_ops import BasisProjectionOperators\n", - "\n", - "basis_ops = BasisProjectionOperators(derham, domain, eq_mhd=mhd_equil)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# pass simulation parameters to Propagators\n", - "from struphy.propagators.base import Propagator\n", - "\n", - "Propagator.derham = derham\n", - "Propagator.domain = domain\n", - "Propagator.mass_ops = mass_ops\n", - "Propagator.basis_ops = basis_ops" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.propagators.propagators_fields import Magnetosonic, ShearAlfven\n", - "\n", - "# default parameters of Propagator\n", - "opts = ShearAlfven.options(default=True)\n", - "opts" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# default parameters of Propagator\n", - "opts = Magnetosonic.options(default=True)\n", - "opts" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "prop_1 = ShearAlfven(mhd_u.vector, b_field.vector, u_space=u_space)\n", - "prop_1_explicit = ShearAlfven(mhd_u.vector, b_field.vector, u_space=u_space, algo=\"rk4\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "prop_2 = Magnetosonic(mhd_rho.vector, mhd_u.vector, mhd_p.vector, u_space=u_space, b=b_field.vector)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# time stepping, with both propagators\n", - "Tend = 180.0 - 1e-6\n", - "dt = 0.15\n", - "\n", - "u_of_t = {}\n", - "p_of_t = {}\n", - "time = 0.0\n", - "n = 0\n", - "while time < Tend:\n", - " n += 1\n", - "\n", - " # advance in time\n", - " prop_1(dt)\n", - " prop_2(dt)\n", - " time += dt\n", - "\n", - " # evaluate solution\n", - " u_of_t[time] = mhd_u(e1, e2, e3)\n", - " p_of_t[time] = [mhd_p(e1, e2, e3)]\n", - "\n", - " if n % 100 == 0:\n", - " print(f\"{n}/{int(np.ceil(Tend / dt))} steps completed.\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# reset initial condition\n", - "mhd_u.initialize_coeffs(pert_params=pert_params_u)\n", - "b_field.initialize_coeffs()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# time stepping, with both propagators\n", - "Tend = 180.0 - 1e-6\n", - "dt = 0.15\n", - "\n", - "u_of_t_ex = {}\n", - "p_of_t_ex = {}\n", - "time = 0.0\n", - "n = 0\n", - "while time < Tend:\n", - " n += 1\n", - "\n", - " # advance in time\n", - " prop_1_explicit(dt)\n", - " prop_2(dt)\n", - " time += dt\n", - "\n", - " # evaluate solution\n", - " u_of_t_ex[time] = mhd_u(e1, e2, e3)\n", - " p_of_t_ex[time] = [mhd_p(e1, e2, e3)]\n", - "\n", - " if n % 100 == 0:\n", - " print(f\"{n}/{int(np.ceil(Tend / dt))} steps completed.\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.diagnostics.diagn_tools import power_spectrum_2d\n", - "\n", - "x, y, z = domain(e1, e2, e3)\n", - "\n", - "# equilibrium pressure\n", - "p0 = beta * (B0x**2 + B0y**2 + B0z**2) / 2\n", - "\n", - "disp_params = {\"B0x\": B0x, \"B0y\": B0y, \"B0z\": B0z, \"p0\": p0, \"n0\": n0, \"gamma\": 5 / 3}\n", - "\n", - "# fft in (t, z) of first component of e_field on physical grid\n", - "power_spectrum_2d(\n", - " u_of_t,\n", - " \"mhd_u\",\n", - " \"notebook tutorial\",\n", - " grids=[e1, e2, e3],\n", - " grids_mapped=[x, y, z],\n", - " component=0,\n", - " slice_at=[0, 0, None],\n", - " do_plot=True,\n", - " disp_name=\"MHDhomogenSlab\",\n", - " disp_params=disp_params,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.diagnostics.diagn_tools import power_spectrum_2d\n", - "\n", - "x, y, z = domain(e1, e2, e3)\n", - "\n", - "# equilibrium pressure\n", - "p0 = beta * (B0x**2 + B0y**2 + B0z**2) / 2\n", - "\n", - "disp_params = {\"B0x\": B0x, \"B0y\": B0y, \"B0z\": B0z, \"p0\": p0, \"n0\": n0, \"gamma\": 5 / 3}\n", - "\n", - "# fft in (t, z) of first component of e_field on physical grid\n", - "power_spectrum_2d(\n", - " u_of_t_ex,\n", - " \"mhd_u\",\n", - " \"notebook tutorial\",\n", - " grids=[e1, e2, e3],\n", - " grids_mapped=[x, y, z],\n", - " component=0,\n", - " slice_at=[0, 0, None],\n", - " do_plot=True,\n", - " disp_name=\"MHDhomogenSlab\",\n", - " disp_params=disp_params,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "power_spectrum_2d(\n", - " p_of_t,\n", - " \"mhd_p\",\n", - " \"notebook tutorial\",\n", - " grids=[e1, e2, e3],\n", - " grids_mapped=[x, y, z],\n", - " component=0,\n", - " slice_at=[0, 0, None],\n", - " do_plot=True,\n", - " disp_name=\"MHDhomogenSlab\",\n", - " disp_params=disp_params,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "power_spectrum_2d(\n", - " p_of_t_ex,\n", - " \"mhd_p\",\n", - " \"notebook tutorial\",\n", - " grids=[e1, e2, e3],\n", - " grids_mapped=[x, y, z],\n", - " component=0,\n", - " slice_at=[0, 0, None],\n", - " do_plot=True,\n", - " disp_name=\"MHDhomogenSlab\",\n", - " disp_params=disp_params,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## $\\theta$-pinch stability\n", - "\n", - "Under construction ..." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## $Z$-pinch stability\n", - "\n", - "Under construction ..." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Screw-pinch modes" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up domain Omega\n", - "from struphy.geometry.domains import HollowCylinder\n", - "\n", - "a = 1\n", - "R0 = 3\n", - "\n", - "a1 = 0.0 + 1e-6\n", - "a2 = a\n", - "Lz = 2 * np.pi * R0\n", - "domain = HollowCylinder(a1=a1, a2=a2, Lz=Lz)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up MHD equilibrium\n", - "from struphy.fields_background.equils import ScrewPinch\n", - "\n", - "mhd_equil = ScrewPinch(a=a, R0=R0)\n", - "\n", - "# must set domain of Cartesian MHD equilibirum\n", - "mhd_equil.domain = domain" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "mhd_equil.plot_profiles()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "mhd_equil.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# set up Derham complex\n", - "from struphy.feec.psydac_derham import Derham\n", - "\n", - "Nel = [16, 32, 8]\n", - "p = [1, 1, 1]\n", - "spl_kind = [False, True, True]\n", - "derham = Derham(Nel, p, spl_kind)\n", - "\n", - "# ..under construction" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# # initial perturbations\n", - "# pert_params = {}\n", - "# pert_params['type'] = 'ModesCos'\n", - "\n", - "# noise_params = {\n", - "# 'comps' : {\n", - "# 'velocity' : [True, True, True],\n", - "# },\n", - "# 'direction' : 'e3',\n", - "# 'amp' : 0.1,\n", - "# 'seed' : None,\n", - "# }\n", - "\n", - "# pert_params['noise'] = noise_params\n", - "# pert_params" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# # create solution field u in Vh_2 subset H(div)\n", - "# u_space = 'Hdiv' # choose 'H1vec' for comparison\n", - "# mhd_u = derham.create_spline_function('velocity', u_space, pert_params=pert_params)\n", - "\n", - "# # create solution field B in Vh_2 subset H(div)\n", - "# b_field = derham.create_spline_function('magnetic field', 'Hdiv', pert_params=pert_params)\n", - "\n", - "# # create solution fields rho and p in Vh_3 subset L2\n", - "# mhd_rho = derham.create_spline_function('mass density', 'L2', pert_params=pert_params)\n", - "# mhd_p = derham.create_spline_function('pressure', 'L2', pert_params=pert_params)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# mhd_u.initialize_coeffs()\n", - "# b_field.initialize_coeffs()\n", - "# mhd_rho.initialize_coeffs()\n", - "# mhd_p.initialize_coeffs()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# # evalaute at logical coordinates\n", - "# import numpy as np\n", - "\n", - "# e1 = .5\n", - "# e2 = .5\n", - "# e3 = np.linspace(0, 1, 100)\n", - "\n", - "# u_vals = mhd_u(e1, e2, e3, squeeze_out=True)\n", - "# b_vals = b_field(e1, e2, e3, squeeze_out=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# # plot inital conditions\n", - "\n", - "# from matplotlib import pyplot as plt\n", - "\n", - "# plt.figure(figsize=(10, 6))\n", - "# for i in range(3):\n", - "# plt.subplot(2, 3, i + 1)\n", - "# plt.plot(e3, u_vals[i])\n", - "# plt.title(f'$\\hat u^{2 if u_space == \"Hdiv2\" else \" \"}_{i + 1}$')\n", - "# plt.xlabel('$\\eta_3$')\n", - "# if i == 0:\n", - "# plt.ylabel('a.u.')\n", - "\n", - "# plt.subplot(2, 3, i + 4)\n", - "# plt.plot(e3, b_vals[i])\n", - "# plt.title(f'$\\hat b^2_{i + 1}$')\n", - "# plt.xlabel('$\\eta_3$')\n", - "# if i == 0:\n", - "# plt.ylabel('a.u.')" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/tutorials_old/tutorial_12_struphy_data_pproc.ipynb b/tutorials_old/tutorial_12_struphy_data_pproc.ipynb deleted file mode 100644 index c05ee2535..000000000 --- a/tutorials_old/tutorial_12_struphy_data_pproc.ipynb +++ /dev/null @@ -1,642 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 12 - Data, post processing and standard plots\n", - "\n", - "Topics coverd in this tutorial:\n", - "\n", - "- Look at the data generated from a Struphy simulation of the model [LinearMHDVlasovCC](https://struphy.pages.mpcdf.de/struphy/sections/models.html#struphy.models.hybrid.LinearMHDVlasovCC).\n", - "- Run the post processing file `strupy/post_processing/pproc_struphy/main.py`, which is executed upon calling\n", - " ```\n", - " $ struphy pproc DIR\n", - " ```\n", - " from the console (we will not invoke the console and run directly from the notebook).\n", - "- Inspect the data generated during post processing.\n", - "- Extract the time grid.\n", - "- Look at binning data of the kinetic distribution function (in v and in x-v space).\n", - "- Look at 1D snapshots of one component of the magnetic field.\n", - "\n", - "Let us generate the data for this tutorial:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!{'struphy run LinearMHDVlasovCC -i tutorials/params_02.yml -o tutorial_02/'}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "\n", - "## Data generated by Struphy runs\n", - "\n", - "At first, we look at the raw data coming from the simulation (before post processing):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "\n", - "import struphy\n", - "\n", - "path_out = os.path.join(struphy.__path__[0], \"io/out\", \"tutorial_02\")\n", - "\n", - "print(path_out)\n", - "os.listdir(path_out)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Two files and one folder named `data/` have been created by the simulation. The file `parameters.yml` is a copy of the parameter file used in the simulation. Let us check the metadata in `meta.txt`, where we can find some useful information about the simulation, such as date of execution, operating system, number of processes etc.:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "with open(os.path.join(path_out, \"meta.txt\")) as file:\n", - " print(file.read())" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us now inspect the content of the `data/` folder:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "path_data = os.path.join(path_out, \"data/\")\n", - "\n", - "os.listdir(path_data)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Since the simulation was ran with only one MPI process from a notebook, only one `.hdf5` file has been created (on process 0). In general, one such file will be created for each process. Let us inpect the content of the file:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import h5py\n", - "\n", - "with h5py.File(os.path.join(path_data, \"data_proc0.hdf5\"), \"r\") as f:\n", - " for key in f.keys():\n", - " print(key + \"/\")\n", - " for subkey in f[key].keys():\n", - " print(\" \" + subkey + \"/\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can see five top level keys under which data is stored:\n", - "\n", - "1. `feec` stores the finite element coefficients of the electromagnetic fields (only the magentic field 'b2' in this example) and of each fluid species (only one species `mhd` in this case).\n", - "2. `kinetic` stores, for each kinetic species (only one species `energetic_ions` in this case), the binning data of the distribution function and a certain number of selected marker trajectories (can be specified in the parameters file).\n", - "3. `restart` stores data in case the simualtion has been interrupted.\n", - "4. `scalar` stores the scalar quantities of the simulation, such as energies for instance.\n", - "5. `time` stores the time grid.\n", - "\n", - "## Data post processing\n", - "\n", - "As a user, we do not need to access the above data directly. This is handled by Struphy's main post processing routine, which should usually be called from the console:\n", - "\n", - " $ struphy pproc DIR\n", - " \n", - "Here, we look at this routine in a bit more detail:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from struphy.post_processing.pproc_struphy import main\n", - "\n", - "help(main)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can see one manadatory argument, namely the `path` to the simulation output folder. Let us perform the post processing on our example folder. This will perform the following steps:\n", - "\n", - "1. Creation of Psydac FemFields.\n", - "2. Evaluation of fields on the grid specified by `celldivide`.\n", - "3. Creation of `.vtk` files for further diagnostics in Paraview.\n", - "4. Evaluation of marker orbits on the mapped domain (Euclidean space).\n", - "5. Collection of binning data of the distriution function (and evaluation of background in case of df-methods) from all processes. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "main(path_out, physical=True)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If we inspect again the simulation folder after post processing, we see that an additional folder `post_processing/` has been created:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "os.listdir(path_out)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us look at its content:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "data_path = os.path.join(path_out, \"post_processing\")\n", - "os.listdir(data_path)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us extract the time grid of the simulation:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "t_grid = np.load(os.path.join(data_path, \"t_grid.npy\"))\n", - "t_grid" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Kinetic data\n", - "\n", - "First, we look at `kinetic_data/`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kinetic_path = os.path.join(data_path, \"kinetic_data\")\n", - "\n", - "print(os.listdir(kinetic_path))" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "`kinetic_data/` contains one folder for each kinetic species (here just one species `energetic_ions/`). Let us inspect its content:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ep_path = os.path.join(kinetic_path, \"energetic_ions\")\n", - "\n", - "os.listdir(ep_path)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There are two kinds of kinetic data: \n", - "\n", - "1. the binned `distribution_function/` ,\n", - "2. particle `orbits/` of selected markers, which can be chosen in the parameter file.\n", - "\n", - "### Particle binning plots\n", - "\n", - "Details on the mathematics of particle binning can be found under https://struphy.pages.mpcdf.de/struphy/sections/discretization.html#particle-binning." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "f_path = os.path.join(ep_path, \"distribution_function\")\n", - "\n", - "print(os.listdir(f_path))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Under `distribution_function/` we find the two folders `e1_v1/` and `v1/`, respectively, one for each binning of $f$ during the simulation. The binning directions have been defined in the parameter file before the run. \n", - "\n", - "You can do several binnings during one simulation. For an *n*-dimensional binning in phase space, there are *n+1* `.npy` files created in the respective folder: one file `f_binned.npy` with the binned distribution function, and *n* files with with the corresponding 1d phase space grids in each direction. The naming convention is as follows:\n", - "\n", - "1. Suppose we want 1-dimensional binning in the direction `v1` in velocity space. The folder is then called `v1`, the binned distribution function is saved under `v1/f_binned.npy` and the velocity binning grid is saved under `v1/grid_v1.npy`. The same could be done for instance in position space along the coordinate `e2`, which would lead to files `e2/f_binned.npy` and `e2/grid_e2.npy`, respectively.\n", - "\n", - "2. Suppose we want to do 2-dimensional binning in the $(e_1,v_1)$ subspace of the phase space. In this case the folder is called `e1_v1`, the distribution function is saved under `e1_v1/f_binned.npy`, the $e_1$-grid is under `e1_v1/grid_e1.npy` and the $v_1$-grid is under `e1_v1/grid_v1.npy`. \n", - "\n", - "3. The kind of binnings are defined in the parameter file under `kinetic//save_data/f/slices`. There you can define a list where each string entry defines one binning. For example, `['v1', 'e1_v1', 'e1_e2', 'v1_v2_v3']` would define four binnings in different subspaces of the phase space.\n", - "\n", - "Let us now plot the binned distribution function. In this example, two binnings have been performed during the simulation, namely `v1/` and `e1_v1/`. We shall perform the 1d plots first:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "grid_v1 = np.load(os.path.join(f_path, \"v1/\", \"grid_v1.npy\"))\n", - "f_binned = np.load(os.path.join(f_path, \"v1/\", \"f_binned.npy\"))\n", - "\n", - "print(grid_v1.shape)\n", - "print(f_binned.shape)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As we can see, the first index in the binning data `f_binned.npy` denotes the time index, whereas the second index is the grid index. Let us plot the data at four different instances in time:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from matplotlib import pyplot as plt\n", - "\n", - "plt.figure(figsize=(12, 12))\n", - "\n", - "steps = [0, 1, 2, -1]\n", - "for n, step in enumerate(steps):\n", - " plt.subplot(2, 2, n + 1)\n", - " plt.plot(grid_v1, f_binned[step], label=f\"time = {t_grid[step]}\")\n", - " plt.xlabel(\"v1\")\n", - " plt.ylabel(\"fvol(v1)\")\n", - " plt.legend()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The 2d plots are working in an analogous fashion. Here, we use pyplot's `pcolor` to display 2d data:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "grid_e1 = np.load(os.path.join(f_path, \"e1_v1/\", \"grid_e1.npy\"))\n", - "grid_v1 = np.load(os.path.join(f_path, \"e1_v1/\", \"grid_v1.npy\"))\n", - "f_binned = np.load(os.path.join(f_path, \"e1_v1/\", \"f_binned.npy\"))\n", - "\n", - "print(grid_e1.shape)\n", - "print(grid_v1.shape)\n", - "print(f_binned.shape)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Again, the first index in `f_binned.npy` is the time index." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "plt.figure(figsize=(12, 12))\n", - "\n", - "steps = [0, 1, 2, -1]\n", - "for n, step in enumerate(steps):\n", - " plt.subplot(2, 2, n + 1)\n", - " plt.pcolor(grid_e1, grid_v1, f_binned[step].T, label=f\"time = {t_grid[step]}\")\n", - " plt.xlabel(\"e1\")\n", - " plt.ylabel(\"v1\")\n", - " plt.title(\"fvol(e1, v1)\")\n", - " plt.legend()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Particle orbits\n", - "\n", - "Let's see how the marker `orbits/` are stored in Struphy:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "orbits_path = os.path.join(ep_path, \"orbits\")\n", - "\n", - "print(len(os.listdir(orbits_path)))\n", - "for el in sorted(os.listdir(orbits_path)):\n", - " print(el)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For each time step *n* (including *n=0*) there is one `.txt` and one `.npy` file of the name `_n`. These files holds the positions **in physical space** of the designated markers, of which we print the first six:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "markers = np.load(os.path.join(orbits_path, \"energetic_ions_00.npy\"))\n", - "\n", - "with open(os.path.join(orbits_path, \"energetic_ions_00.txt\")) as file:\n", - " orbit_str = file.read()\n", - "\n", - "markers_txt = orbit_str.split(\"\\n\")\n", - "markers_txt[:6]" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The first column holds the marker ID number. Actually, the `.npy` files hold also the marker velocities, not just the positions:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "markers[:6]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us plot the markers at the initial time $t=0$ in the $(x, y)$-plane:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "plt.scatter(markers[:, 1], markers[:, 2])\n", - "plt.xlabel(\"x\")\n", - "plt.ylabel(\"y\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## FEEC data\n", - "\n", - "Last but not least we look at some `fields_data/`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "fluid_path = os.path.join(data_path, \"fields_data\")\n", - "\n", - "print(os.listdir(fluid_path))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "`fields_data/` contains binary files for physical and logical grids as well as the following folders:\n", - "\n", - "1. `em_fields/` for the electromagnetic fields evaluated at the grids.\n", - "2. One fluid species `mhd/` for the mhd variables evaluated at the grids. Note that the magnetic field is stored under `em_fields/`.\n", - "3. `vtk/` for viewing FEEC variables in Paraview.\n", - "\n", - "Let us inspect the content of `em_fields/` and `mhd/`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "os.listdir(os.path.join(fluid_path, \"em_fields\"))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "os.listdir(os.path.join(fluid_path, \"mhd\"))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Clearly, this is where the FEEC variables evaluated at the grid points are stored. Since this data is stored in binary format, we use `pickle` for loading: " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import pickle\n", - "\n", - "with open(os.path.join(fluid_path, \"grids_phy.bin\"), \"rb\") as file:\n", - " x_grid, y_grid, z_grid = pickle.load(file)\n", - "\n", - "print(type(x_grid))\n", - "print(x_grid.shape)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We see that `grids_phy.bin` leads to a list of length three, each entry corresponding to a meshgrid of the respective direction." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "with open(os.path.join(fluid_path, \"em_fields\", \"b_field_phy.bin\"), \"rb\") as file:\n", - " b2 = pickle.load(file)\n", - "\n", - "print(type(b2))\n", - "print(len(b2))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "By contrast, `b2_phy.bin` leads to a dictionary with *n+1* keys, where *n* is the number of time steps saved during the simulation. \n", - "\n", - "The keys are the actual time (not the index!) of the time step and the values are lists holding the three components of the magnetic field in physical space (pushed forward 2-form):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "for key, val in b2.items():\n", - " print(key)\n", - " print(type(val))\n", - " for va in val:\n", - " print(va.shape)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We shall plot the *z*-component of the *B*-field as a function of *x*, at eight dfferent instances in time:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "plt.figure(figsize=(12, 12))\n", - "\n", - "steps = [0, 1, 2, 3, 4, 5, 6, -1]\n", - "for n, step in enumerate(steps):\n", - " t = t_grid[step]\n", - " plt.subplot(4, 2, n + 1)\n", - " plt.plot(x_grid[:, 0, 0], b2[t][2][:, 0, 0], label=f\"time = {t}\")\n", - " plt.xlabel(\"x\")\n", - " plt.ylabel(\"$B_z$(x)\")\n", - " plt.legend()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} From e88812cbc06205f86e1cd23fee66af23d295faff Mon Sep 17 00:00:00 2001 From: Stefan Possanner <86720346+spossann@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:50:38 +0100 Subject: [PATCH 22/83] 113 update release workflow (#115) **Solves the following issue(s):** Closes #113 I added a trusted publisher on the struphy project on PyPI. I tested the workflow ` pypi-release.yml` on test.pypi: https://test.pypi.org/project/struphy-test-4/2.6.2/#description --- .github/workflows/gh-release.yml | 42 ++++++++++++++++++++++++++++++ .github/workflows/publish.yml | 33 ----------------------- .github/workflows/pypi-release.yml | 41 +++++++++++++++++++++++++++++ CHANGELOG.md | 3 +++ 4 files changed, 86 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/gh-release.yml delete mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/pypi-release.yml diff --git a/.github/workflows/gh-release.yml b/.github/workflows/gh-release.yml new file mode 100644 index 000000000..27c946633 --- /dev/null +++ b/.github/workflows/gh-release.yml @@ -0,0 +1,42 @@ +name: Release Struphy on Github + +on: + push: + branches: + - main + +jobs: + release: + runs-on: ubuntu-latest + + permissions: + packages: write + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Check pypi versions + uses: maybe-hello-world/pyproject-check-version@v4 + id: versioncheck + with: + pyproject-path: "./pyproject.toml" # default value + + - name: Check output + shell: bash + run: | + echo "Output: ${{ steps.versioncheck.outputs.local_version_is_higher }}" # 'true' or 'false + echo "Local version: ${{ steps.versioncheck.outputs.local_version }}" # e.g., 0.1.1 + echo "Public version: ${{ steps.versioncheck.outputs.public_version }}" # e.g., 0.1.0 + + - name: Release + uses: softprops/action-gh-release@v2 + id: create_release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ steps.versioncheck.outputs.local_version }} + body_path: ${{ github.workspace }}/CHANGELOG.md + + \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 7b7b8166e..000000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Publish Struphy to PyPI - -on: - push: - branches: - - main - -jobs: - build-and-publish: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - - name: Install build tools - run: | - python -m pip install --upgrade pip - pip install build twine - - - name: Build the package - run: python -m build - - - name: Publish to PyPI - env: - TWINE_USERNAME: __token__ # Use API token - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: twine upload dist/* diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml new file mode 100644 index 000000000..6c1261711 --- /dev/null +++ b/.github/workflows/pypi-release.yml @@ -0,0 +1,41 @@ +name: Publish Struphy to PyPI + +on: + push: + branches: + - main + +# .github/workflows/ci-cd.yml +jobs: + pypi-publish: + name: Upload release to PyPI + + runs-on: ubuntu-latest + + environment: + name: pypi + + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install build tools + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build the package + run: python -m build + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + # with: + # repository-url: https://test.pypi.org/legacy/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 28e7978e5..31841dafe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Struphy 2.5.0 +* [Struphy on PyPI](https://pypi.org/project/struphy/) +* [Struphy pages](https://struphy-hub.github.io/struphy/index.html) + ### Headlines * Base install and run without `mpi4py` !775 From b6fe9ec2502d3c972f1e067b303f918f0d9d0f0d Mon Sep 17 00:00:00 2001 From: Stefan Possanner <86720346+spossann@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:26:35 +0100 Subject: [PATCH 23/83] Prepare v 2 6 0 (#110) This is a test deployment before Struphy 3.0 - to make sure our workflow is correct. I also changed some of the links in the doc from Gitlab to Github. --- .github/workflows/pypi-release.yml | 1 + .gitlab-ci.yml | 2 +- CHANGELOG.md | 32 +--- CONTRIBUTING.md | 35 ++-- README.md | 4 +- doc/markdown/vlasov-maxwell.md | 25 ++- doc/sections/feec_classes.rst | 32 ---- doc/sections/install.rst | 6 - doc/sections/numerics.rst | 2 + doc/sections/pic_classes.rst | 31 --- doc/sections/subsections-old/adding_model.rst | 178 +++--------------- .../subsections-old/boundary_conditions.rst | 2 +- doc/sections/subsections-old/write_prop.rst | 12 +- .../subsections/numerics-geomFE-classes.rst | 17 ++ doc/sections/subsections/numerics-geomFE.rst | 2 +- .../subsections/numerics-pic-classes.rst | 21 +++ doc/sections/userguide.rst | 2 + pyproject.toml | 15 +- src/struphy/console/main.py | 2 +- src/struphy/feec/psydac_derham.py | 2 - .../coil_fields/coil_fields.py | 1 - src/struphy/main.py | 2 +- src/struphy/pic/base.py | 4 +- src/struphy/pic/pushing/pusher_kernels_gc.py | 10 +- ...whl => psydac-2.6.0.dev0-py3-none-any.whl} | Bin 242485 -> 278548 bytes 25 files changed, 114 insertions(+), 326 deletions(-) delete mode 100644 doc/sections/feec_classes.rst delete mode 100644 doc/sections/pic_classes.rst create mode 100644 doc/sections/subsections/numerics-geomFE-classes.rst create mode 100644 doc/sections/subsections/numerics-pic-classes.rst rename src/struphy/{psydac-2.5.0.dev0-py3-none-any.whl => psydac-2.6.0.dev0-py3-none-any.whl} (64%) diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml index 6c1261711..2afbaa837 100644 --- a/.github/workflows/pypi-release.yml +++ b/.github/workflows/pypi-release.yml @@ -14,6 +14,7 @@ jobs: environment: name: pypi + url: https://pypi.org/project/struphy/ permissions: id-token: write # IMPORTANT: this permission is mandatory for trusted publishing diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1b5784ba4..c166bfc10 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1160,6 +1160,6 @@ release_job: assets: links: - name: 'Documentation' - url: 'https://struphy.pages.mpcdf.de/struphy/index.html' + url: 'https://struphy-hub.github.io/struphy/index.html' - name: 'PyPI' url: 'https://pypi.org/project/struphy/' diff --git a/CHANGELOG.md b/CHANGELOG.md index 31841dafe..0f98e1163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,36 +1,8 @@ -## Struphy 2.5.0 +## Struphy 2.6.0 * [Struphy on PyPI](https://pypi.org/project/struphy/) * [Struphy pages](https://struphy-hub.github.io/struphy/index.html) ### Headlines -* Base install and run without `mpi4py` !775 -* Addition of a solver for saddle point problems based on the Uzawa algorithm !624 -* New convergence tests for SPH density evaluation: all unit tests are done for the available SPH boundary conditions `periodic`, `mirror` (-> Neumann) and `fixed` (-> Dirichlet) !724 -* Integration of [RatGUI](https://rat-gui.com/) for magnetic coil fields !576 - -### Other user news - -* Verification of model `Maxwell` with analytic solution of coaxial cable !733 -* Improved auto-sampling of markers (-> importance sampling) !735 -* Add `struphy params MODEL --check-file FILE` and the model name in the params file !707 -* Store `Domain` and `Equilibrium` input paramaters unchanged !745 - -### Developer news - -* Refactor console module !711 -* Use `MarkerArguments` in `accum_kernels` !635 -* Format all source files !737 -* Code profiling: Improvements of the time traces !713 -* Expose documentation and lint reports as artifacts in each MR !747 -* Added `ssort` to the linters !762 -* Added a `Pyccelkernel` class with a __call__ method !759 -* Added an `xp` module: this helps with importing `numpy`/`cupy` depending on the environment variable `ARRAY_BACKEND` !768 - -### Bug fixes - -* Allow float evaluation in GVEC equilibrium !729 -* Remove cyclic dependencies between folders !736 -* Post-processing of multiple output folders fixed !744 -* Remove deepcopy from `DESCunit` !772 +* This is a test run for the relaease of Struphy 3.0 from the new Github repo diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 59cd65ab2..57bda9baf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,37 +1,26 @@ -# Contact - -* [Mailing list](https://listserv.gwdg.de/mailman/listinfo/struphy) -* [MatrixChat developer's channel](https://matrix.to/#/!wqjcJpsUvAbTPOUXen:mpg.de?via=mpg.de&via=academiccloud.de) -* [Issue tracker](https://gitlab.mpcdf.mpg.de/struphy/struphy/-/issues) (MPCDF account needed, contacts below) -* [LinkedIn](https://www.linkedin.com/company/struphy/) -* [stefan.possanner@ipp.mpg.de](mailto:spossann@ipp.mpg.de) -* [eric.sonnendruecker@ipp.mpg.de](mailto:eric.sonnendruecker@ipp.mpg.de) -* [xin.wang@ipp.mpg.de](mailto:xin.wang@ipp.mpg.de) - - # Repository -Struphy has two main branches, **master** and **devel**. +Struphy has two protected branches, **main** and **devel**. Nobody can push directly to these branches. -The **master** branch holds the current release of the code. +The **main** branch holds the current release of the code. -**devel** is the main branch for developers. Feature branches must be checked out and merged into **devel**. +**devel** is the branch for developers. Feature branches must be checked out and merged into **devel**. # Forking -In case you are not a [member](https://gitlab.mpcdf.mpg.de/struphy/struphy/-/project_members>) of the Struphy project, -you can contribute code by [forking](https://docs.gitlab.com/ee/user/project/repository/forking_workflow.html>)the Struphy -repository. - -You must create a **public fork** to be able to merge your code into Struphy! +Please create a **public fork** to be able to merge your code into Struphy! You can create feature branches in your forked repo and create merge requests into the original Struphy repo. -[Update your fork](https://docs.gitlab.com/ee/user/project/repository/forking_workflow.html#from-the-command-line) -in case **devel** changes in the Struphy repo while you are working on your feature. -# More info +# Contact -See the [Developer's guide](https://struphy.pages.mpcdf.de/struphy/sections/developers.html). \ No newline at end of file +* [Mailing list](https://listserv.gwdg.de/mailman/listinfo/struphy) +* [MatrixChat developer's channel](https://matrix.to/#/!wqjcJpsUvAbTPOUXen:mpg.de?via=mpg.de&via=academiccloud.de) +* [Issue tracker](https://github.com/struphy-hub/struphy/issues) +* [LinkedIn](https://www.linkedin.com/company/struphy/) +* [stefan.possanner@ipp.mpg.de](mailto:spossann@ipp.mpg.de) +* [max.lindqvist@ipp.mpg.de@ipp.mpg.de](mailto:max.lindqvist@ipp.mpg.de) +* [xin.wang@ipp.mpg.de](mailto:xin.wang@ipp.mpg.de) diff --git a/README.md b/README.md index 31d55e6f3..cc5e3937e 100755 --- a/README.md +++ b/README.md @@ -104,8 +104,8 @@ The doc is on [Github pages](https://struphy-hub.github.io/struphy/index.html), * [Issues](https://github.com/struphy-hub/struphy/issues) * [Discussions](https://github.com/struphy-hub/struphy/discussions) -* @spossann [stefan.possanner@ipp.mpg.de](mailto:spossann@ipp.mpg.de) (Maintainer) -* @max-models [Max.Lindqvist@ipp.mpg.de](mailto:Max.Lindqvist@ipp.mpg.de) (Maintainer) +* [@spossann](https://github.com/spossann) [stefan.possanner@ipp.mpg.de](mailto:spossann@ipp.mpg.de) (Maintainer) +* [@max-models](https://github.com/max-models) [max.lindqvist@ipp.mpg.de](mailto:Max.Lindqvist@ipp.mpg.de) (Maintainer) * [LinkedIn profile](https://www.linkedin.com/company/struphy/) diff --git a/doc/markdown/vlasov-maxwell.md b/doc/markdown/vlasov-maxwell.md index d7b0bcad5..b17e855b0 100644 --- a/doc/markdown/vlasov-maxwell.md +++ b/doc/markdown/vlasov-maxwell.md @@ -161,7 +161,7 @@ $$ (eq:spaces) (pullback)= ### Pull-back to the logical domain -All PDE models in Struphy are discretized on the unit cube $(0,1)^3$, called the "logical domain". The mapping to the actual problem domain, a torus for instance, called "physical" or Cartesian domain and denoted by $\Omega$, is described in the [Domain base class](https://struphy.pages.mpcdf.de/struphy/sections/subsections/domains.html#struphy.geometry.base.Domain). Briefly, logical coordinates are curvi-linear and denoted by $\boldsymbol \eta \in (0, 1)^3$, whereas physical Cartesian coordinates are denoted by $\mathbf x \in \Omega$. The mapping $F: (0, 1)^3 \to \Omega, \boldsymbol \eta \mapsto \mathbf x$ is one-to-one and differentiable, with Jacobian matrix $DF: (0, 1)^3 \to \mathbb R^{3\times 3}$, metric tensor $G = DF^\top DF$ and determinant $\sqrt g = |\textrm{det} DF|$. In Struphy, only right-handed mappings with $\textrm{det} DF > 0$ are allowed. +All PDE models in Struphy are discretized on the unit cube $(0,1)^3$, called the "logical domain". The mapping to the actual problem domain, a torus for instance, called "physical" or Cartesian domain and denoted by $\Omega$, is described under [Geometry](https://struphy-hub.github.io/struphy/sections/domains.html). Briefly, logical coordinates are curvi-linear and denoted by $\boldsymbol \eta \in (0, 1)^3$, whereas physical Cartesian coordinates are denoted by $\mathbf x \in \Omega$. The mapping $F: (0, 1)^3 \to \Omega, \boldsymbol \eta \mapsto \mathbf x$ is one-to-one and differentiable, with Jacobian matrix $DF: (0, 1)^3 \to \mathbb R^{3\times 3}$, metric tensor $G = DF^\top DF$ and determinant $\sqrt g = |\textrm{det} DF|$. In Struphy, only right-handed mappings with $\textrm{det} DF > 0$ are allowed. Usually, any text book model equations are described in Cartesian (physical) coordinates $\mathbf x$, and this is also true for our model {eq}`eq:spaces`. Hence, as a next step we have to transform our model to logical, curvi-linear coordinates $\boldsymbol \eta$. This process is also called the "pull-back" to the logical domain. There are different possible "representations" of a pulled-back variable on the logical domain: @@ -189,7 +189,7 @@ The connection of differential $p$-forms to the {ref}`Struphy de Rham spaces `` restarts the container in detached mode. * ``docker attach `` opens a terminal to a detached container. -* Mirror default Struphy output to ``~/
`` on the host machine:: - - docker run -it -v ~/:/io/out gitlab-registry.mpcdf.mpg.de/struphy/struphy/release - .. _docker_devs: Docker for devs @@ -358,8 +354,6 @@ MPCDF computing clusters ------------------------ Struphy is periodically tested on the `MPCDF HPC facilities `_. -Tests are performed with the `available MPCDF images `_. -The modules loaded in these tests can be found in Struphy's `.gitlab-ci.yml `_. A common installation looks like this diff --git a/doc/sections/numerics.rst b/doc/sections/numerics.rst index 4fa641834..b017354d4 100644 --- a/doc/sections/numerics.rst +++ b/doc/sections/numerics.rst @@ -28,6 +28,8 @@ we detail the discretization of the Vlasov-Maxwell system implemented in Struphy subsections/numerics-sph subsections/numerics-time-discrete ../markdown/vlasov-maxwell + subsections/numerics-pic-classes + subsections/numerics-geomFE-classes diff --git a/doc/sections/pic_classes.rst b/doc/sections/pic_classes.rst deleted file mode 100644 index a1699ff7f..000000000 --- a/doc/sections/pic_classes.rst +++ /dev/null @@ -1,31 +0,0 @@ -.. _Tutorial 1 - Kinetic particles: ../tutorials/tutorial_01_kinetic_particles.ipynb -.. _Tutorial 2 - Fluid particles: ../tutorials/tutorial_02_fluid_particles.ipynb -.. _Tutorial 9 - Vlasov-Maxwell: ../tutorials/tutorial_09_vlasov_maxwell.ipynb - -.. _pic_modules: - -Particle modules -================ - -Check out the following tutorials for how to use the particle modules below: - -* `Tutorial 1 - Kinetic particles`_ -* `Tutorial 2 - Fluid particles`_ -* `Tutorial 9 - Vlasov-Maxwell`_ - -.. toctree:: - :maxdepth: 1 - :caption: Contents: - - subsections/pic_base - subsections/pic_pushers - subsections/pic_accumulation - subsections/pic_sorting_sph - subsections/pic_utilities - - - - - - - diff --git a/doc/sections/subsections-old/adding_model.rst b/doc/sections/subsections-old/adding_model.rst index b99f54b0b..f2e516b96 100644 --- a/doc/sections/subsections-old/adding_model.rst +++ b/doc/sections/subsections-old/adding_model.rst @@ -9,10 +9,10 @@ A model consists of a set of PDEs that has been discretized within the New Struphy models must be added in one of the four modules: -* `models/toy.py `_ -* `models/fluid.py `_ -* `models/kinetic.py `_ -* `models/hybrid.py `_ +* `models/toy.py `_ +* `models/fluid.py `_ +* `models/kinetic.py `_ +* `models/hybrid.py `_ as child classes of the :class:`StruphyModel `. **Please refer to existing models for templates.** Here is a list of points that need to be followed when creating a new model: @@ -26,7 +26,7 @@ Perform the following steps: a. In one of the four files above, copy-and-paste an existing model. b. Change the class name to ````. c. Run ``struphy --refresh-models`` in the console. -d. In the console, run ``struphy run `` which will just execute the copied model after creating a default parameter file. +d. Type ``struphy params `` and run the new model. 2. Derive Struphy discretization of your PDE @@ -42,79 +42,16 @@ and/or given references for a tutorial on how to apply this discretization metho .. _species: -3. Define :code:`species(cls)` +3. Define :code:`Species` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The :code:`species(cls)` method must be implemented in every Struphy model. -It returns a dictionary that holds the information on the models' variables (i.e. the unknowns) -and their respective discrete spaces (PIC or FEEC) in which they are defined. -Let us look at the model `LinearMHDVlasovCC `_ as an example:: +See :ref:`species`. - @classmethod - def species(cls): - dct = {'em_fields': {}, 'fluid': {}, 'kinetic': {}} - - dct['em_fields']['b2'] = 'Hdiv' - dct['fluid']['mhd'] = {'n3': 'L2', 'u2': 'Hdiv', 'p3': 'L2'} - dct['kinetic']['energetic_ions'] = 'Particles6D' - return dct -In Struphy, three types of species can be defined: - -* electromagnetic (EM) fields (under the dict key :code:`em_fields`) -* fluid species (dict key :code:`fluid`) -* kinetic species (dict key :code:`kinetic`) - -Each species can be assigned an arbitrary name, chosen by the developer, -which must appear as a sub-key in one of the above dicts. The corresponding value is either - -* for EM fields: the name of a FEEC space (``H1``, ``Hcurl``, ``Hdiv``, ``L2`` or ``H1vec``) -* for fluid species: a dictionary holding the fluid variable names (keys) and FEEC spaces (values) -* for kinetic species: the name of a :ref:`particle class ` - -In the example above, one field variable (``b2``), one fluid species (``mhd``) and one kinetic species (``energetic_ions``) -are initialized. The corresponding discrete spaces appear as values. -There is no limit in how many species/fields can be defined within a model. - -Later, the variables defined in :code:`species(cls)` can be accessed -via the :attr:`pointer attribute ` -of the :class:`StruphyModel ` base class. -The variable names are to be used as keys, for example:: - - _b2 = self.pointer['b2'] - _n3 = self.pointer['mhd_n3'] - -This returns the :ref:`data_structures` of the variable (the whole :ref:`particle class ` for kinetic species). -In case of a fluid species, the naming convention is :code:`species_variable` -with an underscore separating species name and variable name. - - -4. Define ``bulk_species(cls)`` and ``velocity_scale(cls)`` +4. Define ``bulk_species`` and ``velocity_scale`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -These must be implemented in every Struphy model in order to :func:`struphy.io.setup.derive_units`:: - - @classmethod - def bulk_species(cls): - return 'energetic_ions' - - @classmethod - def velocity_scale(cls): - return 'light' - -The ``bulk_species`` must return the name of one of the species of the model. - -There are four options for the ``velocity_scale``: - -* ``alfvén`` -* ``cyclotron`` -* ``light`` -* ``thermal`` - -The choice corresponds to setting the velocity unit :math:`\hat v` of the :ref:`normalization`. -This then sets the time unit :math:`\hat t = \hat x / \hat v`, where :math:`\hat x` is the -unit of length specified through the parameter file. - +These must be implemented in every Struphy model in order to :func:`struphy.io.setup.derive_units`, see also :ref:`normalization`. .. _add_prop: @@ -131,43 +68,16 @@ When adding a new model to Struphy, make sure to Propagators are in one of the following modules: -* `propagators/propagators_fields.py `_ -* `propagators/propagators_markers.py `_ -* `propagators/propagators_coupling.py `_ +* `propagators/propagators_fields.py `_ +* `propagators/propagators_markers.py `_ +* `propagators/propagators_coupling.py `_ **Check out** :ref:`write_prop` **for practical details on the implementation.** -A model's propagators are defined in :meth:`struphy.models.base.StruphyModel.propagators_dct`. -See `LinearMHD `_ for an example:: - - @staticmethod - def propagators_dct(): - return {propagators_fields.ShearAlfven: ['mhd_velocity', 'b_field'], - propagators_fields.Magnetosonic: ['mhd_density', 'mhd_velocity', 'mhd_pressure']} - -The keys are the :ref:`propagator classes ` themselves; the values are the names of model variales to be updated by the propagator, -as defined in :meth:`struphy.models.base.StruphyModel.species`, see above. -The updated variables must conform to the solution spaces defined in the ``__init__`` of the propagator (arguments BEFORE ``*``). - -The order in which propagators are added in :meth:`~struphy.models.base.StruphyModel.propagators_dct` matters. +A model's propagators are defined in :class:`struphy.models.base.StruphyModel.Propagators`. +The order in which propagators are added in :class:`~struphy.models.base.StruphyModel.Propagators` matters. They are called consecutively according to the time splitting scheme defined in :ref:`time`. -Propagator parameters (passed as keyword arguments) must be defined in the ``__init__`` of the model class -by setting the ``self._kwargs`` dictionary of the model, -see `LinearMHD `_ for an example:: - - # set keyword arguments for propagators - self._kwargs[propagators_fields.ShearAlfven] = {'u_space': u_space, - 'solver': alfven_solver} - - self._kwargs[propagators_fields.Magnetosonic] = {'b': self.pointer['b_field'], - 'u_space': u_space, - 'solver': sonic_solver} - -The given keyword arguments must conform to the ones defined in the ``__init__`` of the propagator -(arguments AFTER ``*``). - - 6. Add scalar quantities ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -181,20 +91,11 @@ e.g. for checking concervation properties. This can be done via the methods Check out existing models for templates. -7. Add options -^^^^^^^^^^^^^^ +7. Overrride `generate_default_parameter_file` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Most of a model's options are defined within :meth:`struphy.propagators.base.Propagator.options`, -i.e within the options of the models's propagators. -It is possible to add additional options through :meth:`struphy.models.base.StruphyModel.options`. -This is done with the method :meth:`struphy.models.base.StruphyModel.add_option`:: - - @classmethod - def options(cls): - dct = super().options() - cls.add_option(species=['fluid', 'mhd'], key='u_space', - option='Hdiv', dct=dct) - return dct +If necessary due to the propagators of the model, override :meth:`struphy.models.base.StruphyModel.generate_default_parameter_file` +to generate the parameter file you intended. 8. Test @@ -203,46 +104,9 @@ This is done with the method :meth:`struphy.models.base.StruphyModel.add_option` Once you added a model and re-installed struphy (``pip install -e .``), you can run the model with:: - struphy run + struphy params -y + python params_.py If the model is not found:: - struphy --refresh-models - -and run again. The parameter file of a model is created via:: - - struphy params - - -9. Add a model docstring -^^^^^^^^^^^^^^^^^^^^^^^^ - -The docstring should have the following form (example taken from `LinearMHD `_):: - - Linear ideal MHD with zero-flow equilibrium (:math:`\mathbf U_0 = 0`). - - :ref:`normalization`: - - .. math:: - - - - :ref:`Equations `: - - .. math:: - - - - :ref:`propagators` (called in sequence): - - 1. :class:`~struphy.propagators.propagators_fields.ShearAlfven` - 2. :class:`~struphy.propagators.propagators_fields.Magnetosonic` - - :ref:`Model info `: - -The equations should be written in strong form (like in a textbook), in the chosen :ref:`normalization`. -Do not include discretized equations in the model docstring. -You can follow :ref:`change_doc` to see if your changes have been taken into -account. - - \ No newline at end of file + struphy --refresh-models \ No newline at end of file diff --git a/doc/sections/subsections-old/boundary_conditions.rst b/doc/sections/subsections-old/boundary_conditions.rst index 99618880c..e26d3c6cc 100644 --- a/doc/sections/subsections-old/boundary_conditions.rst +++ b/doc/sections/subsections-old/boundary_conditions.rst @@ -123,6 +123,6 @@ one has the following possibilities to set boundary conditions: * ``spl_kind = False`` and ``dirichlet_bc = True``: homogeneous Dirichlet bcs. * ``spl_kind = False`` and ``dirichlet_bc = False``: free bcs, possible "natural" Neumann boundary conditions through the equation. -Check out `the Poisson unit test `_ for an example. +Check out `the Poisson unit test `_ for an example. More FEEC boundary conditions will be added in the future. \ No newline at end of file diff --git a/doc/sections/subsections-old/write_prop.rst b/doc/sections/subsections-old/write_prop.rst index 5bdf0fa50..d448333a9 100644 --- a/doc/sections/subsections-old/write_prop.rst +++ b/doc/sections/subsections-old/write_prop.rst @@ -189,13 +189,7 @@ Particle kernels ================ A "kernel" is where the particle loops are written in Struphy. -The following **kernel files** are available: - -* `pic/pushing/pusher_kernels.py `_ for general particle pushing -* `pic/pushing/pusher_kernels_gc.py `_ for guiding-center pushing -* `pic/pushing/eval_kernels_gc.py `_ for particle evaluation of specific functions -* `pic/accumulation/accum_kernels.py `_ for general particle deposition -* `pic/accumulation/accum_kernels_gc.py `_ for particle deposition in guiding-center models +The available **kernel files** can be seen under :ref:`pic_modules`. These kernel files are compiled when the ``struphy compile`` command is executed from the console. @@ -210,12 +204,12 @@ through the following module, imported at the top of the kernel files:: import struphy.geometry.evaluation_kernels as evaluation_kernels -This `provides callables to all things mapping `_. +This `provides callables to all things mapping `_. Linear algebra operations are available through the module:: import struphy.linear_algebra.linalg_kernels as linalg_kernels -which provides `products, transpose, inverse, etc. `_ +which provides `products, transpose, inverse, etc. `_ The evaluation of FEEC spline fields is managed through the following functions, which are imported at the top of the kernel files as well:: diff --git a/doc/sections/subsections/numerics-geomFE-classes.rst b/doc/sections/subsections/numerics-geomFE-classes.rst new file mode 100644 index 000000000..986c5f473 --- /dev/null +++ b/doc/sections/subsections/numerics-geomFE-classes.rst @@ -0,0 +1,17 @@ +.. _feec_base: + +FEEC modules +------------ + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + ../subsections-old/feec_derham + ../subsections-old/feec_projectors + ../subsections-old/feec_weightedmass + ../subsections-old/feec_basisops + ../subsections-old/feec_projected_mhd + ../subsections-old/feec_linalg + + diff --git a/doc/sections/subsections/numerics-geomFE.rst b/doc/sections/subsections/numerics-geomFE.rst index b78a1968d..aa8a0793f 100644 --- a/doc/sections/subsections/numerics-geomFE.rst +++ b/doc/sections/subsections/numerics-geomFE.rst @@ -166,7 +166,7 @@ which satisfy the complex property .. note:: A struphy userguide for the operators :math:`\mathbb G`, :math:`\mathbb C` and :math:`\mathbb D` and for the projection - operators :math:`\Pi_n` is given in `this Jupyter notebook `_. + operators :math:`\Pi_n` is given in the `Tutorials `_. The projectors :math:`\Pi_n,\, 0\leq n \leq 3` into the discrete spaces :math:`V_h^n,\, 0\leq n \leq 3` are constructed such that the commuting relations :math:numref:`commute` hold. They are defined by **Degrees of freedom (DOFs)** diff --git a/doc/sections/subsections/numerics-pic-classes.rst b/doc/sections/subsections/numerics-pic-classes.rst new file mode 100644 index 000000000..82d2c6fa5 --- /dev/null +++ b/doc/sections/subsections/numerics-pic-classes.rst @@ -0,0 +1,21 @@ +.. _pic_modules: + +Particle modules +================ + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + ../subsections-old/pic_base + ../subsections-old/pic_pushers + ../subsections-old/pic_accumulation + ../subsections-old/pic_sorting_sph + ../subsections-old/pic_utilities + + + + + + + diff --git a/doc/sections/userguide.rst b/doc/sections/userguide.rst index 709d1b5cc..e38d65081 100644 --- a/doc/sections/userguide.rst +++ b/doc/sections/userguide.rst @@ -55,6 +55,8 @@ Models See :ref:`models`. +.. _species: + Species types ^^^^^^^^^^^^^ diff --git a/pyproject.toml b/pyproject.toml index 1278abb77..be80246d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,12 +6,13 @@ build-backend = "setuptools.build_meta" [project] name = "struphy" -version = "2.5.0" +version = "2.6.0" readme = "README.md" requires-python = ">=3.10" authors = [ { name = "Max Planck Institute for Plasma Physics" }, { email = "stefan.possanner@ipp.mpg.de" }, + { email = "max.lindqvist@ipp.mpg.de" }, { email = "eric.sonnendruecker@ipp.mpg.de" }, ] description = "Multi-model plasma physics package" @@ -98,11 +99,11 @@ all = [ ] [project.urls] -homepage = "https://struphy.pages.mpcdf.de/struphy/" -documentation = "https://struphy.pages.mpcdf.de/struphy/" -repository = "https://gitlab.mpcdf.mpg.de/struphy/struphy" -changelog = "https://gitlab.mpcdf.mpg.de/struphy/struphy/-/blob/devel/CHANGELOG.md" -"Bug Tracker" = "https://gitlab.mpcdf.mpg.de/struphy/struphy/-/issues" +homepage = "https://struphy-hub.github.io/struphy/index.html" +documentation = "https://struphy-hub.github.io/struphy/index.html" +repository = "https://github.com/struphy-hub/struphy" +changelog = "https://github.com/struphy-hub/struphy/blob/devel/CHANGELOG.md" +"Bug Tracker" = "https://github.com/struphy-hub/struphy/issues" [project.scripts] struphy = "struphy.console.main:struphy" @@ -133,7 +134,7 @@ kinetic-diagnostics = "struphy.diagnostics.console_diagn:main" ] struphy = [ "compile_struphy.mk", - "psydac-2.5.0.dev0-py3-none-any.whl", + "psydac-2.6.0.dev0-py3-none-any.whl", ] [tool.autopep8] diff --git a/src/struphy/console/main.py b/src/struphy/console/main.py index c612a5cb4..7081ea7d5 100644 --- a/src/struphy/console/main.py +++ b/src/struphy/console/main.py @@ -118,7 +118,7 @@ def struphy(): for flag, message in model_flags: if flag: print(message) - print("For more info on Struphy models, visit https://struphy.pages.mpcdf.de/struphy/sections/models.html") + print("For more info on Struphy models, visit https://struphy-hub.github.io/struphy/sections/models.html") sys.exit(0) if args.refresh_models: diff --git a/src/struphy/feec/psydac_derham.py b/src/struphy/feec/psydac_derham.py index 972be4509..e0e261340 100644 --- a/src/struphy/feec/psydac_derham.py +++ b/src/struphy/feec/psydac_derham.py @@ -46,8 +46,6 @@ class Derham: """ The discrete Derham sequence on the logical unit cube (3d). - Check out the corresponding `Struphy API `_ for a hands-on introduction. - The tensor-product discrete deRham complex is loaded using the `Psydac API `_ and then augmented with polar sub-spaces (indicated by a bar) and boundary operators. diff --git a/src/struphy/fields_background/coil_fields/coil_fields.py b/src/struphy/fields_background/coil_fields/coil_fields.py index 1b5c66a15..2de1c437f 100644 --- a/src/struphy/fields_background/coil_fields/coil_fields.py +++ b/src/struphy/fields_background/coil_fields/coil_fields.py @@ -46,7 +46,6 @@ def __init__(self, csv_path=None, Nel=[16, 16, 16], p=[3, 3, 3], domain=None, ** print(f"{self.rhs[2][:].shape =}") # We need to choose Nel and p such that the csv_data fits into this vector. # For a periodic direction, the size of the vector is Nel, for non-periodic (spl_kind=False) the size is Nel + p. - # See the Tutorial on FEEC data structures https://struphy.pages.mpcdf.de/struphy/tutorials/tutorial_06_data_structures.html#FEEC-data-structures on how to address such a vector # TODO: fill ratgui_csv_data into rhs vector diff --git a/src/struphy/main.py b/src/struphy/main.py index 047abea95..c7c582c6a 100644 --- a/src/struphy/main.py +++ b/src/struphy/main.py @@ -63,7 +63,7 @@ def run( Parameters ---------- model : StruphyModel - The model to run. Check https://struphy.pages.mpcdf.de/struphy/sections/models.html for available models. + The model to run. Check https://struphy-hub.github.io/struphy/sections/models.html for available models. params_path : str Absolute path to .py parameter file. diff --git a/src/struphy/pic/base.py b/src/struphy/pic/base.py index 84900418d..1dc148cdb 100644 --- a/src/struphy/pic/base.py +++ b/src/struphy/pic/base.py @@ -62,9 +62,7 @@ class Particles(metaclass=ABCMeta): r""" Base class for particle species. - The marker information is stored in a 2D numpy array, - see `Tutorial on PIC data structures `_. - + The marker information is stored in a 2D numpy array. In ``markers[ip, j]`` The row index ``ip`` refers to a specific particle, the column index ``j`` to its attributes. The columns are indexed as follows: diff --git a/src/struphy/pic/pushing/pusher_kernels_gc.py b/src/struphy/pic/pushing/pusher_kernels_gc.py index 5dfee707b..6e5df8034 100644 --- a/src/struphy/pic/pushing/pusher_kernels_gc.py +++ b/src/struphy/pic/pushing/pusher_kernels_gc.py @@ -1796,7 +1796,7 @@ def push_gc_cc_J1_H1vec( u2: "float[:,:,:]", u3: "float[:,:,:]", ): - r"""Velocity update step for the `CurrentCoupling5DCurlb `_ + r"""Velocity update step for the `CurrentCoupling5DCurlb `_ Marker update : @@ -1931,7 +1931,7 @@ def push_gc_cc_J1_Hcurl( u2: "float[:,:,:]", u3: "float[:,:,:]", ): - r"""Velocity update step for the `CurrentCoupling5DCurlb `_ + r"""Velocity update step for the `CurrentCoupling5DCurlb `_ Marker update: @@ -2078,7 +2078,7 @@ def push_gc_cc_J1_Hdiv( u2: "float[:,:,:]", u3: "float[:,:,:]", ): - r"""Velocity update step for the `CurrentCoupling5DCurlb `_ + r"""Velocity update step for the `CurrentCoupling5DCurlb `_ Marker update: @@ -2233,7 +2233,7 @@ def push_gc_cc_J2_stage_H1vec( b: "float[:]", c: "float[:]", ): - r"""Single stage of a s-stage explicit pushing step for the `CurrentCoupling5DGradB `_ + r"""Single stage of a s-stage explicit pushing step for the `CurrentCoupling5DGradB `_ Marker update: @@ -2426,7 +2426,7 @@ def push_gc_cc_J2_stage_Hdiv( b: "float[:]", c: "float[:]", ): - r"""Single stage of a s-stage explicit pushing step for the `CurrentCoupling5DGradB `_ + r"""Single stage of a s-stage explicit pushing step for the `CurrentCoupling5DGradB `_ Marker update: diff --git a/src/struphy/psydac-2.5.0.dev0-py3-none-any.whl b/src/struphy/psydac-2.6.0.dev0-py3-none-any.whl similarity index 64% rename from src/struphy/psydac-2.5.0.dev0-py3-none-any.whl rename to src/struphy/psydac-2.6.0.dev0-py3-none-any.whl index 53549de165f9fcdd969f6621dd80fa8fc7d7bc12..e6f60d6b78326dfecea67ac78b7c477d5b598394 100644 GIT binary patch delta 89831 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%0DD-rfmv3Tw4}dgL4M+sNLJjrcK5 z1o2u&9GRS9J%`(xQ6iob}V~w0+gn|Zs^0wJnZVZ2s(%yYJ zSgq8rKH6q_Z?={X45KaKeDxHA$8{`Tb*`NM5OD55x|ung=e(H<(!1nXKl}s+VvZ?4 zT3v9?ACK=>!H{%rH5Pu298oRl4xag}M0wr;|NqjhiT{=O#$gko`+mkVSCyt9TV*~H_ zF(yohaipD@P#W)s^^37J&8fC#x`=0UpYI;F#@rDU zVau=%1L!YY5BI>I>=p=0K<;<)1LL&B>h)dQiLBpjKUcZAayIp$9ov~?s>Mlhnoih^ zqPbU=THGOaDb}3Y#LHrs*w3R?J69PdM3qc&t4Pv4wj9nA2&Yq3W1<$nyeIF3S4gLq zRM0TpZrA^O8;5F*GgFpIHH-M?;to+CBEbO{75(M{o7#WDu*pQ63%qE2ORR+_aa@Vq zup;k)ulXSfuZ;;^QqYT*Jbkc3qfgsdUc0ckkxlYUK#jkSxLUy|us_ro!+ zfa#xwRWGv!xa0oxm7AWeVa(1GP5`a1lOdtH%u3-nKSi>TKqFx=z9Sx;`B~qR8J1c7 ztGTr=%*+ieZ8IZ{9FXTWq*n49NmA&eoUZ^`$ANS_;cn|`q94lBewt5>&}0sW;YNz) zxLx_x3s&7_)VZ|Dh-}WtJwRlNmc!*li(T`KdU`0niKwgr`et$h3O~IQBuctqvSe3` z&#E1iLma>21J9-JP*MAZz^}V|%(=R3!sZFJg!+zve%Vw?4p4Dv4}+j`njN>yps#jK z{zH9zypU{FA5U4Oeq)5nwbNRwVX#X)B$MK+2ygVyNwn!}X?J`C+luhm(}(7(`;||y zNiF%GU1tz4Wj| z1%vY-Rh}pQM*#no;C&LitFyJp%^yqv>vPQTX9~%wRM(CobGW}JT0byS=^!zA^RsWwTp)A7OJ)_O=Sda#0+2I@QyI z^=n@*Q~h@4nsDu!Pn}ja!al5)!Ecj#g~lT%V?q=R&6t#jPRg>tzCD_9lcb1(canB& z74MN6V<185s$LiS=Y?#`{;0Ij<;UFpCLd8fG3(ZvSQvHqt7}FPaZxz4Gw15Bk{xt9 z3F6dilLo1^J^YXE?bDYbAA{4N5AzI@q-{g57BJBOeGwHUHPfVUzbxn45?3~t7@@UT zNI?fX0jtL5Sq$tCeW5_Q+on&~+=MImax7wi2#B)zojj|7;XqV1OY-gl&utb}oSF7_ zkH3Ow7n2ugI!&apZW=?ac_F(`ish}oFY0103FX5*I9*&Gab6fOy+1^(?Npb$73cDs zZ|2&($Enz+!DrW)Ck;a4W5R{w+j@&Q-2F#b`n}d#$iFV|sgH{^Lik#kGToco2zFOq z3)mF)XDP^E8~YI!H1a~CQ$EqDn(}1f=rHy@4SKfQ%8z_5U}R%_pSe01%tJjosO$&t zYYOb&_i*;2_AA_W%=DQlQS+3&TaoLaf6&H=fjP9iF69N86o3~FD4ZyleRIuZ_z-uJ z=Gw2JARYTFd|5`6{XTa^p5Q4j`cf2FN;3`&82C7u`ms>6b>@PU(G|xqw)5bPPhJT8 z--Y)-Y-=MlY9qw|@LszxtNxeYYKP^5_rKWz%Q6`_?l+6ZZWSH+{{w~P{ohyG036)^ zW|k44;Qwb}bPv}5e8pU7^gnRe#6ksduwePdgOVKB|6)kp661eScS{cPzgW4y2lii# zJ<|O@#jbB4Vg9$+^YbwH{}kza_XPbfNp<`Uij&%$1CE($?*NYQ-#CxY`Ty#)%0htl zK>e2>!h_!am$aaPN<;rQs8tRZ^xwwwznP|%5`eNJ{nyKhTi;&T_{Y6{aY{WQ03`t~ z`So1ZC7rKdF;&i?gVDla9%Q6pYa0^mO*E=DD;vr9%Tzfa5QemZLBIkQoLv5XzYD$_ znd2w!{j-zUMB&3cLRTwk*iqV9+G7#mCsHz^f>wemLo_`K=znp1Sp0T&9bD4NNTpb< zY9xe(OE5iCMs2V?#?*?##<=cnEi44ga%!a1FA7K=j8hg7W&+gUR?lB9Ssf z@G;>$2=fxROz>z1e=0+Ij+zn5WP9jfRG1qsg?j*KcNt$qdYo>qWZ zPi}oCi>*a>(v_^hu{sF|`Lc4Hp*{4v~fL z4+Z^+a0%IQVAevK3-J;1m2zo8lx``uK5DHYP~E&`UTg-YgTn)(&12}VE9i6kNs#WM z9Ib{<}D zb|7?m7XJ>GPeisc4?%;X;y^*Uonwd|6>V5ZDNdi!N!Bep%Sq|;W&rv9I@c;?9&FGc zE}*JCH;u_fdOhu*0wE+OF46)K48k8fBPdx^@|Sx&ii8w7iQ<8k#tIM(q98MCHr_%d zzAq&~UTP{Lp?T-2{_%c{$YXQwJ6=|!AxP9plfWd^UP1d14sIH>jpgxAU-atVeF5x$ z-fD1bTcG^iXo0=CCwd3c3s7b{gu;4NK8}1yFlIJT)S`_q6ae zOG+ffElWp}&F$zkwNV`%H-+?7=82X;H2s2ow!Tzyb68Y!nglAVBqTGK#*F*W)S0m4 zh4zbO>~)>bD<~bu>UCgUnRseX(KU*s{<#XL3;#BRl!;^m0nRp>C_xj60@js4zUiaY zbEZ#^F@BkE|KvW{uxosIr`@k;ozw+05Tfa+|I>aen8pwTx1;8lz0oQQc8X$HP@g9G zVf`y=Tu)qSt`*p;3#0n?CGDw%j65Svg;0TnlN#(PZ zzJtKRrSLsnp3~g5L2B2LGRDIw4x8~R04u~pU?49UCn>@jw7d%r%7Orp}h>0n2&|#U>B?!hf&CZPrfBO z9Cit|`6e5>!8iPIW$oWg6d1Dz7XRdDRJ|mrMP3B0VhpUhvZbi=hP+$N26~hTHchgWHhK`jimyGd$W?u@@m9T0u>RXX7k33| z_-#CI2lQUOTI|JZg3uj!v6#rnzTpQ7Bt(Vzj8l6+`uN}T3$*)_Ab#r(z3NiD`72zW zSP}%D=_N=DckzTj^jye!utul0Mw*IH9zepEPiv~*J59Ls%3mhXCYNW6-7}St588zz zd^wXLHR@1;#U3*rbIQXSY1N^GyT?W4sjQStKGwZof;VBFIm?04t8UC}oR&C*(=P-_ z;=FZIei*CyDL$!+$fCA7#oK{U$X2`y!#a-pCpPd~3^x^X%Nc6v_{64JLmBBRZCdKBNX#$XpfD-n$Bz+&N*1l!CXAaoJmOtrV|7@4 zyOm`(yU?Te!M(KO&^QKRWl&0$xC=?C`bNxy?+qpA9NTI$nG_{rLR^;bbl719>DZlC zXxB620uHQ;(E2}Yj5e%ccSfw?9Rcz>8IJ+mxs?%Ci*a47^kYbb; z+NQ=acc$kNoq`7Q{ICqzzweC5MtpRO#b6f%z&s!oKDlrkK(3S~woH@vZ)f3o@T{N3 z>ScuI^Bub6!g}FNs@PfSz~zFdIJPV)gnMTB#1LZC>?4tP@WGnI3;(v7ntJPz7K6O^ z*EIWugyy&k^2wA8QPs#8kOO%<{a==UWCYvF9&>ull4-9bN3)T>i=NQA#8c@Q7NX#K zHrvhG8XJ?-Lr&Umug+vGbL*V0kj!FVcO*b@m3JK3nVcbdt<bQjcu1&hvZ zYD|B?Mj!@e;@T1%oTEKTE zOV+Pe3n{!{xE9)A><{+Jk6?h>CX2{SFnrMI4=NH^R>0s<2nT3%&7R68{$4d;3p;1& z%i|5{qU*KXVOfC${uhC?nh^VS*6k*gN%NpCBH{$4v&p*Gv=5-es8|q*st+@pG}^Ge zG#XdTC@lV24`K~YU==9aDEMxfSw;RQF5P^T&c!IX9cRVih>*$%rdXCpnW3r3Lc%iN*QRm8XngseRQHY&YLtMx3(^E@~5!cfiz)e~~ z?C9L<0=J!Nbp?>EFY#wd>s*4K0;~kry7*DR=ZfthZgqum6B&Z~IbPg?{~xN(u{#rB z3$w9p8y(xW)v;~cd83Y#j&0kvZQHhu>3i3#b!WcSFQ`?g&a?N!6B*rw$_JsYJ@}_h z^)L?hR)Acm8Z}4QZ1`-Trgq)?1f?sZCOT2rnvH{_P>XuuyGnNpyjA?ux*0VJeVM&T zmeBfA-Z0yygVov-1By!KAg1Xx%^r-f7HLO?7Ql8>m5+@ilLp~Qa9KD_KnU=JxgW__ z(0u!j>T05n*9$}5#6y&JO$bw-PuQsTcGBzuRW9ClNl>mkpkl|}oQQ*eM%>pYD32dw z@yLtR_Y+D1!?Y?FAT#U?7tA?qhKTW&oJcBvRGK=ci$#Z3?}UtlCU;VWE|W(vU`t|Y z1t`h+XM|Yy1}0vm4=imLx7bU7wisjhMBYbbmRMd=r$>>0_-89#`Yec^)J1oB(Evjk z3p2(^e5ZmT)~&kxtHKz(y)0A2ei~FQmMADj@H?sh1lLz2BwD0o<#5q80B#!B3F#=6F#wUhl0ZJgdZ^+fkm1GOXc)S|w*1N>d_P4CGjN2jK zf3++LYJUBE5%!R8<{wWhCGaKPb9K`sd3>|WF0`<5sUmEqfoS7WcGeBPXiK#Z$ty&K zIxX+A*Xk-o5B(xoQ_@jY#~PZVeUPiL+e6UGGrxR|TT5PbX=GOJ)&`?`TkNE50c7tE zL*EvVDl0eTb=XR{Tj#Mj*P3?OQG^D{7=)Nuj(L+fcA~rJD|2C|FF3jJL61-3y z2r&Uh+Tu9FXmm!EOYvbqvqp~*29SRreg>jdxS&dW21CqXlkn=}Lh%O{isrOZL9s8l zbM8i}m%zae5cR!ckVJ7&WK{QU50)d}E^LDQCJc_$xD5+MU`EuS!2`v7N2d&0^~6Y&ZB*5 zhO5ercdUV5w~cmvnT0HnOvE1L#g=GX@+9v1cD&vt(5HYOnZ~oQJ%PA8Dd_II5A6Hk z{m$5^a_LtkYa%^Wqb5chM~wSwrxL_b%#RzF03Gr<7QdhETQ1`X`GZmZ(!dZiw)f+@ z^yvJCS^uCKJLlPRo4yBpcEi{;@BVZvR=L_ZtYzK zGYY31QIawEz$RFH_u+Cs9-=6q%WMH*ReIMsaOu=3G%1cXSil!9`oq6@Sl4UFC$MCO ztg8WW8K5w4kCKU*;&RT~qhIz^In(OE+-p)_fQ_}i!pO#&4cIyHQudEm9}XyByEp@X z=;*WIbZnf?R#{yI3&7t3yPGul+UTJyoRhNnw0T|Nl>EM!}=A0smHN^7OzSMQ zPF5@KY#2xG3IFTsp8MCgCG@ZF3mpIR*3I}xH|}%YfzD;eid&tpLBqum~F<|l=*%={*>pfv7NJ#>YV)H9w z>^|TPcmK++xi13(1t=FFy8nXsTyOowmO8o;05gn6S9pf*Rq1r%;jpLIcwX;*}Ft6m&)C6~&#|s-=6qbMf~f3W1HgA9Y7ma@5)5 zyUY0{2mso*v>)M)*iI7V@N6-7fdAFFGoS}8FvIfB4#{_zQ$zN6=CQN+IOMi+Fyzy~ zIFW5P#c?6PU}2EKl7bL{K_Y~=}E?W{4C zY?dIhgw>R2Sy-d_&iLY=0nd2a2x5#BUVM1o@@X+wQ9TQSF?tDy9Tn%5#Ld#hnB9Q+<>a^iM?I~f|dc9}tt#0EI()rVwehlw!d&CJ(JOWjfZo4d@Zee(&otS^V0+H)N~+RUK|5lR{}`W zJ-1wU6)$X&^c84osyJTZh)rjluKw_Nrn^3kK>2V@_p-ctqeZ#SOyi&4oNe{ZH5m?E zbZdG4-8g+&-dfms@-FSc*QP{3_bm9beW?q$ncaC#4)=cDNT$9c|FT6~yj<#R{O+g` zRZuwDT4&kN{-GhX;! zMvMS(^@qhwoL{qVpr?->n?bddM4&_T(*=-+!@oT-6m4Og5o$L2@Zy$Q=@r^==h60# z(J0Ys#*lU@Fimr+3)PRv#%@rnpos|3l-QS#QC^|=dwm>3T2*@ZM7TqK?3~qf2}9e0 z<)-~9DQ?Tuxd*@>#HIngPlxvFk;o%GDF^UW0pj~Qx}VTy@eJ_rhcYl6pFf0(d?8vd zr!)?4tO_qb?7~o`dV*90zTk6~ahNVJRVMIlp*W>whkV(AKDg>inz!ohup8YI1XPz7 zJoGPvpYig8_MCAGKOFLd9e$o1AmuoRN*yT{5AbY}$sHb+JGTH@N_2p`=73-Ex}V1! z>-SoAOcAbm&RW4*zsiioGt0oGuibNq{N^iX5v$L47qgE(%jcTA&1-iS-)r;QM`*gQ z#487x#Vnj~1_v`p>~<#|jYn0Z0Rv?&IfSkBr~4VA*L>iK1433>U&|dzLjcUd&4!G{ zSs&~hw2Tv7vps4X*b&nA|C4w7&%fyfBQO#u;6J~mczzLtCU`(V{gpsK)c;$}nVFgz zGnm`h8yVW@o15C(nz}ey8tXea*;|?~d6VR_@x%?OV6^lP6(VNEPAQ=_yT;0eh9@C+%&yZrg z7B|EbRo_CiM1g%!s`^IH9>0T3<3}{cF0_w7)hh1x{G>>^cenRX13rN#K4*G1KLNec)L(n`55wLHSMm(HWo=%YHim{KHF11=*UOcO&Ri3n$4 zfDnSmOt$eMXBC?Sq>hEKaz-6NdRT~5P(JpdRU8P|6w!Ln8)XxI;f#$XErP<)kF;N& z=x1k6@ZRoeCYSVa=1lk`%RWDg7XWER57ylx_(jK05WTBXD5i!Ss9WA=L%8oo4%av6 zS)X~NiC3Xh?73>c0Uo{sdd%gH1BmVMv*lGi@kJ+yQb<$ZZ5A`42ow@rV!BkNy>#a1 zJ9kz)YtsLkdDY4tSyd+Of3I&tMt32?99m1iN0Wn6pIu&E5#BsDaKr@CqRK6t_1*ZF zULgz{}~8he>jzkjd4FA4P5{$ntgX zFtSAkc=3WjWQr9Ldw&-YC6TmE=3xLqiUc4f%Fp$db(48?q2@iF2E4K*tr&tG=#Ux0 z9U$bBb)zt20cHltWXa)m;V*xf3m(gHIJHwl1+o^8cYQ=o>tW9wuLeO6ZO-t%R&N zF^@>cftu(N^mG}RL}1LFGQ}c=%7y8jNf1K;=d`MW0X$&ygSU^W2sBytb+RMubB+xa z1xIKS40>YiqOm;XusanGrhQ^hC;BQ2YtB#t07LqVXZ4W{j5n(H%NHqSzIFbAK`2Ow~7$ zw9kbnLOl=N%E*+D5A056as$J@7nf2G3C0k=06HNC8Vecox$tY*$W`;VR>`Dyx%jiN z-XL+QrE9>Ws1R4eV~QJXv?4+lLtNql%kiF8fM#Ye zG*I@qexp>qbVOy?2lSn z(eNtQckix>ky0sDy!)!dns0$Efq9ho0|=y82g~APFS%Vf>3O&C;(f2Jy3etN5~uwU zq|m+n;FPl;Wv|blN`}4Hj|&yF*yImH0=pB77ZBN&P^F<%SU~H98(8y(U=D|c&1O)G zZ!;z7lOqafkbvm?14Ba8Ce-XwB5VlYhjglwsr^A&IXuS8uVp5&6=g#cVFECbKG@G?Oy( zaSuV8hRTfO33CYOQg&|>o^}@Dy%7lwc?6aiK%%!9{N2cRzs~-5911gJwmggGFB#Go z>fJenFwWwjC6F2brx<(RldgbN2oMY$5E2Wc65v&e*C<@!O50nZ$tk|T0!+y$F;U-j z#Rj3XQ0B3Z?&3@Vxyc>2JO&GI7jL>JKR3F1imV>{7wLDppb>t_kHo)5bi0;GH_kuc z6w9On zIIWzO@{r|j?wPCAA8bvwBmj1M5*Z73DGSy>kTAcCWrD}4AOhIFRk)V`&0!vhhkmE8 z=lwm@L9>I`0D+mRpDtkU_cVy$`&rM==i$}E4e;DH5~lDu44I+%n< z8yS({+k^THEiZ3X={sU znN$OJJkI-{_QYJyT0lr6tMi&jIvGXqehA+5Y>-;c+?$ZxL?I)1g%z+eXp*@aok<`o z8g=?76!<_SEG8n}gm;hPT?mK`R{6SL4@KZ2T(KBHSJS`? zX41N*ob5&j)m6bsv_o;>1egEn&F?ui5j6UjK&MTI5MSj8%^@O{Z5Z+-}eKuiGhC=8%^%WL&tVV9xU{w^G|5(UN@J^&K? z*+mZc>#vD+ql?)f$;KN(m}jo5Eq38b>Z*Rq-+&74wmX?4Y1c_ z*^$FW1&9*E@6(hOifTuWU~1{D!tg>wQ+Tl**9hk$$6{La&mxK~^MZY52B~8n_RX?> zDNPav*59TI3k242nHXdVFggCqQ)!GE-UOOL5W?G*vrB`F)G<2oZCHJ)3sKOg7=lgP z$2%o=V+2N^7&iBb;YmnQbUoN*D`H)`Dw@TK10*@cr3FgO?P9rGJ|xrfui7x0_$zv4%hq(FNT{f`P`m3=DlR}b15{c&!bAeZ}Zo}`rfA% zY;?2^Ajg~)i$eLI7Kx&HyCGX;zXWw4dU2@#`JxY|^@Da0d-_2CsTBzN*=Q@qWH>Bj z0oP)0L0(7KMdNS^DRK1FpXg+Dx4+tfS8#;uu|-{;XbTM#s2mCoIRqnyWVLpM&ClAWZQ=U>m0%nr4wr%h5-@rM-VT+VSk z8?wXonmC!duM4=HH}Uwvz2JA)uG?770UT6qTj$|7;PUL(nm|BzdN&s4uvervt)*Bjn^kLe;C86!1<&OiKP3g3Go79}fLL)W*#8TlQkHcn@=>_S`&YnLF5>lvK8+F)e**B zquy*UbNOndm3bS z8qHyD)bZ0ZHZqPRxyjc!Gl4CQCgiSL-H@=1%-(|C`AeaNU zC%Utm(Olma%z7TuKS?S&SRs#Vh*mijw=SshiV!~E&ZQj;(tPY{mKDa>0qklxZ3_UY zGEAS?zP}Rnq+$p*tiPAYUm%d!;=Qfm)zqegXB@XKw=hvI_&-9DVnPQ{uziyx3wa9w z-mUK}CX5J=+e6d4%YrxhI-4l9`F#+9UOcT&-Ygnb8Be@Ar&Padl^A<6!Ngok{}^5K z9hzWaQQ+6#r=a@?6{+Zb0Fh(rP`oRJF%cL+2*Qm1a^}Y6~0$lZ$fjg!Q?{+U=v+&itEICGvLkG3vDoDS ze^_4(D=-OPj59K@0a#%7zU3wlKHGTNy9)CuSHLx-?ldxtlQ1W{hF=2XM%Bj* z`*{6nz-0l&;z*UFi~F>=q(}G6%yqIA7g@`GcTHsGM%Cmm=h>rG|DaGSb!Cg}eY*X_ zdo{|T=EOkd0#-q>naa`GZF9aR&6A7FCe@9~I6iF_bFp(pfUVO6XA&11H=3(P?x-r> z;B9>3i%33y>{w&LLo+3V0_@sMU*fn<&aeg#k!aBpt;22OG<_`>wRl|6PxhIviY*oi zF8Mia2+;<1j0m>9nMmmZ`=PFRvn`UX5@@b1gOIbCBk1SmHXW}FWiM-Uk#0CYvkPdu z`H*GctVzfwfCXIlEFcbe( z*sW&rHB`h4;^+}lz2)sg^DV&3%TehxC?E(cOVxQA+49?H?8vuBd~zubrWgN26c*Gj zjk>u4m(=PJ%$~iPB}e+LjYLK8SC(?0!dWHE6qySE5NvbhVXOT?Gla>Oaa0{0s(bho zBh8}o$k4#V$ix^?HbJX5GZ`sZl(o^eyhhV1QcOoRX26>q$*a8Kpqgm3>8C~~6@mTO zuaqN;k&tc*k7P*PHEC>*lvRC2_-T^{ue+$bGDWNtTv^$yqO!bE0-$?4OOvX&@@XD&GK>SV(a!A{n3 zb)o>S@FF*k!nU}0Qtv-O6Cprnne9IcslE8uQ2%!PC@N z_N$cBp(KqXJW0!F*b%W9dyibZ6ic`yHv%*UAkBJ8G!55MJ@|)CHI&R%<9$Fs;Kc9a zVxp}R#_wluxgEB)S&}BP<1P#(BP^QUQN3XM)^c|L(lAd#8Cx_|IEydha6}}LG&Pku zT9S&BCw}VjLhn7Nhb;iUOFjAl( z;8Su{6b+CC3F`oJyW`{L`L=Pf15hm4_a}E*VW3qEF^w5CjTer1uvd4US3@jF&z>DNO1yP$UC{U4+&|k58|;p;Sj3$sTgsV1VK#C z4mC$z_@LcA!+F#k=iU%cWi|cr(c;5?L=_cvA!(OnPpB5syjVn!{Zw@$_NFO&cBnwr z>JlkW-McfVWAs4^09PRtuJ0>NnmykmpMFy2pRTqKgt*C5)|g7vRLx2kj1v|GNKE$@|A~H1xbj3JF`n%fIwu=Zgf)-nKPgp0K-icO8kMl> z9CZgy00n8TuiD+**uBhQSFnH|*E zWOX$wKMh%Wg(|Z{xh9>$GuaIW(J6W<-7jesASg0j=KkLA@mB}nhZpGq`u~Y$?5&pU zt=TNB*h-tJE9%dg*i$JlOr93+D*zu^&pqHPLXzLBLMaI-1o3&KsnE}e?$2vgk(HB5 zOJaI6wnfTkFQ;%k&QI-2TfexPucmFrB%64etEOq=3c={*Od;go3Rm|m*Caw-Q|U>{ z?t(dPgR0bCPFRgGCW*>XOjR|B*{Po7zJ#Ej@lRNXrt=z@u68_U(+ ztE~mmJU?3Yq_h2~oVG_9bi6^2D`}pA&lAMp@+ZwhJN=70=X&{FIP$CKdL$r++j5Ox zU841=X>>J|zwI#fmB8}AbQKP@E5r*O8D;+ZO{zw8Oh^WBa=-J@r&Vv;T?GqjX0t7p|2c84lbjaYxlA(eEDY3>w! z=owDq7kj%Wo_yi>yh?d=HNhg=XoT zi9-&*2~@ZP24-wddg=n*4&8C^E3cZW1~%G9N3(L6I8On>4nUTg1D5tD~>3*NA5C#k&m9s}HxoP2QY+Ia^-1CjbPX zw}$LjjZdK-g-fRq$j3U%_EtWHDA#VKzlZ@g@w$U;c2W#l53L7e1%CIC&VkMH?A2Ql zwT^$bC~-Huw{+{{QlD7NaPvMkj&Zgq+?jU)68i2Tj-==hZx2x$*MhJ6PLUf|+y-;whbX?OCBt4p=h6w|@6^{$4~EO0;mGR(d_o$1E>r0^98z z>W#BEFFf>JTMAinmED9j+<95a;%Wk%Ol+!8SD#i|Uv>P+LAzuZ+{1a#D!nhNN%w|* zC<+%(%5qer#)OidZol`(Xb(?n`Mrs;iHEG0hR_Bb0R0rpH0_oduo{NIQH#?*i$12S zg0CU}$N4h!q>LISFt68>WlT#xR$glR)c{t1iFEzoLUw{>$!dJpvXN+cm*EgmjhMcN zOGQhYh~N7q#&@k3*8SlC5W=F3I*_muLD}{z zZ~6x8ts?zdtQK5fFoFG7BA*(7QOp?>0|6I?@{Q#Th!giXc^3r4?(B!?+ZO>Ns0;kq zT^}l?nN)EBB6Bpc)hW9yC+K6FKu5tIvflOh5XJnqp@SfMLo&KIJxtJkpJ7AH)ykc(A?a(RWm7=aE<=+XH9M%Qat{fPba%ZrG?st* zazJ@ftqrDH;WzxtPGBK`ZLJxj?!vO|LKVD5Hyu6F))01Pv0a4^ zc-0T>QwVLi3gNukd)J(9lB}8#^NP6aIa0m@&g|!@gKzU$1`iw4EU&fSJvIdf&GU>M zDoi`BlO>uDQ%66`+-9(R&#GrwXhZhWLyx$^OFZhRP1kP-d|wOz#>_26Eq2Tqvzd=; zqpd)5tHE7o&yz)EI`0GeY=y_k4c+68ht!^t!W^UocC6}}!BwJ_?Qpp~iVg*Ccz*SH z%UYrN@E*9&D`o}l2`}8nn|ro-utu>5(n3^!IPOWi;<>1h@3PbOiRKy&L4ytcm+|s0OiG8a*MY#ub`a zik;4)g@_5q3;D=Cs=YNLW~@_r$fvCBlp1Gli&E}YVcln({TN{`6<>-az?$bh`k!)n zl?Fo91L-V;%=kXv^V4s4{4ZSzKpwYObo-`Gv~5-GibgnKifL8LdxySd!APK6N_9I? z_+6y@zONnca*O(qf#td@#pjZ8{OZ`Mkgu?_!^ZZXNg)4uXeh_Jz8TltWw{+^!xLwR zrBy#B^lmIbKjY62yRE&i+naOh$`9oKFqr==IayXY^w|FKha&&@!+&JR|K$$>4FBD7 zTG*SIoBrEp4ypfhd$1$@Q+xE06VhTv`1v!TTQ9iKu$zmqm|*==QIfA8OUHpok>B}! z#W2MGl6K><844z<8|pY1=s3wXw%CFyi&KO>=f7*Vi-DpU%osAF6pLQ0v!jl;oP(lx zmN32cAeT~E2*Jx@K&aJ1Z9V?@d%{EU(~mo)9sd;0t31<)wC*-3bxMmlNQ_}~cBS5lR}6;4*7|4nWs z>uJdAM+Ravevlc;f(wYD`z9mrb!3Xdq(cr?)bg^=0D0NwQ3dnm-o;7Ex5M&utk%}? zqv)i@QmGvBW{xmK@&H|={!L7WL@#53H40uQB%|~eveLjY*-7fUCzB$wo2Dvh zP`iz*L$W)Q>P3C;cEW&={m~K|Hrjvh(?~EVK)C5Pl2%&qwAxFFnB4IW^%4atO;K%VEDlDs!i)Nn6Cs zyzO}viL=Z?2?s{{(gt8D%uW+;{8!UM?fu8=U{;F()p98?veyoyomYpSC7a{y526Z~7 z9E%xN-nf*7ey1x>c9WA7)>(da1nZbZt`RG=fiI~~b_+&);@{L@!PKb7-k_P0PpJWW z4LRl;!3otfo?^yS7%4!inn)AvDTPvtrH?^UpFT4Eiq~|C?uA2#Qamc<+^Ug!sxbGA zmE2DwwhxG!|9+1BH`cRHTd}z{U#(;E_gOAZQF$?rdjLEx(HZ~J<03w6ePQk4(ZKPW z;Cm+|As5Rr`sQ&;qw4)Od&JY57K;dW5bh@{R^b-OLemuV1hQjWxKjHJOb%5H?9&i_ ztKS^3@pw9aU`h6N6^9AzbNh{$uk>Pn)ED)s7793z#X;Td725}ohaAX+-ldj?fTKa| zKNW^FHoH(h2f-e+$!WVR-6OD`ah*1XmEKF&jG7|f3=W>?(G&bd8dp86 zh;g^1@6}b){a)^#5jaVbEkqo^Cym@!EDKX7#~J1_#iDe}dPw>@O1xhu$%ST(Ad~DI z3zz`h*Nze=V^4}k(0DHd+j~UZV0~*Jiw*aK^amet%{Y<{Ffh}lN(K=3SIHhq2h#?? z@6dW;^!m^_J%9@*KPTgjXG)0bbRXq~xx;jMVb%o#=l>eWpKQ$!1Q_8K@+xLN?aSK5 zMj#EE(KLWm_jGtp!LmCg+IrL_$LY090o;*cgG38yHNi)796q*5QkZf3Xb!VYU$nEC zKDRIEFlePPU{iZ@1r9Po>0mtpV2_>@9w=!uPqtr%HSSEF+a>M+oqUsk&hE4U_Z39t zR!`TjTgH&JtF0>^m<9EPm4yy2-p;n}w13!skzOqbR_;4Y@wQx4uOa6(Fn=K$;Qa$s zVXj)(n8m;b0zGPD&vsThvN}(8s7Kn=$^tb|mO3TR;14_GF=LD%7_WoA685+r<v-=jL{%tNL0hAbnzI1hLVYqCI&A6=CQ|~CEQKv^06gEcbE~$_~&g)v*)yM=6(3)7%7E2XD3+~;SXdz+O7O#H(eApUnYUlWCj(C?z zFgp;ZnMHCK9mH2|RqnZyj3Wq*wn&y!=7FhJw3ud>R)@z6PX-;7Ya>9=>?lMB+G=gz zo4F6Sc`M)gviPM*4{Y49F%0(I;>x#v`pPTi=I6akf6g6uAWOE%SZsnGz;S9%$_tUd zs%d0UOR)o+LjmRXYO*XmcHj|rF|an2{p@^_x)+w*-C8<>*Yxi?~ZLUGqPdm7jG)LxWkDTOnh5N zHX_tJaNg%A(|AdQy@H%YLIjBm1Q(83e-lC|^^bqP%2;p041CBB8vGCM`DU{(Oi~V; zl@|ue8f9Z;LumoNcd%ih>Pg!!zDgGQ4!02{<+oKY&O2%pyz+54BID4k9Q%qbnB@vtA zVU@(g;sAQpOMyyPAUK5cuv{RleUlxv z&Aq<0s61UZNMeCDVJ(+Ia~nJInQ8K- zr)MNyBjM-zX$trCc&2X!0N4HYo+03irOe3Yoy=rkRc!H?aWP8 z&}Wj&=;}p1+{5qsV#M<#DnD`Z|6xK5eqRR*nBjRgd^P+FSC}8Y*;fR?`M6AKGpHt7UC-1hCaNu-(+CuaNrw*IWg9_LZZoR#K*vV@Ta;0u*9>+#O`q{Z+EdBdykb&h(4 zrg%-WbwJ!T#cd5z^Khc@0)DCK4h?-1Fl$)gctRUDlXoQHA#@U^o%naSO??-{*Z!(h2l7+8{n!Wzg*b=-H!r21&qBs@x-oJwmzg zA%kB4M8PLNa`V*s`eQQCsnFuOsHlD+2cc8d2=3%tZEI^`{w#-mgND$*F8RNs=f65HWZybw-vtk1nFtzZgj8a|#p1dx)CPCm+EC!6$Bd znF~069cd@NlFuH@7_ylL=ORGjc*;G=zq#h^P5T9740#eZ(<+z5U#V{FoZG&?h5y!nY_UM`P*YM)T~mHE zs+B%Ofmbk}q(+)HNrTSjlWI<{6sgbRXu~(Ai@p5ahHUa-Wp9qzx)ml#U^$vV_Omr6 zlHeM`TkhOB4n|X5Pn(PdPUfeqwBOq^otHzvqK^)=N+aUOWse+EG7}SVAKNp7t7ACf zY{+?R-1ba$Zn_EpmW&mX3%0nl?k2XCvM@XfuYb(vA45WgYA8grOi_6!>*dM;d7+{q zHcOR6)~-+%A(1E2ha^+5$>nCX+j|uD16EC8bU(lFBb34Hb!Y~)fX?YiX@U$lyt1A> zMd15)Q?6A3Ajb0eMZ}i%h-5^DN1%T}8~xZ0V=k^-sc$QUg0CJOc7E+(N$9tb7zUas zW|;Wnw}MS!3uQUs%f~sP(?L7~Ea0~D`$MQX=PB#s^#|Q@iO*jx*tqfBpo)RFQG>=a zqjRfvaRj)_&Jm;a@h!gkD1L;H8d=+yM)!6L_b!7@TUKr2_3xZB(WVq%8jptsipskR zAy*QAl?G(DH|$C(f4OE!D+V?ft`QyOF}opAr5xzlsk$~KdNxV*U7#-kPL-gs*U{GL zhB3>Gvdk;M-sW=N4e1Q_9xF&@1%7?SA#=nZZ0+VMwF>0aFfAMx;ciu_(P4XYms8kk zPC-k;C))db06ij=>yCSWN21Li9)ubGtAzyS0_BO?6~5N^61><;j5L>f2tCAlDhJ7(SxjRq&n4jtT%CH-rmyqg*`DB@;0v#IK@*$JTxQ#dJ%_hyMh7O1e~T+ z`e~?c?C~^&6Tr8S+I{8?z$s9<_+xt^4^kSbS4?gV{rRKBaV~!cL>hAtD$q$oy;gP3 zOUx4SX73G@X8y@@3k5VJvnmgd?q6ocm!vAgMA~aM5Ukp+Itl#Wj#PEk_Y0@ru@eEF zQ_HSyC`bCaYk4<-tH7>SSXz}STPgRHvX?xc2lGqc#T)pQ45AQrOTS6ij%;E}B{CNY zp}EvreBBB)?_+B#C#4Cdv9oWR?quv`8mi%Hq>Div{dzNOj4oU?lfV#xC~2JT1=rwB z1MVg!4&5bmn>^H^H5V^swzbKsx$A40D9yCCZck}g2+I+`ugr@*x2wSXGy~4uJRgi& zj3|hxr}Ryd!Jg(Or*p4&T+UN?P+>Ms{A&)w$<~o@Qe;8X_$(}&lEoUGhTm21jotkd~ z>NaYW_K5{R#2bQOa(O}S`TZK?@kAMFYELchs#N1riW>+oLJ7C%9~E`mbnW#PaJQ#f z$Xxs#$HD+@tAub=$exnP5IU~v)kXO{l@k5rp}PisH_`q2_M1YQ%aEx#=er> ztc&Yumk!_jia_KyCOc07XQc1ajy7FR`3PgIf9bzvl>y?obwPq0iqHP~4skY|W6|gwro{h(l^>(UC zG9?|#R(+-dE&oxa(fLR9Saj@^CE#DUrplt3wBqmev+ecVv0IXb zMw482qRp|q{Vl;vPHX#d^f6(Y(!QxthRjfcY)e?*)1q66N4PuP)l*&33!og{-bqQW z%>Cus)2{GcG%Zadxl^9GSj&k~S%>MdBMLJzFolsN#%V0^_)IO_2%J^uXJyo?;a?S&{r&{Fn2Vbm#Er?8 zTS(BFqXoGcJd9GZ&fX)Z31E}gd3!Z97&I8PlB&0@b_Keh_FKq9Ho?2pg9U-u&)Lxr zI3v1iM*=mc#uhH0N_6@ON7KL55;ghE5UF?I=v-p*hBR{k3#HnE*|9%elSsFE4FB=E_ts}6OrIya z_3iUA&{%N|T)?0AMzkCD76+C*3}SL$;T|uY)0f3TBjqfdbtx@8Lx4P*I@#? zlENz1yBcpH);+CI3oyg2g#FtfaA>HUi5IT&)ak;;m+z0{ZYYFS$b%TZiIwSG1S-?z z>ULbI=2YG_HR&|03imXz)e4OTgIX+^)MZ)w1~hz;x z-W^{JFAc`1M23uBG708rnx1EOIL46n5Zm?;nOSHVZ@{DtKj1x9X z+a>Ijh1t-OjfOCC#ay4W-dia9&Nu&@b(1$t0wCp$vIjm_N9EM!IpdJ-!MB=MvzCaB z)(Gssa3{l|{=T_8-|6{!j;wcUE+>#m_=43!y&J3xRVgf&2h9WdC|2>Cl%mS&!vESl zqJfeofg8yr8&ISWOD84`QTO=} z7NItDYcfB%=$+o^F)v9eXE;7-SW9bt753u~!Y^3|9{II=s&-D$w*+6FZs=zFY|=eW zqG-Q-0EnjCeY+Lns<%%Qwk`2p*UNs%xhr;nc30#2aHAiM-f8uzq!gX;J`?dkKhwp! zYtgV1$qu&u$+?UM?d-raN#r_Q@ok%m`n_#D+02xj5D`khKbU-U052DA!xr%ngvwi? zx7uR>vd1y^9zp@lqtH{m8SPPcn;%SKnMvLK2eA2bwGyWN>+q~pNxm|d)NAi&2(>`l z>LLaadJcamq186$TXWNcsjf+A4qGt6gRhRI6!6EIaII})2!O;& zS`G;%i@9i(IKRoi772DV?=5zzSzzZ!>FUZhcm4I&<9 zvP2A`bNe+l%WS+kX~+DHWCxljKHg)LH!%};^??s_6*oBltO=EMAluI_#>|=)C&DoI z8KD<|7yrUg<;4?lT5xrWBnIU(Q$9_7Zq#U9h&E_>qId3GR;rZX;a|unXS9i60LW&J z7@q9EHP%6M86I~8PGEhgJ-c)4dn3ycc2WqT>nJRW9^tYPGba}+)A&DJol|s}!McWH z+qP}nY;4;}W9N_2xUp^9YHX*m?Z)ou*?XOvGi&Z<&GoGLUOemiFZe(YdB-`;d>Mb% z$2`W0{dvHyP;%Dfz@_3(YNe0k*4nRFjrM61moKbUYU@Ge<;x#1!@Jmff;LN^OalZ~ zjc6Tunosk&M2K}Kh3}Xg+JJ4=Q2?$XZn8O@TW4F79PN8I&e3`w%f{SAbGiQ2!El52 zgV_YPFSK5mbk=-Vi?wjKLG-Q=tO0(;C{o+{qX#&y3TJBIyC@V_la>?my{>P}Xb1U{ zZ5m32yXGJ3d}CEj&G6AMRt1jJ@Im>QX7EC_Y=sD>um4kKpg~|j{&!H=R`(tB7wUgr z=q2|y)CWco5TV&LAQ+SYuuIzDYCQG)3s;>(r@|Fs#UXqCF9`19WIDRG6h(~bYh@?T zuT@H69gZ87ENd(EWAdM$0UQJrWl3g+hucc&!9E(7W1dvJ8T(+bfPGKl1uUpV9E=={-NG{YZW2bF|Ba&|bn4(`THKR>Qv0R34 z=$w5us)smg@|ATbpl@9?t9LaRE2n)7k~z7XXbM(4*QXbC*z)RrF7u(U!R^3*WNPjY z{{wNuya*`A-%s223}V0P3QySTtVlLgKZI1({pSDdDJS|)|LBP5RZ+&g@QBy>(aF%- z)pold6f({>xp-s&sET@8t)by*wQS#pxbjv--ywQ%s9$w5xQG&KAQF$QwJ=a?G;}*( zfQ+M2EGod1&A4_HLYaO%2_#K=SLwEdLlww-v~WLE$Ks{-F*J2pbKg5?j4!)&yn`CH zomVe5&YV||1!;HAR43aoQnPzu!mAmx(I1*7PP1kF*09qD8tQkRnoLtHU)iH#RMa6T zu#^h&@5cx;bW|qHZGCDMflmhSXUV_@+#qD`@?C;LU_LD?4l#Uw(3h|O_sYG&~ zv;G;sQF+R1^(QbF65rf#QrSE7zXc+=yu+?qSkN+aPc~Cvyn|*kCqPo)Q5iU@;yD_h z=rc`+{poU zFl{9=z!`EyTKmfZz~o+-5_zJ7RMqj4dz7cjF_Q#9|8v&smeRvU`%4NdO+^IE*zkfT zS~3!uo-&Kc8ZVoi#-|*!AyhX+_0-xfAm>Iw7STJn(T6eCM4(>}uvF_%IE0A>`aGg7 zK`u_{DK|Kt_7qAP1iGY5*UG#$VjHlgOoFAJm1~>TEm#kU|T0Y z_UZtMzeU*R4pZe5kD?3p=kZ7uY#J(Hks)ZR7^cI-0dS-T;Tplr1}gthjKNZ5xC34~ zyTH4M;YJzJNs$Ekj!)}dLHUbh4nBxobTLqG9S}LhvbnaCx!LY^%r>c>5~3BY@~O^K z))uR|5-n!njT!~}OnI5IJINJHDw^UuC_50v|2DR>iP?dY1ba7bj;7(#5gC$=XZIED zFoL_6KEx4tPOoAqVIGuWehQkCSPrYjqTrr6=m*b@w)@qK3?aqa2m&gF_53^x#*4Zt z^4O-4Gm5k=eZ9*GNgUNym)nDO0DO@^m5rZijjx@pX<>`Ac7ZYKn8QPYN!3x27XdU9 zusf6XnpXKjdf&fNkMAjO15LeHIGMPMc{12QVpoab)h+UJPLAszbF6j6NlP-Tv6uuN=4yD?9;mCM7QF-sJ8dZb@YDJ7$ZDqU@w zDi;m$qmG=6scTae>@H!#X$+JMPA=DA#8Jq^JuW&(@^`;3wzgeB_;RpcU4V!qKQ)0n zdH%us12AezfttY6pSW%98oy|;Gz_Z8&GukGqCp1?Az!J|7*Lc+LD*rW95~0hCrhZY z%zJ2EyB@8|{i~H%i{`~Ij>Q?0?@!p#n)`nJym3PSGN8yvbOJ2d+Q zZKaZaE}6EDP5h9=vQ};R2f1YwnX}-") + result_lines.append("") + in_list = False + level = {"=": 1, "-": 2, "~": 3, "^": 4, '"': 5, "#": 6}.get(next_line.strip()[0], 3) + result_lines.append("") # blank line before header + result_lines.append(f"{line.strip()}") + first_line = False + i += 2 # Skip both the title and underline + continue + + # Check for bold section headers (**Text**) + bold_header_match = re.match(r"^\*\*([^*]+)\*\*\s*$", line.strip()) + if bold_header_match: + if in_list: + result_lines.append("") + result_lines.append("") + in_list = False + result_lines.append("") # blank line before header + result_lines.append(f"

{bold_header_match.group(1)}

") + first_line = False + i += 1 + continue + + # Check for list items + list_match = re.match(r"^-\s+(.+)$", line) + if list_match: + if not in_list: + result_lines.append("") # blank line before list + result_lines.append("
    ") + in_list = True + result_lines.append(f"
  • {list_match.group(1)}
  • ") + first_line = False + i += 1 + continue + + # Check for numbered list items + numbered_list_match = re.match(r"^\d+\.\s+(.+)$", line) + if numbered_list_match: + if in_list: + result_lines.append("
") + result_lines.append("") + in_list = False + # Just treat it as a regular list item + if not in_list: + result_lines.append("") + result_lines.append("
    ") + in_list = True + result_lines.append(f"
  • {numbered_list_match.group(1)}
  • ") + first_line = False + i += 1 + continue + + # Empty line handling + if not line.strip(): + if in_list: + result_lines.append("
") + result_lines.append("") + in_list = False + else: + result_lines.append("") + first_line = False + i += 1 + continue + + # Regular paragraph text + if in_list: + result_lines.append("") + result_lines.append("") + in_list = False + + if first_line: + # First line (summary) - no

tags + result_lines.append(line) + first_line = False + else: + # Multi-line paragraphs - collect all lines until next blank line + paragraph_lines = [line] + j = i + 1 + while j < len(lines) and lines[j].strip(): + paragraph_lines.append(lines[j]) + j += 1 + + # If it's a single line, wrap in

tags + if len(paragraph_lines) == 1: + result_lines.append(f"

{paragraph_lines[0]}

") + else: + # Check if any line is a math BLOCK placeholder (not inline) + # Inline math placeholders should be kept together with text + has_math_block = any("", + ( + '' + f"{display_math}" + "" + ), + ) + elif unicode_math.lstrip().startswith("", + ( + '' + f"{unicode_math}" + "" + ), + ) + else: + html = html.replace( + f"", + ( + '' + f"{unicode_math}" + "" + ), + ) + + # Restore inline math as code + for i, unicode_math in enumerate(inline_math_items): + html = html.replace(f"", f"{unicode_math}") + + # Restore code blocks + for i, code in enumerate(code_blocks): + # Escape HTML special characters in code + code_escaped = code.replace("&", "&").replace("<", "<").replace(">", ">") + html = html.replace(f"", f"
{code_escaped}
") + + return html + + +def rst_to_markdown(rst_text: str) -> str: + """ + Convert RST docstring to Markdown for Jupyter notebook display. + + Args: + rst_text: RST formatted text + + Returns: + Markdown formatted text + """ + if not rst_text: + return "" + + md = rst_text + + # Extract and convert math blocks + math_blocks = [] + + def save_math(match): + math_content = match.group(1) + # Clean up the math (remove leading spaces) + math_lines = math_content.strip().split("\n") + cleaned_math = "\n".join(line.strip() for line in math_lines if line.strip()) + math_blocks.append(cleaned_math) + return f"" + + # Handle .. math:: blocks + md = re.sub(r"\.\. math::\s*\n\n((?:[ \t]+.*\n)*)", save_math, md) + + # Convert bold (**text**) - already markdown compatible + # Convert italic (*text*) - already markdown compatible + + # Convert inline code (``code``) to `code` + md = re.sub(r"``([^`]+)``", r"`\1`", md) + + # Convert :class: references to code + md = re.sub(r":class:`~?([^`]+)`", r"`\1`", md) + + # Convert :ref: references to bold + md = re.sub(r":ref:`([^`]+)`", r"**\1**", md) + + # Convert :meth:, :func:, :mod: to code + md = re.sub(r":(?:meth|func|mod|attr):`~?([^`]+)`", r"`\1`", md) + + # Restore math blocks as LaTeX math + for i, math in enumerate(math_blocks): + md = md.replace(f"", f"$$\n{math}\n$$") + + return md + + +def auto_convert_docstring(cls): + """ + Decorator/hook to automatically convert __doc_rst__ to __doc__ (HTML). + + If a class has a __doc_rst__ attribute, this converts it to HTML + and sets it as the class docstring for VS Code display. + """ + if hasattr(cls, "__doc_rst__") and cls.__doc_rst__: + # Convert RST to HTML + html_doc = rst_to_html(cls.__doc_rst__) + # Set as the main docstring + cls.__doc__ = html_doc + return cls + + +if __name__ == "__main__": + import argparse + + from struphy.models.utils import get_model_by_name + + parser = argparse.ArgumentParser() + parser.add_argument( + "model_name", + type=str, + help="Name of the model to convert docstring for", + ) + args = parser.parse_args() + model = get_model_by_name(args.model_name) + auto_convert_docstring(model) + print(model.__doc__) diff --git a/struphy-tutorials b/struphy-tutorials index e396c06c0..7a1ed1b90 160000 --- a/struphy-tutorials +++ b/struphy-tutorials @@ -1 +1 @@ -Subproject commit e396c06c083af309bf2141515495a855f41a6b12 +Subproject commit 7a1ed1b90793ce22042b18dba905c5be4d91d3d6 From aa97880c09317d03031e872045f7a055272e5c09 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 5 Mar 2026 10:29:21 +0100 Subject: [PATCH 65/83] Generate parameter files from Simulation class (#193) This PR is based on the idea that `__repr__` should [return a printable representation of an](https://www.geeksforgeeks.org/python/python-__repr__-magic-method/). The goal is for all objects in the API to have this method, so that a simulation setup can be reproduced using a new `generate_script()` or `save_script(filepath)` method. - Added `__repr__` and `__repr_no_defaults__` methods to most classes in the API. This allows us to create a repr of the instance without cluttering it with default parameters. - Renamed a few old `__repr__` methods that would fit better as `__str__` - Updated tutorials by using `print(x)` instead of `x` - Added `generate_script()` and `save_script(filepath)` methods to the Simulation class - The generated strings are formatted with ruff, which also removes unused imports. Therefore, ruff is moved from a dev dependency to a normal dependency. This is the easiest way to use a consistent formatting for the generated scripts. - Added test to compare the script generation from two simulations. --- pyproject.toml | 2 +- src/struphy/fields_background/base.py | 21 ++++- src/struphy/fields_background/equils.py | 2 +- src/struphy/geometry/base.py | 16 ++++ src/struphy/io/options.py | 47 ++++++++++-- src/struphy/models/base.py | 13 +++- src/struphy/models/tests/utils_testing.py | 21 +++++ .../post_processing/post_processing_tools.py | 12 +-- src/struphy/simulation/sim.py | 76 ++++++++++++++++++- src/struphy/topology/grids.py | 11 ++- src/struphy/utils/utils.py | 52 +++++++++++++ struphy-tutorials | 2 +- 12 files changed, 256 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a2081a2df..ee8dd5206 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ dependencies = [ 'pytest', 'pytest-mpi', 'pytest-testmon', + 'ruff>=0.15.0', 'line_profiler', 'scope-profiler>=0.1.7', ] @@ -60,7 +61,6 @@ dev = [ "pylint", "ssort", "add-trailing-comma", - "ruff>=0.15.0", "pre-commit", "nbstripout", "tabulate", diff --git a/src/struphy/fields_background/base.py b/src/struphy/fields_background/base.py index 1b3a3ad70..fbf9d4035 100644 --- a/src/struphy/fields_background/base.py +++ b/src/struphy/fields_background/base.py @@ -7,6 +7,11 @@ from pyevtk.hl import gridToVTK from struphy.geometry.base import Domain +from struphy.utils.utils import ( + __class_with_params_repr_no_defaults__, + __dataclass_repr_no_defaults__, + all_class_params_are_default, +) class FluidEquilibrium(metaclass=ABCMeta): @@ -96,13 +101,27 @@ def domain(self, new_domain): assert isinstance(new_domain, Domain) or new_domain is None self._domain = new_domain - def __repr__(self): + def __str__(self): out = f"{self.__class__.__name__}" for k, v in self.params.items(): out += f"\n {k}:".ljust(20) out += f"{v}" return out + def __repr__(self) -> str: + out = f"{self.__class__.__name__}(" + for k, v in self.params.items(): + out += f"{k}={v}, " + out += ")" + return out + + def __repr_no_defaults__(self): + return __class_with_params_repr_no_defaults__(self) + + @property + def is_default(self): + return all_class_params_are_default(self) + ########################### # Vector-valued callables # ########################### diff --git a/src/struphy/fields_background/equils.py b/src/struphy/fields_background/equils.py index 1b504f7d1..700f94444 100644 --- a/src/struphy/fields_background/equils.py +++ b/src/struphy/fields_background/equils.py @@ -35,7 +35,7 @@ from struphy.fields_background.mhd_equil.eqdsk import readeqdsk from struphy.io.options import BaseUnits from struphy.physics.physics import Units -from struphy.utils.utils import read_state, subp_run +from struphy.utils.utils import all_class_params_are_default, read_state, subp_run if TYPE_CHECKING: from struphy import domains diff --git a/src/struphy/geometry/base.py b/src/struphy/geometry/base.py index f3f230ce6..4cdf0be03 100644 --- a/src/struphy/geometry/base.py +++ b/src/struphy/geometry/base.py @@ -1,6 +1,7 @@ # coding: utf-8 "Base classes for mapped domains (single patch)." +import inspect from abc import ABCMeta, abstractmethod import cunumpy as xp @@ -12,6 +13,7 @@ from struphy.geometry import evaluation_kernels, transform_kernels from struphy.kernel_arguments.pusher_args_kernels import DomainArguments from struphy.linear_algebra import linalg_kron +from struphy.utils.utils import __class_with_params_repr_no_defaults__, all_class_params_are_default class Domain(metaclass=ABCMeta): @@ -209,6 +211,20 @@ def __init__( ) def __repr__(self): + out = f"{self.__class__.__name__}(" + for k, v in self.params.items(): + out += f"{k}={v}, " + out += ")" + return out + + def __repr_no_defaults__(self): + return __class_with_params_repr_no_defaults__(self) + + @property + def is_default(self): + return all_class_params_are_default(self) + + def __str__(self): print(f"{self.__class__.__name__}") for k, v in self.params.items(): print(f"{k}:".ljust(20), v) diff --git a/src/struphy/io/options.py b/src/struphy/io/options.py index 8844aa614..d57838fcb 100644 --- a/src/struphy/io/options.py +++ b/src/struphy/io/options.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from typing import Literal -from struphy.utils.utils import check_option +from struphy.utils.utils import __dataclass_repr_no_defaults__, all_class_params_are_default, check_option @dataclass @@ -130,11 +130,18 @@ class Time: def __post_init__(self): check_option(self.split_algo, LiteralOptions.SplitAlgos) - def __repr__(self): + def __str__(self): for k, v in self.__dict__.items(): print(f"{k}:".ljust(20), v) return "" + def __repr_no_defaults__(self): + return __dataclass_repr_no_defaults__(self) + + @property + def is_default(self): + return all_class_params_are_default(self) + def to_dict(self) -> dict: dct = { "dt": self.dt, @@ -177,12 +184,19 @@ class BaseUnits: n: float = 1.0 kBT: float = None - def __repr__(self): + def __str__(self): units = ["m", "T", "1e20/m^3", "keV"] for (k, v), unit in zip(self.__dict__.items(), units): print(f"{k}:".ljust(20), v, unit) return "" + def __repr_no_defaults__(self): + return __dataclass_repr_no_defaults__(self) + + @property + def is_default(self): + return all_class_params_are_default(self) + def to_dict(self) -> dict: dct = { "x": self.x, @@ -241,11 +255,18 @@ class DerhamOptions: def __post_init__(self): check_option(self.polar_ck, LiteralOptions.PolarRegularity) - def __repr__(self): + def __str__(self): for k, v in self.__dict__.items(): print(f"{k}:".ljust(20), v) return "" + def __repr_no_defaults__(self): + return __dataclass_repr_no_defaults__(self) + + @property + def is_default(self): + return all_class_params_are_default(self) + def to_dict(self) -> dict: dct = { "p": self.p, @@ -295,11 +316,18 @@ class FieldsBackground: def __post_init__(self): check_option(self.type, LiteralOptions.BackgroundTypes) - def __repr__(self): + def __str__(self): for k, v in self.__dict__.items(): print(f"{k}:".ljust(20), v) return "" + def __repr_no_defaults__(self): + return __dataclass_repr_no_defaults__(self) + + @property + def is_default(self): + return all_class_params_are_default(self) + def to_dict(self) -> dict: dct = { "type": self.type, @@ -366,11 +394,18 @@ class EnvironmentOptions: def __post_init__(self): self.path_out: str = os.path.join(self.out_folders, self.sim_folder) - def __repr__(self): + def __str__(self): for k, v in self.__dict__.items(): print(f"{k}:".ljust(20), v) return "" + def __repr_no_defaults__(self): + return __dataclass_repr_no_defaults__(self) + + @property + def is_default(self): + return all_class_params_are_default(self) + def to_dict(self) -> dict: dct = { "out_folders": self.out_folders, diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 71e4188b6..99d7bd928 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -16,6 +16,7 @@ from struphy.propagators.base import Propagator from struphy.utils.clone_config import CloneConfig from struphy.utils.docstring_converter import rst_to_markdown +from struphy.utils.utils import all_class_params_are_default class StruphyModel(metaclass=ABCMeta): @@ -147,7 +148,17 @@ def update_scalar_quantities(self): # -------------- # Common methods # -------------- - def __repr__(self): + def __repr__(self) -> str: + return f"{self.__class__.__name__}()" + + def __repr_no_defaults__(self) -> str: + return self.__repr__() + + @property + def is_default(self): + return all_class_params_are_default(self) + + def __str__(self): out = f"{self.__class__.__name__}\n" for k, v in self.species.items(): out += f" {k}:\n" diff --git a/src/struphy/models/tests/utils_testing.py b/src/struphy/models/tests/utils_testing.py index f2e78b9c8..bf2f6ea88 100644 --- a/src/struphy/models/tests/utils_testing.py +++ b/src/struphy/models/tests/utils_testing.py @@ -1,6 +1,7 @@ import inspect import os import shutil +import tempfile from types import ModuleType from feectools.ddm.mpi import mpi as MPI @@ -69,6 +70,26 @@ def call_test(model: StruphyModel, test_profiling: bool = False, verbose: bool = sim2 = Simulation.from_dict(sim_dict) # test the from_dict method assert sim == sim2, "Simulation to_dict and from_dict methods are not consistent" + # test the generate_script method + sim1_script = sim.generate_script() + sim2_script = sim2.generate_script() + assert sim1_script == sim2_script + + # Save the generated script to a file and check that it can be imported and run + with tempfile.NamedTemporaryFile(suffix=".py", mode="w+") as tmp: + sim.save_script(tmp.name, include_main_guard=True) + tmp.seek(0) + spec = import_parameters_py(tmp.name) + assert isinstance(spec, ModuleType), "Generated script did not import as a module" + assert hasattr(spec, "sim"), "Generated script does not have a 'sim' object" + assert isinstance(spec.sim, Simulation), "'sim' object in generated script is not a Simulation instance" + assert sim.generate_script() == spec.sim.generate_script(), ( + "Generated script does not match original simulation" + ) + assert sim == spec.sim, "Simulation in generated script is not the same as the original simulation" + + # Run the simulation from the generated script + sim.show_parameters() sim.run(verbose=verbose) diff --git a/src/struphy/post_processing/post_processing_tools.py b/src/struphy/post_processing/post_processing_tools.py index c8a69a1ca..9f6301442 100644 --- a/src/struphy/post_processing/post_processing_tools.py +++ b/src/struphy/post_processing/post_processing_tools.py @@ -33,7 +33,7 @@ class SplineValues: - def __repr__(self): + def __str__(self): out = "" for name, species in inspect.getmembers(self): if isinstance(species, SpecHolder): @@ -43,7 +43,7 @@ def __repr__(self): class Orbits: - def __repr__(self): + def __str__(self): out = "" for species, orbits in self.__dict__.items(): shp = orbits.shape @@ -55,7 +55,7 @@ def __repr__(self): class DistributionFunction: - def __repr__(self): + def __str__(self): out = "" for name, species in inspect.getmembers(self): if isinstance(species, SpecHolder): @@ -65,7 +65,7 @@ def __repr__(self): class DensitySPH: - def __repr__(self): + def __str__(self): out = "" for name, species in inspect.getmembers(self): if isinstance(species, SpecHolder): @@ -75,7 +75,7 @@ def __repr__(self): class SpecHolder: - def __repr__(self): + def __str__(self): out = "" for name, val in self.__dict__.items(): out += f" {name}\n" @@ -90,7 +90,7 @@ class DataDict: def __init__(self, data: dict): self.data = data - def __repr__(self): + def __str__(self): out = f"{type(self.data) = }\n" out += f"{len(self.data) = }\n" for key, d in self.data.items(): diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index b3bcf9754..1e1b4a9e7 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -61,7 +61,7 @@ from struphy.propagators.base import Propagator from struphy.simulation.base import SimulationBase from struphy.utils.clone_config import CloneConfig -from struphy.utils.utils import dict_to_yaml +from struphy.utils.utils import dict_to_yaml, ruff_autofix_and_format class Simulation(SimulationBase): @@ -1410,6 +1410,80 @@ def from_dict(cls, dct) -> "Simulation": verbose=dct.get("verbose", False), ) + def generate_script(self, include_main_guard: bool = False) -> str: + """Generate a Python script that can be used to reproduce the simulation.""" + + script = f""" +from struphy import ( + BaseUnits, + DerhamOptions, + EnvironmentOptions, + FieldsBackground, + Simulation, + Time, + domains, + equils, + grids, + perturbations, +) + +from struphy.models import {self.model.__class__.__name__} + +""" + + sim_setup = "" + sim_class_def = "sim = Simulation(" + + # Always include model + sim_setup += f"model = {self.model.__repr_no_defaults__()}\n" + sim_class_def += "model=model," + + # Only include parameters that are not default to avoid cluttering the script with unnecessary lines + if not self.env.is_default: + sim_setup += f"env = {self.env.__repr_no_defaults__()}\n" + sim_class_def += "env=env," + if not self.base_units.is_default: + sim_setup += f"base_units = {self.base_units.__repr_no_defaults__()}\n" + sim_class_def += "base_units=base_units," + if not self.time_opts.is_default: + sim_setup += f"time_opts = {self.time_opts.__repr_no_defaults__()}\n" + sim_class_def += "time_opts=time_opts," + if not self.domain.is_default: + sim_setup += f"domain = domains.{self.domain.__repr_no_defaults__()}\n" + sim_class_def += "domain=domain," + # This is a bit of a special case since the default is None, + if self.equil is not None: + sim_setup += f"equil = equils.{self.equil.__repr_no_defaults__()}\n" + sim_class_def += "equil=equil," + if not self.grid.is_default: + sim_setup += f"grid = grids.{self.grid.__repr_no_defaults__()}\n" + sim_class_def += "grid=grid," + if not self.derham_opts.is_default: + sim_setup += f"derham_opts = {self.derham_opts.__repr_no_defaults__()}\n" + sim_class_def += "derham_opts=derham_opts," + if self.params_path is not None: + sim_class_def += f"params_path={repr(self.params_path)},\n" + + sim_class_def += ")\n" + + script += sim_setup + "\n" + sim_class_def + if include_main_guard: + script += """ +if __name__ == "__main__": + sim.run()""" + + return ruff_autofix_and_format(script) + + def save_script( + self, + file_path: str, + include_main_guard: bool = False, + ): + """Save the generated script to a file.""" + script = self.generate_script(include_main_guard=include_main_guard) + with open(file_path, "w") as f: + f.write(script) + def __eq__(self, value: "Simulation") -> bool: assert isinstance(value, Simulation), "Comparison only implemented between Simulation instances." return self.to_dict() == value.to_dict() diff --git a/src/struphy/topology/grids.py b/src/struphy/topology/grids.py index ca49d61c3..9d66cc97b 100644 --- a/src/struphy/topology/grids.py +++ b/src/struphy/topology/grids.py @@ -2,6 +2,8 @@ import numpy as np +from struphy.utils.utils import __dataclass_repr_no_defaults__, all_class_params_are_default + @dataclass class TensorProductGrid: @@ -20,11 +22,18 @@ class TensorProductGrid: Nel: tuple = (24, 10, 1) mpi_dims_mask: tuple = (True, True, True) - def __repr__(self): + def __str__(self): for k, v in self.__dict__.items(): print(f"{k}:".ljust(20), v) return "" + def __repr_no_defaults__(self): + return __dataclass_repr_no_defaults__(self) + + @property + def is_default(self): + return all_class_params_are_default(self) + def to_dict(self) -> dict: dct = { "Nel": self.Nel, diff --git a/src/struphy/utils/utils.py b/src/struphy/utils/utils.py index df9ca0d18..6b26ab20f 100644 --- a/src/struphy/utils/utils.py +++ b/src/struphy/utils/utils.py @@ -1,5 +1,7 @@ +import inspect import os import subprocess +import tempfile from typing import Literal, get_args import yaml @@ -132,6 +134,56 @@ def subp_run(cmd, cwd="libpath", check=True): subprocess.run(cmd, cwd=cwd, check=check) +def __dataclass_repr_no_defaults__(obj): + out = f"{type(obj).__name__}(" + for k, v in obj.__dict__.items(): + if k not in obj.__dataclass_fields__: + continue + default_value = obj.__dataclass_fields__[k].default + if v != default_value: + out += f"{k}={repr(v)}, " + out = out.rstrip(", ") + ")" + return out + + +def __class_with_params_repr_no_defaults__(cls_instance): + sig = inspect.signature(cls_instance.__class__.__init__) + defaults = {k: v.default for k, v in sig.parameters.items() if k != "self"} + out = f"{cls_instance.__class__.__name__}(" + for k, v in cls_instance.params.items(): + if k in defaults and v != defaults[k]: + out += f"{k}={v}, " + out += ")" + return out + + +def all_class_params_are_default(cls_instance): + return cls_instance.__repr_no_defaults__() == cls_instance.__class__.__name__ + "()" + + +def ruff_autofix_and_format(code: str) -> str: + with tempfile.NamedTemporaryFile(suffix=".py", mode="w+") as tmp: + tmp.write(code) + tmp.flush() + # Run Ruff to autofix (remove unused imports) + subprocess.run( + ["ruff", "check", "--select", "F401", "--fix", tmp.name], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + # Run Ruff formatter + subprocess.run( + ["ruff", "format", tmp.name], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + tmp.seek(0) + result = tmp.read() + return result + + if __name__ == "__main__": state = read_state() for k, val in state.items(): diff --git a/struphy-tutorials b/struphy-tutorials index 7a1ed1b90..c55763af4 160000 --- a/struphy-tutorials +++ b/struphy-tutorials @@ -1 +1 @@ -Subproject commit 7a1ed1b90793ce22042b18dba905c5be4d91d3d6 +Subproject commit c55763af4852f003450e7c462d96efb00324a833 From 96814ac964c9374498081e05c2259d54a8c70338 Mon Sep 17 00:00:00 2001 From: Stefan Possanner <86720346+spossann@users.noreply.github.com> Date: Thu, 5 Mar 2026 12:26:54 +0100 Subject: [PATCH 66/83] Vlasov maxwell bugfix (#192) The weights in models with an initial Poisson solve are set to zero for noise reduction (automatic control variate). In runs without control variate, they must be restored after the initial Poisson solve. --- src/struphy/models/vlasov_ampere_one_species.py | 5 ++++- src/struphy/models/vlasov_maxwell_one_species.py | 9 ++++++--- src/struphy/pic/base.py | 5 +++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/struphy/models/vlasov_ampere_one_species.py b/src/struphy/models/vlasov_ampere_one_species.py index 2fbcba015..d935e8971 100644 --- a/src/struphy/models/vlasov_ampere_one_species.py +++ b/src/struphy/models/vlasov_ampere_one_species.py @@ -99,7 +99,7 @@ def allocate_helpers(self, verbose: bool = False): if MPI.COMM_WORLD.Get_rank() == 0: print("\nINITIAL POISSON SOLVE:") - # use control variate method + # use control variate method (reset weights after Poisson solve) particles = self.kinetic_ions.var.particles particles.update_weights() @@ -136,6 +136,9 @@ def allocate_helpers(self, verbose: bool = False): if MPI.COMM_WORLD.Get_rank() == 0 and verbose: print("... Done.") + # reset particle weights + particles.weights = particles.weights_at_t0.copy() + def update_scalar_quantities(self): # e*M1*e/2 e = self.em_fields.e_field.spline.vector diff --git a/src/struphy/models/vlasov_maxwell_one_species.py b/src/struphy/models/vlasov_maxwell_one_species.py index 9287586ce..ade9742b3 100644 --- a/src/struphy/models/vlasov_maxwell_one_species.py +++ b/src/struphy/models/vlasov_maxwell_one_species.py @@ -174,13 +174,12 @@ def allocate_helpers(self, verbose: bool = False): if MPI.COMM_WORLD.Get_rank() == 0: print("\nINITIAL POISSON SOLVE:") - # use control variate method + # use control variate method (reset weights after Poisson solve) particles = self.kinetic_ions.var.particles particles.update_weights() # sanity check - # self.pointer['species1'].show_distribution_function( - # [True] + [False]*5, [xp.linspace(0, 1, 32)]) + # particles.show_distribution_function([True] + [False]*5, [xp.linspace(0, 1, 32)]) # accumulate charge density charge_accum = AccumulatorVector( @@ -211,6 +210,9 @@ def allocate_helpers(self, verbose: bool = False): if MPI.COMM_WORLD.Get_rank() == 0 and verbose: print("... Done.") + # reset particle weights + particles.weights = particles.weights_at_t0.copy() + def update_scalar_quantities(self): # e*M1*e/2 e = self.em_fields.e_field.spline.vector @@ -235,6 +237,7 @@ def update_scalar_quantities(self): particles.markers_wo_holes[:, 6], ) ) + self.update_scalar("en_f", self._tmp[0]) # en_tot = en_w + en_e diff --git a/src/struphy/pic/base.py b/src/struphy/pic/base.py index d976457fd..505fd8643 100644 --- a/src/struphy/pic/base.py +++ b/src/struphy/pic/base.py @@ -826,6 +826,11 @@ def weights(self, new): assert new.shape == (self.n_mks_loc,) self._markers[self.valid_mks, self.index["weights"]] = new + @property + def weights_at_t0(self): + """Array holding the initial marker weights. The i-th row holds the i-th marker info.""" + return self.markers[self.valid_mks, self.index["w0"]] + @property def sampling_density(self): """Array holding the current marker 0form sampling density s0. The i-th row holds the i-th marker info.""" From ca230a1e6005a87b261434233f8bfaa5f87702bb Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 6 Mar 2026 12:26:25 +0100 Subject: [PATCH 67/83] Add `show_3d` and `create_geometry_mesh` methods to domain class (#195) - Added a few new methods for viewing and exporting 3D views of the domains - `create_geometry_mesh`: Generates a pyvista mesh - `show_3d`: Displays the 3D geometry using pyvista (works in console and notebooks) - `export_geometry`: Save the mesh as `vts` or `vtp`. - Added an iterator to the domain class so that we can loop over all domains - Added 3D views for tutorial 6 - Added the neccesary dependencies. You can now view the domain with: ```python from struphy import domains domain = domains.Tokamak() domain.show_3d() ``` image To display all domains (except for the Spline ones, which don't work with default params) you can do this: ```python from struphy.geometry.base import Domain for domain_cls in Domain: if "Spline" in domain_cls.__name__: continue print(domain_cls.__name__) domain = domain_cls() domain.show_3d() ``` --- .../workflows/submod-struphy-tutorials.yml | 7 ++ pyproject.toml | 8 +- src/struphy/geometry/base.py | 88 +++++++++++++++++-- struphy-tutorials | 2 +- 4 files changed, 98 insertions(+), 7 deletions(-) diff --git a/.github/workflows/submod-struphy-tutorials.yml b/.github/workflows/submod-struphy-tutorials.yml index 417161675..f35b9cd26 100644 --- a/.github/workflows/submod-struphy-tutorials.yml +++ b/.github/workflows/submod-struphy-tutorials.yml @@ -33,6 +33,10 @@ jobs: if: env.SUBMOD_CHANGED == 'true' uses: ./.github/actions/install/ubuntu-latest + - name: Install OSMesa/EGL + if: env.SUBMOD_CHANGED == 'true' + run: sudo apt-get install -y libosmesa6 libosmesa6-dev libegl-mesa0 + - name: Install Struphy if: env.SUBMOD_CHANGED == 'true' uses: ./.github/actions/install/install-struphy @@ -47,6 +51,9 @@ jobs: - name: Run workflow if submodule changed (all PR commits) if: env.SUBMOD_CHANGED == 'true' + env: + PYVISTA_OFF_SCREEN: "true" + PYVISTA_JUPYTER_BACKEND: "static" run: | echo "${{ env.SUBMOD_NAME }} has changed, running tests..." ls diff --git a/pyproject.toml b/pyproject.toml index ee8dd5206..a222dd724 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,8 +34,14 @@ dependencies = [ 'vtk', 'tqdm', 'argcomplete', + 'ipywidgets', 'plotly', - "pyvista", + 'pyvista', + 'trame', + 'trame-vtk', + 'trame-vuetify', + 'nest_asyncio2', + 'vtk', 'pytest', 'pytest-mpi', 'pytest-testmon', diff --git a/src/struphy/geometry/base.py b/src/struphy/geometry/base.py index 4cdf0be03..7bd50fb26 100644 --- a/src/struphy/geometry/base.py +++ b/src/struphy/geometry/base.py @@ -6,6 +6,8 @@ import cunumpy as xp import h5py +import pyvista as pv +import vtk from scipy.sparse import csc_matrix, kron from scipy.sparse.linalg import splu, spsolve @@ -16,7 +18,18 @@ from struphy.utils.utils import __class_with_params_repr_no_defaults__, all_class_params_are_default -class Domain(metaclass=ABCMeta): +def all_subclasses(cls): + subclasses = cls.__subclasses__() + subclasses = subclasses + [g for s in subclasses for g in all_subclasses(s)] + return subclasses + + +class DomainMeta(ABCMeta): + def __iter__(cls): + return iter(all_subclasses(cls)) + + +class Domain(metaclass=DomainMeta): r""" Abstract base class for parametric domains in plasma simulations (single patch). @@ -1463,6 +1476,72 @@ def get_params_numpy(self) -> xp.ndarray: params_numpy.append(v) return xp.array(params_numpy) + def create_geometry_mesh( + self, + nx: int = 32, + ny: int = 32, + nz: int = 32, + verbose: bool = False, + ): + """Create a PyVista mesh with geometry + + Returns + ------- + pyvista.StructuredGrid + """ + + grids_log = [ + xp.linspace(1e-6, 1.0, nx), + xp.linspace(0.0, 1.0, ny), + xp.linspace(0.0, 1.0, nz), + ] + + tmp = self(*grids_log) + grids_phy = [tmp[0], tmp[1], tmp[2]] + + # Create PyVista structured grid + mesh = pv.StructuredGrid(grids_phy[0], grids_phy[1], grids_phy[2]) + + return mesh + + def show_3d( + self, + nx: int = 32, + ny: int = 32, + nz: int = 32, + verbose: bool = False, + ): + """Show the 3D geometry using PyVista.""" + mesh = self.create_geometry_mesh(nx, ny, nz, verbose) + plotter = pv.Plotter() + plotter.add_mesh(mesh, show_edges=True) + plotter.show() + + def export_geometry(self, filename: str): + """Save the geometry to a VTK file. + + Parameters + ---------- + filename : str + The name of the file to save the geometry to. Supported formats include .vts, .vtk, .vtp + """ + mesh = self.create_geometry_mesh() + if filename.endswith(".vts"): + mesh.save(filename, binary=True) + elif filename.endswith(".vtp"): + # Extract the external surface (Geometry Filter) + geom_filter = vtk.vtkGeometryFilter() + geom_filter.SetInputData(mesh) + geom_filter.Update() + + # Write as PolyData (.vtp) + writer = vtk.vtkXMLPolyDataWriter() + writer.SetFileName(filename) + writer.SetInputData(geom_filter.GetOutput()) + writer.Write() + else: + raise ValueError("Unsupported file format. Supported formats are .vts, .vtk, .vtp") + def show( self, logical=False, @@ -1888,9 +1967,9 @@ def __init__( Nel: tuple[int] = (8, 24, 6), p: tuple[int] = (2, 3, 1), spl_kind: tuple[bool] = (False, True, True), - cx: xp.ndarray = None, - cy: xp.ndarray = None, - cz: xp.ndarray = None, + cx: xp.ndarray | None = None, + cy: xp.ndarray | None = None, + cz: xp.ndarray | None = None, ): self.kind_map = 0 @@ -1902,7 +1981,6 @@ def __init__( cx = mhd_equil.domain.cx cx = mhd_equil.domain.cy cx = mhd_equil.domain.cz - # assign control points self._cx = cx self._cy = cy diff --git a/struphy-tutorials b/struphy-tutorials index c55763af4..73f34fb20 160000 --- a/struphy-tutorials +++ b/struphy-tutorials @@ -1 +1 @@ -Subproject commit c55763af4852f003450e7c462d96efb00324a833 +Subproject commit 73f34fb201a3dead9c190f101656e1fc994efbad From dc6055c420f722d5c96b295dfe1ba83c8750bdd3 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 9 Mar 2026 10:01:29 +0100 Subject: [PATCH 68/83] Added export and from_file methods to SimulationBase class (#197) Added `export` and `from_file` methods to the `SimulationBase` class. Now all the following examples work: ```python from struphy import Simulation from struphy.models import Maxwell sim1 = Simulation(model=Maxwell()) # Test 1 - to_dict/from_dict dct = sim1.to_dict() sim2 = Simulation.from_dict(dct) assert sim1 == sim2 # Test 2 - export/import YAML sim1.export("sim1.yaml") sim3 = Simulation.from_file("sim1.yaml") assert sim1 == sim3 # Test 3 - export/import JSON sim1.export("sim1.json") sim4 = Simulation.from_file("sim1.json") assert sim1 == sim4 ``` --- src/struphy/models/tests/utils_testing.py | 14 ++++++++++ src/struphy/simulation/base.py | 29 +++++++++++++++++++++ src/struphy/simulation/sim.py | 31 +++++++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/src/struphy/models/tests/utils_testing.py b/src/struphy/models/tests/utils_testing.py index bf2f6ea88..aa3fc52aa 100644 --- a/src/struphy/models/tests/utils_testing.py +++ b/src/struphy/models/tests/utils_testing.py @@ -90,6 +90,20 @@ def call_test(model: StruphyModel, test_profiling: bool = False, verbose: bool = # Run the simulation from the generated script + # Export to json and import again + with tempfile.NamedTemporaryFile(suffix=".json", mode="w+") as tmp: + sim.export(tmp.name) + tmp.seek(0) + sim_from_json = Simulation.from_file(tmp.name) + assert sim == sim_from_json, "Simulation JSON export/import is not consistent" + + # Export to yaml and import again + with tempfile.NamedTemporaryFile(suffix=".yaml", mode="w+") as tmp: + sim.export(tmp.name) + tmp.seek(0) + sim_from_yaml = Simulation.from_file(tmp.name) + assert sim == sim_from_yaml, "Simulation YAML export/import is not consistent" + sim.show_parameters() sim.run(verbose=verbose) diff --git a/src/struphy/simulation/base.py b/src/struphy/simulation/base.py index e6ea1802c..367b5790f 100644 --- a/src/struphy/simulation/base.py +++ b/src/struphy/simulation/base.py @@ -1,5 +1,8 @@ +import json from abc import ABCMeta, abstractmethod +from struphy.utils.utils import dict_to_yaml + class SimulationBase(metaclass=ABCMeta): """Abstract base class for simulations.""" @@ -38,3 +41,29 @@ def pproc(self, verbose: bool = False): def load_plotting_data(self, verbose: bool = False): """Load post-processed data for visualization.""" pass + + @abstractmethod + def to_dict(self) -> dict: + """Serialize the simulation configuration to a dictionary.""" + pass + + @abstractmethod + def from_dict(cls, dct: dict): + """Deserialize a simulation configuration from a dictionary.""" + pass + + @abstractmethod + def from_file(cls, file_path: str): + """Deserialize a simulation configuration from a file.""" + pass + + def export(self, file_path: str): + """Export a simulation configuration to a YAML or JSON file based on the file extension.""" + dct = self.to_dict() + if file_path.endswith(".yaml") or file_path.endswith(".yml"): + dict_to_yaml(dct, file_path) + elif file_path.endswith(".json"): + with open(file_path, "w") as f: + json.dump(dct, f, indent=4) + else: + raise ValueError("Unsupported file format. Use .yaml, .yml or .json.") diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index 1e1b4a9e7..a0baf9c0f 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -1,5 +1,6 @@ # third party imports import glob +import json import os import pickle import shutil @@ -9,6 +10,7 @@ import cunumpy as xp import h5py import pyvista as pv +import yaml from feectools.ddm.mpi import MockMPI from feectools.ddm.mpi import mpi as MPI from feectools.linalg.stencil import StencilVector @@ -1410,6 +1412,35 @@ def from_dict(cls, dct) -> "Simulation": verbose=dct.get("verbose", False), ) + @classmethod + def from_file(cls, file_path: str) -> "SimulationBase": + """Deserialize a simulation configuration from a file based on the file extension.""" + if file_path.endswith(".yaml") or file_path.endswith(".yml"): + with open(file_path, "r") as f: + dct = yaml.safe_load(f) + elif file_path.endswith(".json"): + with open(file_path, "r") as f: + dct = json.load(f) + else: + raise ValueError("Unsupported file format. Use .yaml, .yml or .json.") + + # YAML and JSON do not have a native tuple type, + # so when you load them with PyYAML or json, + # sequences are always converted to lists + def convert_lists_to_tuples(obj): + if isinstance(obj, dict): + for k, v in obj.items(): + obj[k] = convert_lists_to_tuples(v) + return obj + elif isinstance(obj, list): + return tuple(convert_lists_to_tuples(i) for i in obj) + else: + return obj + + # Convert lists to tuples for relevant keys + dct = convert_lists_to_tuples(dct) + return cls.from_dict(dct) + def generate_script(self, include_main_guard: bool = False) -> str: """Generate a Python script that can be used to reproduce the simulation.""" From 0c49dcfb25ecc6d6ae33b4088373ed9ec49482e0 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 9 Mar 2026 10:01:38 +0100 Subject: [PATCH 69/83] Added name and description to the Simulation class (#198) Example: ```python from struphy import Simulation from struphy.models import Maxwell sim = Simulation( model=Maxwell(), name="Test Simulation", description="This is standard Maxwell simulation.", ) ``` --- src/struphy/models/base.py | 3 +++ src/struphy/simulation/sim.py | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 99d7bd928..0c82d0038 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -627,6 +627,7 @@ def generate_default_parameter_file( # Please fill in a verbal description of the simulation. # It will be printed at the beginning of the simulation and can be used to keep track of the different runs. +name = \"Default {self.__class__.__name__}\" description = \"\"\"\nThis is the default simulation for the model {self.__class__.__name__}. It is meant to be a template for users to set up their own simulations with this model. It contains all the necessary components of a Struphy simulation, including the model, @@ -709,6 +710,8 @@ def generate_default_parameter_file( file.write("\n# Simulation object\n") file.write("""sim = Simulation( model=model, + name=name, + description=description, params_path=__file__, env=env, base_units=base_units, diff --git a/src/struphy/simulation/sim.py b/src/struphy/simulation/sim.py index a0baf9c0f..28102976a 100644 --- a/src/struphy/simulation/sim.py +++ b/src/struphy/simulation/sim.py @@ -79,6 +79,10 @@ class Simulation(SimulationBase): ---------- model : StruphyModel Physics model that provides species, propagators and variables. + name : str, optional + Name of the simulation. + description : str, optional + Description of the simulation. params_path : str, optional Path to a Python parameter file to save alongside outputs. env : EnvironmentOptions @@ -113,6 +117,8 @@ class Simulation(SimulationBase): def __init__( self, model: StruphyModel, + name: str = "", + description: str = "", params_path: str = None, env: EnvironmentOptions = EnvironmentOptions(), base_units: BaseUnits = BaseUnits(), @@ -124,6 +130,8 @@ def __init__( verbose: bool = False, ): + self._name = name + self._description = description self._model = model self._params_path = params_path self._env = env @@ -492,6 +500,10 @@ def run(self, verbose: bool = False): if self.rank == 0: print(f"\nStarting simulation run for model {self.model_name} ...") + if self.name != "": + print(f"Simulation name: {self.name}") + if self.description != "": + print(f"Description: {self.description}") self._remove_existing_output_files(verbose=verbose) @@ -1383,6 +1395,8 @@ def _initialize_from_restart(self, data: DataContainer, verbose: bool = False): def to_dict(self) -> dict: """Serialize the simulation configuration to a dictionary.""" return { + "name": self.name, + "description": self.description, "model": self.model.to_dict(), "params_path": self.params_path, "env": self.env.to_dict(), @@ -1400,6 +1414,8 @@ def from_dict(cls, dct) -> "Simulation": """Deserialize a simulation configuration from a dictionary.""" return cls( + name=dct["name"], + description=dct["description"], model=StruphyModel.from_dict(dct["model"]), params_path=dct["params_path"], env=EnvironmentOptions.from_dict(dct["env"]), @@ -1528,6 +1544,16 @@ def model(self) -> StruphyModel: """StruphyModel object containing the PDE of the model.""" return self._model + @property + def name(self) -> str: + """Name of the simulation.""" + return self._name + + @property + def description(self) -> str: + """Description of the simulation.""" + return self._description + @property def params_path(self): """Path to parameter file used for the run. Can be None if Simulation is instantiated in a notebook environment (no parameter file in this case).""" From 0f9c4058ea39b4067e34fb24b0be76fc0ee2d696 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 9 Mar 2026 10:01:48 +0100 Subject: [PATCH 70/83] Add iterators to models (#196) This makes looping over the models easier, it can now be done with: ```python from struphy.models.base import StruphyModel for model in StruphyModel: print(model.__name__) ``` Output ``` ColdPlasma ColdPlasmaVlasov DeterministicParticleDiffusion DriftKineticElectrostaticAdiabatic GuidingCenter HasegawaWakatani LinearExtendedMHDuniform LinearMHD LinearMHDDriftkineticCC LinearMHDVlasovCC LinearMHDVlasovPC LinearVlasovAmpereOneSpecies Maxwell Poisson PressureLessSPH RandomParticleDiffusion ShearAlfven TwoFluidQuasiNeutralToy VariationalBarotropicFluid VariationalCompressibleFluid VariationalPressurelessFluid ViscoResistiveDeltafMHD ViscoResistiveDeltafMHD_with_q ViscoResistiveLinearMHD ViscoResistiveLinearMHD_with_q ViscoResistiveMHD ViscoResistiveMHD_with_p ViscoResistiveMHD_with_q ViscousEulerSPH ViscousFluid Vlasov VlasovAmpereOneSpecies VlasovMaxwellOneSpecies LinearVlasovMaxwellOneSpecies ``` --- src/struphy/geometry/base.py | 8 +------- src/struphy/models/base.py | 9 +++++++-- src/struphy/models/utils.py | 8 +++----- src/struphy/utils/utils.py | 6 ++++++ struphy-tutorials | 2 +- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/struphy/geometry/base.py b/src/struphy/geometry/base.py index 7bd50fb26..0d1634264 100644 --- a/src/struphy/geometry/base.py +++ b/src/struphy/geometry/base.py @@ -15,13 +15,7 @@ from struphy.geometry import evaluation_kernels, transform_kernels from struphy.kernel_arguments.pusher_args_kernels import DomainArguments from struphy.linear_algebra import linalg_kron -from struphy.utils.utils import __class_with_params_repr_no_defaults__, all_class_params_are_default - - -def all_subclasses(cls): - subclasses = cls.__subclasses__() - subclasses = subclasses + [g for s in subclasses for g in all_subclasses(s)] - return subclasses +from struphy.utils.utils import __class_with_params_repr_no_defaults__, all_class_params_are_default, all_subclasses class DomainMeta(ABCMeta): diff --git a/src/struphy/models/base.py b/src/struphy/models/base.py index 0c82d0038..229591eca 100644 --- a/src/struphy/models/base.py +++ b/src/struphy/models/base.py @@ -16,10 +16,15 @@ from struphy.propagators.base import Propagator from struphy.utils.clone_config import CloneConfig from struphy.utils.docstring_converter import rst_to_markdown -from struphy.utils.utils import all_class_params_are_default +from struphy.utils.utils import all_class_params_are_default, all_subclasses -class StruphyModel(metaclass=ABCMeta): +class StruphyModelMeta(ABCMeta): + def __iter__(cls): + return iter(all_subclasses(cls)) + + +class StruphyModel(metaclass=StruphyModelMeta): """ Abstract base class for all Struphy models. diff --git a/src/struphy/models/utils.py b/src/struphy/models/utils.py index 69706e1d5..33051b4ce 100644 --- a/src/struphy/models/utils.py +++ b/src/struphy/models/utils.py @@ -20,11 +20,9 @@ def get_model_by_name(model_name: str) -> type[StruphyModel]: def get_models(model_type: LiteralOptions.ModelTypes | None = None) -> list[type[StruphyModel]]: model_classes = [] - for name, obj in inspect.getmembers(models): - # Only include classes that are subclasses of StruphyModel, excluding StruphyModel itself - if inspect.isclass(obj) and issubclass(obj, StruphyModel) and obj is not StruphyModel: - if model_type is None or model_type == obj.model_type(): - model_classes.append(obj) + for model in StruphyModel: + if model_type is None or model_type == model.model_type(): + model_classes.append(model) return model_classes diff --git a/src/struphy/utils/utils.py b/src/struphy/utils/utils.py index 6b26ab20f..9eaa98c65 100644 --- a/src/struphy/utils/utils.py +++ b/src/struphy/utils/utils.py @@ -184,6 +184,12 @@ def ruff_autofix_and_format(code: str) -> str: return result +def all_subclasses(cls): + subclasses = cls.__subclasses__() + subclasses = subclasses + [g for s in subclasses for g in all_subclasses(s)] + return subclasses + + if __name__ == "__main__": state = read_state() for k, val in state.items(): diff --git a/struphy-tutorials b/struphy-tutorials index 73f34fb20..e396c06c0 160000 --- a/struphy-tutorials +++ b/struphy-tutorials @@ -1 +1 @@ -Subproject commit 73f34fb201a3dead9c190f101656e1fc994efbad +Subproject commit e396c06c083af309bf2141515495a855f41a6b12 From 20f976fed16d306161002af113c2bc7d2252d7aa Mon Sep 17 00:00:00 2001 From: Stefan Possanner <86720346+spossann@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:58:02 +0100 Subject: [PATCH 71/83] New user guide (#200) Instead of just the userguide, we now have two sections: * Userguide * API guide The userguide contains some copy and paste examples, including * Basics * LineaMHD with DESC equil * Kinetic model: two-stream instability The doc files have been cleaned a bit (removed unused, old files). The new doc is here: https://struphy-hub.github.io/struphy/ --- doc/index.rst | 7 +- doc/markdown/vlasov-maxwell.md | 65 +- doc/sections/abstract.rst | 4 +- doc/sections/api.rst | 19 - doc/sections/api_guide.rst | 98 +++ doc/sections/appendix.rst | 209 ------ doc/sections/developers.rst | 71 --- doc/sections/examples.rst | 12 - doc/sections/fluid-equils.rst | 2 +- doc/sections/kinetic-equils.rst | 2 +- doc/sections/numerics.rst | 2 - doc/sections/propagators.rst | 2 +- doc/sections/quickstart.rst | 35 +- doc/sections/subsections-old/adding_model.rst | 100 --- .../subsections-old/boundary_conditions.rst | 128 ---- .../subsections-old/braginskii_equils.rst | 24 - .../subsections-old/braginskii_equils_sub.rst | 21 - doc/sections/subsections-old/bsplines.rst | 35 -- doc/sections/subsections-old/change_doc.rst | 20 - .../subsections-old/data_structures.rst | 35 -- doc/sections/subsections-old/diagnostics.rst | 22 - doc/sections/subsections-old/dispersions.rst | 24 - .../subsections-old/feec_basisops.rst | 9 - doc/sections/subsections-old/feec_derham.rst | 19 - doc/sections/subsections-old/feec_linalg.rst | 41 -- .../subsections-old/feec_projected_mhd.rst | 9 - .../subsections-old/feec_projectors.rst | 14 - .../subsections-old/feec_weightedmass.rst | 9 - doc/sections/subsections-old/fluid_equils.rst | 12 - .../subsections-old/fluid_equils_sub.rst | 21 - doc/sections/subsections-old/git_workflow.rst | 265 -------- .../subsections-old/initial_conditions.rst | 124 ---- doc/sections/subsections-old/inits.rst | 14 - doc/sections/subsections-old/inits_sub.rst | 60 -- doc/sections/subsections-old/io.rst | 16 - .../subsections-old/linear_algebra.rst | 40 -- doc/sections/subsections-old/mhd_equils.rst | 17 - doc/sections/subsections-old/parameters.rst | 595 ------------------ doc/sections/subsections-old/paraview.rst | 95 --- .../subsections-old/performance_tests.rst | 114 ---- .../subsections-old/pic_accumulation.rst | 32 - doc/sections/subsections-old/pic_base.rst | 43 -- doc/sections/subsections-old/pic_pushers.rst | 26 - .../subsections-old/pic_sorting_sph.rst | 25 - .../subsections-old/pic_utilities.rst | 14 - doc/sections/subsections-old/polar.rst | 30 - .../subsections-old/post_processing.rst | 29 - doc/sections/subsections-old/pproc_tools.rst | 33 - doc/sections/subsections-old/profiling.rst | 15 - doc/sections/subsections-old/struphy_cli.rst | 83 --- doc/sections/subsections-old/write_prop.rst | 294 --------- doc/sections/subsections/equils-avail.rst | 15 - .../subsections/numerics-geomFE-classes.rst | 17 - .../subsections/numerics-pic-classes.rst | 21 - doc/sections/timings.rst | 164 ----- doc/sections/tutorials.rst | 16 - doc/sections/userguide.rst | 243 ++++--- doc/sections/utilities.rst | 19 - src/struphy/geometry/mappings_kernels.py | 52 -- src/struphy/initial/base.py | 2 +- 60 files changed, 293 insertions(+), 3291 deletions(-) delete mode 100644 doc/sections/api.rst create mode 100644 doc/sections/api_guide.rst delete mode 100644 doc/sections/appendix.rst delete mode 100644 doc/sections/developers.rst delete mode 100644 doc/sections/examples.rst delete mode 100644 doc/sections/subsections-old/adding_model.rst delete mode 100644 doc/sections/subsections-old/boundary_conditions.rst delete mode 100644 doc/sections/subsections-old/braginskii_equils.rst delete mode 100644 doc/sections/subsections-old/braginskii_equils_sub.rst delete mode 100644 doc/sections/subsections-old/bsplines.rst delete mode 100644 doc/sections/subsections-old/change_doc.rst delete mode 100644 doc/sections/subsections-old/data_structures.rst delete mode 100644 doc/sections/subsections-old/diagnostics.rst delete mode 100644 doc/sections/subsections-old/dispersions.rst delete mode 100644 doc/sections/subsections-old/feec_basisops.rst delete mode 100644 doc/sections/subsections-old/feec_derham.rst delete mode 100644 doc/sections/subsections-old/feec_linalg.rst delete mode 100644 doc/sections/subsections-old/feec_projected_mhd.rst delete mode 100644 doc/sections/subsections-old/feec_projectors.rst delete mode 100644 doc/sections/subsections-old/feec_weightedmass.rst delete mode 100644 doc/sections/subsections-old/fluid_equils.rst delete mode 100644 doc/sections/subsections-old/fluid_equils_sub.rst delete mode 100644 doc/sections/subsections-old/git_workflow.rst delete mode 100644 doc/sections/subsections-old/initial_conditions.rst delete mode 100644 doc/sections/subsections-old/inits.rst delete mode 100644 doc/sections/subsections-old/inits_sub.rst delete mode 100644 doc/sections/subsections-old/io.rst delete mode 100644 doc/sections/subsections-old/linear_algebra.rst delete mode 100644 doc/sections/subsections-old/mhd_equils.rst delete mode 100644 doc/sections/subsections-old/parameters.rst delete mode 100644 doc/sections/subsections-old/paraview.rst delete mode 100644 doc/sections/subsections-old/performance_tests.rst delete mode 100644 doc/sections/subsections-old/pic_accumulation.rst delete mode 100644 doc/sections/subsections-old/pic_base.rst delete mode 100644 doc/sections/subsections-old/pic_pushers.rst delete mode 100644 doc/sections/subsections-old/pic_sorting_sph.rst delete mode 100644 doc/sections/subsections-old/pic_utilities.rst delete mode 100644 doc/sections/subsections-old/polar.rst delete mode 100644 doc/sections/subsections-old/post_processing.rst delete mode 100644 doc/sections/subsections-old/pproc_tools.rst delete mode 100644 doc/sections/subsections-old/profiling.rst delete mode 100644 doc/sections/subsections-old/struphy_cli.rst delete mode 100644 doc/sections/subsections-old/write_prop.rst delete mode 100644 doc/sections/subsections/numerics-geomFE-classes.rst delete mode 100644 doc/sections/subsections/numerics-pic-classes.rst delete mode 100644 doc/sections/timings.rst delete mode 100644 doc/sections/tutorials.rst delete mode 100644 doc/sections/utilities.rst diff --git a/doc/index.rst b/doc/index.rst index a3a5df992..70ccf4a98 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -11,7 +11,7 @@ Welcome to This website is your starting point for solving PDEs with Struphy. Check the topics below for detailed information on a subject. -The `Struphy repository on Github `_ is the launch point for the related source code. +``_ is the launch point for the related source code. The code is freely available under an `MIT license `_ - Copyright (c) 2019-2026, Struphy developers, Max Planck Institute for Plasma Physics. @@ -27,6 +27,7 @@ The code is freely available under an `MIT license `. Moreover, several other units are fixed, as described in {ref}`normalization`, namely: +In Struphy, the three basic units $\hat x$, $\hat B$ and $\hat n$ are defined by the user. Moreover, several other units are fixed, as described in {ref}`normalization`, namely: $$ \hat t, \, \hat p,\, \hat \rho,\,\hat \jmath \quad \textrm{are fixed}\,. @@ -93,7 +93,7 @@ $$ Let us summarize what we have thus far, omitting the primes in {eq}`eq:norm` for clarity: $$ -\begin{align} +\begin{aligned} & \partial_{t} f + \mathbf{v} \cdot \nabla f - \frac{1}{\varepsilon}\left(\mathbf{E} + \mathbf{v} \times \mathbf{B} \right) \cdot \frac{\partial f}{\partial \mathbf{v}} = 0 \,, \\[2mm] @@ -102,7 +102,7 @@ $$ &\frac{\partial \mathbf{B}}{\partial t} + \nabla \times \mathbf{E} = 0 \,, \\[2mm] &-\Delta \phi = \frac{\alpha^2}{\varepsilon}\left(\rho_\textrm{i0} - \int_{\mathbb{R}^3} f(t=0) \, \text{d} \mathbf{v} \right)\,,\qquad \mathbf E(t=0) = -\nabla \phi\,. -\end{align} +\end{aligned} $$ In Ampere's law we have @@ -120,7 +120,7 @@ $$ leads to the final, Struphy-normalized equations $$ -\begin{align} +\begin{aligned} & \partial_{t} f + \mathbf{v} \cdot \nabla f - \frac{1}{\varepsilon}\left(\mathbf{E} + \mathbf{v} \times \mathbf{B} \right) \cdot \frac{\partial f}{\partial \mathbf{v}} = 0 \,, \\[2mm] @@ -129,11 +129,11 @@ $$ &\frac{\partial \mathbf{B}}{\partial t} + \nabla \times \mathbf{E} = 0 \,, \\[2mm] &-\Delta \phi = \frac{\alpha^2}{\varepsilon}\left(\rho_\textrm{i0} - \int_{\mathbb{R}^3} f(t=0) \, \text{d} \mathbf{v} \right)\,,\qquad \mathbf E(t=0) = -\nabla \phi\,. -\end{align} +\end{aligned} $$ (def_spaces)= -### Definition of solution spaces +## Definition of solution spaces In Struphy, kinetic equations are always solved by means of the PIC method due to the high dimensionality of the phase space. By contrast, field (and fluid) equations are solved with the FEEC method; thus one has to decide in which of the four De Rham spaces $H^1$, $H$(curl), $H$(div) or $L^2$ each of the unkowns lives. The choice is usually informed by the grad-, curl- and div-operators appearing in the model equations, so that all terms are well-defined. For instance in Faraday's law (15), the electric field $\mathbf E$ must be in the domain of the curl operator, whereas the magnetic field $\mathbf B$ must be in the co-domain (or image) of the curl operator, which implies the natural choice $\mathbf E \in H$(curl) and $\mathbf B \in H$(div). In Struphy, there is another very important guideline: @@ -144,7 +144,7 @@ The above rule applied to the current Vlasov-Maxwell model means that Ampère's Find $(f, \mathbf E, \mathbf B, \phi) \in C^\infty \times H(\textrm{curl}) \times H(\textrm{div}) \times H^1$ such that $$ -\begin{align} +\begin{aligned} & \partial_{t} f + \mathbf{v} \cdot \nabla f - \frac{1}{\varepsilon}\left(\mathbf{E} + \mathbf{v} \times \mathbf{B} \right) \cdot \frac{\partial f}{\partial \mathbf{v}} = 0 \,, \\[2mm] @@ -155,11 +155,11 @@ $$ &\int \nabla \psi \cdot \nabla \phi\,\textrm d \mathbf x = \frac{\alpha^2}{\varepsilon}\left(\int \rho_\textrm{i0}\,\psi\,\textrm d \mathbf x - \int\int_{\mathbb{R}^3} f(t=0)\, \psi \, \text{d} \mathbf{v} \textrm d \mathbf x\right)\,, \qquad \forall \ \psi \in H^1\,, \\[4mm] &\mathbf E(t=0) = -\nabla \phi\,. -\end{align} +\end{aligned} $$ (eq:spaces) (pullback)= -### Pull-back to the logical domain +## Pull-back to the logical domain All PDE models in Struphy are discretized on the unit cube $(0,1)^3$, called the "logical domain". The mapping to the actual problem domain, a torus for instance, called "physical" or Cartesian domain and denoted by $\Omega$, is described under [Geometry](https://struphy-hub.github.io/struphy/sections/domains.html). Briefly, logical coordinates are curvi-linear and denoted by $\boldsymbol \eta \in (0, 1)^3$, whereas physical Cartesian coordinates are denoted by $\mathbf x \in \Omega$. The mapping $F: (0, 1)^3 \to \Omega, \boldsymbol \eta \mapsto \mathbf x$ is one-to-one and differentiable, with Jacobian matrix $DF: (0, 1)^3 \to \mathbb R^{3\times 3}$, metric tensor $G = DF^\top DF$ and determinant $\sqrt g = |\textrm{det} DF|$. In Struphy, only right-handed mappings with $\textrm{det} DF > 0$ are allowed. @@ -208,7 +208,7 @@ It is no coincidence that this exactly fits the correspondence between the de Rh Given the choice of spaces we made in writing down the model (17)-(21), we can now apply the appropriate pullback formulas to derive the model on the logical domain. For the kinetic distribution function $f$, the mapping $F:\boldsymbol \eta \mapsto \mathbf x$ only acts on the spatial coordinate; the velocity $\mathbf v$ is not tranformed. Hence we write $\hat f(t,\boldsymbol \eta, \mathbf v) := f(t, F(\boldsymbol \eta), \mathbf v)$ to obtain $$ -\begin{align} +\begin{aligned} & \partial_{t} \hat f + \mathbf{v} \cdot DF^{-\top} \hat\nabla \hat f - \frac{1}{\varepsilon}\left(DF^{-\top}\hat{\mathbf{E}}^1 + \mathbf{v} \times \frac{DF}{\sqrt g}\hat{\mathbf{B}}^2 \right) \cdot \frac{\partial \hat f}{\partial \mathbf{v}} = 0 \,, \\[2mm] @@ -219,7 +219,7 @@ $$ &\int \hat \nabla \hat \psi \,G^{-1} \hat \nabla \hat \phi \sqrt g\,\textrm d \boldsymbol \eta = \frac{\alpha^2}{\varepsilon}\left(\int \hat \rho_\textrm{i0}\,\hat\psi \sqrt g\,\textrm d \boldsymbol \eta - \int\int_{\mathbb{R}^3} \hat f(t=0)\, \hat \psi \sqrt g \, \text{d} \mathbf{v} \textrm d \boldsymbol \eta\right)\,, \qquad \forall \ \hat \psi \in H^1\,, \\[4mm] &\hat{\mathbf E}^1(t=0) = -\hat \nabla \hat \phi\,. -\end{align} +\end{aligned} $$ (eq:pulledback) Note here in particular that the third and fifth equation are independent of metric coefficients, which means they will have the same form for any mapping $F$ (they are indeed coordinate independent with the present choice of spaces). This immediately guarantees @@ -231,7 +231,7 @@ $$ regardless of the mapping $F$ used in the simulation. (semi_disc)= -### Semi-discretization in space +## Semi-discretization in space There are three types of terms that can appear in a Struphy discretization: @@ -276,7 +276,7 @@ $$ Let us substitute these discretizations in the model {eq}`eq:pulledback`: $$ -\begin{align} +\begin{aligned} & \partial_{t} \hat f + \mathbf{v} \cdot DF^{-\top} \hat\nabla \hat f - \frac{1}{\varepsilon}\left(DF^{-\top}\hat{\mathbf{E}}^1_h + \mathbf{v} \times \frac{DF}{\sqrt g}\hat{\mathbf{B}}^2_h \right) \cdot \frac{\partial \hat f}{\partial \mathbf{v}} = 0 \,, \\[2mm] @@ -289,7 +289,7 @@ $$ &\sum_{\mu=1, \nu = 1}^{3,3} (\mathbb G_\mu \boldsymbol \psi)^\top \left(\int \vec{\mathbf \Lambda}^1_\mu \,G^{-1} \left( \vec{\mathbf \Lambda}^1_\nu\right)^\top \sqrt g\,\textrm d \boldsymbol \eta \right) \mathbb G_\nu \boldsymbol \phi = \frac{\alpha^2}{\varepsilon}\boldsymbol \psi^\top\left( \int \hat \rho_\textrm{i0}\,\mathbf \Lambda^0 \sqrt g\,\textrm d \boldsymbol \eta - \int\int_{\mathbb{R}^3} \hat f(t=0)\, \mathbf \Lambda^0 \sqrt g \, \text{d} \mathbf{v} \textrm d \boldsymbol \eta\right)\,, \qquad \forall \ \boldsymbol \psi \in \mathbb R^{N_0}\,, \\[4mm] &\sum_{\mu=1}^3 \mathbf (\mathbf e_\mu + \mathbb G_\mu \boldsymbol \phi)^\top \vec{\mathbf \Lambda}^1_\mu = 0\,. -\end{align} +\end{aligned} $$ There appear a couple of mass matrices that are already [predefined in Struphy](https://struphy-hub.github.io/struphy/sections/subsections-old/feec_weightedmass.html), for any mapping: @@ -305,7 +305,7 @@ $$ These are 3x3 block matrices (implemented as [BlockLinearOperators](https://github.com/struphy-hub/psydac-for-struphy/blob/d9d81cb2104cbff990e129c8785cf4a7cb2dc54b/psydac/linalg/block.py#L492)), where the blocks are indexed by $(\mu, \nu)$. We remark that the weak equations must hold for any choice of $\mathbf f = (\mathbf f_\mu)_{\mu=1}^3 \in \mathbb R^{N_1}$ and $ \boldsymbol \psi \in \mathbb R^{N_0}$, respectively, which means that these can be factored out to lead to a system of equations. Besides, all basis functions are linearly independent such that the coefficients in the third and fifth equation must vanish separately. This leads to the much more compact notation $$ -\begin{align} +\begin{aligned} & \partial_{t} \hat f + \mathbf{v} \cdot DF^{-\top} \hat\nabla \hat f - \frac{1}{\varepsilon}\left(DF^{-\top}\hat{\mathbf{E}}^1_h + \mathbf{v} \times \frac{DF}{\sqrt g}\hat{\mathbf{B}}^2_h \right) \cdot \frac{\partial \hat f}{\partial \mathbf{v}} = 0 \,, \\[2mm] @@ -316,7 +316,7 @@ $$ &\mathbb G^\top \mathbb M^1\mathbb G \boldsymbol \phi = \frac{\alpha^2}{\varepsilon}\left( \int \hat \rho_\textrm{i0}\,\mathbf \Lambda^0 \sqrt g\,\textrm d \boldsymbol \eta - \int\int_{\mathbb{R}^3} \hat f(t=0)\, \mathbf \Lambda^0 \sqrt g \, \text{d} \mathbf{v} \textrm d \boldsymbol \eta\right)\,, \\[4mm] &\mathbf e + \mathbb G\boldsymbol \phi = 0\,. -\end{align} +\end{aligned} $$ (eq:compact) The next step is to discretize the kinetic equation by means of {ref}`particle_discrete`, which leads us to **particle equations of motion**. For this, the volume form $\hat f^\textrm{vol} := \hat f \sqrt g$ in "logical" phase space is such that it includes the measure of the phase space, i.e. the Jacobian determinant arising from coordinate transformations in phase space. The volume form is then aproximated by a sum of Dirac delta functions, @@ -330,15 +330,14 @@ $$ (eq:pic) where $\boldsymbol \eta_p(t)$ and $\mathbf v_p(t)$ satisfy the characteristics of the kinetic transport equation in {eq}`eq:compact`, that is $$ -\begin{align} +\begin{aligned} \dot{\boldsymbol \eta}_p &= DF^{-1}(\boldsymbol \eta_p) \mathbf v_p\,, \\[2mm] \dot{\mathbf v}_p &= -\frac{1}{\varepsilon}\left(DF^{-\top}(\boldsymbol \eta_p)\hat{\mathbf{E}}^1_h (\boldsymbol \eta_p) + \mathbf{v} \times \frac{DF}{\sqrt g} (\boldsymbol \eta_p)\hat{\mathbf{B}}^2_h (\boldsymbol \eta_p) \right)\,. -\end{align} +\end{aligned} $$ Since the number of particles in PIC simulations is usually very large (on the order of millions or even billions), an efficient solution loop over $p$ (sometimes also $k$ is used as the particle index) is absolutely mandatory here. Therefore, specific [pusher kernels](https://github.com/struphy-hub/struphy/blob/devel/src/struphy/pic/pushing/pusher_kernels.py) must be written for each particle pushing step, which are then accelerated (compiled) with Pyccel (see our [Tl:dr](https://struphy-hub.github.io/struphy/sections/abstract.html)) to enable C- or Fortran execution speed. In Struphy models, the pusher kernels are integrated via the [Pusher class](https://github.com/struphy-hub/struphy/blob/devel/src/struphy/pic/pushing/pusher.py) that provides some syntactic sugar for calling the kernels. -See {ref}`prop_kernels` for more details. Now that we know how to discretize the kinetic equation by means of a Lagrangian particle method, it remains to tackle the right-hand sides of Ampère's law and of Poisson's equation in {eq}`eq:compact`. In the latter, there is the source term @@ -417,7 +416,7 @@ $$ with $\mathbf v = (\mathbf v_p)_{p=0}^{N-1} \in \mathbb R^{N\times 3}$. In summary, this leads to the following semi-discrete Vlasov-Maxwell system, which is a coupled, nonlinear system of ordinary differential equations (ODEs): $$ -\begin{align} +\begin{aligned} \dot{\boldsymbol \eta} &= \bar{DF}^{-1} \mathbf v\,, \\[2mm] \dot{\mathbf v} &= -\frac{1}{\varepsilon}\left( \bar{DF}^{-\top} (\mathbb L^1)^\top \mathbf e + \bar{\mathbf B}^2_\times\mathbf{v} \right)\,, @@ -429,7 +428,7 @@ $$ &\mathbb G^\top \mathbb M^1\mathbb G \boldsymbol \phi = \frac{\alpha^2}{\varepsilon}\left( \boldsymbol \rho_\textrm{i0} - \mathbb L^0\mathbf w\right)\,, \\[4mm] &\mathbf e + \mathbb G\boldsymbol \phi = 0\,. -\end{align} +\end{aligned} $$ (eq:semidisc) where we introduced the short-hand notation for the operator @@ -439,7 +438,7 @@ $$ $$ (time_disc)= -### Time discretization +## Time discretization In Struphy, the time discretization is usually informed by the Hamiltonian structure of the model equations. We refer to the time discretization of the simpler [electrostatic Vlasov-Poisson system](https://gitlab.mpcdf.mpg.de/struphy/struphy-projects/-/blob/main/running-projects/2024_VlasovPoissonInhomB.md?ref_type=heads#time-discretization) for a brief introduction. In case of a non-ideal model with dissipation, there usually is an ideal "core model" that is Hamiltonian if the dissipative terms are neglected, which can inform the basic time discretization. The Vlasov-Maxwell system considered here has such a Hamiltonian structure, see for instance [Kraus et al.](https://www.cambridge.org/core/journals/journal-of-plasma-physics/article/gempic-geometric-electromagnetic-particleincell-methods/C32D97F1B5281878F094B7E5075D291A). In most cases, the Hamiltonian structure can be retrieved after semi-discretization is space, here thus from the system {eq}`eq:semidisc`. The methodology goes as follows: @@ -485,7 +484,6 @@ $$ We can thus write {eq}`eq:semidisc` as $$ -\begin{equation} \begin{pmatrix} \dot{\boldsymbol \eta} \\ @@ -514,7 +512,6 @@ $$ \\ \mathbb M^2 \mathbf b \end{pmatrix}\,. - \end{equation} $$ (eq:hamilton) Quite magically, our space discretization led to the skew symmetric Poisson matrix $\mathbb J(\mathbf Z)$ in $\dot{\mathbf Z} = \mathbb J(\mathbf Z)\nabla H(\mathbf Z)$. The skew symmetry of this matrix guarantees energy conservation, because @@ -532,7 +529,7 @@ $$ with $$ - \begin{align} + \begin{aligned} \mathbb J_1 &= \begin{pmatrix} 0 & \frac{1}{\alpha^2} \bar{DF}^{-1}\bar{\mathbf w}^{-1} & 0 & 0 \\ @@ -572,7 +569,7 @@ $$ \\ 0 & 0 & -\mathbb C (\mathbb M^1)^{-1} & 0 \end{pmatrix}\,. - \end{align} + \end{aligned} $$ (eq:Js) These four split Poisson matrices define the four substeps of the time splitting algorithm, called {ref}`propagators` in Struphy. These are maps $\Phi_t^{n}:\mathbf Z_0 \mapsto \mathbf Z(t)$ defined via the solution of @@ -593,7 +590,7 @@ $$ \mathbf Z(t + \Delta t) = (\Phi_{\Delta t/2}^{4} \circ \Phi_{\Delta t/2}^{3} \circ \Phi_{\Delta t/2}^{2} \circ \Phi_{\Delta t}^{1} \circ \Phi_{\Delta t/2}^{2} \circ \Phi_{\Delta t/2}^{3} \circ \Phi_{\Delta t/2}^{4}) \mathbf Z(t)\,. $$ -Once the propagators have been defined and added to a {ref}`struphy_model`, Struphy performs the compositions automatically; the user can choose the splitting algorithm in the {ref}`parameter file