├── .github └── workflows │ ├── publish-to-pypi.yml │ ├── unix.yml │ └── windows.yml ├── .gitignore ├── .readthedocs.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LEGAL.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── RELEASING.md ├── binder └── requirements.txt ├── docs ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── api_reference │ ├── api_reference.rst │ ├── generic_interface.rst │ ├── lpa_diagnostics.rst │ └── particle_tracking.rst │ ├── conf.py │ ├── index.rst │ └── tutorials │ ├── 1_Introduction-to-the-API.ipynb │ ├── 2_Specific-field-geometries.ipynb │ ├── 3_Introduction-to-the-GUI.ipynb │ ├── 4_Particle_selection.ipynb │ ├── 5_Laser-plasma_tools.ipynb │ └── tutorials.rst ├── openpmd_viewer ├── __init__.py ├── __version__.py ├── addons │ ├── __init__.py │ └── pic │ │ ├── __init__.py │ │ └── lpa_diagnostics.py ├── notebook_starter │ ├── Template_notebook.ipynb │ └── openPMD_notebook └── openpmd_timeseries │ ├── __init__.py │ ├── data_order.py │ ├── data_reader │ ├── __init__.py │ ├── data_reader.py │ ├── h5py_reader │ │ ├── __init__.py │ │ ├── field_reader.py │ │ ├── params_reader.py │ │ ├── particle_reader.py │ │ └── utilities.py │ └── io_reader │ │ ├── __init__.py │ │ ├── field_reader.py │ │ ├── params_reader.py │ │ ├── particle_reader.py │ │ └── utilities.py │ ├── field_metainfo.py │ ├── interactive.py │ ├── main.py │ ├── numba_wrapper.py │ ├── particle_tracker.py │ ├── plotter.py │ └── utilities.py ├── opmd_viewer └── __init__.py ├── requirements.txt ├── setup.py └── tests └── test_tutorials.py /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to PyPI 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build-n-publish: 9 | name: Build and publish package to PyPI 10 | runs-on: ubuntu-latest 11 | permissions: 12 | # IMPORTANT: this permission is mandatory for trusted publishing 13 | id-token: write 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Python 3.11 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: 3.11 20 | - name: Install pypa/build 21 | run: >- 22 | python3 -m 23 | pip install 24 | build 25 | --user 26 | - name: Build a binary wheel and a source tarball 27 | run: >- 28 | python3 -m 29 | build 30 | --sdist 31 | --wheel 32 | --outdir dist/ 33 | . 34 | - name: Publish package distribution to PyPI 35 | uses: pypa/gh-action-pypi-publish@release/v1 36 | -------------------------------------------------------------------------------- /.github/workflows/unix.yml: -------------------------------------------------------------------------------- 1 | name: Unix 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | python-version: ["3.8", "3.9", "3.10", "3.11"] 10 | os: [ubuntu-20.04] 11 | include: 12 | - python-version: 3.9 13 | os: macos-latest 14 | runs-on: ${{ matrix.os }} 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: conda-incubator/setup-miniconda@v3 19 | with: 20 | auto-update-conda: true 21 | python-version: ${{ matrix.python-version }} 22 | channels: conda-forge,defaults 23 | 24 | - shell: bash -eo pipefail -l {0} 25 | name: Install dependencies 26 | run: | 27 | if [ "${{ matrix.python-version }}" != "3.8" ]; then 28 | conda install --yes cython numpy scipy h5py openpmd-api matplotlib jupyter pytest pyflakes python=${{ matrix.python-version }} python-wget 29 | else 30 | conda install --yes cython numpy scipy h5py matplotlib jupyter pytest pyflakes python=${{ matrix.python-version }} python-wget 31 | fi; 32 | python setup.py install 33 | 34 | - shell: bash -eo pipefail -l {0} 35 | name: pyflakes 36 | run: python -m pyflakes openpmd_viewer 37 | - shell: bash -eo pipefail -l {0} 38 | name: Test 39 | run: python -m pytest tests 40 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | win_pip: 7 | name: Windows 8 | strategy: 9 | matrix: 10 | python-version: [3.10] 11 | os: [windows-latest] 12 | runs-on: ${{ matrix.os }} 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install cython numpy scipy h5py openpmd-api matplotlib jupyter pytest pyflakes wget; 19 | python setup.py install 20 | - name: Test 21 | run: python -m pytest tests 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.DS_Store 3 | *.egg 4 | build/ 5 | dist/ 6 | openpmd_viewer.egg-info/ 7 | .cache/ 8 | .eggs/ 9 | .vs/ 10 | __pycache__ 11 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/source/conf.py 17 | 18 | # Optional but recommended, declare the Python requirements required 19 | # to build your documentation 20 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 21 | python: 22 | install: 23 | - requirements: docs/requirements.txt 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log / Release Log for openPMD-viewer 2 | 3 | ## 1.10.0 4 | 5 | * use `%matplotlib widget` by default by @BenWibking in https://github.com/openPMD/openPMD-viewer/pull/407 6 | * BP4 Empty Skip: Inline Comment by @ax3l in https://github.com/openPMD/openPMD-viewer/pull/403 7 | * Close #402 Fix deprecated ipython command by @RemiLehe in https://github.com/openPMD/openPMD-viewer/pull/410 8 | 9 | **Full Changelog**: https://github.com/openPMD/openPMD-viewer/compare/1.9.0...1.10.0 10 | 11 | ## 1.9.0 12 | 13 | * When passing `t`, choose the closest iteration by @RemiLehe in https://github.com/openPMD/openPMD-viewer/pull/347 14 | * README: Remove Travis-CI Badges by @ax3l in https://github.com/openPMD/openPMD-viewer/pull/369 15 | * Adding attributes to the FieldMetaInformation object by @juliettepech in https://github.com/openPMD/openPMD-viewer/pull/372 16 | * Sphinx documentation by @RemiLehe in https://github.com/openPMD/openPMD-viewer/pull/303 17 | * Add readthedoc configuration by @RemiLehe in https://github.com/openPMD/openPMD-viewer/pull/375 18 | * setup.py: no upper version of openPMD-api by @ax3l in https://github.com/openPMD/openPMD-viewer/pull/378 19 | * Docs: correct 'get_mean_gamma' description in tutorials by @IlianCS in https://github.com/openPMD/openPMD-viewer/pull/379 20 | * Do not use pyflakes for version 3.7 by @RemiLehe in https://github.com/openPMD/openPMD-viewer/pull/385 21 | * Impose that user always passes iteration or t by @RemiLehe in https://github.com/openPMD/openPMD-viewer/pull/383 22 | * Docs: Improve tutorials by adding basic analysis functions by @IlianCS in https://github.com/openPMD/openPMD-viewer/pull/382 23 | * Improve get_laser_waist by @soerenjalas in https://github.com/openPMD/openPMD-viewer/pull/359 24 | * Python 3.8+ by @ax3l in https://github.com/openPMD/openPMD-viewer/pull/387 25 | * Update version number and CHANGELOG.md by @RemiLehe in https://github.com/openPMD/openPMD-viewer/pull/386 26 | * Add all record attributes to `FieldMetaInformation` by @AngelFP in https://github.com/openPMD/openPMD-viewer/pull/390 27 | * Support complex data in `thetaMode` geometry by @AngelFP in https://github.com/openPMD/openPMD-viewer/pull/389 28 | * fix get_data for weighting in momentum reader by @PrometheusPi in https://github.com/openPMD/openPMD-viewer/pull/393 29 | * Simplify names of radial particle components by @AngelFP in https://github.com/openPMD/openPMD-viewer/pull/392 30 | * Close #396 warning when reading particle data by @RemiLehe in https://github.com/openPMD/openPMD-viewer/pull/398 31 | * Support reading of 'rt' lasy files by @RemiLehe in https://github.com/openPMD/openPMD-viewer/pull/371 32 | 33 | **Full Changelog**: https://github.com/openPMD/openPMD-viewer/compare/1.7.0...1.9.0 34 | 35 | ## 1.8.0 36 | 37 | - The functions `get_field` and `get_particle` now require `iteration` or `t` 38 | to be passed (instead of using a default iteration when none was provided). 39 | (See [#383](https://github.com/openPMD/openPMD-viewer/pull/383)) 40 | 41 | - The function `get_laser_waist` is more robust and does not crash when the 42 | laser field is 0. 43 | (See [#359](https://github.com/openPMD/openPMD-viewer/pull/359)) 44 | 45 | - The `FieldMEtaInformation` object has new attributes `time` and `iteration`. 46 | (See [#372](https://github.com/openPMD/openPMD-viewer/pull/372)) 47 | 48 | - The docstring of `get_mean_gamma` has been updated 49 | (See [#379](https://github.com/openPMD/openPMD-viewer/pull/379)) 50 | and the attributes `ts.t` and `ts.iterations` are now shown in the tutorials 51 | (See [#382](https://github.com/openPMD/openPMD-viewer/pull/382)) 52 | 53 | ## 1.7.0 54 | 55 | This release includes a few improvements: 56 | 57 | - The function `get_laser_waist` is more robust: it does not automatically 58 | assume that the `z` axis is the last axis of the data. In addition, the user 59 | can now specify the laser propagation direction. (The default is `z`.) 60 | (See [#345](https://github.com/openPMD/openPMD-viewer/pull/345)) 61 | 62 | - The handling of `unitSI` is now more robust. (See [#363](https://github.com/openPMD/openPMD-viewer/pull/363)) 63 | 64 | ## 1.6.0 65 | 66 | This release adds a few features: 67 | 68 | - `openPMD-viewer` can now read complex datasets (See [#353](https://github.com/openPMD/openPMD-viewer/pull/353)) 69 | 70 | - Avoid errors in LPA diagnostics in the absence of selected particles (See [#358](https://github.com/openPMD/openPMD-viewer/pull/358)) 71 | 72 | ## 1.5.0 73 | 74 | This release fixes a few miscellaneous bugs: 75 | 76 | - Better 3D reconstruction for `theta=None` (See [#344](https://github.com/openPMD/openPMD-viewer/pull/344)) 77 | 78 | - Better support for ADIOS data (See [#355](https://github.com/openPMD/openPMD-viewer/pull/355)) 79 | 80 | - Support for group-based encoding (See [#346](https://github.com/openPMD/openPMD-viewer/pull/346)) 81 | 82 | ## 1.4.0 83 | 84 | This new release introduces several improvements: 85 | 86 | - The changes introduced in 1.3.0 caused a major slowdown when reading certain 87 | types of data. This has been fixed in this new release. (See [#340](https://github.com/openPMD/openPMD-viewer/pull/340) for more details.) 88 | 89 | - `openPMD-viewer` now supports `thetaMode` geometry with data written using 90 | `r` as the fastest index (as written by e.g. [WarpX](https://github.com/ECP-WarpX/WarpX)) 91 | in addition to the previously supported data format which used `z` as the fastest index 92 | (as written by e.g. [fbpic](https://github.com/fbpic/fbpic)). (See 93 | [337](https://github.com/openPMD/openPMD-viewer/pull/337)) 94 | 95 | - `openPMD-viewer` will raise an exception if the user asks for an iteration 96 | that is not part of the dataset (instead of printing a message and reverting 97 | to the first iteration, which can be confusing) (See [336](https://github.com/openPMD/openPMD-viewer/pull/336)) 98 | 99 | ## 1.3.0 100 | 101 | This new release introduces preliminary support for MR datasets 102 | (see [#332](https://github.com/openPMD/openPMD-viewer/pull/332)). 103 | 104 | ## 1.2.0 105 | 106 | This new release introduces several bug-fixes and miscellaneous features: 107 | 108 | - There is a new function `get_energy_spread` that returns the energy 109 | spread of the beam. This is partially redundant with `get_mean_gamma`, 110 | which is kept for backward compatibility. 111 | (see [#304](https://github.com/openPMD/openPMD-viewer/pull/304) 112 | and [#317](https://github.com/openPMD/openPMD-viewer/pull/317)) 113 | 114 | - The 3D field reconstruction from `ThetaMode` data now has an option 115 | `max_resolution_3d` that limits the resolution of the final 3D array. 116 | This is added in order to limit the memory footprint of this array. 117 | (see [#307](https://github.com/openPMD/openPMD-viewer/pull/307)) 118 | The 3D reconstruction is now also more accurate, thanks to the implementation 119 | of linear interpolation. 120 | (see [#311](https://github.com/openPMD/openPMD-viewer/pull/311)) 121 | 122 | - A bug that affected reading `ThetaMode` data with the `openpmd-api` backend 123 | has been fixed. (see [#313](https://github.com/openPMD/openPMD-viewer/pull/313)) 124 | 125 | - A bug that affected `get_laser_waist` has been fixed: 126 | (see [#320](https://github.com/openPMD/openPMD-viewer/pull/320)) 127 | 128 | ## 1.1.0 129 | 130 | This new release introduces the option to read `openPMD` files with different backends. In addition to the legacy `h5py` backend (which can read only HDF5 openPMD file), `openPMD-viewer` now has the option to use the `openpmd-api` backend (which can read both HDF5 and ADIOS openPMD files). Because the `openpmd-api` backend is thus more general, it is selected by default if available (i.e. if installed locally). 131 | The user can override the default choice, by passing the `backend` argument when creating an `OpenPMDTimeSeries` object, and check which backend has been chosen by inspecting the `.backend` attribute of this object. 132 | 133 | In addition, several smaller changes were introduced in this PR: 134 | - The method `get_laser_envelope` can now take the argument `laser_propagation` in order to support lasers that do not propagates along the `z` axis. 135 | - `openPMD-viewer` can now properly read `groupBased` openPMD files (i.e. files that contain several iterations) [#301](https://github.com/openPMD/openPMD-viewer/pull/301). 136 | - Users can now pass arrays of ID to the `ParticleTracker` [#283](https://github.com/openPMD/openPMD-viewer/pull/283) 137 | 138 | ## 1.0.1 139 | 140 | This is a bug-fix release. 141 | 142 | - Unreadable files are now skipped (instead of crashing the whole timeseries) ; see [#262](https://github.com/openPMD/openPMD-viewer/pull/262). 143 | 144 | - A bug related to units (microns vs meters) was fixed in `get_emittance` and `get_current` (see [#276](https://github.com/openPMD/openPMD-viewer/pull/276)) 145 | 146 | - The quick-start notebook (`openPMD-visualization`) raised a warning saying 147 | `Notebook validation failed` in some cases. This was fixed (see [#274](https://github.com/openPMD/openPMD-viewer/pull/274)) 148 | 149 | - When using the option `plot=True` with Python 2, openPMD-viewer crashed. This is now fixed (see [#271](https://github.com/openPMD/openPMD-viewer/pull/271)). 150 | 151 | ## 1.0.0 152 | 153 | This version introduces major changes and breaks backward compatibility. 154 | 155 | Here is a list of the changes: 156 | - The import statement now uses `openpmd_viewer` instead of `opmd_viewer`, e.g. 157 | ``` 158 | from openpmd_viewer import OpenPMDTimeSeries 159 | ``` 160 | - For consistency, `ts.get_particle` now return particle positions in meters, 161 | instead of microns. For instance, in the code below, `x`, `y`, `z` will be in 162 | meters 163 | ``` 164 | x, y, z = ts.get_particle(['x', 'y', 'z'], iteration=1000) 165 | ``` 166 | - In `ts.get_field`, slicing can now be done in several directions, and for 167 | 1d, 2d, 3d, and circ geometries. As a consequence, this breaks backward 168 | compatibility for 3d field: 169 | ```get_field(field=field, coord=coord, iteration=iteration)``` 170 | used to return the central slice along `y` while it now returns the full 3d field. 171 | In addition, the name of the argument of `get_field` that triggers slicing 172 | has been changed from `slicing_dir` to `slice_across` (and `slicing` has been 173 | changed to `slice_relative_position`). 174 | - `openPMD-viewer` does not rely on Cython anymore. Instead, it uses `numba` 175 | for functions that perform a substantial amount of computation. 176 | - A new function (`ts.iterate`) was introduced in order to quickly apply a 177 | given function to all iterations of a time series. See the docstring of 178 | `ts.iterate` for more information. 179 | - The function `get_laser_envelope` does not support the argument `index` anymore 180 | (which was effectively used in order to perform slicing). Instead, users should use 181 | the argument `slicing_dir`. In addition, `get_laser_envelope` now supports the 182 | argument `plot_range`. 183 | - The function `get_laser_waist` does not support the agument `slicing_dir` anymore. 184 | 185 | ## 0.9.0 186 | 187 | This release adds two features: 188 | - Improved calculation of the laser envelope, using the Hilbert transform. 189 | - Reconstruction of full 3D field from a quasi-3D dataset, when passing `theta=None`. 190 | 191 | ## 0.8.2 192 | 193 | This is a bug-fix release. It allows the slider to work properly in JupyterLab, 194 | by using the `%matplotlib widget` magic. 195 | 196 | ## 0.8.1 197 | 198 | This version includes minor improvements to the viewer: 199 | - (Experiemental) support for Windows users 200 | - In the interactive Jupyter GUI, the user can now select the scale of the vertical axis. 201 | - The function `get_emittance` has more options (including calculation of the slice emttance) 202 | - The default `openPMD_notebook` now avoids warning messages about matplotlib inline, which used to occur even though `%matplotlib notebook` was used. 203 | 204 | Many thanks to @MaxThevenet and @AngelFP for their contributions to this release! 205 | 206 | ## 0.8 207 | 208 | This version introduces several improvements to the viewer: 209 | - The ability to read files that contain fields in different geometries 210 | (e.g. 3D fields and 2D slices). 211 | - Better support for files that do not contain mesh (or do not contain 212 | particles), including support for the openPMD 1.1.0 standard. 213 | - Cloud-In-Cell deposition in histograms. 214 | - Better handling of `%matplotlib notebook` for newer version of jupyter. 215 | 216 | ## 0.7.1 217 | 218 | This version adds better support, when the local installation of matplotlib 219 | has issues: 220 | 221 | - The `LpaDiagnostics` can now work without matplotlib if needed. 222 | - The `MacOSX` matplotlib backend is now avoided, since there can be issues 223 | when using it in the latest version of Jupyter. 224 | 225 | ## 0.7.0 226 | 227 | This version improves support for `ipywidgets` version 7, especially in 228 | the layout of the slider. 229 | 230 | In addition, with this version of `openPMD-viewer`, `matplotlib` is not a 231 | strict requirement anymore. This allows lighter installation for users that 232 | need `openPMD-viewer` only as a data reader. 233 | 234 | Finally, the calculation of the laser envelope in 2D has been improved 235 | (see [PR 170](https://github.com/openPMD/openPMD-viewer/pull/170)). Note 236 | that the function `wstd` (which is not documented in the tutorial, but 237 | which some users might still use) has been renamed to `w_std`. 238 | 239 | ## 0.6.0 240 | 241 | This version improves the layout of the Jupyter GUI and allows the user to 242 | select a particular region of the plots through this GUI. 243 | 244 | In addition, support for massless particle (e.g. photons) was added. In this 245 | case, the momenta are returned in kg.m.s^-1, instead of using the 246 | dimensionless momenta. 247 | 248 | ## 0.5.4 249 | 250 | This is version `0.5.4` of openPMD-viewer. 251 | 252 | It adds support for Python 3.4 (which erroneously dropped in the past). 253 | 254 | ## 0.5.3 255 | 256 | This is version `0.5.3` of openPMD-viewer. 257 | 258 | It corrects some of the issues with the size of boxes and widgets in the 259 | interactive slider. In addition, the iteration number is now read from 260 | the hdf5 metadata, and not the name of the file. 261 | 262 | ## 0.5.2 263 | 264 | This is version `0.5.2` of openPMD-viewer. 265 | 266 | It fixes some of the installation issues associated with Cython. 267 | 268 | ## 0.5.1 269 | 270 | This is version `0.5.1` of openPMD-viewer. 271 | 272 | It corrects a minor bug in the dependencies of the package. 273 | 274 | ## 0.5.0 275 | 276 | This is version `0.5.0` of openPMD-viewer. 277 | 278 | This new version includes the `ParticleTracker` object, which allows user to track individual particles across different iterations, provided that their `id` is stored in the openPMD file. Also, starting with this version, openPMD-viewer now depends on `Cython`. 279 | 280 | For more information on how to use the `ParticleTracker`, see the tutorial notebook. 281 | 282 | ## 0.4.0 283 | 284 | This is version `0.4.0` of openPMD-viewer. 285 | 286 | This new version includes: 287 | - support for 1D data 288 | - an additional option `use_field_mesh` when plotting the particle. When set 289 | to `True`, this option uses information from the field mesh to choose the parameters of the particle histograms (esp. the bins). This is useful in order to avoid plotting/binning artifacts (aliasing) when the particles are evenly spaced. 290 | 291 | In addition, the module `openpmd_viewer` now has an attribute `__version__`. 292 | 293 | ## 0.3.3 294 | 295 | This is version `0.3.3` of openPMD-viewer. 296 | 297 | This version fixed a bug with the executable `openPMD_notebook`. More precisely, the executable was not installed, when using `pip` or `conda`. In addition, it was failing with Python 3. 298 | 299 | ## 0.3.2 300 | 301 | This is version `0.3.2` of openPMD-viewer. The following changes were introduced: 302 | 303 | - The conda recipe in `conda_recipe/` was simplified and streamlined. 304 | - The documentation now explains how to install openPMD-viewer with `conda`, the instructions to release the package was put into a document `RELEASING.md`. 305 | - A file `MANIFEST.in` was added, to avoid issues with pip and Python 3. 306 | 307 | ## 0.3.1 308 | 309 | This is version `0.3.1` of openPMD-viewer. This version introduces minor changes in the way the tests are run in `setup.py`. The aim of these changes are to prepare a conda release. 310 | 311 | ## 0.3.0 312 | 313 | This is version `0.3.0` of openPMD-viewer. This version mainly adapts the interactive GUI so that it can be used with the newer version of `ipwidgets` (`ipywidgets 5.0`), while still being compatible with previous versions of `ipwidgets`. A number of other minor changes have been introduced: 314 | 315 | - In the method `get_particle`, the argument `species` is now optional in the case where there is only one species. 316 | - A number of methods in the LPA addons (`LpaDiagnostics` class) now have an optional argument `plot`, which allows to directly plot the data. 317 | 318 | ## 0.2.0 319 | 320 | This is version `0.2.0` of openPMD-viewer. A number of minor changes and fixes have been made in order to make the package more general and to prepare it for a PyPI release. Here are the main changes: 321 | 322 | - Support for the deprecated widget package `IPython.html` has been dropped. From now on, users need to install the widget package `ipywidgets`, for the GUI to work. 323 | - The initialization of an `OpenPMDTimeSeries` object can now be made faster by setting the optional argument `check_all_files` to `False`. 324 | - The data reader can now support `macroWeighted` quantities. As consequence, output files from [PIConGPU](https://github.com/ComputationalRadiationPhysics/picongpu) can now be correctly read. 325 | - The package does not assume anymore that all species contain the same particle quantities. For instance, the package will support a file that contains the positions of ions, and the positions, momenta and weighting of electrons. As part of this, the attribute `OpenPMDTimeSeries.avail_ptcl_quantities` has been replaced by a dictionary `OpenPMDTimeSeries.avail_record_components`. 326 | - This release introduces automatic PEP8 verification as part of the automatic tests that are run on Travis CI (see CONTRIBUTING.md). 327 | - The evaluation of the waist and duration of the laser is now based on Gaussian fit of the transverse and longtudinal profile respectively. 328 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the openPMD-viewer 2 | 3 | ## How to contribute 4 | 5 | ### Forking the repository 6 | 7 | In order to contribute, please fork the [main repository](https://github.com/openPMD/openPMD-viewer): 8 | 9 | - Click 'Fork' on the page of the main repository, in order to create a personal copy of this repository on your Github account. 10 | 11 | - Clone this copy to your local machine: 12 | ``` 13 | git clone git@github.com:/openPMD-viewer.git 14 | ``` 15 | 16 | ### Implementing a new feature and adding it to the main repository 17 | 18 | - Switch to the development branch 19 | ``` 20 | git checkout dev 21 | ``` 22 | and install it 23 | ``` 24 | python -m pip wheel . 25 | python -m pip install *whl 26 | ``` 27 | 28 | - Start a new branch from the development branch, in order to 29 | implement a new feature. (Choose a branch name that is representative of the 30 | feature that you are implementing, e.g. `add-latex-labels` or 31 | `fix-matplotlib-errors`) 32 | ``` 33 | git checkout -b 34 | ``` 35 | 36 | - Start coding. When your changes are ready, commit them. 37 | ``` 38 | git add 39 | git commit 40 | ``` 41 | 42 | - Synchronize your branch with the main repository. (It may have 43 | changed while you where implementing local changes.) Resolve merging 44 | issues if any, and commit the corresponding changes. 45 | ``` 46 | git pull git@github.com:openPMD/openPMD-viewer.git dev 47 | ``` 48 | 49 | - Test and check your code: 50 | - Use [pyflakes](https://pypi.python.org/pypi/pyflakes) to detect any potential bug. 51 | ``` 52 | cd openPMD-viewer/ 53 | pyflakes openpmd_viewer 54 | ``` 55 | - Make sure that the tests pass (please install `wget` and `jupyter` before running the tests: `pip install wget jupyter`) 56 | ``` 57 | python -m pip wheel . 58 | python -m pip install *whl matplotlib jupyter 59 | python -m pytest tests 60 | ``` 61 | (Be patient: the `test_tutorials.py` can take approx. 20 seconds if 62 | you already downloaded the example openPMD files that are required 63 | in the tutorials. On the other hand, it can take several minutes if 64 | you have not previously downloaded these files.) 65 | 66 | - Push the changes to your personal copy on Github 67 | ``` 68 | git push -u origin 69 | ``` 70 | 71 | - Go on your Github account and create a pull request between **your 72 | new feature branch** and the **dev branch of the main 73 | repository**. Please add some text to the pull request to describe 74 | what feature you just implemented and why. Please also make sure that 75 | the automated tests (on Github) return no error. 76 | 77 | ## Style and conventions 78 | 79 | - Features that **modify** or **improve** the `OpenPMDTimeSeries` object 80 | should be implemented in the 81 | `openpmd_viewer/opempmd_timeseries` folder. Features that **build upon** the 82 | `OpenPMDTimeSeries` object to create domain-specific analysis tools 83 | (e.g. laser diagnostics for PIC simulations) should be implemented in 84 | the `openpmd_viewer/addons` folder. 85 | 86 | - Document the functions and classes that you write, by using a 87 | [docstring](https://www.python.org/dev/peps/pep-0257/). List the 88 | parameters and describe what the functions return, as in this 89 | example: 90 | ```python 91 | def get_data( dset, i_slice=None, pos_slice=None ) : 92 | """ 93 | Extract the data from a (possibly constant) dataset 94 | Slice the data according to the parameters i_slice and pos_slice 95 | 96 | Parameters: 97 | ----------- 98 | dset: an h5py.Dataset or h5py.Group (when constant) 99 | The object from which the data is extracted 100 | 101 | i_slice: int, optional 102 | The index of the slice to be taken 103 | 104 | pos_slice: int, optional 105 | The position at which to slice the array 106 | When None, no slice is performed 107 | 108 | Returns: 109 | -------- 110 | An np.ndarray (non-constant dataset) or a single double (constant dataset) 111 | """ 112 | ``` 113 | Don't use documenting styles like `:param:`, `:return:`, or 114 | `@param`, `@return`, as they are less readable. 115 | 116 | 117 | - Lines of code should **never** have [more than 79 characters per line](https://www.python.org/dev/peps/pep-0008/#maximum-line-length). 118 | 119 | - Names of variables, functions should be lower case (with underscore 120 | if needed: e.g. `get_field`). Names for classes should use the 121 | CapWords convention (e.g. `DataReader`). See [this page](https://www.python.org/dev/peps/pep-0008/#prescriptive-naming-conventions) for more details. 122 | -------------------------------------------------------------------------------- /LEGAL.txt: -------------------------------------------------------------------------------- 1 | openPMD-viewer, Copyright (c) 2015, The Regents of the University of 2 | California, through Lawrence Berkeley National Laboratory (subject to receipt 3 | of any required approvals from the U.S. Dept. of Energy). All rights reserved. 4 | 5 | If you have questions about your rights to use or distribute this software, 6 | please contact Berkeley Lab's Technology Transfer Department at TTD@lbl.gov. 7 | 8 | NOTICE. This software is owned by the U.S. Department of Energy. As such, the 9 | U.S. Government has been granted for itself and others acting on its behalf a 10 | paid-up, nonexclusive, irrevocable, worldwide license in the Software to 11 | reproduce, prepare derivative works, and perform publicly and display publicly. 12 | Beginning five (5) years after the date permission to assert copyright is 13 | obtained from the U.S. Department of Energy, and subject to any subsequent five 14 | (5) year renewals, the U.S. Government is granted for itself and others acting 15 | on its behalf a paid-up, nonexclusive, irrevocable, worldwide license in the 16 | Software to reproduce, prepare derivative works, distribute copies to the 17 | public, perform publicly and display publicly, and to permit others to do so. 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | "openPMD-viewer, Copyright (c) 2015, The Regents of the University of 2 | California, through Lawrence Berkeley National Laboratory (subject to receipt 3 | of any required approvals from the U.S. Dept. of Energy). All rights 4 | reserved." 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | (1) Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | (2) Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation and/ 14 | or other materials provided with the distribution. 15 | 16 | (3) Neither the name of the University of California, Lawrence Berkeley 17 | National Laboratory, U.S. Dept. of Energy nor the names of its contributors may 18 | be used to endorse or promote products derived from this software without 19 | specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | You are under no obligation whatsoever to provide any bug fixes, patches, or 33 | upgrades to the features, functionality or performance of the source code 34 | ("Enhancements") to anyone; however, if you choose to make your Enhancements 35 | available either publicly, or directly to Lawrence Berkeley National 36 | Laboratory, without imposing a separate written license agreement for such 37 | Enhancements, then you hereby grant the following license: a non-exclusive, 38 | royalty-free perpetual license to install, use, modify, prepare derivative 39 | works, incorporate into other computer software, distribute, and sublicense 40 | such enhancements or derivative works thereof, in binary and source code form. 41 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include requirements.txt 3 | include openpmd_viewer/notebook_starter/openPMD_notebook 4 | include openpmd_viewer/notebook_starter/Template_notebook.ipynb 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # openPMD-viewer 2 | 3 | [![pypi version](https://img.shields.io/pypi/v/openPMD-viewer.svg)](https://pypi.python.org/pypi/openPMD-viewer) 4 | [![Binder](https://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/openPMD/openPMD-viewer/dev?filepath=docs/source/tutorials%2F) 5 | [![License](https://img.shields.io/pypi/l/openPMD-viewer.svg)](LICENSE.txt) 6 | 7 | ## Overview 8 | 9 | This package contains a set of tools to load and visualize the 10 | contents of a set of [openPMD](http://www.openpmd.org/#/start) files 11 | (typically, a timeseries). 12 | 13 | The routines of `openPMD-viewer` can be used in two ways : 14 | 15 | - Use the **Python API**, in order to write a script that loads the 16 | data and produces a set of pre-defined plots. 17 | 18 | - Use the **interactive GUI inside the Jupyter Notebook**, in order to interactively 19 | visualize the data. 20 | 21 | ## Usage 22 | 23 | ### Tutorials 24 | 25 | The notebooks in the folder `tutorials/` demonstrate how to use both 26 | the API and the interactive GUI. You can view these notebooks online 27 | [here](https://github.com/openPMD/openPMD-viewer/tree/dev/docs/source/tutorials). 28 | 29 | Alternatively, you can even 30 | [*run* our tutorials online](https://mybinder.org/v2/gh/openPMD/openPMD-viewer/dev?filepath=docs/source/tutorials%2F)! 31 | 32 | You can also download and run these notebooks on your local computer 33 | (when viewing the notebooks with the above link, click on `Raw` to be able to 34 | save them to your local computer). In order to run the notebook on 35 | your local computer, please install `openPMD-viewer` first (see 36 | below), as well as `wget` (`pip install wget`). 37 | 38 | ### Notebook quick-starter 39 | 40 | If you wish to use the **interactive GUI**, the installation of 41 | `openPMD-viewer` provides a convenient executable which automatically 42 | **creates a new pre-filled notebook** and **opens it in a 43 | browser**. To use this executable, simply type in a regular terminal: 44 | 45 | `openPMD_notebook` 46 | 47 | (This executable is installed by default, when installing `openPMD-viewer`.) 48 | 49 | ## Installation 50 | 51 | ### Installation on a local computer 52 | 53 | #### Installation with conda 54 | 55 | In order to install `openPMD-viewer` with `conda`, please install the [Anaconda 56 | distribution](https://docs.anaconda.com/anaconda/install/), and then type 57 | ``` 58 | conda install -c conda-forge openpmd-viewer 59 | ``` 60 | If you are using JupyterLab, please also install the `jupyter-matplotlib` 61 | extension (See installation instructions 62 | [here](https://github.com/matplotlib/jupyter-matplotlib)). 63 | 64 | #### Installation with pip 65 | 66 | You can also install `openPMD-viewer` using `pip` 67 | ``` 68 | pip install openpmd-viewer 69 | ``` 70 | In addition, if you wish to use the interactive GUI, please type 71 | ``` 72 | pip install jupyter 73 | ``` 74 | 75 | ### Installation on a remote scientific cluster 76 | 77 | If you wish to install the `openPMD-viewer` on a remote scientific 78 | cluster, please make sure that the packages `numpy`, `scipy` and `h5py` 79 | are available in your environment. This is typically done by a set of 80 | `module load` commands (e.g. `module load h5py`) -- please refer to 81 | the documentation of your scientific cluster. 82 | 83 | Then type 84 | ``` 85 | pip install openPMD-viewer --user 86 | ``` 87 | 88 | Note: The package `jupyter` is only required for the **interactive 89 | GUI** and thus it does not need to be installed if you are only using 90 | the **Python API**. For [NERSC](http://www.nersc.gov/) users, access to Jupyter 91 | notebooks is provided when logging to 92 | [https://ipython.nersc.gov](https://ipython.nersc.gov). 93 | 94 | ## Contributing to the openPMD-viewer 95 | 96 | We welcome contributions to the code! Please read [this page](https://github.com/openPMD/openPMD-viewer/blob/dev/CONTRIBUTING.md) for 97 | guidelines on how to contribute. 98 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Creating a new release 2 | 3 | This document is only relevant for maintainers of openPMD-viewer. It 4 | explains how to create a new release. In future versions of this 5 | packages, some of the steps below will be automatized. 6 | 7 | ## Preparing your environment for a release 8 | 9 | Make sure that your local environment is ready for a full release on 10 | PyPI and conda. In particular: 11 | 12 | - you should install the package 13 | [`twine`](https://pypi.python.org/pypi/twine). 14 | - you should have a registered account on [PyPI](https://pypi.python.org/pypi) and [test PyPI](https://testpypi.python.org/pypi), and your `$HOME` should contain a file `.pypirc` which contains the following text: 15 | 16 | ``` 17 | [distutils] 18 | index-servers= 19 | pypitest 20 | pypi 21 | 22 | [pypitest] 23 | repository = https://testpypi.python.org/pypi 24 | username = 25 | 26 | [pypi] 27 | repository = https://pypi.python.org/pypi 28 | username = 29 | ``` 30 | 31 | - you should have a registered account on [Anaconda.org](https://anaconda.org/) 32 | 33 | ## Creating a release on Github 34 | 35 | - Make sure that the version number in `openpmd_viewer/__version__.py` 36 | correspond to the new release, and that 37 | the corresponding changes have been documented in `CHANGELOG.md`. 38 | 39 | - Create a new release through the graphical interface on Github 40 | 41 | ## Uploading the package to PyPI 42 | 43 | The code will be automatically uploaded to PyPI upon creation of a new release on Github. -------------------------------------------------------------------------------- /binder/requirements.txt: -------------------------------------------------------------------------------- 1 | # full dependencies for the tutorials, e.g. when run on binder 2 | openPMD-viewer 3 | matplotlib 4 | wget 5 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx-design 3 | sphinx_rtd_theme 4 | jupyter 5 | nbsphinx 6 | wget 7 | numba 8 | numpy 9 | matplotlib 10 | ipympl 11 | pygments 12 | scipy 13 | h5py 14 | tqdm 15 | openpmd_viewer 16 | -------------------------------------------------------------------------------- /docs/source/api_reference/api_reference.rst: -------------------------------------------------------------------------------- 1 | API reference 2 | ============= 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | :caption: Contents: 7 | 8 | generic_interface 9 | particle_tracking 10 | lpa_diagnostics -------------------------------------------------------------------------------- /docs/source/api_reference/generic_interface.rst: -------------------------------------------------------------------------------- 1 | Generic interface: ``OpenPMDTimeSeries`` 2 | ---------------------------------------- 3 | 4 | .. autoclass:: openpmd_viewer.OpenPMDTimeSeries 5 | 6 | .. automethod:: get_field 7 | .. automethod:: get_particle 8 | .. automethod:: iterate 9 | .. automethod:: slider -------------------------------------------------------------------------------- /docs/source/api_reference/lpa_diagnostics.rst: -------------------------------------------------------------------------------- 1 | Domain-specific interface: ``LpaDiagnostics`` 2 | --------------------------------------------- 3 | 4 | .. autoclass:: openpmd_viewer.addons.LpaDiagnostics 5 | 6 | .. automethod:: get_energy_spread 7 | .. automethod:: get_mean_gamma 8 | .. automethod:: get_sigma_gamma_slice 9 | .. automethod:: get_charge 10 | .. automethod:: get_divergence 11 | .. automethod:: get_emittance 12 | .. automethod:: get_current 13 | .. automethod:: get_laser_envelope 14 | .. automethod:: get_main_frequency 15 | .. automethod:: get_spectrum 16 | .. automethod:: get_a0 17 | .. automethod:: get_ctau 18 | .. automethod:: get_laser_waist 19 | .. automethod:: get_spectrogram -------------------------------------------------------------------------------- /docs/source/api_reference/particle_tracking.rst: -------------------------------------------------------------------------------- 1 | Particle tracking: ``ParticleTracker`` 2 | -------------------------------------- 3 | 4 | .. autoclass:: openpmd_viewer.ParticleTracker 5 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | import openpmd_viewer 21 | project = 'openPMD-viewer' 22 | copyright = '2020, openPMD contributors' 23 | author = 'openPMD contributors' 24 | 25 | # The full version, including alpha/beta/rc tags 26 | release = openpmd_viewer.__version__ 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.napoleon', 37 | 'sphinx.ext.viewcode', 38 | 'sphinx.ext.githubpages', 39 | 'nbsphinx', 40 | ] 41 | autoclass_content="init" 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ['_templates'] 45 | 46 | # List of patterns, relative to source directory, that match files and 47 | # directories to ignore when looking for source files. 48 | # This pattern also affects html_static_path and html_extra_path. 49 | exclude_patterns = [] 50 | 51 | 52 | # -- Options for HTML output ------------------------------------------------- 53 | 54 | # The theme to use for HTML and HTML Help pages. See the documentation for 55 | # a list of builtin themes. 56 | # 57 | html_theme = 'sphinx_rtd_theme' 58 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. openPMD-viewer documentation master file, created by 2 | sphinx-quickstart on Thu Jan 30 10:48:39 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | openPMD-viewer documentation 7 | ============================ 8 | 9 | ``openPMD-viewer`` contains a set of tools to load and visualize the 10 | contents of a set of `openPMD `_ files 11 | (typically, a timeseries). 12 | 13 | The routines of ``openPMD-viewer`` can be used in two ways: 14 | 15 | - Using the **Python API**, in order to write a script that loads the data and produces a set of pre-defined plots. 16 | 17 | - Using the **interactive GUI inside a Jupyter Notebook**, in order to interactively visualize the data. 18 | 19 | 20 | Installation 21 | ------------ 22 | 23 | You can install openPMD-viewer with ``pip`` using: 24 | :: 25 | 26 | pip install openpmd-viewer 27 | 28 | or alternatively with ``conda`` using: 29 | :: 30 | 31 | conda install -c conda-forge openpmd-viewer 32 | 33 | Usage 34 | ----- 35 | 36 | The notebooks in the section :doc:`tutorials/tutorials` demonstrate how to use both 37 | the API and the interactive GUI. 38 | 39 | If you wish to use the **interactive GUI**, the installation of 40 | ``openPMD-viewer`` provides a convenient executable which automatically 41 | **creates a new pre-filled notebook** and **opens it in a 42 | browser**. To use this executable, simply type in a regular terminal: 43 | 44 | :: 45 | 46 | openPMD_notebook 47 | 48 | .. toctree:: 49 | :maxdepth: 1 50 | :caption: Contents: 51 | 52 | tutorials/tutorials 53 | api_reference/api_reference -------------------------------------------------------------------------------- /docs/source/tutorials/2_Specific-field-geometries.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": {}, 7 | "source": [ 8 | "# Specific arguments for particular field geometry\n", 9 | "\n", 10 | "The openPMD format supports 3 types of geometries:\n", 11 | "\n", 12 | "- Cartesian 2D\n", 13 | "- Cartesian 3D\n", 14 | "- Cylindrical with azimuthal decomposition (thetaMode)\n", 15 | "\n", 16 | "This notebook shows how to use the arguments of `get_field` which are specific to a given geometry. You can run this notebook locally by downloading it from [this link](https://github.com/openPMD/openPMD-viewer/blob/dev/docs/source/tutorials/2_Specific-field-geometries.ipynb).\n", 17 | "\n", 18 | "## (optional) Preparing this notebook to run it locally\n", 19 | "\n", 20 | "If you choose to run this notebook on your local machine, you will need to download the openPMD data files which will then be visualized. To do so, execute the following cell. (Downloading the data may take a few seconds.)" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "import os, sys\n", 30 | "\n", 31 | "def download_if_absent( dataset_name ):\n", 32 | " \"Function that downloads and decompress a chosen dataset\"\n", 33 | " if os.path.exists( dataset_name ) is False:\n", 34 | " import wget, tarfile\n", 35 | " tar_name = \"%s.tar.gz\" %dataset_name\n", 36 | " url = \"https://github.com/openPMD/openPMD-example-datasets/raw/draft/%s\" %tar_name\n", 37 | " wget.download(url, tar_name)\n", 38 | " with tarfile.open( tar_name ) as tar_file:\n", 39 | " tar_file.extractall()\n", 40 | " os.remove( tar_name )\n", 41 | "\n", 42 | "download_if_absent( 'example-3d' )\n", 43 | "download_if_absent( 'example-thetaMode' )" 44 | ] 45 | }, 46 | { 47 | "attachments": {}, 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "In addition, we choose here to incorporate the plots inside the notebook." 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "%matplotlib inline" 61 | ] 62 | }, 63 | { 64 | "attachments": {}, 65 | "cell_type": "markdown", 66 | "metadata": {}, 67 | "source": [ 68 | "## Preparing the API\n", 69 | "\n", 70 | "Again, we need to import the `OpenPMDTimeSeries` object:" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": null, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "from openpmd_viewer import OpenPMDTimeSeries" 80 | ] 81 | }, 82 | { 83 | "attachments": {}, 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "and to create objects that point to the 3D data and the cylindrical data. \n", 88 | "\n", 89 | "(NB: The argument `check_all_files` below is optional. By default, `check_all_files` is `True`, and in this case the code checks that all files in the timeseries are consistent\n", 90 | "i.e. that they all contain the same fields and particle quantities, with the same metadata. When `check_all_files` is `False`, these verifications are skipped, and this allows to create the `OpenPMDTimeSeries` object faster.)" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": null, 96 | "metadata": {}, 97 | "outputs": [], 98 | "source": [ 99 | "ts_3d = OpenPMDTimeSeries('./example-3d/hdf5/', check_all_files=False )\n", 100 | "ts_circ = OpenPMDTimeSeries('./example-thetaMode/hdf5/', check_all_files=False )" 101 | ] 102 | }, 103 | { 104 | "attachments": {}, 105 | "cell_type": "markdown", 106 | "metadata": { 107 | "collapsed": true 108 | }, 109 | "source": [ 110 | "## 3D Cartesian geometry\n", 111 | "\n", 112 | "For 3D Cartesian geometry, the `get_field` method has additional arguments, in order to select a 2D slice into the 3D volume:\n", 113 | "- `slice_across` allows to choose the axis across which the slice is taken. See the examples below:" 114 | ] 115 | }, 116 | { 117 | "cell_type": "code", 118 | "execution_count": null, 119 | "metadata": {}, 120 | "outputs": [], 121 | "source": [ 122 | "# Slice across y (i.e. in a plane parallel to x-z)\n", 123 | "Ez1, info_Ez1 = ts_3d.get_field( field='E', coord='z', iteration=500, \n", 124 | " slice_across='y', plot=True )" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": null, 130 | "metadata": {}, 131 | "outputs": [], 132 | "source": [ 133 | "# Slice across z (i.e. in a plane parallel to x-y)\n", 134 | "Ez2, info_Ez2 = ts_3d.get_field( field='E', coord='z', iteration=500,\n", 135 | " slice_across='z', plot=True )" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": null, 141 | "metadata": {}, 142 | "outputs": [], 143 | "source": [ 144 | "# Slice across x and y (i.e. along a line parallel to the z axis)\n", 145 | "Ez2, info_Ez2 = ts_3d.get_field( field='E', coord='z', iteration=500,\n", 146 | " slice_across=['x','y'], plot=True )" 147 | ] 148 | }, 149 | { 150 | "attachments": {}, 151 | "cell_type": "markdown", 152 | "metadata": {}, 153 | "source": [ 154 | "- For one given slicing direction, `slice_relative_position` allows to select which slice to take: `slice_relative_position` is a number between -1 and 1, where -1 indicates to take the slice at the lower bound of the slicing range (e.g. $z_{min}$ if `slice_across` is `z`) and 1 indicates to take the slice at the upper bound of the slicing range (e.g. $z_{max}$ if `slice_across` is `z`). For example:" 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": null, 160 | "metadata": {}, 161 | "outputs": [], 162 | "source": [ 163 | "# Slice across z, very close to zmin.\n", 164 | "Ez2, info_Ez2 = ts_3d.get_field( field='E', coord='z', iteration=500, \n", 165 | " slice_across='z', slice_relative_position=-0.9, plot=True )" 166 | ] 167 | }, 168 | { 169 | "attachments": {}, 170 | "cell_type": "markdown", 171 | "metadata": {}, 172 | "source": [ 173 | "When passing `slice_across=None`, `get_field` returns a full 3D Cartesian array. This can be useful for further analysis by hand, with `numpy` (e.g. calculating the total energy in the field)." 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": null, 179 | "metadata": {}, 180 | "outputs": [], 181 | "source": [ 182 | "# Get the full 3D Cartesian array\n", 183 | "Ez_3d, info_Ez_3d = ts_3d.get_field( field='E', coord='z', iteration=500, slice_across=None )\n", 184 | "print( Ez_3d.ndim )" 185 | ] 186 | }, 187 | { 188 | "attachments": {}, 189 | "cell_type": "markdown", 190 | "metadata": {}, 191 | "source": [ 192 | "## Cylindrical geometry (with azimuthal decomposition)\n", 193 | "\n", 194 | "In for data in the `thetaMode` geometry, the fields are decomposed into azimuthal modes. Thus, the `get_field` method has an argument `m`, which allows to select the mode:\n", 195 | "\n", 196 | "- Choosing an integer value for selects a particular mode (for instance, here one can see a laser-wakefield, which is entirely contained in the mode 0)" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": null, 202 | "metadata": {}, 203 | "outputs": [], 204 | "source": [ 205 | "Ey, info_Ey = ts_circ.get_field( field='E', coord='y', iteration=500, m=0, \n", 206 | " plot=True, theta=0.5)" 207 | ] 208 | }, 209 | { 210 | "attachments": {}, 211 | "cell_type": "markdown", 212 | "metadata": {}, 213 | "source": [ 214 | "- Choosing `m='all'` sums all the modes (for instance, here the laser field, which is in the mode 1, dominates the fields)" 215 | ] 216 | }, 217 | { 218 | "cell_type": "code", 219 | "execution_count": null, 220 | "metadata": {}, 221 | "outputs": [], 222 | "source": [ 223 | "Ey, info_Ey = ts_circ.get_field( field='E', coord='y', iteration=500, m='all', \n", 224 | " plot=True, theta=0.5)" 225 | ] 226 | }, 227 | { 228 | "attachments": {}, 229 | "cell_type": "markdown", 230 | "metadata": {}, 231 | "source": [ 232 | "The argument `theta` (in radians) selects the plane of observation: this plane contains the $z$ axis and has an angle `theta` with respect to the $x$ axis.\n", 233 | "\n", 234 | "When passing `theta=None`, `get_field` returns a full 3D Cartesian array. This can be useful for further analysis by hand, with `numpy` (e.g. calculating the total energy in the field), or for comparison with Cartesian simulations." 235 | ] 236 | }, 237 | { 238 | "cell_type": "code", 239 | "execution_count": null, 240 | "metadata": {}, 241 | "outputs": [], 242 | "source": [ 243 | "# Get the full 3D Cartesian array\n", 244 | "Ey_3d, info_Ey3d = ts_circ.get_field( field='E', coord='y', iteration=500, theta=None )\n", 245 | "print( Ey_3d.ndim )" 246 | ] 247 | }, 248 | { 249 | "attachments": {}, 250 | "cell_type": "markdown", 251 | "metadata": {}, 252 | "source": [ 253 | "- In cylindrical geometry, the users can also choose the coordinates `r` and `t` for the radial and azimuthal components of the fields. For instance:" 254 | ] 255 | }, 256 | { 257 | "cell_type": "code", 258 | "execution_count": null, 259 | "metadata": {}, 260 | "outputs": [], 261 | "source": [ 262 | "Er, info_Er = ts_circ.get_field( field='E', coord='r', iteration=500, m=0, \n", 263 | " plot=True, theta=0.5)" 264 | ] 265 | }, 266 | { 267 | "attachments": {}, 268 | "cell_type": "markdown", 269 | "metadata": {}, 270 | "source": [ 271 | "- Finally, in cylindrical geometry, fields can also be sliced, by using the `r` and `z` direction. (Keep in mind that `slice_across` is the direction **orthogonal** to the slice.)" 272 | ] 273 | }, 274 | { 275 | "cell_type": "code", 276 | "execution_count": null, 277 | "metadata": {}, 278 | "outputs": [], 279 | "source": [ 280 | "Er_slice, info = ts_circ.get_field( field='E', coord='r', iteration=500, plot=True, slice_across='r' )" 281 | ] 282 | }, 283 | { 284 | "cell_type": "code", 285 | "execution_count": null, 286 | "metadata": {}, 287 | "outputs": [], 288 | "source": [ 289 | "Er_slice, info = ts_circ.get_field( field='E', coord='r', iteration=500, plot=True, slice_across='z' )" 290 | ] 291 | } 292 | ], 293 | "metadata": { 294 | "kernelspec": { 295 | "display_name": "Python 3", 296 | "language": "python", 297 | "name": "python3" 298 | }, 299 | "language_info": { 300 | "codemirror_mode": { 301 | "name": "ipython", 302 | "version": 3 303 | }, 304 | "file_extension": ".py", 305 | "mimetype": "text/x-python", 306 | "name": "python", 307 | "nbconvert_exporter": "python", 308 | "pygments_lexer": "ipython3", 309 | "version": "3.7.3" 310 | } 311 | }, 312 | "nbformat": 4, 313 | "nbformat_minor": 2 314 | } 315 | -------------------------------------------------------------------------------- /docs/source/tutorials/3_Introduction-to-the-GUI.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": { 7 | "collapsed": true 8 | }, 9 | "source": [ 10 | "# Introduction to the openPMD-viewer GUI\n", 11 | "\n", 12 | "This notebook explains how to use the openPMD GUI. You can run this notebook locally by downloading it from [this link](https://github.com/openPMD/openPMD-viewer/blob/dev/docs/source/tutorials/3_Introduction-to-the-GUI.ipynb).\n", 13 | "\n", 14 | "The GUI relies on the Jupyter notebook, and on the package `ipywidgets`. You will need to install these packages in order to be able to use the GUI.\n", 15 | "\n", 16 | "## (optional) Preparing this notebook to run it locally\n", 17 | "\n", 18 | "If you choose to run this notebook on your local machine, you will need to download the openPMD data files which will then be visualized. To do so, execute the following cell." 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "import os\n", 28 | "\n", 29 | "def download_if_absent( dataset_name ):\n", 30 | " \"Function that downloads and decompress a chosen dataset\"\n", 31 | " if os.path.exists( dataset_name ) is False:\n", 32 | " import wget, tarfile\n", 33 | " tar_name = \"%s.tar.gz\" %dataset_name\n", 34 | " url = \"https://github.com/openPMD/openPMD-example-datasets/raw/draft/%s\" %tar_name\n", 35 | " wget.download(url, tar_name)\n", 36 | " with tarfile.open( tar_name ) as tar_file:\n", 37 | " tar_file.extractall()\n", 38 | " os.remove( tar_name )\n", 39 | "\n", 40 | "download_if_absent( 'example-2d' )\n", 41 | "download_if_absent( 'example-3d' )\n", 42 | "download_if_absent( 'example-thetaMode' )" 43 | ] 44 | }, 45 | { 46 | "attachments": {}, 47 | "cell_type": "markdown", 48 | "metadata": {}, 49 | "source": [ 50 | "In addition, we choose here to incorporate the plots inside the notebook." 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": null, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "%matplotlib widget" 60 | ] 61 | }, 62 | { 63 | "attachments": {}, 64 | "cell_type": "markdown", 65 | "metadata": {}, 66 | "source": [ 67 | "## Prepare and use the GUI \n", 68 | "\n", 69 | "In order to start using the GUI:\n", 70 | "- Load the class `OpenPMDTimeSeries` from the module `openpmd_viewer`" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": null, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "from openpmd_viewer import OpenPMDTimeSeries" 80 | ] 81 | }, 82 | { 83 | "attachments": {}, 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "- Create a time series object by pointing to the folder which contains the corresponding openPMD data" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": null, 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | "ts_2d = OpenPMDTimeSeries('./example-2d/hdf5/')" 97 | ] 98 | }, 99 | { 100 | "attachments": {}, 101 | "cell_type": "markdown", 102 | "metadata": {}, 103 | "source": [ 104 | "- Call the `slider` method" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": null, 110 | "metadata": {}, 111 | "outputs": [], 112 | "source": [ 113 | "ts_2d.slider()" 114 | ] 115 | }, 116 | { 117 | "attachments": {}, 118 | "cell_type": "markdown", 119 | "metadata": {}, 120 | "source": [ 121 | "Under the hood, this panel is calling the openPMD-viewer API with the `plot=True` option, and thus the user can quickly plot different quantities by clicking on the panel:\n", 122 | " - The top slider allows the user to choose the time for which to do the plot. Clicking on the `+` and `-` buttons moves from iteration to iteration.\n", 123 | " - The left subpanel allows to choose the plotted fields \n", 124 | " - The right subpanel allows to choose the particle quantites (note that it also detects the species present in the file)\n", 125 | " - The `Plotting options` menu gathers some common matplotlib options.\n", 126 | "\n", 127 | "Please try this interactive interface for yourself, in order to explore its capabilities!" 128 | ] 129 | }, 130 | { 131 | "attachments": {}, 132 | "cell_type": "markdown", 133 | "metadata": {}, 134 | "source": [ 135 | "## GUI for specific field geometries\n", 136 | "\n", 137 | "Note that the above panel will adapt to the field geometry, and to the presence or absence of particles in the data files.\n", 138 | "\n", 139 | "\n", 140 | "### 3D datasets\n", 141 | "For instance, the code below creates a slider that explores a 3D Cartesian dataset (and contains only electrons; no ions)." 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": null, 147 | "metadata": {}, 148 | "outputs": [], 149 | "source": [ 150 | "ts_3d = OpenPMDTimeSeries('./example-3d/hdf5/')\n", 151 | "ts_3d.slider( fields_figure=2, particles_figure=3 )" 152 | ] 153 | }, 154 | { 155 | "attachments": {}, 156 | "cell_type": "markdown", 157 | "metadata": {}, 158 | "source": [ 159 | "### Cylindrical datasets with azimuthal decomposition\n", 160 | "\n", 161 | "Similarly, the code below explores a dataset with azimuthal decomposition (2 azimuthal modes). Moreover, this dataset does not contain any particles, as reflect by the aspect of the GUI." 162 | ] 163 | }, 164 | { 165 | "cell_type": "code", 166 | "execution_count": null, 167 | "metadata": {}, 168 | "outputs": [], 169 | "source": [ 170 | "ts_circ = OpenPMDTimeSeries('./example-thetaMode/hdf5/')\n", 171 | "ts_circ.slider( fields_figure=4 )" 172 | ] 173 | }, 174 | { 175 | "attachments": {}, 176 | "cell_type": "markdown", 177 | "metadata": {}, 178 | "source": [ 179 | "Note that the interface includes buttons for the choice of modes and for the angle ``theta`` of observation." 180 | ] 181 | } 182 | ], 183 | "metadata": { 184 | "kernelspec": { 185 | "display_name": "Python 3", 186 | "language": "python", 187 | "name": "python3" 188 | }, 189 | "language_info": { 190 | "codemirror_mode": { 191 | "name": "ipython", 192 | "version": 3 193 | }, 194 | "file_extension": ".py", 195 | "mimetype": "text/x-python", 196 | "name": "python", 197 | "nbconvert_exporter": "python", 198 | "pygments_lexer": "ipython3", 199 | "version": "3.6.3" 200 | } 201 | }, 202 | "nbformat": 4, 203 | "nbformat_minor": 2 204 | } 205 | -------------------------------------------------------------------------------- /docs/source/tutorials/5_Laser-plasma_tools.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": { 7 | "collapsed": true 8 | }, 9 | "source": [ 10 | "# Introduction to openPMD-viewer laser-plasma tools\n", 11 | "\n", 12 | "In addition to the general methods `get_field` and `get_particle`, openPMD-viewer can also include a set of domain-specific tools. For instance, this notebook describes a set of methods that are useful when analyzing simulations of **laser-plasma acceleration**. \n", 13 | "\n", 14 | "If you are not interested in laser-plasma simulations, you can skip this notebook as these methods are only an add-on to openPMD-viewer. \n", 15 | "\n", 16 | "You can run this notebook locally by downloading it from [this link](https://github.com/openPMD/openPMD-viewer/blob/dev/docs/source/tutorials/5_Laser-plasma_tools.ipynb).\n", 17 | "\n", 18 | "## (optional) Preparing this notebook to run it locally\n", 19 | "\n", 20 | "If you choose to run this notebook on your local machine, you will need to download the openPMD data files which will then be analysed. To do so, execute the following cell." 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "import os\n", 30 | "\n", 31 | "def download_if_absent( dataset_name ):\n", 32 | " \"Function that downloads and decompress a chosen dataset\"\n", 33 | " if os.path.exists( dataset_name ) is False:\n", 34 | " import wget, tarfile\n", 35 | " tar_name = \"%s.tar.gz\" %dataset_name\n", 36 | " url = \"https://github.com/openPMD/openPMD-example-datasets/raw/draft/%s\" %tar_name\n", 37 | " wget.download(url, tar_name)\n", 38 | " with tarfile.open( tar_name ) as tar_file:\n", 39 | " tar_file.extractall()\n", 40 | " os.remove( tar_name )\n", 41 | "\n", 42 | "download_if_absent( 'example-2d' )" 43 | ] 44 | }, 45 | { 46 | "attachments": {}, 47 | "cell_type": "markdown", 48 | "metadata": {}, 49 | "source": [ 50 | "In addition, we choose here to incorporate the plots inside the notebook." 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": null, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "%matplotlib inline" 60 | ] 61 | }, 62 | { 63 | "attachments": {}, 64 | "cell_type": "markdown", 65 | "metadata": {}, 66 | "source": [ 67 | "## The LpaDiagnostics class\n", 68 | "\n", 69 | "To use the laser-plasma acceleration (LPA) tools:\n", 70 | "- Load the class `LpaDiagnostics` from the module `openpmd_viewer.addons`" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": null, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "from openpmd_viewer.addons import LpaDiagnostics" 80 | ] 81 | }, 82 | { 83 | "attachments": {}, 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "- Create an `LpaDiagnostics` instance in the same way, as you would do for an `OpenPMDTimeSeries`." 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": null, 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | "ts_2d = LpaDiagnostics('./example-2d/hdf5/')" 97 | ] 98 | }, 99 | { 100 | "attachments": {}, 101 | "cell_type": "markdown", 102 | "metadata": {}, 103 | "source": [ 104 | "The `LpaDiagnostics` class inherits from the `OpenPMDTimeSeries` class, and therefore it also has the methods `get_field`, `get_particle` and `slider`. For instance:" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": null, 110 | "metadata": { 111 | "scrolled": false 112 | }, 113 | "outputs": [], 114 | "source": [ 115 | "ts_2d.slider()" 116 | ] 117 | }, 118 | { 119 | "attachments": {}, 120 | "cell_type": "markdown", 121 | "metadata": {}, 122 | "source": [ 123 | "## Diagnostic methods\n", 124 | "\n", 125 | "In addition to the methods `get_particle`, `get_field` and `slider`, the `LpaDiagnotics` class has an extra set of specialized methods.\n", 126 | "\n", 127 | "These diagnostic methods are currently only implemented for API-like usage. In particular, either the time or iteration have to be specified when calling a method, by setting the `t` or `iteration` parameter. " 128 | ] 129 | }, 130 | { 131 | "attachments": {}, 132 | "cell_type": "markdown", 133 | "metadata": {}, 134 | "source": [ 135 | "### Particle diagnostics\n", 136 | "\n", 137 | "For methods involving particle data, the desired particle species also needs to be specified with the `species` parameter. Optionally, these methods can be applied to only a subset of the particles, by using the `select` parameter.\n", 138 | "\n", 139 | "For instance, `select={'uz' : [-1, 2]}` will select only the particles which have a longitudinal normalized momentum between `-1` and `2`. \n", 140 | "\n", 141 | "In the following the available particle diagnostic methods will be explained.\n", 142 | "\n", 143 | "For more information a method's documentation can be called with:" 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": null, 149 | "metadata": {}, 150 | "outputs": [], 151 | "source": [ 152 | "ts_2d.get_mean_gamma?" 153 | ] 154 | }, 155 | { 156 | "attachments": {}, 157 | "cell_type": "markdown", 158 | "metadata": {}, 159 | "source": [ 160 | "#### Charge\n", 161 | "`get_charge` calculates the charge of the given particle selection in Coulomb." 162 | ] 163 | }, 164 | { 165 | "cell_type": "code", 166 | "execution_count": null, 167 | "metadata": {}, 168 | "outputs": [], 169 | "source": [ 170 | "ts_2d.get_charge(iteration=300, species='electrons')" 171 | ] 172 | }, 173 | { 174 | "attachments": {}, 175 | "cell_type": "markdown", 176 | "metadata": {}, 177 | "source": [ 178 | "Note that the evolution of the charge (or of any of the quantities below) can be easily obtained with `ts.iterate`:" 179 | ] 180 | }, 181 | { 182 | "cell_type": "code", 183 | "execution_count": null, 184 | "metadata": {}, 185 | "outputs": [], 186 | "source": [ 187 | "ts_2d.iterate( ts_2d.get_charge, species='electrons' )" 188 | ] 189 | }, 190 | { 191 | "attachments": {}, 192 | "cell_type": "markdown", 193 | "metadata": {}, 194 | "source": [ 195 | "#### Mean gamma\n", 196 | "To calculate the mean gamma value and standard deviation of the selected particles `get_mean_gamma` can be used. In the example below, only the particles with $u_z > 0.05$ are selected." 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": null, 202 | "metadata": {}, 203 | "outputs": [], 204 | "source": [ 205 | "ts_2d.get_mean_gamma(iteration=300, species='electrons', select={'uz' : [0.05, None]})" 206 | ] 207 | }, 208 | { 209 | "attachments": {}, 210 | "cell_type": "markdown", 211 | "metadata": {}, 212 | "source": [ 213 | "#### Divergence\n", 214 | "This method calculates the divergence of the selected particles, using $\\langle \\arctan{u_{x/y}/u_z} \\rangle$" 215 | ] 216 | }, 217 | { 218 | "cell_type": "code", 219 | "execution_count": null, 220 | "metadata": {}, 221 | "outputs": [], 222 | "source": [ 223 | "ts_2d.get_divergence(iteration=300, species='electrons')" 224 | ] 225 | }, 226 | { 227 | "attachments": {}, 228 | "cell_type": "markdown", 229 | "metadata": {}, 230 | "source": [ 231 | "#### Emittance\n", 232 | "This method can be used to calculate the normalized emittance in the x and y plane for selected particles by evaluating\n", 233 | "$$\\epsilon_{n,rms}=\\sqrt{\\langle x^2 \\rangle \\langle u_x^2 \\rangle - \\langle x u_x \\rangle^2}$$" 234 | ] 235 | }, 236 | { 237 | "cell_type": "code", 238 | "execution_count": null, 239 | "metadata": {}, 240 | "outputs": [], 241 | "source": [ 242 | "ts_2d.get_emittance(iteration=300, species='electrons')" 243 | ] 244 | }, 245 | { 246 | "attachments": {}, 247 | "cell_type": "markdown", 248 | "metadata": {}, 249 | "source": [ 250 | "#### Current\n", 251 | "`get_current` can be used to calculate the instantaneous current along the z_axis generated by the selected particles. When setting `plot=True` the resulting current profile is directly plotted. Otherwise an array with the data is returned." 252 | ] 253 | }, 254 | { 255 | "cell_type": "code", 256 | "execution_count": null, 257 | "metadata": {}, 258 | "outputs": [], 259 | "source": [ 260 | "ts_2d.get_current(iteration=300, species='electrons', plot=True);" 261 | ] 262 | }, 263 | { 264 | "attachments": {}, 265 | "cell_type": "markdown", 266 | "metadata": {}, 267 | "source": [ 268 | "### Laser diagnostic\n", 269 | "The laser diagnostic methods require the user to specify the plane of laser polarisation by setting the argument `pol=` to either `'x'` or `'y'`" 270 | ] 271 | }, 272 | { 273 | "attachments": {}, 274 | "cell_type": "markdown", 275 | "metadata": {}, 276 | "source": [ 277 | "#### Laser envelope\n", 278 | "\n", 279 | "This method calculates the envelope of a given laser field. This can be done for a 1D slice of the field or for an entire 2D plane. The resulting data is returned in form of a 1D or 2D array, respectively." 280 | ] 281 | }, 282 | { 283 | "cell_type": "code", 284 | "execution_count": null, 285 | "metadata": {}, 286 | "outputs": [], 287 | "source": [ 288 | "ts_2d.get_laser_envelope(iteration=300, pol='y');" 289 | ] 290 | }, 291 | { 292 | "attachments": {}, 293 | "cell_type": "markdown", 294 | "metadata": {}, 295 | "source": [ 296 | "#### Spectrum\n", 297 | "This function helps to easily calculate (and plot) the spectrum of a given laser field." 298 | ] 299 | }, 300 | { 301 | "cell_type": "code", 302 | "execution_count": null, 303 | "metadata": {}, 304 | "outputs": [], 305 | "source": [ 306 | "ts_2d.get_spectrum(iteration=300, pol='y', plot=True);" 307 | ] 308 | }, 309 | { 310 | "attachments": {}, 311 | "cell_type": "markdown", 312 | "metadata": {}, 313 | "source": [ 314 | "#### Spectrogram\n", 315 | "This method does a time-frequency analysis of the laser, by applying the FROG method.\n", 316 | "\n", 317 | "Mathematically:\n", 318 | " $$ s(\\omega, \\tau) = \\left| \\int_{-\\infty}^{\\infty} E(t) |E(t-\\tau)|^2\n", 319 | " \\exp( -i\\omega t) dt \\right|^2 $$\n", 320 | " \n", 321 | "(Additional matplotlib arguments for the plotting option can directly be passed to the function, e.g `cmap='coolwarm'`\n", 322 | ")" 323 | ] 324 | }, 325 | { 326 | "cell_type": "code", 327 | "execution_count": null, 328 | "metadata": {}, 329 | "outputs": [], 330 | "source": [ 331 | "ts_2d.get_spectrogram(iteration=300, pol='y', plot=True, cmap='YlGnBu_r');" 332 | ] 333 | }, 334 | { 335 | "attachments": {}, 336 | "cell_type": "markdown", 337 | "metadata": {}, 338 | "source": [ 339 | "#### Main frequency\n", 340 | "To calculate the main frequency (i.e. maximum of the spectrum) call. This returns the frequency in $rad.s^{-1}$." 341 | ] 342 | }, 343 | { 344 | "cell_type": "code", 345 | "execution_count": null, 346 | "metadata": {}, 347 | "outputs": [], 348 | "source": [ 349 | "ts_2d.get_main_frequency(iteration=300, pol='y')" 350 | ] 351 | }, 352 | { 353 | "attachments": {}, 354 | "cell_type": "markdown", 355 | "metadata": {}, 356 | "source": [ 357 | "#### Laser $a_0$\n", 358 | "A method to calculate the laser strength a0 as given by $$a0 = E_{max} e / (m_e c \\omega)$$" 359 | ] 360 | }, 361 | { 362 | "cell_type": "code", 363 | "execution_count": null, 364 | "metadata": {}, 365 | "outputs": [], 366 | "source": [ 367 | "ts_2d.get_a0(iteration=300, pol='y')" 368 | ] 369 | }, 370 | { 371 | "attachments": {}, 372 | "cell_type": "markdown", 373 | "metadata": {}, 374 | "source": [ 375 | "#### Laser waist\n", 376 | "\n", 377 | "Calculate the waist of a (gaussian) laser pulse (i.e. $\\sqrt{2} \\sigma_r$, where $\\sigma_r$ is the transverse RMS of the field)." 378 | ] 379 | }, 380 | { 381 | "cell_type": "code", 382 | "execution_count": null, 383 | "metadata": {}, 384 | "outputs": [], 385 | "source": [ 386 | "ts_2d.get_laser_waist(iteration=300, pol='y')" 387 | ] 388 | }, 389 | { 390 | "attachments": {}, 391 | "cell_type": "markdown", 392 | "metadata": {}, 393 | "source": [ 394 | "#### Pulse length\n", 395 | "Calculate the length of a (gaussian) laser pulse. Here 'length' means the 'longitudinal waist' (i.e $\\sqrt{2} \\sigma_z$ where $\\sigma_z$ is the longitudinal RMS of the field).\n" 396 | ] 397 | }, 398 | { 399 | "cell_type": "code", 400 | "execution_count": null, 401 | "metadata": {}, 402 | "outputs": [], 403 | "source": [ 404 | "ts_2d.get_ctau(iteration=300, pol='y')" 405 | ] 406 | } 407 | ], 408 | "metadata": { 409 | "anaconda-cloud": {}, 410 | "kernelspec": { 411 | "display_name": "Python 3", 412 | "language": "python", 413 | "name": "python3" 414 | }, 415 | "language_info": { 416 | "codemirror_mode": { 417 | "name": "ipython", 418 | "version": 3 419 | }, 420 | "file_extension": ".py", 421 | "mimetype": "text/x-python", 422 | "name": "python", 423 | "nbconvert_exporter": "python", 424 | "pygments_lexer": "ipython3", 425 | "version": "3.8.5" 426 | }, 427 | "widgets": { 428 | "state": { 429 | "7f2b0c4c82444cb881753f9785f81e6c": { 430 | "views": [ 431 | { 432 | "cell_index": 9 433 | } 434 | ] 435 | }, 436 | "8c3f18cf657d48afaffabcf9950a8d20": { 437 | "views": [ 438 | { 439 | "cell_index": 9 440 | } 441 | ] 442 | }, 443 | "b0a0574bc9c34b10a7c2c58f30a7ab78": { 444 | "views": [ 445 | { 446 | "cell_index": 9 447 | } 448 | ] 449 | }, 450 | "dcbbddf44d6742b7bf3ccfe452fa56aa": { 451 | "views": [ 452 | { 453 | "cell_index": 9 454 | } 455 | ] 456 | } 457 | }, 458 | "version": "1.2.0" 459 | } 460 | }, 461 | "nbformat": 4, 462 | "nbformat_minor": 2 463 | } 464 | -------------------------------------------------------------------------------- /docs/source/tutorials/tutorials.rst: -------------------------------------------------------------------------------- 1 | Tutorials 2 | --------- 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | :caption: Contents: 7 | 8 | 1_Introduction-to-the-API.ipynb 9 | 2_Specific-field-geometries.ipynb 10 | 3_Introduction-to-the-GUI.ipynb 11 | 4_Particle_selection.ipynb 12 | 5_Laser-plasma_tools.ipynb -------------------------------------------------------------------------------- /openpmd_viewer/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | openPMD-viewer 3 | 4 | Usage 5 | ----- 6 | See the class OpenPMDTimeSeries to open a set of openPMD files 7 | """ 8 | # Make the OpenPMDTimeSeries object accessible from outside the package 9 | from .openpmd_timeseries import OpenPMDTimeSeries, FieldMetaInformation, \ 10 | ParticleTracker 11 | 12 | # Define the version number 13 | from .__version__ import __version__ 14 | __all__ = ['OpenPMDTimeSeries', 'FieldMetaInformation', 15 | 'ParticleTracker', '__version__'] 16 | -------------------------------------------------------------------------------- /openpmd_viewer/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.11.0" 2 | -------------------------------------------------------------------------------- /openpmd_viewer/addons/__init__.py: -------------------------------------------------------------------------------- 1 | from .pic import LpaDiagnostics 2 | __all__ = ['LpaDiagnostics'] 3 | -------------------------------------------------------------------------------- /openpmd_viewer/addons/pic/__init__.py: -------------------------------------------------------------------------------- 1 | from .lpa_diagnostics import LpaDiagnostics 2 | __all__ = ['LpaDiagnostics'] 3 | -------------------------------------------------------------------------------- /openpmd_viewer/notebook_starter/Template_notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "**Instructions:** replace the string in the second cell, and execute each cell (to execute a cell, hit `Shift+Enter`)" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": { 14 | "collapsed": false 15 | }, 16 | "outputs": [], 17 | "source": [ 18 | "import numpy as np\n", 19 | "%matplotlib widget\n", 20 | "# or `%matplotlib inline` for non-interactive plots\n", 21 | "import matplotlib.pyplot as plt\n", 22 | "from openpmd_viewer import OpenPMDTimeSeries" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "metadata": { 29 | "collapsed": false 30 | }, 31 | "outputs": [], 32 | "source": [ 33 | "# Replace the string below, to point to your data\n", 34 | "ts = OpenPMDTimeSeries('./diags/hdf5/')" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "metadata": { 41 | "collapsed": false 42 | }, 43 | "outputs": [], 44 | "source": [ 45 | "# Interactive GUI\n", 46 | "ts.slider()" 47 | ] 48 | } 49 | ], 50 | "metadata": { 51 | "kernelspec": { 52 | "display_name": "Python 3", 53 | "language": "python", 54 | "name": "python3" 55 | }, 56 | "language_info": { 57 | "codemirror_mode": { 58 | "name": "ipython", 59 | "version": 3 60 | }, 61 | "file_extension": ".py", 62 | "mimetype": "text/x-python", 63 | "name": "python", 64 | "nbconvert_exporter": "python", 65 | "pygments_lexer": "ipython3", 66 | "version": "3.8.5" 67 | } 68 | }, 69 | "nbformat": 4, 70 | "nbformat_minor": 2 71 | } 72 | -------------------------------------------------------------------------------- /openpmd_viewer/notebook_starter/openPMD_notebook: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | This executable script is part of the openPMD-viewer package. 4 | 5 | It automatically creates a new pre-filled IPython notebook 6 | in the local directory, and opens it in a browser. 7 | 8 | Usage: Simply type `openPMD_notebook` in a regular terminal 9 | """ 10 | import os 11 | from pkg_resources import resource_string 12 | 13 | # Use pkg_resources to retrieve the location and contents 14 | # of the pre-existing template notebook 15 | notebook_text = resource_string('openpmd_viewer', 16 | 'notebook_starter/Template_notebook.ipynb') 17 | 18 | # Create a new notebook in the local directory and copy 19 | # the contents of the pre-existing template 20 | with open('./openPMD-visualization.ipynb', 'w') as notebook_file: 21 | notebook_file.write( notebook_text.decode() ) 22 | 23 | # Launch the corresponding notebook 24 | os.system('jupyter notebook openPMD-visualization.ipynb') 25 | -------------------------------------------------------------------------------- /openpmd_viewer/openpmd_timeseries/__init__.py: -------------------------------------------------------------------------------- 1 | # Make the OpenPMDTimeSeries accessible from outside the file main 2 | from .main import OpenPMDTimeSeries, ParticleTracker 3 | from .field_metainfo import FieldMetaInformation 4 | __all__ = ['OpenPMDTimeSeries', 'FieldMetaInformation', 'ParticleTracker'] 5 | -------------------------------------------------------------------------------- /openpmd_viewer/openpmd_timeseries/data_order.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the openPMD-viewer. 3 | 4 | It defines an ordering Enum to allow for flexible ordering of components 5 | 6 | Copyright 2022, openPMD-viewer contributors 7 | Authors: Axel Huebl, Ryan Sandberg 8 | License: 3-Clause-BSD-LBNL 9 | """ 10 | 11 | import enum 12 | 13 | # creating enumerations using class 14 | class RZorder(enum.Enum): 15 | """The index names of RZ axes in C order""" 16 | mrz = 1 # z is the fastest varying index in memory 17 | mzr = 2 # r is the fastest varying index in memory 18 | 19 | 20 | order_error_msg = 'Data order is unupported. ' 21 | order_error_msg += 'Allowed orderings: ' 22 | order_error_msg += ' '.join([val.name for val in RZorder]) 23 | -------------------------------------------------------------------------------- /openpmd_viewer/openpmd_timeseries/data_reader/__init__.py: -------------------------------------------------------------------------------- 1 | from .data_reader import DataReader, available_backends 2 | __all__ = ['DataReader', 'available_backends'] 3 | -------------------------------------------------------------------------------- /openpmd_viewer/openpmd_timeseries/data_reader/data_reader.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the openPMD-viewer 3 | 4 | It routes the calls to the data reader to either 5 | the h5py data reader, or to openpmd-api. 6 | 7 | Copyright 2020, openPMD-viewer contributors 8 | Authors: Remi Lehe 9 | License: 3-Clause-BSD-LBNL 10 | """ 11 | import numpy as np 12 | import os 13 | import re 14 | 15 | available_backends = [] 16 | 17 | try: 18 | import openpmd_api as io 19 | from . import io_reader 20 | available_backends.append('openpmd-api') 21 | except ImportError: 22 | pass 23 | 24 | try: 25 | from . import h5py_reader 26 | available_backends.append('h5py') 27 | except ImportError: 28 | pass 29 | 30 | if len(available_backends) == 0: 31 | raise ImportError('No openPMD backend found.\n' 32 | 'Please install either `h5py` or `openpmd-api`:\n' 33 | 'e.g. with `pip install h5py` or `pip install openpmd-api`') 34 | 35 | class DataReader( object ): 36 | """ 37 | Class that performs various type of access the openPMD file. 38 | 39 | The methods of this class are agnostic of the actual backend package 40 | used in order to access the openPMD file (e.g. h5py or openpmd-api). 41 | The backend that is used in practice depends on which package is 42 | available on the current environment. 43 | """ 44 | 45 | def __init__(self, backend): 46 | """ 47 | Initialize the DataReader class. 48 | """ 49 | self.backend = backend 50 | 51 | # Point to the correct reader module 52 | if self.backend == 'h5py': 53 | self.iteration_to_file = {} 54 | elif self.backend == 'openpmd-api': 55 | pass 56 | else: 57 | raise RuntimeError('Unknown backend: %s' % self.backend) 58 | 59 | def list_iterations(self, path_to_dir): 60 | """ 61 | Return a list of the iterations that correspond to the files 62 | in this directory. (The correspondence between iterations and 63 | files is stored internally.) 64 | 65 | Parameter 66 | --------- 67 | path_to_dir : string 68 | The path to the directory where the hdf5 files are. 69 | 70 | Returns 71 | ------- 72 | an array of integers which correspond to the iteration of each file 73 | (in sorted order) 74 | """ 75 | if self.backend == 'h5py': 76 | iterations, iteration_to_file = \ 77 | h5py_reader.list_files( path_to_dir ) 78 | # Store dictionary of correspondence between iteration and file 79 | self.iteration_to_file = iteration_to_file 80 | if len(iterations) == 0: 81 | raise RuntimeError( 82 | "Found no valid files in directory {0}.\n" 83 | "Please check that this is the path to the openPMD files." 84 | "Valid files must have the extension '.h5' if you " 85 | "use the `h5py` backend. For ADIOS '.bp' and other files, " 86 | "please install the `openpmd-api` package." 87 | .format(path_to_dir)) 88 | elif self.backend == 'openpmd-api': 89 | # guess file ending from first file in directory 90 | first_file_name = None 91 | 92 | is_single_file = os.path.isfile(path_to_dir) 93 | if is_single_file: 94 | first_file_name = path_to_dir 95 | else: 96 | for file_name in os.listdir( path_to_dir ): 97 | if file_name.split(os.extsep)[-1] in io.file_extensions: 98 | first_file_name = file_name 99 | if first_file_name is None: 100 | raise RuntimeError( 101 | "Found no valid files in directory {0}.\n" 102 | "Please check that this is the path to the openPMD files." 103 | "(valid files must have one of the following extensions: {1})" 104 | .format(path_to_dir, io.file_extensions)) 105 | 106 | if is_single_file: 107 | file_path = path_to_dir 108 | series_name = file_path 109 | else: 110 | # match last occurrence of integers and replace 111 | # with %T wildcards 112 | # examples: data00000100.h5 diag4_00000500.h5 io12.0.bp 113 | # te42st.1234.yolo.json scan7_run14_data123.h5 114 | file_path = re.sub(r'(\d+)(\.(?!\d).+$)', r'%T\2', first_file_name) 115 | series_name = os.path.join( path_to_dir, file_path) 116 | 117 | self.series = io.Series( 118 | series_name, 119 | io.Access.read_only ) 120 | iterations = np.array( self.series.iterations ) 121 | 122 | return iterations 123 | 124 | def read_openPMD_params(self, iteration, extract_parameters=True): 125 | """ 126 | Extract the time and some openPMD parameters from a file 127 | 128 | Parameter 129 | --------- 130 | iteration: int 131 | The iteration at which the parameters should be extracted 132 | 133 | extract_parameters: bool, optional 134 | Whether to extract all parameters or only the time 135 | (Function execution is faster when extract_parameters is False) 136 | 137 | Returns 138 | ------- 139 | A tuple with: 140 | - A float corresponding to the time of this iteration in SI units 141 | - A dictionary containing several parameters, such as the geometry, etc 142 | When extract_parameters is False, the second argument returned is None 143 | """ 144 | if self.backend == 'h5py': 145 | filename = self.iteration_to_file[iteration] 146 | return h5py_reader.read_openPMD_params( 147 | filename, iteration, extract_parameters) 148 | 149 | elif self.backend == 'openpmd-api': 150 | return io_reader.read_openPMD_params( 151 | self.series, iteration, extract_parameters) 152 | 153 | def read_field_cartesian( self, iteration, field, coord, axis_labels, 154 | slice_relative_position, slice_across ): 155 | """ 156 | Extract a given field from an openPMD file in the openPMD format, 157 | when the geometry is cartesian (1d, 2d or 3d). 158 | 159 | Parameters 160 | ---------- 161 | iteration : int 162 | The iteration at which to extract the fields 163 | 164 | field : string, optional 165 | Which field to extract 166 | 167 | coord : string, optional 168 | Which component of the field to extract 169 | 170 | axis_labels: list of strings 171 | The name of the dimensions of the array (e.g. ['x', 'y', 'z']) 172 | 173 | slice_across : list of str or None 174 | Direction(s) across which the data should be sliced 175 | Elements can be: 176 | - 1d: 'z' 177 | - 2d: 'x' and/or 'z' 178 | - 3d: 'x' and/or 'y' and/or 'z' 179 | Returned array is reduced by 1 dimension per slicing. 180 | 181 | slice_relative_position : list of float or None 182 | Number(s) between -1 and 1 that indicate where to slice the data, 183 | along the directions in `slice_across` 184 | -1 : lower edge of the simulation box 185 | 0 : middle of the simulation box 186 | 1 : upper edge of the simulation box 187 | 188 | Returns 189 | ------- 190 | A tuple with 191 | F : a ndarray containing the required field 192 | info : a FieldMetaInformation object 193 | (contains information about the grid; see the corresponding docstring) 194 | """ 195 | if self.backend == 'h5py': 196 | filename = self.iteration_to_file[iteration] 197 | return h5py_reader.read_field_cartesian( 198 | filename, iteration, field, coord, axis_labels, 199 | slice_relative_position, slice_across ) 200 | elif self.backend == 'openpmd-api': 201 | return io_reader.read_field_cartesian( 202 | self.series, iteration, field, coord, axis_labels, 203 | slice_relative_position, slice_across ) 204 | 205 | def read_field_circ( self, iteration, field, coord, slice_relative_position, 206 | slice_across, m=0, theta=0., max_resolution_3d=None ): 207 | """ 208 | Extract a given field from an openPMD file in the openPMD format, 209 | when the geometry is thetaMode 210 | 211 | Parameters 212 | ---------- 213 | iteration : int 214 | The iteration at which to extract the fields 215 | 216 | field : string, optional 217 | Which field to extract 218 | Either 'rho', 'E', 'B' or 'J' 219 | 220 | coord : string, optional 221 | Which component of the field to extract 222 | Either 'r', 't' or 'z' 223 | 224 | m : int or string, optional 225 | The azimuthal mode to be extracted 226 | 227 | theta : float or None 228 | Angle of the plane of observation with respect to the x axis 229 | If `theta` is not None, then this function returns a 2D array 230 | corresponding to the plane of observation given by `theta` ; 231 | otherwise it returns a full 3D Cartesian array 232 | 233 | slice_across : list of str or None 234 | Direction(s) across which the data should be sliced 235 | Elements can be 'r' and/or 'z' 236 | Returned array is reduced by 1 dimension per slicing. 237 | 238 | slice_relative_position : list of float or None 239 | Number(s) between -1 and 1 that indicate where to slice the data, 240 | along the directions in `slice_across` 241 | -1 : lower edge of the simulation box 242 | 0 : middle of the simulation box 243 | 1 : upper edge of the simulation box 244 | 245 | max_resolution_3d : list of int or None 246 | Maximum resolution that the 3D reconstruction of the field (when 247 | `theta` is None) can have. The list should contain two values, 248 | e.g. `[200, 100]`, indicating the maximum longitudinal and 249 | transverse resolution, respectively. This is useful for 250 | performance reasons, particularly for 3D visualization. 251 | 252 | Returns 253 | ------- 254 | A tuple with 255 | F : a 3darray or 2darray containing the required field, 256 | depending on whether `theta` is None or not 257 | info : a FieldMetaInformation object 258 | (contains information about the grid; see the corresponding docstring) 259 | """ 260 | if self.backend == 'h5py': 261 | filename = self.iteration_to_file[iteration] 262 | return h5py_reader.read_field_circ( 263 | filename, iteration, field, coord, slice_relative_position, 264 | slice_across, m, theta, max_resolution_3d ) 265 | elif self.backend == 'openpmd-api': 266 | return io_reader.read_field_circ( 267 | self.series, iteration, field, coord, slice_relative_position, 268 | slice_across, m, theta, max_resolution_3d ) 269 | 270 | def read_species_data( self, iteration, species, record_comp, extensions): 271 | """ 272 | Extract a given species' record_comp 273 | 274 | Parameters 275 | ---------- 276 | iteration: int 277 | The iteration at which to extract the species data 278 | 279 | species: string 280 | The name of the species to extract (in the openPMD file) 281 | 282 | record_comp: string 283 | The record component to extract 284 | Either 'x', 'y', 'z', 'r', 'ux', 'uy', 'uz', 'ur', or 'w' 285 | 286 | extensions: list of strings 287 | The extensions that the current OpenPMDTimeSeries complies with 288 | """ 289 | if self.backend == 'h5py': 290 | filename = self.iteration_to_file[iteration] 291 | return h5py_reader.read_species_data( 292 | filename, iteration, species, record_comp, extensions ) 293 | elif self.backend == 'openpmd-api': 294 | return io_reader.read_species_data( 295 | self.series, iteration, species, record_comp, extensions ) 296 | 297 | def get_grid_parameters(self, iteration, avail_fields, metadata ): 298 | """ 299 | Return the parameters of the spatial grid (grid size and grid range) 300 | in two dictionaries 301 | 302 | Parameters: 303 | ----------- 304 | iteration: int 305 | The iteration at which to extract the parameters 306 | 307 | avail_fields: list 308 | A list of the available fields 309 | e.g. ['B', 'E', 'rho'] 310 | 311 | metadata: dictionary 312 | A dictionary whose keys are the fields of `avail_fields` and 313 | whose values are dictionaries that contain metadata (e.g. geometry) 314 | 315 | Returns: 316 | -------- 317 | A tuple with `grid_size_dict` and `grid_range_dict` 318 | Both objects are dictionaries, with their keys being the labels of 319 | the axis of the grid (e.g. 'x', 'y', 'z') 320 | The values of `grid_size_dict` are the number of gridpoints along 321 | each axis. 322 | The values of `grid_range_dict` are lists of two floats, which 323 | correspond to the min and max of the grid, along each axis. 324 | """ 325 | if self.backend == 'h5py': 326 | filename = self.iteration_to_file[iteration] 327 | return h5py_reader.get_grid_parameters( 328 | filename, iteration, avail_fields, metadata ) 329 | elif self.backend == 'openpmd-api': 330 | return io_reader.get_grid_parameters( 331 | self.series, iteration, avail_fields, metadata ) 332 | -------------------------------------------------------------------------------- /openpmd_viewer/openpmd_timeseries/data_reader/h5py_reader/__init__.py: -------------------------------------------------------------------------------- 1 | from .particle_reader import read_species_data 2 | from .params_reader import read_openPMD_params 3 | from .field_reader import read_field_cartesian, \ 4 | read_field_circ, get_grid_parameters 5 | from .utilities import list_files 6 | 7 | __all__ = ['read_species_data', 'read_openPMD_params', 'list_files', 8 | 'read_field_cartesian', 'read_field_circ', 'get_grid_parameters'] 9 | -------------------------------------------------------------------------------- /openpmd_viewer/openpmd_timeseries/data_reader/h5py_reader/params_reader.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the openPMD-viewer. 3 | 4 | It defines a function that can read standard parameters from an openPMD file. 5 | 6 | Copyright 2015-2016, openPMD-viewer contributors 7 | Authors: Remi Lehe, Axel Huebl 8 | License: 3-Clause-BSD-LBNL 9 | """ 10 | 11 | import h5py 12 | import numpy as np 13 | from .utilities import is_scalar_record, get_shape, join_infile_path 14 | 15 | 16 | def read_openPMD_params(filename, iteration, extract_parameters=True): 17 | """ 18 | Extract the time and some openPMD parameters from a file 19 | 20 | Parameter 21 | --------- 22 | filename: string 23 | The path to the file from which parameters should be extracted 24 | 25 | iteration : int 26 | The iteration at which to obtain the data 27 | 28 | extract_parameters: bool, optional 29 | Whether to extract all parameters or only the time 30 | (Function execution is faster when extract_parameters is False) 31 | 32 | Returns 33 | ------- 34 | A tuple with: 35 | - A float corresponding to the time of this iteration in SI units 36 | - A dictionary containing several parameters, such as the geometry, etc. 37 | When extract_parameters is False, the second argument returned is None. 38 | """ 39 | # Open the file, and do a version check 40 | f = h5py.File(filename, 'r') 41 | version = f.attrs['openPMD'].decode() 42 | if version[:2] != '1.': 43 | raise ValueError( 44 | "File %s is not supported: Invalid openPMD version: " 45 | "%s)" % (filename, version)) 46 | 47 | # Find the base path object, and extract the time 48 | bpath = f['/data/{0}'.format( iteration )] 49 | t = bpath.attrs["time"] * bpath.attrs["timeUnitSI"] 50 | 51 | # If the user did not request more parameters, close file and exit 52 | if not extract_parameters: 53 | f.close() 54 | return(t, None) 55 | 56 | # Otherwise, extract the rest of the parameters 57 | params = {} 58 | 59 | # Find out supported openPMD extensions claimed by this file 60 | # note: a file might implement multiple extensions 61 | known_extensions = {'ED-PIC': np.uint32(1)} 62 | bitmask_all_extensions = f.attrs['openPMDextension'] 63 | params['extensions'] = [] 64 | for extension, bitmask in known_extensions.items(): 65 | # This uses a bitmask to identify activated extensions 66 | # efficiently in static programming languages via 67 | # a single attribute and a binary AND (&) operation. 68 | # Standard: https://git.io/vwnMw 69 | # Bitmasks: https://en.wikipedia.org/wiki/Mask_%28computing%29 70 | if bitmask_all_extensions & bitmask == bitmask: 71 | params['extensions'].append(extension) 72 | 73 | # Find out whether fields are present and extract their metadata 74 | fields_available = False 75 | if ('meshesPath' in f.attrs): # Check for openPMD 1.1 files 76 | meshes_path = f.attrs['meshesPath'].decode().strip('/') 77 | if meshes_path in bpath.keys(): # Check for openPMD 1.0 files 78 | fields_available = True 79 | if fields_available: 80 | params['avail_fields'] = [] 81 | params['fields_metadata'] = {} 82 | 83 | # Loop through the available fields 84 | for field_name in bpath[meshes_path].keys(): 85 | field = bpath[join_infile_path(meshes_path, field_name)] 86 | metadata = {} 87 | metadata['geometry'] = field.attrs['geometry'].decode() 88 | metadata['axis_labels'] = [ coord.decode() for coord in 89 | field.attrs['axisLabels'] ] 90 | # Swap the order of the labels if the code that wrote the HDF5 file 91 | # was Fortran order (i.e. reverse order with respect to Python) 92 | if field.attrs['dataOrder'].decode() == 'F': 93 | metadata['axis_labels'] = metadata['axis_labels'][::-1] 94 | # Check whether the field is a vector or a scalar 95 | if is_scalar_record(field): 96 | metadata['type'] = 'scalar' 97 | components = [] 98 | else: 99 | metadata['type'] = 'vector' 100 | components = list(field.keys()) 101 | # Register available components 102 | metadata['avail_components'] = components 103 | # Check the number of modes 104 | if metadata['geometry'] == "thetaMode": 105 | if is_scalar_record(field): 106 | Nm, _, _ = get_shape(field) 107 | else: 108 | coord = list(field.keys())[0] 109 | Nm, _, _ = get_shape(field[coord]) 110 | metadata['avail_circ_modes'] = ['all'] + \ 111 | [str(m) for m in range(int(Nm / 2) + 1)] 112 | # Check if this a 1d, 2d or 3d Cartesian 113 | elif metadata['geometry'] == "cartesian": 114 | dim = len(metadata['axis_labels']) 115 | if dim == 1: 116 | metadata['geometry'] = "1dcartesian" 117 | elif dim == 2: 118 | metadata['geometry'] = "2dcartesian" 119 | elif dim == 3: 120 | metadata['geometry'] = "3dcartesian" 121 | metadata['avail_circ_modes'] = [] 122 | 123 | params['avail_fields'].append( field_name ) 124 | params['fields_metadata'][field_name] = metadata 125 | 126 | else: 127 | params['avail_fields'] = None 128 | 129 | # Find out whether particles are present, and if yes of which species 130 | particles_available = False 131 | if ('particlesPath' in f.attrs): # Check for openPMD 1.1 files 132 | particle_path = f.attrs['particlesPath'].decode().strip('/') 133 | if particle_path in bpath.keys(): # Check for openPMD 1.0 files 134 | # Check that there is at least one species 135 | if len(bpath[particle_path].keys()) > 0: 136 | particles_available = True 137 | if particles_available: 138 | # Particles are present ; extract the species 139 | params['avail_species'] = [] 140 | for species_name in bpath[particle_path].keys(): 141 | params['avail_species'].append(species_name) 142 | # dictionary with list of record components for each species 143 | record_components = {} 144 | # Go through all species 145 | for species_name in iter(params['avail_species']): 146 | species = bpath[join_infile_path(particle_path, species_name)] 147 | record_components[species_name] = [] 148 | 149 | # Go through all the particle records of this species 150 | for record_name in species.keys(): 151 | # Skip the particlePatches, which are not used here. 152 | if record_name == 'particlePatches': 153 | continue 154 | record = species[record_name] 155 | if is_scalar_record(record): 156 | # Add the name of the scalar record 157 | record_components[species_name]. \ 158 | append(record_name) 159 | else: 160 | # Add each component of the vector record 161 | for coord in record.keys(): 162 | record_components[species_name]. \ 163 | append(join_infile_path(record_name, coord)) 164 | # Simplify the name of some standard openPMD records 165 | record_components[species_name] = \ 166 | simplify_record(record_components[species_name]) 167 | params['avail_record_components'] = record_components 168 | # deprecated 169 | first_species_name = next(iter(params['avail_species'])) 170 | params['avail_ptcl_quantities'] = \ 171 | record_components[first_species_name] 172 | else: 173 | # Particles are absent 174 | params['avail_species'] = None 175 | params['avail_record_components'] = None 176 | # deprecated 177 | params['avail_ptcl_quantities'] = None 178 | 179 | # Close the file and return the parameters 180 | f.close() 181 | return(t, params) 182 | 183 | 184 | def simplify_record(record_comps): 185 | """ 186 | Replace the names of some standard record by shorter names 187 | 188 | Parameter 189 | --------- 190 | record_comps: a list of strings 191 | A list of available particle record components 192 | 193 | Returns 194 | ------- 195 | A list with shorter names, where applicable 196 | """ 197 | # Replace the names of the positions 198 | if ('position/x' in record_comps) and ('positionOffset/x' in record_comps): 199 | record_comps.remove('position/x') 200 | record_comps.remove('positionOffset/x') 201 | record_comps.append('x') 202 | if ('position/y' in record_comps) and ('positionOffset/y' in record_comps): 203 | record_comps.remove('position/y') 204 | record_comps.remove('positionOffset/y') 205 | record_comps.append('y') 206 | if ('position/z' in record_comps) and ('positionOffset/z' in record_comps): 207 | record_comps.remove('position/z') 208 | record_comps.remove('positionOffset/z') 209 | record_comps.append('z') 210 | if ('position/r' in record_comps) and ('positionOffset/r' in record_comps): 211 | record_comps.remove('position/r') 212 | record_comps.remove('positionOffset/r') 213 | record_comps.append('r') 214 | 215 | # Replace the names of the momenta 216 | if 'momentum/x' in record_comps: 217 | record_comps.remove('momentum/x') 218 | record_comps.append('ux') 219 | if 'momentum/y' in record_comps: 220 | record_comps.remove('momentum/y') 221 | record_comps.append('uy') 222 | if 'momentum/z' in record_comps: 223 | record_comps.remove('momentum/z') 224 | record_comps.append('uz') 225 | if 'momentum/r' in record_comps: 226 | record_comps.remove('momentum/r') 227 | record_comps.append('ur') 228 | 229 | # Replace the name for 'weights' 230 | if 'weighting' in record_comps: 231 | record_comps.remove('weighting') 232 | record_comps.append('w') 233 | 234 | return(record_comps) 235 | -------------------------------------------------------------------------------- /openpmd_viewer/openpmd_timeseries/data_reader/h5py_reader/particle_reader.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the openPMD-viewer. 3 | 4 | It defines a function that reads a species record component (data & meta) 5 | from an openPMD file 6 | 7 | Copyright 2015-2016, openPMD-viewer contributors 8 | Authors: Remi Lehe, Axel Huebl 9 | License: 3-Clause-BSD-LBNL 10 | """ 11 | 12 | import h5py 13 | import numpy as np 14 | from scipy import constants 15 | from .utilities import get_data, join_infile_path 16 | 17 | 18 | def read_species_data(filename, iteration, species, record_comp, extensions): 19 | """ 20 | Extract a given species' record_comp 21 | 22 | Parameters 23 | ---------- 24 | filename : string 25 | The absolute path to the HDF5 file 26 | 27 | iteration : int 28 | The iteration at which to obtain the data 29 | 30 | species: string 31 | The name of the species to extract (in the openPMD file) 32 | 33 | record_comp: string 34 | The record component to extract 35 | Either 'x', 'y', 'z', 'r', 'ux', 'uy', 'uz', 'ur', or 'w' 36 | 37 | extensions: list of strings 38 | The extensions that the current OpenPMDTimeSeries complies with 39 | """ 40 | # Open the HDF5 file 41 | dfile = h5py.File( filename, 'r' ) 42 | # Translate the record component to the openPMD format 43 | dict_record_comp = {'x': 'position/x', 44 | 'y': 'position/y', 45 | 'z': 'position/z', 46 | 'r': 'position/r', 47 | 'ux': 'momentum/x', 48 | 'uy': 'momentum/y', 49 | 'uz': 'momentum/z', 50 | 'ur': 'momentum/r', 51 | 'w': 'weighting'} 52 | if record_comp in dict_record_comp: 53 | opmd_record_comp = dict_record_comp[record_comp] 54 | else: 55 | opmd_record_comp = record_comp 56 | 57 | # Open the HDF5 file 58 | base_path = '/data/{0}'.format( iteration ) 59 | particles_path = dfile.attrs['particlesPath'].decode() 60 | 61 | # Extract the right dataset 62 | species_grp = dfile[ 63 | join_infile_path(base_path, particles_path, species) ] 64 | if opmd_record_comp == 'id': 65 | output_type = np.uint64 66 | else: 67 | output_type = np.float64 68 | data = get_data( species_grp[ opmd_record_comp ], output_type=output_type ) 69 | 70 | # For ED-PIC: if the data is weighted for a full macroparticle, 71 | # divide by the weight with the proper power 72 | # (Skip this if the current record component is the weight itself) 73 | if 'ED-PIC' in extensions and opmd_record_comp != 'weighting': 74 | opmd_record = opmd_record_comp.split('/')[0] 75 | record_dset = species_grp[ opmd_record ] 76 | macro_weighted = record_dset.attrs['macroWeighted'] 77 | weighting_power = record_dset.attrs['weightingPower'] 78 | if (macro_weighted == 1) and (weighting_power != 0): 79 | w = get_data( species_grp[ 'weighting' ] ) 80 | data *= w ** (-weighting_power) 81 | 82 | # - Return positions, with an offset 83 | if record_comp in ['x', 'y', 'z', 'r']: 84 | offset = get_data(species_grp['positionOffset/%s' % record_comp]) 85 | data += offset 86 | # - Return momentum in normalized units 87 | elif record_comp in ['ux', 'uy', 'uz', 'ur']: 88 | m = get_data(species_grp['mass']) 89 | # Normalize only if the particle mass is non-zero 90 | if np.all( m != 0 ): 91 | norm_factor = 1. / (m * constants.c) 92 | data *= norm_factor 93 | 94 | # Close the file 95 | dfile.close() 96 | # Return the data 97 | return(data) 98 | -------------------------------------------------------------------------------- /openpmd_viewer/openpmd_timeseries/data_reader/h5py_reader/utilities.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the openPMD-viewer. 3 | 4 | It defines a set of helper data and functions which 5 | are used by the other files. 6 | 7 | Copyright 2015-2016, openPMD-viewer contributors 8 | Authors: Remi Lehe, Axel Huebl 9 | License: 3-Clause-BSD-LBNL 10 | """ 11 | import os 12 | import h5py 13 | import numpy as np 14 | 15 | 16 | def list_files(path_to_dir): 17 | """ 18 | Return a list of the hdf5 files in this directory, 19 | and a list of the corresponding iterations 20 | 21 | Parameter 22 | --------- 23 | path_to_dir : string 24 | The path to the directory where the hdf5 files are. 25 | 26 | Returns 27 | ------- 28 | A tuple with: 29 | - an array of integers which correspond to the iteration of each file 30 | - a dictionary that matches iterations to the corresponding filename 31 | """ 32 | # group based encoding? 33 | is_single_file = os.path.isfile(path_to_dir) 34 | 35 | if is_single_file: 36 | all_files = [path_to_dir] 37 | else: 38 | # Find all the files in the provided directory 39 | all_files = os.listdir(path_to_dir) 40 | 41 | # Select the hdf5 files, and fill dictionary of correspondence 42 | # between iterations and files 43 | iteration_to_file = {} 44 | for filename in all_files: 45 | # Use only the name that end with .h5 or .hdf5 46 | if filename.endswith('.h5') or filename.endswith('.hdf5'): 47 | if is_single_file: 48 | full_name = filename 49 | else: 50 | full_name = os.path.join( 51 | os.path.abspath(path_to_dir), filename) 52 | # extract all iterations from hdf5 file 53 | f = h5py.File(full_name, 'r') 54 | iterations = list(f['/data'].keys()) 55 | f.close() 56 | # Add iterations to dictionary 57 | for key_iteration in iterations: 58 | iteration_to_file[ int(key_iteration) ] = full_name 59 | 60 | # Extract iterations and sort them 61 | iterations = np.array( sorted( list( iteration_to_file.keys() ) ) ) 62 | 63 | return iterations, iteration_to_file 64 | 65 | 66 | def is_scalar_record(record): 67 | """ 68 | Determine whether a record is a scalar record or a vector record 69 | 70 | Parameter 71 | --------- 72 | record: an h5py Dataset or an h5py Group 73 | 74 | Return 75 | ------ 76 | A boolean indicating whether the record is scalar 77 | """ 78 | scalar = False 79 | if 'value' in record.attrs: 80 | scalar = True 81 | elif isinstance(record, h5py.Dataset): 82 | scalar = True 83 | 84 | return(scalar) 85 | 86 | 87 | def get_data(dset, i_slice=None, pos_slice=None, output_type=None): 88 | """ 89 | Extract the data from a (possibly constant) dataset 90 | Slice the data according to the parameters i_slice and pos_slice 91 | 92 | Parameters: 93 | ----------- 94 | dset: an h5py.Dataset or h5py.Group (when constant) 95 | The object from which the data is extracted 96 | 97 | pos_slice: int or list of int, optional 98 | Slice direction(s). 99 | When None, no slicing is performed 100 | 101 | i_slice: int or list of int, optional 102 | Indices of slices to be taken. 103 | 104 | output_type: a numpy type 105 | The type to which the returned array should be converted 106 | 107 | Returns: 108 | -------- 109 | An np.ndarray (non-constant dataset) or a single double (constant dataset) 110 | """ 111 | # For back-compatibility: Convert pos_slice and i_slice to 112 | # single-element lists if they are not lists (e.g. float 113 | # and int respectively). 114 | if pos_slice is not None and not isinstance(pos_slice, list): 115 | pos_slice = [pos_slice] 116 | if i_slice is not None and not isinstance(i_slice, list): 117 | i_slice = [i_slice] 118 | # Case of a constant dataset 119 | if isinstance(dset, h5py.Group): 120 | shape = dset.attrs['shape'] 121 | # Restrict the shape if slicing is enabled 122 | if pos_slice is not None: 123 | shape = [ x for index, x in enumerate(shape) if 124 | index not in pos_slice ] 125 | # Create the corresponding dataset 126 | data = dset.attrs['value'] * np.ones(shape) 127 | 128 | # Case of a non-constant dataset 129 | elif isinstance(dset, h5py.Dataset): 130 | if pos_slice is None: 131 | data = dset[...] 132 | else: 133 | # Get largest element of pos_slice 134 | max_pos = max(pos_slice) 135 | # Create list of indices list_index of type 136 | # [:, :, :, ...] where Ellipsis starts at max_pos + 1 137 | list_index = [np.s_[:]] * (max_pos + 2) 138 | list_index[max_pos + 1] = np.s_[...] 139 | # Fill list_index with elements of i_slice 140 | for count, dir_index in enumerate(pos_slice): 141 | list_index[dir_index] = i_slice[count] 142 | # Convert list_index into a tuple 143 | tuple_index = tuple(list_index) 144 | # Slice dset according to tuple_index 145 | data = dset[tuple_index] 146 | 147 | # Convert to the right type 148 | if (output_type is not None) and (data.dtype != output_type): 149 | data = data.astype( output_type ) 150 | # Scale by the conversion factor 151 | if np.issubdtype(data.dtype, np.floating) or \ 152 | np.issubdtype(data.dtype, np.complexfloating): 153 | if dset.attrs['unitSI'] != 1.0: 154 | data *= dset.attrs['unitSI'] 155 | 156 | return(data) 157 | 158 | 159 | def get_shape(dset): 160 | """ 161 | Extract the shape of a (possibly constant) dataset 162 | 163 | Parameters: 164 | ----------- 165 | dset: an h5py.Dataset or h5py.Group (when constant) 166 | The object whose shape is extracted 167 | 168 | Returns: 169 | -------- 170 | A tuple corresponding to the shape 171 | """ 172 | # Case of a constant dataset 173 | if isinstance(dset, h5py.Group): 174 | shape = dset.attrs['shape'] 175 | # Case of a non-constant dataset 176 | elif isinstance(dset, h5py.Dataset): 177 | shape = dset.shape 178 | 179 | return(shape) 180 | 181 | 182 | def join_infile_path(*paths): 183 | """ 184 | Join path components using '/' as separator. 185 | This method is defined as an alternative to os.path.join, which uses '\\' 186 | as separator in Windows environments and is therefore not valid to navigate 187 | within data files. 188 | 189 | Parameters: 190 | ----------- 191 | *paths: all strings with path components to join 192 | 193 | Returns: 194 | -------- 195 | A string with the complete path using '/' as separator. 196 | """ 197 | # Join path components 198 | path = '/'.join(paths) 199 | # Correct double slashes, if any is present 200 | path = path.replace('//', '/') 201 | 202 | return path 203 | -------------------------------------------------------------------------------- /openpmd_viewer/openpmd_timeseries/data_reader/io_reader/__init__.py: -------------------------------------------------------------------------------- 1 | from .particle_reader import read_species_data 2 | from .params_reader import read_openPMD_params 3 | from .field_reader import read_field_cartesian, \ 4 | read_field_circ, get_grid_parameters 5 | 6 | __all__ = ['read_species_data', 'read_openPMD_params', 'read_field_cartesian', 7 | 'read_field_circ', 'get_grid_parameters'] 8 | -------------------------------------------------------------------------------- /openpmd_viewer/openpmd_timeseries/data_reader/io_reader/field_reader.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the openPMD-viewer. 3 | 4 | It defines functions that can read the fields from an HDF5 file. 5 | 6 | Copyright 2020, openPMD-viewer contributors 7 | Author: Axel Huebl 8 | License: 3-Clause-BSD-LBNL 9 | """ 10 | 11 | import numpy as np 12 | 13 | from .utilities import get_data 14 | from ...data_order import RZorder, order_error_msg 15 | from openpmd_viewer.openpmd_timeseries.field_metainfo import FieldMetaInformation 16 | from openpmd_viewer.openpmd_timeseries.utilities import construct_3d_from_circ 17 | 18 | 19 | def read_field_cartesian( series, iteration, field_name, component_name, 20 | axis_labels, slice_relative_position, slice_across ): 21 | """ 22 | Extract a given field from a file in the openPMD format, 23 | when the geometry is cartesian (1d, 2d or 3d). 24 | 25 | Parameters 26 | ---------- 27 | series: openpmd_api.Series 28 | An open, readable openPMD-api series object 29 | 30 | iteration: integer 31 | Iteration from which parameters should be extracted 32 | 33 | field_name : string, optional 34 | Which field to extract 35 | 36 | component_name : string, optional 37 | Which component of the field to extract 38 | 39 | axis_labels: list of strings 40 | The name of the dimensions of the array (e.g. ['x', 'y', 'z']) 41 | 42 | slice_across : list of str or None 43 | Direction(s) across which the data should be sliced 44 | Elements can be: 45 | - 1d: 'z' 46 | - 2d: 'x' and/or 'z' 47 | - 3d: 'x' and/or 'y' and/or 'z' 48 | Returned array is reduced by 1 dimension per slicing. 49 | 50 | slice_relative_position : list of float or None 51 | Number(s) between -1 and 1 that indicate where to slice the data, 52 | along the directions in `slice_across` 53 | -1 : lower edge of the simulation box 54 | 0 : middle of the simulation box 55 | 1 : upper edge of the simulation box 56 | 57 | Returns 58 | ------- 59 | A tuple with 60 | F : a ndarray containing the required field 61 | info : a FieldMetaInformation object 62 | (contains information about the grid; see the corresponding docstring) 63 | """ 64 | it = series.iterations[iteration] 65 | 66 | # Extract the dataset and corresponding group 67 | field = it.meshes[field_name] 68 | if field.scalar: 69 | component = next(field.items())[1] 70 | else: 71 | component = field[component_name] 72 | 73 | # Dimensions of the grid 74 | shape = component.shape 75 | # FIXME here and in h5py reader, we need to invert the order on 'F' 76 | grid_spacing = field.grid_spacing 77 | global_offset = field.grid_global_offset 78 | grid_unit_SI = field.grid_unit_SI 79 | grid_position = component.position 80 | time = (it.time + field.time_offset) * it.time_unit_SI 81 | 82 | if field.get_attribute('dataOrder') == 'F': 83 | grid_spacing = grid_spacing[::-1] 84 | global_offset = global_offset[::-1] 85 | grid_position = grid_position[::-1] 86 | 87 | field_attrs = {a: field.get_attribute(a) for a in field.attributes} 88 | component_attrs = {a: component.get_attribute(a) for a in component.attributes} 89 | 90 | # Slice selection 91 | # TODO put in general utilities 92 | if slice_across is not None: 93 | # Get the integer that correspond to the slicing direction 94 | list_slicing_index = [] 95 | list_i_cell = [] 96 | for count, slice_across_item in enumerate(slice_across): 97 | slicing_index = axis_labels.index(slice_across_item) 98 | list_slicing_index.append(slicing_index) 99 | # Number of cells along the slicing direction 100 | n_cells = shape[ slicing_index ] 101 | # Index of the slice (prevent stepping out of the array) 102 | i_cell = int( 0.5 * (slice_relative_position[count] + 1.) * n_cells ) 103 | i_cell = max( i_cell, 0 ) 104 | i_cell = min( i_cell, n_cells - 1) 105 | list_i_cell.append(i_cell) 106 | 107 | # Remove metainformation relative to the slicing index 108 | # Successive pops starting from last coordinate to slice 109 | shape = [ x for index, x in enumerate(shape) 110 | if index not in list_slicing_index ] 111 | grid_spacing = [ x for index, x in enumerate(grid_spacing) 112 | if index not in list_slicing_index ] 113 | global_offset = [ x for index, x in enumerate(global_offset) 114 | if index not in list_slicing_index ] 115 | axis_labels = [ x for index, x in enumerate(axis_labels) 116 | if index not in list_slicing_index ] 117 | 118 | axes = { i: axis_labels[i] for i in range(len(axis_labels)) } 119 | # Extract data 120 | F = get_data( series, component, list_i_cell, list_slicing_index ) 121 | info = FieldMetaInformation( axes, shape, grid_spacing, global_offset, 122 | grid_unit_SI, grid_position, 123 | time, iteration, field_attrs=field_attrs, 124 | component_attrs=component_attrs ) 125 | else: 126 | F = get_data( series, component ) 127 | axes = { i: axis_labels[i] for i in range(len(axis_labels)) } 128 | info = FieldMetaInformation( axes, F.shape, 129 | grid_spacing, global_offset, 130 | grid_unit_SI, grid_position, 131 | time, iteration, field_attrs=field_attrs, 132 | component_attrs=component_attrs ) 133 | 134 | return F, info 135 | 136 | 137 | def read_field_circ( series, iteration, field_name, component_name, 138 | slice_relative_position, slice_across, m=0, theta=0., 139 | max_resolution_3d=None ): 140 | """ 141 | Extract a given field from a file in the openPMD format, 142 | when the geometry is thetaMode 143 | 144 | Parameters 145 | ---------- 146 | series: openpmd_api.Series 147 | An open, readable openPMD-api series object 148 | 149 | iteration: integer 150 | Iteration from which parameters should be extracted 151 | 152 | field_name : string, optional 153 | Which field to extract 154 | 155 | component_name : string, optional 156 | Which component of the field to extract 157 | 158 | m : int or string, optional 159 | The azimuthal mode to be extracted 160 | 161 | theta : float or None 162 | Angle of the plane of observation with respect to the x axis 163 | If `theta` is not None, then this function returns a 2D array 164 | corresponding to the plane of observation given by `theta` ; 165 | otherwise it returns a full 3D Cartesian array 166 | 167 | slice_across : list of str or None 168 | Direction(s) across which the data should be sliced 169 | Elements can be 'r' and/or 'z' 170 | Returned array is reduced by 1 dimension per slicing. 171 | 172 | slice_relative_position : list of float or None 173 | Number(s) between -1 and 1 that indicate where to slice the data, 174 | along the directions in `slice_across` 175 | -1 : lower edge of the simulation box 176 | 0 : middle of the simulation box 177 | 1 : upper edge of the simulation box 178 | 179 | max_resolution_3d : list of int or None 180 | Maximum resolution that the 3D reconstruction of the field (when 181 | `theta` is None) can have. The list should contain two values, 182 | e.g. `[200, 100]`, indicating the maximum longitudinal and transverse 183 | resolution, respectively. This is useful for performance reasons, 184 | particularly for 3D visualization. 185 | 186 | Returns 187 | ------- 188 | A tuple with 189 | F : a 3darray or 2darray containing the required field, 190 | depending on whether `theta` is None or not 191 | info : a FieldMetaInformation object 192 | (contains information about the grid; see the corresponding docstring) 193 | """ 194 | it = series.iterations[iteration] 195 | 196 | # Extract the dataset and corresponding group 197 | field = it.meshes[field_name] 198 | if field.scalar: 199 | component = next(field.items())[1] 200 | else: 201 | component = field[component_name] 202 | 203 | field_attrs = {a: field.get_attribute(a) for a in field.attributes} 204 | component_attrs = {a: component.get_attribute(a) for a in component.attributes} 205 | 206 | # Extract the metainformation 207 | # FIXME here and in h5py reader, we need to invert the order on 'F' for 208 | # grid spacing/offset/position 209 | 210 | coord_labels = {ii: coord for (ii, coord) in enumerate(field.axis_labels)} 211 | 212 | if coord_labels[0] == 'r': 213 | coord_order = RZorder.mrz 214 | Nm, Nr, Nz = component.shape 215 | N_pair = (Nr, Nz) 216 | elif coord_labels[1] == 'r': 217 | Nm, Nz, Nr = component.shape 218 | N_pair = (Nz, Nr) 219 | coord_order = RZorder.mzr 220 | else: 221 | raise Exception(order_error_msg) 222 | time = (it.time + field.time_offset) * it.time_unit_SI 223 | 224 | # Nm, Nr, Nz = component.shape 225 | info = FieldMetaInformation( coord_labels, N_pair, 226 | field.grid_spacing, field.grid_global_offset, 227 | field.grid_unit_SI, component.position, time, iteration, 228 | thetaMode=True, field_attrs=field_attrs, 229 | component_attrs=component_attrs ) 230 | 231 | # Convert to a 3D Cartesian array if theta is None 232 | if theta is None: 233 | 234 | # Get cylindrical info 235 | rmax = info.rmax 236 | inv_dr = 1./info.dr 237 | Fcirc = get_data( series, component ) # (Extracts all modes) 238 | if m == 'all': 239 | modes = [ mode for mode in range(0, int(Nm / 2) + 1) ] 240 | else: 241 | modes = [ m ] 242 | modes = np.array( modes, dtype='int' ) 243 | nmodes = len(modes) 244 | 245 | # If necessary, reduce resolution of 3D reconstruction 246 | if max_resolution_3d is not None: 247 | max_res_lon, max_res_transv = max_resolution_3d 248 | if Nz > max_res_lon: 249 | # Calculate excess of elements along z 250 | excess_z = int(np.round(Nz/max_res_lon)) 251 | # Preserve only one every excess_z elements 252 | if coord_order is RZorder.mrz: 253 | Fcirc = Fcirc[:, :, ::excess_z] 254 | elif coord_order is RZorder.mzr: 255 | Fcirc = Fcirc[:, ::excess_z, :] 256 | else: 257 | raise Exception(order_error_msg) 258 | # Update info accordingly 259 | info.z = info.z[::excess_z] 260 | info.dz = info.z[1] - info.z[0] 261 | if Nr > max_res_transv/2: 262 | # Calculate excess of elements along r 263 | excess_r = int(np.round(Nr/(max_res_transv/2))) 264 | # Preserve only one every excess_r elements 265 | if coord_order is RZorder.mrz: 266 | Fcirc = Fcirc[:, ::excess_r, :] 267 | elif coord_order is RZorder.mzr: 268 | Fcirc = Fcirc[:, :, ::excess_r] 269 | else: 270 | raise Exception(order_error_msg) 271 | # Update info and necessary parameters accordingly 272 | info.r = info.r[::excess_r] 273 | info.dr = info.r[1] - info.r[0] 274 | inv_dr = 1./info.dr 275 | # Update Nr after reducing radial resolution. 276 | if coord_order is RZorder.mrz: 277 | Nr = Fcirc.shape[1] 278 | elif coord_order is RZorder.mzr: 279 | Nr = Fcirc.shape[2] 280 | else: 281 | raise Exception(order_error_msg) 282 | 283 | # Convert cylindrical data to Cartesian data 284 | info._convert_cylindrical_to_3Dcartesian() 285 | nx, ny, nz = len(info.x), len(info.y), len(info.z) 286 | F_total = np.zeros( (nx, ny, nz), dtype=component.dtype ) 287 | construct_3d_from_circ( F_total, Fcirc, info.x, info.y, modes, 288 | nx, ny, nz, Nr, nmodes, inv_dr, rmax, coord_order) 289 | 290 | else: 291 | 292 | # Extract the modes and recombine them properly 293 | if m == 'all': 294 | # Sum of all the modes 295 | # - Prepare the multiplier arrays 296 | mult_above_axis = [1] 297 | mult_below_axis = [1] 298 | for mode in range(1, int(Nm / 2) + 1): 299 | cos = np.cos( mode * theta ) 300 | sin = np.sin( mode * theta ) 301 | mult_above_axis += [cos, sin] 302 | mult_below_axis += [ (-1) ** mode * cos, (-1) ** mode * sin ] 303 | mult_above_axis = np.array( mult_above_axis ) 304 | mult_below_axis = np.array( mult_below_axis ) 305 | # - Sum the modes 306 | F = get_data( series, component ) # (Extracts all modes) 307 | if coord_order is RZorder.mrz: 308 | F_total = np.zeros( (2 * Nr, Nz ), dtype=F.dtype ) 309 | F_total[Nr:, :] = np.tensordot( mult_above_axis, 310 | F, axes=(0, 0) )[:, :] 311 | F_total[:Nr, :] = np.tensordot( mult_below_axis, 312 | F, axes=(0, 0) )[::-1, :] 313 | elif coord_order is RZorder.mzr: 314 | F_total = np.zeros( (Nz, 2 * Nr ), dtype=F.dtype ) 315 | F_total[:, Nr:] = np.tensordot( mult_above_axis, 316 | F, axes=(0, 0) )[:, :] 317 | F_total[:, :Nr] = np.tensordot( mult_below_axis, 318 | F, axes=(0, 0) )[:, ::-1] 319 | else: 320 | raise Exception(order_error_msg) 321 | elif m == 0: 322 | # Extract mode 0 323 | F = get_data( series, component, 0, 0 ) 324 | if coord_order is RZorder.mrz: 325 | F_total = np.zeros( (2 * Nr, Nz ), dtype=F.dtype ) 326 | F_total[Nr:, :] = F[:, :] 327 | F_total[:Nr, :] = F[::-1, :] 328 | elif coord_order is RZorder.mzr: 329 | F_total = np.zeros( (Nz, 2 * Nr ), dtype=F.dtype ) 330 | F_total[:, Nr:] = F[:, :] 331 | F_total[:, :Nr] = F[:, ::-1] 332 | else: 333 | raise Exception(order_error_msg) 334 | else: 335 | # Extract higher mode 336 | cos = np.cos( m * theta ) 337 | sin = np.sin( m * theta ) 338 | F_cos = get_data( series, component, 2 * m - 1, 0 ) 339 | F_sin = get_data( series, component, 2 * m, 0 ) 340 | F = cos * F_cos + sin * F_sin 341 | if coord_order is RZorder.mrz: 342 | F_total = np.zeros( (2 * Nr, Nz ), dtype=F.dtype ) 343 | F_total[Nr:, :] = F[:, :] 344 | F_total[:Nr, :] = (-1) ** m * F[::-1, :] 345 | elif coord_order is RZorder.mzr: 346 | F_total = np.zeros( (Nz, 2 * Nr ), dtype=F.dtype ) 347 | F_total[:, Nr:] = F[:, :] 348 | F_total[:, :Nr] = (-1) ** m * F[:, ::-1] 349 | else: 350 | raise Exception(order_error_msg) 351 | 352 | # Perform slicing if needed 353 | if slice_across is not None: 354 | # Slice field and clear metadata 355 | inverted_axes_dict = {info.axes[key]: key for key in info.axes.keys()} 356 | for count, slice_across_item in enumerate(slice_across): 357 | slicing_index = inverted_axes_dict[slice_across_item] 358 | coord_array = getattr( info, slice_across_item ) 359 | # Number of cells along the slicing direction 360 | n_cells = len(coord_array) 361 | # Index of the slice (prevent stepping out of the array) 362 | i_cell = int( 0.5 * (slice_relative_position[count] + 1.) * n_cells ) 363 | i_cell = max( i_cell, 0 ) 364 | i_cell = min( i_cell, n_cells - 1) 365 | F_total = np.take( F_total, [i_cell], axis=slicing_index ) 366 | F_total = np.squeeze(F_total) 367 | # Remove the sliced labels from the FieldMetaInformation 368 | for slice_across_item in slice_across: 369 | info._remove_axis(slice_across_item) 370 | 371 | return F_total, info 372 | 373 | 374 | # FIXME this looks like it can be generalized from already read meta-data 375 | def get_grid_parameters( series, iteration, avail_fields, metadata ): 376 | """ 377 | Return the parameters of the spatial grid (grid size and grid range) 378 | in two dictionaries 379 | 380 | Parameters: 381 | ----------- 382 | series: openpmd_api.Series 383 | An open, readable openPMD-api series object 384 | 385 | iteration: integer 386 | Iteration from which parameters should be extracted 387 | 388 | avail_fields: list 389 | A list of the available fields 390 | e.g. ['B', 'E', 'rho'] 391 | 392 | metadata: dictionary 393 | A dictionary whose keys are the fields of `avail_fields` and 394 | whose values are dictionaries that contain metadata (e.g. geometry) 395 | 396 | Returns: 397 | -------- 398 | A tuple with `grid_size_dict` and `grid_range_dict` 399 | Both objects are dictionaries, with their keys being the labels of the axis 400 | of the grid (e.g. 'x', 'y', 'z') 401 | The values of `grid_size_dict` are the number of gridpoints along each axis 402 | The values of `grid_range_dict` are lists of two floats, which correspond 403 | to the min and max of the grid, along each axis. 404 | """ 405 | it = series.iterations[iteration] 406 | 407 | # Pick field with the highest dimensionality ('3d'>'thetaMode'>'2d') 408 | # (This function is for the purpose of histogramming the particles; 409 | # in this case, the highest dimensionality ensures that more particle 410 | # quantities can be properly histogrammed.) 411 | geometry_ranking = {'1dcartesian': 0, '2dcartesian': 1, 412 | 'thetaMode': 2, '3dcartesian': 3} 413 | fields_ranking = [ geometry_ranking[ metadata[field]['geometry'] ] 414 | for field in avail_fields ] 415 | index_best_field = fields_ranking.index( max(fields_ranking) ) 416 | field_name = avail_fields[ index_best_field ] 417 | 418 | # Extract the dataset and and corresponding group 419 | # For field vector, extract the first component, to get the dataset 420 | field = it.meshes[field_name] 421 | component = next(field.items())[1] 422 | 423 | # Extract relevant quantities 424 | # FIXME here and in h5py reader, we need to invert the order on 'F' for 425 | # grid spacing/offset/position 426 | labels = field.axis_labels 427 | grid_spacing = np.array(field.grid_spacing) * field.grid_unit_SI 428 | grid_offset = np.array(field.grid_global_offset) * field.grid_unit_SI 429 | grid_size = component.shape 430 | if metadata[field_name]['geometry'] == 'thetaMode': 431 | # In thetaMode: skip the first number of dset.shape, as this 432 | # corresponds to the number of modes 433 | grid_size = component.shape[1:] 434 | 435 | # Build the dictionaries grid_size_dict and grid_range_dict 436 | grid_size_dict = {} 437 | grid_range_dict = {} 438 | for i in range(len(labels)): 439 | coord = labels[i] 440 | grid_size_dict[coord] = grid_size[i] 441 | grid_range_dict[coord] = \ 442 | [ grid_offset[i], grid_offset[i] + grid_size[i] * grid_spacing[i] ] 443 | 444 | return grid_size_dict, grid_range_dict 445 | -------------------------------------------------------------------------------- /openpmd_viewer/openpmd_timeseries/data_reader/io_reader/params_reader.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the openPMD-viewer. 3 | 4 | It defines a function that can read standard parameters from an openPMD file. 5 | 6 | Copyright 2020, openPMD-viewer contributors 7 | Authors: Axel Huebl 8 | License: 3-Clause-BSD-LBNL 9 | """ 10 | 11 | import numpy as np 12 | from .utilities import join_infile_path 13 | 14 | 15 | def read_openPMD_params(series, iteration, extract_parameters=True): 16 | """ 17 | Extract the time and some openPMD parameters from a file 18 | 19 | Parameter 20 | --------- 21 | series: openpmd_api.Series 22 | An open, readable openPMD-api series object 23 | 24 | iteration: integer 25 | Iteration from which parameters should be extracted 26 | 27 | extract_parameters: bool, optional 28 | Whether to extract all parameters or only the time 29 | (Function execution is faster when extract_parameters is False) 30 | 31 | Returns 32 | ------- 33 | A tuple with: 34 | - A float corresponding to the time of this iteration in SI units 35 | - A dictionary containing several parameters, such as the geometry, etc. 36 | When extract_parameters is False, the second argument returned is None. 37 | """ 38 | it = series.iterations[iteration] 39 | 40 | # extract the time 41 | if callable(it.time): # prior to openPMD-api 0.13.0 42 | t = it.time() * it.time_unit_SI() 43 | else: 44 | t = it.time * it.time_unit_SI 45 | 46 | # If the user did not request more parameters, close file and exit 47 | if not extract_parameters: 48 | return t, None 49 | 50 | # Otherwise, extract the rest of the parameters 51 | params = {} 52 | 53 | # Find out supported openPMD extensions claimed by this file 54 | # note: a file might implement multiple extensions 55 | known_extensions = {'ED-PIC': np.uint32(1)} 56 | bitmask_all_extensions = series.openPMD_extension 57 | params['extensions'] = [] 58 | for extension, bitmask in known_extensions.items(): 59 | # This uses a bitmask to identify activated extensions 60 | # efficiently in static programming languages via 61 | # a single attribute and a binary AND (&) operation. 62 | # Standard: https://git.io/vwnMw 63 | # Bitmasks: https://en.wikipedia.org/wiki/Mask_%28computing%29 64 | if bitmask_all_extensions & bitmask == bitmask: 65 | params['extensions'].append(extension) 66 | 67 | # Find out whether fields are present and extract their metadata 68 | fields_available = len(it.meshes) > 0 69 | if fields_available: 70 | params['avail_fields'] = [] 71 | params['fields_metadata'] = {} 72 | 73 | # Loop through the available fields 74 | for field_name, field in it.meshes.items(): 75 | metadata = {} 76 | metadata['geometry'] = field.get_attribute('geometry') 77 | metadata['axis_labels'] = field.axis_labels 78 | 79 | # Swap the order of the labels if the code that wrote the HDF5 file 80 | # was Fortran order (i.e. reverse order with respect to Python) 81 | if field.get_attribute('dataOrder') == 'F': 82 | metadata['axis_labels'] = metadata['axis_labels'][::-1] 83 | # Check whether the field is a vector or a scalar 84 | if field.scalar: 85 | metadata['type'] = 'scalar' 86 | components = [] 87 | else: 88 | metadata['type'] = 'vector' 89 | components = [comp for comp, _ in field.items()] 90 | # Register available components 91 | metadata['avail_components'] = components 92 | # Check the number of modes 93 | if metadata['geometry'] == "thetaMode": 94 | # simply check first record component 95 | field_component = next(field.items())[1] 96 | Nm = field_component.shape[0] 97 | metadata['avail_circ_modes'] = ['all'] + \ 98 | [str(m) for m in range(int(Nm / 2) + 1)] 99 | # Check if this a 1d, 2d or 3d Cartesian 100 | elif metadata['geometry'] == "cartesian": 101 | dim = len(metadata['axis_labels']) 102 | if dim == 1: 103 | metadata['geometry'] = "1dcartesian" 104 | elif dim == 2: 105 | metadata['geometry'] = "2dcartesian" 106 | elif dim == 3: 107 | metadata['geometry'] = "3dcartesian" 108 | metadata['avail_circ_modes'] = [] 109 | 110 | params['avail_fields'].append( field_name ) 111 | params['fields_metadata'][field_name] = metadata 112 | 113 | else: 114 | params['avail_fields'] = None 115 | 116 | # Find out whether particles are present, and if yes of which species 117 | particles_available = len(it.particles) > 0 118 | if particles_available: 119 | # Particles are present ; extract the species 120 | params['avail_species'] = list(it.particles) 121 | # dictionary with list of record components for each species 122 | record_components = {} 123 | # Go through all species 124 | # TODO: I did this more elegant in ParaView... for later 125 | for species_name, species in it.particles.items(): 126 | record_components[species_name] = [] 127 | 128 | # Go through all the particle records of this species 129 | for record_name, record in species.items(): 130 | # Skip the particlePatches, which are not used here. 131 | # API should filter this... 132 | # if record_name == 'particlePatches': 133 | # continue 134 | if record.scalar: 135 | # Add the name of the scalar record 136 | record_components[species_name]. \ 137 | append(record_name) 138 | else: 139 | # Add each component of the vector record 140 | for compo_name in list(record): 141 | record_components[species_name]. \ 142 | append(join_infile_path(record_name, compo_name)) 143 | # Simplify the name of some standard openPMD records 144 | record_components[species_name] = \ 145 | simplify_record(record_components[species_name]) 146 | params['avail_record_components'] = record_components 147 | # deprecated 148 | first_species_name = next(iter(params['avail_species'])) 149 | params['avail_ptcl_quantities'] = \ 150 | record_components[first_species_name] 151 | else: 152 | # Particles are absent 153 | params['avail_species'] = None 154 | params['avail_record_components'] = None 155 | # deprecated 156 | params['avail_ptcl_quantities'] = None 157 | 158 | return t, params 159 | 160 | 161 | def simplify_record(record_comps): 162 | """ 163 | Replace the names of some standard record by shorter names 164 | 165 | Parameter 166 | --------- 167 | record_comps: a list of strings 168 | A list of available particle record components 169 | 170 | Returns 171 | ------- 172 | A list with shorter names, where applicable 173 | """ 174 | # Replace the names of the positions 175 | if ('position/x' in record_comps) and ('positionOffset/x' in record_comps): 176 | record_comps.remove('position/x') 177 | record_comps.remove('positionOffset/x') 178 | record_comps.append('x') 179 | if ('position/y' in record_comps) and ('positionOffset/y' in record_comps): 180 | record_comps.remove('position/y') 181 | record_comps.remove('positionOffset/y') 182 | record_comps.append('y') 183 | if ('position/z' in record_comps) and ('positionOffset/z' in record_comps): 184 | record_comps.remove('position/z') 185 | record_comps.remove('positionOffset/z') 186 | record_comps.append('z') 187 | if ('position/r' in record_comps) and ('positionOffset/r' in record_comps): 188 | record_comps.remove('position/r') 189 | record_comps.remove('positionOffset/r') 190 | record_comps.append('r') 191 | 192 | # Replace the names of the momenta 193 | if 'momentum/x' in record_comps: 194 | record_comps.remove('momentum/x') 195 | record_comps.append('ux') 196 | if 'momentum/y' in record_comps: 197 | record_comps.remove('momentum/y') 198 | record_comps.append('uy') 199 | if 'momentum/z' in record_comps: 200 | record_comps.remove('momentum/z') 201 | record_comps.append('uz') 202 | if 'momentum/r' in record_comps: 203 | record_comps.remove('momentum/r') 204 | record_comps.append('ur') 205 | 206 | # Replace the name for 'weights' 207 | if 'weighting' in record_comps: 208 | record_comps.remove('weighting') 209 | record_comps.append('w') 210 | 211 | return record_comps 212 | -------------------------------------------------------------------------------- /openpmd_viewer/openpmd_timeseries/data_reader/io_reader/particle_reader.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the openPMD-viewer. 3 | 4 | It defines a function that reads a species record component (data & meta) 5 | from an openPMD file 6 | 7 | Copyright 2020, openPMD-viewer contributors 8 | Authors: Axel Huebl 9 | License: 3-Clause-BSD-LBNL 10 | """ 11 | 12 | import numpy as np 13 | from scipy import constants 14 | from .utilities import get_data 15 | 16 | 17 | def read_species_data(series, iteration, species_name, component_name, 18 | extensions): 19 | """ 20 | Extract a given species' record_comp 21 | 22 | Parameters 23 | ---------- 24 | series: openpmd_api.Series 25 | An open, readable openPMD-api series object 26 | 27 | iteration: integer 28 | Iteration from which parameters should be extracted 29 | 30 | species_name: string 31 | The name of the species to extract (in the openPMD file) 32 | 33 | component_name: string 34 | The record component to extract 35 | Either 'x', 'y', 'z', 'r', 'ux', 'uy', 'uz', 'ur', or 'w' 36 | 37 | extensions: list of strings 38 | The extensions that the current OpenPMDTimeSeries complies with 39 | """ 40 | it = series.iterations[iteration] 41 | 42 | # Translate the record component to the openPMD format 43 | dict_record_comp = {'x': ['position', 'x'], 44 | 'y': ['position', 'y'], 45 | 'z': ['position', 'z'], 46 | 'r': ['position', 'r'], 47 | 'ux': ['momentum', 'x'], 48 | 'uy': ['momentum', 'y'], 49 | 'uz': ['momentum', 'z'], 50 | 'ur': ['momentum', 'r'], 51 | 'w': ['weighting', None]} 52 | 53 | if component_name in dict_record_comp: 54 | ompd_record_name, ompd_record_comp_name = \ 55 | dict_record_comp[component_name] 56 | elif component_name.find('/') != -1: 57 | ompd_record_name, ompd_record_comp_name = \ 58 | component_name.split('/') 59 | else: 60 | ompd_record_name = component_name 61 | ompd_record_comp_name = None 62 | 63 | # Extract the right dataset 64 | species = it.particles[species_name] 65 | record = species[ompd_record_name] 66 | if record.scalar: 67 | component = next(record.items())[1] 68 | else: 69 | component = record[ompd_record_comp_name] 70 | 71 | if ompd_record_name == 'id': 72 | output_type = np.uint64 73 | else: 74 | output_type = np.float64 75 | data = get_data( series, component, output_type=output_type ) 76 | 77 | # For ED-PIC: if the data is weighted for a full macroparticle, 78 | # divide by the weight with the proper power 79 | # (Skip this if the current record component is the weight itself) 80 | if 'ED-PIC' in extensions and ompd_record_name != 'weighting': 81 | macro_weighted = record.get_attribute('macroWeighted') 82 | weighting_power = record.get_attribute('weightingPower') 83 | if (macro_weighted == 1) and (weighting_power != 0): 84 | w_component = next(species['weighting'].items())[1] 85 | w = get_data( series, w_component ) 86 | data *= w ** (-weighting_power) 87 | 88 | # - Return positions, with an offset 89 | if component_name in ['x', 'y', 'z', 'r']: 90 | offset = get_data(series, species['positionOffset'][component_name]) 91 | data += offset 92 | # - Return momentum in normalized units 93 | elif component_name in ['ux', 'uy', 'uz', 'ur']: 94 | mass_component = next(species['mass'].items())[1] 95 | m = get_data(series, mass_component) 96 | # Normalize only if the particle mass is non-zero 97 | if np.all( m != 0 ): 98 | norm_factor = 1. / (m * constants.c) 99 | data *= norm_factor 100 | 101 | 102 | # Return the data 103 | return data 104 | -------------------------------------------------------------------------------- /openpmd_viewer/openpmd_timeseries/data_reader/io_reader/utilities.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the openPMD-viewer. 3 | 4 | It defines a set of helper data and functions which 5 | are used by the other files. 6 | 7 | Copyright 2015-2016, openPMD-viewer contributors 8 | Authors: Remi Lehe, Axel Huebl 9 | License: 3-Clause-BSD-LBNL 10 | """ 11 | import numpy as np 12 | 13 | 14 | def chunk_to_slice(chunk): 15 | """ 16 | Convert an openPMD_api.ChunkInfo to slice 17 | """ 18 | stops = [a + b for a, b in zip(chunk.offset, chunk.extent)] 19 | indices_per_dim = zip(chunk.offset, stops) 20 | index_tuple = map(lambda s: slice(s[0], s[1], None), indices_per_dim) 21 | return tuple(index_tuple) 22 | 23 | 24 | def get_data(series, record_component, i_slice=None, pos_slice=None, 25 | output_type=None): 26 | """ 27 | Extract the data from a (possibly constant) dataset 28 | Slice the data according to the parameters i_slice and pos_slice 29 | 30 | Parameters: 31 | ----------- 32 | series: openpmd_api.Series 33 | An open, readable openPMD-api series object 34 | 35 | record_component: an openPMD.Record_Component 36 | 37 | pos_slice: int or list of int, optional 38 | Slice direction(s). 39 | When None, no slicing is performed 40 | 41 | i_slice: int or list of int, optional 42 | Indices of slices to be taken. 43 | 44 | output_type: a numpy type 45 | The type to which the returned array should be converted 46 | 47 | Returns: 48 | -------- 49 | An np.ndarray (non-constant dataset) or a single double (constant dataset) 50 | """ 51 | # For back-compatibility: Convert pos_slice and i_slice to 52 | # single-element lists if they are not lists (e.g. float 53 | # and int respectively). 54 | if pos_slice is not None and not isinstance(pos_slice, list): 55 | pos_slice = [pos_slice] 56 | if i_slice is not None and not isinstance(i_slice, list): 57 | i_slice = [i_slice] 58 | 59 | # ADIOS2: Actual chunks, all other: one chunk 60 | chunks = record_component.available_chunks() 61 | 62 | # mask invalid regions with NaN: fill value 63 | # note: NaN is only defined for floating point types 64 | NaN_value = np.nan if np.issubdtype(record_component.dtype, np.floating) or np.issubdtype(record_component.dtype, np.complexfloating) else 0 65 | 66 | # read whole data set 67 | if pos_slice is None: 68 | # mask invalid regions with NaN 69 | # note: full_like triggers a full read, thus we avoid it #340 70 | data = np.full(record_component.shape, NaN_value, record_component.dtype) 71 | 72 | for chunk in chunks: 73 | chunk_slice = chunk_to_slice(chunk) 74 | 75 | # skip empty slices: issue for BP4 only 76 | # https://github.com/ornladios/ADIOS2/issues/3459 77 | volume = 1 78 | for csl in chunk_slice: 79 | volume *= csl.stop - csl.start 80 | if volume == 0: 81 | continue 82 | 83 | # read only valid region 84 | x = record_component[chunk_slice] 85 | series.flush() 86 | data[chunk_slice] = x 87 | # slice: read only part of the data set 88 | else: 89 | full_shape = record_component.shape 90 | 91 | slice_shape = list(full_shape) # copy 92 | pos_slice_sorted = pos_slice.copy() # copy for in-place sort 93 | pos_slice_sorted.sort(reverse=True) 94 | for dir_index in pos_slice_sorted: # remove indices in list 95 | del slice_shape[dir_index] 96 | 97 | # mask invalid regions with NaN 98 | data = np.full(slice_shape, NaN_value, dtype=record_component.dtype) 99 | 100 | # build requested ND slice with respect to full data 101 | s = [] 102 | for d in range(len(full_shape)): 103 | if d in pos_slice: 104 | s.append(i_slice[pos_slice.index(d)]) # one index in such directions 105 | else: # all indices in other direction 106 | s.append(slice(None, None, None)) 107 | s = tuple(s) 108 | 109 | # now we check which chunks contribute to the slice 110 | for chunk in chunks: 111 | skip_this_chunk = False 112 | s_valid = list(s) # same as s but reduced to valid regions in chunk 113 | s_target = [] # starts and stops in sliced array 114 | chunk_slice = chunk_to_slice(chunk) 115 | 116 | # skip empty slices: issue for BP4 only 117 | # https://github.com/ornladios/ADIOS2/issues/3459 118 | volume = 1 119 | for csl in chunk_slice: 120 | volume *= csl.stop - csl.start 121 | if volume == 0: 122 | continue 123 | 124 | # read only valid region 125 | for d, slice_d in enumerate(s): 126 | start = chunk_slice[d].start 127 | stop = chunk_slice[d].stop 128 | if isinstance(slice_d, int): 129 | # Nothing to do for s_target (dimension sliced out) 130 | # Nothing to do for s_valid (dimension index is set) 131 | if slice_d < start or slice_d >= stop: 132 | # chunk not in slice line/plane 133 | skip_this_chunk = True 134 | else: 135 | if slice_d.start is None or slice_d.start < start: 136 | s_valid[d] = slice(start, s_valid[d].stop) 137 | if slice_d.stop is None or slice_d.stop > stop: 138 | s_valid[d] = slice(s_valid[d].start, stop) 139 | s_target.append(slice(start, stop)) 140 | 141 | s_valid = tuple(s_valid) 142 | s_target = tuple(s_target) 143 | 144 | # read 145 | if not skip_this_chunk: 146 | x = record_component[s_valid] 147 | series.flush() 148 | data[s_target] = x 149 | 150 | # Convert to the right type 151 | if (output_type is not None) and (data.dtype != output_type): 152 | data = data.astype( output_type ) 153 | # Scale by the conversion factor 154 | if record_component.unit_SI != 1.0: 155 | if np.issubdtype(data.dtype, np.floating) or \ 156 | np.issubdtype(data.dtype, np.complexfloating): 157 | data *= record_component.unit_SI 158 | else: 159 | data = data * record_component.unit_SI 160 | 161 | return data 162 | 163 | 164 | def join_infile_path(*paths): 165 | """ 166 | Join path components using '/' as separator. 167 | This method is defined as an alternative to os.path.join, which uses '\\' 168 | as separator in Windows environments and is therefore not valid to navigate 169 | within data files. 170 | 171 | Parameters: 172 | ----------- 173 | *paths: all strings with path components to join 174 | 175 | Returns: 176 | -------- 177 | A string with the complete path using '/' as separator. 178 | """ 179 | # Join path components 180 | path = '/'.join(paths) 181 | # Correct double slashes, if any is present 182 | path = path.replace('//', '/') 183 | 184 | return path 185 | -------------------------------------------------------------------------------- /openpmd_viewer/openpmd_timeseries/field_metainfo.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the openPMD-viewer. 3 | 4 | It defines the main FieldMetaInformation class, which 5 | is returned by `get_field` along with the array of field values, 6 | and gathers information collected from the openPMD file. 7 | 8 | Copyright 2015-2016, openPMD-viewer contributors 9 | Author: Remi Lehe 10 | License: 3-Clause-BSD-LBNL 11 | """ 12 | 13 | import numpy as np 14 | 15 | 16 | class FieldMetaInformation(object): 17 | """ 18 | An object that is typically returned along with an array of field 19 | values, and which contains meta-information about the grid. 20 | 21 | Attributes 22 | ---------- 23 | - axes: dict 24 | A dictionary of the form {0:'x', 1:'z'}, which indicates the name 25 | of the coordinate along each axis of the field array. 26 | For instance, in the case of {0:'x', 1:'y'}, the first axis of field 27 | array corresponds to the 'x' coordinate, and the second to 'y'. 28 | 29 | - xmin, xmax, zmin, zmax: double 30 | Scalars that indicate the position of the first grid point and 31 | last grid point along each axis. 32 | Notice that the name of these variables change according to 33 | the values in `axes`. For instance, if `axes` is {0: 'x', 1: 'y'}, 34 | then these variables will be called xmin, xmax, ymin, ymax. 35 | 36 | - dx, dz: double 37 | Scalars that indicate the resolution of the grid on each axis. 38 | Notice that the name of these variables change according to 39 | the values in `axes`. For instance, if `axes` is {0: 'x', 1: 'y'}, 40 | then these variables will be called dx and dy. 41 | 42 | - x, z: 1darrays of double 43 | The position of all the gridpoints, along each axis 44 | Notice that the name of these variables change according to 45 | the values in `axes`. For instance, if `axes` is {0: 'x', 1: 'y'}, 46 | then these variables will be called x, y. 47 | 48 | - imshow_extent: 1darray 49 | (Only for 2D data) 50 | An array of 4 elements that can be passed as the `extent` in 51 | matplotlib's imshow function. 52 | Because of the API of the imshow function, the coordinates are 53 | 'swapped' inside imshow_extent. For instance, if axes is 54 | {0: 'x', 1: 'y'}, then imshow_extent will be [ymin, ymax, xmin, xmax]. 55 | 56 | (NB: in the details, imshow_extent contains slightly different values 57 | than ymin, ymax, xmin, xmax: these values are shifted by half a cell. 58 | The reason for this is that imshow plots a finite-width square for each 59 | value of the field array.) 60 | 61 | - t: float (in seconds), optional 62 | The simulation time of the data 63 | It allows the user to get the simulation time when calling the 64 | get_particle method for a given iteration and vice versa. 65 | Either `t` or `iteration` should be given. 66 | 67 | - iteration: int 68 | The iteration of the data 69 | It allows the user to get the simulation time when calling the 70 | get_particle method for a given iteration and vice versa. 71 | Either `t` or `iteration` should be given. 72 | 73 | - field_attrs: dict 74 | All the attributes of the field record in the openPMD file. 75 | 76 | - component_attrs: dict 77 | All the attributes of the field component record in the openPMD file. 78 | 79 | """ 80 | 81 | def __init__(self, axes, shape, grid_spacing, 82 | global_offset, grid_unitSI, position, t, iteration, 83 | thetaMode=False, field_attrs=None, component_attrs=None): 84 | """ 85 | Create a FieldMetaInformation object 86 | 87 | The input arguments correspond to their openPMD standard definition 88 | """ 89 | # Register important initial information 90 | self.axes = axes 91 | 92 | # Create the elements 93 | for axis in sorted(axes.keys()): 94 | # Create the coordinates along this axis 95 | step = grid_spacing[axis] * grid_unitSI 96 | n_points = shape[axis] 97 | start = global_offset[axis] * grid_unitSI + position[axis] * step 98 | end = start + (n_points - 1) * step 99 | axis_points = np.linspace(start, end, n_points, endpoint=True) 100 | # Create the points below the axis if thetaMode is true 101 | if axes[axis] == 'r' and thetaMode: 102 | axis_points = np.concatenate((-axis_points[::-1], axis_points)) 103 | start = -end 104 | # Register the results in the object 105 | axis_name = axes[axis] 106 | setattr(self, axis_name, axis_points) 107 | setattr(self, 'd' + axis_name, step) 108 | setattr(self, axis_name + 'min', axis_points[0]) 109 | setattr(self, axis_name + 'max', axis_points[-1]) 110 | 111 | # Register current simulation time and iteration in the object 112 | setattr(self, 'time', t) 113 | setattr(self, 'iteration', iteration) 114 | 115 | self.field_attrs = field_attrs 116 | self.component_attrs = component_attrs 117 | self._generate_imshow_extent() 118 | 119 | 120 | def restrict_to_1Daxis(self, axis): 121 | """ 122 | Suppresses the information that correspond to other axes than `axis` 123 | 124 | Parameters 125 | ---------- 126 | axis: string 127 | The axis to keep 128 | This has to be one of the keys of the self.axes dictionary 129 | """ 130 | # Check if axis is a valid key 131 | if (axis in self.axes.values()) is False: 132 | raise ValueError('`axis` is not one of the coordinates ' 133 | 'that are present in this object.') 134 | 135 | # Loop through the coordinates and suppress them 136 | for obsolete_axis in list(self.axes.values()): 137 | if obsolete_axis != axis: 138 | self._remove_axis(obsolete_axis) 139 | 140 | 141 | def _generate_imshow_extent(self): 142 | """ 143 | Generate the list `imshow_extent`, which can be used directly 144 | as the argument `extent` of matplotlib's `imshow` command 145 | """ 146 | if len(self.axes) == 2: 147 | self.imshow_extent = [] 148 | for label in [self.axes[1], self.axes[0]]: 149 | coord_min = getattr( self, label+'min' ) 150 | coord_max = getattr( self, label+'max' ) 151 | coord_step = getattr( self, 'd'+label ) 152 | self.imshow_extent += [ coord_min - 0.5*coord_step, 153 | coord_max + 0.5*coord_step ] 154 | self.imshow_extent = np.array(self.imshow_extent) 155 | else: 156 | if hasattr(self, 'imshow_extent'): 157 | delattr(self, 'imshow_extent') 158 | 159 | 160 | def _remove_axis(self, obsolete_axis): 161 | """ 162 | Remove the axis `obsolete_axis` from the MetaInformation object 163 | """ 164 | delattr(self, obsolete_axis) 165 | delattr(self, obsolete_axis + 'min') 166 | delattr(self, obsolete_axis + 'max') 167 | # Rebuild the dictionary `axes`, by including the axis 168 | # label in the same order, but omitting obsolete_axis 169 | ndim = len(self.axes) 170 | self.axes = dict( enumerate([ 171 | self.axes[i] for i in range(ndim) \ 172 | if self.axes[i] != obsolete_axis ])) 173 | 174 | self._generate_imshow_extent() 175 | 176 | 177 | def _convert_cylindrical_to_3Dcartesian(self): 178 | """ 179 | Convert FieldMetaInformation from cylindrical to 3D Cartesian 180 | """ 181 | 182 | try: 183 | assert (self.axes[0] == 'r' and self.axes[1] == 'z') or (self.axes[0] == 'z' and self.axes[1] == 'r') 184 | except (KeyError, AssertionError): 185 | raise ValueError('_convert_cylindrical_to_3Dcartesian' 186 | ' can only be applied to a timeseries in thetaMode geometry') 187 | 188 | # Create x and y arrays 189 | self.x = self.r.copy() 190 | self.y = self.r.copy() 191 | del self.r 192 | 193 | # Create dx and dy 194 | self.dx = self.dr 195 | self.dy = self.dr 196 | del self.dr 197 | 198 | # Create xmin, xmax, ymin, ymax 199 | self.xmin = self.rmin 200 | self.ymin = self.rmin 201 | del self.rmin 202 | self.xmax = self.rmax 203 | self.ymax = self.rmax 204 | del self.rmax 205 | 206 | # Change axes 207 | self.axes = {0:'x', 1:'y', 2:'z'} 208 | -------------------------------------------------------------------------------- /openpmd_viewer/openpmd_timeseries/numba_wrapper.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the openPMD-viewer. 3 | 4 | It defines a wrapper around numba. 5 | 6 | Copyright 2019, openPMD-viewer contributors 7 | Author: Remi Lehe 8 | License: 3-Clause-BSD-LBNL 9 | """ 10 | import warnings 11 | 12 | try: 13 | # Import jit decorator from numba 14 | import numba 15 | numba_installed = True 16 | jit = numba.njit(cache=True) 17 | 18 | except ImportError: 19 | numba_installed = False 20 | # Create dummy decorator: warns about installing numba when calling 21 | # the decorated function. 22 | def jit(f): 23 | def decorated_f(*args, **kwargs): 24 | warnings.warn( 25 | '\nOne of the functions called by openPMD-viewer ' +\ 26 | '(%s)\n' %f.__name__ +\ 27 | 'could have been faster if `numba` had been installed.\n' +\ 28 | 'Please consider installing `numba` (e.g. `pip install numba`)') 29 | return f(*args, **kwargs) 30 | return decorated_f 31 | -------------------------------------------------------------------------------- /openpmd_viewer/openpmd_timeseries/particle_tracker.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the openPMD-viewer. 3 | 4 | It defines the ParticleTracker class. 5 | 6 | Copyright 2015-2016, openPMD-viewer contributors 7 | Authors: Remi Lehe 8 | License: 3-Clause-BSD-LBNL 9 | """ 10 | import numpy as np 11 | from .numba_wrapper import jit 12 | 13 | class ParticleTracker( object ): 14 | """ 15 | Class that allows to select particles at a given iteration 16 | (by initializing an instance of this class) and then 17 | to return the same particles at another iteration (by passing 18 | this instance as the argument `select` of the method `get_particle` 19 | of an `OpenPMDTimeSeries`) 20 | 21 | Usage 22 | ----- 23 | Here is a minimal example of how this class is used. 24 | In this example, all the particles in the simulation box at iteration 300 25 | are selected, and then the position of the same particles at iteration 400 26 | (or at least of those particles that remained in the simulation box at 27 | iteration 400) are returned. 28 | ``` 29 | >>> ts = OpenPMDTimeSeries('./hdf5_directory/') 30 | >>> pt = ParticleTracker( ts, iteration=300 ) 31 | >>> x, = ts.get_particle( ['x'], select=pt, iteration=400 ) 32 | ``` 33 | For more details on the API of ParticleTracker, see the docstring of 34 | the `__init__` method of this class. 35 | 36 | Note 37 | ---- 38 | `ParticleTracker` requires the `id` of the particles 39 | to be stored in the openPMD files. 40 | """ 41 | 42 | def __init__(self, ts, species=None, t=None, 43 | iteration=None, select=None, preserve_particle_index=False): 44 | """ 45 | Initialize an instance of `ParticleTracker`: select particles at 46 | a given iteration, so that they can be retrieved at a later iteration. 47 | 48 | Parameters 49 | ---------- 50 | ts: an OpenPMDTimeSeries object 51 | Contains the data on the particles 52 | 53 | species: string 54 | A string indicating the name of the species 55 | This is optional if there is only one species 56 | 57 | t : float (in seconds), optional 58 | Time at which to obtain the data (if this does not correspond to 59 | an existing iteration, the closest existing iteration will be used) 60 | Either `t` or `iteration` should be given by the user. 61 | 62 | iteration : int 63 | The iteration at which to obtain the data 64 | Either `t` or `iteration` should be given by the user. 65 | 66 | select: dict or 1darray of int, optional 67 | Either None or a dictionary of rules 68 | to select the particles, of the form 69 | 'x' : [-4., 10.] (Particles having x between -4 and 10) 70 | 'ux' : [-0.1, 0.1] (Particles having ux between -0.1 and 0.1 mc) 71 | 'uz' : [5., None] (Particles with uz above 5 mc). 72 | Can also be a 1d array of interegers corresponding to the 73 | selected particles `id` 74 | 75 | preserve_particle_index: bool, optional 76 | When retrieving particles at a several iterations, 77 | (for instance, with: 78 | ``` 79 | >>> x1, = ts.get_particle( ['x'], select=pt, iteration=400 ) 80 | >>> x2, = ts.get_particle( ['x'], select=pt, iteration=500 ) 81 | ``` 82 | it is sometimes important that the same individual particle has 83 | the same index in the array `x1` and `x2`. 84 | Using `preserve_particle_index=True` ensures that this is the case. 85 | However, this means that, for a particle that becomes absent at 86 | a later iteration, its index in the array has to be filled also. 87 | In this case, a NaN is returned at the index of this particle. 88 | When `preserve_particle_index=False`, no NaN is returned (the 89 | returned array is simply smaller when particles are absent) but 90 | then it is not guaranteed that a given particle keeps the same 91 | index 92 | """ 93 | 94 | # Extract or load the particle id and sort them 95 | if (type(select) is dict) or (select is None): 96 | self.selected_pid, = ts.get_particle(['id'], species=species, 97 | select=select, t=t, iteration=iteration) 98 | elif (type(select) is np.ndarray): 99 | self.selected_pid = select 100 | 101 | self.selected_pid.sort() 102 | 103 | # Register a few metadata 104 | self.N_selected = len( self.selected_pid ) 105 | self.species = species 106 | self.preserve_particle_index = preserve_particle_index 107 | 108 | 109 | def extract_tracked_particles( self, iteration, data_reader, data_list, 110 | species, extensions ): 111 | """ 112 | Select the elements of each particle quantities in data_list, 113 | so as to only return those that correspond to the tracked particles 114 | 115 | Parameters 116 | ---------- 117 | iteration: int 118 | The iteration at which to extract the particles 119 | 120 | data_reader: a DataReader object 121 | Used in order to extract the macroparticle IDs 122 | 123 | data_list: list of 1darrays 124 | A list of arrays with one element per macroparticle, that represent 125 | different particle quantities 126 | 127 | species: string 128 | Name of the species being requested 129 | 130 | extensions: list of strings 131 | The extensions that the current OpenPMDTimeSeries complies with 132 | 133 | Returns 134 | ------- 135 | A list of 1darrays that correspond to data_list, but where only the 136 | particles that are tracked are kept. (NaNs may or may not be returned 137 | depending on whether `preserve_particle_index` was chosen at 138 | initialization) 139 | """ 140 | # Extract the particle id, and get the extraction indices 141 | pid = data_reader.read_species_data(iteration, species, 'id', extensions) 142 | selected_indices = self.get_extraction_indices( pid ) 143 | 144 | # For each particle quantity, select only the tracked particles 145 | for i in range(len(data_list)): 146 | if len(data_list[i]) > 1: # Do not apply selection on scalars 147 | data_list[i] = self.extract_quantity( 148 | data_list[i], selected_indices ) 149 | 150 | return( data_list ) 151 | 152 | def extract_quantity( self, q, selected_indices ): 153 | """ 154 | Select the elements of the array `q`, so as to only return those 155 | that correspond to the tracked particles. 156 | 157 | Parameters 158 | ---------- 159 | q: 1d array of floats or ints 160 | A particle quantity (one element per particle) 161 | 162 | selected_indices: 1d array of ints 163 | The indices (in array q) of the particles to be selected. 164 | If `preserve_particle_index` was selected to be True, this array 165 | contains -1 at the position of particles that are no longer present 166 | 167 | Returns 168 | ------- 169 | selected_q: 1d array of floats or ints 170 | A particle quantity (one element per particles) 171 | where only the tracked particles are kept 172 | """ 173 | # Extract the selected elements 174 | selected_q = q[ selected_indices ] 175 | 176 | # Handle the absent particles 177 | if self.preserve_particle_index: 178 | if q.dtype in [ np.float64, np.float32 ]: 179 | # Fill the position of absent particles by NaNs 180 | selected_q = np.where( selected_indices == -1, 181 | np.nan, selected_q) 182 | else: 183 | # The only non-float quantity in openPMD-viewer is particle id 184 | selected_q = self.selected_pid 185 | 186 | return( selected_q ) 187 | 188 | def get_extraction_indices( self, pid ): 189 | """ 190 | For each tracked particle (i.e. for each element of self.selected_pid) 191 | find the index of the same particle in the array `pid` 192 | 193 | Return these indices in an array, so that it can then be used to 194 | extract quantities (position, momentum, etc.) for the tracked particles 195 | 196 | Parameters 197 | ---------- 198 | pid: 1darray of ints 199 | The id of each particle (one element per particle) 200 | 201 | Returns 202 | ------- 203 | selected_indices: 1d array of ints 204 | The index of the tracked particles in the array `pid` 205 | If `preserve_particle_index` was selected to be True, this array 206 | contains -1 at the position of particles that are no longer present 207 | 208 | Note on the implementation 209 | -------------------------- 210 | This could be implemented in brute force (i.e. for each element of 211 | `self.selected_pid`, search the entire array `pid` for the same 212 | element), but would be very costly. 213 | Instead, we sort the array `pid` (and keep track of the original 214 | pid, so as to be able to retrieve the indices) and use the fact 215 | that it is sorted in order to rapidly extract the elements 216 | that are also in `self.selected_pid` (which is also sorted) 217 | """ 218 | # Sort the pid, and keep track of the original index 219 | # at which each pid was 220 | original_indices = pid.argsort().astype(np.int64) 221 | sorted_pid = pid[ original_indices ] 222 | 223 | # Extract only the indices for which sorted_pid is one of pid 224 | # in self.sselected_pid (i.e. which correspond to one 225 | # of the original particles) 226 | selected_indices = np.empty( self.N_selected, dtype=np.int64 ) 227 | N_extracted = extract_indices( 228 | original_indices, selected_indices, sorted_pid, 229 | self.selected_pid, self.preserve_particle_index ) 230 | 231 | # If there are less particles then self.N_selected 232 | # (i.e. not all the pid in self.selected_pid were in sorted_pid) 233 | if N_extracted < self.N_selected: 234 | if self.preserve_particle_index: 235 | # Finish filling the array with absent particles 236 | selected_indices[N_extracted:] = -1 237 | else: 238 | # Resize the array 239 | selected_indices = \ 240 | selected_indices[:N_extracted] 241 | 242 | return( selected_indices ) 243 | 244 | @jit 245 | def extract_indices( original_indices, selected_indices, 246 | pid, selected_pid, preserve_particle_index ): 247 | """ 248 | Go through the sorted arrays `pid` and `selected_pid`, and record 249 | the indices (of the array `pid`) where they match, by storing them 250 | in the array `selected_indices` (this array is thus modified in-place) 251 | 252 | Return the number of elements that were filled in `selected_indices` 253 | """ 254 | i = 0 255 | i_select = 0 256 | i_fill = 0 257 | N = len(pid) 258 | N_selected = len(selected_pid) 259 | 260 | # Go through both sorted arrays (pid and selected_pid) and match them. 261 | # i.e. whenever the same number appears in both arrays, 262 | # record the corresponding original index in selected_indices 263 | while i < N and i_select < N_selected: 264 | 265 | if pid[i] < selected_pid[i_select]: 266 | i += 1 267 | elif pid[i] == selected_pid[i_select]: 268 | selected_indices[i_fill] = original_indices[i] 269 | i_fill += 1 270 | i_select += 1 271 | elif pid[i] > selected_pid[i_select]: 272 | i_select += 1 273 | if preserve_particle_index: 274 | # Fill the index, to indicate that the particle is absent 275 | selected_indices[i_fill] = -1 276 | i_fill += 1 277 | 278 | return( i_fill ) 279 | -------------------------------------------------------------------------------- /openpmd_viewer/openpmd_timeseries/plotter.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the openPMD-viewer. 3 | 4 | It defines a set of methods which are useful for plotting 5 | (and labeling the plots). 6 | 7 | Copyright 2015-2016, openPMD-viewer contributors 8 | Author: Remi Lehe 9 | License: 3-Clause-BSD-LBNL 10 | """ 11 | import numpy as np 12 | import math 13 | try: 14 | import warnings 15 | import matplotlib 16 | import matplotlib.pyplot as plt 17 | matplotlib_installed = True 18 | except ImportError: 19 | matplotlib_installed = False 20 | 21 | from .numba_wrapper import numba_installed 22 | if numba_installed: 23 | from .utilities import histogram_cic_1d, histogram_cic_2d 24 | 25 | # Redefine the default matplotlib formatter for ticks 26 | if matplotlib_installed: 27 | from matplotlib.ticker import ScalarFormatter 28 | 29 | class PowerOfThreeFormatter( ScalarFormatter ): 30 | """ 31 | Formatter for matplotlib's axes ticks, 32 | that prints numbers as e.g. 1.5e3, 3.2e6, 0.2e-9, 33 | where the exponent is always a multiple of 3. 34 | 35 | This helps a human reader to quickly identify the closest units 36 | (e.g. nanometer) of the plotted quantity. 37 | 38 | This class derives from `ScalarFormatter`, which 39 | provides a nice `offset` feature. 40 | """ 41 | def __init__( self, *args, **kwargs ): 42 | ScalarFormatter.__init__( self, *args, **kwargs ) 43 | # Do not print the order of magnitude on the side of the axis 44 | self.set_scientific(False) 45 | # Reduce the threshold for printing an offset on side of the axis 46 | self._offset_threshold = 2 47 | 48 | def __call__(self, x, pos=None): 49 | """ 50 | Function called for each tick of an axis (for matplotlib>=3.1) 51 | Returns the string that appears in the plot. 52 | """ 53 | return self.pprint_val( x, pos ) 54 | 55 | def pprint_val( self, x, pos=None): 56 | """ 57 | Function called for each tick of an axis (for matplotlib<3.1) 58 | Returns the string that appears in the plot. 59 | """ 60 | # Calculate the exponent (power of 3) 61 | xp = (x - self.offset) 62 | if xp != 0: 63 | exponent = int(3 * math.floor( math.log10(abs(xp)) / 3 )) 64 | else: 65 | exponent = 0 66 | # Show 3 digits at most after decimal point 67 | mantissa = round( xp * 10**(-exponent), 3) 68 | # After rounding the exponent might change (e.g. 0.999 -> 1.) 69 | if mantissa != 0 and math.log10(abs(mantissa)) == 3: 70 | exponent += 3 71 | mantissa /= 1000 72 | string = "{:.3f}".format( mantissa ) 73 | if '.' in string: 74 | # Remove trailing zeros and ., for integer mantissa 75 | string = string.rstrip('0') 76 | string = string.rstrip('.') 77 | if exponent != 0: 78 | string += "e{:d}".format( exponent ) 79 | return string 80 | 81 | tick_formatter = PowerOfThreeFormatter() 82 | 83 | 84 | class Plotter(object): 85 | 86 | """ 87 | Class which is used for plotting particles and fields 88 | (and labeling the plots) 89 | """ 90 | 91 | def __init__(self, t, iterations): 92 | """ 93 | Initialize the object 94 | 95 | Parameters 96 | ---------- 97 | t: 1darray of floats (seconds) 98 | Time for each available iteration of the timeseries 99 | 100 | iterations: 1darray of ints 101 | Iteration number for each available iteration of the timeseries 102 | """ 103 | # Default fontsize 104 | self.fontsize = 12 105 | 106 | # Register the time array and iterations array 107 | # (Useful when labeling the figures) 108 | self.t = t 109 | self.iterations = iterations 110 | 111 | def hist1d(self, q1, w, quantity1, species, current_i, nbins, hist_range, 112 | cmap='Blues', vmin=None, vmax=None, deposition='cic', **kw): 113 | """ 114 | Plot a 1D histogram of the particle quantity q1 115 | Sets the proper labels 116 | 117 | Parameters 118 | ---------- 119 | q1: 1darray of floats 120 | An array with one element per macroparticle, representing 121 | the quantity to be plotted. 122 | 123 | w: 1darray of floats 124 | An array with one element per macroparticle, representing 125 | the number of real particles that correspond to each macroparticle 126 | 127 | quantity1: string 128 | The name of the quantity to be plotted (for labeling purposes) 129 | 130 | species: string 131 | The name of the species from which the data is taken 132 | 133 | current_i: int 134 | The index of this iteration, within the iterations list 135 | 136 | nbins : int 137 | Number of bins for the histograms 138 | 139 | hist_range : list contains 2 lists of 2 floats 140 | Extent of the histogram along each direction 141 | 142 | deposition : string 143 | Either `ngp` (Nearest Grid Point) or `cic` (Cloud-In-Cell) 144 | When plotting the particle histogram, this determines how 145 | particles affects neighboring bins. 146 | `cic` (which is the default) leads to smoother results than `ngp`. 147 | 148 | **kw : dict, optional 149 | Additional options to be passed to matplotlib's bar function 150 | """ 151 | # Check if matplotlib is available 152 | check_matplotlib() 153 | 154 | # Find the iteration and time 155 | iteration = self.iterations[current_i] 156 | time = self.t[current_i] 157 | 158 | # Check deposition method 159 | if deposition == 'cic' and not numba_installed: 160 | print_cic_unavailable() 161 | deposition = 'ngp' 162 | 163 | # Bin the particle data 164 | q1 = q1.astype( np.float64 ) 165 | if deposition == 'ngp': 166 | binned_data, _ = np.histogram(q1, nbins, hist_range[0], weights=w) 167 | elif deposition == 'cic': 168 | binned_data = histogram_cic_1d( 169 | q1, w, nbins, hist_range[0][0], hist_range[0][1]) 170 | else: 171 | raise ValueError('Unknown deposition method: %s' % deposition) 172 | 173 | # Do the plot 174 | bin_size = (hist_range[0][1] - hist_range[0][0]) / nbins 175 | bin_coords = hist_range[0][0] + bin_size * ( 0.5 + np.arange(nbins) ) 176 | plt.bar( bin_coords, binned_data, width=bin_size, **kw ) 177 | plt.xlim( hist_range[0] ) 178 | plt.ylim( hist_range[1] ) 179 | plt.xlabel(quantity1, fontsize=self.fontsize) 180 | plt.title("%s: t = %.2e s (iteration %d)" 181 | % (species, time, iteration), fontsize=self.fontsize) 182 | # Format the ticks 183 | ax = plt.gca() 184 | ax.get_xaxis().set_major_formatter( tick_formatter ) 185 | ax.get_yaxis().set_major_formatter( tick_formatter ) 186 | 187 | def hist2d(self, q1, q2, w, quantity1, quantity2, species, current_i, 188 | nbins, hist_range, cmap='Blues', vmin=None, vmax=None, 189 | deposition='cic', **kw): 190 | """ 191 | Plot a 2D histogram of the particle quantity q1 192 | Sets the proper labels 193 | 194 | Parameters 195 | ---------- 196 | q1: 1darray of floats 197 | An array with one element per macroparticle, representing 198 | the quantity to be plotted. 199 | 200 | w: 1darray of floats 201 | An array with one element per macroparticle, representing 202 | the number of real particles that correspond to each macroparticle 203 | 204 | quantity1, quantity2: strings 205 | The name of the quantity to be plotted (for labeling purposes) 206 | 207 | species: string 208 | The name of the species from which the data is taken 209 | 210 | current_i: int 211 | The index of this iteration, within the iterations list 212 | 213 | nbins : list of 2 ints 214 | Number of bins along each direction, for the histograms 215 | 216 | hist_range : list contains 2 lists of 2 floats 217 | Extent of the histogram along each direction 218 | 219 | deposition : string 220 | Either `ngp` (Nearest Grid Point) or `cic` (Cloud-In-Cell) 221 | When plotting the particle histogram, this determines how 222 | particles affects neighboring bins. 223 | `cic` (which is the default) leads to smoother results than `ngp`. 224 | 225 | **kw : dict, optional 226 | Additional options to be passed to matplotlib's imshow function 227 | """ 228 | # Check if matplotlib is available 229 | check_matplotlib() 230 | 231 | # Find the iteration and time 232 | iteration = self.iterations[current_i] 233 | time = self.t[current_i] 234 | 235 | # Check deposition method 236 | if deposition == 'cic' and not numba_installed: 237 | print_cic_unavailable() 238 | deposition = 'ngp' 239 | 240 | # Bin the particle data 241 | q1 = q1.astype( np.float64 ) 242 | q2 = q2.astype( np.float64 ) 243 | if deposition == 'ngp': 244 | binned_data, _, _ = np.histogram2d( 245 | q1, q2, nbins, hist_range, weights=w) 246 | elif deposition == 'cic': 247 | binned_data = histogram_cic_2d( q1, q2, w, 248 | nbins[0], hist_range[0][0], hist_range[0][1], 249 | nbins[1], hist_range[1][0], hist_range[1][1] ) 250 | else: 251 | raise ValueError('Unknown deposition method: %s' % deposition) 252 | 253 | # Do the plot 254 | plt.imshow( binned_data.T, extent=hist_range[0] + hist_range[1], 255 | origin='lower', interpolation='nearest', aspect='auto', 256 | cmap=cmap, vmin=vmin, vmax=vmax, **kw ) 257 | plt.colorbar() 258 | plt.xlabel(quantity1, fontsize=self.fontsize) 259 | plt.ylabel(quantity2, fontsize=self.fontsize) 260 | plt.title("%s: t = %.2e s (iteration %d)" 261 | % (species, time, iteration), fontsize=self.fontsize) 262 | # Format the ticks 263 | ax = plt.gca() 264 | ax.get_xaxis().set_major_formatter( tick_formatter ) 265 | ax.get_yaxis().set_major_formatter( tick_formatter ) 266 | 267 | def show_field_1d( self, F, info, field_label, current_i, plot_range, 268 | vmin=None, vmax=None, **kw ): 269 | """ 270 | Plot the given field in 1D 271 | 272 | Parameters 273 | ---------- 274 | F: 1darray of floats 275 | Contains the field to be plotted 276 | 277 | info: a FieldMetaInformation object 278 | Contains the information about the plotted field 279 | 280 | field_label: string 281 | The name of the field plotted (for labeling purposes) 282 | 283 | vmin, vmax: floats or None 284 | The amplitude of the field 285 | 286 | plot_range : list of lists 287 | Indicates the values between which to clip the plot, 288 | along the 1st axis (first list) and 2nd axis (second list) 289 | """ 290 | # Check if matplotlib is available 291 | check_matplotlib() 292 | 293 | # Find the iteration and time 294 | iteration = self.iterations[current_i] 295 | time = self.t[current_i] 296 | 297 | # Get the x axis 298 | xaxis = getattr( info, info.axes[0] ) 299 | # Plot the data 300 | if np.issubdtype(F.dtype, np.complexfloating): 301 | plot_data = abs(F) # For complex numbers, plot the absolute value 302 | title = "|%s|" %field_label 303 | else: 304 | plot_data = F 305 | title = "%s" %field_label 306 | 307 | # Get the title and labels 308 | title += " at %.2e s (iteration %d)" % (time, iteration) 309 | plt.title(title, fontsize=self.fontsize) 310 | # Add the name of the axes 311 | plt.xlabel('$%s \;(m)$' % info.axes[0], fontsize=self.fontsize) 312 | 313 | plt.plot( xaxis, plot_data ) 314 | # Get the limits of the plot 315 | # - Along the first dimension 316 | if (plot_range[0][0] is not None) and (plot_range[0][1] is not None): 317 | plt.xlim( plot_range[0][0], plot_range[0][1] ) 318 | else: 319 | plt.xlim( xaxis.min(), xaxis.max() ) # Full extent of the box 320 | # - Along the second dimension 321 | if (plot_range[1][0] is not None) and (plot_range[1][1] is not None): 322 | plt.ylim( plot_range[1][0], plot_range[1][1] ) 323 | # Format the ticks 324 | ax = plt.gca() 325 | ax.get_xaxis().set_major_formatter( tick_formatter ) 326 | ax.get_yaxis().set_major_formatter( tick_formatter ) 327 | 328 | def show_field_2d(self, F, info, slice_across, m, field_label, geometry, 329 | current_i, plot_range, **kw): 330 | """ 331 | Plot the given field in 2D 332 | 333 | Parameters 334 | ---------- 335 | F: 2darray of floats 336 | Contains the field to be plotted 337 | 338 | info: a FieldMetaInformation object 339 | Contains the information about the plotted field 340 | 341 | slice_across : str, optional 342 | Only used for 3dcartesian geometry 343 | The direction across which the data is sliced 344 | 345 | m: int 346 | Only used for thetaMode geometry 347 | The azimuthal mode used when plotting the fields 348 | 349 | field_label: string 350 | The name of the field plotted (for labeling purposes) 351 | 352 | geometry: string 353 | Either "2dcartesian", "3dcartesian" or "thetaMode" 354 | 355 | plot_range : list of lists 356 | Indicates the values between which to clip the plot, 357 | along the 1st axis (first list) and 2nd axis (second list) 358 | """ 359 | # Check if matplotlib is available 360 | check_matplotlib() 361 | 362 | # Find the iteration and time 363 | iteration = self.iterations[current_i] 364 | time = self.t[current_i] 365 | 366 | # Plot the data 367 | if np.issubdtype(F.dtype, np.complexfloating): 368 | plot_data = abs(F) 369 | title = "|%s|" %field_label 370 | else: 371 | plot_data = F 372 | title = "%s" %field_label 373 | plt.imshow(plot_data, extent=info.imshow_extent, origin='lower', 374 | interpolation='nearest', aspect='auto', **kw) 375 | plt.colorbar() 376 | 377 | # Get the title and labels 378 | # Cylindrical geometry 379 | if geometry == "thetaMode": 380 | mode = str(m) 381 | title += " in the mode %s at %.2e s (iteration %d)" \ 382 | % (mode, time, iteration) 383 | # 2D Cartesian geometry 384 | else: 385 | title += " at %.2e s (iteration %d)" % (time, iteration) 386 | plt.title(title, fontsize=self.fontsize) 387 | 388 | # Add the name of the axes 389 | plt.xlabel('$%s \;(m)$' % info.axes[1], fontsize=self.fontsize) 390 | plt.ylabel('$%s \;(m)$' % info.axes[0], fontsize=self.fontsize) 391 | 392 | # Get the limits of the plot 393 | # - Along the first dimension 394 | if (plot_range[0][0] is not None) and (plot_range[0][1] is not None): 395 | plt.xlim( plot_range[0][0], plot_range[0][1] ) 396 | # - Along the second dimension 397 | if (plot_range[1][0] is not None) and (plot_range[1][1] is not None): 398 | plt.ylim( plot_range[1][0], plot_range[1][1] ) 399 | # Format the ticks 400 | ax = plt.gca() 401 | ax.get_xaxis().set_major_formatter( tick_formatter ) 402 | ax.get_yaxis().set_major_formatter( tick_formatter ) 403 | 404 | 405 | def print_cic_unavailable(): 406 | warnings.warn( 407 | "\nCIC particle histogramming is unavailable because \n" 408 | "Numba is not installed. NGP histogramming is used instead.\n" 409 | "Please considering installing numba (e.g. `pip install numba`)") 410 | 411 | 412 | def check_matplotlib(): 413 | """Raise error messages or warnings when potential issues when 414 | potential issues with matplotlib are detected.""" 415 | 416 | if not matplotlib_installed: 417 | raise RuntimeError( "Failed to import the openPMD-viewer plotter.\n" 418 | "(Make sure that matplotlib is installed.)") 419 | 420 | elif ('MacOSX' in matplotlib.get_backend()): 421 | warnings.warn("\n\nIt seems that you are using the matplotlib MacOSX " 422 | "backend. \n(This typically obtained when typing `%matplotlib`.)\n" 423 | "With recent version of Jupyter, the plots might not appear.\nIn this " 424 | "case, switch to `%matplotlib widget` and restart the notebook.") 425 | -------------------------------------------------------------------------------- /openpmd_viewer/openpmd_timeseries/utilities.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the openPMD-viewer. 3 | 4 | It defines a number of helper functions that are used in main.py 5 | 6 | Copyright 2015-2017, openPMD-viewer contributors 7 | Authors: Remi Lehe, Richard Pausch 8 | License: 3-Clause-BSD-LBNL 9 | """ 10 | 11 | import copy 12 | import math 13 | import numpy as np 14 | from .numba_wrapper import jit 15 | from .data_order import RZorder, order_error_msg 16 | 17 | def sanitize_slicing(slice_across, slice_relative_position): 18 | """ 19 | Return standardized format for `slice_across` and `slice_relative_position`: 20 | - either `slice_across` and `slice_relative_position` are both `None` (no slicing) 21 | - or `slice_across` and `slice_relative_position` are both lists, 22 | with the same number of elements 23 | 24 | Parameters 25 | ---------- 26 | slice_relative_position : float, or list of float, or None 27 | 28 | slice_across : str, or list of str, or None 29 | Direction(s) across which the data should be sliced 30 | """ 31 | # Skip None and empty lists 32 | if slice_across is None or slice_across == []: 33 | return None, None 34 | 35 | # Convert to lists 36 | if not isinstance(slice_across, list): 37 | slice_across = [slice_across] 38 | if slice_relative_position is None: 39 | slice_relative_position = [0]*len(slice_across) 40 | if not isinstance(slice_relative_position, list): 41 | slice_relative_position = [slice_relative_position] 42 | # Check that the length are matching 43 | if len(slice_across) != len(slice_relative_position): 44 | raise ValueError( 45 | 'The argument `slice_relative_position` is erroneous: \nIt should have' 46 | 'the same number of elements as `slice_across`.') 47 | 48 | # Return a copy. This is because the rest of the `openPMD-viewer` code 49 | # sometimes modifies the objects returned by `sanitize_slicing`. 50 | # Using a copy avoids directly modifying objects that the user may pass 51 | # to this function (and live outside of openPMD-viewer, e.g. directly in 52 | # a user's notebook) 53 | return copy.copy(slice_across), copy.copy(slice_relative_position) 54 | 55 | def apply_selection(iteration, data_reader, data_list, 56 | select, species, extensions): 57 | """ 58 | Select the elements of each particle quantities in data_list, 59 | based on the selection rules in `select` 60 | 61 | Parameters 62 | ---------- 63 | iteration: int 64 | The iteration at which to apply the selection 65 | 66 | data_reader: a DataReader object 67 | Contains the method that read particle data 68 | 69 | data_list: list of 1darrays 70 | A list of arrays with one element per macroparticle, that represent 71 | different particle quantities 72 | 73 | select: dict 74 | A dictionary of rules to select the particles 75 | 'x' : [-4., 10.] (Particles having x between -4 and 10) 76 | 'ux' : [-0.1, 0.1] (Particles having ux between -0.1 and 0.1 mc) 77 | 'uz' : [5., None] (Particles with uz above 5 mc) 78 | 79 | species: string 80 | Name of the species being requested 81 | 82 | extensions: list of strings 83 | The extensions that the current OpenPMDTimeSeries complies with 84 | 85 | Returns 86 | ------- 87 | A list of 1darrays that correspond to data_list, but were only the 88 | macroparticles that meet the selection rules are kept 89 | """ 90 | # Create the array that determines whether the particle 91 | # should be selected or not. 92 | Ntot = len(data_list[0]) 93 | select_array = np.ones(Ntot, dtype='bool') 94 | 95 | # Loop through the selection rules, and aggregate results in select_array 96 | for quantity in select.keys(): 97 | q = data_reader.read_species_data( 98 | iteration, species, quantity, extensions) 99 | # Check lower bound 100 | if select[quantity][0] is not None: 101 | select_array = np.logical_and( 102 | select_array, 103 | q > select[quantity][0]) 104 | # Check upper bound 105 | if select[quantity][1] is not None: 106 | select_array = np.logical_and( 107 | select_array, 108 | q < select[quantity][1]) 109 | 110 | # Use select_array to reduce each quantity 111 | for i in range(len(data_list)): 112 | if len(data_list[i]) > 1: # Do not apply selection on scalar records 113 | data_list[i] = data_list[i][select_array] 114 | 115 | return(data_list) 116 | 117 | 118 | def try_array( L ): 119 | """ 120 | Attempt to convert L to a single array. 121 | """ 122 | try: 123 | # Stack the arrays 124 | return np.stack( L, axis=0 ) 125 | except ValueError: 126 | # Do not stack 127 | return L 128 | 129 | 130 | def fit_bins_to_grid( hist_size, grid_size, grid_range ): 131 | """ 132 | Given a tentative number of bins `hist_size` for a histogram over 133 | the range `grid_range`, return a modified number of bins `hist_size` 134 | and a modified range `hist_range` so that the spacing of the histogram 135 | bins is an integer multiple (or integer divisor) of the grid spacing. 136 | 137 | Parameters: 138 | ---------- 139 | hist_size: integer 140 | The number of bins in the histogram along the considered direction 141 | 142 | grid_size: integer 143 | The number of cells in the grid 144 | 145 | grid_range: list of floats (in) 146 | The extent of the grid 147 | 148 | Returns: 149 | -------- 150 | hist_size: integer 151 | The new number of bins 152 | 153 | hist_range: list of floats 154 | The new range of the histogram 155 | """ 156 | # The new histogram range is the same as the grid range 157 | hist_range = grid_range 158 | 159 | # Calculate histogram tentative spacing, and grid spacing 160 | hist_spacing = ( hist_range[1] - hist_range[0] ) * 1. / hist_size 161 | grid_spacing = ( grid_range[1] - grid_range[0] ) * 1. / grid_size 162 | 163 | # Modify the histogram spacing, so that either: 164 | if hist_spacing >= grid_spacing: 165 | # - The histogram spacing is an integer multiple of the grid spacing 166 | hist_spacing = int( hist_spacing / grid_spacing ) * grid_spacing 167 | else: 168 | # - The histogram spacing is an integer divisor of the grid spacing 169 | hist_spacing = grid_spacing / int( grid_spacing / hist_spacing ) 170 | 171 | # Get the corresponding new number of bins, and the new range 172 | hist_size = int( ( hist_range[1] - hist_range[0] ) / hist_spacing ) 173 | hist_range[1] = hist_range[0] + hist_size * hist_spacing 174 | 175 | return( hist_size, hist_range ) 176 | 177 | 178 | def combine_cylindrical_components( Fr, Ft, theta, coord, info ): 179 | """ 180 | Calculate the catesian field Fx or Fy, 181 | from the cylindrical components Fr and Ft. 182 | 183 | Parameters: 184 | ----------- 185 | Fr, Ft: 3darrays or 2Darrays (depending on whether `theta` is None) 186 | Contains the value of the fields 187 | theta: float or None 188 | Indicates the angle of the plane in which Fr and Ft where taken 189 | coord: string 190 | Either 'x' or 'y' ; indicates which component to calculate 191 | info: FieldMetaInformation object 192 | Contains info on the coordinate system 193 | """ 194 | if theta is not None: 195 | if coord == 'x': 196 | F = np.cos(theta) * Fr - np.sin(theta) * Ft 197 | elif coord == 'y': 198 | F = np.sin(theta) * Fr + np.cos(theta) * Ft 199 | # Revert the sign below the axis 200 | if info.axes[0] == 'r': 201 | F[ : int(F.shape[0]/2) ] *= -1 202 | elif (F.ndim == 2) and (info.axes[1] == 'r'): 203 | F[ : , : int(F.shape[1]/2) ] *= -1 204 | else: 205 | # Fr, Ft are 3Darrays, info corresponds to Cartesian data 206 | assert (Fr.ndim == 3) and (Ft.ndim == 3) 207 | 208 | # Calculate cos(theta) and sin(theta) in the transverse Cartesian plane 209 | # while avoiding divisions by 0 210 | r = np.sqrt( info.x[:,np.newaxis]**2 + info.y[np.newaxis,:]**2 ) 211 | inv_r = 1./np.where( r!=0, r, 1. ) 212 | # The value `1.`` is a placeholder in the above (to avoid division by 0) 213 | # The lines below replace this placeholder value. 214 | cos = np.where( r!=0, info.x[:,np.newaxis]*inv_r, 1. ) 215 | sin = np.where( r!=0, info.y[np.newaxis,:]*inv_r, 0. ) 216 | if coord == 'x': 217 | F = cos[:,:,np.newaxis] * Fr - sin[:,:,np.newaxis] * Ft 218 | elif coord == 'y': 219 | F = sin[:,:,np.newaxis] * Fr + cos[:,:,np.newaxis] * Ft 220 | 221 | return F 222 | 223 | @jit 224 | def histogram_cic_1d( q1, w, nbins, bins_start, bins_end ): 225 | """ 226 | Return an 1D histogram of the values in `q1` weighted by `w`, 227 | consisting of `nbins` evenly-spaced bins between `bins_start` 228 | and `bins_end`. Contribution to each bins is determined by the 229 | CIC weighting scheme (i.e. linear weights). 230 | """ 231 | # Define various scalars 232 | bin_spacing = (bins_end-bins_start)/nbins 233 | inv_spacing = 1./bin_spacing 234 | n_ptcl = len(w) 235 | 236 | # Allocate array for histogrammed data 237 | hist_data = np.zeros( nbins, dtype=np.float64 ) 238 | 239 | # Go through particle array and bin the data 240 | for i in range(n_ptcl): 241 | # Calculate the index of lower bin to which this particle contributes 242 | q1_cell = (q1[i] - bins_start) * inv_spacing 243 | i_low_bin = int( math.floor( q1_cell ) ) 244 | # Calculate corresponding CIC shape and deposit the weight 245 | S_low = 1. - (q1_cell - i_low_bin) 246 | if (i_low_bin >= 0) and (i_low_bin < nbins): 247 | hist_data[ i_low_bin ] += w[i] * S_low 248 | if (i_low_bin + 1 >= 0) and (i_low_bin + 1 < nbins): 249 | hist_data[ i_low_bin + 1 ] += w[i] * (1. - S_low) 250 | 251 | return( hist_data ) 252 | 253 | 254 | @jit 255 | def histogram_cic_2d( q1, q2, w, 256 | nbins_1, bins_start_1, bins_end_1, 257 | nbins_2, bins_start_2, bins_end_2 ): 258 | """ 259 | Return an 2D histogram of the values in `q1` and `q2` weighted by `w`, 260 | consisting of `nbins_1` bins in the first dimension and `nbins_2` bins 261 | in the second dimension. 262 | Contribution to each bins is determined by the 263 | CIC weighting scheme (i.e. linear weights). 264 | """ 265 | # Define various scalars 266 | bin_spacing_1 = (bins_end_1-bins_start_1)/nbins_1 267 | inv_spacing_1 = 1./bin_spacing_1 268 | bin_spacing_2 = (bins_end_2-bins_start_2)/nbins_2 269 | inv_spacing_2 = 1./bin_spacing_2 270 | n_ptcl = len(w) 271 | 272 | # Allocate array for histogrammed data 273 | hist_data = np.zeros( (nbins_1, nbins_2), dtype=np.float64 ) 274 | 275 | # Go through particle array and bin the data 276 | for i in range(n_ptcl): 277 | 278 | # Calculate the index of lower bin to which this particle contributes 279 | q1_cell = (q1[i] - bins_start_1) * inv_spacing_1 280 | q2_cell = (q2[i] - bins_start_2) * inv_spacing_2 281 | i1_low_bin = int( math.floor( q1_cell ) ) 282 | i2_low_bin = int( math.floor( q2_cell ) ) 283 | 284 | # Calculate corresponding CIC shape and deposit the weight 285 | S1_low = 1. - (q1_cell - i1_low_bin) 286 | S2_low = 1. - (q2_cell - i2_low_bin) 287 | if (i1_low_bin >= 0) and (i1_low_bin < nbins_1): 288 | if (i2_low_bin >= 0) and (i2_low_bin < nbins_2): 289 | hist_data[ i1_low_bin, i2_low_bin ] += w[i]*S1_low*S2_low 290 | if (i2_low_bin+1 >= 0) and (i2_low_bin+1 < nbins_2): 291 | hist_data[ i1_low_bin, i2_low_bin+1 ] += w[i]*S1_low*(1.-S2_low) 292 | if (i1_low_bin+1 >= 0) and (i1_low_bin+1 < nbins_1): 293 | if (i2_low_bin >= 0) and (i2_low_bin < nbins_2): 294 | hist_data[ i1_low_bin+1, i2_low_bin ] += w[i]*(1.-S1_low)*S2_low 295 | if (i2_low_bin+1 >= 0) and (i2_low_bin+1 < nbins_2): 296 | hist_data[ i1_low_bin+1, i2_low_bin+1 ] += w[i]*(1.-S1_low)*(1.-S2_low) 297 | 298 | return( hist_data ) 299 | 300 | 301 | @jit 302 | def construct_3d_from_circ( F3d, Fcirc, x_array, y_array, modes, 303 | nx, ny, nz, nr, nmodes, inv_dr, rmax, coord_order): 304 | """ 305 | Reconstruct the field from a quasi-cylindrical simulation (`Fcirc`), as 306 | a 3D cartesian array (`F3d`). 307 | """ 308 | for ix in range(nx): 309 | x = x_array[ix] 310 | for iy in range(ny): 311 | y = y_array[iy] 312 | r = np.sqrt( x**2 + y**2 ) 313 | ir = nr - 1 - int( (rmax - r) * inv_dr + 0.5 ) 314 | 315 | # Handle out-of-bounds 316 | if ir < 0: 317 | ir = 0 318 | if ir >= nr: 319 | ir = nr-1 320 | 321 | # Calculate linear projection from ir and ir-1 322 | if ir>0: 323 | s0 = ir + 0.5 - r* inv_dr 324 | s1 = 1. - s0 325 | if coord_order is RZorder.mrz: 326 | Fcirc_proj = s1*Fcirc[:, ir, :] + s0*Fcirc[:, ir-1, :] 327 | elif coord_order is RZorder.mzr: 328 | Fcirc_proj = s1*Fcirc[:, :, ir] + s0*Fcirc[:, :, ir-1] 329 | else: 330 | raise Exception(order_error_msg) 331 | else: 332 | if coord_order is RZorder.mrz: 333 | Fcirc_proj = Fcirc[:, ir, :] 334 | elif coord_order is RZorder.mzr: 335 | Fcirc_proj = Fcirc[:, :, ir] 336 | else: 337 | raise Exception(order_error_msg) 338 | 339 | # Loop over all modes and reconstruct data 340 | if r == 0: 341 | expItheta = 1. + 0.j 342 | else: 343 | expItheta = (x+1.j*y)/r 344 | 345 | for im in range(nmodes): 346 | mode = modes[im] 347 | if mode==0: 348 | F3d[ix, iy, :] += Fcirc_proj[0, :] 349 | else: 350 | cos = (expItheta**mode).real 351 | sin = (expItheta**mode).imag 352 | F3d[ix, iy, :] += Fcirc_proj[2*mode-1,:]*cos + \ 353 | Fcirc_proj[2*mode,:]*sin 354 | -------------------------------------------------------------------------------- /opmd_viewer/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a stub that detects whether the user is attempting to use the 3 | old import statement from openPMD-viewer 0.X (`import opmd_viewer`). 4 | 5 | In that case, an exception is raised that prompts the user to use the new API. 6 | """ 7 | # Define the version number 8 | from openpmd_viewer import __version__ 9 | __all__ = ['__version__'] 10 | 11 | raise DeprecationWarning(""" 12 | It looks like you are trying to use the API from openPMD-viewer version 0.X 13 | but the installed version on your system is openPMD-viewer version 1.X. 14 | 15 | * If you wish to use the new openPMD-viewer version 1.X, the import statement 16 | should use `openpmd_viewer` instead of `opmd_viewer`, e.g.: 17 | ``` 18 | from openpmd_viewer import OpenPMDTimeSeries 19 | ``` 20 | Please have a look at the list of the changes introduced in version 1.X here: 21 | https://github.com/openPMD/openPMD-viewer/blob/upcoming-1.0/CHANGELOG.md#10 22 | In particular, note that `get_particle` now returns particle positions in 23 | meters (not in microns anymore) and that the syntax for slicing fields has 24 | changed. 25 | 26 | * If you wish to go back to the old openPMD-viewer version 0.X, use: 27 | ``` 28 | pip install openPMD-viewer==0.9 29 | ``` 30 | """) 31 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.15 2 | scipy 3 | h5py>=2.8.0 4 | tqdm 5 | 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | # Get the long description 4 | with open('./README.md') as f: 5 | long_description = f.read() 6 | # Get the package requirements from the requirements.txt file 7 | with open('./requirements.txt') as f: 8 | install_requires = [line.strip('\n') for line in f.readlines()] 9 | 10 | # Read the version number, by executing the file openpmd_viewer/__version__.py 11 | # This defines the variable __version__ 12 | with open('./openpmd_viewer/__version__.py') as f: 13 | exec( f.read() ) 14 | 15 | # Main setup command 16 | setup(name='openPMD-viewer', 17 | version=__version__, 18 | description='Visualization tools for openPMD files', 19 | long_description=long_description, 20 | long_description_content_type='text/markdown', 21 | url='https://github.com/openPMD/openPMD-viewer.git', 22 | maintainer='Remi Lehe', 23 | maintainer_email='remi.lehe@lbl.gov', 24 | license='BSD-3-Clause-LBNL', 25 | packages=find_packages('.'), 26 | package_data={'openpmd_viewer': ['notebook_starter/*.ipynb']}, 27 | scripts=['openpmd_viewer/notebook_starter/openPMD_notebook'], 28 | install_requires=install_requires, 29 | extras_require = { 30 | 'all': ["ipympl", "ipywidgets", "matplotlib", "numba", "openpmd-api", "wget"], 31 | 'GUI': ["ipywidgets", "ipympl", "matplotlib"], 32 | 'plot': ["matplotlib"], 33 | 'tutorials': ["ipywidgets", "ipympl", "matplotlib", "wget"], 34 | 'numba': ["numba"], 35 | 'openpmd-api': ["openpmd-api"] 36 | }, 37 | platforms='any', 38 | python_requires='>=3.8', 39 | classifiers=[ 40 | 'Programming Language :: Python', 41 | 'Development Status :: 4 - Beta', 42 | 'Natural Language :: English', 43 | 'Environment :: Console', 44 | 'Intended Audience :: Science/Research', 45 | 'Operating System :: OS Independent', 46 | 'Topic :: Scientific/Engineering :: Physics', 47 | 'Topic :: Scientific/Engineering :: Visualization', 48 | 'Topic :: Database :: Front-Ends', 49 | 'Programming Language :: Python :: 3', 50 | 'Programming Language :: Python :: 3.8', 51 | 'Programming Language :: Python :: 3.9', 52 | 'Programming Language :: Python :: 3.10', 53 | 'Programming Language :: Python :: 3.11'], 54 | ) 55 | -------------------------------------------------------------------------------- /tests/test_tutorials.py: -------------------------------------------------------------------------------- 1 | """ 2 | This test file is part of the openPMD-viewer. 3 | 4 | It makes sure that the tutorial notebooks run without error. 5 | 6 | Usage: 7 | This file is meant to be run from the root directory of openPMD-viewer, 8 | by any of the following commands 9 | $ python tests/test_tutorials.py 10 | $ py.test 11 | $ python -m pytest tests 12 | 13 | Copyright 2015-2016, openPMD-viewer contributors 14 | Authors: Remi Lehe, Axel Huebl 15 | License: 3-Clause-BSD-LBNL 16 | """ 17 | 18 | import os 19 | import re 20 | 21 | 22 | def test_tutorials(): 23 | """Test all the tutorial notebooks""" 24 | 25 | # Go to the relative path where all tutorial notebooks are 26 | os.chdir('docs/source/tutorials') 27 | tutorial_notebooks = [filename for filename in os.listdir('./') 28 | if filename[-6:] == '.ipynb'] 29 | 30 | # Loop through the tutorials and test them 31 | for notebook_name in tutorial_notebooks: 32 | 33 | # Do a first pass where only the non-IPython features are tested. 34 | # (This gives better debugging information.) 35 | # The notebook is converted to a standard Python script and 36 | # run directly with `execfile` 37 | script_name = notebook_name[:-6] + '.py' 38 | os.system('jupyter nbconvert --to=python %s' % notebook_name) 39 | clean_ipython_features(script_name) 40 | try: 41 | response = os.system('python3 ' + script_name) 42 | assert response == 0 43 | except: 44 | # now we might want to know the script that was executed 45 | print(open(script_name).read()) 46 | # re-raise same exception to make test fail 47 | raise 48 | os.remove(script_name) 49 | 50 | 51 | def clean_ipython_features(script_name): 52 | """ 53 | Rewrites the Python script `script_name` by removing 54 | all the IPython-specific commands 55 | 56 | Parameters 57 | ---------- 58 | script_name: string 59 | Name of the Python script 60 | """ 61 | # Read the script file 62 | with open(script_name) as script_file: 63 | lines = script_file.readlines() 64 | 65 | # Go through the lines and replace the IPython-specific commands 66 | # using regular expressions 67 | for i in range(len(lines)): 68 | 69 | # Replace the lines that activate matplotlib in a notebook 70 | # by a line that selects the PS backend 71 | if re.search(r"get_ipython.*matplotlib", lines[i]) is not None: 72 | lines[i] = "import matplotlib; matplotlib.use('ps')\n" 73 | 74 | # Discard the lines that use in-notebook documentation 75 | if re.search(r"get_ipython.*pinfo", lines[i]) is not None: 76 | lines[i] = '' 77 | 78 | # Discard the lines that use the GUI 79 | if re.match(r"[\w]*\.slider", lines[i]) is not None: 80 | lines[i] = '' 81 | 82 | # Replace the lines that call the OS by proper lines 83 | if re.match(r"[ ]*get_ipython\(\)\.system", lines[i]) is not None: 84 | matched = re.match(r"([ ]*)get_ipython\(\)\.system(.*)", lines[i]) 85 | spaces = matched.groups()[0] 86 | command_line = matched.groups()[1] 87 | lines[i] = '%simport os; os.system%s\n' % (spaces, command_line) 88 | 89 | # Write the cleaned file 90 | with open(script_name, 'w') as script_file: 91 | for line in lines: 92 | script_file.write(line) 93 | 94 | 95 | if __name__ == '__main__': 96 | test_tutorials() 97 | --------------------------------------------------------------------------------