diff --git a/.github/last_tested_fds_version.txt b/.github/last_tested_fds_version.txt new file mode 100644 index 00000000..26c4cb5f --- /dev/null +++ b/.github/last_tested_fds_version.txt @@ -0,0 +1 @@ +FDS-6.8.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..8420d4ac --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI + +on: + push: + branches: [master] + paths: + - "fdsreader/**" + - "tests/**" + - "pyproject.toml" + - ".github/workflows/ci.yml" + pull_request: + paths: + - "fdsreader/**" + - "tests/**" + - "pyproject.toml" + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install ruff + - run: ruff check fdsreader/ + - run: ruff format --check fdsreader/ + + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + with: + lfs: true + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + - name: Install dependencies + run: pip install -e ".[dev]" + - name: Prepare test cases + run: cd tests/cases && for f in *.tgz; do tar -xzvf "$f"; done + - name: Run tests + run: pytest tests/ --cov=fdsreader --cov-report=xml --cov-report=term-missing + - name: Upload coverage + if: matrix.python-version == '3.12' + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.xml diff --git a/.github/workflows/fds_compat.yml b/.github/workflows/fds_compat.yml new file mode 100644 index 00000000..71f1d29f --- /dev/null +++ b/.github/workflows/fds_compat.yml @@ -0,0 +1,81 @@ +name: FDS Compatibility Check + +on: + workflow_dispatch: + inputs: + fds_version: + description: "FDS version tag (e.g. FDS-6.10.1) — leave empty for latest" + required: false + schedule: + - cron: "0 6 * * 1" # Every Monday 06:00 UTC + +jobs: + detect-version: + runs-on: ubuntu-latest + outputs: + run_compat: ${{ steps.check.outputs.run_compat }} + fds_version: ${{ steps.check.outputs.fds_version }} + steps: + - uses: actions/checkout@v4 + - id: check + run: | + if [ -n "${{ inputs.fds_version }}" ]; then + echo "fds_version=${{ inputs.fds_version }}" >> $GITHUB_OUTPUT + echo "run_compat=true" >> $GITHUB_OUTPUT + else + LATEST=$(gh api repos/firemodels/fds/releases/latest --jq '.tag_name') + LAST=$(cat .github/last_tested_fds_version.txt 2>/dev/null || echo "none") + echo "fds_version=$LATEST" >> $GITHUB_OUTPUT + if [ "$LATEST" != "$LAST" ]; then + echo "run_compat=true" >> $GITHUB_OUTPUT + else + echo "run_compat=false" >> $GITHUB_OUTPUT + fi + fi + env: + GH_TOKEN: ${{ github.token }} + + check-compat: + needs: detect-version + if: needs.detect-version.outputs.run_compat == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + lfs: true + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Download FDS + run: | + VERSION=${{ needs.detect-version.outputs.fds_version }} + wget -q https://github.com/firemodels/fds/releases/download/${VERSION}/FDS-${VERSION#FDS-}_SMV-${VERSION#FDS-}_lnx.sh \ + -O fds_install.sh || \ + wget -q https://github.com/firemodels/fds/releases/download/${VERSION}/FDS-6.10.1_SMV-6.10.1_lnx.sh \ + -O fds_install.sh + echo "" | bash fds_install.sh --prefix=$HOME/fds + - name: Run test simulations + run: | + FDS=$HOME/fds/bin/fds_openmp + for dir in tests/cases/fds_inputs/*.fds; do + name=$(basename "$dir" .fds) + mkdir -p /tmp/fds_out/$name + cd /tmp/fds_out/$name + $FDS "$GITHUB_WORKSPACE/$dir" || true + cd $GITHUB_WORKSPACE + done + - name: Install fdsreader and run tests + run: | + pip install ".[dev]" + cd tests/cases && for f in *.tgz; do tar -xzvf "$f"; done + cd ../.. + pytest tests/ -v + - name: Update tracked FDS version + if: success() + run: | + echo "${{ needs.detect-version.outputs.fds_version }}" > .github/last_tested_fds_version.txt + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add .github/last_tested_fds_version.txt + git diff --staged --quiet || git commit -m "chore: update last tested FDS version to ${{ needs.detect-version.outputs.fds_version }}" + git push diff --git a/.github/workflows/publish_n_pack.yml b/.github/workflows/publish_n_pack.yml index 9df9fa75..e282f24d 100644 --- a/.github/workflows/publish_n_pack.yml +++ b/.github/workflows/publish_n_pack.yml @@ -1,6 +1,9 @@ name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI -on: push +on: + push: + tags: + - "v*" jobs: build: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..c9a36ea7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,65 @@ +name: Release + +on: + push: + tags: + - "v*" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + - run: pip install build + - run: python -m build + - uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + publish-testpypi: + if: contains(github.ref, 'b') || contains(github.ref, 'a') || contains(github.ref, 'rc') + needs: build + runs-on: ubuntu-latest + environment: + name: testpypi + url: https://test.pypi.org/p/fdsreader + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + + publish-pypi: + if: "!contains(github.ref, 'a') && !contains(github.ref, 'b') && !contains(github.ref, 'rc')" + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/fdsreader + permissions: + id-token: write + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + gh release create '${{ github.ref_name }}' dist/** \ + --repo '${{ github.repository }}' \ + --generate-notes diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml deleted file mode 100644 index ec25b003..00000000 --- a/.github/workflows/testsuite.yml +++ /dev/null @@ -1,42 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python - -name: FDSreader testsuite - -on: - workflow_dispatch: {} - push: - branches: - - master - paths: - - 'fdsreader/**' - pull_request: - paths: - - 'fdsreader/**' - -permissions: - contents: read - -jobs: - run-tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - lfs: true - - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pytest numpy - pip install ${{ github.workspace }} - - name: Prepare test cases - run: | - cd ${{ github.workspace }}/tests/cases/ - for f in *.tgz; do tar -xzvf "$f"; done - - name: Test with pytest - run: | - cd ${{ github.workspace }}/tests/cases - pytest ../acceptance_tests/ diff --git a/.gitignore b/.gitignore index 13d73043..77637714 100644 --- a/.gitignore +++ b/.gitignore @@ -508,4 +508,29 @@ examples/ !examples/**/*.fds /docs/build/ -Dockerfile \ No newline at end of file +Dockerfile + +# fdsreader specific +# Extracted test case directories (only .tgz archives are tracked) +tests/cases/steckler_data/ +tests/cases/bndf_data/ +tests/cases/devc_data/ +tests/cases/geom_data/ +tests/cases/part_data/ +tests/cases/pl3d_data/ +tests/cases/steckler_data_fds*/ +tests/cases/bndf_data_fds*/ +tests/cases/devc_data_fds*/ +tests/cases/geom_data_fds*/ +tests/cases/geom_data_new/ +tests/cases/part_data_fds*/ +tests/cases/pl3d_data_fds*/ + +# Pickle cache files +*.pickle + +# Ruff cache +.ruff_cache/ + +# Claude Code internal directory +.claude/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..2029b7b7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.9 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + args: [--maxkb=10000] + exclude: ^tests/cases/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..c2c21460 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,17 @@ +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: "3.12" + +sphinx: + configuration: docs/conf.py + fail_on_warning: false + +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..5bb53b94 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,99 @@ +# Contributing to fdsreader + +Thank you for your interest in contributing! This guide explains how to set up +your development environment and what to expect from the contribution process. + +## Development setup + +```bash +# 1. Fork and clone the repository +git clone https://github.com/FireDynamics/fdsreader.git +cd fdsreader + +# 2. Install in editable mode with dev dependencies +pip install -e ".[dev]" + +# 3. Set up pre-commit hooks (runs ruff automatically before each commit) +pre-commit install + +# 4. Extract test data +cd tests/cases && for f in *.tgz; do tar -xzvf "$f"; done && cd ../.. +``` + +## Running tests + +```bash +# Run all tests +pytest tests/ + +# With coverage report +pytest tests/ --cov=fdsreader --cov-report=term-missing +``` + +All 46 tests must pass before opening a pull request. + +## Code style + +This project uses [ruff](https://docs.astral.sh/ruff/) for linting and formatting. + +```bash +# Check for issues +ruff check fdsreader/ + +# Auto-fix issues +ruff check fdsreader/ --fix + +# Format code +ruff format fdsreader/ +``` + +The CI will fail if ruff reports any errors. If you have pre-commit installed, +ruff runs automatically on every commit. + +## Regenerating test data + +Test data archives (`.tgz`) are generated with specific FDS versions and stored +in `tests/cases/`. The corresponding FDS input files (`.fds`) are in +`tests/cases/fds_inputs/` and can be used to regenerate the data. + +```bash +FDS=/path/to/fds_openmp +BASE=tests/cases + +mkdir -p $BASE/steckler_data_fds +cd $BASE/steckler_data_fds +$FDS $BASE/fds_inputs/input_steckler.fds + +# Repeat for other cases, then archive: +cd $BASE +tar -czf steckler_data_fds.tgz steckler_data_fds/ +``` + +See the [FDS version compatibility table](README.md#fds-version-compatibility) +for which versions have been tested. + +## Open issues and known bugs + +Before starting work please check the +[issue tracker](https://github.com/FireDynamics/fdsreader/issues) for known bugs. + +### Critical bugs (good first issues) + +| File | Line | Bug | +|------|------|-----| +| `fdsreader/utils/extent.py` | 14 | `ValueError` is created but never raised → silent data corruption | +| `fdsreader/utils/misc.py` | 19 | `log_error` decorator returns `None` when an exception is caught | +| `fdsreader/utils/data.py` | 66 | `open()` without context manager → potential file handle leak | + +### FDS 6.10.1 compatibility + +Geometry data (`geom_data`) cannot be read from FDS 6.10.1 outputs because +the `BGEOM` block was removed from the SMV format. A fix requires updating +`simulation.py` to handle the new format. + +## Pull request checklist + +- [ ] Tests pass (`pytest tests/`) +- [ ] No ruff errors (`ruff check fdsreader/`) +- [ ] New features include tests +- [ ] Commit messages follow the existing style (see `git log --oneline`) diff --git a/MANIFEST.in b/MANIFEST.in index 66cabd65..38a101ad 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ -include requirements.txt +include requirements-dev.txt include README.md \ No newline at end of file diff --git a/README.md b/README.md index 205836f6..f71182c0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,18 @@ # FDSReader > Fast and easy-to-use Python reader for FDS data -[![PyPI version](https://badge.fury.io/py/fdsreader.png)](https://badge.fury.io/py/fdsreader) +[![PyPI version](https://badge.fury.io/py/fdsreader.png)](https://badge.fury.io/py/fdsreader) +[![CI](https://github.com/FireDynamics/fdsreader/actions/workflows/ci.yml/badge.svg)](https://github.com/FireDynamics/fdsreader/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/FireDynamics/fdsreader/branch/master/graph/badge.svg)](https://codecov.io/gh/FireDynamics/fdsreader) +## FDS Version Compatibility + +| fdsreader | FDS 6.7 | FDS 6.8 | FDS 6.9 | FDS 6.10 | +|-------------------|---------|---------|---------|----------| +| ≤ 1.11.x | ✅ | ✅ | ✅ | ⚠️ (Geometry bug, [#TODO](https://github.com/FireDynamics/fdsreader/issues)) | +| 1.12.x *(planed)* | ✅ | ✅ | ✅ | ✅ | + +_Tested against FDS outputs. If you find a compatibility issue please [open an issue](https://github.com/FireDynamics/fdsreader/issues)._ ## Installation @@ -52,34 +62,17 @@ documentation of all classes check the API Documentation below. ## API Documentation [https://firedynamics.github.io/fdsreader/](https://firedynamics.github.io/fdsreader/) -Deployment now follows the [Python Packaging User Guide's recommendation](https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/). - -With this setup, deployments to both TestPyPI and PyPI are automated. Every push to GitHub triggers a deployment to TestPyPI, simplifying the testing of new changes and validating the CI pipeline. Therefore, it is necessary to set the package version to `.dev` to avoid blocking version numbers. - -### Deploying an Untested/Unstable Version: -1. Execute: `python3 -m incremental.update fdsreader --dev` -2. Push changes to GitHub. - -If you are sure your changes are stable push a GitHub Tag to perform deployment -to PyPI and to pack a GitHub Release -Deploying a tested/stable version: -1. set the new version with `python3 -m incremental.update fdsreader --newversion=` -2. Push changes to GitHub -3. Create tag `git tag -a v -m "Version "` -4. Push Tag to GitHub with `git push origin tag ` - - -### Manual deployment -It is also possible to deploy to PyPI and Github pages manually using the following steps: -1. python setup.py sdist bdist_wheel -2. twine upload dist/* -3. sphinx-build -b html docs docs/build -4. cd .. && mkdir gh-pages && cd gh-pages -5. git init && git remote add origin git@github.com:FireDynamics/fdsreader.git -6. git fetch origin gh-pages:gh-pages -7. git checkout gh-pages -8. cp -r ../fdsreader/docs/build/* . -9. git add . && git commit -m "..." && git push origin HEAD:gh-pages +## Releasing a new version + +Versioning is handled automatically via Git tags using `setuptools-scm`. + +```bash +# New release +git tag -a v1.12.0 -m "Version 1.12.0" +git push origin v1.12.0 +``` + +This triggers the release workflow which builds the package and publishes it to PyPI. ## Meta diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..739051ac --- /dev/null +++ b/codecov.yml @@ -0,0 +1,13 @@ +coverage: + status: + project: + default: + target: 50% + threshold: 2% + patch: + default: + target: 40% + +comment: + layout: "reach,diff,flags,files" + behavior: default diff --git a/docs/conf.py b/docs/conf.py index e5c99e9b..4069aefb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,10 +12,9 @@ # import os import sys +from importlib.metadata import version, PackageNotFoundError sys.path.insert(0, os.path.abspath('..')) -from fdsreader._version import __version__ -# import sphinx_rtd_theme # -- Project information ----------------------------------------------------- @@ -24,7 +23,10 @@ author = 'FZJ IAS-7 (Prof. Dr. Lukas Arnold, Jan Vogelsang)' # The full version, including alpha/beta/rc tags -release = str(__version__) +try: + release = version("fdsreader") +except PackageNotFoundError: + release = "unknown" # -- General configuration --------------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index e31705a1..7caaadfd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,6 +1,12 @@ FDSReader documentation! ======================== +.. toctree:: + :maxdepth: 1 + :caption: Maintainer Guide + + releasing + .. toctree:: :maxdepth: 1 :caption: FDSReader diff --git a/docs/releasing.rst b/docs/releasing.rst new file mode 100644 index 00000000..d7ac73c9 --- /dev/null +++ b/docs/releasing.rst @@ -0,0 +1,91 @@ +Releasing a New Version +======================= + +This page explains how to publish a new version of fdsreader to PyPI. +The entire process is automated — no manual upload or token handling is required. + +Overview +-------- + +Versioning is driven entirely by **Git tags**. +When you push a tag that starts with ``v``, GitHub Actions automatically: + +1. Builds the Python package (wheel + sdist) +2. Publishes it to PyPI (stable release) or TestPyPI (beta release) +3. Creates a GitHub Release with auto-generated release notes + +Prerequisites (one-time setup) +------------------------------- + +Before the first release, a maintainer must configure **Trusted Publishing** on PyPI: + +* Log in to `pypi.org `_ with the project account +* Go to **Account settings → Publishing → Add a new publisher** +* Fill in: + + * PyPI project name: ``fdsreader`` + * GitHub owner: ``FireDynamics`` + * Repository: ``fdsreader`` + * Workflow name: ``release.yml`` + * Environment name: ``pypi`` + +* Repeat for `test.pypi.org `_ with environment name ``testpypi`` + +This setup only needs to be done once. +See :ref:`Trusted Publishing docs ` for details. + +Stable release (publishes to PyPI) +----------------------------------- + +.. code-block:: bash + + # 1. Make sure master is up to date and all tests are green + git checkout master + git pull + + # 2. Tag the release (use semantic versioning: MAJOR.MINOR.PATCH) + git tag -a v1.12.0 -m "Version 1.12.0" + + # 3. Push the tag — this triggers the release workflow + git push origin v1.12.0 + +That's it. GitHub Actions takes over from here. +The new version appears on `PyPI `_ +within a few minutes. + +Beta / pre-release (publishes to TestPyPI) +------------------------------------------- + +If you want to test the package on TestPyPI before a stable release: + +.. code-block:: bash + + git tag -a v1.12.0b1 -m "Version 1.12.0 beta 1" + git push origin v1.12.0b1 + +Tags containing ``a``, ``b``, or ``rc`` (e.g. ``v1.12.0b1``, ``v1.12.0rc1``) +are automatically routed to TestPyPI instead of PyPI. + +Checking the release +-------------------- + +After pushing the tag: + +1. Go to **GitHub → Actions → Release** to watch the build progress +2. Check the **GitHub Releases** page for the auto-generated release notes +3. Verify the new version on `pypi.org/project/fdsreader `_ + +Updating the FDS compatibility table +-------------------------------------- + +After testing with a new FDS version, update the compatibility table in ``README.md`` +and commit the change to master before tagging the release. + +Version number guidelines +-------------------------- + +We follow `Semantic Versioning `_: + +* **PATCH** (1.x.y → 1.x.(y+1)): Bug fixes, no breaking changes +* **MINOR** (1.x.y → 1.(x+1).0): New features, backwards compatible +* **MAJOR** (1.x.y → 2.0.0): Breaking changes to the public API diff --git a/fdsreader/__init__.py b/fdsreader/__init__.py index bf8a3c55..b152f717 100644 --- a/fdsreader/__init__.py +++ b/fdsreader/__init__.py @@ -1,6 +1,9 @@ -from . import _version -__version__ = str(_version.__version__.public()) +from importlib.metadata import PackageNotFoundError, version -from .simulation import Simulation +try: + __version__ = version("fdsreader") +except PackageNotFoundError: + __version__ = "unknown" -from . import settings +from . import settings as settings +from .simulation import Simulation as Simulation diff --git a/fdsreader/_version.py b/fdsreader/_version.py deleted file mode 100644 index 5a76cd7b..00000000 --- a/fdsreader/_version.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -Provides fdsreader version information. -""" - -# This file is auto-generated! Do not edit! -# Use `python -m incremental.update fdsreader` to change this file. - -from incremental import Version - -__version__ = Version("fdsreader", 1, 11, 7) -__all__ = ["__version__"] diff --git a/fdsreader/bndf/__init__.py b/fdsreader/bndf/__init__.py index 1439627a..0480f135 100644 --- a/fdsreader/bndf/__init__.py +++ b/fdsreader/bndf/__init__.py @@ -1,3 +1,5 @@ -from .obstruction import Obstruction, SubObstruction, Patch, Boundary - -from .obstruction_collection import ObstructionCollection +from .obstruction import Boundary as Boundary +from .obstruction import Obstruction as Obstruction +from .obstruction import Patch as Patch +from .obstruction import SubObstruction as SubObstruction +from .obstruction_collection import ObstructionCollection as ObstructionCollection diff --git a/fdsreader/bndf/obstruction.py b/fdsreader/bndf/obstruction.py index d9ba6a41..ba8c8b00 100644 --- a/fdsreader/bndf/obstruction.py +++ b/fdsreader/bndf/obstruction.py @@ -1,16 +1,15 @@ import math import os -from typing import List, Dict, Tuple, Union, Sequence -from typing_extensions import Literal -import numpy as np +# Unfortunately, this is necessary due to a cyclic reference. "Mesh" is only needed for static type hints anyway +from typing import TYPE_CHECKING, Dict, List, Sequence, Tuple, Union + +import numpy as np +from typing_extensions import Literal -from fdsreader.utils import Extent, Quantity, Dimension import fdsreader.utils.fortran_data as fdtype from fdsreader import settings - -# Unfortunately, this is necessary due to a cyclic reference. "Mesh" is only needed for static type hints anyway -from typing import TYPE_CHECKING +from fdsreader.utils import Dimension, Extent, Quantity if TYPE_CHECKING: from fdsreader.fds_classes import Mesh @@ -27,8 +26,18 @@ class Patch: :ivar _n_t: Total number of time steps for which output data has been written. """ - def __init__(self, file_path: str, dimension: Dimension, extent: Extent, orientation: int, cell_centered: bool, - patch_offset: int, initial_offset: int, n_t: int, mesh: 'Mesh'): + def __init__( + self, + file_path: str, + dimension: Dimension, + extent: Extent, + orientation: int, + cell_centered: bool, + patch_offset: int, + initial_offset: int, + n_t: int, + mesh: "Mesh", + ): self.file_path = file_path self.dimension = dimension self.extent = extent @@ -39,7 +48,7 @@ def __init__(self, file_path: str, dimension: Dimension, extent: Extent, orienta self._time_offset = -1 self._n_t = n_t self.mesh = mesh - self._boundary_parent: 'Boundary' = None + self._boundary_parent: Boundary = None def n_t(self, count_duplicates=True) -> int: """Get the number of timesteps for which data was output. @@ -53,31 +62,28 @@ def n_t(self, count_duplicates=True) -> int: @property def shape(self) -> Tuple: - """Convenience function to calculate the shape of the array containing data for this patch. - """ + """Convenience function to calculate the shape of the array containing data for this patch.""" return self.dimension.shape(self.cell_centered) @property def size(self) -> int: - """Convenience function to calculate the number of data points in the array for this patch. - """ + """Convenience function to calculate the number of data points in the array for this patch.""" return self.dimension.size(self.cell_centered) def _post_init(self, time_offset: int): - """Fully initialize the patch as soon as the number of timesteps is known. - """ + """Fully initialize the patch as soon as the number of timesteps is known.""" self._time_offset = time_offset - def get_coordinates(self, ignore_cell_centered: bool = False) -> Dict[Literal['x', 'y', 'z'], np.ndarray]: + def get_coordinates(self, ignore_cell_centered: bool = False) -> Dict[Literal["x", "y", "z"], np.ndarray]: """Returns a dictionary containing a numpy ndarray with coordinates for each dimension. - For cell-centered boundary data, the coordinates can be adjusted to represent cell-centered coordinates. + For cell-centered boundary data, the coordinates can be adjusted to represent cell-centered coordinates. - :param ignore_cell_centered: Whether to shift the coordinates when the bndf is cell_centered or not. + :param ignore_cell_centered: Whether to shift the coordinates when the bndf is cell_centered or not. """ # orientation = ('x', 'y', 'z')[self.orientation - 1] if self.orientation != 0 else '' # coords = {'x': set(), 'y': set(), 'z': set()} - coords: Dict[Literal['x', 'y', 'z'], np.ndarray] = {} - for dim in ('x', 'y', 'z'): + coords: Dict[Literal["x", "y", "z"], np.ndarray] = {} + for dim in ("x", "y", "z"): co = self.mesh.coordinates[dim].copy() # In case the slice is cell-centered, we will shift the coordinates by half a cell # and remove the last coordinate @@ -93,20 +99,20 @@ def get_coordinates(self, ignore_cell_centered: bool = False) -> Dict[Literal['x @property def data(self): - """Method to load the quantity data for a single patch. - """ + """Method to load the quantity data for a single patch.""" if not hasattr(self, "_data"): - dtype_data = fdtype.new((('f', self.dimension.size(cell_centered=False)),)) + dtype_data = fdtype.new((("f", self.dimension.size(cell_centered=False)),)) if self._n_t == -1: self._n_t = (os.stat(self.file_path).st_size - self._initial_offset) // self._time_offset self._data = np.empty((self.n_t(count_duplicates=True),) + self.shape) - with open(self.file_path, 'rb') as infile: + with open(self.file_path, "rb") as infile: for t in range(self.n_t(count_duplicates=True)): infile.seek(self._initial_offset + self._patch_offset + t * self._time_offset) data = np.fromfile(infile, dtype_data, 1)[0][1].reshape( - self.dimension.shape(cell_centered=False), order='F') + self.dimension.shape(cell_centered=False), order="F" + ) if self.cell_centered: self._data[t, :] = data[:-1, :-1] else: @@ -116,8 +122,7 @@ def data(self): return self._data def clear_cache(self): - """Remove all data from the internal cache that has been loaded so far to free memory. - """ + """Remove all data from the internal cache that has been loaded so far to free memory.""" if hasattr(self, "_data"): del self._data @@ -137,8 +142,15 @@ class Boundary: :ivar n_t: Total number of time steps for which output data has been written. """ - def __init__(self, quantity: Quantity, cell_centered: bool, times: Sequence[float], patches: List[Patch], - lower_bounds: np.ndarray, upper_bounds: np.ndarray): + def __init__( + self, + quantity: Quantity, + cell_centered: bool, + times: Sequence[float], + patches: List[Patch], + lower_bounds: np.ndarray, + upper_bounds: np.ndarray, + ): self.quantity = quantity self.cell_centered = cell_centered self._patches = patches @@ -146,7 +158,6 @@ def __init__(self, quantity: Quantity, cell_centered: bool, times: Sequence[floa self.lower_bounds = lower_bounds self.upper_bounds = upper_bounds - def n_t(self, count_duplicates=True) -> int: """Get the number of timesteps for which data was output. :param count_duplicates: If true, return the total number of data points, even if there is @@ -157,24 +168,22 @@ def n_t(self, count_duplicates=True) -> int: @property def orientations(self): - """Return all orientations for which there is data available. - """ + """Return all orientations for which there is data available.""" return [p.orientation for p in self._patches] def get_nearest_timestep(self, time: float) -> int: - """Calculates the nearest timestep for which data has been output for this obstruction. - """ + """Calculates the nearest timestep for which data has been output for this obstruction.""" idx = np.searchsorted(self.times, time, side="left") - if time > 0 and (idx == len(self.times) or math.fabs( - time - self.times[idx - 1]) < math.fabs(time - self.times[idx])): + if time > 0 and ( + idx == len(self.times) or math.fabs(time - self.times[idx - 1]) < math.fabs(time - self.times[idx]) + ): return idx - 1 else: return idx @property def data(self) -> Dict[int, Patch]: - """The :class:`Patch` in each direction (-3=-z, -2=-y, -1=-x, 1=x, 2=y, 3=y). - """ + """The :class:`Patch` in each direction (-3=-z, -2=-y, -1=-x, 1=x, 2=y, 3=y).""" return {p.orientation: p for p in self._patches} def vmin(self, orientation: Literal[-3, -2, -1, 0, 1, 2, 3] = 0) -> float: @@ -204,8 +213,7 @@ def vmax(self, orientation: Literal[-3, -2, -1, 0, 1, 2, 3] = 0) -> float: return float(np.max(self.data[orientation].data)) def clear_cache(self): - """Remove all data from the internal cache that has been loaded so far to free memory. - """ + """Remove all data from the internal cache that has been loaded so far to free memory.""" for p in self._patches: p.clear_cache() @@ -223,13 +231,16 @@ class SubObstruction: :ivar show_times: List with points in time from when on the SubObstruction will be shown. """ - def __init__(self, side_surfaces: Tuple, bound_indices: Tuple[int, int, int, int, int, int], - extent: Extent, mesh: 'Mesh'): + def __init__( + self, side_surfaces: Tuple, bound_indices: Tuple[int, int, int, int, int, int], extent: Extent, mesh: "Mesh" + ): self.extent = extent self.side_surfaces = side_surfaces - self.bound_indices = {'x': (bound_indices[0], bound_indices[1]), - 'y': (bound_indices[2], bound_indices[3]), - 'z': (bound_indices[4], bound_indices[5])} + self.bound_indices = { + "x": (bound_indices[0], bound_indices[1]), + "y": (bound_indices[2], bound_indices[3]), + "z": (bound_indices[4], bound_indices[5]), + } self.mesh = mesh self._boundary_data: Dict[int, Boundary] = dict() @@ -237,12 +248,22 @@ def __init__(self, side_surfaces: Tuple, bound_indices: Tuple[int, int, int, int self.hide_times = list() self.show_times = list() - def _add_patches(self, bid: int, cell_centered: bool, quantity: str, short_name: str, unit: str, - patches: List[Patch], times: Sequence[float], lower_bounds: np.ndarray, - upper_bounds: np.ndarray): + def _add_patches( + self, + bid: int, + cell_centered: bool, + quantity: str, + short_name: str, + unit: str, + patches: List[Patch], + times: Sequence[float], + lower_bounds: np.ndarray, + upper_bounds: np.ndarray, + ): if bid not in self._boundary_data: - self._boundary_data[bid] = Boundary(Quantity(quantity, short_name, unit), cell_centered, times, - patches, lower_bounds, upper_bounds) + self._boundary_data[bid] = Boundary( + Quantity(quantity, short_name, unit), cell_centered, times, patches, lower_bounds, upper_bounds + ) # Add reference to parent boundary class in patches for patch in patches: patch._boundary_parent = self._boundary_data[bid] @@ -252,32 +273,32 @@ def _add_patches(self, bid: int, cell_centered: bool, quantity: str, short_name: @property def orientations(self): - """Return all orientations for which there is data available. - """ + """Return all orientations for which there is data available.""" if self.has_boundary_data: return next(iter(self._boundary_data.values())).orientations return [] - def get_coordinates(self, ignore_cell_centered: bool = False) -> Dict[ - int, Dict[Literal['x', 'y', 'z'], np.ndarray]]: + def get_coordinates( + self, ignore_cell_centered: bool = False + ) -> Dict[int, Dict[Literal["x", "y", "z"], np.ndarray]]: """Returns a dictionary containing a numpy ndarray with coordinates for each dimension. - For cell-centered boundary data, the coordinates can be adjusted to represent cell-centered coordinates. + For cell-centered boundary data, the coordinates can be adjusted to represent cell-centered coordinates. - :param ignore_cell_centered: Whether to shift the coordinates when the bndf is cell_centered or not. + :param ignore_cell_centered: Whether to shift the coordinates when the bndf is cell_centered or not. """ if self.has_boundary_data: - return {orientation: patch.get_coordinates(ignore_cell_centered) for orientation, patch in - next(iter(self._boundary_data.values())).data.items()} + return { + orientation: patch.get_coordinates(ignore_cell_centered) + for orientation, patch in next(iter(self._boundary_data.values())).data.items() + } return {} - def get_nearest_index(self, dimension: Literal['x', 'y', 'z'], orientation: int, value: float) -> int: - """Get the nearest mesh coordinate index in a specific dimension for a specific orientation. - """ + def get_nearest_index(self, dimension: Literal["x", "y", "z"], orientation: int, value: float) -> int: + """Get the nearest mesh coordinate index in a specific dimension for a specific orientation.""" if self.has_boundary_data: coords = self.get_coordinates()[orientation][dimension] idx = np.searchsorted(coords, value, side="left") - if idx > 0 and (idx == coords.size or math.fabs(value - coords[idx - 1]) < math.fabs( - value - coords[idx])): + if idx > 0 and (idx == coords.size or math.fabs(value - coords[idx - 1]) < math.fabs(value - coords[idx])): return idx - 1 else: return idx @@ -288,13 +309,16 @@ def has_boundary_data(self): return len(self._boundary_data) != 0 def get_data(self, quantity: Union[str, Quantity]): - if type(quantity) == Quantity: + if isinstance(quantity, Quantity): quantity = quantity.name - return next(b for b in self._boundary_data.values() if - b.quantity.name.lower() == quantity.lower() or b.quantity.short_name.lower() == quantity.lower()) + return next( + b + for b in self._boundary_data.values() + if b.quantity.name.lower() == quantity.lower() or b.quantity.short_name.lower() == quantity.lower() + ) def __getitem__(self, item): - if type(item) == int: + if isinstance(item, int): return self._boundary_data[item] return self.get_data(item) @@ -318,15 +342,13 @@ def n_t(self, count_duplicates=True) -> int: @property def times(self): - """Return all timesteps for which boundary data is available, if any. - """ + """Return all timesteps for which boundary data is available, if any.""" if self.has_boundary_data: return next(iter(self._boundary_data.values())).times return np.array([]) def get_visible_times(self, times: Sequence[float]) -> np.ndarray: - """Returns a ndarray filtering all time steps when the SubObstruction is visible/not hidden. - """ + """Returns a ndarray filtering all time steps when the SubObstruction is visible/not hidden.""" ret = list() hidden = False for time in times: @@ -357,8 +379,7 @@ def vmax(self, quantity: Union[str, Quantity], orientation: Literal[-3, -2, -1, return np.nan def clear_cache(self): - """Remove all data from the internal cache that has been loaded so far to free memory. - """ + """Remove all data from the internal cache that has been loaded so far to free memory.""" for bndf in self._boundary_data.values(): bndf.clear_cache() @@ -386,8 +407,14 @@ class Obstruction: (ranging from 0.0 to 1.0). """ - def __init__(self, oid: str, color_index: int, block_type: int, texture_origin: Tuple[float, float, float], - rgba: Union[Tuple[()], Tuple[float, float, float, float]] = ()): + def __init__( + self, + oid: str, + color_index: int, + block_type: int, + texture_origin: Tuple[float, float, float], + rgba: Union[Tuple[()], Tuple[float, float, float, float]] = (), + ): self.id = oid self.color_index = color_index self.block_type = block_type @@ -399,18 +426,21 @@ def __init__(self, oid: str, color_index: int, block_type: int, texture_origin: @property def bounding_box(self) -> Extent: - """:class:`Extent` object representing the bounding box around the Obstruction. - """ + """:class:`Extent` object representing the bounding box around the Obstruction.""" extents = [sub.extent for sub in self._subobstructions.values()] - return Extent(min(extents, key=lambda e: e.x_start).x_start, max(extents, key=lambda e: e.x_end).x_end, - min(extents, key=lambda e: e.y_start).y_start, max(extents, key=lambda e: e.y_end).y_end, - min(extents, key=lambda e: e.z_start).z_start, max(extents, key=lambda e: e.z_end).z_end) + return Extent( + min(extents, key=lambda e: e.x_start).x_start, + max(extents, key=lambda e: e.x_end).x_end, + min(extents, key=lambda e: e.y_start).y_start, + max(extents, key=lambda e: e.y_end).y_end, + min(extents, key=lambda e: e.z_start).z_start, + max(extents, key=lambda e: e.z_end).z_end, + ) @property def orientations(self): - """Return all orientations for which there is data available. - """ + """Return all orientations for which there is data available.""" if self.has_boundary_data: orientations = set() for subobst in self._subobstructions.values(): @@ -431,33 +461,32 @@ def n_t(self, count_duplicates=True) -> int: @property def times(self): - """Return all timesteps for which boundary data is available, if any. - """ + """Return all timesteps for which boundary data is available, if any.""" for subobst in self._subobstructions.values(): if subobst.has_boundary_data: return subobst.times return np.array([]) def get_visible_times(self, times: Sequence[float]): - """Returns an ndarray filtering all time steps when theSubObstruction is visible/not hidden. - """ + """Returns an ndarray filtering all time steps when theSubObstruction is visible/not hidden.""" for subobst in self._subobstructions.values(): return subobst.get_visible_times(times) return np.array([]) - def get_coordinates(self, ignore_cell_centered: bool = False) -> Dict[ - int, Dict[Literal['x', 'y', 'z'], np.ndarray]]: + def get_coordinates( + self, ignore_cell_centered: bool = False + ) -> Dict[int, Dict[Literal["x", "y", "z"], np.ndarray]]: """Returns a dictionary containing a numpy ndarray with coordinates for each dimension. - For cell-centered boundary data, the coordinates can be adjusted to represent cell-centered coordinates. + For cell-centered boundary data, the coordinates can be adjusted to represent cell-centered coordinates. - :param ignore_cell_centered: Whether to shift the coordinates when the bndf is cell_centered or not. + :param ignore_cell_centered: Whether to shift the coordinates when the bndf is cell_centered or not. """ if self.has_boundary_data: all_coords = dict() for orientation_int in self.orientations: - orientation = ('x', 'y', 'z')[abs(orientation_int) - 1] - coords = {'x': set(), 'y': set(), 'z': set()} - for dim in ('x', 'y', 'z'): + orientation = ("x", "y", "z")[abs(orientation_int) - 1] + coords = {"x": set(), "y": set(), "z": set()} + for dim in ("x", "y", "z"): if orientation == dim: bounding_box_index = 0 if orientation_int < 0 else 1 coords[dim] = np.array([self.bounding_box[dim][bounding_box_index]]) @@ -486,9 +515,11 @@ def get_coordinates(self, ignore_cell_centered: bool = False) -> Dict[ mesh = subobst.mesh mesh_coords = mesh.coordinates[dim] idx = np.searchsorted(mesh_coords, single_coordinate, side="left") - if idx > 0 and (idx == mesh_coords.size or math.fabs( - single_coordinate - mesh_coords[idx - 1]) < math.fabs( - single_coordinate - mesh_coords[idx])): + if idx > 0 and ( + idx == mesh_coords.size + or math.fabs(single_coordinate - mesh_coords[idx - 1]) + < math.fabs(single_coordinate - mesh_coords[idx]) + ): idx = idx + 1 if mesh_coords[idx] - single_coordinate < nearest_coordinate - single_coordinate: nearest_coordinate = mesh_coords[idx] @@ -498,14 +529,12 @@ def get_coordinates(self, ignore_cell_centered: bool = False) -> Dict[ return all_coords return dict() - def get_nearest_index(self, dimension: Literal['x', 'y', 'z'], orientation: int, value: float) -> int: - """Get the nearest mesh coordinate index in a specific dimension for a specific orientation. - """ + def get_nearest_index(self, dimension: Literal["x", "y", "z"], orientation: int, value: float) -> int: + """Get the nearest mesh coordinate index in a specific dimension for a specific orientation.""" if self.has_boundary_data: coords = self.get_coordinates()[orientation][dimension] idx = np.searchsorted(coords, value, side="left") - if idx > 0 and (idx == coords.size or math.fabs(value - coords[idx - 1]) < math.fabs( - value - coords[idx])): + if idx > 0 and (idx == coords.size or math.fabs(value - coords[idx - 1]) < math.fabs(value - coords[idx])): return idx - 1 else: return idx @@ -513,8 +542,7 @@ def get_nearest_index(self, dimension: Literal['x', 'y', 'z'], orientation: int, @property def quantities(self) -> List[Quantity]: - """Get a list of all quantities for which boundary data exists. - """ + """Get a list of all quantities for which boundary data exists.""" if self.has_boundary_data: qs = set() for subobst in self._subobstructions.values(): @@ -524,45 +552,45 @@ def quantities(self) -> List[Quantity]: return [] @property - def meshes(self) -> List['Mesh']: - """Returns a list of all meshes this slice cuts through. - """ + def meshes(self) -> List["Mesh"]: + """Returns a list of all meshes this slice cuts through.""" return [subobst.mesh for subobst in self._subobstructions.values()] def filter_by_orientation(self, orientation: Literal[-3, -2, -1, 0, 1, 2, 3] = 0) -> List[SubObstruction]: """Filter all SubObstructions by a specific orientation. All returned SubObstructions will contain boundary data - in the specified orientation. + in the specified orientation. """ if self.has_boundary_data: - return [subobst for subobst in self._subobstructions.values() if - orientation in subobst.orientations] + return [subobst for subobst in self._subobstructions.values() if orientation in subobst.orientations] return [] - def get_boundary_data(self, quantity: Union[Quantity, str], - orientation: Literal[-3, -2, -1, 0, 1, 2, 3] = 0) -> Dict[str, Boundary]: + def get_boundary_data( + self, quantity: Union[Quantity, str], orientation: Literal[-3, -2, -1, 0, 1, 2, 3] = 0 + ) -> Dict[str, Boundary]: """Gets the boundary data for a specific quantity of all SubObstructions. :param quantity: The quantity to filter by. :param orientation: Optionally filter by a specific orientation as well (-3=-z, -2=-y, -1=-x, 1=x, 2=y, 3=z). A value of 0 indicates to no filter. """ - if type(quantity) == Quantity: + if isinstance(quantity, Quantity): quantity = quantity.name - ret = {subobst.mesh.id: subobst.get_data(quantity) for subobst in self._subobstructions.values() if - subobst.has_boundary_data} + ret = { + subobst.mesh.id: subobst.get_data(quantity) + for subobst in self._subobstructions.values() + if subobst.has_boundary_data + } if orientation == 0: return ret return {mesh: bndf for mesh, bndf in ret.items() if orientation in bndf.data.keys()} def get_nearest_timestep(self, time: float, visible_only: bool = False) -> int: - """Calculates the nearest timestep for which data has been output for this obstruction. - """ + """Calculates the nearest timestep for which data has been output for this obstruction.""" if self.has_boundary_data: times = self.get_visible_times(self.times) if visible_only else self.times idx = np.searchsorted(times, time, side="left") - if time > 0 and (idx == len(times) or math.fabs( - time - times[idx - 1]) < math.fabs(time - times[idx])): + if time > 0 and (idx == len(times) or math.fabs(time - times[idx - 1]) < math.fabs(time - times[idx])): return idx - 1 else: return idx @@ -570,7 +598,7 @@ def get_nearest_timestep(self, time: float, visible_only: bool = False) -> int: def get_nearest_patch(self, x: float = None, y: float = None, z: float = None): """Gets the patch of the :class:`SubObstruction` that has the least distance to the given point. - If there are multiple patches with the same distance, a random one will be selected. + If there are multiple patches with the same distance, a random one will be selected. """ if self.has_boundary_data: d_min = np.finfo(float).max @@ -587,11 +615,11 @@ def get_nearest_patch(self, x: float = None, y: float = None, z: float = None): patches_min.append(patch) if x is not None: - patches_min.sort(key=lambda patch: (patch.extent.x_end - patch.extent.x_start)) + patches_min.sort(key=lambda patch: patch.extent.x_end - patch.extent.x_start) if y is not None: - patches_min.sort(key=lambda patch: (patch.extent.y_end - patch.extent.y_start)) + patches_min.sort(key=lambda patch: patch.extent.y_end - patch.extent.y_start) if z is not None: - patches_min.sort(key=lambda patch: (patch.extent.z_end - patch.extent.z_start)) + patches_min.sort(key=lambda patch: patch.extent.z_end - patch.extent.z_start) if len(patches_min) > 0: return patches_min[0] @@ -599,15 +627,15 @@ def get_nearest_patch(self, x: float = None, y: float = None, z: float = None): @property def has_boundary_data(self): - """Whether boundary data has been output in the simulation. - """ + """Whether boundary data has been output in the simulation.""" return any(subobst.has_boundary_data for subobst in self._subobstructions.values()) - def get_global_boundary_data_arrays(self, quantity: Union[str, Quantity]) -> Dict[ - int, Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]]: + def get_global_boundary_data_arrays( + self, quantity: Union[str, Quantity] + ) -> Dict[int, Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]]: """Creates a global numpy ndarray from all subobstruction's boundary data for each orientation. - :param quantity: The quantity's name or short name for which to load the global arrays. + :param quantity: The quantity's name or short name for which to load the global arrays. """ if not self.has_boundary_data: return dict() @@ -618,9 +646,10 @@ def get_global_boundary_data_arrays(self, quantity: Union[str, Quantity]) -> Dic result = dict() for orientation_int in self.orientations: subobst_sets = [list(), list()] - dim = ['x', 'y', 'z'][abs(orientation_int) - 1] + dim = ["x", "y", "z"][abs(orientation_int) - 1] random_subobst = next( - subobst for subobst in self._subobstructions.values() if orientation_int in subobst.get_coordinates()) + subobst for subobst in self._subobstructions.values() if orientation_int in subobst.get_coordinates() + ) base_coord = random_subobst.get_coordinates(ignore_cell_centered=False)[orientation_int][dim][0] for subobst in self._subobstructions.values(): @@ -638,9 +667,9 @@ def get_global_boundary_data_arrays(self, quantity: Union[str, Quantity]) -> Dic first_result_grid = None for subobsts in subobst_sets: - coord_min = {'x': math.inf, 'y': math.inf, 'z': math.inf} - coord_max = {'x': -math.inf, 'y': -math.inf, 'z': -math.inf} - for dim in ('x', 'y', 'z'): + coord_min = {"x": math.inf, "y": math.inf, "z": math.inf} + coord_max = {"x": -math.inf, "y": -math.inf, "z": -math.inf} + for dim in ("x", "y", "z"): for subobst in subobsts: patch_extent = subobst.get_data(quantity).data[orientation_int].extent co = subobst.get_coordinates(ignore_cell_centered=True)[orientation_int][dim] @@ -650,14 +679,16 @@ def get_global_boundary_data_arrays(self, quantity: Union[str, Quantity]) -> Dic # The global grid will use the finest mesh as base and duplicate values of the coarser meshes # Therefore we first find the finest mesh and calculate the step size in each dimension - step_sizes_min = {'x': coord_max['x'] - coord_min['x'], - 'y': coord_max['y'] - coord_min['y'], - 'z': coord_max['z'] - coord_min['z']} - step_sizes_max = {'x': 0, 'y': 0, 'z': 0} + step_sizes_min = { + "x": coord_max["x"] - coord_min["x"], + "y": coord_max["y"] - coord_min["y"], + "z": coord_max["z"] - coord_min["z"], + } + step_sizes_max = {"x": 0, "y": 0, "z": 0} steps = dict() - global_max = {'x': -math.inf, 'y': -math.inf, 'z': -math.inf} + global_max = {"x": -math.inf, "y": -math.inf, "z": -math.inf} - for dim in ('x', 'y', 'z'): + for dim in ("x", "y", "z"): for subobst in subobsts: subobst_coords = subobst.get_coordinates(ignore_cell_centered=True)[orientation_int] if len(subobst_coords[dim]) <= 1: @@ -668,36 +699,41 @@ def get_global_boundary_data_arrays(self, quantity: Union[str, Quantity]) -> Dic step_sizes_max[dim] = max(step_size, step_sizes_max[dim]) global_max[dim] = max(subobst_coords[dim][-1], global_max[dim]) - for dim in ('x', 'y', 'z'): + for dim in ("x", "y", "z"): if step_sizes_min[dim] == 0: step_sizes_min[dim] = math.inf steps[dim] = 1 else: steps[dim] = max(int(round((coord_max[dim] - coord_min[dim]) / step_sizes_min[dim])), 1) + ( - 0 if cell_centered else 1) + 0 if cell_centered else 1 + ) - grid = np.full((self.n_t(count_duplicates=False), steps['x'], steps['y'], steps['z']), np.nan) + grid = np.full((self.n_t(count_duplicates=False), steps["x"], steps["y"], steps["z"]), np.nan) - start_coordinates = {'x': coord_min['x'], 'y': coord_min['y'], 'z': coord_min['z']} + start_coordinates = {"x": coord_min["x"], "y": coord_min["y"], "z": coord_min["z"]} start_idx = dict() end_idx = dict() for subobst in subobsts: - patch_data = np.expand_dims(subobst.get_data(quantity).data[orientation_int].data, - axis=abs(orientation_int)) + patch_data = np.expand_dims( + subobst.get_data(quantity).data[orientation_int].data, axis=abs(orientation_int) + ) subobst_coords = subobst.get_coordinates(ignore_cell_centered=True)[orientation_int] for axis in (0, 1, 2): - dim = ('x', 'y', 'z')[axis] + dim = ("x", "y", "z")[axis] if axis == abs(orientation_int) - 1: start_idx[dim] = 0 end_idx[dim] = 1 continue n_repeat = max( - int(round((subobst_coords[dim][1] - subobst_coords[dim][0]) / step_sizes_min[dim])), 1) + int(round((subobst_coords[dim][1] - subobst_coords[dim][0]) / step_sizes_min[dim])), 1 + ) start_idx[dim] = int( - round((subobst_coords[dim][0] - start_coordinates[dim]) / step_sizes_min[dim])) + round((subobst_coords[dim][0] - start_coordinates[dim]) / step_sizes_min[dim]) + ) end_idx[dim] = int( - round((subobst_coords[dim][-1] - start_coordinates[dim]) / step_sizes_min[dim])) + round((subobst_coords[dim][-1] - start_coordinates[dim]) / step_sizes_min[dim]) + ) # We ignore border points unless they are actually on the border of the simulation space as all # other border points actually appear twice, as the subobstructions overlap. This only @@ -723,12 +759,19 @@ def get_global_boundary_data_arrays(self, quantity: Union[str, Quantity]) -> Dic if not cell_centered and subobst_coords[dim][-1] == global_max[dim]: patch_data = np.concatenate((patch_data, temp_data), axis=axis + 1) - grid[:, start_idx['x']: end_idx['x'], start_idx['y']: end_idx['y'], - start_idx['z']: end_idx['z']] = patch_data.reshape( - (self.n_t(count_duplicates=False), end_idx['x'] - start_idx['x'], end_idx['y'] - start_idx['y'], - end_idx['z'] - start_idx['z'])) - - # Remove empty dimensions, but make sure to note remove the time dimension if there is only a single timestep + grid[ + :, start_idx["x"] : end_idx["x"], start_idx["y"] : end_idx["y"], start_idx["z"] : end_idx["z"] + ] = patch_data.reshape( + ( + self.n_t(count_duplicates=False), + end_idx["x"] - start_idx["x"], + end_idx["y"] - start_idx["y"], + end_idx["z"] - start_idx["z"], + ) + ) + + # Remove empty dimensions, but make sure to note remove the time dimension if there is only a single + # timestep grid = np.squeeze(grid) if len(grid.shape) == 2: grid = grid[np.newaxis, :, :] @@ -739,8 +782,7 @@ def get_global_boundary_data_arrays(self, quantity: Union[str, Quantity]) -> Dic return result def clear_cache(self): - """Remove all data from the internal cache that has been loaded so far to free memory. - """ + """Remove all data from the internal cache that has been loaded so far to free memory.""" for s in self._subobstructions.values(): s.clear_cache() @@ -763,13 +805,12 @@ def vmax(self, quantity: Union[str, Quantity], orientation: Literal[-3, -2, -1, return max(s.vmax(quantity, orientation) for s in self._subobstructions.values()) return np.nan - def __getitem__(self, key: Union[int, str, 'Mesh']): - """Gets either the nth :class:`SubObstruction` or the one with the given mesh-id. - """ + def __getitem__(self, key: Union[int, str, "Mesh"]): + """Gets either the nth :class:`SubObstruction` or the one with the given mesh-id.""" - if type(key) == int: + if isinstance(key, int): return tuple(self._subobstructions.values())[key] - elif type(key) == str: + elif isinstance(key, str): return self._subobstructions[key] return self._subobstructions[key.id] @@ -777,5 +818,9 @@ def __eq__(self, other): return self.id == other.id def __repr__(self, *args, **kwargs): - return f"Obstruction(id={self.id}, Bounding-Box={self.bounding_box}, SubObstructions={len(self._subobstructions)}" + ( - f", Quantities={[q.short_name for q in self.quantities]}" if self.has_boundary_data else "") + ")" + return ( + f"Obstruction(id={self.id}, Bounding-Box={self.bounding_box}, " + f"SubObstructions={len(self._subobstructions)}" + + (f", Quantities={[q.short_name for q in self.quantities]}" if self.has_boundary_data else "") + + ")" + ) diff --git a/fdsreader/bndf/obstruction_collection.py b/fdsreader/bndf/obstruction_collection.py index ee4cfbc1..fd3da6df 100644 --- a/fdsreader/bndf/obstruction_collection.py +++ b/fdsreader/bndf/obstruction_collection.py @@ -1,4 +1,5 @@ -from typing import Iterable, Tuple, List +from typing import Iterable, List + import numpy as np from fdsreader.bndf import Obstruction @@ -7,7 +8,7 @@ class ObstructionCollection(FDSDataCollection): """Collection of :class:`Obstruction` objects. Offers extensive functionality for filtering and - using obstructions as well as dependent such as :class:`Boundary`. + using obstructions as well as dependent such as :class:`Boundary`. """ def __init__(self, *obstructions: Iterable[Obstruction]): @@ -22,18 +23,15 @@ def quantities(self) -> List[Quantity]: return list(qs) def filter_by_boundary_data(self): - """Filters all obstructions for which output data exists. - """ + """Filters all obstructions for which output data exists.""" return ObstructionCollection(x for x in self._elements if x.has_boundary_data) def get_by_id(self, obst_id: str): - """Get the obstruction with corresponding id if it exists. - """ + """Get the obstruction with corresponding id if it exists.""" return next((obst for obst in self._elements if obst.id == obst_id), None) def get_nearest(self, x: float = None, y: float = None, z: float = None) -> Obstruction: - """Filters the obstruction with the shortest distance to the given point. - """ + """Filters the obstruction with the shortest distance to the given point.""" d_min = np.finfo(float).max obst_min = None @@ -50,4 +48,4 @@ def get_nearest(self, x: float = None, y: float = None, z: float = None) -> Obst return obst_min def __repr__(self): - return "ObstructionCollection(" + super(ObstructionCollection, self).__repr__() + ")" + return "ObstructionCollection(" + super().__repr__() + ")" diff --git a/fdsreader/bndf/utils.py b/fdsreader/bndf/utils.py index 4a73aa46..9be1aeb3 100644 --- a/fdsreader/bndf/utils.py +++ b/fdsreader/bndf/utils.py @@ -4,8 +4,7 @@ def sort_patches_cartesian(patches_in: List[Patch]): - """Returns all patches (of same orientation!) sorted in cartesian coordinates. - """ + """Returns all patches (of same orientation!) sorted in cartesian coordinates.""" patches = patches_in.copy() if len(patches) != 0: patches_cart = [[patches[0]]] @@ -30,4 +29,4 @@ def sort_patches_cartesian(patches_in: List[Patch]): else: patches_cart.append([patch]) return patches_cart - return patches \ No newline at end of file + return patches diff --git a/fdsreader/devc/__init__.py b/fdsreader/devc/__init__.py index 7e92e5cc..912ec140 100644 --- a/fdsreader/devc/__init__.py +++ b/fdsreader/devc/__init__.py @@ -1,3 +1,2 @@ -from .device import Device - -from .device_collection import DeviceCollection \ No newline at end of file +from .device import Device as Device +from .device_collection import DeviceCollection as DeviceCollection diff --git a/fdsreader/devc/device.py b/fdsreader/devc/device.py index 85e5bf09..f4a16dad 100644 --- a/fdsreader/devc/device.py +++ b/fdsreader/devc/device.py @@ -1,4 +1,4 @@ -from typing import Tuple, Union +from typing import Tuple from fdsreader.utils import Quantity @@ -12,8 +12,14 @@ class Device: :ivar orientation: The direction the device was facing. :ivar data: All data the device measured. """ - def __init__(self, device_id: str, quantity: Quantity, position: Tuple[float, float, float], - orientation: Tuple[float, float, float]): + + def __init__( + self, + device_id: str, + quantity: Quantity, + position: Tuple[float, float, float], + orientation: Tuple[float, float, float], + ): self.id = device_id self.quantity = quantity self.position = position @@ -32,33 +38,28 @@ def data(self): @property def quantity_name(self): - """Alias for :class:`Device`.quantity.name. - """ + """Alias for :class:`Device`.quantity.name.""" return self.quantity.name @property def unit(self): - """Alias for :class:`Device`.quantity.unit. - """ + """Alias for :class:`Device`.quantity.unit.""" return self.quantity.unit @property def xyz(self): - """Alias for :class:`Device`.position. - """ + """Alias for :class:`Device`.position.""" return self.position def clear_cache(self): - """Remove all data from the internal cache that has been loaded so far to free memory. - """ + """Remove all data from the internal cache that has been loaded so far to free memory.""" if hasattr(self, "_data"): del self._data def __eq__(self, other): - if type(other) == str: + if isinstance(other, str): return self.id == other return self.id == other.id def __repr__(self): return f"Device(id='{self.id}', xyz={self.position}, quantity={self.quantity})" - diff --git a/fdsreader/devc/device_collection.py b/fdsreader/devc/device_collection.py index e54f1952..a4f69600 100644 --- a/fdsreader/devc/device_collection.py +++ b/fdsreader/devc/device_collection.py @@ -1,38 +1,39 @@ - -from typing import Iterable, Union, List +from typing import Iterable, List, Union from fdsreader.devc import Device from fdsreader.utils.data import FDSDataCollection class DeviceCollection(FDSDataCollection): - """Collection of :class:`Device` objects. Offers additional functionality for working on devices using pandas. - """ + """Collection of :class:`Device` objects. Offers additional functionality for working on devices using pandas.""" def __init__(self, *devices: Iterable[Device]): super().__init__(*devices) def __getitem__(self, key) -> Union[Device, List[Device]]: - if type(key) == int: + if isinstance(key, int): return self._elements[key] else: - return next(devc for devc in self._elements if (devc.id == key if type(devc) == Device else devc[0].id == key)) + return next( + devc for devc in self._elements if (devc.id == key if isinstance(devc, Device) else devc[0].id == key) + ) def __contains__(self, value: Union[Device, str]): - id_matching = any((devc.id == value if type(devc) == Device else devc[0].id == value) for devc in self._elements) + id_matching = any( + (devc.id == value if isinstance(devc, Device) else devc[0].id == value) for devc in self._elements + ) return value in self._elements or id_matching def to_pandas_dataframe(self): - """Returns a pandas DataFrame with device-IDs as column names and device data as column values. - """ + """Returns a pandas DataFrame with device-IDs as column names and device data as column values.""" import pandas as pd + data = dict() for devc in self: - if type(devc) == Device: + if isinstance(devc, Device): data[devc.id] = devc.data - elif type(devc) == list: + elif isinstance(devc, list): # It might be the case that there are multiple devices with the same name for i, list_devc in enumerate(devc): data[list_devc.id + "_" + str(i)] = list_devc.data return pd.DataFrame(data) - diff --git a/fdsreader/evac/__init__.py b/fdsreader/evac/__init__.py index ddd92a28..20c3b5ad 100644 --- a/fdsreader/evac/__init__.py +++ b/fdsreader/evac/__init__.py @@ -1,3 +1,2 @@ -from .evacuation import Evacuation - -from .evac_collection import EvacCollection +from .evac_collection import EvacCollection as EvacCollection +from .evacuation import Evacuation as Evacuation diff --git a/fdsreader/evac/evac_collection.py b/fdsreader/evac/evac_collection.py index 383564bc..ecf7af12 100644 --- a/fdsreader/evac/evac_collection.py +++ b/fdsreader/evac/evac_collection.py @@ -1,31 +1,31 @@ import logging import os -from typing import Iterable, Dict, List, Union +from typing import Dict, Iterable, List, Union + import numpy as np -from fdsreader.evac import Evacuation -from fdsreader.fds_classes import Mesh -from fdsreader.utils.data import FDSDataCollection, Quantity import fdsreader.utils.fortran_data as fdtype from fdsreader import settings +from fdsreader.evac.evacuation import Evacuation +from fdsreader.utils.data import FDSDataCollection, Quantity class EvacCollection(FDSDataCollection): """Collection of :class:`Evacuation` objects. Next to agent-class specific data (such as - trajectories) lots of other data such as FED-data is provided via this class. - Note: Evac support was removed from FDS in all versions after 6.7.7! - - :ivar times: List of all time steps of the simulation. - :ivar z_offsets: The offset in z-direction for each mesh where the evac plane lays. - :ivar all_agents: Number of all agents per time step. - :ivar agents_inside_mesh: Number of all agents per time step inside a specific mesh. - :ivar number_of_deads: Number of dead agents per time step. - :ivar fed_max: FED max per time step. - :ivar fed_max_alive: FED max alive per time step. - :ivar exit_counters: Exit counts per time step. - :ivar target_exit_counters: Target exit counts per time step. - :ivar door_counters: Door counts per time step. - :ivar target_door_counters: Target door counts per time step. + trajectories) lots of other data such as FED-data is provided via this class. + Note: Evac support was removed from FDS in all versions after 6.7.7! + + :ivar times: List of all time steps of the simulation. + :ivar z_offsets: The offset in z-direction for each mesh where the evac plane lays. + :ivar all_agents: Number of all agents per time step. + :ivar agents_inside_mesh: Number of all agents per time step inside a specific mesh. + :ivar number_of_deads: Number of dead agents per time step. + :ivar fed_max: FED max per time step. + :ivar fed_max_alive: FED max alive per time step. + :ivar exit_counters: Exit counts per time step. + :ivar target_exit_counters: Target exit counts per time step. + :ivar door_counters: Door counts per time step. + :ivar target_door_counters: Target door counts per time step. """ def __init__(self, evacs: Iterable[Evacuation], base_path: str, times: Iterable[float]): @@ -52,8 +52,7 @@ def __init__(self, evacs: Iterable[Evacuation], base_path: str, times: Iterable[ @property def quantities(self) -> List[Quantity]: - """Gives a list of all quantities that are written out for some (or sometimes all) human classes. - """ + """Gives a list of all quantities that are written out for some (or sometimes all) human classes.""" qs = set() for evac in self: for q in evac.quantities: @@ -64,10 +63,10 @@ def _load_csv_data(self): file_path = self._base_path + ".csv" if not os.path.exists(file_path): return - with open(file_path, 'r') as infile: - units = [unit.replace('"', '').replace('\n', '').strip() for unit in infile.readline().split(',')] - names = [name.replace('"', '').replace('\n', '').strip() for name in infile.readline().split(',')] - values = np.genfromtxt(infile, delimiter=',', dtype=np.float32, autostrip=True) + with open(file_path) as infile: + units = [unit.replace('"', "").replace("\n", "").strip() for unit in infile.readline().split(",")] + names = [name.replace('"', "").replace("\n", "").strip() for name in infile.readline().split(",")] + values = np.genfromtxt(infile, delimiter=",", dtype=np.float32, autostrip=True) dtypes = [int] * len(names) dtypes[0] = float dtypes[-2:] = (float, float) @@ -80,24 +79,26 @@ def _load_csv_data(self): self.times = data["EVAC_Time"] self.all_agents = data["AllAgents"] - self.agents_inside_mesh = {names[i]: data[names[i]] for i in range(len(names)) if - units[i] == "AgentsInsideMesh"} + self.agents_inside_mesh = { + names[i]: data[names[i]] for i in range(len(names)) if units[i] == "AgentsInsideMesh" + } self.number_of_deads = data["Number_of_Deads"] self.fed_max = data["FED_max"] self.fed_max_alive = data["FED_max_alive"] self.exit_counters = {names[i]: data[names[i]] for i in range(len(names)) if units[i] == "ExitCounter"} - self.target_exit_counters = {names[i]: data[names[i]] for i in range(len(names)) if - units[i] == "TargetExitCounter"} + self.target_exit_counters = { + names[i]: data[names[i]] for i in range(len(names)) if units[i] == "TargetExitCounter" + } self.door_counters = {names[i]: data[names[i]] for i in range(len(names)) if units[i] == "DoorCounter"} - self.target_door_counters = {names[i]: data[names[i]] for i in range(len(names)) if - units[i] == "TargetDoorCounter"} + self.target_door_counters = { + names[i]: data[names[i]] for i in range(len(names)) if units[i] == "TargetDoorCounter" + } @property def xyz(self) -> List[np.ndarray]: - """List of xyz-data for each mesh. - """ + """List of xyz-data for each mesh.""" if not hasattr(self, "_xyz"): self._load_xyz_data() return self._xyz @@ -110,20 +111,22 @@ def _load_xyz_data(self): self._load_fed_data(0) return - with open(file_path, 'rb') as infile: - dtype_meta = fdtype.new((('i', 6),)) - dtype_grid_meta = fdtype.new((('i', 4),)) - dtype_grid_data = fdtype.new((('f', 3),)) + with open(file_path, "rb") as infile: + dtype_meta = fdtype.new((("i", 6),)) + dtype_grid_meta = fdtype.new((("i", 4),)) + dtype_grid_data = fdtype.new((("f", 3),)) file_format = fdtype.read(infile, fdtype.INT, 1)[0][0][0] if file_format != -4: - logging.warning("The evac xyz file (" + file_path + ") was written in an unsupported file format. " - "Please submit an issue on Github!") + logging.warning( + "The evac xyz file (" + file_path + ") was written in an unsupported file format. " + "Please submit an issue on Github!" + ) meta = fdtype.read(infile, dtype_meta, 1)[0][0] n_grids = meta[0] n_corrs = meta[2] - n_devc = fdtype.read(infile, fdtype.INT, 1)[0][0][0] + fdtype.read(infile, fdtype.INT, 1)[0][0][0] n_i = list() n_j = list() @@ -151,32 +154,28 @@ def _load_xyz_data(self): @property def fed_grid(self) -> Dict[str, List[np.ndarray]]: - """ - """ + """ """ if not hasattr(self, "_fed_grid"): self._load_xyz_data() return self._fed_grid @property def fed_corr(self) -> Dict[str, List[np.ndarray]]: - """ - """ + """ """ if not hasattr(self, "_fed_corr"): self._load_xyz_data() return self._fed_corr @property def devc(self) -> Dict[str, Union[List[np.ndarray], np.ndarray]]: - """ - """ + """ """ if not hasattr(self, "_devc"): self._load_xyz_data() return self._devc @property def fed_times(self) -> np.ndarray: - """ - """ + """ """ if not hasattr(self, "_fed_times"): self._load_xyz_data() return self._fed_times @@ -191,19 +190,21 @@ def _load_fed_data(self, n_corrs: int): self._fed_times = np.array([]) return - with open(file_path, 'rb') as infile: - dtype_meta = fdtype.new((('i', 6),)) - dtype_time = fdtype.new((('f', 2),)) - dtype_grid_meta = fdtype.new((('i', 4),)) - dtype_corr = fdtype.new((('f', 8),)) - dtype_devs_meta = fdtype.new((('i', 2),)) - dtype_devs_data = fdtype.new((('i', 1), ('f', 1), ('i', 2), ('f', 1))) + with open(file_path, "rb") as infile: + dtype_meta = fdtype.new((("i", 6),)) + dtype_time = fdtype.new((("f", 2),)) + dtype_grid_meta = fdtype.new((("i", 4),)) + dtype_corr = fdtype.new((("f", 8),)) + dtype_devs_meta = fdtype.new((("i", 2),)) + dtype_devs_data = fdtype.new((("i", 1), ("f", 1), ("i", 2), ("f", 1))) # File header file_format = fdtype.read(infile, fdtype.INT, 1)[0][0][0] if file_format != -4: - logging.warning("The evac fed file (" + file_path + ") was written in an unsupported file format. " - "Please submit an issue on Github!") + logging.warning( + "The evac fed file (" + file_path + ") was written in an unsupported file format. " + "Please submit an issue on Github!" + ) n_grids = fdtype.read(infile, dtype_meta, 1)[0][0][0] n_devc = fdtype.read(infile, fdtype.INT, 1)[0][0][0] @@ -220,7 +221,7 @@ def _load_fed_data(self, n_corrs: int): n_i.append(n_i_g) n_j.append(n_j_g) n.append(n_g) - dtype_grid_data.append(fdtype.new((('f', n_g),))) + dtype_grid_data.append(fdtype.new((("f", n_g),))) for i in range(n_i_g): for j in range(n_j_g): _ = fdtype.read(infile, dtype_grid_data[-1], 1) @@ -229,24 +230,36 @@ def _load_fed_data(self, n_corrs: int): infile.seek(dtype_meta.itemsize + fdtype.INT.itemsize * 2) n_t = (os.stat(file_path).st_size - (fdtype.INT.itemsize * 2 + dtype_meta.itemsize)) // ( - dtype_time.itemsize + sum( - (dtype_grid_meta.itemsize + n_i[g] * n_j[g] * dtype_grid_data[g].itemsize) for g in - range(n_grids)) + n_corrs * dtype_corr.itemsize + fdtype.FLOAT.itemsize + n_devc * ( - dtype_devs_meta.itemsize + dtype_devs_data.itemsize)) + dtype_time.itemsize + + sum( + (dtype_grid_meta.itemsize + n_i[g] * n_j[g] * dtype_grid_data[g].itemsize) for g in range(n_grids) + ) + + n_corrs * dtype_corr.itemsize + + fdtype.FLOAT.itemsize + + n_devc * (dtype_devs_meta.itemsize + dtype_devs_data.itemsize) + ) times = list() - self._fed_grid = dict(co_co2_o2=[np.empty((n_t, n_i[g], n_j[g])) for g in range(n_grids)], - soot_dens=[np.empty((n_t, n_i[g], n_j[g])) for g in range(n_grids)], - tmp_g=[np.empty((n_t, n_i[g], n_j[g])) for g in range(n_grids)], - radflux=[np.empty((n_t, n_i[g], n_j[g])) for g in range(n_grids)]) - self._fed_corr = dict(co_co2_o2=[np.empty((n_t, 2)) for _ in range(n_corrs)], - soot_dens=[np.empty((n_t, 2)) for _ in range(n_corrs)], - tmp_g=[np.empty((n_t, 2)) for _ in range(n_corrs)], - radflux=[np.empty((n_t, 2)) for _ in range(n_corrs)]) - self._devc = dict(i_type=[0 for _ in range(n_devc)], devc_id=[0 for _ in range(n_devc)], - current=[np.empty((n_t,), dtype=int) for _ in range(n_devc)], - prior=[np.empty((n_t,), dtype=int) for _ in range(n_devc)], - t_change=[np.empty((n_t,)) for _ in range(n_devc)], times=np.empty((n_t,))) + self._fed_grid = dict( + co_co2_o2=[np.empty((n_t, n_i[g], n_j[g])) for g in range(n_grids)], + soot_dens=[np.empty((n_t, n_i[g], n_j[g])) for g in range(n_grids)], + tmp_g=[np.empty((n_t, n_i[g], n_j[g])) for g in range(n_grids)], + radflux=[np.empty((n_t, n_i[g], n_j[g])) for g in range(n_grids)], + ) + self._fed_corr = dict( + co_co2_o2=[np.empty((n_t, 2)) for _ in range(n_corrs)], + soot_dens=[np.empty((n_t, 2)) for _ in range(n_corrs)], + tmp_g=[np.empty((n_t, 2)) for _ in range(n_corrs)], + radflux=[np.empty((n_t, 2)) for _ in range(n_corrs)], + ) + self._devc = dict( + i_type=[0 for _ in range(n_devc)], + devc_id=[0 for _ in range(n_devc)], + current=[np.empty((n_t,), dtype=int) for _ in range(n_devc)], + prior=[np.empty((n_t,), dtype=int) for _ in range(n_devc)], + t_change=[np.empty((n_t,)) for _ in range(n_devc)], + times=np.empty((n_t,)), + ) for t in range(n_t): times.append(fdtype.read(infile, dtype_time, 1)[0][0]) # t, dt_save @@ -263,8 +276,9 @@ def _load_fed_data(self, n_corrs: int): # Corr for c in range(n_corrs): - co_co2_o2_1, soot_dens_1, tmp_g_1, radflux_1, co_co2_o2_2, soot_dens_2, tmp_g_2, radflux_2 = \ + co_co2_o2_1, soot_dens_1, tmp_g_1, radflux_1, co_co2_o2_2, soot_dens_2, tmp_g_2, radflux_2 = ( fdtype.read(infile, dtype_corr, 1)[0][0][:8] + ) self._fed_corr["co_co2_o2"][c][t] = (co_co2_o2_1, co_co2_o2_2) self._fed_corr["soot_dens"][c][t] = (soot_dens_1, soot_dens_2) self._fed_corr["tmp_g"][c][t] = (tmp_g_1, tmp_g_2) @@ -294,14 +308,15 @@ def _load_eff_data(self): self._eff = None return - with open(file_path, 'rb') as infile: - dtype_grid_meta = fdtype.new((('i', 3),)) - dtype_grid_data = fdtype.new((('f', 2),)) # u, v + with open(file_path, "rb") as infile: + dtype_grid_meta = fdtype.new((("i", 3),)) + dtype_grid_data = fdtype.new((("f", 2),)) # u, v - n_grids = fdtype.read(infile, fdtype.INT, 1)[0][0][0] + fdtype.read(infile, fdtype.INT, 1)[0][0][0] # n_fields = (os.stat(file_path).st_size - fdtype.INT.itemsize) // ( - # sum(dtype_grid_meta.itemsize + n_i[g] * n_j[g] * dtype_grid_data.itemsize for g in range(n_grids))) + # sum(dtype_grid_meta.itemsize + n_i[g] * n_j[g] * dtype_grid_data.itemsize + # for g in range(n_grids))) # # self._eff = np.empty((n_grids, n_fields, n_i, n_j, 2)) # for g in range(n_grids): @@ -340,7 +355,7 @@ def get_unfiltered_positions(self, quantity: Union[Quantity, str] = None): size = 0 for evac in filtered_evacs: pos = evac.positions[t] - combined_positions[t][size:size + pos.shape[0]] = pos + combined_positions[t][size : size + pos.shape[0]] = pos size += pos.shape[0] return combined_positions @@ -366,14 +381,13 @@ def get_unfiltered_data(self, quantity: Union[Quantity, str]): if size != 0: size = 0 for d in data: - combined_data[t][size:size + d[t].shape[0]] = d[t] + combined_data[t][size : size + d[t].shape[0]] = d[t] size += d[t].shape[0] return combined_data def _load_prt_data(self): - """Function to read in all evac data for a simulation. - """ + """Function to read in all evac data for a simulation.""" evacs = self pointer_location = {evac: [0] * len(self.times) for evac in evacs} @@ -393,14 +407,13 @@ def _load_prt_data(self): evac._tags.append(np.empty((size,), dtype=int)) for mesh, file_path in self._file_paths.items(): - with open(file_path, 'rb') as infile: + with open(file_path, "rb") as infile: # Initial offset (ONE, fds version and number of evac classes) offset = 3 * fdtype.INT.itemsize # Number of quantities for each evac class (plus an INTEGER_ZERO) - offset += fdtype.new((('i', 2),)).itemsize * len(evacs) + offset += fdtype.new((("i", 2),)).itemsize * len(evacs) # 30-char long name and unit information for each quantity - offset += fdtype.new((('c', 30),)).itemsize * 2 * sum( - [len(evac.quantities) for evac in evacs]) + offset += fdtype.new((("c", 30),)).itemsize * 2 * sum([len(evac.quantities) for evac in evacs]) infile.seek(offset) for t in range(len(self.times)): @@ -413,34 +426,35 @@ def _load_prt_data(self): n_humans = fdtype.read(infile, fdtype.INT, 1)[0][0][0] offset = pointer_location[evac][t] # Read positions - dtype_positions = fdtype.new((('f', 7 * n_humans),)) - pos = fdtype.read(infile, dtype_positions, 1)[0][0].reshape((n_humans, 7), - order='F').astype(float) - evac._positions[t][offset: offset + n_humans] = pos[:, :3] - evac._body_angles[t][offset: offset + n_humans] = pos[:, 3] - evac._semi_major_axis[t][offset: offset + n_humans] = pos[:, 4] - evac._semi_minor_axis[t][offset: offset + n_humans] = pos[:, 5] - evac._agent_heights[t][offset: offset + n_humans] = pos[:, 6] + dtype_positions = fdtype.new((("f", 7 * n_humans),)) + pos = ( + fdtype.read(infile, dtype_positions, 1)[0][0] + .reshape((n_humans, 7), order="F") + .astype(float) + ) + evac._positions[t][offset : offset + n_humans] = pos[:, :3] + evac._body_angles[t][offset : offset + n_humans] = pos[:, 3] + evac._semi_major_axis[t][offset : offset + n_humans] = pos[:, 4] + evac._semi_minor_axis[t][offset : offset + n_humans] = pos[:, 5] + evac._agent_heights[t][offset : offset + n_humans] = pos[:, 6] # Read tags - dtype_tags = fdtype.new((('i', n_humans),)) - evac._tags[t][offset: offset + n_humans] = fdtype.read(infile, dtype_tags, 1)[0][0] + dtype_tags = fdtype.new((("i", n_humans),)) + evac._tags[t][offset : offset + n_humans] = fdtype.read(infile, dtype_tags, 1)[0][0] # Read actual quantity values if len(evac.quantities) > 0: - dtype_data = fdtype.new( - (('f', str((n_humans, len(evac.quantities)))),)) + dtype_data = fdtype.new((("f", str((n_humans, len(evac.quantities)))),)) data_raw = fdtype.read(infile, dtype_data, 1)[0][0].reshape( - (n_humans, len(evac.quantities)), order='F') + (n_humans, len(evac.quantities)), order="F" + ) for q, quantity in enumerate(evac.quantities): - evac._data[quantity.name][t][ - offset:offset + n_humans] = data_raw[:, q].astype(float) + evac._data[quantity.name][t][offset : offset + n_humans] = data_raw[:, q].astype(float) pointer_location[evac][t] += evac.n_humans[mesh][t] def __getitem__(self, key): - """Get the evac data for a specific agent/human class. - """ - if type(key) == int: + """Get the evac data for a specific agent/human class.""" + if isinstance(key, int): return self._elements[key] for evac in self: if evac.class_name == key: @@ -455,4 +469,4 @@ def __contains__(self, value): return False def __repr__(self): - return "EvacCollection(" + super(EvacCollection, self).__repr__() + ")" + return "EvacCollection(" + super().__repr__() + ")" diff --git a/fdsreader/evac/evacuation.py b/fdsreader/evac/evacuation.py index d8379d1f..c583c00b 100644 --- a/fdsreader/evac/evacuation.py +++ b/fdsreader/evac/evacuation.py @@ -1,11 +1,9 @@ -from typing import List, Tuple, Dict, Sequence, Union +from typing import Dict, List, Sequence, Tuple, Union import numpy as np -from fdsreader.fds_classes import Mesh from fdsreader.utils import Quantity - # class Entrance: # def __init__(self): # self.id = "" @@ -74,14 +72,14 @@ def id(self): return self.class_name def has_quantity(self, quantity: Union[Quantity, str]): - if type(quantity) == Quantity: + if isinstance(quantity, Quantity): quantity = quantity.name return any( - q.name.lower() == quantity.lower() or q.short_name.lower() == quantity.lower() for q in self.quantities) + q.name.lower() == quantity.lower() or q.short_name.lower() == quantity.lower() for q in self.quantities + ) def filter_by_tag(self, tag: int): - """Filter all evacs by a single one with the specified tag. - """ + """Filter all evacs by a single one with the specified tag.""" data = self.data tags = self.tags positions = self.positions @@ -116,7 +114,7 @@ def filter_by_tag(self, tag: int): @property def data(self) -> Dict[str, List[np.ndarray]]: """Dictionary with quantities as keys and a list with a numpy array for each timestep which - contains data for each person in that timestep. + contains data for each person in that timestep. """ if len(self._positions) == 0 and len(self._tags) == 0: self._init_callback() @@ -124,10 +122,10 @@ def data(self) -> Dict[str, List[np.ndarray]]: def get_data(self, quantity: Union[Quantity, str]) -> List[np.ndarray]: """Returns a list with a numpy array for each timestep which contains data about the specified quantity for - each person in that timestep. + each person in that timestep. """ if self.has_quantity(quantity): - if type(quantity) == Quantity: + if isinstance(quantity, Quantity): quantity = quantity.name return self.data[quantity] return [] @@ -135,7 +133,7 @@ def get_data(self, quantity: Union[Quantity, str]) -> List[np.ndarray]: @property def tags(self) -> List[np.ndarray]: """List with a numpy array for each timestep which contains a tag for each evac in that - timestep. + timestep. """ if len(self._positions) == 0 and len(self._tags) == 0: self._init_callback() @@ -144,7 +142,7 @@ def tags(self) -> List[np.ndarray]: @property def positions(self) -> List[np.ndarray]: """List with a numpy array for each timestep which contains the position of each evac in - that timestep. + that timestep. """ if len(self._positions) == 0 and len(self._tags) == 0: self._init_callback() @@ -152,39 +150,34 @@ def positions(self) -> List[np.ndarray]: @property def body_angles(self) -> List[np.ndarray]: - """ - """ + """ """ if len(self._positions) == 0 and len(self._tags) == 0: self._init_callback() return self._body_angles @property def semi_major_axis(self) -> List[np.ndarray]: - """ - """ + """ """ if len(self._positions) == 0 and len(self._tags) == 0: self._init_callback() return self._semi_major_axis @property def semi_minor_axis(self) -> List[np.ndarray]: - """ - """ + """ """ if len(self._positions) == 0 and len(self._tags) == 0: self._init_callback() return self._semi_minor_axis @property def agent_heights(self) -> List[np.ndarray]: - """ - """ + """ """ if len(self._positions) == 0 and len(self._tags) == 0: self._init_callback() return self._agent_heights def clear_cache(self): - """Remove all data from the internal cache that has been loaded so far to free memory. - """ + """Remove all data from the internal cache that has been loaded so far to free memory.""" if len(self._positions) != 0: del self._positions self._positions = list() diff --git a/fdsreader/export/__init__.py b/fdsreader/export/__init__.py index 8f37608e..dd2261a6 100644 --- a/fdsreader/export/__init__.py +++ b/fdsreader/export/__init__.py @@ -1,4 +1,4 @@ -from .smoke3d_exporter import export_smoke_raw -from .slcf_exporter import export_slcf_raw -from .obst_exporter import export_obst_raw -from .sim_exporter import export_sim +from .obst_exporter import export_obst_raw as export_obst_raw +from .sim_exporter import export_sim as export_sim +from .slcf_exporter import export_slcf_raw as export_slcf_raw +from .smoke3d_exporter import export_smoke_raw as export_smoke_raw diff --git a/fdsreader/export/obst_exporter.py b/fdsreader/export/obst_exporter.py index 04206280..0cb978f1 100644 --- a/fdsreader/export/obst_exporter.py +++ b/fdsreader/export/obst_exporter.py @@ -1,13 +1,14 @@ import os from pathlib import Path -from typing import Dict, Union, Tuple +from typing import Dict, Tuple, Union import numpy as np from typing_extensions import Literal + from ..bndf import Obstruction -def export_obst_raw(obst: Obstruction, output_dir: str, ordering: Literal['C', 'F'] = 'C'): +def export_obst_raw(obst: Obstruction, output_dir: str, ordering: Literal["C", "F"] = "C"): """Exports the 3d arrays to raw binary files with corresponding .yaml meta files. :param obst: The :class:`Obstruction` object to export including its :class:`Boundary` data. @@ -17,13 +18,17 @@ def export_obst_raw(obst: Obstruction, output_dir: str, ordering: Literal['C', ' if len(obst.quantities) == 0: return "" + from multiprocess import Manager from pathos.pools import ProcessPool as Pool - from multiprocess import Lock, Manager + obst_filename_base = "obst-" + str(obst.id) bounding_box = obst.bounding_box.as_list() - meta = {"BoundingBox": " ".join(f"{b:.6f}" for b in bounding_box), "NumQuantities": len(obst.quantities), - "Orientations": list()} + meta = { + "BoundingBox": " ".join(f"{b:.6f}" for b in bounding_box), + "NumQuantities": len(obst.quantities), + "Orientations": list(), + } m = Manager() lock = m.Lock() meta["Quantities"] = m.list() @@ -42,15 +47,18 @@ def export_obst_raw(obst: Obstruction, output_dir: str, ordering: Literal['C', ' spacing1 = (bounding_box[1] - bounding_box[0]) / face.shape[0] spacing2 = (bounding_box[3] - bounding_box[2]) / face.shape[1] - meta["Orientations"].append({ - "BoundaryOrientation": orientation, - # "MeshPos": f"{meta['BoundingBox'][0]:.6f} {meta['BoundingBox'][2]:.6f} {meta['BoundingBox'][4]:.6f}", - "Spacing": f"{random_bndf.times[1] - random_bndf.times[0]:.6f} {spacing1:.6f} {spacing2:.6f}", - "DimSize": f"{face.shape[0]} {face.shape[1]}" - }) - - def worker(quantity: str, faces: Dict[int, Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]], vmin: float, - vmax: float): + meta["Orientations"].append( + { + "BoundaryOrientation": orientation, + # "MeshPos": f"{meta['BoundingBox'][0]:.6f} {meta['BoundingBox'][2]:.6f} {meta['BoundingBox'][4]:.6f}", + "Spacing": f"{random_bndf.times[1] - random_bndf.times[0]:.6f} {spacing1:.6f} {spacing2:.6f}", + "DimSize": f"{face.shape[0]} {face.shape[1]}", + } + ) + + def worker( + quantity: str, faces: Dict[int, Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]], vmin: float, vmax: float + ): quantity_name = quantity.replace(" ", "_").replace(".", "-") filename = obst_filename_base + "_quantity-" + quantity_name + ".dat" @@ -59,17 +67,17 @@ def worker(quantity: str, faces: Dict[int, Union[np.ndarray, Tuple[np.ndarray, n "DataFile": os.path.join(quantity_name, filename), "DataValMax": vmax, "DataValMin": vmin, - "ScaleFactor": 255.0 / vmax + "ScaleFactor": 255.0 / vmax, } # Abort if no useful data is available if out["DataValMax"] <= 0: return - with open(os.path.join(output_dir, quantity_name, filename), 'wb') as rawfile: + with open(os.path.join(output_dir, quantity_name, filename), "wb") as rawfile: for face in faces.values(): # face for each orientation for d in (face * out["ScaleFactor"]).astype(np.uint8): - if ordering == 'F': + if ordering == "F": d = d.T d.tofile(rawfile) @@ -78,11 +86,18 @@ def worker(quantity: str, faces: Dict[int, Union[np.ndarray, Tuple[np.ndarray, n worker_args = list() for i, bndf_quantity in enumerate(obst.quantities): - worker_args.append((bndf_quantity.name, obst.get_global_boundary_data_arrays(bndf_quantity), - obst.vmin(bndf_quantity, 0), obst.vmax(bndf_quantity, 0))) + worker_args.append( + ( + bndf_quantity.name, + obst.get_global_boundary_data_arrays(bndf_quantity), + obst.vmin(bndf_quantity, 0), + obst.vmax(bndf_quantity, 0), + ) + ) # Create all requested directories if they don't exist yet - Path(os.path.join(output_dir, bndf_quantity.name.replace(" ", "_").replace(".", "-"))).mkdir(parents=True, - exist_ok=True) + Path(os.path.join(output_dir, bndf_quantity.name.replace(" ", "_").replace(".", "-"))).mkdir( + parents=True, exist_ok=True + ) with Pool(len(obst.quantities)) as pool: pool.map(lambda args: worker(*args), worker_args) @@ -90,8 +105,9 @@ def worker(quantity: str, faces: Dict[int, Union[np.ndarray, Tuple[np.ndarray, n meta["Quantities"] = list(meta["Quantities"]) meta_file_path = os.path.join(output_dir, obst_filename_base + ".yaml") - with open(meta_file_path, 'w') as meta_file: + with open(meta_file_path, "w") as meta_file: import yaml + yaml.dump(meta, meta_file) return meta_file_path diff --git a/fdsreader/export/sim_exporter.py b/fdsreader/export/sim_exporter.py index 05388c4a..7f4d8367 100644 --- a/fdsreader/export/sim_exporter.py +++ b/fdsreader/export/sim_exporter.py @@ -1,10 +1,12 @@ import os + from typing_extensions import Literal -from . import export_slcf_raw, export_obst_raw, export_smoke_raw + from .. import Simulation +from . import export_obst_raw, export_slcf_raw, export_smoke_raw -def export_sim(sim: Simulation, output_dir: str, ordering: Literal['C', 'F'] = 'C'): +def export_sim(sim: Simulation, output_dir: str, ordering: Literal["C", "F"] = "C"): """Exports the 3d arrays to raw binary files with corresponding .yaml meta files. Warning: This method does not work for large simulations as some internal multiprocess buffers overflow after a few GB of data. Please export the simulation manually using the functions used in this method in separate @@ -22,12 +24,16 @@ def export_sim(sim: Simulation, output_dir: str, ordering: Literal['C', 'F'] = ' meta["Obstructions"].append(os.path.relpath(obst_path, output_dir).replace("\\", "/")) for slc in sim.slices: - slice_path = export_slcf_raw(slc, os.path.join(output_dir, "slices", slc.quantity.name.replace(' ', '_').lower()), ordering) + slice_path = export_slcf_raw( + slc, os.path.join(output_dir, "slices", slc.quantity.name.replace(" ", "_").lower()), ordering + ) if slice_path: meta["Slices"].append(os.path.relpath(slice_path, output_dir).replace("\\", "/")) for smoke in sim.smoke_3d: - volume_path = export_smoke_raw(smoke, os.path.join(output_dir, "smoke", smoke.quantity.name.replace(' ', '_').lower()), ordering) + volume_path = export_smoke_raw( + smoke, os.path.join(output_dir, "smoke", smoke.quantity.name.replace(" ", "_").lower()), ordering + ) if volume_path: meta["Volumes"].append(os.path.relpath(volume_path, output_dir).replace("\\", "/")) @@ -36,8 +42,9 @@ def export_sim(sim: Simulation, output_dir: str, ordering: Literal['C', 'F'] = ' meta["NumVolumes"] = len(meta["Volumes"]) import yaml + meta_file_path = os.path.join(output_dir, sim.chid + "-smv.yaml") - with open(meta_file_path, 'w') as metafile: + with open(meta_file_path, "w") as metafile: yaml.dump(meta, metafile) return meta_file_path diff --git a/fdsreader/export/slcf_exporter.py b/fdsreader/export/slcf_exporter.py index 1751d3f5..ef1ab2ff 100644 --- a/fdsreader/export/slcf_exporter.py +++ b/fdsreader/export/slcf_exporter.py @@ -1,22 +1,31 @@ import os from pathlib import Path + import numpy as np from typing_extensions import Literal + from ..slcf import Slice -def export_slcf_raw(slc: Slice, output_dir: str, ordering: Literal['C', 'F'] = 'C'): +def export_slcf_raw(slc: Slice, output_dir: str, ordering: Literal["C", "F"] = "C"): """Exports the 3d arrays to raw binary files with corresponding .yaml meta files. :param slc: The :class:`Slice` object to export. :param output_dir: The directory in which to save all files. :param ordering: Whether to write the data in C or Fortran ordering. """ + from multiprocess import Manager from pathos.pools import ProcessPool as Pool - from multiprocess import Lock, Manager - slc2d = slc.type == '2D' - meta = {"CellCentered": 1 if slc.cell_centered else 0, "DataValMax": float(slc.vmax), "DataValMin": float(slc.vmin), "ScaleFactor": 255. / (float(slc.vmax) - float(slc.vmin)), - "MeshNum": len(slc.subslices), "Quantity": slc.quantity.name} + + slc2d = slc.type == "2D" + meta = { + "CellCentered": 1 if slc.cell_centered else 0, + "DataValMax": float(slc.vmax), + "DataValMin": float(slc.vmin), + "ScaleFactor": 255.0 / (float(slc.vmax) - float(slc.vmin)), + "MeshNum": len(slc.subslices), + "Quantity": slc.quantity.name, + } filename_base = ("slice" + ("2D-" if slc2d else "3D-") + slc.id.lower()).replace(" ", "_").replace(".", "-") # Create all requested directories if they don't exist yet @@ -33,26 +42,32 @@ def worker(mesh, subslice): data = ((subslice.data - meta["DataValMin"]) * meta["ScaleFactor"]).astype(np.uint8) shape = data.shape if slc2d: - shape = shape[:subslice.orientation] + (1,) + shape[subslice.orientation:] + shape = shape[: subslice.orientation] + (1,) + shape[subslice.orientation :] - with open(os.path.join(output_dir, filename_base + "-data", filename), 'wb') as rawfile: + with open(os.path.join(output_dir, filename_base + "-data", filename), "wb") as rawfile: for d in data: - if ordering == 'F': + if ordering == "F": d = d.T d.tofile(rawfile) - spacing = [slc.times[1] - slc.times[0], - mesh.coordinates['x'][1] - mesh.coordinates['x'][0], - mesh.coordinates['y'][1] - mesh.coordinates['y'][0], - mesh.coordinates['z'][1] - mesh.coordinates['z'][0]] + spacing = [ + slc.times[1] - slc.times[0], + mesh.coordinates["x"][1] - mesh.coordinates["x"][0], + mesh.coordinates["y"][1] - mesh.coordinates["y"][0], + mesh.coordinates["z"][1] - mesh.coordinates["z"][0], + ] with lock: - meta["Meshes"].append({ - "Mesh": mesh_id, - "DataFile": os.path.join(filename_base + "-data", filename), - "MeshPos": f"{subslice.extent['x'][0]:.6} {subslice.extent['y'][0]:.6} {subslice.extent['z'][0]:.6}", - "Spacing": f"{spacing[0]:.6} {spacing[1]:.6} {spacing[2]:.6} {spacing[3]:.6}", - "DimSize": f"{shape[0]} {shape[1]} {shape[2]} {shape[3]}" - }) + meta["Meshes"].append( + { + "Mesh": mesh_id, + "DataFile": os.path.join(filename_base + "-data", filename), + "MeshPos": ( + f"{subslice.extent['x'][0]:.6} {subslice.extent['y'][0]:.6} {subslice.extent['z'][0]:.6}" + ), + "Spacing": f"{spacing[0]:.6} {spacing[1]:.6} {spacing[2]:.6} {spacing[3]:.6}", + "DimSize": f"{shape[0]} {shape[1]} {shape[2]} {shape[3]}", + } + ) with Pool() as pool: pool.map(lambda args: worker(*args), list(slc._subslices.items())) @@ -60,8 +75,9 @@ def worker(mesh, subslice): meta["Meshes"] = list(meta["Meshes"]) meta_file_path = os.path.join(output_dir, filename_base + ".yaml") - with open(meta_file_path, 'w') as meta_file: + with open(meta_file_path, "w") as meta_file: import yaml + yaml.dump(meta, meta_file) return meta_file_path diff --git a/fdsreader/export/smoke3d_exporter.py b/fdsreader/export/smoke3d_exporter.py index 7b46b351..f4128726 100644 --- a/fdsreader/export/smoke3d_exporter.py +++ b/fdsreader/export/smoke3d_exporter.py @@ -1,25 +1,33 @@ import os from pathlib import Path + import numpy as np from typing_extensions import Literal + from ..smoke3d import Smoke3D -def export_smoke_raw(smoke3d: Smoke3D, output_dir: str, ordering: Literal['C', 'F'] = 'C'): +def export_smoke_raw(smoke3d: Smoke3D, output_dir: str, ordering: Literal["C", "F"] = "C"): """Exports the 3d arrays to raw binary files with corresponding .yaml meta files. :param smoke3d: The :class:`Smoke3D` object to export. :param output_dir: The directory in which to save all files. :param ordering: Whether to write the data in C or Fortran ordering. """ + from multiprocess import Manager from pathos.pools import ProcessPool as Pool - from multiprocess import Lock, Manager + filename_base = ("smoke-" + smoke3d.quantity.name.lower()).replace(" ", "_").replace(".", "-") # Create all requested directories if they don't exist yet Path(os.path.join(output_dir, filename_base + "-data")).mkdir(parents=True, exist_ok=True) - meta = {"DataValMax": -100000., "DataValMin": 100000., "ScaleFactor": 1, "MeshNum": len(smoke3d.subsmokes), - "Quantity": smoke3d.quantity.name} + meta = { + "DataValMax": -100000.0, + "DataValMin": 100000.0, + "ScaleFactor": 1, + "MeshNum": len(smoke3d.subsmokes), + "Quantity": smoke3d.quantity.name, + } for subsmoke in smoke3d._subsmokes.values(): meta["DataValMax"] = max(meta["DataValMax"], np.max(subsmoke.data)) @@ -43,24 +51,30 @@ def worker(mesh, subsmoke): data = (subsmoke.data * meta["ScaleFactor"]).astype(np.uint8) - with open(os.path.join(output_dir, filename_base + "-data", filename), 'wb') as rawfile: + with open(os.path.join(output_dir, filename_base + "-data", filename), "wb") as rawfile: for d in data: - if ordering == 'F': + if ordering == "F": d = d.T d.tofile(rawfile) - spacing = [smoke3d.times[1] - smoke3d.times[0], - mesh.coordinates['x'][1] - mesh.coordinates['x'][0], - mesh.coordinates['y'][1] - mesh.coordinates['y'][0], - mesh.coordinates['z'][1] - mesh.coordinates['z'][0]] + spacing = [ + smoke3d.times[1] - smoke3d.times[0], + mesh.coordinates["x"][1] - mesh.coordinates["x"][0], + mesh.coordinates["y"][1] - mesh.coordinates["y"][0], + mesh.coordinates["z"][1] - mesh.coordinates["z"][0], + ] with lock: - meta["Meshes"].append({ - "Mesh": mesh_id, - "DataFile": os.path.join(filename_base + "-data", filename), - "MeshPos": f"{mesh.coordinates['x'][0]:.6} {mesh.coordinates['y'][0]:.6} {mesh.coordinates['z'][0]:.6}", - "Spacing": f"{spacing[0]:.6} {spacing[1]:.6} {spacing[2]:.6} {spacing[3]:.6}", - "DimSize": f"{data.shape[0]} {data.shape[1]} {data.shape[2]} {data.shape[3]}" - }) + meta["Meshes"].append( + { + "Mesh": mesh_id, + "DataFile": os.path.join(filename_base + "-data", filename), + "MeshPos": ( + f"{mesh.coordinates['x'][0]:.6} {mesh.coordinates['y'][0]:.6} {mesh.coordinates['z'][0]:.6}" + ), + "Spacing": f"{spacing[0]:.6} {spacing[1]:.6} {spacing[2]:.6} {spacing[3]:.6}", + "DimSize": f"{data.shape[0]} {data.shape[1]} {data.shape[2]} {data.shape[3]}", + } + ) with Pool() as pool: pool.map(lambda args: worker(*args), list(smoke3d._subsmokes.items())) @@ -68,8 +82,9 @@ def worker(mesh, subsmoke): meta["Meshes"] = list(meta["Meshes"]) meta_file_path = os.path.join(output_dir, filename_base + ".yaml") - with open(meta_file_path, 'w') as meta_file: + with open(meta_file_path, "w") as meta_file: import yaml + yaml.dump(meta, meta_file) return meta_file_path diff --git a/fdsreader/fds_classes/__init__.py b/fdsreader/fds_classes/__init__.py index d254bd59..442b6a1f 100644 --- a/fdsreader/fds_classes/__init__.py +++ b/fdsreader/fds_classes/__init__.py @@ -1,7 +1,4 @@ -from .mesh import Mesh - -from .mesh_collection import MeshCollection - -from .surface import Surface - -from .ventilation import Ventilation +from .mesh import Mesh as Mesh +from .mesh_collection import MeshCollection as MeshCollection +from .surface import Surface as Surface +from .ventilation import Ventilation as Ventilation diff --git a/fdsreader/fds_classes/mesh.py b/fdsreader/fds_classes/mesh.py index fffda51a..41e897a9 100644 --- a/fdsreader/fds_classes/mesh.py +++ b/fdsreader/fds_classes/mesh.py @@ -1,11 +1,12 @@ -from typing import Dict, Tuple, Sequence, List, Union -from typing_extensions import Literal -import numpy as np import math +from typing import Dict, List, Sequence, Tuple, Union + +import numpy as np +from typing_extensions import Literal from fdsreader import settings -from fdsreader.utils import Dimension, Extent, Quantity from fdsreader.bndf import Boundary, Patch +from fdsreader.utils import Dimension, Extent, Quantity class Mesh: @@ -19,8 +20,12 @@ class Mesh: :var id: Mesh id/short_name assigned to this mesh. """ - def __init__(self, coordinates: Dict[Literal['x', 'y', 'z'], np.ndarray], - extents: Dict[Literal['x', 'y', 'z'], Tuple[float, float]], mesh_id: str): + def __init__( + self, + coordinates: Dict[Literal["x", "y", "z"], np.ndarray], + extents: Dict[Literal["x", "y", "z"], Tuple[float, float]], + mesh_id: str, + ): """ :param coordinates: Coordinate values of the three axes. :param extents: Extent of the mesh in each dimension. @@ -29,13 +34,15 @@ def __init__(self, coordinates: Dict[Literal['x', 'y', 'z'], np.ndarray], self.id = mesh_id self.coordinates = coordinates self.dimension = Dimension( - coordinates['x'].size if coordinates['x'].size > 0 else 1, - coordinates['y'].size if coordinates['y'].size > 0 else 1, - coordinates['z'].size if coordinates['z'].size > 0 else 1) + coordinates["x"].size if coordinates["x"].size > 0 else 1, + coordinates["y"].size if coordinates["y"].size > 0 else 1, + coordinates["z"].size if coordinates["z"].size > 0 else 1, + ) self.n_size = self.dimension.size() - self.extent = Extent(extents['x'][0], extents['x'][1], extents['y'][0], extents['y'][1], - extents['z'][0], extents['z'][1]) + self.extent = Extent( + extents["x"][0], extents["x"][1], extents["y"][0], extents["y"][1], extents["z"][0], extents["z"][1] + ) self.obstructions = list() @@ -54,14 +61,14 @@ def get_obstruction_mask(self, times: Sequence[float], cell_centered=False) -> n for obst in self.obstructions: subobst = obst[self] - x1, x2 = subobst.bound_indices['x'] - y1, y2 = subobst.bound_indices['y'] - z1, z2 = subobst.bound_indices['z'] + x1, x2 = subobst.bound_indices["x"] + y1, y2 = subobst.bound_indices["y"] + z1, z2 = subobst.bound_indices["z"] t_idx = 0 for t in subobst.get_visible_times(times): while not np.isclose(t, times[t_idx]): t_idx += 1 - mask[t_idx, x1:max(x2 + c, x1 + 1), y1:max(y2 + c, y1 + 1), z1:max(z2 + c, z1 + 1)] = False + mask[t_idx, x1 : max(x2 + c, x1 + 1), y1 : max(y2 + c, y1 + 1), z1 : max(z2 + c, z1 + 1)] = False return mask def get_obstruction_mask_slice(self, subslice): @@ -82,9 +89,12 @@ def get_obstruction_mask_slice(self, subslice): return self.get_obstruction_mask(subslice.times, cell_centered=cell_centered)[mask_indices] - def coordinate_to_index(self, coordinate: Tuple[float, ...], - dimension: Tuple[Literal[1, 2, 3, 'x', 'y', 'z'], ...] = ('x', 'y', 'z'), - cell_centered=False) -> Tuple[int, ...]: + def coordinate_to_index( + self, + coordinate: Tuple[float, ...], + dimension: Tuple[Literal[1, 2, 3, "x", "y", "z"], ...] = ("x", "y", "z"), + cell_centered=False, + ) -> Tuple[int, ...]: """Finds the nearest point in the mesh's grid and returns its indices. :param coordinate: Tuple of 3 floats. If the dimension parameter is supplied, up to 2 @@ -93,7 +103,7 @@ def coordinate_to_index(self, coordinate: Tuple[float, ...], :param cell_centered: Instead of finding the nearest point on the mesh, find the center of the nearest cell. """ # Convert possible integer input to chars - dimension = tuple(('x', 'y', 'z')[dim - 1] if type(dim) == int else dim for dim in dimension) + dimension = tuple(("x", "y", "z")[dim - 1] if isinstance(dim, int) else dim for dim in dimension) ret = list() for i, dim in enumerate(dimension): @@ -102,16 +112,18 @@ def coordinate_to_index(self, coordinate: Tuple[float, ...], if cell_centered: coords = coords[:-1] + (coords[1] - coords[0]) / 2 idx = np.searchsorted(coords, co, side="left") - if co > 0 and (idx == len(coords) or math.fabs(co - coords[idx - 1]) < math.fabs( - co - coords[idx])): + if co > 0 and (idx == len(coords) or math.fabs(co - coords[idx - 1]) < math.fabs(co - coords[idx])): ret.append(idx - 1) else: ret.append(idx) return tuple(ret) - def get_nearest_coordinate(self, coordinate: Tuple[float, ...], - dimension: Tuple[Literal[1, 2, 3, 'x', 'y', 'z'], ...] = ('x', 'y', 'z'), - cell_centered=False) -> Tuple[float, ...]: + def get_nearest_coordinate( + self, + coordinate: Tuple[float, ...], + dimension: Tuple[Literal[1, 2, 3, "x", "y", "z"], ...] = ("x", "y", "z"), + cell_centered=False, + ) -> Tuple[float, ...]: """Finds the nearest point in the mesh's grid. :param coordinate: Tuple of 3 floats. If the dimension parameter is supplied, up to 2 @@ -128,12 +140,22 @@ def get_nearest_coordinate(self, coordinate: Tuple[float, ...], ret.append(coords[indices[i]]) return tuple(ret) - def _add_patches(self, bid: int, cell_centered: bool, quantity: str, short_name: str, unit: str, - patches: List[Patch], times: np.ndarray, lower_bounds: np.ndarray, - upper_bounds: np.ndarray): + def _add_patches( + self, + bid: int, + cell_centered: bool, + quantity: str, + short_name: str, + unit: str, + patches: List[Patch], + times: np.ndarray, + lower_bounds: np.ndarray, + upper_bounds: np.ndarray, + ): if bid not in self._boundary_data: - self._boundary_data[bid] = Boundary(Quantity(quantity, short_name, unit), cell_centered, times, - patches, lower_bounds, upper_bounds) + self._boundary_data[bid] = Boundary( + Quantity(quantity, short_name, unit), cell_centered, times, patches, lower_bounds, upper_bounds + ) # Add reference to parent boundary class in patches for patch in patches: patch._boundary_parent = self._boundary_data[bid] @@ -142,19 +164,22 @@ def _add_patches(self, bid: int, cell_centered: bool, quantity: str, short_name: _ = self._boundary_data[bid].data def get_boundary_data(self, quantity: Union[str, Quantity]): - if type(quantity) == Quantity: + if isinstance(quantity, Quantity): quantity = quantity.name - return next(b for b in self._boundary_data.values() if - b.quantity.name.lower() == quantity.lower() or b.quantity.short_name.lower() == quantity.lower()) + return next( + b + for b in self._boundary_data.values() + if b.quantity.name.lower() == quantity.lower() or b.quantity.short_name.lower() == quantity.lower() + ) - def __getitem__(self, dimension: Literal[0, 1, 2, 'x', 'y', 'z']) -> np.ndarray: + def __getitem__(self, dimension: Literal[0, 1, 2, "x", "y", "z"]) -> np.ndarray: """Get all values in given dimension. :param dimension: The dimension in which to return all grid values (0=x, 1=y, 2=z). """ # Convert possible integer input to chars - if type(dimension) == int: - dimension = ('x', 'y', 'z')[dimension] + if isinstance(dimension, int): + dimension = ("x", "y", "z")[dimension] return self.coordinates[dimension] def __eq__(self, other): diff --git a/fdsreader/fds_classes/mesh_collection.py b/fdsreader/fds_classes/mesh_collection.py index a58d684a..cc3c40f6 100644 --- a/fdsreader/fds_classes/mesh_collection.py +++ b/fdsreader/fds_classes/mesh_collection.py @@ -6,16 +6,15 @@ class MeshCollection(FDSDataCollection): """Collection of :class:`Obstruction` objects. Offers extensive functionality for filtering and - using obstructions as well as dependent such as :class:`Boundary`. + using obstructions as well as dependent such as :class:`Boundary`. """ def __init__(self, *meshes: Iterable[Mesh]): super().__init__(*meshes) def get_by_id(self, mesh_id: str): - """Get the mesh with corresponding id if it exists. - """ + """Get the mesh with corresponding id if it exists.""" return next((mesh for mesh in self if mesh.id == mesh_id), None) def __repr__(self): - return "MeshCollection(" + super(MeshCollection, self).__repr__() + ")" + return "MeshCollection(" + super().__repr__() + ")" diff --git a/fdsreader/fds_classes/surface.py b/fdsreader/fds_classes/surface.py index 154a2022..80131151 100644 --- a/fdsreader/fds_classes/surface.py +++ b/fdsreader/fds_classes/surface.py @@ -1,4 +1,4 @@ -from typing import Optional, Tuple +from typing import Tuple class Surface: @@ -15,9 +15,18 @@ class Surface: :ivar transparency: Transparency of the color (alpha channel). """ - def __init__(self, name: str, tmpm: float, material_emissivity: float, surface_type: int, - texture_width: float, texture_height: float, texture_map: Optional[str], - rgb: Tuple[float, float, float], transparency: float): + def __init__( + self, + name: str, + tmpm: float, + material_emissivity: float, + surface_type: int, + texture_width: float, + texture_height: float, + texture_map: str | None, + rgb: Tuple[float, float, float], + transparency: float, + ): self.name = name self.tmpm = tmpm self.material_emissivity = material_emissivity diff --git a/fdsreader/fds_classes/ventilation.py b/fdsreader/fds_classes/ventilation.py index 586b8bba..324fe12e 100644 --- a/fdsreader/fds_classes/ventilation.py +++ b/fdsreader/fds_classes/ventilation.py @@ -1,6 +1,6 @@ -from typing import Union, Tuple, Dict +from typing import Dict, Tuple, Union -from fdsreader.fds_classes import Surface, Mesh +from fdsreader.fds_classes import Mesh, Surface from fdsreader.utils import Extent @@ -44,12 +44,17 @@ class Ventilation: :ivar radius: Radius of the ventilation circle. """ - def __init__(self, surface: Surface, bound_indices: Tuple[int, int, int, int, int, int], - color_index: int, draw_type: int, - rgba: Union[Tuple[()], Tuple[float, float, float, float]] = (), - texture_origin: Union[Tuple[()], Tuple[float, float, float]] = (), - circular_vent_origin: Union[Tuple[()], Tuple[float, float, float]] = (), - radius: float = -1): + def __init__( + self, + surface: Surface, + bound_indices: Tuple[int, int, int, int, int, int], + color_index: int, + draw_type: int, + rgba: Union[Tuple[()], Tuple[float, float, float, float]] = (), + texture_origin: Union[Tuple[()], Tuple[float, float, float]] = (), + circular_vent_origin: Union[Tuple[()], Tuple[float, float, float]] = (), + radius: float = -1, + ): self.surface = surface self.bound_indices = bound_indices self.color_index = color_index @@ -72,4 +77,4 @@ def _add_subventilation(self, mesh: Mesh, extent: Extent): self._subventilations[mesh.id] = SubVentilation(mesh, extent) def __repr__(self, *args, **kwargs): - return f"Ventilation()" + return "Ventilation()" diff --git a/fdsreader/geom/__init__.py b/fdsreader/geom/__init__.py index e4842744..9714fe51 100644 --- a/fdsreader/geom/__init__.py +++ b/fdsreader/geom/__init__.py @@ -1,3 +1,3 @@ -from .geometry import Geometry, GeomBoundary - -from .geometry_collection import GeometryCollection +from .geometry import GeomBoundary as GeomBoundary +from .geometry import Geometry as Geometry +from .geometry_collection import GeometryCollection as GeometryCollection diff --git a/fdsreader/geom/geometry.py b/fdsreader/geom/geometry.py index 3db4924a..3d789495 100644 --- a/fdsreader/geom/geometry.py +++ b/fdsreader/geom/geometry.py @@ -1,11 +1,10 @@ -import threading -from typing import Tuple, Dict, Iterable +from typing import Dict, Iterable, Tuple import numpy as np +import fdsreader.utils.fortran_data as fdtype from fdsreader.fds_classes import Surface from fdsreader.utils import Quantity -import fdsreader.utils.fortran_data as fdtype class GeomBoundary: @@ -29,8 +28,9 @@ def __init__(self, quantity: Quantity, times: np.ndarray, n_t: int): self.file_paths_be: Dict[int, str] = dict() self.file_paths_gbf: Dict[int, str] = dict() - def _add_data(self, mesh: int, file_path_be: str, file_path_gbf: str, lower_bounds: np.ndarray, - upper_bounds: np.ndarray): + def _add_data( + self, mesh: int, file_path_be: str, file_path_gbf: str, lower_bounds: np.ndarray, upper_bounds: np.ndarray + ): self.file_paths_be[mesh] = file_path_be self.file_paths_gbf[mesh] = file_path_gbf @@ -46,35 +46,32 @@ def _load_data(self): file_path_be = self.file_paths_be[mesh] file_path_gbf = self.file_paths_gbf[mesh] # Load .gbf - with open(file_path_gbf, 'rb') as infile: - offset = fdtype.INT.itemsize * 2 + fdtype.new( - (('i', 3),)).itemsize + fdtype.FLOAT.itemsize + with open(file_path_gbf, "rb") as infile: + offset = fdtype.INT.itemsize * 2 + fdtype.new((("i", 3),)).itemsize + fdtype.FLOAT.itemsize infile.seek(offset) - dtype_meta = fdtype.new((('i', 3),)) + dtype_meta = fdtype.new((("i", 3),)) n_vertices, n_faces, _ = np.fromfile(infile, dtype_meta, 1)[0][1] - dtype_vertices = fdtype.new((('f', 3 * n_vertices),)) - vertices = np.fromfile(infile, dtype_vertices, 1)[0][1].reshape( - (n_vertices, 3)).astype(float) + dtype_vertices = fdtype.new((("f", 3 * n_vertices),)) + vertices = np.fromfile(infile, dtype_vertices, 1)[0][1].reshape((n_vertices, 3)).astype(float) - dtype_faces = fdtype.new((('i', 3 * n_faces),)) - faces = fdtype.read(infile, dtype_faces, 1)[0][0].reshape((n_faces, 3)).astype( - int) - 1 + dtype_faces = fdtype.new((("i", 3 * n_faces),)) + faces = fdtype.read(infile, dtype_faces, 1)[0][0].reshape((n_faces, 3)).astype(int) - 1 # Load .be - dtype_faces = fdtype.new((('f', n_faces),)) - dtype_meta = fdtype.new((('i', 4),)) + dtype_faces = fdtype.new((("f", n_faces),)) + dtype_meta = fdtype.new((("i", 4),)) data = np.empty((self.n_t, n_faces), dtype=np.float32) - with open(file_path_be, 'rb') as infile: + with open(file_path_be, "rb") as infile: offset = fdtype.INT.itemsize * 2 infile.seek(offset) for t in range(self.n_t): # Skip time value infile.seek(fdtype.FLOAT.itemsize, 1) - assert fdtype.read(infile, dtype_meta, 1)[0][0][3] == n_faces, "Something went wrong, please" \ - " submit an issue on Github" \ - " attaching your fds case!" + assert fdtype.read(infile, dtype_meta, 1)[0][0][3] == n_faces, ( + "Something went wrong, please submit an issue on Github attaching your fds case!" + ) if n_faces > 0: data[t, :] = np.fromfile(infile, dtype_faces, 1)[0][1] @@ -140,7 +137,8 @@ def _load_data(self): # times.append(self.n_t) # threads = list() # for i in range(len(times)-1): - # threads.append(BEReader(file_path_be, offset+stride*times[i], times[i+1] - times[i], data, times[i], n_faces)) + # threads.append(BEReader(file_path_be, offset+stride*times[i], times[i+1] - times[i], + # data, times[i], n_faces)) # # for thread in threads: # thread.start() @@ -154,8 +152,7 @@ def _load_data(self): @property def vertices(self) -> Iterable: - """Returns a global array of the vertices from all meshes. - """ + """Returns a global array of the vertices from all meshes.""" if not hasattr(self, "_vertices"): self._load_data() @@ -165,15 +162,14 @@ def vertices(self) -> Iterable: for v in self._vertices.values(): size = v.shape[0] - ret[counter:counter + size, :] = v + ret[counter : counter + size, :] = v counter += size return ret @property def faces(self) -> np.ndarray: - """Returns a global array of the faces from all meshes. - """ + """Returns a global array of the faces from all meshes.""" if not hasattr(self, "_faces"): self._load_data() @@ -184,7 +180,7 @@ def faces(self) -> np.ndarray: for m, f in self._faces.items(): size = f.shape[0] - ret[counter:counter + size, :] = f + verts_counter + ret[counter : counter + size, :] = f + verts_counter counter += size verts_counter += self._vertices[m].shape[0] @@ -192,8 +188,7 @@ def faces(self) -> np.ndarray: @property def data(self) -> np.ndarray: - """Returns a global array of the loaded data for the quantity with data from all meshes. - """ + """Returns a global array of the loaded data for the quantity with data from all meshes.""" if not hasattr(self, "_data"): self._load_data() @@ -203,7 +198,7 @@ def data(self) -> np.ndarray: counter = 0 for d in self._data.values(): size = d[t].shape[0] - ret[t, counter:counter + size] = d[t] + ret[t, counter : counter + size] = d[t] counter += size return ret @@ -229,8 +224,7 @@ def data(self) -> np.ndarray: @property def vmin(self) -> float: - """Minimum value of all faces at any time. - """ + """Minimum value of all faces at any time.""" curr_min = min(np.min(b) for b in self.lower_bounds.values()) if curr_min == 0.0: return min(min(np.min(p.data) for p in ps) for ps in self._faces.values()) @@ -238,8 +232,7 @@ def vmin(self) -> float: @property def vmax(self) -> float: - """Maximum value of all faces at any time. - """ + """Maximum value of all faces at any time.""" curr_max = max(np.max(b) for b in self.upper_bounds.values()) if curr_max == np.float32(-1e33): return max(max(np.max(p.data) for p in ps) for ps in self._faces.values()) @@ -260,9 +253,15 @@ class Geometry: :ivar surface: Surface object used for the geometry. """ - def __init__(self, file_path: str, texture_mapping: str, - texture_origin: Tuple[float, float, float], - is_terrain: bool, rgb: Tuple[int, int, int], surface: Surface = None): + def __init__( + self, + file_path: str, + texture_mapping: str, + texture_origin: Tuple[float, float, float], + is_terrain: bool, + rgb: Tuple[int, int, int], + surface: Surface = None, + ): self.file_path = file_path self.texture_map = texture_mapping self.texture_origin = texture_origin diff --git a/fdsreader/geom/geometry_collection.py b/fdsreader/geom/geometry_collection.py index 6f71de4b..f25d652f 100644 --- a/fdsreader/geom/geometry_collection.py +++ b/fdsreader/geom/geometry_collection.py @@ -1,4 +1,4 @@ -from typing import Iterable, Union, List +from typing import Iterable, List, Union from fdsreader import settings from fdsreader.geom import GeomBoundary @@ -7,7 +7,7 @@ class GeometryCollection(FDSDataCollection): """Collection of :class:`GeomBoundary` objects. Offers extensive functionality for filtering and - using geometry data. + using geometry data. """ def __init__(self, *geom_boundaries: Iterable[GeomBoundary]): @@ -22,12 +22,14 @@ def quantities(self) -> List[Quantity]: return list({geom.name for geom in self}) def filter_by_quantity(self, quantity: Union[str, Quantity]): - """Filters all GeomBoundaries by a specific quantity. - """ - if type(quantity) == Quantity: + """Filters all GeomBoundaries by a specific quantity.""" + if isinstance(quantity, Quantity): quantity = quantity.name - return GeometryCollection(x for x in self if - x.quantity.name.lower() == quantity.lower() or x.quantity.short_name.lower() == quantity.lower()) + return GeometryCollection( + x + for x in self + if x.quantity.name.lower() == quantity.lower() or x.quantity.short_name.lower() == quantity.lower() + ) def __repr__(self): - return "GeometryCollection(" + super(GeometryCollection, self).__repr__() + ")" + return "GeometryCollection(" + super().__repr__() + ")" diff --git a/fdsreader/isof/__init__.py b/fdsreader/isof/__init__.py index 219a9c1a..2b876109 100644 --- a/fdsreader/isof/__init__.py +++ b/fdsreader/isof/__init__.py @@ -1,3 +1,2 @@ -from .isosurface import Isosurface - -from .isosurface_collection import IsosurfaceCollection +from .isosurface import Isosurface as Isosurface +from .isosurface_collection import IsosurfaceCollection as IsosurfaceCollection diff --git a/fdsreader/isof/isosurface.py b/fdsreader/isof/isosurface.py index 811b6006..10118f27 100644 --- a/fdsreader/isof/isosurface.py +++ b/fdsreader/isof/isosurface.py @@ -1,14 +1,14 @@ +import math import operator from functools import reduce -from typing import BinaryIO, Dict, Union, List, Tuple, Optional +from typing import BinaryIO, Dict, List, Tuple, Union import numpy as np -import math +import fdsreader.utils.fortran_data as fdtype +from fdsreader import settings from fdsreader.fds_classes import Mesh from fdsreader.utils import Quantity -from fdsreader import settings -import fdsreader.utils.fortran_data as fdtype class SubSurface: @@ -29,12 +29,11 @@ def __init__(self, mesh: Mesh, iso_filepath: str, times: List, viso_filepath: st if viso_filepath != "": self.v_file_path = viso_filepath - with open(self.file_path, 'rb') as infile: + with open(self.file_path, "rb") as infile: nlevels = fdtype.read(infile, fdtype.INT, 3)[2][0][0] - dtype_header_levels = fdtype.new((('f', nlevels),)) - dtype_header_zeros = fdtype.combine(fdtype.INT, fdtype.new((('i', 2),))) - self._offset = fdtype.INT.itemsize * 3 + dtype_header_levels.itemsize + \ - dtype_header_zeros.itemsize + dtype_header_levels = fdtype.new((("f", nlevels),)) + dtype_header_zeros = fdtype.combine(fdtype.INT, fdtype.new((("i", 2),))) + self._offset = fdtype.INT.itemsize * 3 + dtype_header_levels.itemsize + dtype_header_zeros.itemsize self.times = times self.n_vertices = list() @@ -45,24 +44,22 @@ def __init__(self, mesh: Mesh, iso_filepath: str, times: List, viso_filepath: st if self.has_color_data: if not settings.LAZY_LOAD: - with open(self.v_file_path, 'rb') as infile: + with open(self.v_file_path, "rb") as infile: self._load_vdata(infile) @property def vertices(self): - """Property to lazy load all vertices for all triangles of any level. - """ + """Property to lazy load all vertices for all triangles of any level.""" if not hasattr(self, "_vertices"): - with open(self.file_path, 'rb') as infile: + with open(self.file_path, "rb") as infile: self._load_data(infile) return self._vertices @property def triangles(self): - """Property to lazy load all triangles of any level. - """ + """Property to lazy load all triangles of any level.""" if not hasattr(self, "_triangles"): - with open(self.file_path, 'rb') as infile: + with open(self.file_path, "rb") as infile: self._load_data(infile) return self._triangles @@ -72,35 +69,34 @@ def surfaces(self): The list has the size n_triangles, while the indices correspond to indices of the triangles. """ if not hasattr(self, "_surfaces"): - with open(self.file_path, 'rb') as infile: + with open(self.file_path, "rb") as infile: self._load_data(infile) return self._surfaces @property def has_color_data(self): - """Defines whether there is color data for this subsurface or not. - """ + """Defines whether there is color data for this subsurface or not.""" return hasattr(self, "v_file_path") @property def colors(self): - """Property to lazy load the color data that might be associated with the isosurfaces. - """ + """Property to lazy load the color data that might be associated with the isosurfaces.""" if self.has_color_data: if not hasattr(self, "_colors"): - with open(self.v_file_path, 'rb') as infile: + with open(self.v_file_path, "rb") as infile: self._load_vdata(infile) return self._colors else: - raise UserWarning("The isosurface does not have any associated color-data. Use the" - " attribute 'has_color_data' to check if an isosurface has associated" - " color-data.") + raise UserWarning( + "The isosurface does not have any associated color-data. Use the" + " attribute 'has_color_data' to check if an isosurface has associated" + " color-data." + ) def _load_data(self, infile: BinaryIO): - """Loads data for the subsurface which is given in an iso file. - """ - dtype_time = fdtype.new((('f', 1), ('i', 1))) - dtype_dims = fdtype.new((('i', 2),)) + """Loads data for the subsurface which is given in an iso file.""" + dtype_time = fdtype.new((("f", 1), ("i", 1))) + dtype_dims = fdtype.new((("i", 2),)) self._vertices = list() self._triangles = list() @@ -115,16 +111,16 @@ def _load_data(self, infile: BinaryIO): n_vertices = dims_data[0][0][0] n_triangles = dims_data[0][0][1] if n_vertices > 0: - dtype_vertices = fdtype.new((('f', 3 * n_vertices),)) - dtype_triangles = fdtype.new((('i', 3 * n_triangles),)) - dtype_surfaces = fdtype.new((('i', n_triangles),)) + dtype_vertices = fdtype.new((("f", 3 * n_vertices),)) + dtype_triangles = fdtype.new((("i", 3 * n_triangles),)) + dtype_surfaces = fdtype.new((("i", n_triangles),)) self._vertices.append( - fdtype.read(infile, dtype_vertices, 1)[0][0].reshape((n_vertices, 3)).astype( - float)) + fdtype.read(infile, dtype_vertices, 1)[0][0].reshape((n_vertices, 3)).astype(float) + ) self._triangles.append( - fdtype.read(infile, dtype_triangles, 1)[0][0].reshape((n_triangles, 3)).astype( - int) - 1) + fdtype.read(infile, dtype_triangles, 1)[0][0].reshape((n_triangles, 3)).astype(int) - 1 + ) self._surfaces.append(fdtype.read(infile, dtype_surfaces, 1)[0][0].astype(int) - 1) self.n_vertices.append(n_vertices) self.n_triangles.append(n_triangles) @@ -140,10 +136,9 @@ def _load_data(self, infile: BinaryIO): self.n_t = len(self.times) def _load_vdata(self, infile: BinaryIO): - """Loads all color data for all isosurfaces in a given viso file. - """ + """Loads all color data for all isosurfaces in a given viso file.""" self._colors = np.empty((self.n_t,), dtype=object) - dtype_nverts = fdtype.new((('i', 4),)) + dtype_nverts = fdtype.new((("i", 4),)) infile.seek(fdtype.INT.itemsize * 2) t = fdtype.read(infile, fdtype.FLOAT, 1) @@ -151,12 +146,11 @@ def _load_vdata(self, infile: BinaryIO): time_index = self.times.index(t[0][0][0]) n_vertices = fdtype.read(infile, dtype_nverts, 1)[0][0][2] if n_vertices > 0: - self._colors[time_index] = fdtype.read(infile, fdtype.new((('f', n_vertices),)), 1) + self._colors[time_index] = fdtype.read(infile, fdtype.new((("f", n_vertices),)), 1) t = fdtype.read(infile, fdtype.FLOAT, 1) def clear_cache(self): - """Remove all data from the internal cache that has been loaded so far to free memory. - """ + """Remove all data from the internal cache that has been loaded so far to free memory.""" if hasattr(self, "_vertices"): del self._vertices if hasattr(self, "_triangles"): @@ -181,9 +175,18 @@ class Isosurface: :ivar levels: All isosurface levels. """ - def __init__(self, isosurface_id: int, double_quantity: bool, quantity: str, - short_name: str, unit: str, levels: List[float], v_quantity: str = "", - v_short_name: str = "", v_unit: str = ""): + def __init__( + self, + isosurface_id: int, + double_quantity: bool, + quantity: str, + short_name: str, + unit: str, + levels: List[float], + v_quantity: str = "", + v_short_name: str = "", + v_unit: str = "", + ): self.id = isosurface_id self.quantity = Quantity(quantity, short_name, unit) self._double_quantity = double_quantity @@ -196,8 +199,7 @@ def __init__(self, isosurface_id: int, double_quantity: bool, quantity: str, if self._double_quantity: self.v_quantity = Quantity(v_quantity, v_short_name, v_unit) - def _add_subsurface(self, mesh: Mesh, iso_file_path: str, - viso_file_path: str = "") -> SubSurface: + def _add_subsurface(self, mesh: Mesh, iso_file_path: str, viso_file_path: str = "") -> SubSurface: if viso_file_path != "": subsurface = SubSurface(mesh, iso_file_path, self._times, viso_file_path) else: @@ -207,37 +209,32 @@ def _add_subsurface(self, mesh: Mesh, iso_file_path: str, return subsurface def __getitem__(self, mesh: str) -> SubSurface: - """Returns the :class:`SubSurface` that contains data for the given mesh id. - """ + """Returns the :class:`SubSurface` that contains data for the given mesh id.""" return self._subsurfaces[mesh] @property def vertices(self) -> Dict[str, List[np.ndarray]]: - """Gets all vertices per mesh. - """ + """Gets all vertices per mesh.""" return {mesh: subsurface.vertices for mesh, subsurface in self._subsurfaces.items()} @property def triangles(self) -> Dict[str, List[np.ndarray]]: - """Gets all triangles per mesh. - """ + """Gets all triangles per mesh.""" return {mesh: subsurface.triangles for mesh, subsurface in self._subsurfaces.items()} @property def surfaces(self) -> Dict[str, List[np.ndarray]]: - """Gets all surfaces per mesh. - """ + """Gets all surfaces per mesh.""" return {mesh: subsurface.surfaces for mesh, subsurface in self._subsurfaces.items()} - def to_global(self, time: Union[int, float]) -> Tuple[ - np.ndarray, List[np.ndarray], Optional[np.ndarray]]: + def to_global(self, time: Union[int, float]) -> Tuple[np.ndarray, List[np.ndarray], np.ndarray | None]: """Creates an array containing all global vertices and a list containing numpy arrays with triangles for each surface level. :param time: Either the index of the timestep or an actual time value. In the latter case data for the nearest matching timestep will be used. """ - if type(time) == float: + if isinstance(time, float): time = self.get_nearest_timestep(time) if time > len(self.times): @@ -245,8 +242,7 @@ def to_global(self, time: Union[int, float]) -> Tuple[ if time < 0: time = 0 - n_vertices = sum( - x[time].shape[0] if len(x[time].shape) > 0 else 0 for x in self.vertices.values()) + n_vertices = sum(x[time].shape[0] if len(x[time].shape) > 0 else 0 for x in self.vertices.values()) vertices = np.empty((n_vertices, 3)) colors = np.empty((n_vertices,)) if self.has_color_data else None @@ -257,11 +253,10 @@ def to_global(self, time: Union[int, float]) -> Tuple[ verts_counter += verts.shape[0] vertices[tmp:verts_counter] = verts if self.has_color_data: - colors[tmp: verts_counter] = self[mesh].colors[time] + colors[tmp:verts_counter] = self[mesh].colors[time] triangles = list() - num_levels = max( - max(np.max(t) if t.shape[0] != 0 else 0 for t in s) for s in self.surfaces.values()) + 1 + num_levels = max(max(np.max(t) if t.shape[0] != 0 else 0 for t in s) for s in self.surfaces.values()) + 1 for surf in range(num_levels): n_triangles = sum(np.count_nonzero(s[time] == surf) for s in self.surfaces.values()) triangles.append(np.empty((n_triangles, 3), dtype=int)) @@ -278,16 +273,17 @@ def to_global(self, time: Union[int, float]) -> Tuple[ return vertices, triangles, colors def get_pyvista_mesh(self, vertices: np.ndarray, triangles: np.ndarray): - """Creates a PyVista mesh from the data. - """ + """Creates a PyVista mesh from the data.""" try: from pyvista import PolyData triangles = np.hstack(np.append(np.full((triangles.shape[0], 1), 3), triangles, axis=1)) return PolyData(vertices, triangles) except ImportError: - raise ImportError("The 'get_pyvista_mesh' method requires the PyVista python-package to" - " be installed. Consider installing it via 'pip install pyvista'.") + raise ImportError( + "The 'get_pyvista_mesh' method requires the PyVista python-package to" + " be installed. Consider installing it via 'pip install pyvista'." + ) def join_pyvista_meshes(self, meshes: List): """Combines multiple PyVista meshes. @@ -297,13 +293,16 @@ def join_pyvista_meshes(self, meshes: List): try: from pyvista import PolyData - assert all(type(mesh) == PolyData for mesh in - meshes), "Argument 'meshes' has to be a sequence of type pyvista.Polydata" + assert all(isinstance(mesh, PolyData) for mesh in meshes), ( + "Argument 'meshes' has to be a sequence of type pyvista.Polydata" + ) return reduce(operator.add, meshes) except ImportError: - raise ImportError("The 'join_pyvista_meshes' method requires the PyVista python-package" - " to be installed. Consider installing it via 'pip install pyvista'.") + raise ImportError( + "The 'join_pyvista_meshes' method requires the PyVista python-package" + " to be installed. Consider installing it via 'pip install pyvista'." + ) def export(self, file_path: str, mesh): """Export the isosurface for a single timestep into one of many formats. @@ -316,52 +315,53 @@ def export(self, file_path: str, mesh): try: from pyvista import PolyData - assert type(mesh) == PolyData, "Argument 'mesh' has to be of type pyvista.Polydata" + assert isinstance(mesh, PolyData), "Argument 'mesh' has to be of type pyvista.Polydata" if "vtk" in file_path or "vtp" in file_path: return mesh.save(file_path) else: try: - import meshio + import meshio # noqa: F401 return mesh.save_meshio(file_path) except ImportError: - raise ImportError("The 'export' method requires the Meshio python-package to be" - " installed. " - "Consider installing it via 'pip install meshio'.") + raise ImportError( + "The 'export' method requires the Meshio python-package to be" + " installed. " + "Consider installing it via 'pip install meshio'." + ) except ImportError: - raise ImportError("The 'export' method requires the PyVista python-package to be" - " installed. Consider installing it via 'pip install pyvista'.") + raise ImportError( + "The 'export' method requires the PyVista python-package to be" + " installed. Consider installing it via 'pip install pyvista'." + ) def get_nearest_timestep(self, time: float) -> int: - """Calculates the nearest timestep for which data has been output for this isosurface. - """ + """Calculates the nearest timestep for which data has been output for this isosurface.""" idx = np.searchsorted(self.times, time, side="left") - if time > 0 and (idx == len(self.times) or math.fabs( - time - self.times[idx - 1]) < math.fabs(time - self.times[idx])): + if time > 0 and ( + idx == len(self.times) or math.fabs(time - self.times[idx - 1]) < math.fabs(time - self.times[idx]) + ): return idx - 1 else: return idx @property def has_color_data(self) -> bool: - """Defines whether there is color data for this isosurface or not. - """ + """Defines whether there is color data for this isosurface or not.""" return self._double_quantity @property def times(self) -> List[float]: - """List containing all times for which data has been recorded. - """ + """List containing all times for which data has been recorded.""" if len(self._times) == 0: # Implicitly load the data for one subsurface and read times _ = next(iter(self._subsurfaces.values())).vertices return self._times def clear_cache(self): - """Remove all data from the internal cache that has been loaded so far to free memory. - """ + """Remove all data from the internal cache that has been loaded so far to free memory.""" for subsurface in self._subsurfaces.values(): subsurface.clear_cache() diff --git a/fdsreader/isof/isosurface_collection.py b/fdsreader/isof/isosurface_collection.py index 98238722..fb5f8473 100644 --- a/fdsreader/isof/isosurface_collection.py +++ b/fdsreader/isof/isosurface_collection.py @@ -1,4 +1,4 @@ -from typing import Iterable, Union, List +from typing import Iterable, List, Union from fdsreader.isof import Isosurface from fdsreader.utils.data import FDSDataCollection, Quantity @@ -6,7 +6,7 @@ class IsosurfaceCollection(FDSDataCollection): """Collection of :class:`Isosurface` objects. Offers extensive functionality for filtering and - using isosurfaces as well as its subclasses such as :class:`SubSurface`. + using isosurfaces as well as its subclasses such as :class:`SubSurface`. """ def __init__(self, *isosurfaces: Iterable[Isosurface]): @@ -17,12 +17,14 @@ def quantities(self) -> List[Quantity]: return list({iso.name for iso in self}) def filter_by_quantity(self, quantity: Union[str, Quantity]): - """Filters all isosurfaces by a specific quantity. - """ - if type(quantity) == Quantity: + """Filters all isosurfaces by a specific quantity.""" + if isinstance(quantity, Quantity): quantity = quantity.name - return IsosurfaceCollection(x for x in self if - x.quantity.name.lower() == quantity.lower() or x.quantity.short_name.lower() == quantity.lower()) + return IsosurfaceCollection( + x + for x in self + if x.quantity.name.lower() == quantity.lower() or x.quantity.short_name.lower() == quantity.lower() + ) def __repr__(self): - return "IsosurfaceCollection(" + super(IsosurfaceCollection, self).__repr__() + ")" + return "IsosurfaceCollection(" + super().__repr__() + ")" diff --git a/fdsreader/part/__init__.py b/fdsreader/part/__init__.py index 63843560..5ffb17a2 100644 --- a/fdsreader/part/__init__.py +++ b/fdsreader/part/__init__.py @@ -1,3 +1,2 @@ -from .particle import Particle - -from .particle_collection import ParticleCollection +from .particle import Particle as Particle +from .particle_collection import ParticleCollection as ParticleCollection diff --git a/fdsreader/part/particle.py b/fdsreader/part/particle.py index fb5f4d04..e04793b3 100644 --- a/fdsreader/part/particle.py +++ b/fdsreader/part/particle.py @@ -1,8 +1,7 @@ -from typing import List, Tuple, Dict, Sequence +from typing import Dict, List, Sequence, Tuple import numpy as np -from fdsreader.fds_classes import Mesh from fdsreader.utils import Quantity @@ -38,8 +37,7 @@ def id(self): return self.class_name def filter_by_tag(self, tag: int): - """Filter all particles by a single one with the specified tag. - """ + """Filter all particles by a single one with the specified tag.""" data = self.data tags = self.tags positions = self.positions @@ -74,7 +72,7 @@ def filter_by_tag(self, tag: int): @property def data(self) -> Dict[str, List[np.ndarray]]: """Dictionary with quantities as keys and a list with a numpy array for each timestep which - contains data for each particle in that timestep. + contains data for each particle in that timestep. """ if len(self._positions) == 0 and len(self._tags) == 0: self._init_callback() @@ -83,7 +81,7 @@ def data(self) -> Dict[str, List[np.ndarray]]: @property def tags(self) -> List[np.ndarray]: """List with a numpy array for each timestep which contains a tag for each particle in that - timestep. + timestep. """ if len(self._positions) == 0 and len(self._tags) == 0: self._init_callback() @@ -92,15 +90,14 @@ def tags(self) -> List[np.ndarray]: @property def positions(self) -> List[np.ndarray]: """List with a numpy array for each timestep which contains the position of each particle in - that timestep. + that timestep. """ if len(self._positions) == 0 and len(self._tags) == 0: self._init_callback() return self._positions def clear_cache(self): - """Remove all data from the internal cache that has been loaded so far to free memory. - """ + """Remove all data from the internal cache that has been loaded so far to free memory.""" if len(self._positions) != 0: del self._positions self._positions = list() diff --git a/fdsreader/part/particle_collection.py b/fdsreader/part/particle_collection.py index 400d0751..6404718c 100644 --- a/fdsreader/part/particle_collection.py +++ b/fdsreader/part/particle_collection.py @@ -1,16 +1,15 @@ -from typing import Iterable, Dict, List +from typing import Dict, Iterable, List + import numpy as np -from fdsreader.part import Particle -from fdsreader.fds_classes import Mesh -from fdsreader.utils.data import FDSDataCollection, Quantity import fdsreader.utils.fortran_data as fdtype from fdsreader import settings +from fdsreader.part import Particle +from fdsreader.utils.data import FDSDataCollection, Quantity class ParticleCollection(FDSDataCollection): - """Collection of :class:`Particle` objects. - """ + """Collection of :class:`Particle` objects.""" def __init__(self, times: Iterable[float], particles: Iterable[Particle]): super().__init__(particles) @@ -38,8 +37,7 @@ def _post_init(self): self._load_data() def _load_data(self): - """Function to read in all particle data for a simulation. - """ + """Function to read in all particle data for a simulation.""" particles = self pointer_location = {particle: [0] * len(self.times) for particle in particles} @@ -55,14 +53,15 @@ def _load_data(self): particle._tags.append(np.empty((size,), dtype=int)) for mesh, file_path in self._file_paths.items(): - with open(file_path, 'rb') as infile: + with open(file_path, "rb") as infile: # Initial offset (ONE, fds version and number of particle classes) offset = 3 * fdtype.INT.itemsize # Number of quantities for each particle class (plus an INTEGER_ZERO) - offset += fdtype.new((('i', 2),)).itemsize * len(particles) + offset += fdtype.new((("i", 2),)).itemsize * len(particles) # 30-char long name and unit information for each quantity - offset += fdtype.new((('c', 30),)).itemsize * 2 * sum( - [len(particle.quantities) for particle in particles]) + offset += ( + fdtype.new((("c", 30),)).itemsize * 2 * sum([len(particle.quantities) for particle in particles]) + ) infile.seek(offset) for t in range(len(self.times)): @@ -75,30 +74,32 @@ def _load_data(self): n_particles = fdtype.read(infile, fdtype.INT, 1)[0][0][0] offset = pointer_location[particle][t] # Read positions - dtype_positions = fdtype.new((('f', 3 * n_particles),)) + dtype_positions = fdtype.new((("f", 3 * n_particles),)) pos = fdtype.read(infile, dtype_positions, 1)[0][0] - particle._positions[t][offset: offset + n_particles] = pos.reshape((n_particles, 3), - order='F').astype(float) + particle._positions[t][offset : offset + n_particles] = pos.reshape( + (n_particles, 3), order="F" + ).astype(float) # Read tags - dtype_tags = fdtype.new((('i', n_particles),)) - particle._tags[t][offset: offset + n_particles] = fdtype.read(infile, dtype_tags, 1)[0][0] + dtype_tags = fdtype.new((("i", n_particles),)) + particle._tags[t][offset : offset + n_particles] = fdtype.read(infile, dtype_tags, 1)[0][0] # Read actual quantity values if len(particle.quantities) > 0: - dtype_data = fdtype.new( - (('f', str((n_particles, len(particle.quantities)))),)) + dtype_data = fdtype.new((("f", str((n_particles, len(particle.quantities)))),)) data_raw = fdtype.read(infile, dtype_data, 1)[0][0].reshape( - (n_particles, len(particle.quantities)), order='F') + (n_particles, len(particle.quantities)), order="F" + ) for q, quantity in enumerate(particle.quantities): - particle._data[quantity.name][t][ - offset:offset + n_particles] = data_raw[:, q].astype(float) + particle._data[quantity.name][t][offset : offset + n_particles] = data_raw[:, q].astype( + float + ) pointer_location[particle][t] += particle.n_particles[mesh][t] def __getitem__(self, key): - if type(key) == int: + if isinstance(key, int): return self._elements[key] for particle in self: if particle.class_name == key: @@ -113,4 +114,4 @@ def __contains__(self, value): return False def __repr__(self): - return "ParticleCollection(" + super(ParticleCollection, self).__repr__() + ")" + return "ParticleCollection(" + super().__repr__() + ")" diff --git a/fdsreader/pl3d/__init__.py b/fdsreader/pl3d/__init__.py index ad9f4a52..16792c01 100644 --- a/fdsreader/pl3d/__init__.py +++ b/fdsreader/pl3d/__init__.py @@ -1,3 +1,2 @@ -from .pl3d import Plot3D - -from .plot3D_collection import Plot3DCollection +from .pl3d import Plot3D as Plot3D +from .plot3D_collection import Plot3DCollection as Plot3DCollection diff --git a/fdsreader/pl3d/pl3d.py b/fdsreader/pl3d/pl3d.py index 721e97bc..3fd508bc 100644 --- a/fdsreader/pl3d/pl3d.py +++ b/fdsreader/pl3d/pl3d.py @@ -1,22 +1,22 @@ +import bisect import logging +import math import os from copy import deepcopy -from typing import Dict, Sequence, Tuple, Literal, Union, List +from typing import Dict, List, Literal, Tuple, Union + import numpy as np -import math -import bisect +import fdsreader.utils.fortran_data as fdtype +from fdsreader import settings from fdsreader.fds_classes import Mesh from fdsreader.utils import Quantity -from fdsreader import settings -import fdsreader.utils.fortran_data as fdtype _HANDLED_FUNCTIONS = {np.mean: (lambda pl: pl.mean)} def implements(np_function): - """Decorator to register an __array_function__ implementation for Plot3Ds. - """ + """Decorator to register an __array_function__ implementation for Plot3Ds.""" def decorator(func): _HANDLED_FUNCTIONS[np_function] = func @@ -30,8 +30,9 @@ class SubPlot3D: :ivar mesh: The mesh containing the data. """ + # Offset of the binary file to the end of the file header. - _offset = fdtype.new((('i', 3),)).itemsize + fdtype.new((('i', 4),)).itemsize + _offset = fdtype.new((("i", 3),)).itemsize + fdtype.new((("i", 4),)).itemsize def __init__(self, mesh: Mesh, quantity_idx: int): self.file_paths: List[str] = list() # Path to the binary data file for each time step @@ -49,17 +50,17 @@ def data(self) -> np.ndarray: """ if not hasattr(self, "_data"): self._data = np.empty(shape=(len(self.file_paths),) + self.mesh.dimension.shape()) - dtype_data = fdtype.new((('f', self.mesh.dimension.size() * 5),)) + dtype_data = fdtype.new((("f", self.mesh.dimension.size() * 5),)) for t, file_path in enumerate(self.file_paths): - with open(file_path, 'rb') as infile: + with open(file_path, "rb") as infile: infile.seek(self._offset) self._data[t, :, :, :] = fdtype.read(infile, dtype_data, 1)[0][0].reshape( - self.mesh.dimension.shape() + (5,), order='F')[:, :, :, self._quantity_idx] + self.mesh.dimension.shape() + (5,), order="F" + )[:, :, :, self._quantity_idx] return self._data def clear_cache(self): - """Remove all data from the internal cache that has been loaded so far to free memory. - """ + """Remove all data from the internal cache that has been loaded so far to free memory.""" if hasattr(self, "_data"): del self._data @@ -96,8 +97,7 @@ def _add_subplot(self, filename: str, time: float, quantity: Quantity, quantity_ _ = self._subplots[mesh.id].data def __getitem__(self, mesh: Mesh): - """Returns the :class:`SubPlot` that contains data for the given mesh. - """ + """Returns the :class:`SubPlot` that contains data for the given mesh.""" return self._subplots[mesh.id] @implements(np.mean) @@ -123,28 +123,28 @@ def std(self) -> float: return np.sqrt(sum / N) def clear_cache(self): - """Remove all data from the internal cache that has been loaded so far to free memory. - """ + """Remove all data from the internal cache that has been loaded so far to free memory.""" for subplot in self._subplots.values(): subplot.clear_cache() - def to_global(self, masked: bool = False, fill: float = 0, return_coordinates: bool = False) -> \ - Union[np.ndarray, Tuple[np.ndarray, Dict[Literal['x', 'y', 'z'], np.ndarray]]]: + def to_global( + self, masked: bool = False, fill: float = 0, return_coordinates: bool = False + ) -> Union[np.ndarray, Tuple[np.ndarray, Dict[Literal["x", "y", "z"], np.ndarray]]]: """Creates a global numpy ndarray from all subplots. - :param masked: Whether to apply the obstruction mask to the data or not. - :param fill: The fill value to use for masked entries. Only used when masked=True. - :param return_coordinates: If true, return the matching coordinate for each value on the generated grid. + :param masked: Whether to apply the obstruction mask to the data or not. + :param fill: The fill value to use for masked entries. Only used when masked=True. + :param return_coordinates: If true, return the matching coordinate for each value on the generated grid. """ if len(self._subplots) == 0: if return_coordinates: - return np.array([]), {d: np.array([]) for d in ('x', 'y', 'z')} + return np.array([]), {d: np.array([]) for d in ("x", "y", "z")} else: return np.array([]) - coord_min = {'x': math.inf, 'y': math.inf, 'z': math.inf} - coord_max = {'x': -math.inf, 'y': -math.inf, 'z': -math.inf} - for dim in ('x', 'y', 'z'): + coord_min = {"x": math.inf, "y": math.inf, "z": math.inf} + coord_max = {"x": -math.inf, "y": -math.inf, "z": -math.inf} + for dim in ("x", "y", "z"): for subplot in self._subplots.values(): co = subplot.mesh.coordinates[dim] coord_min[dim] = min(co[0], coord_min[dim]) @@ -153,29 +153,32 @@ def to_global(self, masked: bool = False, fill: float = 0, return_coordinates: b # The global grid will use the finest mesh as base and duplicate values of the coarser # meshes. Therefore, we first find the finest mesh and calculate the step size in each # dimension. - step_sizes_min = {'x': coord_max['x'] - coord_min['x'], - 'y': coord_max['y'] - coord_min['y'], - 'z': coord_max['z'] - coord_min['z']} - step_sizes_max = {'x': 0, 'y': 0, 'z': 0} + step_sizes_min = { + "x": coord_max["x"] - coord_min["x"], + "y": coord_max["y"] - coord_min["y"], + "z": coord_max["z"] - coord_min["z"], + } + step_sizes_max = {"x": 0, "y": 0, "z": 0} steps = dict() - global_max = {'x': -math.inf, 'y': -math.inf, 'z': -math.inf} + global_max = {"x": -math.inf, "y": -math.inf, "z": -math.inf} - for dim in ('x', 'y', 'z'): + for dim in ("x", "y", "z"): for subplot in self._subplots.values(): step_size = subplot.mesh.coordinates[dim][1] - subplot.mesh.coordinates[dim][0] step_sizes_min[dim] = min(step_size, step_sizes_min[dim]) step_sizes_max[dim] = max(step_size, step_sizes_max[dim]) global_max[dim] = max(subplot.mesh.coordinates[dim][-1], global_max[dim]) - for dim in ('x', 'y', 'z'): + for dim in ("x", "y", "z"): if step_sizes_min[dim] == 0: step_sizes_min[dim] = math.inf steps[dim] = 1 else: - steps[dim] = max(int(round((coord_max[dim] - coord_min[dim]) / step_sizes_min[dim])), - 1) + 1 # + step_sizes_max[dim] / step_sizes_min[dim] + steps[dim] = ( + max(int(round((coord_max[dim] - coord_min[dim]) / step_sizes_min[dim])), 1) + 1 + ) # + step_sizes_max[dim] / step_sizes_min[dim] - grid = np.full((self.n_t, steps['x'], steps['y'], steps['z']), np.nan) + grid = np.full((self.n_t, steps["x"], steps["y"], steps["z"]), np.nan) start_idx = dict() end_idx = dict() @@ -184,15 +187,19 @@ def to_global(self, masked: bool = False, fill: float = 0, return_coordinates: b if masked: mask = subplot.mesh.get_obstruction_mask(self.times) - start_idx = {dim: int(round( - (subplot.mesh.coordinates[dim][0] - coord_min[dim]) / step_sizes_min[dim])) for dim in ('x', 'y', 'z')} - end_idx = {dim: int(round( - (subplot.mesh.coordinates[dim][-1] - coord_min[dim]) / step_sizes_min[dim])) for dim in ('x', 'y', 'z')} + start_idx = { + dim: int(round((subplot.mesh.coordinates[dim][0] - coord_min[dim]) / step_sizes_min[dim])) + for dim in ("x", "y", "z") + } + end_idx = { + dim: int(round((subplot.mesh.coordinates[dim][-1] - coord_min[dim]) / step_sizes_min[dim])) + for dim in ("x", "y", "z") + } temp_data = dict() temp_mask = dict() for axis in range(3): - dim = ('x', 'y', 'z')[axis] + dim = ("x", "y", "z")[axis] # Temporarily save border points to add them back to the array again later if np.isclose(subplot.mesh.coordinates[dim][-1], global_max[dim]): temp_data_slices = [slice(s) for s in subplot_data.shape] @@ -205,26 +212,35 @@ def to_global(self, masked: bool = False, fill: float = 0, return_coordinates: b # We ignore border points unless they are actually on the border of the simulation space as all # other border points actually appear twice, as the subslices overlap. This only # applies for face_centered slices, as cell_centered slices will not overlap. - reduced_shape_slices = (slice(subplot.data.shape[0]),) + tuple(slice(1, None) for s in subplot.data.shape[1:]) + reduced_shape_slices = (slice(subplot.data.shape[0]),) + tuple( + slice(1, None) for s in subplot.data.shape[1:] + ) subplot_data = subplot_data[reduced_shape_slices] if masked: mask = mask[reduced_shape_slices] - n_repeat = max(int(round( - (subplot.mesh.coordinates[dim][1] - subplot.mesh.coordinates[dim][0]) / - step_sizes_min[dim])), 1) + n_repeat = max( + int( + round( + (subplot.mesh.coordinates[dim][1] - subplot.mesh.coordinates[dim][0]) / step_sizes_min[dim] + ) + ), + 1, + ) if n_repeat > 1: subplot_data = np.repeat(subplot_data, n_repeat, axis=axis + 1) if masked: mask = np.repeat(mask, n_repeat, axis=axis + 1) for axis in range(3): - dim = ('x', 'y', 'z')[axis] + dim = ("x", "y", "z")[axis] # Add border points back again if needed if np.isclose(subplot.mesh.coordinates[dim][-1], global_max[dim]): temp_data_slices = [slice(s) for s in subplot_data.shape] temp_data_slices[axis + 1] = slice(None) - subplot_data = np.concatenate((subplot_data, temp_data[dim][tuple(temp_data_slices)]), axis=axis + 1) + subplot_data = np.concatenate( + (subplot_data, temp_data[dim][tuple(temp_data_slices)]), axis=axis + 1 + ) if masked: mask = np.concatenate((mask, temp_mask[dim][tuple(temp_data_slices)]), axis=axis + 1) @@ -233,14 +249,20 @@ def to_global(self, masked: bool = False, fill: float = 0, return_coordinates: b if masked: subplot_data = np.where(mask, subplot_data, fill) - grid[:, start_idx['x']: end_idx['x'], start_idx['y']: end_idx['y'], - start_idx['z']: end_idx['z']] = subplot_data.reshape( - (self.n_t, end_idx['x'] - start_idx['x'], end_idx['y'] - start_idx['y'], - end_idx['z'] - start_idx['z'])) + grid[:, start_idx["x"] : end_idx["x"], start_idx["y"] : end_idx["y"], start_idx["z"] : end_idx["z"]] = ( + subplot_data.reshape( + ( + self.n_t, + end_idx["x"] - start_idx["x"], + end_idx["y"] - start_idx["y"], + end_idx["z"] - start_idx["z"], + ) + ) + ) if return_coordinates: coordinates = dict() - for dim_index, dim in enumerate(('x', 'y', 'z')): + for dim_index, dim in enumerate(("x", "y", "z")): coordinates[dim] = np.linspace(coord_min[dim], coord_max[dim], grid.shape[dim_index + 1]) if return_coordinates: @@ -250,23 +272,21 @@ def to_global(self, masked: bool = False, fill: float = 0, return_coordinates: b @property def n_t(self) -> int: - """Get the number of timesteps for which data was output. - """ + """Get the number of timesteps for which data was output.""" return len(self.times) @property def subplots(self): - """Returns a list with one SubPlot3D object per mesh. - """ + """Returns a list with one SubPlot3D object per mesh.""" return list(self._subplots.values()) def __array__(self): - """Method that will be called by numpy when trying to convert the object to a numpy ndarray. - """ + """Method that will be called by numpy when trying to convert the object to a numpy ndarray.""" raise UserWarning( "Plot3Ds can not be converted to numpy arrays, but they support all typical numpy" " operations such as np.multiply. If a 'global' array containing all subplots is" - " required, please use the 'to_global' method and use the returned numpy-array explicitly.") + " required, please use the 'to_global' method and use the returned numpy-array explicitly." + ) def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): """Method that will be called by numpy when using a ufunction with a Plot3D as input. @@ -277,7 +297,9 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): logging.warning( "The %s method has been used which is not explicitly implemented. Correctness of" " results is not guaranteed. If you require this feature to be implemented please" - " submit an issue on Github where you explain your use case.", method) + " submit an issue on Github where you explain your use case.", + method, + ) input_list = list(inputs) for i, inp in enumerate(inputs): if isinstance(inp, self.__class__): @@ -300,5 +322,6 @@ def __array_function__(self, func, types, args, kwargs): return NotImplemented return _HANDLED_FUNCTIONS[func](*args, **kwargs) + # __array_function__ implementations # ... diff --git a/fdsreader/pl3d/plot3D_collection.py b/fdsreader/pl3d/plot3D_collection.py index 95eb384a..3480918d 100644 --- a/fdsreader/pl3d/plot3D_collection.py +++ b/fdsreader/pl3d/plot3D_collection.py @@ -1,4 +1,5 @@ -from typing import Iterable, Union, List +from typing import Iterable, List, Union + import numpy as np from fdsreader.pl3d import Plot3D @@ -7,7 +8,7 @@ class Plot3DCollection(FDSDataCollection): """Collection of :class:`Plot3D` objects. Offers extensive functionality for filtering and - using plot3Ds as well as its subclasses such as :class:`SubPlot3D`. + using plot3Ds as well as its subclasses such as :class:`SubPlot3D`. """ def __init__(self, *plot3ds: Iterable[Plot3D]): @@ -22,12 +23,14 @@ def quantities(self) -> List[Quantity]: return [pl.quantity for pl in self._elements] def get_by_quantity(self, quantity: Union[str, Quantity]): - """Filters all plot3d data by a specific quantity. - """ - if type(quantity) == Quantity: + """Filters all plot3d data by a specific quantity.""" + if isinstance(quantity, Quantity): quantity = quantity.name - return next(x for x in self._elements if - x.quantity.name.lower() == quantity.lower() or x.quantity.short_name.lower() == quantity.lower()) + return next( + x + for x in self._elements + if x.quantity.name.lower() == quantity.lower() or x.quantity.short_name.lower() == quantity.lower() + ) def __repr__(self): - return "Plot3DCollection(" + super(Plot3DCollection, self).__repr__() + ")" + return "Plot3DCollection(" + super().__repr__() + ")" diff --git a/fdsreader/simulation.py b/fdsreader/simulation.py index e0692b24..660c4190 100644 --- a/fdsreader/simulation.py +++ b/fdsreader/simulation.py @@ -1,27 +1,26 @@ import glob import logging import os -import warnings -from typing import List, TextIO, Dict, AnyStr, Sequence, Tuple, Union import pickle +import warnings +from typing import AnyStr, Dict, List, Sequence, TextIO, Tuple, Union import numpy as np +import fdsreader.utils.fortran_data as fdtype +from fdsreader import __version__, settings +from fdsreader.bndf import Obstruction, ObstructionCollection, Patch, SubObstruction +from fdsreader.devc import Device, DeviceCollection +from fdsreader.evac import EvacCollection, Evacuation from fdsreader.fds_classes import Mesh, MeshCollection, Surface, Ventilation -from fdsreader.bndf import Obstruction, Patch, ObstructionCollection, SubObstruction -from fdsreader.geom import Geometry, GeomBoundary, GeometryCollection +from fdsreader.geom import GeomBoundary, Geometry, GeometryCollection from fdsreader.isof import Isosurface, IsosurfaceCollection from fdsreader.part import Particle, ParticleCollection -from fdsreader.evac import Evacuation, EvacCollection from fdsreader.pl3d import Plot3D, Plot3DCollection +from fdsreader.slcf import GeomSlice, GeomSliceCollection, Slice, SliceCollection from fdsreader.smoke3d import Smoke3D, Smoke3DCollection -from fdsreader.slcf import Slice, SliceCollection, GeomSliceCollection, GeomSlice -from fdsreader.devc import Device, DeviceCollection -from fdsreader.utils import Dimension, Quantity, Extent, log_error -from fdsreader.utils.data import create_hash, get_smv_file, Profile -import fdsreader.utils.fortran_data as fdtype -from fdsreader import settings -from fdsreader._version import __version__ +from fdsreader.utils import Dimension, Extent, Quantity, log_error +from fdsreader.utils.data import Profile, create_hash, get_smv_file class Simulation: @@ -60,7 +59,7 @@ def __new__(cls, path: str): smv_file_path = get_smv_file(path) root_path = os.path.dirname(smv_file_path) - with open(smv_file_path, 'r') as infile: + with open(smv_file_path) as infile: for line in infile: if line.strip() == "CHID": chid = infile.readline().strip() @@ -71,7 +70,7 @@ def __new__(cls, path: str): if not Simulation._loading and os.path.isfile(pickle_file_path): Simulation._loading = True try: - with open(pickle_file_path, 'rb') as f: + with open(pickle_file_path, "rb") as f: sim = pickle.load(f) except Exception as e: if settings.DEBUG: @@ -93,25 +92,27 @@ def __new__(cls, path: str): else: if os.path.isfile(pickle_file_path): os.remove(pickle_file_path) - return super(Simulation, cls).__new__(cls) + return super().__new__(cls) def __getnewargs__(self): - return self.smv_file_path, + return (self.smv_file_path,) def __repr__(self): - r = f"Simulation(chid={self.chid},\n" + \ - f" meshes={len(self.meshes)},\n" + \ - (f" obstructions={len(self.obstructions)},\n" if len(self.obstructions) > 0 else "") + \ - (f" geoms={len(self.geoms)},\n" if len(self.geoms) > 0 else "") + \ - (f" slices={len(self.slices)},\n" if len(self.slices) > 0 else "") + \ - (f" geomslices={len(self.geomslices)},\n" if len(self.geomslices) > 0 else "") + \ - (f" data_3d={len(self.data_3d)},\n" if len(self.data_3d) > 0 else "") + \ - (f" smoke_3d={len(self.smoke_3d)},\n" if len(self.smoke_3d) > 0 else "") + \ - (f" isosurfaces={len(self.isosurfaces)},\n" if len(self.isosurfaces) > 0 else "") + \ - (f" particles={len(self.particles)},\n" if len(self.particles) > 0 else "") + \ - (f" evacs={len(self.evacs)},\n" if len(self.evacs) > 0 else "") + \ - (f" devices={len(self.devices)},\n" if len(self.devices) > 0 else "") - return r[:-2] + ')' + r = ( + f"Simulation(chid={self.chid},\n" + + f" meshes={len(self.meshes)},\n" + + (f" obstructions={len(self.obstructions)},\n" if len(self.obstructions) > 0 else "") + + (f" geoms={len(self.geoms)},\n" if len(self.geoms) > 0 else "") + + (f" slices={len(self.slices)},\n" if len(self.slices) > 0 else "") + + (f" geomslices={len(self.geomslices)},\n" if len(self.geomslices) > 0 else "") + + (f" data_3d={len(self.data_3d)},\n" if len(self.data_3d) > 0 else "") + + (f" smoke_3d={len(self.smoke_3d)},\n" if len(self.smoke_3d) > 0 else "") + + (f" isosurfaces={len(self.isosurfaces)},\n" if len(self.isosurfaces) > 0 else "") + + (f" particles={len(self.particles)},\n" if len(self.particles) > 0 else "") + + (f" evacs={len(self.evacs)},\n" if len(self.evacs) > 0 else "") + + (f" devices={len(self.devices)},\n" if len(self.devices) > 0 else "") + ) + return r[:-2] + ")" def __init__(self, path: str): """ @@ -165,7 +166,7 @@ def __init__(self, path: str): self.ventilations: List[Ventilation] = list(self.ventilations.values()) for device_id, device in self._devices.items(): - if type(device) == list: + if isinstance(device, list): for devc in device: devc._data_callback = self._load_DEVC_data else: @@ -174,17 +175,19 @@ def __init__(self, path: str): # Combine the gathered temporary information into data collections self.geom_data = GeometryCollection(self._geom_data) self.slices = SliceCollection( - Slice(self.root_path, slice_data[0]["id"], slice_data[0]["cell_centered"], - slice_data[1:]) for slice_data in self._slices.values()) + Slice(self.root_path, slice_data[0]["id"], slice_data[0]["cell_centered"], slice_data[1:]) + for slice_data in self._slices.values() + ) self.geomslices = GeomSliceCollection( - GeomSlice(self.root_path, slice_data[0]["id"], - slice_data[0]["times"], slice_data[1:]) for slice_data in self._geomslices.values()) + GeomSlice(self.root_path, slice_data[0]["id"], slice_data[0]["times"], slice_data[1:]) + for slice_data in self._geomslices.values() + ) self.smoke_3d = Smoke3DCollection(self._smoke_3d.values()) self.isosurfaces = IsosurfaceCollection(self._isosurfaces.values()) self.devices = DeviceCollection(self._devices.values()) self.obstructions = ObstructionCollection(self._obstructions) # If no particles are simulated, initialize empty data container for consistency - if type(self._particles) == list: + if isinstance(self._particles, list): self.particles = ParticleCollection((), ()) else: self.particles = self._particles @@ -193,16 +196,28 @@ def __init__(self, path: str): if len(self._evacs) == 0: self.evacs = EvacCollection((), "", ()) self.meshes = MeshCollection(self._meshes) - del self._geom_data, self._geomslices, self._slices, self._obstructions, self._smoke_3d, self._isosurfaces, self._devices, self._particles, self._evacs, self._meshes, self._subobstructions + del ( + self._geom_data, + self._geomslices, + self._slices, + self._obstructions, + self._smoke_3d, + self._isosurfaces, + self._devices, + self._particles, + self._evacs, + self._meshes, + self._subobstructions, + ) if settings.ENABLE_CACHING: # Hash will be saved to simulation pickle file and compared to new hash when loading # the pickled simulation again in the next run of the program. self._hash = create_hash(self.smv_file_path) - pickle.dump(self, open(Simulation._get_pickle_filename(self.root_path, self.chid), 'wb'), protocol=4) + pickle.dump(self, open(Simulation._get_pickle_filename(self.root_path, self.chid), "wb"), protocol=4) def parse_smv_file(self): - with open(self.smv_file_path, 'r') as smv_file: + with open(self.smv_file_path) as smv_file: for line in smv_file: keyword = line.strip() if keyword == "VERSION" or keyword == "FDSVERSION": @@ -223,8 +238,9 @@ def parse_smv_file(self): self.steps = self._load_step_data(file_path) elif csv_type == "devc": self.devc_path = file_path - self._devices["Time"] = Device("Time", Quantity("TIME", "TIME", "s"), (.0, .0, .0), - (.0, .0, .0)) + self._devices["Time"] = Device( + "Time", Quantity("TIME", "TIME", "s"), (0.0, 0.0, 0.0), (0.0, 0.0, 0.0) + ) elif keyword == "HRRPUVCUT": self.hrrpuv_cutoff = float(smv_file.readline().strip()) elif keyword == "TOFFSET": @@ -243,7 +259,7 @@ def parse_smv_file(self): elif keyword == "DEVICE": device_id, device = self._register_device(smv_file) if device_id in self._devices: - if type(self._devices[device_id]) == list: + if isinstance(self._devices[device_id], list): self._devices[device_id].append(device) else: self._devices[device_id] = [self._devices[device_id], device] @@ -276,28 +292,27 @@ def parse_smv_file(self): @classmethod def _get_pickle_filename(cls, root_path: str, chid: str) -> AnyStr: - """Get the filename used to save the pickled simulation. - """ + """Get the filename used to save the pickled simulation.""" return os.path.join(root_path, chid + ".pickle") def _load_mesh(self, smv_file: TextIO, line: str) -> Mesh: - """Load information for a single mesh from the smv file at current pointer position. - """ + """Load information for a single mesh from the smv file at current pointer position.""" mesh_id = "".join(line.split()[1:]) grid_numbers = smv_file.readline().strip().split() - grid_dimensions = {'x': int(grid_numbers[0]) + 1, 'y': int(grid_numbers[1]) + 1, - 'z': int(grid_numbers[2]) + 1} + grid_dimensions = {"x": int(grid_numbers[0]) + 1, "y": int(grid_numbers[1]) + 1, "z": int(grid_numbers[2]) + 1} smv_file.readline() # Blank line assert smv_file.readline().strip() == "PDIM" coordinates = dict() extents = smv_file.readline().split() - extents = {'x': (float(extents[0]), float(extents[1])), - 'y': (float(extents[2]), float(extents[3])), - 'z': (float(extents[4]), float(extents[5]))} + extents = { + "x": (float(extents[0]), float(extents[1])), + "y": (float(extents[2]), float(extents[3])), + "z": (float(extents[4]), float(extents[5])), + } - for dim in ('x', 'y', 'z'): + for dim in ("x", "y", "z"): smv_file.readline() # Blank line assert smv_file.readline().strip()[:3] == "TRN" noc = int(smv_file.readline().strip()) @@ -325,7 +340,7 @@ def _load_mesh(self, smv_file: TextIO, line: str) -> Mesh: assert smv_file.readline().strip() == "OBST" self._load_obstructions(smv_file, mesh) - a = smv_file.readline() # Blank line + smv_file.readline() # Blank line assert smv_file.readline().strip() == "VENT" self._load_vents(smv_file, mesh) @@ -340,7 +355,7 @@ def _load_obstructions(self, smv_file: TextIO, mesh: Mesh): self._subobstructions[mesh.id] = list() for _ in range(n): - line = smv_file.readline().strip().split('!') + line = smv_file.readline().strip().split("!") line_floats = line[0].strip().split() ext = [float(line_floats[i]) for i in range(6)] # The ordinal is negative if the obstruction was created due to a hole, the negative @@ -361,18 +376,18 @@ def _load_obstructions(self, smv_file: TextIO, mesh: Mesh): if obst_data[0] == obst_id: # The obstruction that was already added to the temp_data has to receive an # updated ID as well, so the user can recognize it as special obstruction - obst_data[0] = obst_data[0] + '_from-hole-1' + obst_data[0] = obst_data[0] + "_from-hole-1" # Now set the next obst_id to '_from-hole-2' - obst_id += '_from-hole-2' + obst_id += "_from-hole-2" # ...For subsequent cases (i.e., when the third or fourth obstruction with the same id # is found), we need to catch the case differently for obst_data in reversed(temp_data): - if obst_data[0][:-1] == obst_id + '_from-hole-': + if obst_data[0][:-1] == obst_id + "_from-hole-": # Find the last obstruction with the same id and set the number of the current # one accordingly - obst_id += '_from-hole-' + str(int(obst_data[0][-1]) + 1) + obst_id += "_from-hole-" + str(int(obst_data[0][-1]) + 1) temp_data.append([obst_id, Extent(*ext), side_surfaces, texture_origin]) @@ -381,8 +396,13 @@ def _load_obstructions(self, smv_file: TextIO, mesh: Mesh): line = smv_file.readline().strip().split() bound_indices = ( - int(float(line[0])), int(float(line[1])), int(float(line[2])), - int(float(line[3])), int(float(line[4])), int(float(line[5]))) + int(float(line[0])), + int(float(line[1])), + int(float(line[2])), + int(float(line[3])), + int(float(line[4])), + int(float(line[5])), + ) color_index = int(line[6]) block_type = int(line[7]) rgba = tuple(float(line[i]) for i in range(8, 12)) if color_index == -3 else () @@ -425,16 +445,14 @@ def _load_geoms(self, smv_file: TextIO, line: str): rgb_line = line[1].split() texture_mapping = texture_line[0] - texture_origin = ( - float(texture_line[1]), float(texture_line[2]), float(texture_line[3])) + texture_origin = (float(texture_line[1]), float(texture_line[2]), float(texture_line[3])) is_terrain = bool(texture_line[4]) rgb = (int(rgb_line[0]), int(rgb_line[1]), int(rgb_line[2])) - if '%' in line[0]: + if "%" in line[0]: surface_id = line[0].split("%")[-1] surface = next((s for s in self.surfaces if s.id() == surface_id), None) - geom = Geometry(file_path, texture_mapping, texture_origin, is_terrain, rgb, - surface=surface) + geom = Geometry(file_path, texture_mapping, texture_origin, is_terrain, rgb, surface=surface) else: geom = Geometry(file_path, texture_mapping, texture_origin, is_terrain, rgb) self.geoms.append(geom) @@ -448,8 +466,7 @@ def _load_vents(self, smv_file: TextIO, mesh: Mesh): def read_common_info(): line = smv_file.readline().strip().split() - return line, [float(line[i]) for i in range(6)], int(line[6]) - 1, self.surfaces[ - int(line[7])] + return line, [float(line[i]) for i in range(6)], int(line[6]) - 1, self.surfaces[int(line[7])] def read_common_info2(): line = smv_file.readline().strip().split() @@ -479,9 +496,9 @@ def read_common_info2(): extent, vent_index, surface = temp_data[v] bound_indices, color_index, draw_type, rgba = read_common_info2() if vent_index not in self.ventilations: - self.ventilations[vent_index] = Ventilation(surface, bound_indices, color_index, - draw_type, rgba=rgba, - texture_origin=texture_origin) + self.ventilations[vent_index] = Ventilation( + surface, bound_indices, color_index, draw_type, rgba=rgba, texture_origin=texture_origin + ) self.ventilations[vent_index]._add_subventilation(mesh, extent) smv_file.readline() @@ -493,24 +510,27 @@ def read_common_info2(): line, extent, vent_index, surface = read_common_info() circular_vent_origin = (float(line[12]), float(line[13]), float(line[14])) radius = float(line[15]) - temp_data.append( - (extent, vent_index, surface, texture_origin, circular_vent_origin, radius)) + temp_data.append((extent, vent_index, surface, texture_origin, circular_vent_origin, radius)) for v in range(n): extent, vent_index, surface, texture_origin, circular_vent_origin, radius = temp_data[v] bound_indices, color_index, draw_type, rgba = read_common_info2() if vent_index not in self.ventilations: - self.ventilations[vent_index] = Ventilation(surface, bound_indices, - color_index, draw_type, rgba=rgba, - texture_origin=texture_origin, - circular_vent_origin=circular_vent_origin, - radius=radius) + self.ventilations[vent_index] = Ventilation( + surface, + bound_indices, + color_index, + draw_type, + rgba=rgba, + texture_origin=texture_origin, + circular_vent_origin=circular_vent_origin, + radius=radius, + ) self.ventilations[vent_index]._add_subventilation(mesh, extent) @log_error("surface") def _load_surface(self, smv_file: TextIO) -> Surface: - """Load the information for a single surface from the smv file at current pointer position. - """ + """Load the information for a single surface from the smv file at current pointer position.""" surface_id = smv_file.readline().strip() @@ -527,27 +547,35 @@ def _load_surface(self, smv_file: TextIO) -> Surface: texture_map = smv_file.readline().strip() texture_map = None if texture_map == "null" else os.path.join(self.root_path, texture_map) - return Surface(surface_id, tmpm, material_emissivity, surface_type, texture_width, - texture_height, texture_map, rgb, transparency) + return Surface( + surface_id, + tmpm, + material_emissivity, + surface_type, + texture_width, + texture_height, + texture_map, + rgb, + transparency, + ) @log_error("slcf") def _load_slice(self, smv_file: TextIO, line: str): - """Loads the slice at current pointer position. - """ + """Loads the slice at current pointer position.""" if "SLCC" in line: cell_centered = True else: cell_centered = False - slice_index = int(line.split('!')[1].strip().split()[0]) + slice_index = int(line.split("!")[1].strip().split()[0]) - slice_id = line.split('%')[1].split('&')[0].strip() if '%' in line else "" + slice_id = line.split("%")[1].split("&")[0].strip() if "%" in line else "" - mesh_index = int(line.split('&')[0].strip().split()[1]) - 1 + mesh_index = int(line.split("&")[0].strip().split()[1]) - 1 mesh = self._meshes[mesh_index] # Read in index ranges for x, y and z - bound_indices = [int(i.strip()) for i in line.split('&')[1].split('!')[0].strip().split()] + bound_indices = [int(i.strip()) for i in line.split("&")[1].split("!")[0].strip().split()] extent, dimension = self._indices_to_extent(bound_indices, mesh) filename = smv_file.readline().strip() @@ -556,25 +584,31 @@ def _load_slice(self, smv_file: TextIO, line: str): unit = smv_file.readline().strip() if slice_index not in self._slices: - self._slices[slice_index] = [ - {"cell_centered": cell_centered, "id": slice_id}] + self._slices[slice_index] = [{"cell_centered": cell_centered, "id": slice_id}] self._slices[slice_index].append( - {"dimension": dimension, "extent": extent, "mesh": mesh, "filename": filename, - "quantity": quantity, "short_name": short_name, "unit": unit}) + { + "dimension": dimension, + "extent": extent, + "mesh": mesh, + "filename": filename, + "quantity": quantity, + "short_name": short_name, + "unit": unit, + } + ) @log_error("slcf") def _load_geomslice(self, smv_file: TextIO, line: str): - """Loads the geomslice at current pointer position. - """ - slice_index = int(line.split('!')[1].strip().split()[0]) + """Loads the geomslice at current pointer position.""" + slice_index = int(line.split("!")[1].strip().split()[0]) - slice_id = "".join(line.split('%')[1].split('&')).strip() if '%' in line else "" + slice_id = "".join(line.split("%")[1].split("&")).strip() if "%" in line else "" - mesh_index = int(line.split('&')[0].strip().split()[1]) - 1 + mesh_index = int(line.split("&")[0].strip().split()[1]) - 1 mesh = self._meshes[mesh_index] # Read in index ranges for x, y and z - bound_indices = [int(i.strip()) for i in line.split('&')[1].split('!')[0].strip().split()] + bound_indices = [int(i.strip()) for i in line.split("&")[1].split("!")[0].strip().split()] extent, _ = self._indices_to_extent(bound_indices, mesh) filename = smv_file.readline().strip() @@ -587,7 +621,7 @@ def _load_geomslice(self, smv_file: TextIO, line: str): if os.path.exists(file_path + ".bnd"): times = list() - with open(file_path + ".bnd", 'r') as bnd_file: + with open(file_path + ".bnd") as bnd_file: for line in bnd_file: times.append(float(line.split()[0])) times = np.array(times) @@ -595,16 +629,22 @@ def _load_geomslice(self, smv_file: TextIO, line: str): times = None if slice_index not in self._slices: - self._geomslices[slice_index] = [ - {"times": times, "id": slice_id}] + self._geomslices[slice_index] = [{"times": times, "id": slice_id}] self._geomslices[slice_index].append( - {"extent": extent, "mesh": mesh, "filename": filename, "geomfilename": geom_filename, - "quantity": quantity, "short_name": short_name, "unit": unit}) + { + "extent": extent, + "mesh": mesh, + "filename": filename, + "geomfilename": geom_filename, + "quantity": quantity, + "short_name": short_name, + "unit": unit, + } + ) @log_error("bndf") def _load_boundary_data(self, smv_file: TextIO, line: str, cell_centered: bool): - """Loads the boundary data at current pointer position. - """ + """Loads the boundary data at current pointer position.""" line = line.split() mesh_index = int(line[1]) - 1 mesh = self._meshes[mesh_index] @@ -614,7 +654,7 @@ def _load_boundary_data(self, smv_file: TextIO, line: str, cell_centered: bool): short_name = smv_file.readline().strip() unit = smv_file.readline().strip() - bid = int(filename.split('_')[-1][:-3]) - 1 + bid = int(filename.split("_")[-1][:-3]) - 1 file_path = os.path.join(self.root_path, filename) @@ -624,14 +664,14 @@ def _load_boundary_data(self, smv_file: TextIO, line: str, cell_centered: bool): lower_bounds = np.array([np.float32(+1e33)], dtype=np.float32) upper_bounds = np.array([np.float32(-1e33)], dtype=np.float32) - with open(file_path, 'rb') as infile: + with open(file_path, "rb") as infile: # Offset of the binary file to the end of the file header. - initial_offset = 3 * fdtype.new((('c', 30),)).itemsize + initial_offset = 3 * fdtype.new((("c", 30),)).itemsize infile.seek(initial_offset) n_patches = fdtype.read(infile, fdtype.INT, 1)[0][0][0] - dtype_patches = fdtype.new((('i', 9),)) + dtype_patches = fdtype.new((("i", 9),)) patch_infos = fdtype.read(infile, dtype_patches, n_patches) initial_offset += fdtype.INT.itemsize + dtype_patches.itemsize * n_patches patch_offset = fdtype.FLOAT.itemsize @@ -641,9 +681,9 @@ def _load_boundary_data(self, smv_file: TextIO, line: str, cell_centered: bool): for patch_info in patch_infos: patch_info = patch_info[0] extent, dimension = self._indices_to_extent(patch_info[:6], mesh) - patches_data_bytes += fdtype.new((('f', str(dimension.shape(cell_centered=False))),)).itemsize + patches_data_bytes += fdtype.new((("f", str(dimension.shape(cell_centered=False))),)).itemsize - # Time info + # Time info time_bytes = fdtype.FLOAT.itemsize n_t = (os.stat(file_path).st_size - initial_offset) // (time_bytes + patches_data_bytes) @@ -662,8 +702,9 @@ def _load_boundary_data(self, smv_file: TextIO, line: str, cell_centered: bool): orientation = patch_info[6] obst_index = patch_info[7] - 1 - p = Patch(file_path, dimension, extent, orientation, cell_centered, - patch_offset, initial_offset, n_t, mesh) + p = Patch( + file_path, dimension, extent, orientation, cell_centered, patch_offset, initial_offset, n_t, mesh + ) # "Obstacles" with index -1 give the extent of the (whole) mesh faces and refer to # "closed" mesh faces, therefore that data will be added to the corresponding mesh instead @@ -676,21 +717,22 @@ def _load_boundary_data(self, smv_file: TextIO, line: str, cell_centered: bool): mesh_patches[mesh.id] = list() mesh_patches[mesh.id].append(p) - patch_offset += fdtype.new((('f', str(p.dimension.shape(cell_centered=False))),)).itemsize + patch_offset += fdtype.new((("f", str(p.dimension.shape(cell_centered=False))),)).itemsize for obst_index, p in patches.items(): for patch in p: patch._post_init(patch_offset) - self._subobstructions[mesh.id][obst_index]._add_patches(bid, cell_centered, quantity, - short_name, unit, p, times, - lower_bounds, upper_bounds) + self._subobstructions[mesh.id][obst_index]._add_patches( + bid, cell_centered, quantity, short_name, unit, p, times, lower_bounds, upper_bounds + ) for p in mesh_patches.values(): for patch in p: patch._post_init(patch_offset) - patch.mesh._add_patches(bid, cell_centered, quantity, short_name, unit, p, times, - lower_bounds, upper_bounds) + patch.mesh._add_patches( + bid, cell_centered, quantity, short_name, unit, p, times, lower_bounds, upper_bounds + ) @log_error("geom") def _load_boundary_data_geom(self, smv_file: TextIO, line: str): @@ -705,7 +747,7 @@ def _load_boundary_data_geom(self, smv_file: TextIO, line: str): short_name = smv_file.readline().strip() unit = smv_file.readline().strip() - bid = int(filename_be.split('_')[-1][:-3]) - 1 + bid = int(filename_be.split("_")[-1][:-3]) - 1 file_path_be = os.path.join(self.root_path, filename_be) file_path_gbf = os.path.join(self.root_path, filename_gbf) @@ -713,7 +755,7 @@ def _load_boundary_data_geom(self, smv_file: TextIO, line: str): times = list() lower_bounds = list() upper_bounds = list() - with open(file_path_be + ".bnd", 'r') as bnd_file: + with open(file_path_be + ".bnd") as bnd_file: for line in bnd_file: splits = line.split() times.append(float(splits[0])) @@ -726,13 +768,11 @@ def _load_boundary_data_geom(self, smv_file: TextIO, line: str): if bid >= len(self._geom_data): self._geom_data.append(GeomBoundary(Quantity(quantity, short_name, unit), times, n_t)) - self._geom_data[bid]._add_data(mesh_index, file_path_be, file_path_gbf, lower_bounds, - upper_bounds) + self._geom_data[bid]._add_data(mesh_index, file_path_be, file_path_gbf, lower_bounds, upper_bounds) @log_error("pl3d") def _load_plot_3d(self, smv_file: TextIO, line: str): - """Loads the pl3d at current pointer position. - """ + """Loads the pl3d at current pointer position.""" line = line.strip().split() time = float(line[1]) @@ -745,12 +785,13 @@ def _load_plot_3d(self, smv_file: TextIO, line: str): short_name = smv_file.readline().strip() unit = smv_file.readline().strip() - self.data_3d[i]._add_subplot(filename, time, Quantity(quantity, short_name, unit), i, self._meshes[mesh_index]) + self.data_3d[i]._add_subplot( + filename, time, Quantity(quantity, short_name, unit), i, self._meshes[mesh_index] + ) @log_error("smoke3d") def _load_smoke_3d(self, smv_file: TextIO, line: str): - """Loads the smoke3d at current pointer position. - """ + """Loads the smoke3d at current pointer position.""" line = line.strip().split() mesh_index = int(line[1]) - 1 @@ -763,7 +804,7 @@ def _load_smoke_3d(self, smv_file: TextIO, line: str): times = list() upper_bounds = list() - with open(os.path.join(self.root_path, filename + ".sz"), 'r') as sizefile: + with open(os.path.join(self.root_path, filename + ".sz")) as sizefile: # skip version line sizefile.readline() for line in sizefile: @@ -781,13 +822,12 @@ def _load_smoke_3d(self, smv_file: TextIO, line: str): @log_error("isof") def _load_isosurface(self, smv_file: TextIO, line: str): - """Loads the isosurface at current pointer position. - """ - double_quantity = line[0] == 'T' + """Loads the isosurface at current pointer position.""" + double_quantity = line[0] == "T" mesh_index = int(line.strip().split()[1]) - 1 iso_filename = smv_file.readline().strip() - iso_id = int(iso_filename.split('_')[-1][:-4]) + iso_id = int(iso_filename.split("_")[-1][:-4]) iso_file_path = os.path.join(self.root_path, iso_filename) if double_quantity: @@ -801,22 +841,30 @@ def _load_isosurface(self, smv_file: TextIO, line: str): v_unit = smv_file.readline().strip() if iso_id not in self._isosurfaces: - with open(iso_file_path, 'rb') as infile: + with open(iso_file_path, "rb") as infile: nlevels = fdtype.read(infile, fdtype.INT, 3)[2][0][0] - dtype_header_levels = fdtype.new((('f', nlevels),)) + dtype_header_levels = fdtype.new((("f", nlevels),)) levels = fdtype.read(infile, dtype_header_levels, 1)[0] if double_quantity: if iso_id not in self._isosurfaces: - self._isosurfaces[iso_id] = Isosurface(iso_id, double_quantity, quantity, short_name, - unit, levels, v_quantity=v_quantity, - v_short_name=v_short_name, v_unit=v_unit) - self._isosurfaces[iso_id]._add_subsurface(self._meshes[mesh_index], iso_file_path, - viso_file_path=viso_file_path) + self._isosurfaces[iso_id] = Isosurface( + iso_id, + double_quantity, + quantity, + short_name, + unit, + levels, + v_quantity=v_quantity, + v_short_name=v_short_name, + v_unit=v_unit, + ) + self._isosurfaces[iso_id]._add_subsurface( + self._meshes[mesh_index], iso_file_path, viso_file_path=viso_file_path + ) else: if iso_id not in self._isosurfaces: - self._isosurfaces[iso_id] = Isosurface(iso_id, double_quantity, quantity, short_name, - unit, levels) + self._isosurfaces[iso_id] = Isosurface(iso_id, double_quantity, quantity, short_name, unit, levels) self._isosurfaces[iso_id]._add_subsurface(self._meshes[mesh_index], iso_file_path) @log_error("part") @@ -833,10 +881,14 @@ def _register_particle(self, smv_file: TextIO) -> Particle: quantities.append(Quantity(quantity, short_name, unit)) return Particle(particle_class, quantities, color) - def _load_prt5_meta(self, prts: Union[List[Particle], ParticleCollection, List[Evacuation], EvacCollection], - file_path: str, mesh: Mesh) -> List[float]: - is_evac = type(prts[0]) == Evacuation - with open(file_path, 'r') as bnd_file: + def _load_prt5_meta( + self, + prts: Union[List[Particle], ParticleCollection, List[Evacuation], EvacCollection], + file_path: str, + mesh: Mesh, + ) -> List[float]: + is_evac = isinstance(prts[0], Evacuation) + with open(file_path) as bnd_file: line = bnd_file.readline().strip().split() n_classes = int(line[1]) times = list() @@ -875,8 +927,8 @@ def _load_particle_data(self, smv_file: TextIO, line: str): mesh_index = int(line.split()[1].strip()) - 1 mesh = self._meshes[mesh_index] - times = self._load_prt5_meta(self._particles, file_path + '.bnd', mesh) - if type(self._particles) == list: + times = self._load_prt5_meta(self._particles, file_path + ".bnd", mesh) + if isinstance(self._particles, list): self._particles = ParticleCollection(times, self._particles) self._particles._file_paths[mesh.id] = file_path @@ -906,8 +958,8 @@ def _load_evac_data(self, smv_file: TextIO, line: str): mesh_index, z_offset = line.split()[1:] mesh = self._meshes[int(mesh_index) - 1] - times = self._load_prt5_meta(self._evacs, file_path + '.bnd', mesh)[1:] # First timestep is weird somehow - if type(self._evacs) == list: + times = self._load_prt5_meta(self._evacs, file_path + ".bnd", mesh)[1:] # First timestep is weird somehow + if isinstance(self._evacs, list): self.evacs = EvacCollection(self._evacs, os.path.join(self.root_path, self.chid + "_evac"), times) self.evacs.z_offsets[mesh.id] = float(z_offset) @@ -920,41 +972,41 @@ def _load_evac_data(self, smv_file: TextIO, line: str): @log_error("prof") def _load_profiles(self): for f in glob.glob(str(os.path.join(self.root_path, self.chid)) + "_prof*"): - with open(f, 'r') as infile: + with open(f) as infile: profile_id = infile.readline() infile.readline() # Skip header - data: np.ndarray = np.genfromtxt(infile, delimiter=',', dtype=np.float32, autostrip=True).T + data: np.ndarray = np.genfromtxt(infile, delimiter=",", dtype=np.float32, autostrip=True).T times = data[0] npoints = data[1].astype(int) depths = np.empty((data.shape[1],), dtype=object) values = np.empty((data.shape[1],), dtype=object) for i, n in enumerate(npoints): - depths[i] = data[2: 2 + n, i] - values[i] = data[2 + n:, i] + depths[i] = data[2 : 2 + n, i] + values[i] = data[2 + n :, i] self.profiles[profile_id] = Profile(profile_id, times, npoints, depths, values) @log_error("devc") def _register_device(self, smv_file: TextIO) -> Tuple[str, Device]: - line = smv_file.readline().strip().split('%') + line = smv_file.readline().strip().split("%") device_id = line[0].strip() quantity = None if len(line) > 1: quantity_name = line[1].strip() quantity = Quantity(quantity_name, quantity_name, "") - line = smv_file.readline().strip().split('#')[0].split() + line = smv_file.readline().strip().split("#")[0].split() position = (float(line[0]), float(line[1]), float(line[2])) orientation = (float(line[3]), float(line[4]), float(line[5])) return device_id, Device(device_id, quantity, position, orientation) def _load_DEVC_data(self): - with open(self.devc_path, 'r') as infile: - units = infile.readline().split(',') - names = [name.replace('"', '').replace('\n', '').strip() for name in infile.readline().split(',"')] - values = np.genfromtxt(infile, delimiter=',', dtype=np.float32, autostrip=True) + with open(self.devc_path) as infile: + units = infile.readline().split(",") + names = [name.replace('"', "").replace("\n", "").strip() for name in infile.readline().split(',"')] + values = np.genfromtxt(infile, delimiter=",", dtype=np.float32, autostrip=True) for k in range(len(names)): - if type(self.devices[names[k]]) == list: + if isinstance(self.devices[names[k]], list): for devc in self.devices[names[k]]: if not hasattr(devc, "_data"): # Find the first device in the list that does not yet have any data associated with it @@ -970,10 +1022,10 @@ def _load_DEVC_data(self): line_path = self.devc_path.replace("devc", "line") if os.path.exists(line_path): - with open(line_path, 'r') as infile: + with open(line_path) as infile: units = infile.readline() - names = [name.replace('"', '').replace('\n', '').strip() for name in infile.readline().split(',')] - data = np.genfromtxt(infile, delimiter=',', dtype=np.float32, autostrip=True) + names = [name.replace('"', "").replace("\n", "").strip() for name in infile.readline().split(",")] + data = np.genfromtxt(infile, delimiter=",", dtype=np.float32, autostrip=True) for k, key in enumerate(names): if key in self.devices: devc = self.devices[key] @@ -985,21 +1037,22 @@ def _load_DEVC_data(self): @log_error("csv") def _load_HRR_data(self, file_path: str) -> Dict[str, np.ndarray]: - with open(file_path, 'r') as infile: + with open(file_path) as infile: infile.readline() - keys = [name.replace('"', '').replace('\n', '').strip() for name in infile.readline().split(',')] + keys = [name.replace('"', "").replace("\n", "").strip() for name in infile.readline().split(",")] - values = np.loadtxt(file_path, delimiter=',', ndmin=2, skiprows=2) + values = np.loadtxt(file_path, delimiter=",", ndmin=2, skiprows=2) return self._transform_csv_data(keys, values) @log_error("csv") def _load_step_data(self, file_path: str) -> Dict[str, np.ndarray]: - with open(file_path, 'r') as infile: + with open(file_path) as infile: infile.readline() - keys = [name.replace('"', '').replace('\n', '').strip() for name in infile.readline().split(',')][2:] - timesteps = np.loadtxt(file_path, dtype=np.dtype("datetime64[ms]"), delimiter=',', ndmin=1, usecols=1, - skiprows=2) - float_values = np.loadtxt(file_path, delimiter=',', usecols=range(2, len(keys) + 2), ndmin=2, skiprows=2) + keys = [name.replace('"', "").replace("\n", "").strip() for name in infile.readline().split(",")][2:] + timesteps = np.loadtxt( + file_path, dtype=np.dtype("datetime64[ms]"), delimiter=",", ndmin=1, usecols=1, skiprows=2 + ) + float_values = np.loadtxt(file_path, delimiter=",", usecols=range(2, len(keys) + 2), ndmin=2, skiprows=2) data = self._transform_csv_data(keys, float_values) data["Time Step"] = timesteps return data @@ -1008,9 +1061,9 @@ def _load_step_data(self, file_path: str) -> Dict[str, np.ndarray]: def _load_CPU_data(self) -> Dict[str, np.ndarray]: file_path = os.path.join(self.root_path, self.chid + "_cpu.csv") if os.path.exists(file_path): - with open(file_path, 'r') as infile: - keys = [name.replace('"', '').replace('\n', '').strip() for name in infile.readline().split(',')] - values = np.loadtxt(file_path, delimiter=',', ndmin=2, skiprows=1) + with open(file_path) as infile: + keys = [name.replace('"', "").replace("\n", "").strip() for name in infile.readline().split(",")] + values = np.loadtxt(file_path, delimiter=",", ndmin=2, skiprows=1) else: return dict() data = self._transform_csv_data(keys, values) @@ -1025,16 +1078,20 @@ def _transform_csv_data(self, keys, values): arr[i] = values[i][k] return data - def _indices_to_extent(self, indices: Sequence[Union[int, str]], mesh: Mesh) -> Tuple[ - Extent, Dimension]: + def _indices_to_extent(self, indices: Sequence[Union[int, str]], mesh: Mesh) -> Tuple[Extent, Dimension]: co = mesh.coordinates indices = tuple(int(index) for index in indices) x_min, x_max, y_min, y_max, z_min, z_max = indices co_x_min, co_x_max, co_y_min, co_y_max, co_z_min, co_z_max = ( - co['x'][x_min], co['x'][x_max], co['y'][y_min], - co['y'][y_max], co['z'][z_min], co['z'][z_max]) + co["x"][x_min], + co["x"][x_max], + co["y"][y_min], + co["y"][y_max], + co["z"][z_min], + co["z"][z_max], + ) dimension = Dimension(indices[1] - indices[0] + 1, indices[3] - indices[2] + 1, indices[5] - indices[4] + 1) extent = Extent(co_x_min, co_x_max, co_y_min, co_y_max, co_z_min, co_z_max) diff --git a/fdsreader/slcf/__init__.py b/fdsreader/slcf/__init__.py index b22b7a8f..eff6379b 100644 --- a/fdsreader/slcf/__init__.py +++ b/fdsreader/slcf/__init__.py @@ -1,7 +1,4 @@ -from .slice import Slice - -from .slice_collection import SliceCollection - -from .geomslice import GeomSlice - -from .geomslice_collection import GeomSliceCollection +from .geomslice import GeomSlice as GeomSlice +from .geomslice_collection import GeomSliceCollection as GeomSliceCollection +from .slice import Slice as Slice +from .slice_collection import SliceCollection as SliceCollection diff --git a/fdsreader/slcf/geomslice.py b/fdsreader/slcf/geomslice.py index adde0d81..38244c20 100644 --- a/fdsreader/slcf/geomslice.py +++ b/fdsreader/slcf/geomslice.py @@ -1,23 +1,22 @@ +import logging import math import os from copy import deepcopy +from typing import Collection, Dict, List, Union import numpy as np -import logging -from typing import Dict, Collection, Union, List from typing_extensions import Literal -from fdsreader.fds_classes import Mesh -from fdsreader.utils import Quantity, Extent -from fdsreader import settings import fdsreader.utils.fortran_data as fdtype +from fdsreader import settings +from fdsreader.fds_classes import Mesh +from fdsreader.utils import Extent, Quantity _HANDLED_FUNCTIONS = {} def implements(np_function): - """Decorator to register an __array_function__ implementation for GeomSlices. - """ + """Decorator to register an __array_function__ implementation for GeomSlices.""" def decorator(func): _HANDLED_FUNCTIONS[np_function] = func @@ -45,50 +44,49 @@ def __init__(self, parent_slice, filename: str, geom_filename: str, extent: Exte @property def orientation(self) -> Literal[1, 2, 3]: - """Orientation [1,2,3] of the geomslice in case it is 2D, 0 otherwise. - """ + """Orientation [1,2,3] of the geomslice in case it is 2D, 0 otherwise.""" return self._parent_slice.orientation @property def times(self): return self._parent_slice.times - @property def n_t(self) -> int: - """Get the number of timesteps for which data was output. - """ + """Get the number of timesteps for which data was output.""" return self._parent_slice.n_t def _load_geom_data(self): - with open(self.geom_file_path, 'rb') as infile: - dtype_meta = fdtype.new((('i', 3),)) + with open(self.geom_file_path, "rb") as infile: + dtype_meta = fdtype.new((("i", 3),)) infile.seek(2 * fdtype.INT.itemsize + dtype_meta.itemsize + fdtype.FLOAT.itemsize) self.n_verts, self.n_faces, n_vols = fdtype.read(infile, dtype_meta, 1)[0][0] if self.n_verts > 0 and self.n_faces > 0: - dtype_verts = fdtype.new((('f', 3 * self.n_verts),)) - dtype_faces = fdtype.new((('i', 3 * self.n_faces),)) + dtype_verts = fdtype.new((("f", 3 * self.n_verts),)) + dtype_faces = fdtype.new((("i", 3 * self.n_faces),)) # dtype_locations = fdtype.new((('i', self.n_faces),)) # dtype_zero_floats = fdtype.new((('f', 3 * self.n_faces * 2),)) - self._vertices = fdtype.read(infile, dtype_verts, 1)[0][0].reshape((self.n_verts, 3), order='F') - self._faces = fdtype.read(infile, dtype_faces, 1)[0][0].reshape((self.n_faces, 3), order='F') + self._vertices = fdtype.read(infile, dtype_verts, 1)[0][0].reshape((self.n_verts, 3), order="F") + self._faces = fdtype.read(infile, dtype_faces, 1)[0][0].reshape((self.n_faces, 3), order="F") else: self._vertices = np.array([]) self._faces = np.array([]) def _load_data(self): - with open(self.file_path, 'rb') as infile: + with open(self.file_path, "rb") as infile: infile.seek(2 * fdtype.INT.itemsize) if self.n_verts > 0 and self.n_faces > 0: - dtype_data = fdtype.combine(fdtype.FLOAT, fdtype.new((('i', 4),)), fdtype.new((('f', self.n_faces),))) + dtype_data = fdtype.combine(fdtype.FLOAT, fdtype.new((("i", 4),)), fdtype.new((("f", self.n_faces),))) else: - dtype_data = fdtype.combine(fdtype.FLOAT, fdtype.new((('i', 4),))) + dtype_data = fdtype.combine(fdtype.FLOAT, fdtype.new((("i", 4),))) load_times = self.n_t == -1 if load_times: - self._parent_slice.n_t = (os.stat(self.file_path).st_size - 2 * fdtype.INT.itemsize) // dtype_data.itemsize + self._parent_slice.n_t = ( + os.stat(self.file_path).st_size - 2 * fdtype.INT.itemsize + ) // dtype_data.itemsize self._parent_slice.times = np.empty(self.n_t) self._data = np.empty((self.n_t, self.n_faces), dtype=np.float32) @@ -101,8 +99,7 @@ def _load_data(self): @property def data(self) -> np.ndarray: - """Method to lazy load the geomslice's data. - """ + """Method to lazy load the geomslice's data.""" if not hasattr(self, "_data"): _ = self.vertices # Make sure geom data has been loaded already self._load_data() @@ -110,16 +107,14 @@ def data(self) -> np.ndarray: @property def vertices(self) -> np.ndarray: - """Method to lazy load the geomslice's data. - """ + """Method to lazy load the geomslice's data.""" if not hasattr(self, "_vertices"): self._load_geom_data() return self._vertices @property def faces(self) -> np.ndarray: - """Method to lazy load the geomslice's data. - """ + """Method to lazy load the geomslice's data.""" if not hasattr(self, "_faces"): self._load_geom_data() return self._faces @@ -148,8 +143,7 @@ def vmax(self): return np.max(self.data) def clear_cache(self): - """Remove all data from the internal cache that has been loaded so far to free memory. - """ + """Remove all data from the internal cache that has been loaded so far to free memory.""" if hasattr(self, "_data"): del self._data # del self._vector_data @@ -195,7 +189,9 @@ def __init__(self, root_path: str, geomslice_id: str, times: np.ndarray, multime for mesh_data in multimesh_data: if mesh_data["mesh"].id not in self._subgeomslices: self.quantity = Quantity(mesh_data["quantity"], mesh_data["short_name"], mesh_data["unit"]) - self._subgeomslices[mesh_data["mesh"].id] = SubGeomSlice(self, mesh_data["filename"], mesh_data["geomfilename"], mesh_data["extent"], mesh_data["mesh"]) + self._subgeomslices[mesh_data["mesh"].id] = SubGeomSlice( + self, mesh_data["filename"], mesh_data["geomfilename"], mesh_data["extent"], mesh_data["mesh"] + ) # if "-VELOCITY" in mesh_data["quantity"]: # vector_temp[mesh_data["mesh"]][mesh_data["quantity"]] = mesh_data["filename"] @@ -222,12 +218,14 @@ def __init__(self, root_path: str, geomslice_id: str, times: np.ndarray, multime # del self._subgeomslices[mesh] # vals = self._subgeomslices.values() - self.extent = Extent(min(vals, key=lambda e: e.extent.x_start).extent.x_start, - max(vals, key=lambda e: e.extent.x_end).extent.x_end, - min(vals, key=lambda e: e.extent.y_start).extent.y_start, - max(vals, key=lambda e: e.extent.y_end).extent.y_end, - min(vals, key=lambda e: e.extent.z_start).extent.z_start, - max(vals, key=lambda e: e.extent.z_end).extent.z_end) + self.extent = Extent( + min(vals, key=lambda e: e.extent.x_start).extent.x_start, + max(vals, key=lambda e: e.extent.x_end).extent.x_end, + min(vals, key=lambda e: e.extent.y_start).extent.y_start, + max(vals, key=lambda e: e.extent.y_end).extent.y_end, + min(vals, key=lambda e: e.extent.z_start).extent.z_start, + max(vals, key=lambda e: e.extent.z_end).extent.z_end, + ) if self.extent.x_start == self.extent.x_end: self.orientation = 1 @@ -245,17 +243,17 @@ def __init__(self, root_path: str, geomslice_id: str, times: np.ndarray, multime def get_subgeomslice(self, key: Union[int, str, Mesh]) -> SubGeomSlice: """Returns the :class:`SubGeomSlice` that cuts through the given mesh. When an int is - provided the nth SubGeomSlice will be returned. + provided the nth SubGeomSlice will be returned. """ return self[key] def __getitem__(self, key: Union[int, str, Mesh]) -> SubGeomSlice: """Returns the :class:`SubGeomSlice` that cuts through the given mesh. When an int is - provided the nth SubGeomSlice will be returned. + provided the nth SubGeomSlice will be returned. """ - if type(key) == int: + if isinstance(key, int): return tuple(self._subgeomslices.values())[key] - if type(key) == str: + if isinstance(key, str): return self._subgeomslices[key] return self._subgeomslices[key.id] @@ -264,8 +262,7 @@ def __len__(self): @property def subgeomslices(self) -> List[SubGeomSlice]: - """Get a list with all SubGeomSlices. - """ + """Get a list with all SubGeomSlices.""" return list(self._subgeomslices.values()) # @property @@ -283,11 +280,11 @@ def subgeomslices(self) -> List[SubGeomSlice]: # return 'x', 'y' def get_nearest_timestep(self, time: float) -> int: - """Calculates the nearest timestep for which data has been output for this geomslice. - """ + """Calculates the nearest timestep for which data has been output for this geomslice.""" idx = np.searchsorted(self.times, time, side="left") - if time > 0 and (idx == len(self.times) or math.fabs( - time - self.times[idx - 1]) < math.fabs(time - self.times[idx])): + if time > 0 and ( + idx == len(self.times) or math.fabs(time - self.times[idx - 1]) < math.fabs(time - self.times[idx]) + ): return idx - 1 else: return idx @@ -305,8 +302,7 @@ def get_nearest_timestep(self, time: float) -> int: @property def meshes(self) -> List[Mesh]: - """Returns a list of all meshes this geomslice cuts through. - """ + """Returns a list of all meshes this geomslice cuts through.""" return [subgeomslc.mesh for subgeomslc in self._subgeomslices] # @property @@ -337,8 +333,7 @@ def vmax(self): return np.max(self) def clear_cache(self): - """Remove all data from the internal cache that has been loaded so far to free memory. - """ + """Remove all data from the internal cache that has been loaded so far to free memory.""" for subgeomslice in self._subgeomslices.values(): subgeomslice.clear_cache() @@ -350,7 +345,7 @@ def vertices(self): counter = 0 for subgeomslice in self._subgeomslices.values(): size = subgeomslice.vertices.shape[0] - vertices[:, counter:counter+size] = subgeomslice.vertices + vertices[:, counter : counter + size] = subgeomslice.vertices counter += size return vertices @@ -363,7 +358,7 @@ def faces(self): counter = 0 for subgeomslice in self._subgeomslices.values(): size = subgeomslice.faces.shape[0] - faces[:, counter:counter + size] = subgeomslice.faces + faces[:, counter : counter + size] = subgeomslice.faces counter += size return faces @@ -376,7 +371,7 @@ def data(self): counter = 0 for subgeomslice in self._subgeomslices.values(): size = subgeomslice.data.shape[1] - data[:, counter:counter + size] = subgeomslice.data + data[:, counter : counter + size] = subgeomslice.data counter += size return data @@ -409,12 +404,12 @@ def std(self): return np.sqrt(sum / N) def __array__(self): - """Method that will be called by numpy when trying to convert the object to a numpy ndarray. - """ + """Method that will be called by numpy when trying to convert the object to a numpy ndarray.""" raise UserWarning( "Slices can not be converted to numpy arrays, but they support all typical numpy" " operations such as np.multiply. If a 'global' array containing all subgeomslices is" - " required, use the 'to_global' method and use the returned numpy-array explicitly.") + " required, use the 'to_global' method and use the returned numpy-array explicitly." + ) def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): """Method that will be called by numpy when using a ufunction with a GeomSlice as input. @@ -425,7 +420,9 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): logging.warning( "The %s method has been used which is not explicitly implemented. Correctness of" " results is not guaranteed. If you require this feature to be implemented please" - " submit an issue on Github where you explain your use case.", method) + " submit an issue on Github where you explain your use case.", + method, + ) input_list = list(inputs) for i, inp in enumerate(inputs): if isinstance(inp, self.__class__): @@ -434,7 +431,8 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): raise UserWarning( f"The {method} operation is not implemented for multiple geomslices as input yet. If" " you require this feature, please request this functionality by submitting an" - " issue on Github.") + " issue on Github." + ) new_slice = deepcopy(self) for subgeomslice in new_slice._subgeomslices.values(): @@ -455,10 +453,12 @@ def __array_function__(self, func, types, args, kwargs): def __repr__(self): # if self.type == '3D': # 3D-Slice - # return f"GeomSlice([3D] quantity={self.quantity}, cell_centered={self.cell_centered}, extent={self.extent})" + # return (f"GeomSlice([3D] quantity={self.quantity}, cell_centered={self.cell_centered}, " + # f"extent={self.extent})") # else: # 2D-Slice - # return f"GeomSlice([2D] quantity={self.quantity}, cell_centered={self.cell_centered}, extent={self.extent}, " \ - # f"extent_dirs={self.extent_dirs}, orientation={self.orientation})" + # return (f"GeomSlice([2D] quantity={self.quantity}, cell_centered={self.cell_centered}, " + # f"extent={self.extent}, extent_dirs={self.extent_dirs}, orientation={self.orientation})") return f"GeomSlice(quantity={self.quantity})" + # __array_function__ implementations diff --git a/fdsreader/slcf/geomslice_collection.py b/fdsreader/slcf/geomslice_collection.py index c8c733a7..8b854f53 100644 --- a/fdsreader/slcf/geomslice_collection.py +++ b/fdsreader/slcf/geomslice_collection.py @@ -8,7 +8,7 @@ class GeomSliceCollection(FDSDataCollection): """Collection of :class:`GeomSlice` objects. Offers extensive functionality for filtering and - using geomslices as well as its subclasses such as :class:`SubSlice`. + using geomslices as well as its subclasses such as :class:`SubSlice`. """ def __init__(self, *geomslices: Iterable[GeomSlice]): @@ -19,21 +19,22 @@ def quantities(self) -> List[Quantity]: return list({slc.name for slc in self}) def filter_by_quantity(self, quantity: Union[str, Quantity]): - """Filters all geomslices by a specific quantity. - """ - if type(quantity) == Quantity: + """Filters all geomslices by a specific quantity.""" + if isinstance(quantity, Quantity): quantity = quantity.name - return GeomSliceCollection(x for x in self if x.quantity.name.lower() == quantity.lower() - or x.quantity.short_name.lower() == quantity.lower()) + return GeomSliceCollection( + x + for x in self + if x.quantity.name.lower() == quantity.lower() or x.quantity.short_name.lower() == quantity.lower() + ) def get_by_id(self, geomslice_id: str): - """Get the geomslice with corresponding id if it exists. - """ + """Get the geomslice with corresponding id if it exists.""" return next((slc for slc in self if slc.id == geomslice_id), None) def get_nearest(self, x: float = None, y: float = None, z: float = None) -> GeomSlice: """Filters the geomslice with the shortest distance to the given point. - If there are multiple geomslices with the same distance, a random one will be selected. + If there are multiple geomslices with the same distance, a random one will be selected. """ d_min = np.finfo(float).max geomslices_min = list() @@ -48,15 +49,15 @@ def get_nearest(self, x: float = None, y: float = None, z: float = None) -> Geom geomslices_min.append(slc) if x is not None: - geomslices_min.sort(key=lambda slc: (slc.extent.x_end - slc.extent.x_start)) + geomslices_min.sort(key=lambda slc: slc.extent.x_end - slc.extent.x_start) if y is not None: - geomslices_min.sort(key=lambda slc: (slc.extent.y_end - slc.extent.y_start)) + geomslices_min.sort(key=lambda slc: slc.extent.y_end - slc.extent.y_start) if z is not None: - geomslices_min.sort(key=lambda slc: (slc.extent.z_end - slc.extent.z_start)) + geomslices_min.sort(key=lambda slc: slc.extent.z_end - slc.extent.z_start) if len(geomslices_min) > 0: return geomslices_min[0] return None def __repr__(self): - return "GeomSliceCollection(" + super(GeomSliceCollection, self).__repr__() + ")" + return "GeomSliceCollection(" + super().__repr__() + ")" diff --git a/fdsreader/slcf/slice.py b/fdsreader/slcf/slice.py index 394f8045..d0f479c2 100644 --- a/fdsreader/slcf/slice.py +++ b/fdsreader/slcf/slice.py @@ -1,23 +1,22 @@ +import logging import math import os from copy import deepcopy +from typing import Collection, Dict, List, Tuple, Union import numpy as np -import logging -from typing import Dict, Collection, Tuple, Union, List from typing_extensions import Literal -from fdsreader.fds_classes import Mesh -from fdsreader.utils import Dimension, Quantity, Extent -from fdsreader import settings import fdsreader.utils.fortran_data as fdtype +from fdsreader import settings +from fdsreader.fds_classes import Mesh +from fdsreader.utils import Dimension, Extent, Quantity _HANDLED_FUNCTIONS = {} def implements(np_function): - """Decorator to register an __array_function__ implementation for Slices. - """ + """Decorator to register an __array_function__ implementation for Slices.""" def decorator(func): _HANDLED_FUNCTIONS[np_function] = func @@ -34,7 +33,7 @@ class SubSlice: :ivar dimension: :class:`Dimension` object containing information about steps in each dimension. """ - _offset = 3 * fdtype.new((('c', 30),)).itemsize + fdtype.new((('i', 6),)).itemsize + _offset = 3 * fdtype.new((("c", 30),)).itemsize + fdtype.new((("i", 6),)).itemsize def __init__(self, parent_slc, filename: str, dimension: Dimension, extent: Extent, mesh: Mesh): self._parent_slice = parent_slc @@ -47,17 +46,16 @@ def __init__(self, parent_slc, filename: str, dimension: Dimension, extent: Exte self.vector_filenames = dict() self._vector_data = dict() - def get_coordinates(self, ignore_cell_centered: bool = False) -> Dict[ - Literal['x', 'y', 'z'], np.ndarray]: + def get_coordinates(self, ignore_cell_centered: bool = False) -> Dict[Literal["x", "y", "z"], np.ndarray]: """Returns a dictionary containing a numpy ndarray with coordinates for each dimension. - For cell-centered slices, the coordinates can be adjusted to represent cell-centered coordinates. + For cell-centered slices, the coordinates can be adjusted to represent cell-centered coordinates. - :param ignore_cell_centered: Whether to shift the coordinates when the slice is cell_centered or not. + :param ignore_cell_centered: Whether to shift the coordinates when the slice is cell_centered or not. """ # orientation = ('x', 'y', 'z')[self.orientation - 1] if self.orientation != 0 else '' # coords = {'x': set(), 'y': set(), 'z': set()} - coords: Dict[Literal['x', 'y', 'z'], np.ndarray] = {} - for dim in ('x', 'y', 'z'): + coords: Dict[Literal["x", "y", "z"], np.ndarray] = {} + for dim in ("x", "y", "z"): co = self.mesh.coordinates[dim].copy() # In case the slice is cell-centered, we will shift the coordinates by half a cell # and remove the last coordinate @@ -65,8 +63,7 @@ def get_coordinates(self, ignore_cell_centered: bool = False) -> Dict[ co = co[:-1] co += abs(co[1] - co[0]) / 2 - coords[dim] = co[ - np.where(np.logical_and(co >= self.extent[dim][0], co <= self.extent[dim][1]))] + coords[dim] = co[np.where(np.logical_and(co >= self.extent[dim][0], co <= self.extent[dim][1]))] if coords[dim].size == 0: coords[dim] = np.array([co[np.argmin(np.abs(co - self.extent[dim][0]))]]) @@ -74,8 +71,7 @@ def get_coordinates(self, ignore_cell_centered: bool = False) -> Dict[ @property def shape(self) -> Tuple[int, int]: - """2D-shape of the slice. - """ + """2D-shape of the slice.""" shape = self.dimension.shape(cell_centered=self.cell_centered) if self.orientation != 0: if len(shape) < 2: @@ -85,14 +81,12 @@ def shape(self) -> Tuple[int, int]: @property def orientation(self) -> Literal[0, 1, 2, 3]: - """Orientation [1,2,3] of the slice in case it is 2D, 0 otherwise. - """ + """Orientation [1,2,3] of the slice in case it is 2D, 0 otherwise.""" return self._parent_slice.orientation @property def cell_centered(self) -> bool: - """Indicates whether centered positioning for data is used. - """ + """Indicates whether centered positioning for data is used.""" return self._parent_slice.cell_centered @property @@ -101,27 +95,25 @@ def times(self): @property def n_t(self) -> int: - """Get the number of timesteps for which data was output. - """ + """Get the number of timesteps for which data was output.""" return self._parent_slice.n_t def _load_data(self, file_path: str, data_out: np.ndarray): # Both cases (cell_centered True/False) output the same number of data points n = self.dimension.size(cell_centered=False) - dtype_data = fdtype.combine(fdtype.FLOAT, fdtype.new((('f', n),))) + dtype_data = fdtype.combine(fdtype.FLOAT, fdtype.new((("f", n),))) load_times = self.n_t == -1 if load_times: - self._parent_slice.n_t = (os.stat( - file_path).st_size - self._offset) // dtype_data.itemsize + self._parent_slice.n_t = (os.stat(file_path).st_size - self._offset) // dtype_data.itemsize self._parent_slice.times = np.empty(self.n_t) - with open(file_path, 'rb') as infile: + with open(file_path, "rb") as infile: infile.seek(self._offset) for t, data in enumerate(fdtype.read(infile, dtype_data, self.n_t)): if load_times: self.times[t] = data[0][0] - data = data[1].reshape(self.dimension.shape(cell_centered=False), order='F') + data = data[1].reshape(self.dimension.shape(cell_centered=False), order="F") if self.cell_centered: data_out[t, :] = data[1:, 1:] # Ignore ghost points else: @@ -130,14 +122,14 @@ def _load_data(self, file_path: str, data_out: np.ndarray): def _load_times(self) -> np.ndarray: # Read in (only) the times for which SLCF data is available n = self.dimension.size(cell_centered=False) - dtype_data = fdtype.combine(fdtype.FLOAT, fdtype.new((('f', n),))) + dtype_data = fdtype.combine(fdtype.FLOAT, fdtype.new((("f", n),))) file_path = os.path.join(self._parent_slice._root_path, self.filename) n_t = (os.stat(file_path).st_size - self._offset) // dtype_data.itemsize times = np.empty(n_t) - with open(file_path, 'rb') as infile: + with open(file_path, "rb") as infile: infile.seek(self._offset) for t, data in enumerate(fdtype.read(infile, dtype_data, n_t)): times[t] = data[0][0] @@ -146,8 +138,7 @@ def _load_times(self) -> np.ndarray: @property def data(self) -> np.ndarray: - """Method to lazy load the slice's data. - """ + """Method to lazy load the slice's data.""" if not hasattr(self, "_data"): file_path = os.path.join(self._parent_slice._root_path, self.filename) self._data = np.empty((self.n_t,) + self.shape, dtype=np.float32) @@ -156,16 +147,13 @@ def data(self) -> np.ndarray: @property def vector_data(self) -> Dict[str, np.ndarray]: - """Method to lazy load the slice's vector data if it exists. - """ + """Method to lazy load the slice's vector data if it exists.""" if not hasattr(self, "_vector_data"): raise AttributeError("There is no vector data available for this slice.") if len(self._vector_data) == 0: for direction in self.vector_filenames.keys(): - file_path = os.path.join(self._parent_slice._root_path, - self.vector_filenames[direction]) - self._vector_data[direction] = np.empty((self.n_t,) + self.shape, - dtype=np.float32) + file_path = os.path.join(self._parent_slice._root_path, self.vector_filenames[direction]) + self._vector_data[direction] = np.empty((self.n_t,) + self.shape, dtype=np.float32) self._load_data(file_path, self._vector_data[direction]) return self._vector_data @@ -178,8 +166,7 @@ def vmax(self): return np.max(self.data) def clear_cache(self): - """Remove all data from the internal cache that has been loaded so far to free memory. - """ + """Remove all data from the internal cache that has been loaded so far to free memory.""" if hasattr(self, "_data"): del self._data del self._vector_data @@ -206,8 +193,7 @@ class Slice(np.lib.mixins.NDArrayOperatorsMixin): :ivar extent: :class:`Extent` object containing 3-dimensional extent information. """ - def __init__(self, root_path: str, slice_id: str, cell_centered: bool, - multimesh_data: Collection[Dict]): + def __init__(self, root_path: str, slice_id: str, cell_centered: bool, multimesh_data: Collection[Dict]): self._root_path = root_path self.cell_centered = cell_centered @@ -223,12 +209,10 @@ def __init__(self, root_path: str, slice_id: str, cell_centered: bool, for mesh_data in multimesh_data: if mesh_data["mesh"].id not in self._subslices: - self.quantity = Quantity(mesh_data["quantity"], mesh_data["short_name"], - mesh_data["unit"]) - self._subslices[mesh_data["mesh"].id] = SubSlice(self, mesh_data["filename"], - mesh_data["dimension"], - mesh_data["extent"], - mesh_data["mesh"]) + self.quantity = Quantity(mesh_data["quantity"], mesh_data["short_name"], mesh_data["unit"]) + self._subslices[mesh_data["mesh"].id] = SubSlice( + self, mesh_data["filename"], mesh_data["dimension"], mesh_data["extent"], mesh_data["mesh"] + ) if "-VELOCITY" in mesh_data["quantity"]: vector_temp[mesh_data["mesh"].id][mesh_data["quantity"]] = mesh_data["filename"] @@ -256,21 +240,22 @@ def __init__(self, root_path: str, slice_id: str, cell_centered: bool, orientations.append(0) self.orientation = 0 if any(o != orientations[0] for o in orientations) else orientations[0] - x_start, x_end, y_start, y_end, z_start, z_end = min(subslices, key=lambda - e: e.extent.x_start).extent.x_start, \ - max(subslices, key=lambda - e: e.extent.x_end).extent.x_end, \ - min(subslices, key=lambda - e: e.extent.y_start).extent.y_start, \ - max(subslices, key=lambda - e: e.extent.y_end).extent.y_end, \ - min(subslices, key=lambda - e: e.extent.z_start).extent.z_start, \ - max(subslices, key=lambda - e: e.extent.z_end).extent.z_end - self.extent = Extent(x_start, x_start if self.orientation == 1 else x_end, - y_start, y_start if self.orientation == 2 else y_end, - z_start, z_start if self.orientation == 3 else z_end) + x_start, x_end, y_start, y_end, z_start, z_end = ( + min(subslices, key=lambda e: e.extent.x_start).extent.x_start, + max(subslices, key=lambda e: e.extent.x_end).extent.x_end, + min(subslices, key=lambda e: e.extent.y_start).extent.y_start, + max(subslices, key=lambda e: e.extent.y_end).extent.y_end, + min(subslices, key=lambda e: e.extent.z_start).extent.z_start, + max(subslices, key=lambda e: e.extent.z_end).extent.z_end, + ) + self.extent = Extent( + x_start, + x_start if self.orientation == 1 else x_end, + y_start, + y_start if self.orientation == 2 else y_end, + z_start, + z_start if self.orientation == 3 else z_end, + ) # Read in the available time steps, using arbitrary the first sub-slice self.times = self.subslices[0]._load_times() @@ -283,17 +268,17 @@ def __init__(self, root_path: str, slice_id: str, cell_centered: bool, def get_subslice(self, key: Union[int, str, Mesh]) -> SubSlice: """Returns the :class:`SubSlice` that cuts through the given mesh. When an int is provided - the nth SubSlice will be returned. + the nth SubSlice will be returned. """ return self[key] def __getitem__(self, key: Union[int, str, Mesh]) -> SubSlice: """Returns the :class:`SubSlice` that cuts through the given mesh. When an int is provided - the nth SubSlice will be returned. + the nth SubSlice will be returned. """ - if type(key) == int: + if isinstance(key, int): return tuple(self._subslices.values())[key] - elif type(key) == str: + elif isinstance(key, str): key = tuple(mesh for mesh in self.meshes if mesh.id == key)[0] return self._subslices[key.id] @@ -302,62 +287,55 @@ def __len__(self): @property def subslices(self) -> List[SubSlice]: - """Get a list with all SubSlices. - """ + """Get a list with all SubSlices.""" return list(self._subslices.values()) @property - def extent_dirs(self) -> Tuple[ - Literal['x', 'y', 'z'], Literal['x', 'y', 'z'], Literal['x', 'y', 'z']]: - """The directions in which there is an extent. All three dimensions in case the slice is 3D. - """ + def extent_dirs(self) -> Tuple[Literal["x", "y", "z"], Literal["x", "y", "z"], Literal["x", "y", "z"]]: + """The directions in which there is an extent. All three dimensions in case the slice is 3D.""" ior = self.orientation if ior == 0: - return 'x', 'y', 'z' + return "x", "y", "z" elif ior == 1: - return 'y', 'z' + return "y", "z" elif ior == 2: - return 'x', 'z' + return "x", "z" else: - return 'x', 'y' + return "x", "y" def get_nearest_timestep(self, time: float) -> int: - """Calculates the nearest timestep for which data has been output for this slice. - """ + """Calculates the nearest timestep for which data has been output for this slice.""" idx = np.searchsorted(self.times, time, side="left") - if time > 0 and (idx == len(self.times) or math.fabs( - time - self.times[idx - 1]) < math.fabs(time - self.times[idx])): + if time > 0 and ( + idx == len(self.times) or math.fabs(time - self.times[idx - 1]) < math.fabs(time - self.times[idx]) + ): return idx - 1 else: return idx - def get_nearest_index(self, dimension: Literal['x', 'y', 'z'], value: float) -> int: - """Get the nearest mesh coordinate index in a specific dimension. - """ + def get_nearest_index(self, dimension: Literal["x", "y", "z"], value: float) -> int: + """Get the nearest mesh coordinate index in a specific dimension.""" coords = self.get_coordinates()[dimension] idx = np.searchsorted(coords, value, side="left") - if idx > 0 and (idx == coords.size or math.fabs(value - coords[idx - 1]) < math.fabs( - value - coords[idx])): + if idx > 0 and (idx == coords.size or math.fabs(value - coords[idx - 1]) < math.fabs(value - coords[idx])): return idx - 1 else: return idx @property def meshes(self) -> List[Mesh]: - """Returns a list of all meshes this slice cuts through. - """ + """Returns a list of all meshes this slice cuts through.""" return [subslc.mesh for subslc in self._subslices.values()] - def get_coordinates(self, ignore_cell_centered: bool = False) -> Dict[ - Literal['x', 'y', 'z'], np.ndarray]: + def get_coordinates(self, ignore_cell_centered: bool = False) -> Dict[Literal["x", "y", "z"], np.ndarray]: """Returns a dictionary containing a numpy ndarray with coordinates for each dimension. - For cell-centered slices, the coordinates can be adjusted to represent cell-centered coordinates. + For cell-centered slices, the coordinates can be adjusted to represent cell-centered coordinates. - :param ignore_cell_centered: Whether to shift the coordinates when the slice is cell_centered or not. + :param ignore_cell_centered: Whether to shift the coordinates when the slice is cell_centered or not. """ - orientation = ('x', 'y', 'z')[self.orientation - 1] if self.orientation != 0 else '' - coords = {'x': list(), 'y': list(), 'z': list()} - for dim in ('x', 'y', 'z'): + orientation = ("x", "y", "z")[self.orientation - 1] if self.orientation != 0 else "" + coords = {"x": list(), "y": list(), "z": list()} + for dim in ("x", "y", "z"): if orientation == dim: coords[dim] = np.array([self.extent[dim][0]]) continue @@ -370,8 +348,7 @@ def get_coordinates(self, ignore_cell_centered: bool = False) -> Dict[ # floating point inaccuraciesas floats get written out with limited precision from FDS # (sometimes only 6 decimal places, e.g. 1.0 and 1.000001) diff = coords[dim][1:] - coords[dim][:-1] - coords[dim] = np.concatenate((coords[dim][:1], coords[dim][1:][diff > 0.000002]), - axis=0) + coords[dim] = np.concatenate((coords[dim][:1], coords[dim][1:][diff > 0.000002]), axis=0) if len(coords[dim]) == 0: slice_coordinate = self.extent[dim][0] @@ -379,9 +356,11 @@ def get_coordinates(self, ignore_cell_centered: bool = False) -> Dict[ for mesh in self._subslices.keys(): mesh_coords = mesh.coordinates[dim] idx = np.searchsorted(mesh_coords, slice_coordinate, side="left") - if idx > 0 and (idx == mesh_coords.size or math.fabs( - slice_coordinate - mesh_coords[idx - 1]) < math.fabs( - slice_coordinate - mesh_coords[idx])): + if idx > 0 and ( + idx == mesh_coords.size + or math.fabs(slice_coordinate - mesh_coords[idx - 1]) + < math.fabs(slice_coordinate - mesh_coords[idx]) + ): idx = idx + 1 if mesh_coords[idx] - slice_coordinate < nearest_coordinate - slice_coordinate: nearest_coordinate = mesh_coords[idx] @@ -398,12 +377,11 @@ def vmax(self): return np.max(self) def clear_cache(self): - """Remove all data from the internal cache that has been loaded so far to free memory. - """ + """Remove all data from the internal cache that has been loaded so far to free memory.""" for subslice in self._subslices.values(): subslice.clear_cache() - def get_2d_slice_from_3d(self, slice_direction: Literal['x', 'y', 'z', 1, 2, 3], value: float): + def get_2d_slice_from_3d(self, slice_direction: Literal["x", "y", "z", 1, 2, 3], value: float): """Creates a 2D-Slice from a 3D-Slice by slicing the Slice. :param slice_direction: The direction in which to cut through the slice. :param value: The position at which to start cutting through the slice. @@ -411,24 +389,23 @@ def get_2d_slice_from_3d(self, slice_direction: Literal['x', 'y', 'z', 1, 2, 3], if self.type == "2D": logging.error("Trying to slice a 2D-slice, which might not yield the desired result.") - if type(slice_direction) == str: - slice_dim = {'x': 1, 'y': 2, 'z': 3}[slice_direction] + if isinstance(slice_direction, str): + slice_dim = {"x": 1, "y": 2, "z": 3}[slice_direction] else: slice_dim = slice_direction - slice_direction = ('x', 'y', 'z')[slice_dim - 1] + slice_direction = ("x", "y", "z")[slice_dim - 1] new_slice = deepcopy(self) to_remove = list() for mesh_id, subslc in new_slice._subslices.items(): mesh = subslc.mesh # Check if the new slice cuts through the subslice - cut_index = mesh.coordinate_to_index((value,), (slice_direction,), - cell_centered=self.cell_centered) - cut_value = mesh.get_nearest_coordinate((value,), (slice_direction,), - cell_centered=self.cell_centered)[0] - if mesh.coordinates[slice_direction][0] <= value <= mesh.coordinates[slice_direction][ - -1] and \ - subslc.extent[slice_dim][0] <= cut_value <= subslc.extent[slice_dim][1]: + cut_index = mesh.coordinate_to_index((value,), (slice_direction,), cell_centered=self.cell_centered) + cut_value = mesh.get_nearest_coordinate((value,), (slice_direction,), cell_centered=self.cell_centered)[0] + if ( + mesh.coordinates[slice_direction][0] <= value <= mesh.coordinates[slice_direction][-1] + and subslc.extent[slice_dim][0] <= cut_value <= subslc.extent[slice_dim][1] + ): shape = subslc.dimension.shape(cell_centered=self.cell_centered) indices = [slice(None)] @@ -440,11 +417,13 @@ def get_2d_slice_from_3d(self, slice_direction: Literal['x', 'y', 'z', 1, 2, 3], subslc._data = np.squeeze(subslc.data[tuple(indices)], axis=slice_dim) for direction in subslc.vector_filenames.keys(): - subslc._vector_data[direction] = np.squeeze(subslc.vector_data[direction][tuple(indices)], axis=slice_dim) + subslc._vector_data[direction] = np.squeeze( + subslc.vector_data[direction][tuple(indices)], axis=slice_dim + ) # Change 3D extent to 2D one new_extent = subslc.extent.as_list() - new_extent[(slice_dim - 1) * 2:slice_dim * 2] = (cut_value, cut_value) + new_extent[(slice_dim - 1) * 2 : slice_dim * 2] = (cut_value, cut_value) subslc.extent = Extent(*new_extent) else: to_remove.append(mesh_id) @@ -454,22 +433,27 @@ def get_2d_slice_from_3d(self, slice_direction: Literal['x', 'y', 'z', 1, 2, 3], new_slice.orientation = slice_dim subslices = new_slice._subslices.values() - x_start, x_end, y_start, y_end, z_start, z_end = \ - min(subslices, key=lambda e: e.extent.x_start).extent.x_start, \ - max(subslices, key=lambda e: e.extent.x_end).extent.x_end, \ - min(subslices, key=lambda e: e.extent.y_start).extent.y_start, \ - max(subslices, key=lambda e: e.extent.y_end).extent.y_end, \ - min(subslices, key=lambda e: e.extent.z_start).extent.z_start, \ - max(subslices, key=lambda e: e.extent.z_end).extent.z_end - new_slice.extent = Extent(x_start, x_start if new_slice.orientation == 1 else x_end, - y_start, y_start if new_slice.orientation == 2 else y_end, - z_start, z_start if new_slice.orientation == 3 else z_end) + x_start, x_end, y_start, y_end, z_start, z_end = ( + min(subslices, key=lambda e: e.extent.x_start).extent.x_start, + max(subslices, key=lambda e: e.extent.x_end).extent.x_end, + min(subslices, key=lambda e: e.extent.y_start).extent.y_start, + max(subslices, key=lambda e: e.extent.y_end).extent.y_end, + min(subslices, key=lambda e: e.extent.z_start).extent.z_start, + max(subslices, key=lambda e: e.extent.z_end).extent.z_end, + ) + new_slice.extent = Extent( + x_start, + x_start if new_slice.orientation == 1 else x_end, + y_start, + y_start if new_slice.orientation == 2 else y_end, + z_start, + z_start if new_slice.orientation == 3 else z_end, + ) return new_slice def sort_subslices_cartesian(self): - """Returns all subslices sorted in cartesian coordinates. - """ + """Returns all subslices sorted in cartesian coordinates.""" slices = list(self._subslices.values()) slices_cart = [[slices[0]]] orientation = abs(slices[0].orientation) @@ -504,36 +488,42 @@ def sort_subslices_cartesian(self): slices_cart.append([slc]) return slices_cart - def to_global(self, masked: bool = False, fill: float = 0, return_coordinates: bool = False) -> \ - Union[np.ndarray, Tuple[np.ndarray, np.ndarray], Tuple[ - np.ndarray, Dict[Literal['x', 'y', 'z'], np.ndarray]], Tuple[ - Tuple[np.ndarray, np.ndarray], Tuple[Dict[Literal['x', 'y', 'z'], np.ndarray], Dict[ - Literal['x', 'y', 'z'], np.ndarray]]]]: + def to_global( + self, masked: bool = False, fill: float = 0, return_coordinates: bool = False + ) -> Union[ + np.ndarray, + Tuple[np.ndarray, np.ndarray], + Tuple[np.ndarray, Dict[Literal["x", "y", "z"], np.ndarray]], + Tuple[ + Tuple[np.ndarray, np.ndarray], + Tuple[Dict[Literal["x", "y", "z"], np.ndarray], Dict[Literal["x", "y", "z"], np.ndarray]], + ], + ]: """Creates a global numpy ndarray from all subslices (works for 2D- and 3D-slices). - Note: This method might create a sparse np-array that consumes lots of memory. - Attention: Two global slices are returned in cases where cell-centered slices cut right - through one or more mesh borders. If there is a cell-centered slice that cuts right - through the border of two meshes (mesh1 and mesh2), there will actually be two slices - that could be equally relevant for the user. The one will cells on mesh1 and the one - with cells on mesh2. As there are no cells in between (i.e. where the slice "should" be) - and cell-centered slices output values at the centers of each cell, FDS simply outputs - two slices, one on each side of the mesh borders. The fdsreader will not discard any - data, both slices that are output by FDS are sent to the user for him to decide which - one to use. - - :param masked: Whether to apply the obstruction mask to the slice or not. - :param fill: The fill value to use for masked slice entries. Only used when masked=True. - :param return_coordinates: If true, return the matching coordinate for each value on the generated grid. + Note: This method might create a sparse np-array that consumes lots of memory. + Attention: Two global slices are returned in cases where cell-centered slices cut right + through one or more mesh borders. If there is a cell-centered slice that cuts right + through the border of two meshes (mesh1 and mesh2), there will actually be two slices + that could be equally relevant for the user. The one will cells on mesh1 and the one + with cells on mesh2. As there are no cells in between (i.e. where the slice "should" be) + and cell-centered slices output values at the centers of each cell, FDS simply outputs + two slices, one on each side of the mesh borders. The fdsreader will not discard any + data, both slices that are output by FDS are sent to the user for him to decide which + one to use. + + :param masked: Whether to apply the obstruction mask to the slice or not. + :param fill: The fill value to use for masked slice entries. Only used when masked=True. + :param return_coordinates: If true, return the matching coordinate for each value on the generated grid. """ if len(self._subslices) == 0: if return_coordinates: - return np.array([]), {d: np.array([]) for d in ('x', 'y', 'z')} + return np.array([]), {d: np.array([]) for d in ("x", "y", "z")} else: return np.array([]) subslice_sets = [dict(), dict()] - dimension = ['x', 'y', 'z'][self.orientation - 1] + dimension = ["x", "y", "z"][self.orientation - 1] base_coord = next(iter(self._subslices.values())).get_coordinates(ignore_cell_centered=False)[dimension][0] for mesh, slc in self._subslices.items(): @@ -546,67 +536,69 @@ def to_global(self, masked: bool = False, fill: float = 0, return_coordinates: b subslice_sets.pop(1) first_result_grid = None + first_result_coordinates = None for subslices in subslice_sets: - coord_min = {'x': math.inf, 'y': math.inf, 'z': math.inf} - coord_max = {'x': -math.inf, 'y': -math.inf, 'z': -math.inf} - for dim in ('x', 'y', 'z'): + coord_min = {"x": math.inf, "y": math.inf, "z": math.inf} + coord_max = {"x": -math.inf, "y": -math.inf, "z": -math.inf} + for dim in ("x", "y", "z"): for slc in subslices.values(): co = slc.mesh.coordinates[dim] - co = co[np.where( - np.logical_and(co >= slc.extent[dim][0], co <= slc.extent[dim][1]))] + co = co[np.where(np.logical_and(co >= slc.extent[dim][0], co <= slc.extent[dim][1]))] coord_min[dim] = min(co[0], coord_min[dim]) coord_max[dim] = max(co[-1], coord_max[dim]) # The global grid will use the finest mesh as base and duplicate values of the coarser # meshes. Therefore, we first find the finest mesh and calculate the step size in each # dimension. - step_sizes_min = {'x': coord_max['x'] - coord_min['x'], - 'y': coord_max['y'] - coord_min['y'], - 'z': coord_max['z'] - coord_min['z']} - step_sizes_max = {'x': 0, 'y': 0, 'z': 0} + step_sizes_min = { + "x": coord_max["x"] - coord_min["x"], + "y": coord_max["y"] - coord_min["y"], + "z": coord_max["z"] - coord_min["z"], + } + step_sizes_max = {"x": 0, "y": 0, "z": 0} steps = dict() - global_max = {'x': -math.inf, 'y': -math.inf, 'z': -math.inf} + global_max = {"x": -math.inf, "y": -math.inf, "z": -math.inf} - for dim in ('x', 'y', 'z'): + for dim in ("x", "y", "z"): for sslc in subslices.values(): step_size = sslc.mesh.coordinates[dim][1] - sslc.mesh.coordinates[dim][0] step_sizes_min[dim] = min(step_size, step_sizes_min[dim]) step_sizes_max[dim] = max(step_size, step_sizes_max[dim]) global_max[dim] = max(sslc.mesh.coordinates[dim][-1], global_max[dim]) - for dim in ('x', 'y', 'z'): + for dim in ("x", "y", "z"): if step_sizes_min[dim] == 0: step_sizes_min[dim] = math.inf steps[dim] = 1 else: - steps[dim] = max( - int(round((coord_max[dim] - coord_min[dim]) / step_sizes_min[dim])), 1) + ( - 0 if self.cell_centered else 1) # + step_sizes_max[dim] / step_sizes_min[dim] + steps[dim] = max(int(round((coord_max[dim] - coord_min[dim]) / step_sizes_min[dim])), 1) + ( + 0 if self.cell_centered else 1 + ) # + step_sizes_max[dim] / step_sizes_min[dim] - grid = np.full((self.n_t, steps['x'], steps['y'], steps['z']), np.nan) + grid = np.full((self.n_t, steps["x"], steps["y"], steps["z"]), np.nan) start_idx = dict() end_idx = dict() for slc in subslices.values(): - slc_data = slc.data.copy() if slc.orientation == 0 else np.expand_dims(slc.data.copy(), - axis=slc.orientation) + slc_data = ( + slc.data.copy() if slc.orientation == 0 else np.expand_dims(slc.data.copy(), axis=slc.orientation) + ) if masked: mask = slc.mesh.get_obstruction_mask_slice(slc) for axis in (0, 1, 2): - dim = ('x', 'y', 'z')[axis] + dim = ("x", "y", "z")[axis] if axis == slc.orientation - 1: start_idx[dim] = 0 end_idx[dim] = 1 continue - n_repeat = max(int(round( - (slc.mesh.coordinates[dim][1] - slc.mesh.coordinates[dim][0]) / - step_sizes_min[dim])), 1) + n_repeat = max( + int(round((slc.mesh.coordinates[dim][1] - slc.mesh.coordinates[dim][0]) / step_sizes_min[dim])), + 1, + ) - start_idx[dim] = int(round( - (slc.mesh.coordinates[dim][0] - coord_min[dim]) / step_sizes_min[dim])) - end_idx[dim] = int(round( - (slc.mesh.coordinates[dim][-1] - coord_min[dim]) / step_sizes_min[dim])) + start_idx[dim] = int(round((slc.mesh.coordinates[dim][0] - coord_min[dim]) / step_sizes_min[dim])) + end_idx[dim] = int(round((slc.mesh.coordinates[dim][-1] - coord_min[dim]) / step_sizes_min[dim])) # We ignore border points unless they are actually on the border of the simulation space as all # other border points actually appear twice, as the subslices overlap. This only @@ -644,16 +636,21 @@ def to_global(self, masked: bool = False, fill: float = 0, return_coordinates: b if masked: slc_data = np.where(mask, slc_data, fill) - grid[:, start_idx['x']: end_idx['x'], start_idx['y']: end_idx['y'], - start_idx['z']: end_idx['z']] = slc_data.reshape( - (self.n_t, end_idx['x'] - start_idx['x'], end_idx['y'] - start_idx['y'], - end_idx['z'] - start_idx['z'])) + grid[:, start_idx["x"] : end_idx["x"], start_idx["y"] : end_idx["y"], start_idx["z"] : end_idx["z"]] = ( + slc_data.reshape( + ( + self.n_t, + end_idx["x"] - start_idx["x"], + end_idx["y"] - start_idx["y"], + end_idx["z"] - start_idx["z"], + ) + ) + ) if return_coordinates: coordinates = dict() - for dim_index, dim in enumerate(('x', 'y', 'z')): - coordinates[dim] = np.linspace(coord_min[dim], coord_max[dim], - grid.shape[dim_index + 1]) + for dim_index, dim in enumerate(("x", "y", "z")): + coordinates[dim] = np.linspace(coord_min[dim], coord_max[dim], grid.shape[dim_index + 1]) # Remove dimension corresponding to orientation for 2D slices if self.orientation != 0: @@ -677,10 +674,10 @@ def to_global(self, masked: bool = False, fill: float = 0, return_coordinates: b return first_result_grid @property - def type(self) -> Literal['2D', '3D']: + def type(self) -> Literal["2D", "3D"]: if self.orientation == 0: - return '3D' - return '2D' + return "3D" + return "2D" @implements(np.amin) def _min(self): @@ -705,18 +702,17 @@ def std(self): :returns: The calculated standard deviation. """ mean = self.mean - sum = np.sum( - [np.sum(np.power(subsclice.data - mean, 2)) for subsclice in self._subslices.values()]) + sum = np.sum([np.sum(np.power(subsclice.data - mean, 2)) for subsclice in self._subslices.values()]) N = np.sum([subsclice.data.size for subsclice in self._subslices.values()]) return np.sqrt(sum / N) def __array__(self): - """Method that will be called by numpy when trying to convert the object to a numpy ndarray. - """ + """Method that will be called by numpy when trying to convert the object to a numpy ndarray.""" raise UserWarning( "Slices can not be converted to numpy arrays, but they support all typical numpy" " operations such as np.multiply. If a 'global' array containing all subslices is" - " required, use the 'to_global' method and use the returned numpy-array explicitly.") + " required, use the 'to_global' method and use the returned numpy-array explicitly." + ) def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): """Method that will be called by numpy when using a ufunction with a Slice as input. @@ -727,7 +723,9 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): logging.warning( "The %s method has been used which is not explicitly implemented. Correctness of" " results is not guaranteed. If you require this feature to be implemented please" - " submit an issue on Github where you explain your use case.", method) + " submit an issue on Github where you explain your use case.", + method, + ) input_list = list(inputs) for i, inp in enumerate(inputs): if isinstance(inp, self.__class__): @@ -736,7 +734,8 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): raise UserWarning( f"The {method} operation is not implemented for multiple slices as input yet. If" " you require this feature, please request this functionality by submitting an" - " issue on Github.") + " issue on Github." + ) new_slice = deepcopy(self) for subslice in new_slice._subslices.values(): @@ -756,10 +755,13 @@ def __array_function__(self, func, types, args, kwargs): return _HANDLED_FUNCTIONS[func](*args, **kwargs) def __repr__(self): - if self.type == '3D': # 3D-Slice + if self.type == "3D": # 3D-Slice return f"Slice([3D] quantity={self.quantity}, cell_centered={self.cell_centered}, extent={self.extent})" else: # 2D-Slice - return f"Slice([2D] quantity={self.quantity}, cell_centered={self.cell_centered}, extent={self.extent}, " \ - f"extent_dirs={self.extent_dirs}, orientation={self.orientation})" + return ( + f"Slice([2D] quantity={self.quantity}, cell_centered={self.cell_centered}, extent={self.extent}, " + f"extent_dirs={self.extent_dirs}, orientation={self.orientation})" + ) + # __array_function__ implementations diff --git a/fdsreader/slcf/slice_collection.py b/fdsreader/slcf/slice_collection.py index aa334e24..dd135a9a 100644 --- a/fdsreader/slcf/slice_collection.py +++ b/fdsreader/slcf/slice_collection.py @@ -8,7 +8,7 @@ class SliceCollection(FDSDataCollection): """Collection of :class:`Slice` objects. Offers additional functionality for filtering and - using slices as well as its subclasses such as :class:`SubSlice`. + using slices as well as its subclasses such as :class:`SubSlice`. """ def __init__(self, *slices: Iterable[Slice]): @@ -19,21 +19,22 @@ def quantities(self) -> List[Quantity]: return list({slc.quantity.name for slc in self}) def filter_by_quantity(self, quantity: Union[str, Quantity]): - """Filters all slices by a specific quantity. - """ - if type(quantity) == Quantity: + """Filters all slices by a specific quantity.""" + if isinstance(quantity, Quantity): quantity = quantity.name - return SliceCollection(x for x in self if x.quantity.name.lower() == quantity.lower() - or x.quantity.short_name.lower() == quantity.lower()) + return SliceCollection( + x + for x in self + if x.quantity.name.lower() == quantity.lower() or x.quantity.short_name.lower() == quantity.lower() + ) def get_by_id(self, slice_id: str): - """Get the slice with corresponding id if it exists. - """ + """Get the slice with corresponding id if it exists.""" return next((slc for slc in self if slc.id == slice_id), None) def get_nearest(self, x: float = None, y: float = None, z: float = None) -> Slice: """Filters the slice with the shortest distance to the given point. - If there are multiple slices with the same distance, a random one will be selected. + If there are multiple slices with the same distance, a random one will be selected. """ d_min = np.finfo(float).max @@ -49,15 +50,15 @@ def get_nearest(self, x: float = None, y: float = None, z: float = None) -> Slic slices_min.append(slc) if x is not None: - slices_min.sort(key=lambda slc: (slc.extent.x_end - slc.extent.x_start)) + slices_min.sort(key=lambda slc: slc.extent.x_end - slc.extent.x_start) if y is not None: - slices_min.sort(key=lambda slc: (slc.extent.y_end - slc.extent.y_start)) + slices_min.sort(key=lambda slc: slc.extent.y_end - slc.extent.y_start) if z is not None: - slices_min.sort(key=lambda slc: (slc.extent.z_end - slc.extent.z_start)) + slices_min.sort(key=lambda slc: slc.extent.z_end - slc.extent.z_start) if len(slices_min) > 0: return slices_min[0] return None def __repr__(self): - return "SliceCollection(" + super(SliceCollection, self).__repr__() + ")" + return "SliceCollection(" + super().__repr__() + ")" diff --git a/fdsreader/smoke3d/__init__.py b/fdsreader/smoke3d/__init__.py index ca62e895..261118be 100644 --- a/fdsreader/smoke3d/__init__.py +++ b/fdsreader/smoke3d/__init__.py @@ -1,3 +1,2 @@ -from .smoke3d import Smoke3D - -from .smoke3D_collection import Smoke3DCollection +from .smoke3d import Smoke3D as Smoke3D +from .smoke3D_collection import Smoke3DCollection as Smoke3DCollection diff --git a/fdsreader/smoke3d/smoke3D_collection.py b/fdsreader/smoke3d/smoke3D_collection.py index 4e738382..359dd43d 100644 --- a/fdsreader/smoke3d/smoke3D_collection.py +++ b/fdsreader/smoke3d/smoke3D_collection.py @@ -6,7 +6,7 @@ class Smoke3DCollection(FDSDataCollection): """Collection of :class:`Smoke3D` objects. Offers extensive functionality for filtering and using Smoke3Ds as well - as its subclasses such as :class:`SubSmoke3D`. + as its subclasses such as :class:`SubSmoke3D`. """ def __init__(self, *smoke3ds: Iterable[Smoke3D]): @@ -17,12 +17,14 @@ def quantities(self) -> List[Quantity]: return [smoke3d.name for smoke3d in self._elements] def get_by_quantity(self, quantity: Union[Quantity, str]): - """Gets the :class:`Smoke3D` with a specific quantity. - """ - if type(quantity) == Quantity: + """Gets the :class:`Smoke3D` with a specific quantity.""" + if isinstance(quantity, Quantity): quantity = quantity.name - return next(x for x in self._elements if - x.quantity.name.lower() == quantity.lower() or x.quantity.short_name.lower() == quantity.lower()) + return next( + x + for x in self._elements + if x.quantity.name.lower() == quantity.lower() or x.quantity.short_name.lower() == quantity.lower() + ) def __repr__(self): - return "Smoke3DCollection(" + super(Smoke3DCollection, self).__repr__() + ")" + return "Smoke3DCollection(" + super().__repr__() + ")" diff --git a/fdsreader/smoke3d/smoke3d.py b/fdsreader/smoke3d/smoke3d.py index 82922549..d84a0a65 100644 --- a/fdsreader/smoke3d/smoke3d.py +++ b/fdsreader/smoke3d/smoke3d.py @@ -1,21 +1,21 @@ import logging +import math import os from copy import deepcopy -from typing import Dict, Tuple, Literal, Union +from typing import Dict, Literal, Tuple, Union + import numpy as np -import math +import fdsreader.utils.fortran_data as fdtype +from fdsreader import settings from fdsreader.fds_classes import Mesh from fdsreader.utils import Quantity -from fdsreader import settings -import fdsreader.utils.fortran_data as fdtype _HANDLED_FUNCTIONS = {np.mean: (lambda pl: pl.mean)} def implements(np_function): - """Decorator to register an __array_function__ implementation for Smoke3Ds. - """ + """Decorator to register an __array_function__ implementation for Smoke3Ds.""" def decorator(func): _HANDLED_FUNCTIONS[np_function] = func @@ -41,12 +41,11 @@ def __init__(self, file_path: str, mesh: Mesh, upper_bounds: np.ndarray, times: @property def data(self) -> np.ndarray: - """Method to lazy load the Smoke3D data of a single mesh. - """ + """Method to lazy load the Smoke3D data of a single mesh.""" if not hasattr(self, "_data"): - with open(self._file_path, 'rb') as infile: - dtype_header = fdtype.new((('i', 8),)) - dtype_nchars = fdtype.new((('i', 2),)) + with open(self._file_path, "rb") as infile: + dtype_header = fdtype.new((("i", 8),)) + dtype_nchars = fdtype.new((("i", 2),)) header = fdtype.read(infile, dtype_header, 1)[0][0] nx, ny, nz = int(header[3]), int(header[5]), int(header[7]) @@ -59,10 +58,10 @@ def data(self) -> np.ndarray: nchars_out = int(fdtype.read(infile, dtype_nchars, 1)[0][0][1]) if nchars_out > 0: - dtype_data = fdtype.new((('u', nchars_out),)) + dtype_data = fdtype.new((("u", nchars_out),)) rle_data = fdtype.read(infile, dtype_data, 1)[0][0] - decoded_data = np.empty(((nx + 1) * (ny + 1) * (nz + 1))) + decoded_data = np.empty((nx + 1) * (ny + 1) * (nz + 1)) # Decode run-length-encoded data (see "RLE" subroutine in smvv.f90) i = 0 @@ -77,16 +76,15 @@ def data(self) -> np.ndarray: value = rle_data[i] repeats = 1 i += 1 - decoded_data[out_pos:out_pos + repeats] = value + decoded_data[out_pos : out_pos + repeats] = value out_pos += repeats - self._data[t, :, :, :] = decoded_data.reshape(data_shape, order='F') + self._data[t, :, :, :] = decoded_data.reshape(data_shape, order="F") return self._data def clear_cache(self): - """Remove all data from the internal cache that has been loaded so far to free memory. - """ + """Remove all data from the internal cache that has been loaded so far to free memory.""" if hasattr(self, "_data"): del self._data @@ -117,23 +115,24 @@ def _add_subsmoke(self, filename: str, mesh: Mesh, upper_bounds: np.ndarray) -> return subsmoke - def to_global(self, masked: bool = False, fill: float = 0, return_coordinates: bool = False) -> \ - Union[np.ndarray, Tuple[np.ndarray, Dict[Literal['x', 'y', 'z'], np.ndarray]]]: + def to_global( + self, masked: bool = False, fill: float = 0, return_coordinates: bool = False + ) -> Union[np.ndarray, Tuple[np.ndarray, Dict[Literal["x", "y", "z"], np.ndarray]]]: """Creates a global numpy ndarray from all subsmokes. - :param masked: Whether to apply the obstruction mask to the data or not. - :param fill: The fill value to use for masked entries. Only used when masked=True. - :param return_coordinates: If true, return the matching coordinate for each value on the generated grid. + :param masked: Whether to apply the obstruction mask to the data or not. + :param fill: The fill value to use for masked entries. Only used when masked=True. + :param return_coordinates: If true, return the matching coordinate for each value on the generated grid. """ if len(self._subsmokes) == 0: if return_coordinates: - return np.array([]), {d: np.array([]) for d in ('x', 'y', 'z')} + return np.array([]), {d: np.array([]) for d in ("x", "y", "z")} else: return np.array([]) - coord_min = {'x': math.inf, 'y': math.inf, 'z': math.inf} - coord_max = {'x': -math.inf, 'y': -math.inf, 'z': -math.inf} - for dim in ('x', 'y', 'z'): + coord_min = {"x": math.inf, "y": math.inf, "z": math.inf} + coord_max = {"x": -math.inf, "y": -math.inf, "z": -math.inf} + for dim in ("x", "y", "z"): for subsmoke in self._subsmokes.values(): co = subsmoke.mesh.coordinates[dim] coord_min[dim] = min(co[0], coord_min[dim]) @@ -142,44 +141,51 @@ def to_global(self, masked: bool = False, fill: float = 0, return_coordinates: b # The global grid will use the finest mesh as base and duplicate values of the coarser # meshes. Therefore, we first find the finest mesh and calculate the step size in each # dimension. - step_sizes_min = {'x': coord_max['x'] - coord_min['x'], - 'y': coord_max['y'] - coord_min['y'], - 'z': coord_max['z'] - coord_min['z']} - step_sizes_max = {'x': 0, 'y': 0, 'z': 0} + step_sizes_min = { + "x": coord_max["x"] - coord_min["x"], + "y": coord_max["y"] - coord_min["y"], + "z": coord_max["z"] - coord_min["z"], + } + step_sizes_max = {"x": 0, "y": 0, "z": 0} steps = dict() - global_max = {'x': -math.inf, 'y': -math.inf, 'z': -math.inf} + global_max = {"x": -math.inf, "y": -math.inf, "z": -math.inf} - for dim in ('x', 'y', 'z'): + for dim in ("x", "y", "z"): for subsmoke in self._subsmokes.values(): step_size = subsmoke.mesh.coordinates[dim][1] - subsmoke.mesh.coordinates[dim][0] step_sizes_min[dim] = min(step_size, step_sizes_min[dim]) step_sizes_max[dim] = max(step_size, step_sizes_max[dim]) global_max[dim] = max(subsmoke.mesh.coordinates[dim][-1], global_max[dim]) - for dim in ('x', 'y', 'z'): + for dim in ("x", "y", "z"): if step_sizes_min[dim] == 0: step_sizes_min[dim] = math.inf steps[dim] = 1 else: - steps[dim] = max(int(round((coord_max[dim] - coord_min[dim]) / step_sizes_min[dim])), - 1) + 1 # + step_sizes_max[dim] / step_sizes_min[dim] + steps[dim] = ( + max(int(round((coord_max[dim] - coord_min[dim]) / step_sizes_min[dim])), 1) + 1 + ) # + step_sizes_max[dim] / step_sizes_min[dim] - grid = np.full((self.n_t, steps['x'], steps['y'], steps['z']), np.nan) + grid = np.full((self.n_t, steps["x"], steps["y"], steps["z"]), np.nan) for subsmoke in self._subsmokes.values(): subsmoke_data = subsmoke.data.copy() if masked: mask = subsmoke.mesh.get_obstruction_mask(self.times) - start_idx = {dim: int(round( - (subsmoke.mesh.coordinates[dim][0] - coord_min[dim]) / step_sizes_min[dim])) for dim in ('x', 'y', 'z')} - end_idx = {dim: int(round( - (subsmoke.mesh.coordinates[dim][-1] - coord_min[dim]) / step_sizes_min[dim])) for dim in ('x', 'y', 'z')} + start_idx = { + dim: int(round((subsmoke.mesh.coordinates[dim][0] - coord_min[dim]) / step_sizes_min[dim])) + for dim in ("x", "y", "z") + } + end_idx = { + dim: int(round((subsmoke.mesh.coordinates[dim][-1] - coord_min[dim]) / step_sizes_min[dim])) + for dim in ("x", "y", "z") + } temp_data = dict() temp_mask = dict() for axis in range(3): - dim = ('x', 'y', 'z')[axis] + dim = ("x", "y", "z")[axis] # Temporarily save border points to add them back to the array again later if np.isclose(subsmoke.mesh.coordinates[dim][-1], global_max[dim]): temp_data_slices = [slice(s) for s in subsmoke_data.shape] @@ -192,26 +198,36 @@ def to_global(self, masked: bool = False, fill: float = 0, return_coordinates: b # We ignore border points unless they are actually on the border of the simulation space as all # other border points actually appear twice, as the subslices overlap. This only # applies for face_centered slices, as cell_centered slices will not overlap. - reduced_shape_slices = (slice(subsmoke.data.shape[0]),) + tuple(slice(1, None) for s in subsmoke.data.shape[1:]) + reduced_shape_slices = (slice(subsmoke.data.shape[0]),) + tuple( + slice(1, None) for s in subsmoke.data.shape[1:] + ) subsmoke_data = subsmoke_data[reduced_shape_slices] if masked: mask = mask[reduced_shape_slices] - n_repeat = max(int(round( - (subsmoke.mesh.coordinates[dim][1] - subsmoke.mesh.coordinates[dim][0]) / - step_sizes_min[dim])), 1) + n_repeat = max( + int( + round( + (subsmoke.mesh.coordinates[dim][1] - subsmoke.mesh.coordinates[dim][0]) + / step_sizes_min[dim] + ) + ), + 1, + ) if n_repeat > 1: subsmoke_data = np.repeat(subsmoke_data, n_repeat, axis=axis + 1) if masked: mask = np.repeat(mask, n_repeat, axis=axis + 1) for axis in range(3): - dim = ('x', 'y', 'z')[axis] + dim = ("x", "y", "z")[axis] # Add border points back again if needed if np.isclose(subsmoke.mesh.coordinates[dim][-1], global_max[dim]): temp_data_slices = [slice(s) for s in subsmoke_data.shape] temp_data_slices[axis + 1] = slice(None) - subsmoke_data = np.concatenate((subsmoke_data, temp_data[dim][tuple(temp_data_slices)]), axis=axis + 1) + subsmoke_data = np.concatenate( + (subsmoke_data, temp_data[dim][tuple(temp_data_slices)]), axis=axis + 1 + ) if masked: mask = np.concatenate((mask, temp_mask[dim][tuple(temp_data_slices)]), axis=axis + 1) @@ -220,14 +236,20 @@ def to_global(self, masked: bool = False, fill: float = 0, return_coordinates: b if masked: subsmoke_data = np.where(mask, subsmoke_data, fill) - grid[:, start_idx['x']: end_idx['x'], start_idx['y']: end_idx['y'], - start_idx['z']: end_idx['z']] = subsmoke_data.reshape( - (self.n_t, end_idx['x'] - start_idx['x'], end_idx['y'] - start_idx['y'], - end_idx['z'] - start_idx['z'])) + grid[:, start_idx["x"] : end_idx["x"], start_idx["y"] : end_idx["y"], start_idx["z"] : end_idx["z"]] = ( + subsmoke_data.reshape( + ( + self.n_t, + end_idx["x"] - start_idx["x"], + end_idx["y"] - start_idx["y"], + end_idx["z"] - start_idx["z"], + ) + ) + ) if return_coordinates: coordinates = dict() - for dim_index, dim in enumerate(('x', 'y', 'z')): + for dim_index, dim in enumerate(("x", "y", "z")): coordinates[dim] = np.linspace(coord_min[dim], coord_max[dim], grid.shape[dim_index + 1]) if return_coordinates: @@ -236,31 +258,26 @@ def to_global(self, masked: bool = False, fill: float = 0, return_coordinates: b return grid def __getitem__(self, mesh: Mesh): - """Returns the :class:`SubSmoke` that contains data for the given mesh. - """ + """Returns the :class:`SubSmoke` that contains data for the given mesh.""" return self.get_subsmoke(mesh) @property def n_t(self) -> int: - """Get the number of timesteps for which data was output. - """ + """Get the number of timesteps for which data was output.""" return len(self.times) def get_subsmoke(self, mesh: Mesh): - """Returns the :class:`SubSmoke` that contains data for the given mesh. - """ + """Returns the :class:`SubSmoke` that contains data for the given mesh.""" return self._subsmokes[mesh.id] @property def subsmokes(self): - """Returns a list with one SubSmoke3D object per mesh. - """ + """Returns a list with one SubSmoke3D object per mesh.""" return list(self._subsmokes.values()) @property def vmax(self): - """Maximum value of all data at any time. - """ + """Maximum value of all data at any time.""" curr_max = max(np.max(subsmoke3d.upper_bounds) for subsmoke3d in self._subsmokes.values()) if curr_max == np.float32(-1e33): return max(np.max(subsmoke3d.data) for subsmoke3d in self._subsmokes.values()) @@ -268,32 +285,29 @@ def vmax(self): @implements(np.mean) def mean(self) -> np.ndarray: - """Calculates the mean value of all Smoke3D data for this quantity. - """ + """Calculates the mean value of all Smoke3D data for this quantity.""" return np.sum([np.mean(subsmoke.data) for subsmoke in self._subsmokes.values()]) / len(self._subsmokes) @implements(np.std) def std(self) -> np.ndarray: - """Calculates the standard deviation of all Smoke3D data for this quantity. - """ + """Calculates the standard deviation of all Smoke3D data for this quantity.""" mean = self.mean sum = np.sum([np.sum(np.power(subsmoke.data - mean, 2)) for subsmoke in self._subsmokes.values()]) N = np.sum([subsmoke.data.size for subsmoke in self._subsmokes.values()]) return np.sqrt(sum / N) def clear_cache(self): - """Remove all data from the internal cache that has been loaded so far to free memory. - """ + """Remove all data from the internal cache that has been loaded so far to free memory.""" for subsmoke in self._subsmokes.values(): subsmoke.clear_cache() def __array__(self): - """Method that will be called by numpy when trying to convert the object to a numpy ndarray. - """ + """Method that will be called by numpy when trying to convert the object to a numpy ndarray.""" raise UserWarning( "Smoke3Ds can not be converted to numpy arrays, but they support all typical numpy" " operations such as np.multiply. If a 'global' array containing all subsmokes is" - " required, please use the 'to_global' method and use the returned numpy-array explicitly.") + " required, please use the 'to_global' method and use the returned numpy-array explicitly." + ) def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): """Method that will be called by numpy when using a ufunction with a Smoke3D as input. @@ -304,7 +318,9 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): logging.warning( "The %s method has been used which is not explicitly implemented. Correctness of" " results is not guaranteed. If you require this feature to be implemented please" - " submit an issue on Github where you explain your use case.", method) + " submit an issue on Github where you explain your use case.", + method, + ) input_list = list(inputs) for i, inp in enumerate(inputs): if isinstance(inp, self.__class__): @@ -327,5 +343,6 @@ def __array_function__(self, func, types, args, kwargs): return NotImplemented return _HANDLED_FUNCTIONS[func](*args, **kwargs) + # __array_function__ implementations # ... diff --git a/fdsreader/utils/__init__.py b/fdsreader/utils/__init__.py index 63f3b714..5a1738d6 100644 --- a/fdsreader/utils/__init__.py +++ b/fdsreader/utils/__init__.py @@ -1,7 +1,5 @@ -from .data import Quantity +from fdsreader.utils.dimension import Dimension as Dimension +from fdsreader.utils.extent import Extent as Extent -from fdsreader.utils.dimension import Dimension - -from fdsreader.utils.extent import Extent - -from.misc import * +from .data import Quantity as Quantity +from .misc import log_error as log_error diff --git a/fdsreader/utils/data.py b/fdsreader/utils/data.py index 631b3eda..908a1dea 100644 --- a/fdsreader/utils/data.py +++ b/fdsreader/utils/data.py @@ -1,10 +1,13 @@ """ Collection of internal utilities (convenience functions and classes) for data handling. """ + import glob import hashlib import os + import numpy as np + try: # Python <= 3.9 from collections import Iterable @@ -20,6 +23,7 @@ class Quantity: :ivar quantity: The name of the quantity. :ivar unit: The corresponding unit of the quantity. """ + def __init__(self, quantity: str, short_name: str, unit: str): self.short_name = short_name self.unit = unit @@ -44,8 +48,8 @@ def __repr__(self): class Profile: - """Class containing profile data. - """ + """Class containing profile data.""" + def __init__(self, profile_id: str, times: np.ndarray, npoints: np.ndarray, depths: np.ndarray, values: np.ndarray): self.id = profile_id self.times = times @@ -61,9 +65,8 @@ def __repr__(self): def create_hash(path: str): - """Returns the md5 hash as string for the given file. - """ - return str(hashlib.md5(open(path, 'rb').read()).hexdigest()) + """Returns the md5 hash as string for the given file.""" + return str(hashlib.md5(open(path, "rb").read()).hexdigest()) def scan_directory_smv(directory: str): @@ -87,19 +90,18 @@ def get_smv_file(path: str): elif os.path.isdir(path): files = scan_directory_smv(path) if len(files) > 1: - raise IOError("There are multiple simulations in the directory: " + path) + raise OSError("There are multiple simulations in the directory: " + path) elif len(files) == 0: - raise IOError("No simulations were found in the directory: " + path) + raise OSError("No simulations were found in the directory: " + path) return files[0] elif os.path.isfile(path + ".smv"): return path + ".smv" else: - raise IOError("The given path does neither point to a directory nor a file: " + path) + raise OSError("The given path does neither point to a directory nor a file: " + path) class FDSDataCollection: - """(Abstract) Base class for any collection of FDS data. - """ + """(Abstract) Base class for any collection of FDS data.""" def __init__(self, *elements: Iterable): self._elements = tuple(*elements) @@ -120,7 +122,6 @@ def __repr__(self): return "[" + ",\n".join(str(e) for e in self._elements) + "]" def clear_cache(self): - """Remove all data from the internal cache that has been loaded so far to free memory. - """ + """Remove all data from the internal cache that has been loaded so far to free memory.""" for element in self._elements: element.clear_cache() diff --git a/fdsreader/utils/dimension.py b/fdsreader/utils/dimension.py index 82a31896..930ba1e0 100644 --- a/fdsreader/utils/dimension.py +++ b/fdsreader/utils/dimension.py @@ -1,6 +1,6 @@ from functools import reduce from operator import mul -from typing import Tuple, List +from typing import List, Tuple from typing_extensions import Literal @@ -13,14 +13,14 @@ class Dimension: :ivar z: Number of data points in z-direction (end is exclusive). """ - def __init__(self, *args, skip_dimension: Literal['x', 1, 'y', 2, 'z', 3, ''] = ''): + def __init__(self, *args, skip_dimension: Literal["x", 1, "y", 2, "z", 3, ""] = ""): dimensions = list(args) - if skip_dimension in ('x', 1): + if skip_dimension in ("x", 1): dimensions.insert(0, 1) - elif skip_dimension in ('y', 2): + elif skip_dimension in ("y", 2): dimensions.insert(1, 1) - elif skip_dimension in ('z', 3): + elif skip_dimension in ("z", 3): dimensions.append(1) self.x = dimensions[0] @@ -33,17 +33,16 @@ def __eq__(self, other): def __repr__(self, *args, **kwargs): return f"Dimension({self.x}, {self.y}, {self.z})" - def __getitem__(self, dimension: Literal[0, 1, 2, 'x', 'y', 'z']): - if type(dimension) == int: - dimension = ('x', 'y', 'z')[dimension] + def __getitem__(self, dimension: Literal[0, 1, 2, "x", "y", "z"]): + if isinstance(dimension, int): + dimension = ("x", "y", "z")[dimension] return self.__dict__[dimension] def size(self, cell_centered=False): return reduce(mul, self.shape(cell_centered)) def shape(self, cell_centered=False) -> Tuple: - """Method to get the actual number of data points per dimension. - """ + """Method to get the actual number of data points per dimension.""" s = list() c = -1 if cell_centered else 0 if self.x != 1: diff --git a/fdsreader/utils/extent.py b/fdsreader/utils/extent.py index d654bc44..74814bdf 100644 --- a/fdsreader/utils/extent.py +++ b/fdsreader/utils/extent.py @@ -1,13 +1,12 @@ -from typing import Tuple, List, Union +from typing import List, Tuple, Union from typing_extensions import Literal class Extent: - """Three-dimensional value-based extent with support for a missing dimension (2D). - """ + """Three-dimensional value-based extent with support for a missing dimension (2D).""" - def __init__(self, *args, skip_dimension: Literal['x', 1, 'y', 2, 'z', 3, ''] = ''): + def __init__(self, *args, skip_dimension: Literal["x", 1, "y", 2, "z", 3, ""] = ""): self._extents = list() if len(args) % 2 != 0: @@ -15,64 +14,59 @@ def __init__(self, *args, skip_dimension: Literal['x', 1, 'y', 2, 'z', 3, ''] = for i in range(0, len(args), 2): self._extents.append((float(args[i]), float(args[i + 1]))) - if skip_dimension in ('x', 1): + if skip_dimension in ("x", 1): self._extents.insert(0, (0, 0)) - elif skip_dimension in ('y', 2): + elif skip_dimension in ("y", 2): self._extents.insert(1, (0, 0)) - elif skip_dimension in ('z', 3): + elif skip_dimension in ("z", 3): self._extents.append((0, 0)) def __eq__(self, other): return self._extents == other._extents def __repr__(self, *args, **kwargs): - return "Extent([{:.2f}, {:.2f}] x [{:.2f}, {:.2f}] x [{:.2f}, {:.2f}])".format(self.x_start, self.x_end, - self.y_start, self.y_end, - self.z_start, self.z_end) + return ( + f"Extent([{self.x_start:.2f}, {self.x_end:.2f}] x [{self.y_start:.2f}, {self.y_end:.2f}]" + f" x [{self.z_start:.2f}, {self.z_end:.2f}])" + ) - def __getitem__(self, item: Union[Literal['x', 'y', 'z', 1, 2, 3]]): - if item == 'x': + def __getitem__(self, item: Union[Literal["x", "y", "z", 1, 2, 3]]): + if item == "x": return self.x_start, self.x_end - elif item == 'y': + elif item == "y": return self.y_start, self.y_end - elif item == 'z': + elif item == "z": return self.z_start, self.z_end return self._extents[item - 1] @property def x_start(self) -> float: - """Gives the absolute extent in x-direction. - """ + """Gives the absolute extent in x-direction.""" return self._extents[0][0] @property def y_start(self) -> float: - """Gives the absolute extent in y-direction. - """ + """Gives the absolute extent in y-direction.""" return self._extents[1][0] @property def z_start(self) -> float: - """Gives the absolute extent in z-direction. - """ + """Gives the absolute extent in z-direction.""" return self._extents[2][0] @property def x_end(self) -> float: - """Gives the absolute extent in x-direction. - """ + """Gives the absolute extent in x-direction.""" return self._extents[0][1] @property def y_end(self) -> float: - """Gives the absolute extent in y-direction. - """ + """Gives the absolute extent in y-direction.""" return self._extents[1][1] @property def z_end(self) -> float: - """Gives the absolute extent in z-direction. - """ + """Gives the absolute extent in z-direction.""" return self._extents[2][1] def as_tuple(self, reduced=True) -> Tuple[float, ...]: diff --git a/fdsreader/utils/fortran_data.py b/fdsreader/utils/fortran_data.py index 7e5f8f33..d398f125 100644 --- a/fdsreader/utils/fortran_data.py +++ b/fdsreader/utils/fortran_data.py @@ -1,22 +1,31 @@ -from typing import Sequence, BinaryIO, Union, Tuple +from typing import BinaryIO, Sequence, Tuple, Union + import numpy as np -from fdsreader.settings import FORTRAN_DATA_TYPE_CHAR, FORTRAN_DATA_TYPE_FLOAT, FORTRAN_DATA_TYPE_INTEGER, \ - FORTRAN_DATA_TYPE_UINT8, FORTRAN_BACKWARD +from fdsreader.settings import ( + FORTRAN_BACKWARD, + FORTRAN_DATA_TYPE_CHAR, + FORTRAN_DATA_TYPE_FLOAT, + FORTRAN_DATA_TYPE_INTEGER, + FORTRAN_DATA_TYPE_UINT8, +) -_BASE_FORMAT = f"{FORTRAN_DATA_TYPE_INTEGER}, {{}}" + ( - f", {FORTRAN_DATA_TYPE_INTEGER}" if FORTRAN_BACKWARD else "") +_BASE_FORMAT = f"{FORTRAN_DATA_TYPE_INTEGER}, {{}}" + (f", {FORTRAN_DATA_TYPE_INTEGER}" if FORTRAN_BACKWARD else "") -_DATA_TYPES = {'i': FORTRAN_DATA_TYPE_INTEGER, 'f': FORTRAN_DATA_TYPE_FLOAT, 'c': FORTRAN_DATA_TYPE_CHAR, - 'u': FORTRAN_DATA_TYPE_UINT8, '{}': "{}"} +_DATA_TYPES = { + "i": FORTRAN_DATA_TYPE_INTEGER, + "f": FORTRAN_DATA_TYPE_FLOAT, + "c": FORTRAN_DATA_TYPE_CHAR, + "u": FORTRAN_DATA_TYPE_UINT8, + "{}": "{}", +} def _get_dtype_output_format(d, n): - """Returns the correct output format needed to create a numpy dtype depending on input. - """ - if d == 'c': + """Returns the correct output format needed to create a numpy dtype depending on input.""" + if d == "c": return str(n) - if type(n) == int or type(n) == np.int32: + if isinstance(n, (int, np.int32)): return f"({n},)" return str(n) @@ -54,15 +63,15 @@ def combine(*dtypes: np.dtype): type_combination = list() for types in dtypes: for dtype in types.descr: - type_combination.append(tuple(['f' + str(count)] + list(dtype[1:]))) + type_combination.append(tuple(["f" + str(count)] + list(dtype[1:]))) count += 1 return np.dtype(type_combination) # Commonly used datatypes -CHAR = new((('c', 1),)) -INT = new((('i', 1),)) -FLOAT = new((('f', 1),)) +CHAR = new((("c", 1),)) +INT = new((("i", 1),)) +FLOAT = new((("f", 1),)) # Border datatype to get the border of a fortran write PRE_BORDER = np.dtype(FORTRAN_DATA_TYPE_INTEGER) HAS_POST_BORDER = FORTRAN_BACKWARD @@ -77,5 +86,5 @@ def read(infile: BinaryIO, dtype: np.dtype, n: int): :returns: Read in data. """ return np.array( - [[t[i] for i in range(1, len(t), 3)] for t in np.fromfile(infile, dtype=dtype, count=n)], - dtype=object) + [[t[i] for i in range(1, len(t), 3)] for t in np.fromfile(infile, dtype=dtype, count=n)], dtype=object + ) diff --git a/fdsreader/utils/misc.py b/fdsreader/utils/misc.py index 23f616bb..4559604b 100644 --- a/fdsreader/utils/misc.py +++ b/fdsreader/utils/misc.py @@ -1,6 +1,7 @@ import logging -from functools import wraps import sys +from functools import wraps + from fdsreader import settings @@ -14,7 +15,14 @@ def wrapped(*args, **kwargs): if settings.DEBUG: raise e elif not settings.IGNORE_ERRORS: - e = type(e)(f"Module {str(module)}: {str(e)}\nThe error can be safely ignored if not requiring the {str(module)} module. However, please consider to submit an issue on Github including the error message, the stack trace and your FDS input-file so we can reproduce the error and fix it as soon as possible!").with_traceback(sys.exc_info()[2]) + e = type(e)( + f"Module {str(module)}: {str(e)}\nThe error can be safely ignored if not requiring the" + f" {str(module)} module. However, please consider to submit an issue on Github including" + f" the error message, the stack trace and your FDS input-file so we can reproduce the" + f" error and fix it as soon as possible!" + ).with_traceback(sys.exc_info()[2]) logging.warning(e) + return wrapped + return decorated diff --git a/pyproject.toml b/pyproject.toml index c33555ed..aab9d142 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,30 +1,28 @@ [build-system] -requires = [ - "setuptools", - "wheel", - "incremental>=24.7.2", # ← Add incremental as a build dependency -] +requires = ["setuptools", "wheel", "setuptools_scm[toml]"] build-backend = "setuptools.build_meta" [project] name = "fdsreader" -dynamic = ["version"] +dynamic = ["version"] description = "Python reader for data generated by FDS." readme = "README.md" -requires-python = ">=3.6" +requires-python = ">=3.10" license = {file = "LICENSE"} authors = [ {name = "FZJ IAS-7 (Jan Vogelsang, Prof. Dr. Lukas Arnold, Tristan Hehnen)", email = "j.vogelsang@fz-juelich.de"} ] -classifiers=[ - "Programming Language :: Python :: 3", - "Operating System :: OS Independent", - "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", - ] +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", +] dependencies = [ - "incremental", "numpy", - "typing_extensions" + "typing_extensions", ] [project.urls] @@ -32,13 +30,62 @@ Repository = "https://github.com/FireDynamics/fdsreader" [project.optional-dependencies] dev = [ - "pytest >=6.0" + "pytest>=7.0", + "pytest-cov", + "ruff", + "coverage[toml]", + "pre-commit", +] +docs = [ + "sphinx>=7.0", + "sphinx-rtd-theme", + "autodocsumm", + "nbsphinx", ] -[tool.incremental] +[tool.setuptools_scm] +version_scheme = "post-release" +local_scheme = "no-local-version" [tool.setuptools] -packages = ["fdsreader","fdsreader.bndf","fdsreader.devc","fdsreader.evac","fdsreader.export","fdsreader.fds_classes","fdsreader.geom","fdsreader.isof","fdsreader.part","fdsreader.pl3d","fdsreader.slcf","fdsreader.smoke3d","fdsreader.utils"] +packages = [ + "fdsreader", + "fdsreader.bndf", + "fdsreader.devc", + "fdsreader.evac", + "fdsreader.export", + "fdsreader.fds_classes", + "fdsreader.geom", + "fdsreader.isof", + "fdsreader.part", + "fdsreader.pl3d", + "fdsreader.slcf", + "fdsreader.smoke3d", + "fdsreader.utils", +] [tool.pytest.ini_options] testpaths = ["tests"] + +[tool.ruff] +line-length = 120 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "UP"] +ignore = ["UP006", "UP007", "UP035"] # typing generics – schrittweise migrieren + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" + +[tool.coverage.run] +branch = true +source = ["fdsreader"] + +[tool.coverage.report] +fail_under = 30 +exclude_lines = [ + "if TYPE_CHECKING:", + "pragma: no cover", +] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..3dbb7d42 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +pytest>=7.0 +pytest-cov +ruff +coverage[toml] +pre-commit diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 61de388d..00000000 --- a/requirements.txt +++ /dev/null @@ -1,163 +0,0 @@ -aiohttp==3.9.1 -aiosignal==1.3.1 -alabaster==0.7.13 -appdirs==1.4.4 -argcomplete==3.1.6 -argon2-cffi==23.1.0 -argon2-cffi-bindings==21.2.0 -asttokens==2.4.1 -async-timeout==4.0.3 -asynctest==0.13.0 -attrs==23.1.0 -autodocsumm==0.2.11 -Automat==22.10.0 -Babel==2.13.1 -backcall==0.2.0 -beautifulsoup4==4.12.2 -bleach==6.1.0 -bpyutils==0.5.8 -brotlipy==0.7.0 -certifi==2023.11.17 -cffi==1.16.0 -charset-normalizer==2.0.12 -click==8.1.7 -colorama==0.4.6 -comm==0.2.0 -constantly==23.10.4 -contourpy==1.2.0 -cryptography==41.0.7 -cycler==0.12.1 -debugpy==1.8.0 -decorator==5.1.1 -defusedxml==0.7.1 -dill==0.3.7 -docutils==0.20.1 -dpcpp-cpp-rt==2024.0.0 -entrypoints==0.4 -environment-kernels==1.2.0 -executing==2.0.1 -fastjsonschema==2.19.0 -fonttools==4.45.1 -frozenlist==1.4.0 -hyperlink==21.0.0 -idna==3.6 -imageio==2.33.0 -imageio-ffmpeg==0.4.9 -imagesize==1.4.1 -importlib-metadata==6.8.0 -importlib-resources==6.1.1 -incremental==22.10.0 -intel-cmplr-lib-rt==2024.0.0 -intel-cmplr-lic-rt==2024.0.0 -intel-opencl-rt==2024.0.0 -intel-openmp==2024.0.0 -ipykernel==6.27.1 -ipython==8.18.1 -ipython-genutils==0.2.0 -jaraco.classes==3.3.0 -jedi==0.19.1 -jeepney==0.8.0 -Jinja2==3.1.2 -jsonschema==4.20.0 -jsonschema-specifications==2023.11.1 -jupyter_client==8.6.0 -jupyter_core==5.5.0 -jupyterlab_pygments==0.3.0 -keyring==24.3.0 -kiwisolver==1.4.5 -markdown-it-py==3.0.0 -MarkupSafe==2.1.3 -matplotlib==3.8.2 -matplotlib-inline==0.1.6 -mdurl==0.1.2 -meshio==5.3.4 -mistune==3.0.2 -mkl==2024.0.0 -more-itertools==10.1.0 -multidict==6.0.4 -multiprocess==0.70.15 -nbclient==0.9.0 -nbconvert==7.11.0 -nbformat==5.9.2 -nbsphinx==0.9.3 -nest-asyncio==1.5.8 -nh3==0.2.14 -numpy==1.26.2 -olefile==0.46 -packaging==23.2 -pandocfilters==1.5.0 -parso==0.8.3 -pathos==0.3.1 -pexpect==4.9.0 -pickleshare==0.7.5 -Pillow==10.1.0 -pkginfo==1.9.6 -platformdirs==4.0.0 -pooch==1.8.0 -pox==0.3.3 -ppft==1.7.6.7 -prometheus-client==0.19.0 -prompt-toolkit==3.0.41 -psutil==5.9.6 -ptyprocess==0.7.0 -pure-eval==0.2.2 -pyasn1==0.5.1 -pyasn1-modules==0.3.0 -pycparser==2.21 -Pygments==2.17.2 -pyparsing==3.1.1 -PyQt5==5.15.10 -PyQt5-Qt5==5.15.2 -PyQt5-sip==12.13.0 -pyrsistent==0.20.0 -python-dateutil==2.8.2 -pytz==2023.3.post1 -pyvista==0.42.3 -pyvistaqt==0.11.0 -pywinpty==0.5.7 -PyYAML==6.0.1 -pyzmq==25.1.1 -QtPy==2.4.1 -readme-renderer==42.0 -referencing==0.31.0 -requests==2.31.0 -requests-toolbelt==1.0.0 -rfc3986==2.0.0 -rich==13.7.0 -rpds-py==0.13.1 -scooby==0.9.2 -SecretStorage==3.3.3 -setuptools-scm==8.0.4 -six==1.16.0 -snowballstemmer==2.2.0 -soupsieve==2.5 -Sphinx==7.2.6 -sphinx-rtd-theme==2.0.0 -sphinxcontrib-applehelp==1.0.7 -sphinxcontrib-devhelp==1.0.5 -sphinxcontrib-htmlhelp==2.0.4 -sphinxcontrib-jquery==4.1 -sphinxcontrib-jsmath==1.0.1 -sphinxcontrib-qthelp==1.0.6 -sphinxcontrib-serializinghtml==1.1.9 -stack-data==0.6.3 -tbb==2021.11.0 -terminado==0.18.0 -testpath==0.6.0 -tinycss2==1.2.1 -tomli==2.0.1 -tornado==6.4 -traitlets==5.14.0 -transforms3d==0.4.1 -twine==4.0.2 -Twisted==23.10.0 -typing_extensions==4.8.0 -urllib3==2.1.0 -vtk==9.3.0 -wcwidth==0.2.12 -webencodings==0.5.1 -wincertstore==0.2 -wslink==1.12.4 -yarl==1.9.3 -zipp==3.17.0 -zope.interface==6.1 diff --git a/tests/acceptance_tests/test_bndf.py b/tests/acceptance_tests/test_bndf.py index 1513649b..5d6de626 100644 --- a/tests/acceptance_tests/test_bndf.py +++ b/tests/acceptance_tests/test_bndf.py @@ -1,9 +1,41 @@ +import numpy as np +import pytest + from fdsreader import Simulation -def test_bndf(): - sim = Simulation("./bndf_data") - obst = sim.obstructions.get_nearest(-0.8, 1, 1) - face = obst.get_global_boundary_data_arrays("Wall Temperature")[1] +@pytest.fixture(scope="module") +def bndf_sim(): + return Simulation("./bndf_data") + + +@pytest.fixture(scope="module") +def bndf_obst(bndf_sim): + return bndf_sim.obstructions.get_nearest(-0.8, 1, 1) + +def test_bndf(bndf_obst): + face = bndf_obst.get_global_boundary_data_arrays("Wall Temperature")[1] assert len(face[-1]) == 68 + + +def test_bndf_times_non_empty(bndf_obst): + assert len(bndf_obst.times) > 0 + + +def test_bndf_times_start_at_zero(bndf_obst): + assert bndf_obst.times[0] == pytest.approx(0.0) + + +def test_bndf_times_monotonic(bndf_obst): + assert np.all(np.diff(bndf_obst.times) > 0), "BNDF time steps must be monotonically increasing" + + +def test_bndf_data_shape_matches_times(bndf_obst): + face = bndf_obst.get_global_boundary_data_arrays("Wall Temperature")[1] + assert face.shape[0] == len(bndf_obst.times) + + +def test_bndf_no_nan(bndf_obst): + face = bndf_obst.get_global_boundary_data_arrays("Wall Temperature")[1] + assert not np.isnan(face).any(), "NaN values found in BNDF data" diff --git a/tests/acceptance_tests/test_devc.py b/tests/acceptance_tests/test_devc.py index 9a86a63f..30a674b9 100644 --- a/tests/acceptance_tests/test_devc.py +++ b/tests/acceptance_tests/test_devc.py @@ -1,6 +1,32 @@ +import numpy as np +import pytest + from fdsreader import Simulation -def test_devc(): - sim = Simulation("./steckler_data") - assert abs(sim.devices["TC_Door"][0].data - 23.58) < 1e-6 +@pytest.fixture(scope="module") +def devc_sim(): + return Simulation("./steckler_data") + + +def test_devc(devc_sim): + assert abs(devc_sim.devices["TC_Door"][0].data - 23.58) < 1e-6 + + +def test_devc_time_channel_non_empty(devc_sim): + time_dev = devc_sim.devices["Time"] + assert len(time_dev.data) > 0 + + +def test_devc_time_channel_starts_at_zero(devc_sim): + time_dev = devc_sim.devices["Time"] + assert time_dev.data[0] == pytest.approx(0.0) + + +def test_devc_time_channel_monotonic(devc_sim): + time_dev = devc_sim.devices["Time"] + assert np.all(np.diff(time_dev.data) > 0), "Time channel must be monotonically increasing" + + +def test_devc_quantity_has_name(devc_sim): + assert devc_sim.devices["TC_Door"][0].quantity_name != "" diff --git a/tests/acceptance_tests/test_geom.py b/tests/acceptance_tests/test_geom.py index 68770a79..896bbc77 100644 --- a/tests/acceptance_tests/test_geom.py +++ b/tests/acceptance_tests/test_geom.py @@ -1,9 +1,33 @@ +import numpy as np +import pytest + from fdsreader import Simulation -def test_geom(): - sim = Simulation("./geom_data") - geom = sim.geom_data.filter_by_quantity("Radiative Heat Flux")[0] +@pytest.fixture(scope="module") +def geom_sim(): + return Simulation("./geom_data") + + +@pytest.fixture(scope="module") +def geom(geom_sim): + return geom_sim.geom_data.filter_by_quantity("Radiative Heat Flux")[0] + +def test_geom(geom): assert len(geom.faces) == len(geom.data[-1]) == 19816 assert len(geom.vertices) == 40624 + + +def test_geom_data_non_empty(geom): + assert len(geom.data) > 0 + + +def test_geom_faces_match_data(geom): + assert len(geom.faces) == len(geom.data[-1]) + + +def test_geom_vertices_finite(geom): + vertices = np.array(geom.vertices) + assert not np.isnan(vertices).any(), "NaN in Geometrie-Vertices" + assert not np.isinf(vertices).any(), "Inf in Geometrie-Vertices" diff --git a/tests/acceptance_tests/test_isof.py b/tests/acceptance_tests/test_isof.py index aa8a0cc2..b472e563 100644 --- a/tests/acceptance_tests/test_isof.py +++ b/tests/acceptance_tests/test_isof.py @@ -1,10 +1,39 @@ +import numpy as np +import pytest + from fdsreader import Simulation -def test_isof(): - sim = Simulation("./steckler_data") - isosurface = sim.isosurfaces.filter_by_quantity("TEMP")[0] - vertices, triangles, _ = isosurface.to_global(len(isosurface.times) - 1) +@pytest.fixture(scope="module") +def isof_sim(): + return Simulation("./steckler_data") + + +@pytest.fixture(scope="module") +def isosurface(isof_sim): + return isof_sim.isosurfaces.filter_by_quantity("TEMP")[0] + +def test_isof(isosurface): + vertices, triangles, _ = isosurface.to_global(len(isosurface.times) - 1) assert abs(vertices[-1][0] - 2.80595016) < 1e-6 and abs(vertices[-1][1] - 0.1) < 1e-6 and abs(vertices[-1][2] - 1.83954549) < 1e-6 assert abs(triangles[-1][-1][0] - 4625) < 1e-6 and abs(triangles[-1][-1][1] - 4627) < 1e-6 and abs(triangles[-1][-1][2] - 4708) < 1e-6 + + +def test_isof_times_non_empty(isosurface): + assert len(isosurface.times) > 0 + + +def test_isof_times_start_at_zero(isosurface): + assert isosurface.times[0] == pytest.approx(0.0) + + +def test_isof_times_monotonic(isosurface): + times = np.array(isosurface.times) + assert np.all(np.diff(times) > 0), "ISOF time steps must be monotonically increasing" + + +def test_isof_to_global_returns_vertices(isosurface): + vertices, triangles, _ = isosurface.to_global(0) + assert vertices is not None + assert triangles is not None diff --git a/tests/acceptance_tests/test_part.py b/tests/acceptance_tests/test_part.py index 75eda866..225932f1 100644 --- a/tests/acceptance_tests/test_part.py +++ b/tests/acceptance_tests/test_part.py @@ -1,11 +1,36 @@ +import numpy as np +import pytest + from fdsreader import Simulation -def test_part(): - sim = Simulation("./part_data") - particles = sim.particles["WATER PARTICLES"] +@pytest.fixture(scope="module") +def part_sim(): + return Simulation("./part_data") + + +@pytest.fixture(scope="module") +def particles(part_sim): + return part_sim.particles["WATER PARTICLES"] + + +def test_part(particles): position = particles.positions[-1][-1] color_data = particles.data["PARTICLE DIAMETER"] - assert abs(position[0] + 0.04036335) < 1e-6 and abs(position[1] - 0.05389348) < 1e-6 and abs(position[2] - 13.596354) < 1e-6 assert abs(color_data[-1][-1] - 2.2600818) < 1e-6 + + +def test_part_positions_non_empty(particles): + assert len(particles.positions) > 0 + + +def test_part_positions_match_timesteps(particles): + assert len(particles.positions) == len(particles.data["PARTICLE DIAMETER"]) + + +def test_part_positions_finite(particles): + for positions_at_t in particles.positions: + arr = np.array(positions_at_t) + assert not np.isnan(arr).any(), "NaN in Partikel-Positionen" + assert not np.isinf(arr).any(), "Inf in Partikel-Positionen" diff --git a/tests/acceptance_tests/test_pl3d.py b/tests/acceptance_tests/test_pl3d.py index 10dc8a68..4b3c2f9c 100644 --- a/tests/acceptance_tests/test_pl3d.py +++ b/tests/acceptance_tests/test_pl3d.py @@ -1,10 +1,31 @@ +import numpy as np +import pytest + from fdsreader import Simulation -def test_pl3d(): - sim = Simulation("./pl3d_data") - pl_t1 = sim.data_3d.get_by_quantity("Temperature") - data, coordinates = pl_t1.to_global(masked=True, return_coordinates=True) +@pytest.fixture(scope="module") +def pl3d_sim(): + return Simulation("./pl3d_data") + + +@pytest.fixture(scope="module") +def pl3d(pl3d_sim): + return pl3d_sim.data_3d.get_by_quantity("Temperature") + +def test_pl3d(pl3d): + data, coordinates = pl3d.to_global(masked=True, return_coordinates=True) assert abs(data[-1, 41, 27, 0] - 55.85966110229492) < 1e-6 - assert abs(coordinates['x'][41] - 9.25) < 1e-6 + assert abs(coordinates["x"][41] - 9.25) < 1e-6 + + +def test_pl3d_data_non_empty(pl3d): + data = pl3d.to_global(masked=True) + assert data.size > 0 + + +def test_pl3d_no_nan_inf(pl3d): + data = pl3d.to_global(masked=True) + assert not np.isnan(data.data).any(), "NaN values found in PL3D data" + assert not np.isinf(data.data).any(), "Inf values found in PL3D data" diff --git a/tests/acceptance_tests/test_slcf.py b/tests/acceptance_tests/test_slcf.py index 39954d37..9c741ea4 100644 --- a/tests/acceptance_tests/test_slcf.py +++ b/tests/acceptance_tests/test_slcf.py @@ -1,8 +1,44 @@ +import numpy as np +import pytest + from fdsreader import Simulation -def test_slcf(): - sim = Simulation("./steckler_data") - data, coordinates = sim.slices[0].to_global(masked=True, return_coordinates=True) +@pytest.fixture(scope="module") +def slcf_sim(): + return Simulation("./steckler_data") + + +def test_slcf(slcf_sim): + data, coordinates = slcf_sim.slices[0].to_global(masked=True, return_coordinates=True) assert abs(data[-1, -1, -1] - 33.311744689941406) < 1e-6 - assert abs(coordinates['x'][0] - 0.) < 1e-6 and abs(coordinates['x'][-1] - 3.6) < 1e-6 + assert abs(coordinates["x"][0] - 0.0) < 1e-6 and abs(coordinates["x"][-1] - 3.6) < 1e-6 + + +def test_slcf_times_non_empty(slcf_sim): + assert slcf_sim.slices[0].n_t > 0 + + +def test_slcf_times_start_at_zero(slcf_sim): + assert slcf_sim.slices[0].times[0] == pytest.approx(0.0) + + +def test_slcf_times_monotonic(slcf_sim): + times = slcf_sim.slices[0].times + assert np.all(np.diff(times) > 0), "Time steps must be strictly monotonically increasing" + + +def test_slcf_data_shape_matches_times(slcf_sim): + slc = slcf_sim.slices[0] + data = slc.to_global(masked=True) + assert data.shape[0] == slc.n_t + + +def test_slcf_no_nan_inf(slcf_sim): + data = slcf_sim.slices[0].to_global(masked=True) + assert not np.isnan(data.data).any(), "NaN values found in SLCF data" + assert not np.isinf(data.data).any(), "Inf values found in SLCF data" + + +def test_slcf_quantity_has_name(slcf_sim): + assert slcf_sim.slices[0].quantity.name != "" diff --git a/tests/acceptance_tests/test_smoke3d.py b/tests/acceptance_tests/test_smoke3d.py index 1dc8b081..6e40b74d 100644 --- a/tests/acceptance_tests/test_smoke3d.py +++ b/tests/acceptance_tests/test_smoke3d.py @@ -1,10 +1,42 @@ +import numpy as np +import pytest + from fdsreader import Simulation -def test_smoke3d(): - sim = Simulation("./steckler_data") - smoke = sim.smoke_3d.get_by_quantity("Temperature") +@pytest.fixture(scope="module") +def smoke_sim(): + return Simulation("./steckler_data") + + +@pytest.fixture(scope="module") +def smoke(smoke_sim): + return smoke_sim.smoke_3d.get_by_quantity("Temperature") + + +def test_smoke3d(smoke): data, coordinates = smoke.to_global(masked=True, return_coordinates=True) + assert abs(data[-1, 13, 13, 1] - 77.0) < 1e-6 + assert abs(coordinates["x"][13] - 1.3) < 1e-6 + + +def test_smoke3d_times_non_empty(smoke): + assert smoke.n_t > 0 + + +def test_smoke3d_times_start_at_zero(smoke): + assert smoke.times[0] == pytest.approx(0.0) + + +def test_smoke3d_times_monotonic(smoke): + assert np.all(np.diff(smoke.times) > 0), "SMOKE3D time steps must be monotonically increasing" + + +def test_smoke3d_data_shape_matches_times(smoke): + data = smoke.to_global(masked=True) + assert data.shape[0] == smoke.n_t + - assert abs(data[-1, 13, 13, 1] - 77.) < 1e-6 - assert abs(coordinates['x'][13] - 1.3) < 1e-6 +def test_smoke3d_no_nan(smoke): + data = smoke.to_global(masked=True) + assert not np.isnan(data.data).any(), "NaN values found in SMOKE3D data" diff --git a/tests/cases/bndf_data_fds6100.tgz b/tests/cases/bndf_data_fds6100.tgz new file mode 100644 index 00000000..60144f29 --- /dev/null +++ b/tests/cases/bndf_data_fds6100.tgz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:721eab1c864cda3c5e69daaa6f570c50eb448b59085e8e71b532cb9ef2d980bb +size 1442952 diff --git a/tests/cases/devc_data_fds6100.tgz b/tests/cases/devc_data_fds6100.tgz new file mode 100644 index 00000000..2724220e --- /dev/null +++ b/tests/cases/devc_data_fds6100.tgz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:127c76909fcd92a9b699086e3498e33bf42706a899d784f5370df740853ec4cb +size 990919 diff --git a/tests/cases/fds_inputs/input_steckler.fds b/tests/cases/fds_inputs/input_steckler.fds new file mode 100644 index 00000000..b326d3c6 --- /dev/null +++ b/tests/cases/fds_inputs/input_steckler.fds @@ -0,0 +1,49 @@ +&HEAD CHID='Steckler_010', TITLE='Steckler Compartment, Test 10'/ + +&MESH IJK=36,28,22, XB=0.00,3.60,-1.40,1.40,0.00,2.13 / + +&TIME T_END=100.0, TIME_SHRINK_FACTOR=10. / +&DUMP NFRAMES=100, DT_DEVC=10., DT_HRR=10., SIG_FIGS=4, SIG_FIGS_EXP=2 / + +&MISC TMPA=22. / + +&SURF ID='BURNER',HRRPUA=1048.3,TMP_FRONT=100.,COLOR='ORANGE' / 62.9 kW + +&HOLE XB=2.75,2.95,-0.10,0.15,0.00,1.83 / 2/6 Door + +&VENT XB=1.30,1.50,-0.10,0.10,0.00,0.00, SURF_ID='BURNER' / Position A +&VENT XB=1.25,1.30,-0.05,0.05,0.00,0.00, SURF_ID='BURNER' / +&VENT XB=1.50,1.55,-0.05,0.05,0.00,0.00, SURF_ID='BURNER' / +&VENT XB=1.35,1.45,-0.15,-.10,0.00,0.00, SURF_ID='BURNER' / +&VENT XB=1.35,1.45, 0.10,0.15,0.00,0.00, SURF_ID='BURNER' / + +&SURF ID='FIBER BOARD', DEFAULT=.TRUE., MATL_ID='INSULATION', THICKNESS=0.013, COLOR='BEIGE' / + +&REAC FUEL='METHANE',SOOT_YIELD=0. / + +&OBST XB=2.80,2.90,-1.40,1.40,0.00,2.13 / Wall with door or window + +&VENT MB='XMAX',SURF_ID='OPEN'/ +&VENT XB= 2.90,3.60,-1.40,-1.40,0.00,2.13, SURF_ID='OPEN'/ +&VENT XB= 2.90,3.60, 1.40, 1.40,0.00,2.13, SURF_ID='OPEN'/ +&VENT XB= 2.90,3.60,-1.40, 1.40,2.13,2.13, SURF_ID='OPEN'/ + +&MATL ID = 'INSULATION' + DENSITY = 200. + CONDUCTIVITY = 0.1 + SPECIFIC_HEAT = 1. / + + +&ISOF QUANTITY='TEMPERATURE', VALUE(1)=10., VALUE(2)=30., VALUE(3)=60. / +&ISOF QUANTITY='U-VELOCITY' , VALUE(1)=0.08, QUANTITY2='TEMPERATURE' / + +&DEVC ID='TC_Room', POINTS=44, XB=2.50,2.50,1.10,1.10,0.02,2.11, QUANTITY='TEMPERATURE', Z_ID='Room_z' / +&DEVC ID='TC_Door', POINTS=38, XB=2.85,2.85,0.00,0.00,0.02,1.82, QUANTITY='TEMPERATURE', Z_ID='Door_z' / +&DEVC ID='BP_Door', POINTS=38, XB=2.85,2.85,0.00,0.00,0.02,1.82, QUANTITY='VELOCITY', VELO_INDEX=1, HIDE_COORDINATES=.TRUE. / + +&DEVC ID='HGL Temp', XB=2.50,2.50,1.10,1.10,0.00,2.13, QUANTITY='UPPER TEMPERATURE' / +&DEVC ID='HGL Height', XB=2.50,2.50,1.10,1.10,0.00,2.13, QUANTITY='LAYER HEIGHT' / + +&SLCF PBY=0.0,QUANTITY='TEMPERATURE',VECTOR=.TRUE./ + +&TAIL / diff --git a/tests/cases/fds_inputs/sphere_radiate.fds b/tests/cases/fds_inputs/sphere_radiate.fds new file mode 100644 index 00000000..5478769f --- /dev/null +++ b/tests/cases/fds_inputs/sphere_radiate.fds @@ -0,0 +1,1443 @@ +&HEAD CHID='sphere_radiate', TITLE='Complex Geometry: Test radiation from a sphere, 1 mesh.' / + +&MESH IJK=64,64,64, XB=-2.0,2.0,-2.0,2.0,0.0,4.0 / + +&TIME T_END=10, TIME_SHRINK_FACTOR=50. / +&PRES SOLVER='UGLMAT' / + +&SPEC ID='NITROGEN', BACKGROUND=.TRUE. / + +&RADI ANGLE_INCREMENT=1, TIME_STEP_INCREMENT=1, NUMBER_RADIATION_ANGLES=400, INITIAL_RADIATION_ITERATIONS=1 / + +&SURF ID='WALL', DEFAULT=.TRUE., TMP_FRONT=-273., TAU_T=0., HEAT_TRANSFER_COEFFICIENT=0., EMISSIVITY=1. / +&SURF ID='SPHERE', COLOR='GREEN', TMP_FRONT=500., TAU_T=0., HEAT_TRANSFER_COEFFICIENT=0., EMISSIVITY=1. / + +&BNDF QUANTITY='RADIATIVE HEAT FLUX', CELL_CENTERED=.TRUE. / + +&DEVC ID='HF1', QUANTITY='RADIATIVE HEAT FLUX', XYZ=-2, 0, 2, IOR= 1 / +&DEVC ID='HF2', QUANTITY='RADIATIVE HEAT FLUX', XYZ= 2, 0, 2, IOR=-1 / +&DEVC ID='HF3', QUANTITY='RADIATIVE HEAT FLUX', XYZ= 0,-2, 2, IOR= 2 / +&DEVC ID='HF4', QUANTITY='RADIATIVE HEAT FLUX', XYZ= 0, 2, 2, IOR=-2 / +&DEVC ID='HF5', QUANTITY='RADIATIVE HEAT FLUX', XYZ= 0, 0, 0, IOR= 3 / +&DEVC ID='HF6', QUANTITY='RADIATIVE HEAT FLUX', XYZ= 0, 0, 4, IOR=-3 / +&DEVC ID='HFT', QUANTITY='RADIATIVE HEAT FLUX', STATISTICS='SURFACE INTEGRAL', SURF_ID='WALL', XB=-2,2,-2,2,0,4 / + +&GEOM ID='SPHERE', SURF_ID='SPHERE' +VERTS= + 0.000, 0.000, 1.000, + 1.000, 0.000, 2.000, + 0.000, 0.000, 3.000, + -1.000, 0.000, 2.000, + 0.188, 0.000, 1.018, + 0.369, 0.000, 1.071, + 0.545, 0.000, 1.162, + 0.700, 0.000, 1.286, + 0.827, 0.000, 1.438, + 0.922, 0.000, 1.613, + 0.980, 0.000, 1.802, + 0.982, 0.000, 2.188, + 0.929, 0.000, 2.369, + 0.838, 0.000, 2.545, + 0.714, 0.000, 2.700, + 0.562, 0.000, 2.827, + 0.387, 0.000, 2.922, + 0.198, 0.000, 2.980, + -0.188, 0.000, 1.018, + -0.369, 0.000, 1.071, + -0.545, 0.000, 1.162, + -0.700, 0.000, 1.286, + -0.827, 0.000, 1.438, + -0.922, 0.000, 1.613, + -0.980, 0.000, 1.802, + -0.982, 0.000, 2.188, + -0.929, 0.000, 2.369, + -0.838, 0.000, 2.545, + -0.714, 0.000, 2.700, + -0.562, 0.000, 2.827, + -0.387, 0.000, 2.922, + -0.198, 0.000, 2.980, + -0.014, 0.092, 2.996, + -0.227, 0.133, 2.965, + -0.426, 0.158, 2.891, + -0.604, 0.174, 2.777, + -0.755, 0.181, 2.630, + -0.866, 0.182, 2.465, + -0.941, 0.176, 2.290, + -0.978, 0.178, 2.109, + -0.979, 0.188, 1.918, + -0.939, 0.190, 1.714, + -0.859, 0.186, 1.523, + -0.743, 0.180, 1.356, + -0.591, 0.174, 1.213, + -0.406, 0.160, 1.100, + -0.204, 0.131, 1.030, + 0.001, 0.084, 1.004, + 0.198, 0.168, 2.966, + 0.427, 0.174, 2.887, + 0.605, 0.177, 2.777, + 0.751, 0.181, 2.636, + 0.863, 0.183, 2.470, + 0.939, 0.185, 2.291, + 0.977, 0.188, 2.105, + 0.978, 0.190, 1.908, + 0.938, 0.187, 1.708, + 0.859, 0.183, 1.522, + 0.739, 0.178, 1.351, + 0.585, 0.167, 1.207, + 0.402, 0.151, 1.097, + 0.200, 0.127, 1.029, + -0.043, 0.241, 2.970, + -0.257, 0.294, 2.921, + -0.455, 0.331, 2.827, + -0.626, 0.351, 2.697, + -0.763, 0.359, 2.537, + -0.862, 0.352, 2.363, + -0.923, 0.322, 2.211, + -0.933, 0.358, 2.048, + -0.915, 0.370, 1.842, + -0.855, 0.371, 1.637, + -0.755, 0.365, 1.455, + -0.617, 0.354, 1.297, + -0.440, 0.337, 1.167, + -0.226, 0.300, 1.073, + -0.002, 0.229, 1.027, + 0.283, 0.336, 2.898, + 0.468, 0.340, 2.816, + 0.626, 0.347, 2.699, + 0.756, 0.357, 2.549, + 0.852, 0.356, 2.383, + 0.910, 0.360, 2.208, + 0.929, 0.370, 2.025, + 0.912, 0.371, 1.828, + 0.854, 0.368, 1.633, + 0.753, 0.358, 1.448, + 0.610, 0.343, 1.286, + 0.430, 0.320, 1.156, + 0.222, 0.285, 1.068, + 0.128, 0.326, 2.937, + -0.070, 0.401, 2.913, + -0.274, 0.463, 2.843, + -0.464, 0.501, 2.731, + -0.623, 0.514, 2.590, + -0.741, 0.521, 2.424, + -0.836, 0.507, 2.209, + -0.847, 0.532, 1.982, + -0.812, 0.534, 1.764, + -0.732, 0.534, 1.577, + -0.619, 0.520, 1.412, + -0.461, 0.508, 1.273, + -0.258, 0.480, 1.161, + -0.003, 0.420, 1.092, + 0.326, 0.489, 2.809, + 0.127, 0.466, 2.875, + 0.491, 0.473, 2.732, + 0.611, 0.512, 2.604, + 0.732, 0.517, 2.443, + 0.818, 0.493, 2.296, + 0.834, 0.533, 2.146, + 0.844, 0.535, 1.957, + 0.813, 0.532, 1.764, + 0.734, 0.531, 1.577, + 0.610, 0.515, 1.398, + 0.447, 0.487, 1.250, + 0.254, 0.448, 1.143, + -0.066, 0.564, 2.823, + -0.270, 0.628, 2.730, + -0.465, 0.649, 2.601, + -0.601, 0.641, 2.477, + -0.663, 0.683, 2.308, + -0.743, 0.661, 1.897, + -0.652, 0.703, 1.718, + -0.601, 0.647, 1.531, + -0.470, 0.655, 1.409, + -0.288, 0.643, 1.290, + -0.079, 0.603, 1.206, + 0.107, 0.561, 1.179, + 0.402, 0.628, 2.666, + 0.566, 0.661, 2.492, + 0.697, 0.659, 2.282, + 0.737, 0.674, 2.053, + 0.751, 0.650, 1.885, + 0.686, 0.677, 1.733, + 0.587, 0.667, 1.541, + 0.441, 0.643, 1.374, + 0.271, 0.606, 1.252, + -0.727, 0.681, 2.089, + 0.161, 0.622, 2.767, + -0.042, 0.722, 2.691, + -0.263, 0.770, 2.581, + -0.461, 0.783, 2.418, + -0.553, 0.814, 2.180, + -0.608, 0.792, 1.942, + -0.476, 0.773, 1.581, + -0.297, 0.781, 1.451, + -0.102, 0.749, 1.346, + 0.091, 0.691, 1.283, + 0.200, 0.762, 2.615, + 0.391, 0.757, 2.524, + 0.482, 0.798, 2.361, + 0.629, 0.771, 1.904, + 0.525, 0.799, 1.709, + 0.411, 0.771, 1.514, + 0.260, 0.737, 1.376, + 0.572, 0.810, 2.130, + -0.009, 0.861, 2.509, + -0.246, 0.870, 2.427, + -0.346, 0.904, 2.253, + -0.422, 0.906, 2.030, + -0.471, 0.856, 1.789, + -0.287, 0.890, 1.647, + -0.096, 0.872, 1.520, + 0.083, 0.813, 1.424, + 0.264, 0.863, 2.431, + 0.357, 0.910, 2.210, + 0.406, 0.911, 1.928, + 0.317, 0.890, 1.672, + 0.240, 0.837, 1.508, + -0.123, 0.946, 2.299, + -0.207, 0.973, 2.097, + -0.257, 0.958, 1.871, + -0.071, 0.960, 1.729, + 0.107, 0.912, 1.604, + 0.111, 0.949, 2.293, + 0.170, 0.983, 2.068, + 0.156, 0.972, 1.822, + -0.018, 0.989, 2.146, + -0.039, 0.998, 1.955, + -0.084, -0.110, 2.990, + -0.245, -0.145, 2.959, + -0.431, -0.166, 2.887, + -0.605, -0.178, 2.776, + -0.752, -0.183, 2.633, + -0.866, -0.179, 2.467, + -0.941, -0.170, 2.292, + -0.978, -0.174, 2.114, + -0.979, -0.188, 1.918, + -0.940, -0.190, 1.718, + -0.860, -0.187, 1.524, + -0.742, -0.180, 1.354, + -0.591, -0.168, 1.211, + -0.418, -0.152, 1.104, + -0.233, -0.132, 1.036, + -0.010, -0.113, 1.006, + 0.302, -0.157, 2.940, + 0.472, -0.128, 2.872, + 0.612, -0.162, 2.774, + 0.756, -0.175, 2.630, + 0.867, -0.183, 2.464, + 0.941, -0.186, 2.283, + 0.978, -0.186, 2.095, + 0.978, -0.185, 1.900, + 0.937, -0.185, 1.705, + 0.859, -0.183, 1.522, + 0.742, -0.176, 1.353, + 0.594, -0.168, 1.213, + 0.419, -0.160, 1.106, + 0.221, -0.146, 1.036, + 0.093, -0.146, 2.985, + -0.067, -0.251, 2.966, + -0.269, -0.312, 2.911, + -0.459, -0.342, 2.820, + -0.626, -0.355, 2.695, + -0.763, -0.354, 2.541, + -0.866, -0.336, 2.370, + -0.927, -0.307, 2.217, + -0.933, -0.355, 2.063, + -0.916, -0.374, 1.854, + -0.856, -0.368, 1.637, + -0.754, -0.356, 1.448, + -0.616, -0.343, 1.291, + -0.450, -0.317, 1.165, + -0.271, -0.280, 1.079, + -0.113, -0.237, 1.035, + 0.170, -0.339, 2.925, + 0.448, -0.310, 2.838, + 0.643, -0.335, 2.689, + 0.769, -0.348, 2.537, + 0.857, -0.363, 2.367, + 0.913, -0.365, 2.184, + 0.935, -0.356, 2.001, + 0.915, -0.356, 1.810, + 0.853, -0.360, 1.623, + 0.756, -0.353, 1.449, + 0.623, -0.332, 1.291, + 0.463, -0.319, 1.173, + 0.271, -0.311, 1.089, + 0.062, -0.275, 1.041, + -0.066, -0.425, 2.903, + -0.283, -0.481, 2.830, + -0.471, -0.503, 2.724, + -0.623, -0.512, 2.591, + -0.752, -0.504, 2.425, + -0.854, -0.463, 2.238, + -0.834, -0.551, 2.031, + -0.815, -0.532, 1.771, + -0.732, -0.520, 1.559, + -0.619, -0.504, 1.398, + -0.465, -0.484, 1.258, + -0.285, -0.441, 1.149, + -0.103, -0.375, 1.079, + 0.306, -0.514, 2.802, + 0.518, -0.495, 2.698, + 0.661, -0.469, 2.586, + 0.742, -0.518, 2.426, + 0.804, -0.533, 2.263, + 0.855, -0.514, 2.073, + 0.873, -0.481, 1.913, + 0.820, -0.517, 1.753, + 0.737, -0.523, 1.572, + 0.626, -0.498, 1.400, + 0.508, -0.456, 1.269, + 0.323, -0.501, 1.197, + 0.099, -0.439, 1.107, + 0.095, -0.522, 2.848, + -0.090, -0.596, 2.798, + -0.306, -0.635, 2.710, + -0.475, -0.628, 2.616, + -0.576, -0.665, 2.476, + -0.726, -0.632, 2.273, + -0.735, -0.670, 1.892, + -0.680, -0.662, 1.685, + -0.608, -0.627, 1.514, + -0.472, -0.634, 1.388, + -0.290, -0.601, 1.255, + -0.096, -0.539, 1.163, + 0.140, -0.680, 2.720, + 0.396, -0.656, 2.643, + 0.582, -0.614, 2.533, + 0.665, -0.663, 2.344, + 0.735, -0.664, 2.137, + 0.782, -0.620, 1.932, + 0.698, -0.671, 1.750, + 0.597, -0.661, 1.545, + 0.463, -0.602, 1.350, + 0.107, -0.619, 1.222, + -0.673, -0.734, 2.097, + 0.289, -0.685, 1.331, + -0.107, -0.744, 2.659, + -0.364, -0.764, 2.532, + -0.545, -0.774, 2.321, + -0.566, -0.814, 1.870, + -0.489, -0.764, 1.580, + -0.292, -0.746, 1.401, + -0.094, -0.692, 1.284, + 0.258, -0.802, 2.540, + 0.479, -0.756, 2.446, + 0.567, -0.794, 2.219, + 0.643, -0.765, 1.974, + 0.507, -0.825, 1.751, + 0.421, -0.758, 1.502, + 0.102, -0.765, 1.364, + -0.468, -0.874, 2.128, + 0.054, -0.812, 2.581, + 0.252, -0.804, 1.462, + -0.126, -0.862, 2.490, + -0.315, -0.888, 2.334, + -0.359, -0.898, 1.747, + -0.249, -0.867, 1.568, + -0.075, -0.825, 1.439, + 0.101, -0.894, 2.437, + 0.330, -0.887, 2.324, + 0.444, -0.895, 2.040, + 0.295, -0.880, 1.628, + -0.232, -0.962, 2.145, + -0.350, -0.936, 1.956, + 0.287, -0.947, 1.858, + 0.111, -0.874, 1.527, + -0.079, -0.947, 2.313, + -0.191, -0.937, 1.706, + -0.223, -0.961, 1.835, + 0.117, -0.956, 2.268, + 0.224, -0.969, 2.100, + 0.113, -0.952, 1.717, + -0.043, -0.917, 1.603, + -0.005, -0.992, 2.126, + 0.077, -0.995, 1.931, + -0.061, -0.977, 1.794, + -0.139, -0.990, 1.981, + 0.284, 0.724, 1.954, + 0.078, 0.489, 1.911, + 0.287, 0.410, 2.062, + 0.128, 0.741, 2.051, + 0.285, 0.420, 1.848, + 0.430, 0.609, 2.097, + 0.479, 0.587, 1.927, + -0.008, -0.209, 1.821, + 0.141, -0.160, 1.528, + -0.050, -0.278, 1.567, + -0.128, -0.114, 1.598, + 0.564, 0.516, 2.041, + 0.480, 0.415, 1.926, + 0.526, 0.498, 2.213, + 0.365, 0.605, 2.273, + 0.117, 0.731, 1.866, + 0.240, 0.675, 1.751, + -0.010, 0.520, 2.077, + -0.146, 0.453, 1.821, + -0.006, 0.269, 2.042, + -0.520, -0.554, 2.075, + 0.104, 0.364, 1.787, + -0.030, 0.757, 1.966, + 0.150, 0.534, 1.685, + -0.252, -0.273, 1.782, + -0.178, -0.131, 1.839, + 0.000, 0.001, 1.760, + -0.194, 0.486, 2.558, + -0.038, 0.295, 2.418, + -0.340, 0.485, 2.449, + -0.128, 0.447, 2.231, + 0.162, -0.506, 1.656, + 0.081, -0.469, 1.409, + -0.039, -0.426, 1.710, + 0.075, -0.334, 1.320, + -0.072, -0.151, 1.380, + -0.214, -0.340, 1.343, + 0.109, -0.348, 1.767, + -0.071, -0.526, 1.455, + -0.219, -0.462, 1.434, + -0.074, -0.411, 1.361, + 0.408, -0.606, 2.168, + 0.476, -0.568, 1.992, + 0.179, -0.730, 2.063, + 0.247, -0.663, 2.242, + 0.362, -0.572, 2.337, + 0.257, -0.385, 2.226, + 0.560, -0.507, 2.105, + -0.032, 0.545, 2.521, + 0.205, 0.397, 2.275, + -0.157, 0.740, 2.074, + -0.014, 0.760, 2.112, + -0.262, 0.685, 2.192, + -0.080, -0.531, 2.001, + -0.262, -0.715, 1.955, + -0.099, -0.418, 1.864, + -0.123, -0.601, 1.811, + 0.151, 0.577, 2.465, + 0.084, 0.717, 2.222, + 0.122, 0.472, 2.581, + -0.187, 0.664, 2.326, + -0.319, 0.685, 2.022, + -0.194, 0.723, 1.903, + -0.356, 0.647, 1.841, + -0.418, 0.615, 2.136, + -0.461, 0.601, 1.956, + 0.232, 0.001, 1.416, + 0.207, -0.237, 1.305, + 0.000, 0.001, 1.344, + 0.156, 0.010, 1.192, + 0.329, 0.245, 1.356, + -0.002, 0.175, 1.257, + 0.158, 0.007, 1.612, + 0.000, 0.001, 1.542, + 0.114, 0.148, 1.519, + 0.300, -0.495, 2.486, + 0.195, -0.606, 2.408, + 0.106, -0.512, 2.542, + 0.230, -0.396, 2.616, + -0.584, -0.271, 2.414, + 0.390, -0.379, 2.534, + 0.029, -0.425, 2.304, + 0.063, -0.494, 2.134, + 0.051, -0.270, 2.441, + 0.024, -0.215, 2.164, + 0.139, -0.117, 2.131, + 0.508, -0.506, 2.263, + 0.420, -0.297, 2.372, + 0.446, -0.469, 2.408, + 0.090, -0.733, 2.205, + 0.041, -0.630, 2.444, + 0.305, 0.476, 2.505, + 0.467, 0.392, 2.463, + 0.430, 0.503, 2.374, + 0.311, 0.300, 2.463, + -0.800, -0.142, 2.094, + -0.766, -0.253, 2.179, + -0.714, -0.271, 2.048, + -0.771, -0.139, 2.240, + -0.755, 0.001, 2.142, + -0.073, 0.662, 1.636, + -0.217, 0.673, 1.733, + -0.054, 0.726, 1.796, + 0.082, 0.695, 1.698, + -0.561, -0.398, 1.662, + -0.171, 0.228, 1.296, + -0.116, 0.155, 1.525, + -0.003, 0.317, 1.316, + -0.155, 0.000, 1.192, + -0.311, 0.123, 1.312, + -0.334, 0.257, 1.367, + -0.232, 0.001, 1.415, + -0.196, 0.364, 1.365, + 0.324, -0.583, 1.613, + 0.233, -0.665, 1.716, + 0.435, -0.508, 1.663, + 0.432, -0.007, 1.714, + 0.207, -0.112, 1.982, + 0.381, -0.210, 1.961, + 0.509, 0.001, 1.897, + 0.717, -0.141, 1.774, + 0.705, 0.000, 1.704, + 0.251, 0.051, 2.078, + 0.510, -0.096, 2.049, + 0.490, 0.097, 2.152, + 0.716, 0.143, 1.777, + -0.770, 0.144, 2.237, + -0.584, 0.210, 2.141, + -0.759, 0.265, 2.174, + -0.661, 0.271, 2.279, + 0.746, 0.145, 1.930, + 0.749, 0.000, 1.849, + -0.594, -0.196, 2.139, + 0.120, 0.117, 2.160, + 0.229, 0.188, 2.312, + 0.344, 0.216, 2.133, + -0.439, -0.506, 2.362, + -0.664, -0.137, 2.358, + -0.709, -0.275, 2.303, + -0.522, -0.519, 1.754, + -0.556, -0.521, 1.911, + -0.281, -0.685, 1.803, + -0.379, -0.345, 1.944, + -0.368, -0.573, 1.685, + -0.499, -0.515, 1.601, + -0.385, -0.394, 1.692, + -0.280, 0.413, 2.091, + -0.441, -0.189, 1.813, + -0.576, -0.272, 1.578, + -0.435, 0.000, 1.712, + -0.322, -0.192, 1.622, + 0.250, 0.375, 2.620, + 0.220, 0.254, 2.699, + 0.360, 0.262, 2.627, + 0.097, 0.358, 2.672, + 0.083, 0.204, 2.595, + -0.054, -0.071, 2.640, + 0.128, -0.255, 2.696, + 0.104, 0.001, 2.516, + 0.300, -0.080, 2.556, + 0.287, -0.201, 2.251, + -0.024, -0.022, 2.232, + 0.206, -0.055, 2.381, + -0.060, 0.460, 1.395, + 0.070, 0.532, 1.449, + 0.067, 0.354, 1.483, + 0.088, 0.460, 1.327, + 0.558, -0.399, 1.674, + 0.630, -0.397, 1.811, + 0.529, -0.508, 1.811, + 0.578, -0.269, 1.578, + 0.487, -0.390, 1.539, + 0.058, -0.757, 1.947, + -0.107, -0.759, 1.985, + -0.004, -0.757, 2.096, + 0.640, 0.409, 2.112, + -0.259, 0.280, 1.797, + -0.250, 0.015, 1.896, + -0.392, 0.348, 1.945, + -0.403, 0.192, 1.706, + -0.712, 0.148, 1.778, + -0.508, 0.098, 1.958, + -0.582, 0.281, 1.594, + -0.378, 0.411, 1.704, + -0.652, 0.283, 1.723, + -0.656, 0.142, 1.636, + 0.309, 0.004, 1.229, + -0.567, 0.138, 1.508, + 0.561, 0.396, 2.340, + 0.656, 0.274, 2.295, + 0.673, 0.406, 2.243, + 0.520, 0.314, 2.188, + 0.394, 0.187, 2.287, + -0.566, 0.398, 2.323, + -0.492, 0.525, 2.391, + -0.477, 0.395, 2.452, + -0.377, 0.393, 2.309, + -0.567, -0.135, 1.513, + -0.286, 0.001, 1.561, + -0.632, 0.000, 1.571, + -0.540, 0.011, 1.449, + -0.656, -0.142, 1.637, + -0.481, -0.396, 2.454, + -0.299, -0.385, 2.397, + -0.390, -0.515, 2.505, + -0.389, -0.260, 2.220, + 0.221, -0.523, 1.490, + 0.207, -0.660, 1.558, + 0.078, -0.585, 1.513, + 0.697, 0.284, 1.868, + 0.446, 0.199, 1.803, + 0.624, 0.409, 1.819, + 0.649, 0.412, 1.967, + -0.653, -0.353, 2.182, + -0.372, -0.392, 2.565, + -0.574, -0.384, 2.325, + -0.549, -0.477, 2.206, + -0.627, -0.414, 2.023, + 0.244, -0.379, 1.393, + -0.159, -0.775, 1.757, + -0.050, -0.800, 1.831, + -0.190, -0.660, 1.671, + -0.035, -0.749, 1.676, + -0.463, -0.136, 2.593, + -0.431, 0.000, 2.634, + -0.241, -0.003, 2.585, + -0.377, -0.002, 2.369, + -0.343, -0.267, 2.619, + -0.338, -0.128, 2.687, + -0.546, 0.000, 2.535, + -0.575, -0.139, 2.484, + 0.652, 0.281, 1.720, + 0.656, 0.140, 1.635, + -0.698, 0.282, 1.879, + -0.645, 0.406, 1.986, + -0.619, 0.408, 1.820, + -0.748, 0.144, 1.938, + -0.714, 0.274, 2.037, + 0.304, 0.088, 1.587, + 0.568, 0.147, 1.502, + 0.574, 0.273, 1.580, + 0.539, 0.002, 1.449, + 0.633, 0.000, 1.570, + 0.657, -0.139, 1.634, + 0.459, -0.188, 1.792, + 0.568, -0.134, 1.505, + 0.311, -0.087, 1.588, + 0.230, -0.186, 1.682, + 0.164, 0.047, 1.788, + -0.057, -0.629, 1.572, + 0.121, -0.110, 1.837, + 0.340, -0.351, 1.867, + 0.324, -0.290, 1.541, + 0.039, 0.230, 1.650, + 0.230, 0.334, 1.675, + 0.661, 0.141, 2.360, + 0.579, 0.274, 2.421, + 0.574, 0.139, 2.486, + 0.719, 0.142, 2.223, + 0.711, 0.000, 2.282, + 0.641, 0.000, 2.417, + -0.349, -0.644, 2.083, + -0.272, -0.392, 2.161, + -0.176, -0.730, 2.110, + 0.663, -0.140, 2.355, + 0.000, 0.000, 2.829, + -0.244, -0.657, 2.257, + 0.600, -0.476, 1.948, + 0.699, -0.420, 2.060, + 0.721, -0.397, 1.928, + 0.559, -0.307, 1.944, + 0.700, 0.277, 2.160, + 0.748, 0.144, 2.080, + 0.752, 0.000, 2.144, + 0.170, 0.149, 1.974, + 0.484, 0.194, 2.013, + 0.330, 0.289, 1.949, + -0.001, 0.000, 1.999, + -0.059, -0.724, 2.238, + -0.144, 0.141, 2.122, + -0.255, 0.275, 2.221, + 0.403, 0.389, 2.601, + 0.481, 0.267, 2.537, + -0.695, -0.283, 1.890, + -0.747, -0.143, 1.937, + -0.507, -0.096, 1.957, + -0.766, 0.000, 2.000, + -0.528, 0.008, 2.094, + -0.702, 0.001, 1.708, + -0.419, 0.000, 1.356, + -0.652, -0.280, 1.724, + -0.717, -0.145, 1.785, + -0.748, 0.000, 1.849, + -0.330, -0.185, 2.370, + -0.317, 0.000, 2.754, + -0.462, 0.133, 2.594, + -0.476, -0.387, 1.537, + -0.358, -0.372, 1.430, + 0.465, 0.262, 1.455, + -0.364, -0.496, 1.527, + -0.093, -0.196, 1.201, + 0.051, -0.224, 1.216, + -0.223, -0.229, 1.244, + -0.713, 0.000, 2.283, + -0.641, 0.000, 2.417, + -0.448, -0.100, 2.247, + 0.421, 0.012, 1.355, + 0.194, 0.343, 1.346, + -0.050, 0.394, 1.663, + 0.548, 0.406, 1.681, + 0.448, 0.509, 1.650, + 0.526, 0.519, 1.795, + 0.458, 0.384, 1.551, + 0.456, -0.128, 1.396, + 0.322, -0.123, 1.314, + 0.341, 0.372, 1.427, + 0.666, -0.283, 1.712, + 0.479, -0.255, 1.455, + 0.720, -0.142, 2.217, + 0.657, -0.278, 2.281, + -0.042, -0.227, 1.940, + 0.421, -0.278, 2.138, + 0.620, 0.536, 1.905, + 0.711, 0.284, 2.019, + -0.203, -0.234, 2.694, + -0.064, -0.458, 2.616, + -0.051, -0.192, 2.741, + 0.159, 0.001, 2.752, + 0.105, 0.268, 2.770, + -0.033, 0.185, 2.743, + 0.076, -0.119, 2.805, + -0.238, 0.263, 1.625, + -0.452, 0.133, 1.399, + -0.471, 0.270, 1.464, + -0.132, 0.155, 1.888, + -0.347, 0.393, 1.445, + -0.474, 0.399, 1.539, + 0.748, -0.291, 1.845, + -0.577, 0.139, 2.481, + -0.478, 0.268, 2.532, + -0.583, 0.275, 2.411, + -0.663, 0.140, 2.356, + -0.091, -0.669, 2.385, + 0.591, -0.267, 2.413, + 0.395, -0.091, 2.329, + 0.492, -0.256, 2.528, + -0.342, -0.124, 1.267, + -0.345, -0.243, 1.360, + 0.086, -0.671, 1.636, + 0.087, -0.727, 1.784, + -0.050, 0.429, 2.626, + 0.216, -0.712, 1.893, + 0.062, -0.492, 1.858, + 0.765, -0.291, 2.000, + -0.552, 0.518, 2.067, + -0.571, 0.509, 1.921, + -0.502, 0.518, 2.234, + -0.632, 0.383, 2.158, + 0.385, -0.103, 2.723, + 0.338, -0.233, 2.632, + 0.470, -0.124, 2.594, + 0.299, 0.012, 2.710, + 0.459, 0.000, 2.676, + -0.152, -0.170, 2.474, + -0.129, -0.144, 2.204, + -0.205, -0.270, 2.266, + 0.064, 0.625, 1.558, + 0.331, 0.501, 1.512, + -0.402, 0.141, 2.092, + -0.410, -0.131, 2.095, + -0.219, 0.490, 1.460, + -0.214, -0.066, 2.063, + -0.169, 0.010, 2.378, + 0.056, 0.143, 2.407, + -0.225, 0.594, 1.583, + -0.201, -0.119, 2.786, + -0.163, 0.000, 2.808, + 0.417, -0.375, 1.400, + -0.038, -0.048, 2.443, + 0.315, 0.093, 2.405, + -0.009, 0.058, 2.627, + 0.765, 0.000, 2.000, + 0.300, 0.581, 2.403, + -0.360, 0.502, 1.548, + 0.046, 0.173, 1.854, + -0.302, 0.000, 1.238, + -0.620, -0.404, 1.826, + 0.700, -0.279, 2.141, + 0.749, -0.142, 2.073, + -0.143, 0.168, 2.483, + -0.224, -0.475, 2.536, + -0.274, -0.574, 2.400, + 0.212, 0.603, 1.490, + 0.320, 0.596, 1.630, + 0.198, 0.688, 1.596, + 0.398, 0.605, 1.780, + -0.077, 0.572, 1.501, + -0.753, 0.135, 2.083, + -0.348, 0.591, 2.315, + -0.220, -0.560, 1.559, + -0.494, 0.533, 1.786, + -0.327, 0.122, 2.683, + -0.350, 0.385, 2.558, + 0.749, -0.141, 1.923, + 0.571, -0.398, 2.328, + 0.544, -0.386, 2.482, + -0.326, 0.181, 2.369, + 0.463, 0.136, 2.594, + -0.456, 0.094, 2.239, + 0.353, -0.459, 1.503, + -0.354, 0.259, 2.627, + 0.208, 0.466, 1.425, + -0.081, -0.563, 2.499, + -0.194, 0.235, 2.701, + -0.482, -0.252, 1.455, + -0.477, -0.277, 2.533, + -0.070, -0.091, 2.822, + -0.560, 0.409, 1.676, + -0.493, 0.531, 1.615, + 0.011, -0.009, 1.154, + 0.072, -0.398, 2.648, + 0.547, 0.000, 2.536, + 0.580, -0.134, 2.483, + -0.362, 0.589, 1.681, +FACES= + 289, 294, 305, 1, + 215, 185, 216, 1, + 274, 248, 249, 1, + 271, 272, 293, 1, + 209, 6, 7, 1, + 243, 242, 214, 1, + 330, 326, 329, 1, + 318, 331, 317, 1, + 14, 201, 13, 1, + 18, 49, 33, 1, + 317, 321, 309, 1, + 45, 46, 21, 1, + 115, 87, 88, 1, + 46, 76, 47, 1, + 320, 326, 327, 1, + 66, 94, 95, 1, + 171, 179, 172, 1, + 280, 255, 254, 1, + 306, 308, 313, 1, + 60, 89, 88, 1, + 69, 68, 97, 1, + 313, 298, 306, 1, + 195, 196, 226, 1, + 264, 287, 265, 1, + 46, 47, 20, 1, + 98, 71, 70, 1, + 156, 137, 138, 1, + 94, 66, 65, 1, + 314, 315, 300, 1, + 180, 177, 178, 1, + 50, 49, 17, 1, + 38, 37, 67, 1, + 45, 75, 46, 1, + 325, 315, 314, 1, + 158, 171, 159, 1, + 5, 6, 210, 1, + 313, 314, 298, 1, + 177, 176, 167, 1, + 24, 25, 42, 1, + 279, 298, 280, 1, + 142, 120, 119, 1, + 163, 164, 147, 1, + 127, 148, 128, 1, + 174, 180, 178, 1, + 21, 46, 20, 1, + 128, 104, 103, 1, + 152, 167, 166, 1, + 274, 249, 275, 1, + 224, 194, 225, 1, + 210, 196, 5, 1, + 176, 179, 171, 1, + 53, 14, 13, 1, + 166, 176, 158, 1, + 59, 58, 9, 1, + 105, 78, 79, 1, + 14, 200, 201, 1, + 230, 229, 256, 1, + 15, 16, 199, 1, + 176, 171, 158, 1, + 43, 42, 72, 1, + 160, 171, 172, 1, + 311, 322, 310, 1, + 67, 68, 38, 1, + 139, 97, 122, 1, + 98, 97, 139, 1, + 122, 144, 139, 1, + 97, 98, 70, 1, + 139, 123, 98, 1, + 123, 99, 98, 1, + 108, 109, 131, 1, + 106, 105, 140, 1, + 130, 131, 151, 1, + 132, 152, 131, 1, + 165, 148, 164, 1, + 162, 145, 161, 1, + 139, 144, 145, 1, + 125, 101, 100, 1, + 172, 180, 173, 1, + 161, 173, 162, 1, + 177, 179, 176, 1, + 164, 163, 174, 1, + 170, 165, 175, 1, + 164, 174, 175, 1, + 175, 165, 164, 1, + 179, 177, 180, 1, + 87, 59, 88, 1, + 156, 155, 137, 1, + 115, 116, 137, 1, + 176, 166, 167, 1, + 163, 162, 173, 1, + 172, 173, 161, 1, + 124, 123, 145, 1, + 107, 130, 105, 1, + 168, 153, 154, 1, + 105, 130, 140, 1, + 151, 166, 150, 1, + 140, 150, 141, 1, + 133, 111, 112, 1, + 152, 157, 167, 1, + 144, 122, 143, 1, + 140, 130, 150, 1, + 130, 151, 150, 1, + 151, 152, 166, 1, + 106, 140, 118, 1, + 160, 159, 171, 1, + 68, 96, 97, 1, + 144, 160, 161, 1, + 145, 123, 139, 1, + 74, 101, 102, 1, + 56, 85, 84, 1, + 113, 112, 85, 1, + 115, 137, 136, 1, + 137, 155, 136, 1, + 128, 129, 104, 1, + 61, 60, 7, 1, + 129, 149, 138, 1, + 128, 148, 149, 1, + 121, 120, 143, 1, + 120, 95, 94, 1, + 121, 95, 120, 1, + 157, 152, 132, 1, + 132, 133, 157, 1, + 127, 102, 126, 1, + 127, 128, 103, 1, + 102, 103, 75, 1, + 102, 127, 103, 1, + 34, 63, 64, 1, + 93, 94, 65, 1, + 119, 120, 94, 1, + 132, 110, 111, 1, + 110, 82, 83, 1, + 132, 111, 133, 1, + 103, 104, 76, 1, + 311, 312, 327, 1, + 193, 22, 21, 1, + 74, 102, 75, 1, + 67, 66, 95, 1, + 63, 91, 92, 1, + 83, 84, 111, 1, + 76, 104, 77, 1, + 76, 75, 103, 1, + 76, 46, 75, 1, + 129, 117, 104, 1, + 287, 286, 303, 1, + 96, 95, 121, 1, + 67, 37, 66, 1, + 28, 185, 29, 1, + 68, 67, 96, 1, + 41, 42, 25, 1, + 96, 121, 122, 1, + 67, 95, 96, 1, + 84, 85, 112, 1, + 57, 58, 86, 1, + 136, 114, 115, 1, + 138, 137, 116, 1, + 87, 58, 59, 1, + 253, 240, 266, 1, + 117, 89, 90, 1, + 78, 105, 106, 1, + 91, 63, 49, 1, + 80, 81, 108, 1, + 16, 50, 17, 1, + 80, 79, 51, 1, + 51, 79, 50, 1, + 16, 51, 50, 1, + 130, 107, 108, 1, + 322, 327, 330, 1, + 99, 123, 124, 1, + 43, 72, 73, 1, + 76, 77, 47, 1, + 43, 44, 23, 1, + 64, 93, 65, 1, + 35, 64, 65, 1, + 34, 64, 35, 1, + 320, 307, 316, 1, + 86, 58, 87, 1, + 84, 83, 55, 1, + 57, 56, 11, 1, + 203, 233, 204, 1, + 87, 114, 86, 1, + 57, 85, 56, 1, + 291, 292, 308, 1, + 64, 92, 93, 1, + 63, 33, 49, 1, + 289, 272, 247, 1, + 41, 40, 70, 1, + 69, 39, 68, 1, + 145, 162, 124, 1, + 6, 62, 61, 1, + 59, 60, 88, 1, + 6, 61, 7, 1, + 257, 258, 231, 1, + 5, 48, 62, 1, + 62, 6, 5, 1, + 77, 104, 90, 1, + 48, 5, 1, 1, + 2, 55, 12, 1, + 83, 82, 54, 1, + 84, 55, 56, 1, + 12, 55, 54, 1, + 54, 55, 83, 1, + 21, 20, 194, 1, + 48, 19, 47, 1, + 43, 73, 44, 1, + 258, 257, 282, 1, + 47, 77, 48, 1, + 68, 39, 38, 1, + 40, 26, 39, 1, + 69, 70, 40, 1, + 41, 4, 40, 1, + 28, 38, 27, 1, + 218, 246, 217, 1, + 38, 39, 27, 1, + 249, 248, 221, 1, + 226, 196, 240, 1, + 61, 89, 60, 1, + 62, 90, 61, 1, + 13, 12, 54, 1, + 13, 202, 12, 1, + 51, 52, 80, 1, + 53, 13, 54, 1, + 34, 33, 63, 1, + 253, 278, 252, 1, + 186, 28, 27, 1, + 322, 311, 327, 1, + 18, 33, 3, 1, + 268, 291, 279, 1, + 229, 255, 256, 1, + 255, 229, 228, 1, + 281, 280, 299, 1, + 255, 280, 281, 1, + 304, 307, 320, 1, + 290, 307, 304, 1, + 303, 290, 287, 1, + 303, 307, 290, 1, + 312, 320, 327, 1, + 238, 237, 264, 1, + 19, 196, 195, 1, + 210, 239, 240, 1, + 20, 47, 19, 1, + 240, 196, 210, 1, + 288, 304, 297, 1, + 328, 321, 317, 1, + 328, 325, 324, 1, + 325, 329, 319, 1, + 330, 323, 322, 1, + 329, 325, 328, 1, + 324, 325, 314, 1, + 313, 308, 321, 1, + 237, 238, 208, 1, + 307, 303, 316, 1, + 315, 319, 302, 1, + 319, 329, 326, 1, + 248, 247, 220, 1, + 328, 331, 329, 1, + 308, 292, 309, 1, + 293, 309, 292, 1, + 312, 297, 304, 1, + 304, 320, 312, 1, + 311, 296, 312, 1, + 77, 90, 62, 1, + 60, 8, 7, 1, + 277, 297, 296, 1, + 276, 277, 296, 1, + 312, 296, 297, 1, + 311, 295, 296, 1, + 168, 178, 177, 1, + 177, 167, 168, 1, + 211, 197, 18, 1, + 34, 35, 31, 1, + 254, 267, 279, 1, + 324, 314, 313, 1, + 291, 308, 306, 1, + 291, 269, 292, 1, + 212, 241, 227, 1, + 305, 293, 289, 1, + 218, 188, 219, 1, + 271, 293, 292, 1, + 293, 272, 289, 1, + 303, 302, 316, 1, + 56, 2, 11, 1, + 259, 258, 283, 1, + 237, 208, 207, 1, + 246, 218, 219, 1, + 303, 286, 302, 1, + 286, 262, 285, 1, + 263, 286, 287, 1, + 250, 276, 275, 1, + 250, 275, 249, 1, + 276, 251, 277, 1, + 295, 275, 276, 1, + 181, 211, 3, 1, + 268, 241, 242, 1, + 242, 269, 268, 1, + 269, 242, 243, 1, + 200, 229, 230, 1, + 231, 232, 202, 1, + 283, 258, 282, 1, + 281, 282, 257, 1, + 44, 22, 23, 1, + 242, 241, 213, 1, + 259, 260, 233, 1, + 284, 261, 260, 1, + 232, 258, 259, 1, + 262, 235, 261, 1, + 233, 232, 259, 1, + 259, 284, 260, 1, + 276, 250, 251, 1, + 225, 195, 226, 1, + 251, 252, 277, 1, + 223, 251, 250, 1, + 297, 277, 278, 1, + 252, 251, 224, 1, + 203, 12, 202, 1, + 204, 233, 234, 1, + 235, 262, 236, 1, + 233, 260, 234, 1, + 253, 226, 240, 1, + 225, 252, 224, 1, + 252, 225, 253, 1, + 266, 240, 239, 1, + 278, 288, 297, 1, + 226, 253, 225, 1, + 214, 215, 243, 1, + 4, 26, 40, 1, + 187, 217, 186, 1, + 216, 185, 186, 1, + 245, 216, 217, 1, + 270, 271, 292, 1, + 8, 9, 207, 1, + 263, 287, 264, 1, + 58, 57, 10, 1, + 10, 206, 9, 1, + 236, 263, 237, 1, + 206, 207, 9, 1, + 6, 209, 210, 1, + 209, 238, 239, 1, + 207, 236, 237, 1, + 228, 229, 199, 1, + 231, 230, 257, 1, + 219, 188, 189, 1, + 64, 63, 92, 1, + 289, 247, 273, 1, + 43, 23, 24, 1, + 208, 209, 7, 1, + 265, 287, 290, 1, + 265, 266, 239, 1, + 210, 209, 239, 1, + 208, 238, 209, 1, + 200, 199, 229, 1, + 203, 232, 233, 1, + 202, 13, 201, 1, + 1, 196, 19, 1, + 231, 258, 232, 1, + 231, 202, 201, 1, + 194, 193, 21, 1, + 194, 20, 195, 1, + 193, 194, 224, 1, + 251, 223, 224, 1, + 22, 45, 21, 1, + 250, 249, 222, 1, + 216, 186, 217, 1, + 27, 187, 186, 1, + 31, 35, 30, 1, + 28, 186, 185, 1, + 35, 65, 36, 1, + 8, 208, 7, 1, + 59, 9, 8, 1, + 207, 208, 8, 1, + 53, 52, 14, 1, + 200, 14, 15, 1, + 200, 15, 199, 1, + 201, 230, 231, 1, + 192, 191, 23, 1, + 223, 192, 193, 1, + 45, 22, 44, 1, + 192, 23, 22, 1, + 189, 190, 220, 1, + 221, 222, 249, 1, + 184, 214, 183, 1, + 214, 242, 213, 1, + 29, 184, 30, 1, + 184, 183, 30, 1, + 244, 245, 271, 1, + 236, 207, 206, 1, + 82, 110, 109, 1, + 205, 206, 10, 1, + 79, 78, 50, 1, + 17, 198, 16, 1, + 190, 189, 25, 1, + 24, 23, 191, 1, + 190, 24, 191, 1, + 189, 188, 4, 1, + 42, 43, 24, 1, + 221, 190, 191, 1, + 25, 24, 190, 1, + 222, 221, 191, 1, + 214, 213, 183, 1, + 183, 31, 30, 1, + 228, 227, 254, 1, + 182, 31, 183, 1, + 32, 31, 182, 1, + 158, 150, 166, 1, + 121, 143, 122, 1, + 97, 96, 122, 1, + 174, 173, 180, 1, + 180, 172, 179, 1, + 144, 143, 160, 1, + 161, 160, 172, 1, + 158, 141, 150, 1, + 141, 142, 119, 1, + 119, 118, 141, 1, + 141, 118, 140, 1, + 115, 114, 87, 1, + 136, 154, 135, 1, + 135, 154, 153, 1, + 135, 114, 136, 1, + 82, 81, 53, 1, + 152, 151, 131, 1, + 110, 132, 109, 1, + 100, 73, 72, 1, + 101, 74, 73, 1, + 146, 163, 147, 1, + 126, 146, 147, 1, + 101, 126, 102, 1, + 118, 93, 92, 1, + 157, 168, 167, 1, + 52, 81, 80, 1, + 53, 54, 82, 1, + 66, 36, 65, 1, + 35, 36, 30, 1, + 36, 66, 37, 1, + 9, 58, 10, 1, + 153, 133, 134, 1, + 91, 49, 78, 1, + 106, 92, 91, 1, + 71, 41, 70, 1, + 100, 72, 99, 1, + 41, 71, 42, 1, + 32, 34, 31, 1, + 33, 34, 32, 1, + 39, 69, 40, 1, + 97, 70, 69, 1, + 74, 75, 45, 1, + 74, 44, 73, 1, + 38, 28, 37, 1, + 36, 37, 29, 1, + 36, 29, 30, 1, + 60, 59, 8, 1, + 14, 52, 15, 1, + 193, 192, 22, 1, + 28, 29, 37, 1, + 29, 185, 184, 1, + 309, 293, 305, 1, + 295, 276, 296, 1, + 273, 274, 294, 1, + 318, 305, 294, 1, + 331, 328, 317, 1, + 317, 305, 318, 1, + 3, 32, 181, 1, + 49, 18, 17, 1, + 32, 182, 181, 1, + 211, 18, 3, 1, + 198, 197, 228, 1, + 309, 321, 308, 1, + 328, 324, 321, 1, + 313, 321, 324, 1, + 316, 302, 319, 1, + 301, 315, 302, 1, + 319, 315, 325, 1, + 316, 319, 326, 1, + 294, 295, 310, 1, + 294, 289, 273, 1, + 288, 290, 304, 1, + 278, 253, 266, 1, + 288, 278, 266, 1, + 252, 278, 277, 1, + 245, 272, 271, 1, + 216, 245, 244, 1, + 168, 169, 178, 1, + 302, 285, 301, 1, + 285, 261, 284, 1, + 234, 260, 261, 1, + 274, 275, 295, 1, + 288, 265, 290, 1, + 266, 265, 288, 1, + 282, 281, 299, 1, + 281, 256, 255, 1, + 256, 281, 257, 1, + 219, 220, 247, 1, + 223, 250, 222, 1, + 223, 193, 224, 1, + 182, 183, 213, 1, + 241, 212, 213, 1, + 184, 185, 215, 1, + 184, 215, 214, 1, + 244, 243, 215, 1, + 244, 215, 216, 1, + 235, 236, 206, 1, + 199, 16, 198, 1, + 213, 212, 182, 1, + 211, 212, 227, 1, + 218, 187, 188, 1, + 25, 189, 4, 1, + 27, 26, 187, 1, + 238, 265, 239, 1, + 200, 230, 201, 1, + 230, 256, 257, 1, + 199, 198, 228, 1, + 11, 204, 205, 1, + 203, 2, 12, 1, + 206, 205, 235, 1, + 197, 17, 18, 1, + 197, 211, 227, 1, + 25, 4, 41, 1, + 26, 4, 188, 1, + 182, 212, 181, 1, + 181, 212, 211, 1, + 127, 126, 147, 1, + 148, 147, 164, 1, + 125, 126, 101, 1, + 116, 115, 88, 1, + 116, 117, 138, 1, + 104, 117, 90, 1, + 61, 90, 89, 1, + 142, 158, 159, 1, + 159, 160, 143, 1, + 128, 149, 129, 1, + 149, 148, 165, 1, + 117, 129, 138, 1, + 149, 165, 156, 1, + 156, 138, 149, 1, + 170, 175, 169, 1, + 114, 113, 86, 1, + 134, 112, 113, 1, + 157, 133, 153, 1, + 107, 79, 80, 1, + 109, 132, 131, 1, + 130, 108, 131, 1, + 73, 100, 101, 1, + 125, 124, 146, 1, + 126, 125, 146, 1, + 125, 100, 124, 1, + 89, 116, 88, 1, + 117, 116, 89, 1, + 109, 108, 81, 1, + 81, 52, 53, 1, + 134, 133, 112, 1, + 45, 44, 74, 1, + 299, 298, 314, 1, + 112, 111, 84, 1, + 318, 294, 310, 1, + 295, 311, 310, 1, + 323, 331, 318, 1, + 326, 330, 327, 1, + 320, 316, 326, 1, + 318, 310, 323, 1, + 331, 330, 329, 1, + 331, 323, 330, 1, + 299, 314, 300, 1, + 3, 33, 32, 1, + 283, 284, 259, 1, + 300, 283, 282, 1, + 315, 301, 300, 1, + 300, 301, 283, 1, + 285, 284, 301, 1, + 270, 269, 243, 1, + 270, 243, 244, 1, + 248, 274, 273, 1, + 272, 246, 247, 1, + 248, 273, 247, 1, + 264, 265, 238, 1, + 262, 263, 236, 1, + 227, 267, 254, 1, + 268, 267, 241, 1, + 191, 192, 222, 1, + 222, 192, 223, 1, + 225, 194, 195, 1, + 19, 195, 20, 1, + 187, 218, 217, 1, + 56, 55, 2, 1, + 203, 202, 232, 1, + 205, 204, 234, 1, + 2, 204, 11, 1, + 5, 196, 1, 1, + 27, 39, 26, 1, + 26, 188, 187, 1, + 156, 165, 170, 1, + 154, 136, 155, 1, + 77, 62, 48, 1, + 147, 148, 127, 1, + 170, 155, 156, 1, + 170, 169, 155, 1, + 107, 105, 79, 1, + 80, 108, 107, 1, + 98, 99, 71, 1, + 305, 317, 309, 1, + 262, 286, 263, 1, + 264, 237, 263, 1, + 246, 219, 247, 1, + 246, 272, 245, 1, + 300, 282, 299, 1, + 2, 203, 204, 1, + 168, 157, 153, 1, + 153, 134, 135, 1, + 93, 119, 94, 1, + 10, 57, 11, 1, + 163, 146, 162, 1, + 163, 173, 174, 1, + 72, 42, 71, 1, + 161, 145, 144, 1, + 10, 11, 205, 1, + 85, 57, 86, 1, + 302, 286, 285, 1, + 174, 178, 175, 1, + 146, 124, 162, 1, + 100, 99, 124, 1, + 72, 71, 99, 1, + 221, 220, 190, 1, + 262, 261, 285, 1, + 234, 235, 205, 1, + 1, 19, 48, 1, + 135, 113, 114, 1, + 85, 86, 113, 1, + 169, 154, 155, 1, + 169, 175, 178, 1, + 217, 246, 245, 1, + 221, 248, 220, 1, + 294, 274, 295, 1, + 244, 271, 270, 1, + 268, 269, 291, 1, + 234, 261, 235, 1, + 291, 306, 279, 1, + 310, 322, 323, 1, + 143, 120, 142, 1, + 220, 219, 189, 1, + 299, 280, 298, 1, + 269, 270, 292, 1, + 227, 241, 267, 1, + 306, 298, 279, 1, + 15, 52, 51, 1, + 16, 15, 51, 1, + 83, 111, 110, 1, + 301, 284, 283, 1, + 268, 279, 267, 1, + 280, 254, 279, 1, + 228, 254, 255, 1, + 82, 109, 81, 1, + 227, 228, 197, 1, + 49, 50, 78, 1, + 198, 17, 197, 1, + 91, 78, 106, 1, + 92, 106, 118, 1, + 118, 119, 93, 1, + 169, 168, 154, 1, + 135, 134, 113, 1, + 159, 143, 142, 1, + 141, 158, 142, 1, +/ + +&TAIL / diff --git a/tests/cases/fds_inputs/steckler.fds b/tests/cases/fds_inputs/steckler.fds new file mode 100644 index 00000000..872caaae --- /dev/null +++ b/tests/cases/fds_inputs/steckler.fds @@ -0,0 +1,40 @@ +&HEAD CHID='out_Steckler_013', TITLE='Steckler Compartment, Test 013'/ + +## Computational domain, simulation time and ambient values +&MESH IJK=18,14,11, XB=0.00,3.60,-1.40,1.40,0.00,2.20 / +&TIME T_END=1800.0, TIME_SHRINK_FACTOR=10./ +&MISC TMPA=20.0 / + +## Fire definition +&REAC FUEL='METHANE', SOOT_YIELD=0. / +&SURF ID='BURNER', HRRPUA=1572.0, TMP_FRONT=100.0, COLOR='ORANGE' / + +## Geometry, here a wall with a hole +&OBST XB=2.80,2.90, -1.40,1.40, 0.00,2.20 / Wall with door +&HOLE XB=2.75,2.95, -0.30,0.30, 0.00,1.83 / Door opening + +## Burner surface +&VENT XB=1.30,1.50, -0.10,0.10, 0.00,0.00, SURF_ID='BURNER' / + +## Domain boundary conditions, here open, but in the compartment +&VENT MB='XMAX',SURF_ID='OPEN'/ +&VENT XB= 2.90,3.60, -1.40,-1.40, 0.00,2.20, SURF_ID='OPEN'/ +&VENT XB= 2.90,3.60, 1.40, 1.40, 0.00,2.20, SURF_ID='OPEN'/ +&VENT XB= 2.90,3.60, -1.40, 1.40, 2.20,2.20, SURF_ID='OPEN'/ + +## Boundary conditions of the walls, default for all walls +&SURF ID='FIBER BOARD', DEFAULT=.TRUE., MATL_ID='INSULATION', THICKNESS=0.013 / +&MATL ID = 'INSULATION' + DENSITY = 200. + CONDUCTIVITY = 0.1 + SPECIFIC_HEAT = 1. / + +## Output definition +&DEVC ID='TC_Room', POINTS=44, XB=2.50,2.50,1.10,1.10,0.02,2.11, QUANTITY='TEMPERATURE', Z_ID='Room_z' / +&DEVC ID='TC_Door', POINTS=38, XB=2.85,2.85,0.00,0.00,0.02,1.82, QUANTITY='TEMPERATURE', Z_ID='Door_z' / +&DEVC ID='BP_Door', POINTS=38, XB=2.85,2.85,0.00,0.00,0.02,1.82, QUANTITY='VELOCITY', VELO_INDEX=1, HIDE_COORDINATES=.TRUE. / + +&DEVC ID='TC_Door_Single', XB=2.85,2.85,0.00,0.00,1.62,1.62, QUANTITY='TEMPERATURE'/ +&DEVC ID='BP_Door_Single', XB=2.85,2.85,0.00,0.00,1.62,1.62, QUANTITY='VELOCITY'/ + +&TAIL / \ No newline at end of file diff --git a/tests/cases/fds_inputs/vort3d_80.fds b/tests/cases/fds_inputs/vort3d_80.fds new file mode 100644 index 00000000..d7d64249 --- /dev/null +++ b/tests/cases/fds_inputs/vort3d_80.fds @@ -0,0 +1,49 @@ +&HEAD CHID='vort3d_80', TITLE='2D Vortex Simulation, Mesh Size = 80' / + +&TIME T_END=50.0/ + +&MESH ID='Mesh1', IJK=24,36,12, XB=-1.0, 5.0,-1.0,8.0,0.0,3.0/ +&MESH ID='Mesh2', IJK=24,36,12, XB= 5.0,11.0,-1.0,8.0,0.0,3.0/ + + +&REAC ID='POLYURETHANE', + FYI='NFPA Babrauskas', + FUEL='REAC_FUEL', + C=6.3, + H=7.1, + O=2.1, + N=1.0, + SOOT_YIELD=0.1/ + +&SURF ID='FIRE', + COLOR='RED', + HRRPUA=50.0/ + +&OBST XB=5.0,5.5,5.5,7.5,0.0,2.5, RGB=255,255,255, TRANSPARENCY=0.407843, SURF_ID='INERT'/ AcDb3dSolid - 14AD +&OBST XB=-0.5,0.0,7.0,7.5,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 13D9 +&OBST XB=-0.5,10.5,7.5,8.0,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 13D9 +&OBST XB=10.0,10.5,7.0,7.5,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 13D9 +&OBST XB=-0.5,0.0,0.0,5.0,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 16F9 +&OBST XB=-0.5,0.0,5.5,6.0,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 16F9 +&OBST XB=-0.5,1.5,5.0,5.5,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 16F9 +&OBST XB=-0.5,10.5,-0.5,0.0,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 16F9 +&OBST XB=8.5,10.5,5.0,5.5,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 16F9 +&OBST XB=10.0,10.5,0.0,5.0,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 16F9 +&OBST XB=10.0,10.5,5.5,6.0,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 16F9 +&OBST XB=2.5,7.5,5.0,5.5,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 16FE +&OBST XB=1.5,2.5,5.0,5.5,2.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 1445 +&OBST XB=7.5,8.5,5.0,5.5,2.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 1452 +&OBST XB=-0.5,0.0,6.0,7.0,2.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 1456 +&OBST XB=10.0,10.5,6.0,7.0,2.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 1463 +&OBST XB=-0.5,10.5,-0.5,8.0,2.5,2.75, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 1469 + +&VENT SURF_ID='FIRE', XB=9.5,10.0,6.0,6.5,0.0,0.0/ Vent +&VENT SURF_ID='OPEN', XB=11.0,11.0,-1.0,8.0,0.0,3.0/ Mesh Vent: Mesh1 [XMAX] +&VENT SURF_ID='OPEN', XB=-1.0,-1.0,-1.0,8.0,0.0,3.0/ Mesh Vent: Mesh1 [XMIN] +&VENT SURF_ID='OPEN', XB=-1.0,11.0,8.0,8.0,0.0,3.0/ Mesh Vent: Mesh1 [YMAX] +&VENT SURF_ID='OPEN', XB=-1.0,11.0,-1.0,-1.0,0.0,3.0/ Mesh Vent: Mesh1 [YMIN] +&VENT SURF_ID='OPEN', XB=-1.0,11.0,-1.0,8.0,3.0,3.0 XYZ=5.0,4.5,3.0, RADIUS=0.5/ Mesh Vent: Mesh1 [ZMAX] + +&DUMP DT_PL3D=10, PLOT3D_QUANTITY(1:5)='TEMPERATURE', 'U-VELOCITY','V-VELOCITY','W-VELOCITY','HRRPUV' / + +&TAIL / \ No newline at end of file diff --git a/tests/cases/fds_inputs/wall_005.fds b/tests/cases/fds_inputs/wall_005.fds new file mode 100644 index 00000000..d60aa8f0 --- /dev/null +++ b/tests/cases/fds_inputs/wall_005.fds @@ -0,0 +1,108 @@ +&HEAD CHID='wall_test' / + +&TIME T_END=20.0, TIME_SHRINK_FACTOR=10./ + +&MULT ID='mesh', DX=1, DY=0.8, I_UPPER=3, J_UPPER=5,/ + +&MESH IJK= 20,16,72 XB=-1.5,-0.5,-2,-1.2,-0.1000,3.5, MULT_ID='mesh' / + +&DUMP RENDER_FILE='Scene.ge1' + STATUS_FILES=.TRUE. + SMOKE3D=.FALSE. / + +/&MISC RESTART=.TRUE./ + +&VENT MB='XMIN', SURF_ID='OPEN'/ +&VENT MB='XMAX', SURF_ID='OPEN'/ +&VENT MB='YMIN', SURF_ID='OPEN'/ +&VENT MB='YMAX', SURF_ID='OPEN'/ +&VENT MB='ZMIN', SURF_ID='OPEN'/ +&VENT MB='ZMAX', SURF_ID='OPEN'/ + + +&SURF ID='wall', MATL_ID='Stahlbeton',THICKNESS=0.08, BACKING="EXPOSED"/ +&MATL ID = 'Stahlbeton', CONDUCTIVITY = 2.3, SPECIFIC_HEAT = 1, DENSITY = 2300./ +&SURF ID='Boden', MATL_ID='Stahlbeton',THICKNESS=0.2, BACKING="EXPOSED"/ + +/Boden +&OBST ID='Boden' XB=-1,2,-1.2,2.4,-0.1,0, SURF_ID='Boden'/ + +/Decke +&OBST ID='Decke' XB=-1,2,-1.2,2.4,3,3.2, SURF_ID='Boden'/ + +/Wand rechts +&OBST ID='Wand_r' XB=-0.85,-0.8,-1,2.4,0,3, SURF_ID='wall'/ + +/Wand vorne +&OBST ID='Wand_v' XB=-1,2,-1.2,-1,0,3, SURF_ID='wall'/ + +&OBST ID='Golf_brandfläche' XB= -0.3,1.1,0.4,1.7,0.4,0.45 / + +/Brand +&SPEC ID='PUR', LUMPED_COMPONENT_ONLY=.FALSE., FORMULA='C4.837353H7.917138N0.427652O1.745690' / +&SPEC ID='OXYGEN', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C0.000H0.000O2.000N0.000Cl0.000' / +&SPEC ID='HCN', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C1.000H1.000O0.000N1.000Cl0.000' / +&SPEC ID='AMMONIA', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C0.000H3.000O0.000N1.000Cl0.000' / +&SPEC ID='CARBON MONOXIDE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C1.000H0.000O1.000N0.000Cl0.000' / +&SPEC ID='CARBON DIOXIDE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C1.000H0.000O2.000N0.000Cl0.000' / +&SPEC ID='WATER VAPOR', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C0.000H2.000O1.000N0.000Cl0.000' / +&SPEC ID='NITROGEN', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C0.000H0.000O0.000N2.000Cl0.000' / +&SPEC ID='SOOT', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C1.000H0.000O0.000N0.000Cl0.000' / +&SPEC ID='BENZENE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C6.000H6.000O0.000N0.000Cl0.000' / +&SPEC ID='BENZONITRILE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C7.000H5.000O0.000N1.000Cl0.000' / +&SPEC ID='BIPHENYL', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C12.000H10.000O0.000N0.000Cl0.000' / +&SPEC ID='INDENE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C9.000H8.000O0.000N0.000Cl0.000' / +&SPEC ID='ISOQUINOLINE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C9.000H7.000O0.000N1.000Cl0.000' / +&SPEC ID='NAPHTHALENE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C10.000H8.000O0.000N0.000Cl0.000' / +&SPEC ID='NAPHTHALENE-1-CARBONITRILE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C11.000H7.000O0.000N1.000Cl0.000' / +&SPEC ID='STYRENE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C8.000H8.000O0.000N0.000Cl0.000' / +&SPEC ID='TOLUENE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C7.000H8.000O0.000N0.000Cl0.000' / +&SPEC ID='XYLENE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C8.000H10.000O0.000N0.000Cl0.000' / + + +&SPEC ID = 'AIR', + SPEC_ID(1) = 'OXYGEN', VOLUME_FRACTION(1) = 1.000, + SPEC_ID(2) = 'NITROGEN', VOLUME_FRACTION(2) = 3.760, + BACKGROUND=.TRUE. / + +&SPEC ID='PRODUCTS', + SPEC_ID(1)='HCN', VOLUME_FRACTION(1)=0.038951, + SPEC_ID(2)='AMMONIA', VOLUME_FRACTION(2)=0.091211, + SPEC_ID(3)='CARBON MONOXIDE', VOLUME_FRACTION(3)=1.789193, + SPEC_ID(4)='CARBON DIOXIDE', VOLUME_FRACTION(4)=2.194841, + SPEC_ID(5)='WATER VAPOR', VOLUME_FRACTION(5)=3.653950, + SPEC_ID(6)='NITROGEN', VOLUME_FRACTION(6)=15.346936, + SPEC_ID(7)='SOOT', VOLUME_FRACTION(7)=0.483967, + SPEC_ID(8)='BENZENE', VOLUME_FRACTION(8)=0.022844, + SPEC_ID(9)='BENZONITRILE', VOLUME_FRACTION(9)=0.008317, + SPEC_ID(10)='BIPHENYL', VOLUME_FRACTION(10)=0.000334, + SPEC_ID(11)='INDENE', VOLUME_FRACTION(11)=0.001073, + SPEC_ID(12)='ISOQUINOLINE', VOLUME_FRACTION(12)=0.001932, + SPEC_ID(13)='NAPHTHALENE', VOLUME_FRACTION(13)=0.005348, + SPEC_ID(14)='NAPHTHALENE-1-CARBONITRILE', VOLUME_FRACTION(14)=0.000995, + SPEC_ID(15)='STYRENE', VOLUME_FRACTION(15)=0.002278, + SPEC_ID(16)='TOLUENE', VOLUME_FRACTION(16)=0.002920, + SPEC_ID(17)='XYLENE', VOLUME_FRACTION(17)=0.000123/ + + +&VENT XB=-0.3,1.1,0.4,1.7,0.45,0.45 SURF_ID='Brandfläche'/ +&REAC ID='Brandfläche', FUEL='PUR', SPEC_ID_NU = 'PUR','AIR','PRODUCTS', NU= -1,-4.043567,1., HEAT_OF_COMBUSTION=40000 / +&SURF ID='Brandfläche',SPEC_ID='PUR', HRRPUA=2823.129, COLOR='RED', RAMP_Q='fire_ramp'/ + +/Vorgabe der Brandkurve + +&RAMP ID='fire_ramp', T=0, F=0.0/ +&RAMP ID='fire_ramp', T=240, F=0.1687/ +&RAMP ID='fire_ramp', T=960, F=0.1687/ +&RAMP ID='fire_ramp', T=1440, F=0.6627/ +&RAMP ID='fire_ramp', T=1500, F=1.0/ +&RAMP ID='fire_ramp', T=1620, F=0.5422/ +&RAMP ID='fire_ramp', T=2280, F=0.1205/ +&RAMP ID='fire_ramp', T=4200, F=0/ + +/Wall Temperature + +&BNDF QUANTITY='WALL TEMPERATURE', CELL_CENTERED=.TRUE./ +&BNDF QUANTITY='BACK WALL TEMPERATURE', CELL_CENTERED=.TRUE./ + +&TAIL / diff --git a/tests/cases/fds_inputs/water_droplets.fds b/tests/cases/fds_inputs/water_droplets.fds new file mode 100644 index 00000000..a67d35ba --- /dev/null +++ b/tests/cases/fds_inputs/water_droplets.fds @@ -0,0 +1,44 @@ +1 L water droplets cascading down over a stack of boxes. +The purpose of this case is to test functionality of the droplet routine. + +&HEAD CHID='cascade', TITLE='Water droplets cascading over a stack of boxes' / + +&MESH IJK=16,16,56, XB=-2.0,2.0,-2.0,2.0,0.0,14.0 / + +&TIME T_END=1.0 / + +&MISC HUMIDITY=0. / + +! ALLOW_UNDERSIDE_PARTICLES moved from MISC to SURF in FDS 6.9+ +&SURF ID='BOX', COLOR='TAN', ALLOW_UNDERSIDE_PARTICLES=.TRUE. / + +&OBST XB=-1.5, 1.5,-1.5, 1.5, 1.0, 3.0, SURF_ID='BOX' / +&OBST XB=-1.5, 1.5,-1.5, 1.5, 4.0, 6.0, SURF_ID='BOX' / +&OBST XB=-1.5, 1.5,-1.5, 1.5, 7.0, 9.0, SURF_ID='BOX' / + + VENT MB='XMIN', SURF_ID='OPEN' / + VENT MB='XMAX', SURF_ID='OPEN' / + VENT MB='YMIN', SURF_ID='OPEN' / + VENT MB='YMAX', SURF_ID='OPEN' / + +&SPEC ID='WATER VAPOR'/ +&PART ID='WATER PARTICLES', SPEC_ID='WATER VAPOR', DIAMETER=1000., SAMPLING_FACTOR=1, QUANTITIES='PARTICLE DIAMETER','PARTICLE VELOCITY' / + +&PROP ID='nozzle', PART_ID='WATER PARTICLES', PARTICLE_VELOCITY=2.0, FLOW_RATE=6., FLOW_RAMP='flow', SPRAY_ANGLE=30.,40. / +&RAMP ID='flow', T= 0., F=0. / +&RAMP ID='flow', T= 2., F=1. / +&RAMP ID='flow', T=10., F=1. / +&RAMP ID='flow', T=12., F=0. / + +&DEVC ID='N-1', XYZ=0.0,0.0,13.7, PROP_ID='nozzle', QUANTITY='TIME', SETPOINT=0.0 / + +&DUMP DT_DEVC=1. / + +&DEVC ID='water liquid', QUANTITY='AMPUA', PART_ID='WATER PARTICLES', SPATIAL_STATISTIC='SURFACE INTEGRAL', XB=-2,2,-2,2,0,1 / +&DEVC ID='water vapor', QUANTITY='DENSITY', SPEC_ID='WATER VAPOR', SPATIAL_STATISTIC='VOLUME INTEGRAL', XB=-2,2,-2,2,0,14 / +&DEVC ID='water mass', QUANTITY='CONTROL VALUE', CTRL_ID='Add', XYZ=1,1,1, UNITS='kg' / +&CTRL ID='Add', FUNCTION_TYPE='SUM', INPUT_ID='water liquid','water vapor' / + +&RADI RADIATION=.FALSE./ + +&TAIL / \ No newline at end of file diff --git a/tests/cases/part_data_fds6100.tgz b/tests/cases/part_data_fds6100.tgz new file mode 100644 index 00000000..3ff38aee --- /dev/null +++ b/tests/cases/part_data_fds6100.tgz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:41fa495353518854808b29f3baacd5f27f0424f9611c42feccbe16f207f79cc9 +size 390183 diff --git a/tests/cases/pl3d_data_fds6100.tgz b/tests/cases/pl3d_data_fds6100.tgz new file mode 100644 index 00000000..0239ffef --- /dev/null +++ b/tests/cases/pl3d_data_fds6100.tgz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92d5dd73c2e417956cd38267c4f604b71a93a55e2d7f34334b6968f59b1d77f9 +size 2693493 diff --git a/tests/cases/steckler_data_fds6100.tgz b/tests/cases/steckler_data_fds6100.tgz new file mode 100644 index 00000000..a7413d1e --- /dev/null +++ b/tests/cases/steckler_data_fds6100.tgz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cef0948df6b7b702f53f3ecf66cef72f4ecac9970bfabb3209ac4727a17c5ee +size 12443429 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..57c0a6f1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +import os +from pathlib import Path + + +def pytest_configure(config): + """Change CWD to tests/cases/ so acceptance tests can use relative paths like './steckler_data'.""" + cases_dir = Path(__file__).parent / "cases" + if cases_dir.exists(): + os.chdir(cases_dir) diff --git a/tests/test_basic_read.py b/tests/test_basic_read.py new file mode 100644 index 00000000..01d10eb5 --- /dev/null +++ b/tests/test_basic_read.py @@ -0,0 +1,45 @@ +"""Basic integration tests: load a simulation and verify core properties are accessible.""" + +from pathlib import Path + +import pytest + +from fdsreader import Simulation + +CASES_DIR = Path(__file__).parent / "cases" +TESTS_DIR = Path(__file__).parent + + +@pytest.fixture(scope="module") +def basic_sim(): + return Simulation(str(TESTS_DIR / "test.smv")) + + +@pytest.fixture(scope="module") +def steckler_sim(): + return Simulation(str(CASES_DIR / "steckler_data")) + + +def test_basic_chid(basic_sim): + assert basic_sim.chid == "test" + + +def test_basic_mesh_count(basic_sim): + assert len(basic_sim.meshes) >= 1 + + +def test_steckler_loads(steckler_sim): + assert steckler_sim.chid is not None + assert len(steckler_sim.meshes) > 0 + + +def test_steckler_slices_accessible(steckler_sim): + assert len(steckler_sim.slices) > 0 + + +def test_steckler_devices_accessible(steckler_sim): + assert len(steckler_sim.devices) > 0 + + +def test_steckler_obstructions_accessible(steckler_sim): + assert len(steckler_sim.obstructions) >= 0 diff --git a/tests/test_version_compatibility.py b/tests/test_version_compatibility.py deleted file mode 100644 index 8ca7595a..00000000 --- a/tests/test_version_compatibility.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Basic tests to ensure version compatibility. -""" - -import unittest - -from fdsreader import Simulation - - -class SimTest(unittest.TestCase): - def test_sim(self): - sim = Simulation(".") - self.assertEqual(sim.chid, "test") - - -if __name__ == '__main__': - unittest.main()