├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── config.yml └── workflows │ ├── joss.yml │ ├── publish_n_pack.yml │ └── testsuite.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── RELEASE.md ├── docs ├── bndf.rst ├── conf.py ├── devc.rst ├── evac.rst ├── export.rst ├── fds_classes.rst ├── geom.rst ├── img │ ├── data-structure.drawio │ └── data-structure.svg ├── index.rst ├── isof.rst ├── part.rst ├── pl3d.rst ├── quantity.rst ├── simulation.rst ├── slcf.rst ├── smoke3d.rst └── utils.rst ├── examples ├── bndf │ ├── bndf_example_anim.py │ └── fds_data │ │ └── wall_005.fds ├── devc │ ├── devc_example.py │ └── fds_steckler │ │ └── steckler.fds ├── evac │ ├── evac_example.py │ ├── fds_fed │ │ └── fed_verification_multi_mesh.fds │ └── fds_no_fed │ │ └── stairs_evss_meshes.fds ├── export │ ├── export_all.py │ ├── obst_export.py │ ├── slcf_export.py │ └── smoke3d_export.py ├── geom │ ├── fds_data │ │ └── sphere_radiate.fds │ ├── fds_geomslices │ │ └── PWB01_GEOM.fds │ ├── geom_example.py │ └── geom_slices.py ├── isof │ ├── fds_steckler │ │ └── input_steckler.fds │ └── isof_example.py ├── part │ └── part_example.py ├── pl3d │ ├── fds_data │ │ └── vort3d_80.fds │ └── pl3d_example.py ├── slcf │ ├── fds_data │ │ └── vort2d_80.fds │ ├── fds_multimesh │ │ └── multimesh.fds │ ├── fds_steckler │ │ └── input_steckler.fds │ ├── slcf_example_global.py │ └── slcf_example_mask.py └── smoke3d │ ├── fds_steckler │ └── input_steckler.fds │ └── smoke3d_example.py ├── fdsreader ├── __init__.py ├── _version.py ├── bndf │ ├── __init__.py │ ├── obstruction.py │ ├── obstruction_collection.py │ └── utils.py ├── devc │ ├── __init__.py │ ├── device.py │ └── device_collection.py ├── evac │ ├── __init__.py │ ├── evac_collection.py │ └── evacuation.py ├── export │ ├── __init__.py │ ├── obst_exporter.py │ ├── sim_exporter.py │ ├── slcf_exporter.py │ └── smoke3d_exporter.py ├── fds_classes │ ├── __init__.py │ ├── mesh.py │ ├── mesh_collection.py │ ├── surface.py │ └── ventilation.py ├── geom │ ├── __init__.py │ ├── geometry.py │ └── geometry_collection.py ├── isof │ ├── __init__.py │ ├── isosurface.py │ └── isosurface_collection.py ├── part │ ├── __init__.py │ ├── particle.py │ └── particle_collection.py ├── pl3d │ ├── __init__.py │ ├── pl3d.py │ └── plot3D_collection.py ├── settings.py ├── simulation.py ├── slcf │ ├── __init__.py │ ├── geomslice.py │ ├── geomslice_collection.py │ ├── slice.py │ └── slice_collection.py ├── smoke3d │ ├── __init__.py │ ├── smoke3D_collection.py │ └── smoke3d.py └── utils │ ├── __init__.py │ ├── data.py │ ├── dimension.py │ ├── extent.py │ ├── fortran_data.py │ └── misc.py ├── paper.bib ├── paper.md ├── pyproject.toml ├── requirements.txt └── tests ├── acceptance_tests ├── test_bndf.py ├── test_devc.py ├── test_geom.py ├── test_isof.py ├── test_part.py ├── test_pl3d.py ├── test_slcf.py └── test_smoke3d.py ├── cases ├── bndf_data.tgz ├── devc_data.tgz ├── geom_data.tgz ├── part_data.tgz ├── pl3d_data.tgz └── steckler_data.tgz ├── test.smv └── test_version_compatibility.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.tgz filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report an error or unexpected behavior 3 | title: "[Bug]: " 4 | labels: [bug] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Welcome to the fdsreader GitHub repository! 11 | We always appreciate feedback from our users. 12 | Please fill out as many fields as possible to help us reproduce the bug 13 | more easily. 14 | 15 | Thank you for using fdsreader! 16 | 17 | - type: textarea 18 | id: what-happened 19 | attributes: 20 | label: What happened? 21 | description: Also tell us, what did you expect to happen? 22 | placeholder: Tell us what you see! 23 | value: "A bug happened!" 24 | validations: 25 | required: true 26 | 27 | - type: textarea 28 | attributes: 29 | label: Steps to reproduce 30 | description: | 31 | Tell us how to reproduce this bug. 32 | If possible, please include a minimal, fully reproducible example or a link to a Git repository. 33 | placeholder: Put your code or a link here 34 | 35 | - type: textarea 36 | id: fds-version 37 | attributes: 38 | label: FDS version used 39 | description: Please tell us what FDS version was used. 40 | placeholder: Version 41 | validations: 42 | required: true 43 | 44 | - type: textarea 45 | id: fdsreader-version 46 | attributes: 47 | label: fdsreader version used 48 | description: Please tell us what fdsreader version was used. 49 | placeholder: Version 50 | validations: 51 | required: true 52 | 53 | - type: markdown 54 | attributes: 55 | value: "_Thanks for submitting this bug report!_" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true -------------------------------------------------------------------------------- /.github/workflows/joss.yml: -------------------------------------------------------------------------------- 1 | name: FDSreader JOSS submission paper 2 | 3 | on: 4 | workflow_dispatch: {} 5 | push: 6 | branches: 7 | - master 8 | paths: 9 | - 'paper.md' 10 | - 'paper.bib' 11 | pull_request: 12 | paths: 13 | - 'paper.md' 14 | - 'paper.bib' 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | paper: 21 | runs-on: ubuntu-latest 22 | name: Paper Draft 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | - name: Build draft PDF 27 | uses: openjournals/openjournals-draft-action@master 28 | with: 29 | journal: joss 30 | # This should be the path to the paper within your repo. 31 | paper-path: paper.md 32 | - name: Upload 33 | uses: actions/upload-artifact@v1 34 | with: 35 | name: paper 36 | path: paper.pdf 37 | -------------------------------------------------------------------------------- /.github/workflows/publish_n_pack.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | name: Build distribution 📦 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Set up Python 13 | uses: actions/setup-python@v5 14 | with: 15 | python-version: "3.x" 16 | - name: Install pypa/build 17 | run: >- 18 | python3 -m 19 | pip install 20 | build 21 | --user 22 | - name: Build a binary wheel and a source tarball 23 | run: python3 -m build 24 | - name: Store the distribution packages 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: python-package-distributions 28 | path: dist/ 29 | 30 | publish-to-pypi: 31 | name: >- 32 | Publish Python 🐍 distribution 📦 to PyPI 33 | if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes 34 | needs: 35 | - build 36 | runs-on: ubuntu-latest 37 | environment: 38 | name: pypi 39 | url: https://pypi.org/p/fdsreader 40 | permissions: 41 | id-token: write # IMPORTANT: mandatory for trusted publishing 42 | 43 | steps: 44 | - name: Download all the dists 45 | uses: actions/download-artifact@v4 46 | with: 47 | name: python-package-distributions 48 | path: dist/ 49 | - name: Publish distribution 📦 to PyPI 50 | uses: pypa/gh-action-pypi-publish@release/v1 51 | 52 | github-release: 53 | name: >- 54 | Sign the Python 🐍 distribution 📦 with Sigstore 55 | and upload them to GitHub Release 56 | needs: 57 | - publish-to-pypi 58 | runs-on: ubuntu-latest 59 | 60 | permissions: 61 | contents: write # IMPORTANT: mandatory for making GitHub Releases 62 | id-token: write # IMPORTANT: mandatory for sigstore 63 | 64 | steps: 65 | - name: Download all the dists 66 | uses: actions/download-artifact@v4 67 | with: 68 | name: python-package-distributions 69 | path: dist/ 70 | - name: Sign the dists with Sigstore 71 | uses: sigstore/gh-action-sigstore-python@v3.0.0 72 | with: 73 | inputs: >- 74 | ./dist/*.tar.gz 75 | ./dist/*.whl 76 | - name: Create GitHub Release 77 | env: 78 | GITHUB_TOKEN: ${{ github.token }} 79 | run: >- 80 | gh release create 81 | '${{ github.ref_name }}' 82 | --repo '${{ github.repository }}' 83 | --notes "" 84 | - name: Upload artifact signatures to GitHub Release 85 | env: 86 | GITHUB_TOKEN: ${{ github.token }} 87 | # Upload to GitHub Release using the `gh` CLI. 88 | # `dist/` contains the built packages, and the 89 | # sigstore-produced signatures and certificates. 90 | run: >- 91 | gh release upload 92 | '${{ github.ref_name }}' dist/** 93 | --repo '${{ github.repository }}' 94 | 95 | publish-to-testpypi: 96 | name: Publish Python 🐍 distribution 📦 to TestPyPI 97 | if: startsWith(github.ref, 'refs/tags/') != true 98 | needs: 99 | - build 100 | runs-on: ubuntu-latest 101 | 102 | environment: 103 | name: testpypi 104 | url: https://test.pypi.org/p/fdsreader 105 | 106 | permissions: 107 | id-token: write # IMPORTANT: mandatory for trusted publishing 108 | 109 | steps: 110 | - name: Download all the dists 111 | uses: actions/download-artifact@v4 112 | with: 113 | name: python-package-distributions 114 | path: dist/ 115 | - name: Publish distribution 📦 to TestPyPI 116 | uses: pypa/gh-action-pypi-publish@release/v1 117 | with: 118 | repository-url: https://test.pypi.org/legacy/ 119 | -------------------------------------------------------------------------------- /.github/workflows/testsuite.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: FDSreader testsuite 5 | 6 | on: 7 | workflow_dispatch: {} 8 | push: 9 | branches: 10 | - master 11 | paths: 12 | - 'fdsreader/**' 13 | pull_request: 14 | paths: 15 | - 'fdsreader/**' 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | run-tests: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | lfs: true 27 | - uses: actions/setup-python@v5 28 | with: 29 | python-version: '3.10' 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | pip install pytest numpy 34 | pip install ${{ github.workspace }} 35 | - name: Prepare test cases 36 | run: | 37 | cd ${{ github.workspace }}/tests/cases/ 38 | for f in *.tgz; do tar -xzvf "$f"; done 39 | - name: Test with pytest 40 | run: | 41 | cd ${{ github.workspace }}/tests/cases 42 | pytest ../acceptance_tests/ 43 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | include README.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FDSReader 2 | > Fast and easy-to-use Python reader for FDS data 3 | 4 | [![PyPI version](https://badge.fury.io/py/fdsreader.png)](https://badge.fury.io/py/fdsreader) 5 | 6 | 7 | ## Installation 8 | 9 | The package is available on PyPI and can be installed using pip: 10 | ```sh 11 | pip install fdsreader 12 | ``` 13 | _FDS Version 6.7.5 and above are fully supported. Versions below 6.7.5 might work, but are not guaranteed to work._ 14 | 15 | ## Usage example 16 | 17 | ```python 18 | import fdsreader as fds 19 | 20 | # Creates an instance of a simulation master-class which manages all data for a given simulation 21 | sim = fds.Simulation("./sample_data") 22 | 23 | # Examples of data that can be easily accessed 24 | print(sim.meshes, sim.surfaces, sim.slices, sim.data_3d, sim.smoke_3d, sim.isosurfaces, sim.particles, sim.obstructions) 25 | ``` 26 | 27 | More advanced examples can be found in the respective data type directories inside of the examples directory. 28 | 29 | ### Configuration 30 | The package provides a few configuration options that can be set using the `settings` module. 31 | ```python 32 | fds.settings.KEY = VALUE 33 | 34 | # Example 35 | fds.settings.DEBUG = True 36 | ``` 37 | 38 | | KEY | VALUE | Default | Description | 39 | |----------------|---------|---------|-------------| 40 | | LAZY_LOAD | boolean | True | Load all data when initially loading the simulation (False) or only when specific data is needed (True). | 41 | | ENABLE_CACHING | boolean | True | Cache the loaded simulation to reduce startup times when loading the same simulation again. | 42 | | DEBUG | boolean | False | Crash on non-critical errors with an exception (True) or output non-critical errors as warnings (False). | 43 | | IGNORE_ERRORS | boolean | False | Ignore any non-critical errors completely. | 44 | 45 | 46 | ### Data structure 47 | ![Data structure](https://raw.githubusercontent.com/FireDynamics/fdsreader/master/docs/img/data-structure.svg) 48 | 49 | Beware that not all attributes and methods are covered in this diagram. For a complete 50 | documentation of all classes check the API Documentation below. 51 | 52 | ## API Documentation 53 | [https://firedynamics.github.io/fdsreader/](https://firedynamics.github.io/fdsreader/) 54 | 55 | 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/). 56 | 57 | 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. 58 | 59 | ### Deploying an Untested/Unstable Version: 60 | 1. Execute: `python3 -m incremental.update fdsreader --dev` 61 | 2. Push changes to GitHub. 62 | 63 | If you are sure your changes are stable push a GitHub Tag to perform deployment 64 | to PyPI and to pack a GitHub Release 65 | Deploying a tested/stable version: 66 | 1. set the new version with `python3 -m incremental.update fdsreader --newversion=` 67 | 2. Push changes to GitHub 68 | 3. Create tag `git tag -a v -m "Version "` 69 | 4. Push Tag to GitHub with `git push origin tag ` 70 | 71 | 72 | ### Manual deployment 73 | It is also possible to deploy to PyPI and Github pages manually using the following steps: 74 | 1. python setup.py sdist bdist_wheel 75 | 2. twine upload dist/* 76 | 3. sphinx-build -b html docs docs/build 77 | 4. cd .. && mkdir gh-pages && cd gh-pages 78 | 5. git init && git remote add origin git@github.com:FireDynamics/fdsreader.git 79 | 6. git fetch origin gh-pages:gh-pages 80 | 7. git checkout gh-pages 81 | 8. cp -r ../fdsreader/docs/build/* . 82 | 9. git add . && git commit -m "..." && git push origin HEAD:gh-pages 83 | 84 | ## Meta 85 | 86 | * Jan Vogelsang – j.vogelsang@fz-juelich.de 87 | * Prof. Dr. Lukas Arnold - l.arnold@fz-juelich.de 88 | 89 | Distributed under the LGPLv3 (GNU Lesser General Public License v3) license. See ``LICENSE`` for more information. 90 | 91 | [https://github.com/FireDynamics/fdsreader](https://github.com/FireDynamics/fdsreader) 92 | 93 | ## Contributing 94 | 95 | 1. Fork it () 96 | 2. Create your feature branch (`git checkout -b feature/fooBar`) 97 | 3. Commit your changes (`git commit -am 'Add some fooBar'`) 98 | 4. Push to the branch (`git push origin feature/fooBar`) 99 | 5. Create a new Pull Request 100 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | ## Release History 2 | * 1.10.3 3 | * Fixed particle documentation 4 | * 1.10.2 5 | * Fixed to_global function for Plot3D data with obstruction masks 6 | * Fixed to_global function for Smoke3D data with obstruction masks 7 | * 1.10.1 8 | * Fixed to_global function for Plot3D data 9 | * Fixed to_global function for Smoke3D data 10 | * 1.10.0 11 | * Restructured Plot3D data to now be represented by a single Plot3D instance for all timesteps instead of one individual object per timestep 12 | * Added a to_global function for Plot3D data 13 | * Added a to_global function for Smoke3D data 14 | * Fixed bugs and typos in documentation 15 | * 1.9.13 16 | * Fixed slice.to_global for 3D slices 17 | * 1.9.12 18 | * Fixed bug in slice.to_global caused by improper floating point comparison 19 | * 1.9.11 20 | * Fixed issues with boundary data 21 | * 1.9.10 22 | * Reading out TITLE and TIMES from smv-file 23 | * Fixed obstruction mask for slices with multiple mesh resolutions 24 | * 1.9.9 25 | * Fixed obst side_surfaces 26 | * 1.9.8 27 | * Fixed to_global for 3D slices 28 | * 1.9.7 29 | * Fixed obstructions for simulations with restart in between 30 | * 1.9.6 31 | * Supporting obstructions with holes 32 | * 1.9.5 33 | * Added option to read the actual obstruction id set in the fds-file for FDS 6.7.10+ 34 | * 1.9.4 35 | * Fixed bug when reading in SHOW/HIDE OBST lines 36 | * 1.9.3 37 | * Reading FDS version from .smv file is now backwards compatible 38 | * 1.9.2 39 | * Removed prints 40 | * 1.9.1 41 | * Improved slcf get_coordinates and added option to return coordinates for to_global method 42 | * 1.9.0 43 | * Several bugfixes 44 | * 1.8.7 45 | * Fixed devices and improved global slcf example 46 | * 1.8.6 47 | * Fixed __repr__ of Simulation 48 | * 1.8.5 49 | * Fixed particle reader 50 | * 1.8.4 51 | * Obst vmax and vmin now correctly return floats instead of numpy scalars 52 | * 1.8.3 53 | * Fixed obst exporter and added safe guards to obstructions for when there is no boundary data 54 | * 1.8.2 55 | * Added option to mask global slice and fixed csv and slice mask bugs 56 | * 1.8.1 57 | * Lots of improvements and convenience methods for obstructions 58 | * 1.8.0 59 | * Reworked Devices: Now using a DeviceCollection with pandas support, devices do now lazyload as well 60 | * 1.7.5 61 | * Fixed bug in get_subslice method 62 | * 1.7.4 63 | * Several bugfixes 64 | * 1.7.3 65 | * Fixed bug in sublices get_coordinates method 66 | * 1.7.2 67 | * Fixed naming in \_\_repr\_\_ string 68 | * 1.7.1 69 | * Fixed cpu, hrr and steps reader 70 | * 1.7.0 71 | * Changed the behavior of the to_global slcf method to return two slices for cases where slices cut right through mesh boundaries 72 | * 1.6.6 73 | * Fixed exception when devices with duplicated IDs are given 74 | * 1.6.5 75 | * Fixed slcf to_global methods for specific cell-centered slice scenarios 76 | * 1.6.4 77 | * Adding hash to sim exporter 78 | * 1.6.3 79 | * Fixed slcf reader for irregular meshes 80 | * 1.6.2 81 | * Added cell-centered information to slcf exporter 82 | * 1.6.1 83 | * Fixed slcf exporter 84 | * 1.6.0 85 | * Fixed evac reader, now works with latest evac changes 86 | * 1.5.1 87 | * Fixed sim exporter 88 | * 1.5.0 89 | * Added python 3.10+ support 90 | * 1.4.4 91 | * Added sim exporter 92 | * 1.4.3 93 | * Fixed some slcf bugs 94 | * 1.4.2 95 | * Added cli-friendliness to exporters 96 | * 1.4.1 97 | * Fixed slcf reader slice_id bug and improved export example 98 | * 1.4.0 99 | * Added option to ignore all non-critical errors and warnings to provide optional cli-friendliness 100 | * 1.3.12 101 | * Fixed a bug that did not allow spaces in Mesh IDs 102 | * 1.3.11 103 | * Fixed obst exporter 104 | * 1.3.10 105 | * Fixed some dependency conflicts 106 | * 1.3.9 107 | * Fixed rounding issues with slices 108 | * 1.3.8 109 | * Made exporter imports optional 110 | * 1.3.7 111 | * Fixed bug for 3d to 2d slcf function 112 | * 1.3.6 113 | * Fixed a typo in code 114 | * 1.3.5 115 | * Added obst/bndf exporter 116 | * Fixed some slcf bugs 117 | * 1.3.4 118 | * Fixing an issue with pip when the working directory contains chinese characters 119 | * 1.3.3 120 | * Fixed the reading of the column names of HRR/CPU data 121 | * 1.3.2 122 | * Fixed a bndf bug when no data existed for one or more Subobstructions 123 | * 1.3.1 124 | * Fixed imports 125 | * 1.3.0 126 | * Fixed documentation 127 | * 1.2.12 128 | * Added a convenience function for quantity filtering to pl3d 129 | * 1.2.11 130 | * Finished the slcf exporter 131 | * 1.2.10 132 | * Fixed some edge cases for evac when no data existed 133 | * 1.2.9 134 | * Bugfixes for smoke3d exporter 135 | * 1.2.8 136 | * Modified smoke3d exporter to export yaml instead of mhd 137 | * 1.2.7 138 | * Fixed issues with imports 139 | * 1.2.6 140 | * Modified smoke3d exporter to export 4d data instead of 3d 141 | * 1.2.5 142 | * Made usage of quantities easier 143 | * 1.2.4 144 | * Added to_global convenience function for non-uniform meshes 145 | * 1.2.3 146 | * Support for profile data (prof) 147 | * 1.2.2 148 | * Support for mesh boundary data 149 | * 1.2.1 150 | * Added gslcf support (geomslices) 151 | * 1.2.0 152 | * Added evac support 153 | * Optimized manual cache clearing functionality 154 | * 1.1.1 155 | * Slcf hotfixes 156 | * 1.1.0 157 | * Added smoke3d support 158 | * Bugfixes for devices 159 | * Devices now also contain units of output quantities 160 | * 1.0.11 161 | * Bugfixes for cell-centered slices 162 | * Added get_by_id filter for obstructions and meshes 163 | * 1.0.10 164 | * Improvements for bndf 165 | * 1.0.9 166 | * Improvements for complex geometry 167 | * 1.0.8 168 | * Bugfixes for obst slice masks 169 | * 1.0.7 170 | * Caching bugfixes 171 | * 1.0.6 172 | * Caching bugfixes 173 | * 1.0.5 174 | * Bugfixes for obst slice masks 175 | * Added tag convenience function for particles 176 | * 1.0.4 177 | * Bugfixes for obst masks 178 | * 1.0.3 179 | * Bugfixes for part 180 | * 1.0.2 181 | * Bugfixes for Python 3.6/3.7 182 | * 1.0.1 183 | * Vmin/Vmax and numpy min/max support for slcf 184 | * 1.0.0 185 | * Reworked bndf data structure 186 | * Fixed multiple bugs 187 | * Added quantities property to all collections 188 | 189 | ### Beta 190 | * 0.9.22 191 | * Bugfixes for bndf 192 | * 0.9.21 193 | * Slice and pl3d now support numpy.mean 194 | * 0.9.20 195 | * Slice coordinates now correctly represent cell-centered slices 196 | * 0.9.19 197 | * Added more utility functions for slices 198 | * 0.9.18 199 | * Improved slice filtering 200 | * 0.9.17 201 | * Made collection output easier to read 202 | * 0.9.16 203 | * Added verbosity to Simulation class 204 | * 0.9.15 205 | * Improved collection filtering 206 | * 0.9.14 207 | * Resolved possible requirement version conflicts 208 | * 0.9.13 209 | * Refactored requirements 210 | * 0.9.12 211 | * Bugfixes for slcf 212 | * 0.9.11 213 | * Added option to retrieve coordinates from slices 214 | * 0.9.10 215 | * Improved slice filtering functionality 216 | * 0.9.9 217 | * Added bndf support for older versions of fds 218 | * 0.9.8 219 | * Fixed an issue that caused numpy to crash on Unix-based machines 220 | * 0.9.7 221 | * Added slcf support for older versions of fds 222 | * 0.9.6 223 | * Bugfixes for complex geometry 224 | * 0.9.5 225 | * Fixed issue with invalid pickle files (cache) 226 | * 0.9.4 227 | * Bugfixes for complex geometry 228 | * 0.9.3 229 | * Added lazy loading for complex geometry data 230 | * 0.9.2 231 | * Improved documentation for complex geometry 232 | * 0.9.1 233 | * Added documentation for complex geometry 234 | * 0.9.0 235 | * Added rudimentary support for complex geometry 236 | 237 | ### Alpha 238 | * 0.8.3 239 | * Added collections to documentation 240 | * 0.8.2 241 | * Fixed slcf examples 242 | * 0.8.1 243 | * Bugfixes for vents 244 | * 0.8.0 245 | * Major bugfixes 246 | 247 | ### Pre-Alpha 248 | * 0.7.4 249 | * Added obst hole support (bndf) 250 | * 0.7.3 251 | * Added exception handling for optional modules 252 | * 0.7.2 253 | * Improved slice combination functionality 254 | * 0.7.1 255 | * Made PyVista (Vtk) requirement optional 256 | * 0.7.0 257 | * Added automatic version compatibility testing 258 | * Usability improvements for slcf 259 | * 0.6.5 260 | * Bugfixes 261 | * 0.6.4 262 | * Added more convenience functions (mainly filters) 263 | * Added color-data isof support 264 | * 0.6.3 265 | * Added some convenience functions (filters, obstruction masks, ...) 266 | * 0.6.2 267 | * Improved documentation 268 | * 0.6.1 269 | * Added multimesh part support 270 | * 0.6.0 271 | * Added part example 272 | * Added pl3d example 273 | * Added slcf example 274 | * Added two bndf examples 275 | * Added isof example 276 | * 0.5.3 277 | * Usability improvements for bndf 278 | * 0.5.2 279 | * Bugfixes for bndf 280 | * 0.5.1 281 | * Several bugfixes and improvements 282 | * 0.5.0 283 | * Preparing for alpha release 284 | * Usability improvements for simulation 285 | * Usability improvements for part 286 | * Added devc support (devices) 287 | * Added part support (particles) 288 | * Bugfixes for bndf 289 | * 0.4.10 290 | * Bugfixes for slcf 291 | * 0.4.9 292 | * Bugfixes for bndf 293 | * Improved 2D-Slice functionality 294 | * 0.4.8 295 | * Complete rework of internal reading process (higher performance) 296 | * Complete rework of bndf 297 | * Bugfixes (obstructions, extents, simulation) 298 | * 0.4.7 299 | * Added cache clearing functionality 300 | * Bugfixes 301 | * 0.4.6 302 | * Added automatic caching for simulations (significant loading time reduction) 303 | * Reworked internal slcf data structure 304 | * Fixed isof reader (now correctly reads in data for all time steps) 305 | * Connected bndf data to obstructions 306 | * Simplified instantiation of Simulation objects 307 | * 0.4.5 308 | * Added multimesh isof support 309 | * Improved slcf stability 310 | * 0.4.4 311 | * Bugfixes (bndf and pl3d) 312 | * 0.4.3 313 | * Bugfixes (slcf and isof) 314 | * 0.4.2 315 | * Completed API documentation 316 | * 0.4.1 317 | * Bugfixes (python import issues) 318 | * 0.4.0 319 | * Added bndf support (boundaries) 320 | * 0.3.1 321 | * Added multimesh pl3d support 322 | * 0.3.0 323 | * Added basic pl3d support 324 | * 0.2.0 325 | * Added isof support (isosurfaces) 326 | * 0.1.2 327 | * Added numpy support for slices 328 | * 0.1.1 329 | * Added multimesh slcf support 330 | * Added API documentation 331 | * Package available on PyPI 332 | * 0.1.0 333 | * Added basic slcf support (2D + 3D slices) -------------------------------------------------------------------------------- /docs/bndf.rst: -------------------------------------------------------------------------------- 1 | Boundary files 2 | ______________ 3 | 4 | .. automodule:: fdsreader.bndf.ObstructionCollection 5 | :autosummary: 6 | :members: 7 | :undoc-members: 8 | :noindex: 9 | 10 | .. automodule:: fdsreader.bndf.obstruction 11 | :autosummary: 12 | :members: 13 | :undoc-members: 14 | :noindex: 15 | 16 | .. automodule:: fdsreader.bndf.utils 17 | :autosummary: 18 | :members: 19 | :undoc-members: 20 | :noindex: 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath('..')) 17 | from fdsreader._version import __version__ 18 | # import sphinx_rtd_theme 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'fdsreader' 23 | copyright = '2020, FZJ IAS-7 (Prof. Dr. Lukas Arnold, Jan Vogelsang)' 24 | author = 'FZJ IAS-7 (Prof. Dr. Lukas Arnold, Jan Vogelsang)' 25 | 26 | # The full version, including alpha/beta/rc tags 27 | release = str(__version__) 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.viewcode', 37 | 'sphinx_rtd_theme', 38 | 'autodocsumm', 39 | 'nbsphinx' 40 | ] 41 | 42 | autoclass_content = 'both' 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | # templates_path = ['_templates'] 46 | 47 | # List of patterns, relative to source directory, that match files and 48 | # directories to ignore when looking for source files. 49 | # This pattern also affects html_static_path and html_extra_path. 50 | exclude_patterns = ['Thumbs.db', '.DS_Store'] 51 | 52 | # -- Options for HTML output ------------------------------------------------- 53 | 54 | # The theme to use for HTML and HTML Help pages. See the documentation for 55 | # a list of builtin themes. 56 | # 57 | html_theme = "sphinx_rtd_theme" 58 | html_show_sourcelink = False 59 | 60 | html_sidebars = { 61 | '**': [ 62 | 'about.html', 63 | 'navigation.html', 64 | 'relations.html', 65 | 'searchbox.html', 66 | 'donate.html', 67 | ] 68 | } 69 | 70 | # Add any paths that contain custom static files (such as style sheets) here, 71 | # relative to this directory. They are copied after the builtin static files, 72 | # so a file named "default.css" will overwrite the builtin "default.css". 73 | # html_static_path = ['_static'] 74 | -------------------------------------------------------------------------------- /docs/devc.rst: -------------------------------------------------------------------------------- 1 | Devices 2 | _______ 3 | 4 | .. autoclass:: fdsreader.devc.Device 5 | :members: 6 | :undoc-members: 7 | :noindex: 8 | -------------------------------------------------------------------------------- /docs/evac.rst: -------------------------------------------------------------------------------- 1 | Evacuation 2 | __________ 3 | 4 | .. automodule:: fdsreader.evac.EvacCollection 5 | :autosummary: 6 | :members: 7 | :undoc-members: 8 | :noindex: 9 | 10 | .. automodule:: fdsreader.evac.evacuation 11 | :autosummary: 12 | :members: 13 | :undoc-members: 14 | :noindex: 15 | -------------------------------------------------------------------------------- /docs/export.rst: -------------------------------------------------------------------------------- 1 | Export 2 | ______ 3 | 4 | .. automodule:: fdsreader.export.sim_exporter 5 | :members: 6 | :undoc-members: 7 | :noindex: 8 | 9 | .. automodule:: fdsreader.export.slcf_exporter 10 | :members: 11 | :undoc-members: 12 | :noindex: 13 | 14 | .. automodule:: fdsreader.export.obst_exporter 15 | :members: 16 | :undoc-members: 17 | :noindex: 18 | 19 | .. automodule:: fdsreader.export.smoke3d_exporter 20 | :members: 21 | :undoc-members: 22 | :noindex: 23 | -------------------------------------------------------------------------------- /docs/fds_classes.rst: -------------------------------------------------------------------------------- 1 | FDS classes 2 | ___________ 3 | 4 | .. automodule:: fdsreader.fds_classes.mesh 5 | :members: 6 | :undoc-members: 7 | :noindex: 8 | 9 | .. automodule:: fdsreader.fds_classes.surface 10 | :members: 11 | :undoc-members: 12 | :noindex: 13 | 14 | .. automodule:: fdsreader.fds_classes.ventilation 15 | :members: 16 | :undoc-members: 17 | :noindex: 18 | -------------------------------------------------------------------------------- /docs/geom.rst: -------------------------------------------------------------------------------- 1 | Complex Geometry 2 | ________________ 3 | 4 | .. automodule:: fdsreader.geom.geometry 5 | :autosummary: 6 | :members: 7 | :undoc-members: 8 | :noindex: 9 | 10 | .. automodule:: fdsreader.geom.GeometryCollection 11 | :autosummary: 12 | :members: 13 | :undoc-members: 14 | :noindex: -------------------------------------------------------------------------------- /docs/img/data-structure.drawio: -------------------------------------------------------------------------------- 1 | 7V3ZcuI6EP0aqu59IOUd/JhlJrNkJpmbWZ9SDgjwxCBimwTy9VcG2XhpGQNesaqmamJjbOFzJJ1udbc68uV0eW0b88kXPERWRxKGy4581ZEkUevJ5D/vzGpzRpeVzYmxbQ7pRdsT9+YboicFenZhDpETudDF2HLNefTkAM9maOBGzhm2jV+jl42wFX3q3BijxIn7gWElz/4yh+6EnhU1ffvBB2SOJ/TRfam3+WBq+BfTX+JMjCF+DZ2S33XkSxtjd/PXdHmJLO/l+e/l18fVL+vmSbv+9M15Nn5cfP7+9Wd3c7P3+3wl+Ak2mrkH3/r7p9mfv/rTg9oV/3zXJuPx4vpnV9Hob3NX/gtDQ/L+6CG23Qke45lhvduevbDxYjZE3m0FcrS95gbjOTkpkpN/keuuKBmMhYvJqYk7tein5GfYq9/e989U//APvd364GoZOVrRI8c1bPfcIwU5McMz5J97b1pW8I2hfwWeo9nmTPjzpemGHk2O/vitIn9vH+wd+M/N+P4pTg5e2AP6Mt+eRu8//FT+/p7fvf/2KlyfP37qUpwE0vAxclPAkdXNhR4iITJTeK8RniLydsgFNrIM13yJMt6gHWccXBd89Q6b5IdIgt/JBdoi2sVlXYjeYvOL6Le2FCPv2ViFLpt7Fzjs5/Tgx7w/6HLyx+b5/lHojWxPrfvAHv1B7VXSHw7gZJV9qKL+oOiF9IdjadxPp7Gspl5fEI/7nMe15bHYbyKPA0VXJo91vSk8zkWbHNgXquKx1EQea4paPo9FQeZEDon/yohsar//rjQ0//x8+9idfxg9YXXZLUlny0qMqWJMZ28amrvOrnhgB1+52HihvYvL2cRHjHMVjOJaEwbtokU0zNHGi+gGchRGogkcVfRiOZr2Bl8Ma0Hfwr05XXhvBc8S7HVezallrGf0EZ65PpE9lhiWOSb4Xw0IsMgmJ16Q7ZoDwzqnH7gefS8GE9Ma3hgrvPDgIrJg8OQfXUywbb6R2xo+k9eqgfJc0iJX3HvfpCSzkUOuufM5JQanbgzHpdcMsGUZc8d8DBo8JZwxZxfYdfHUv1Gk6wXu2o2osfFT4AAW95Ya3ttAyxDNklz0Z9MYCTZHryHXs+9tm4S8zrIm7CDvdPkfGrjGbEzewPZhcvRpoq8wws/TgOdJWvRxhkVQnxkuuvDeoRPjbS5MFRNM7UgXnuN/Qr4WZyp52W6IlRYauUxOOnNjYM7GN+trrpTtmf/o7/VOYfLdkbXmA3necD3W2dg1XGNDKY8kdDwgDVAvyD/y2i698VQlDbokx+L2mPzzLrfdSzwj1DLMNYcQ4esr8jgLsCu19+5m1yqK2y4+xfEN0ykC7L4oSgwUp8iZIOfkcEwZUuiUDZst+aCtShWjLTPQdhb2yBhwvHPGu9cvD2/QFkiqiQ3e+JG8rcXAExUc83wxFwWlYtBZE/ML+aXmRkly0HMGXQZ0Wqmgs+ZxxzL5uJ473GqJsk03ltqP6du716vv1o3Qf5K+oY9dmTWyD8kbf5BPT4BXDHi/ROUGAi71QcAvpc65YDqYC7hizDOhRAUHd3TWbE5+LIHQ4pDnDblcon6DIWfN5UP0wifzAlwwJWo30BvOssrRizFoN9oJFLMSgI12v0TpBqKtsJT6FD+hliu3/PGW/aCWynxuEN4xiC1zvYYTWjRMrm/swH9KkFyvV1LAv69XJ7tighRykhQyQADLeETWHXbM9cKTfGVvro0Royq3uSxn1WVSQahqCVSvzCmaOXyZrpplun5ssbaXcVKPh8ZnW6cTY+t0/tJwHdfpdD7+7Bp/tMxs23PZrl+QiSCpDBFhOA/uYm6hf/49OR1RFJaV23tSci4JwLRMx+VYZsayTFMODopI2nJfkDPhkqACSSAKWnSWFnU1Gz1E32jYTxUk4oEhVdArXRXAi4SsGYSMLIiGK7dwxAn6by0jeOB4XwaQQ98eOT0XUmFgVh6gI7K8QwOM7aHpjQkczlrG38BNhkZZbnwdCWvmEJsczC+4ydCYG4M1FIpPk67IGSrBRBqrH4rM36GRooIqnLWArEf8Gk5YWJ8gH/gk8eVfLDEgkQLmpyl0tkkK25SFvOsowIbQ5rpw9kCau3BnehfkjDky5asbzwmQ5egtGKUVgBoNkftIcqwpjNSx3PJwEwT+uY0OS3CZWyvlOzCLNlZq6sKEjRVWEMSJGiuHLoindvVaWjTwPMCCm0Y5nRzeRSFZpjkDIykxkLSNoblogyWTD5CVJxL4coUbMjmiWqYdgy9+WPdd/UaUFs7No9yTvoh3vpBuRrG4/YRX2NhILXYStjbUlfR0210srj4tnpXLB/H5+uHCv2F+xSSO64jJEfV2m8zDlXsFyl3tx5S03kt2axno1spu87Ppwp0HP+2cNoIefZRAh/iVw7QB17GsZt5oTTG61CJzO+u/SEqtJiwogUG9WC+Kq1cgj2683hfFfudcFYwA63mEKe6vsnfLIMMqGZJKSxvTp3TC1YOh+UE4k2Q/j+RQ92OQjxL9Bh6NHFSIg1BM5hvdWdiVk5BxiVFCKIOWQWKAlkOKQXgigQxQfKPnYnheGDOXzOCtWDBN78O1dP3Bkxcrr9U1p631++2PY5mOPxhHKKeJC/8jYc3qBixK+EtJc+5+8ch1QW10gSZmrE92UroAzqJlRVJNoXjclswkQQdukCLoMXD0Kl1wHJujCHgeUgGwVq0I/PAn7gos0RXo7yO0e4OVrJFyJbkCgSm5Ea5AVoRqla7AbuwbxbkC5WQQ871XVYwr/hqUI87qCBTllCmgcYof5ikrE4Z7AoMu3BzdL7N0v+cJ5EA2R/jLcOU6MneSLz9sJgQyqnJAaxgLCHtWWPWqyPRLfiycQsDhPDYIsLgOyi3zInDNWhK4KNNcSS6l3S8eHa7c66HcM7vqRe3klbsfCNmW/J6Mg07Qg5uj2hWJgeSQWR2Ng1kD5Q4LPZZBTUbQeWujMfYHsnrFzjLB2ryGtj+OpUp1GEiWU+SFCABsP3A8C5HoxU2XvD5IEbhm3Z4jLXP+OFwbvxtuA5dFFerH2LksqtYrQ0IBZucmLIv6JK/ZsmhpKRJK0m/2MdiJJYEbd7EU72KRhZjPI/PqqKbsJFrDfSwqy8fS5sj6oAs3x8PiewUSOFroBVmtXRjdH8nK3SsqK3PpZDfzKgzLyj0sKsvD4trmetLgYDbIzaKy3SweOhzMJvlYVJ6KVgSuWX0sRS1vq2Au2j23vmpjfWVf4T792FSVVW6/7TOK2ryMtOBpXOwdj2bl4amiwDKo226G7Q9mDcpUsrxcZBrFNoeynkYY3OYMRfbbrtsPwLXqsFQt2UPv6FbmXLXXQLVLUta68yel2kE/ELDCxzPKYt24lrod9uqxZHubo6b2x7H6dROWxpvTWZd3yloqdhhMlvXlGmOOY4PkusZDGYvAtUS5DkugfgLF9uwRoNMLwxGF8Fuinu6aRBTqyRlyLZINem9uY5VrY8V3CFD9gWBXBfcDbax+bIsARaqFjQVzlSUAqI2VZGxLRICei4UFsSoPDQBulcIKm+CVHg7CE7K0isITntaY4rzNNVj2BxKyskrtmKxEzrnhDiYcySPtrKKghNvM7awicIXsrIL21IEzOxMglpowpumd8LbPutbrpGaNkYM7ZJNZYG0YRDPJusKZICkRs+1MlgM7jmG6rY/i99xPkB+ywTRsp3dytuZiuUo+JcXY2kuw5++OXaDPbdtYhS6jnYj9ICG26ELDkdibU8eul+hi7JbhmybkutriZ25V1QckPdwJxDNB0A7vBGKU/7vYXzzR4evUkpguxJnuEyx3pgtxU3sH0+MNE3u5Mv3r9eR28XU5vPqlvcPjc/3b23jRVaslek+KDvZ9PZ3ngTNuP05D/aMqnufunzt0HNbLZ5tWLdtkTY7Sraek0g3KLE8fas8EMSY3NE2qp9wAARLLGoRjJciUeK50boOwtKfciFc17svFd4uKSzRoohjtFrqcbRT25LUgx/iuaofwvUYjtJh7TYYDqevvV1kmFatZY9sK3wPKhZBBV4ouzZ3JkljPQRe+rphRd3/CiXsKVjFumqm5EjTtpUZ8TprleWQeyR9j748frulln2/OkocEHyS4vWWux5fXiemi+7mxBvXVNuZRlu7HjuxLcnqvF+3nJaYpgq84mUDMc9k61W2sJsR3XFUyEuSwndVquql76lSdWE2YGa2oJJLafZsTDgvs8BzJe3pwV60o8pkPnJVHxfIi7vmDWuLuaqlhZZFcGXeQ3L6SS4Lyg7h6IpAoAwZxpZWZbFwQF0xUVoBBu7YESe/EtYzjgoPyWQqvLYW/cwKy8gAuZtmJlqQ85YRjmfFbcJOhyEqu7o6ENXMwVw76DvT7JhMSr8O35gqvXIXX17nPJ32JMjGPjEwLPcwNN2mSnN5kktqFa+n3AVsM+fJDbh8OZA09PmCLeV3D/EEt0eMDNjjpxfPayJP3KlQFSrxgiZ6RI6dVVB6WBaySGi1K3kvtx82RBWLa/gBtyBDKB8fKVYHvbG5zpdF8oCyzNgqs1JlWV0vKUuaDY6m1UeA+ydoxvSVu2JxwLDNGC8YRWhfhJteRsJZYSR5Mjq4m7awx++4VnZbQi5k7/rF/h03EbyIpIXEfJe691WOEySm7Id5eap5lblb0+qPjdUFKV5NgVnrRJvC3AzWbUpPEy6/ZlNbs6HYWt49kKF4MwPAJ7vspP+ZH0TKuBGsCmyaNC/kB+coyM4l4RXSAPW1Rm9qLaxnrA7aYS9r8Qc0a91NU3GgyGu8d3Cv5HFJBKkkvoxmrHzSH1DSqAOQpj2raGdWkZabbngsJRY09EsvXZTgP7mJuoX/+bYE4yAdLueqNMCVWIDAB0zIdl2OZGUutROelqf3+u9LQ/PPz7WN3/mH0hNVlNxkKfD8lAykFlauCNFWQgDmrUGCrgpgoaOm2KyBTWckkJ7/ryg6asbt1LcIKwOaxSgKf8MpX/iiWGVQANo+VKnzqO67kD2WZQQVg81hbWp3wfiv5o1hqSAHYPjDyrl22cwGwlrjbCtg8qeLKkOFCUEHtpx1FIaOLfbtLQAVr0wPLcBxz0AkvT28ld2xZ7rAFQrjrACuE8IU6zJbcFwRTmxn24r4Yg+Qgzc214s21eBA4N9dCRJVASXEpdc5p8PD6Ty4uYl27vkYbM14RjUYcxqwwVm61MQt1LVdvHMasMFZusYmsIg4jNHwY2+S7HMuMWNbAbuO72heBa9WGm5h0q3hifQFXPuKKvXzFnnX9rQWCXYJ8R3yBZU8vQF3EOjNLjC+x7INj5WrdFwJ8keV4MCvX7MwoKL7Msg+O1et1MAKK6/VjcS1Or5NDG2M3rJhsYz75gofIu+J/ -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | FDSReader documentation! 2 | ======================== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | :caption: FDSReader 7 | 8 | simulation 9 | slcf 10 | bndf 11 | geom 12 | isof 13 | pl3d 14 | smoke3d 15 | part 16 | devc 17 | evac 18 | quantity 19 | fds_classes 20 | utils 21 | export 22 | 23 | .. include:: simulation.rst 24 | .. include:: slcf.rst 25 | .. include:: bndf.rst 26 | .. include:: geom.rst 27 | .. include:: isof.rst 28 | .. include:: pl3d.rst 29 | .. include:: smoke3d.rst 30 | .. include:: part.rst 31 | .. include:: devc.rst 32 | .. include:: evac.rst 33 | .. include:: quantity.rst 34 | .. include:: fds_classes.rst 35 | .. include:: utils.rst 36 | .. include:: export.rst 37 | -------------------------------------------------------------------------------- /docs/isof.rst: -------------------------------------------------------------------------------- 1 | Isosurface files 2 | ________________ 3 | 4 | .. automodule:: fdsreader.isof.IsosurfaceCollection 5 | :autosummary: 6 | :members: 7 | :undoc-members: 8 | :noindex: 9 | 10 | .. automodule:: fdsreader.isof.isosurface 11 | :autosummary: 12 | :members: 13 | :undoc-members: 14 | :noindex: 15 | -------------------------------------------------------------------------------- /docs/part.rst: -------------------------------------------------------------------------------- 1 | Particles 2 | _________ 3 | 4 | .. automodule:: fdsreader.part.ParticleCollection 5 | :autosummary: 6 | :members: 7 | :undoc-members: 8 | :noindex: 9 | 10 | .. automodule:: fdsreader.part.particle 11 | :autosummary: 12 | :members: 13 | :undoc-members: 14 | :noindex: 15 | -------------------------------------------------------------------------------- /docs/pl3d.rst: -------------------------------------------------------------------------------- 1 | Plot3D files 2 | ____________ 3 | 4 | .. automodule:: fdsreader.pl3d.Plot3DCollection 5 | :autosummary: 6 | :members: 7 | :undoc-members: 8 | :noindex: 9 | 10 | .. automodule:: fdsreader.pl3d.pl3d 11 | :autosummary: 12 | :members: 13 | :undoc-members: 14 | :noindex: 15 | -------------------------------------------------------------------------------- /docs/quantity.rst: -------------------------------------------------------------------------------- 1 | Quantity 2 | ________ 3 | 4 | .. autoclass:: fdsreader.utils.data.Quantity 5 | :members: 6 | :undoc-members: 7 | :noindex: 8 | -------------------------------------------------------------------------------- /docs/simulation.rst: -------------------------------------------------------------------------------- 1 | Simulation 2 | __________ 3 | 4 | .. automodule:: fdsreader.simulation 5 | :members: 6 | :undoc-members: 7 | :noindex: 8 | -------------------------------------------------------------------------------- /docs/slcf.rst: -------------------------------------------------------------------------------- 1 | Slice files 2 | ___________ 3 | 4 | .. automodule:: fdsreader.slcf.SliceCollection 5 | :autosummary: 6 | :members: 7 | :undoc-members: 8 | :noindex: 9 | 10 | .. automodule:: fdsreader.slcf.slice 11 | :autosummary: 12 | :members: 13 | :undoc-members: 14 | :noindex: 15 | 16 | .. automodule:: fdsreader.slcf.GeomSliceCollection 17 | :autosummary: 18 | :members: 19 | :undoc-members: 20 | :noindex: 21 | 22 | .. automodule:: fdsreader.slcf.geomslice 23 | :autosummary: 24 | :members: 25 | :undoc-members: 26 | :noindex: 27 | -------------------------------------------------------------------------------- /docs/smoke3d.rst: -------------------------------------------------------------------------------- 1 | Smoke3D files 2 | _____________ 3 | 4 | .. automodule:: fdsreader.smoke3d.Smoke3DCollection 5 | :autosummary: 6 | :members: 7 | :undoc-members: 8 | :noindex: 9 | 10 | .. automodule:: fdsreader.smoke3d.smoke3d 11 | :autosummary: 12 | :members: 13 | :undoc-members: 14 | :noindex: 15 | -------------------------------------------------------------------------------- /docs/utils.rst: -------------------------------------------------------------------------------- 1 | Utilities 2 | _________ 3 | 4 | .. automodule:: fdsreader.utils.extent 5 | :members: 6 | :undoc-members: 7 | :noindex: 8 | 9 | .. automodule:: fdsreader.utils.dimension 10 | :members: 11 | :undoc-members: 12 | :noindex: 13 | -------------------------------------------------------------------------------- /examples/bndf/bndf_example_anim.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib.animation as animation 3 | import numpy as np 4 | 5 | from fdsreader import Simulation 6 | 7 | 8 | def main(): 9 | sim = Simulation("./fds_data") 10 | 11 | # Get first obstruction 12 | obst = sim.obstructions.get_nearest(-0.8, 1, 1) 13 | 14 | # Get all patches with orientation=1 (equals positive x-dimension) 15 | orientation = 1 16 | quantity = "Wall Temperature" 17 | face = obst.get_global_boundary_data_arrays(quantity)[orientation] 18 | 19 | # Value range 20 | vmax = np.ceil(obst.vmax(quantity)) 21 | vmin = np.floor(obst.vmin(quantity)) 22 | 23 | # Value ticks 24 | ticks = [vmin+i*(vmax-vmin)/10 for i in range(11)] 25 | 26 | t = 0 27 | fig = plt.figure() 28 | # Initial contour plot 29 | cont = plt.contourf(face[t].T, origin="lower", vmin=vmin, vmax=vmax, levels=ticks, cmap="coolwarm") 30 | plt.colorbar(cont) 31 | 32 | # Contour plot animation 33 | def animate(i): 34 | nonlocal t, cont 35 | for c in cont.collections: 36 | c.remove() 37 | cont = plt.contourf(face[t].T, origin="lower", vmin=vmin, vmax=vmax, levels=ticks, cmap="coolwarm") 38 | t += 1 39 | return cont 40 | # Show animation or save it to disk 41 | anim = animation.FuncAnimation(fig, animate, interval=5, frames=face.shape[0]-2, repeat=False) 42 | # anim.save("anim.mp4", fps=25) 43 | plt.show() 44 | 45 | 46 | if __name__ == "__main__": 47 | main() 48 | -------------------------------------------------------------------------------- /examples/bndf/fds_data/wall_005.fds: -------------------------------------------------------------------------------- 1 | &HEAD CHID='wall_test' / 2 | 3 | &TIME T_END=20.0, TIME_SHRINK_FACTOR=10./ 4 | 5 | &MULT ID='mesh', DX=1, DY=0.8, I_UPPER=3, J_UPPER=5,/ 6 | 7 | &MESH IJK= 20,16,72 XB=-1.5,-0.5,-2,-1.2,-0.1000,3.5, MULT_ID='mesh' / 8 | 9 | &DUMP RENDER_FILE='Scene.ge1' 10 | STATUS_FILES=.TRUE. 11 | SMOKE3D=.FALSE. / 12 | 13 | /&MISC RESTART=.TRUE./ 14 | 15 | &VENT MB='XMIN', SURF_ID='OPEN'/ 16 | &VENT MB='XMAX', SURF_ID='OPEN'/ 17 | &VENT MB='YMIN', SURF_ID='OPEN'/ 18 | &VENT MB='YMAX', SURF_ID='OPEN'/ 19 | &VENT MB='ZMIN', SURF_ID='OPEN'/ 20 | &VENT MB='ZMAX', SURF_ID='OPEN'/ 21 | 22 | 23 | &SURF ID='wall', MATL_ID='Stahlbeton',THICKNESS=0.08, BACKING="EXPOSED"/ 24 | &MATL ID = 'Stahlbeton', CONDUCTIVITY = 2.3, SPECIFIC_HEAT = 1, DENSITY = 2300./ 25 | &SURF ID='Boden', MATL_ID='Stahlbeton',THICKNESS=0.2, BACKING="EXPOSED"/ 26 | 27 | /Boden 28 | &OBST ID='Boden' XB=-1,2,-1.2,2.4,-0.1,0, SURF_ID='Boden'/ 29 | 30 | /Decke 31 | &OBST ID='Decke' XB=-1,2,-1.2,2.4,3,3.2, SURF_ID='Boden'/ 32 | 33 | /Wand rechts 34 | &OBST ID='Wand_r' XB=-0.85,-0.8,-1,2.4,0,3, SURF_ID='wall'/ 35 | 36 | /Wand vorne 37 | &OBST ID='Wand_v' XB=-1,2,-1.2,-1,0,3, SURF_ID='wall'/ 38 | 39 | &OBST ID='Golf_brandfläche' XB= -0.3,1.1,0.4,1.7,0.4,0.45 / 40 | 41 | /Brand 42 | &SPEC ID='PUR', LUMPED_COMPONENT_ONLY=.FALSE., FORMULA='C4.837353H7.917138N0.427652O1.745690' / 43 | &SPEC ID='OXYGEN', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C0.000H0.000O2.000N0.000Cl0.000' / 44 | &SPEC ID='HCN', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C1.000H1.000O0.000N1.000Cl0.000' / 45 | &SPEC ID='AMMONIA', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C0.000H3.000O0.000N1.000Cl0.000' / 46 | &SPEC ID='CARBON MONOXIDE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C1.000H0.000O1.000N0.000Cl0.000' / 47 | &SPEC ID='CARBON DIOXIDE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C1.000H0.000O2.000N0.000Cl0.000' / 48 | &SPEC ID='WATER VAPOR', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C0.000H2.000O1.000N0.000Cl0.000' / 49 | &SPEC ID='NITROGEN', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C0.000H0.000O0.000N2.000Cl0.000' / 50 | &SPEC ID='SOOT', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C1.000H0.000O0.000N0.000Cl0.000' / 51 | &SPEC ID='BENZENE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C6.000H6.000O0.000N0.000Cl0.000' / 52 | &SPEC ID='BENZONITRILE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C7.000H5.000O0.000N1.000Cl0.000' / 53 | &SPEC ID='BIPHENYL', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C12.000H10.000O0.000N0.000Cl0.000' / 54 | &SPEC ID='INDENE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C9.000H8.000O0.000N0.000Cl0.000' / 55 | &SPEC ID='ISOQUINOLINE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C9.000H7.000O0.000N1.000Cl0.000' / 56 | &SPEC ID='NAPHTHALENE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C10.000H8.000O0.000N0.000Cl0.000' / 57 | &SPEC ID='NAPHTHALENE-1-CARBONITRILE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C11.000H7.000O0.000N1.000Cl0.000' / 58 | &SPEC ID='STYRENE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C8.000H8.000O0.000N0.000Cl0.000' / 59 | &SPEC ID='TOLUENE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C7.000H8.000O0.000N0.000Cl0.000' / 60 | &SPEC ID='XYLENE', LUMPED_COMPONENT_ONLY=.TRUE., FORMULA='C8.000H10.000O0.000N0.000Cl0.000' / 61 | 62 | 63 | &SPEC ID = 'AIR', 64 | SPEC_ID(1) = 'OXYGEN', VOLUME_FRACTION(1) = 1.000, 65 | SPEC_ID(2) = 'NITROGEN', VOLUME_FRACTION(2) = 3.760, 66 | BACKGROUND=.TRUE. / 67 | 68 | &SPEC ID='PRODUCTS', 69 | SPEC_ID(1)='HCN', VOLUME_FRACTION(1)=0.038951, 70 | SPEC_ID(2)='AMMONIA', VOLUME_FRACTION(2)=0.091211, 71 | SPEC_ID(3)='CARBON MONOXIDE', VOLUME_FRACTION(3)=1.789193, 72 | SPEC_ID(4)='CARBON DIOXIDE', VOLUME_FRACTION(4)=2.194841, 73 | SPEC_ID(5)='WATER VAPOR', VOLUME_FRACTION(5)=3.653950, 74 | SPEC_ID(6)='NITROGEN', VOLUME_FRACTION(6)=15.346936, 75 | SPEC_ID(7)='SOOT', VOLUME_FRACTION(7)=0.483967, 76 | SPEC_ID(8)='BENZENE', VOLUME_FRACTION(8)=0.022844, 77 | SPEC_ID(9)='BENZONITRILE', VOLUME_FRACTION(9)=0.008317, 78 | SPEC_ID(10)='BIPHENYL', VOLUME_FRACTION(10)=0.000334, 79 | SPEC_ID(11)='INDENE', VOLUME_FRACTION(11)=0.001073, 80 | SPEC_ID(12)='ISOQUINOLINE', VOLUME_FRACTION(12)=0.001932, 81 | SPEC_ID(13)='NAPHTHALENE', VOLUME_FRACTION(13)=0.005348, 82 | SPEC_ID(14)='NAPHTHALENE-1-CARBONITRILE', VOLUME_FRACTION(14)=0.000995, 83 | SPEC_ID(15)='STYRENE', VOLUME_FRACTION(15)=0.002278, 84 | SPEC_ID(16)='TOLUENE', VOLUME_FRACTION(16)=0.002920, 85 | SPEC_ID(17)='XYLENE', VOLUME_FRACTION(17)=0.000123/ 86 | 87 | 88 | &VENT XB=-0.3,1.1,0.4,1.7,0.45,0.45 SURF_ID='Brandfläche'/ 89 | &REAC ID='Brandfläche', FUEL='PUR', SPEC_ID_NU = 'PUR','AIR','PRODUCTS', NU= -1,-4.043567,1., HEAT_OF_COMBUSTION=40000 / 90 | &SURF ID='Brandfläche',SPEC_ID='PUR', HRRPUA=2823.129, COLOR='RED', RAMP_Q='fire_ramp'/ 91 | 92 | /Vorgabe der Brandkurve 93 | 94 | &RAMP ID='fire_ramp', T=0, F=0.0/ 95 | &RAMP ID='fire_ramp', T=240, F=0.1687/ 96 | &RAMP ID='fire_ramp', T=960, F=0.1687/ 97 | &RAMP ID='fire_ramp', T=1440, F=0.6627/ 98 | &RAMP ID='fire_ramp', T=1500, F=1.0/ 99 | &RAMP ID='fire_ramp', T=1620, F=0.5422/ 100 | &RAMP ID='fire_ramp', T=2280, F=0.1205/ 101 | &RAMP ID='fire_ramp', T=4200, F=0/ 102 | 103 | /Wall Temperature 104 | 105 | &BNDF QUANTITY='WALL TEMPERATURE', CELL_CENTERED=.TRUE./ 106 | &BNDF QUANTITY='BACK WALL TEMPERATURE', CELL_CENTERED=.TRUE./ 107 | 108 | &TAIL / 109 | -------------------------------------------------------------------------------- /examples/devc/devc_example.py: -------------------------------------------------------------------------------- 1 | import fdsreader as fds 2 | 3 | 4 | def main(): 5 | sim = fds.Simulation("./fds_steckler") 6 | 7 | print(sim.devices["TC_Door"][0].data) 8 | 9 | try: 10 | df = sim.devices.to_pandas_dataframe() 11 | print(df) 12 | except ModuleNotFoundError: 13 | pass # pandas not installed 14 | 15 | 16 | if __name__ == "__main__": 17 | main() 18 | -------------------------------------------------------------------------------- /examples/devc/fds_steckler/steckler.fds: -------------------------------------------------------------------------------- 1 | &HEAD CHID='Steckler', TITLE='Steckler Compartment'/ 2 | 3 | &MESH IJK=36,28,22, XB=0.00,3.60,-1.40,1.40,0.00,2.13 / 4 | 5 | &TIME T_END=100.0, TIME_SHRINK_FACTOR=10. / 6 | &DUMP NFRAMES=100, DT_DEVC=10., DT_HRR=10., SIG_FIGS=4, SIG_FIGS_EXP=2 / 7 | 8 | &MISC TMPA=22. / 9 | 10 | &SURF ID='BURNER',HRRPUA=1048.3,TMP_FRONT=100.,COLOR='ORANGE' / 62.9 kW 11 | 12 | &HOLE XB=2.75,2.95,-0.10,0.15,0.00,1.83 / 2/6 Door 13 | 14 | &VENT XB=1.30,1.50,-0.10,0.10,0.00,0.00, SURF_ID='BURNER' / Position A 15 | &VENT XB=1.25,1.30,-0.05,0.05,0.00,0.00, SURF_ID='BURNER' / 16 | &VENT XB=1.50,1.55,-0.05,0.05,0.00,0.00, SURF_ID='BURNER' / 17 | &VENT XB=1.35,1.45,-0.15,-.10,0.00,0.00, SURF_ID='BURNER' / 18 | &VENT XB=1.35,1.45, 0.10,0.15,0.00,0.00, SURF_ID='BURNER' / 19 | 20 | &SURF ID='FIBER BOARD', DEFAULT=.TRUE., MATL_ID='INSULATION', THICKNESS=0.013, COLOR='BEIGE' / 21 | 22 | &REAC FUEL='METHANE',SOOT_YIELD=0. / 23 | 24 | &OBST XB=2.80,2.90,-1.40,1.40,0.00,2.13 / Wall with door or window 25 | 26 | &VENT MB='XMAX',SURF_ID='OPEN'/ 27 | &VENT XB= 2.90,3.60,-1.40,-1.40,0.00,2.13, SURF_ID='OPEN'/ 28 | &VENT XB= 2.90,3.60, 1.40, 1.40,0.00,2.13, SURF_ID='OPEN'/ 29 | &VENT XB= 2.90,3.60,-1.40, 1.40,2.13,2.13, SURF_ID='OPEN'/ 30 | 31 | &MATL ID = 'INSULATION' 32 | DENSITY = 200. 33 | CONDUCTIVITY = 0.1 34 | SPECIFIC_HEAT = 1. / 35 | 36 | &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' / 37 | &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' / 38 | &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. / 39 | 40 | &DEVC ID='HGL Temp', XB=2.50,2.50,1.10,1.10,0.00,2.13, QUANTITY='UPPER TEMPERATURE' / 41 | &DEVC ID='HGL Height', XB=2.50,2.50,1.10,1.10,0.00,2.13, QUANTITY='LAYER HEIGHT' / 42 | 43 | &TAIL / 44 | -------------------------------------------------------------------------------- /examples/evac/evac_example.py: -------------------------------------------------------------------------------- 1 | import fdsreader as fds 2 | 3 | 4 | def main(): 5 | fds.settings.DEBUG = True 6 | sim = fds.Simulation("fds_no_fed/stairs_evss_meshes_evmc.smv") 7 | 8 | # Get all particles with specified id 9 | evacs = sim.evacs 10 | 11 | print(evacs, evacs.eff, evacs.xyz, evacs.fed_grid, evacs.fed_corr, evacs.devc, evacs[0], 12 | evacs.get_unfiltered_positions(), evacs.get_unfiltered_data("HUMAN_SPEED")) 13 | 14 | 15 | if __name__ == "__main__": 16 | main() 17 | -------------------------------------------------------------------------------- /examples/evac/fds_fed/fed_verification_multi_mesh.fds: -------------------------------------------------------------------------------- 1 | / ------------------------------------------------------------------- 2 | &HEAD CHID ='fed_verification_multi_mesh', 3 | TITLE ='Test: FED fire+evac multi meshes' / 4 | / ------------------------------------------------------------------- 5 | / Mesh - fire 6 | / ------------------------------------------------------------------- 7 | &MESH IJK=22,20,5, XB= -0.2,10.6, 0.0,10.0, 0.0,2.5, ID='Fire_1stFloor' / 8 | &MESH IJK=22,20,5, XB= -0.2,10.6, 0.0,10.0, 2.5,5.0, ID='Fire_2ndFloor' / 9 | 10 | / ------------------------------------------------------------------- 11 | / Mesh - evac 12 | / ------------------------------------------------------------------- 13 | &MESH IJK = 60,54,1, 14 | XB = -0.4,11.6,-0.4,10.4, 0.4,1.6, 15 | EVAC_Z_OFFSET=1.2, 16 | EVACUATION=.TRUE., 17 | EVAC_HUMANS=.TRUE., 18 | ID='MainEvacGrid_base_floor' / 19 | &MESH IJK=54,54,1, 20 | XB= -0.4,10.4,-0.4,10.4, 3.0,4.2, 21 | EVAC_Z_OFFSET=1.2, 22 | EVACUATION=.TRUE., 23 | EVAC_HUMANS=.TRUE., 24 | ID='MainEvacGrid_1st_floor' / 25 | 26 | 27 | / ------------------------------------------------------------------- 28 | /miscellaneous 29 | / ------------------------------------------------------------------- 30 | &MISC NO_EVACUATION = .FALSE., 31 | EVACUATION_INITIALIZATION = .TRUE., 32 | EVACUATION_MC_MODE = .FALSE., 33 | EVACUATION_DRILL = .FALSE., 34 | EVACUATION_WRITE_FED = .FALSE., 35 | EVAC_TIME_ITERATIONS = 50, 36 | EVAC_PRESSURE_ITERATIONS = 50 / 37 | 38 | &DUMP SMOKE3D=.TRUE., 39 | DT_PART=0.5, 40 | DT_HRR=1.0, 41 | DT_SLCF=1.0, 42 | DT_BNDF=1000.0, 43 | DT_PL3D=1000.0, 44 | DT_ISOF=1000.0 / 45 | 46 | / ------------------------------------------------------------------- 47 | / simulation time 48 | / ------------------------------------------------------------------- 49 | &TIME T_BEGIN = 0.0, 50 | T_END = 600.0, 51 | EVAC_DT_FLOWFIELD = 0.01 / 52 | 53 | 54 | &REAC ID = 'POLYURETHANE' 55 | FUEL = 'myPOLYURETHANE' 56 | FYI = 'C_6.3 H_7.1 N O_2.1, NFPA Handbook, Babrauskas' 57 | SOOT_YIELD = 0.10 58 | CO_YIELD = 0.05 59 | N = 1.0 60 | C = 6.3 61 | H = 7.1 62 | O = 2.1 / 63 | 64 | &SURF ID='BURNER', HRRPUA=1000., COLOR='RASPBERRY' / 65 | 66 | &MATL ID = 'GYPSUM PLASTER' 67 | FYI = 'Quintiere, Fire Behavior' 68 | CONDUCTIVITY = 0.48 69 | SPECIFIC_HEAT = 0.84 70 | DENSITY = 1440. / 71 | 72 | &SURF ID = 'WALL', DEFAULT=.TRUE., 73 | RGB = 100,100,100 74 | MATL_ID = 'GYPSUM PLASTER' 75 | THICKNESS = 0.012 / 76 | 77 | &SURF ID= 'EVAC_WALL', COLOR='RED', EVAC_DEFAULT=.TRUE. / or RGB 78 | 79 | ============= FIRE FDS GEOMETRY STARTS ================ 80 | 81 | &OBST XB= -0.20,10.20, -0.20, 10.20, 2.40, 2.60, SURF_ID='WALL' / floor 82 | &HOLE XB= 2.20, 7.80, 2.20, 7.80, 2.39, 2.61 / floor hole 83 | &OBST XB= 2.00, 8.00, 2.00, 2.20, 2.60, 3.60, SURF_ID='WALL', COLOR='BLUE' / balustrade 84 | &OBST XB= 2.00, 8.00, 7.80, 8.00, 2.60, 3.60, SURF_ID='WALL', COLOR='BLUE' / balustrade 85 | &OBST XB= 2.00, 2.20, 2.00, 8.00, 2.60, 3.60, SURF_ID='WALL', COLOR='BLUE' / balustrade 86 | &OBST XB= 7.80, 8.00, 2.00, 8.00, 2.60, 3.60, SURF_ID='WALL', COLOR='BLUE' / balustrade 87 | &OBST XB= 10.20,11.60, 4.20, 5.80, 2.40, 2.60, SURF_ID='WALL', COLOR='BLUE' / floor 88 | 89 | &OBST XB= -0.20, 0.00, -0.20, 10.20, 0.00, 5.00, SURF_ID='WALL' / 90 | &OBST XB= 10.00,10.20, -0.20, 10.20, 0.00, 5.00, SURF_ID='WALL' / 91 | &OBST XB= -0.20,10.20, -0.20, 0.00, 0.00, 5.00, SURF_ID='WALL' / 92 | &OBST XB= -0.20,10.20, 10.00, 10.20, 0.00, 5.00, SURF_ID='WALL' / 93 | &OBST XB= 10.00,11.60, 4.20, 4.40, 0.00, 2.40, SURF_ID='WALL' / Right Corridor Wall 94 | &OBST XB= 10.00,11.60, 5.60, 5.80, 0.00, 2.40, SURF_ID='WALL' / Right Corridor Wall 95 | &HOLE XB= -0.21, 0.01, 4.39, 5.61, 0.00, 2.00 / Left Door 96 | &HOLE XB= 9.99,10.21, 4.39, 5.61, 0.00, 2.00 / Right Door Hole to Corridor 97 | 98 | The fire as an burner. 99 | &OBST XB= 3.00, 4.00, 3.00, 4.00, 0.00, 0.60, SURF_ID='INERT' / 100 | &VENT XB= 3.00, 4.00, 3.00, 4.00, 0.60, 0.60, SURF_ID='BURNER' / 101 | 102 | &VENT XB= 4.0,6.0, 10.0,10.0, 0.0,2.0, SURF_ID='OPEN', COLOR='MAGENTA' / 103 | &VENT XB= 4.0,6.0, 0.0, 0.0, 0.0,2.0, SURF_ID='OPEN', COLOR='MAGENTA' / 104 | 105 | &VENT XB= -0.2,-0.2, 4.4,5.6, 0.0,2.0, SURF_ID='OPEN', COLOR='MAGENTA' / Left Exit 106 | &VENT XB= 10.6,10.6, 4.4,5.6, 0.0,2.0, SURF_ID='OPEN', COLOR='MAGENTA' / Rigth Exit 107 | 108 | ============= FIRE FDS GEOMETRY ENDS ================== 109 | 110 | ============= EVAC GEOMETRY STARTS ==================== 111 | 112 | &HOLE XB= -0.21, 0.01, 7.39, 8.61, 2.60, 4.60, EVACUATION=.TRUE. / Left Door, 2nd floor 113 | &HOLE XB= 9.99,10.21, 2.39, 3.61, 2.60, 4.60, EVACUATION=.TRUE. / Right Door, 2nd floor 114 | 115 | &EXIT ID='LeftExit', IOR=-1, 116 | FYI= 'Comment line', 117 | COLOR='YELLOW', HEIGHT=2.0, SHOW=.TRUE., 118 | TIME_OPEN=7.2,TIME_CLOSE=12.2, 119 | XYZ= 0.00, 3.00, 1.00, 120 | XB= -0.20,-0.20, 4.40,5.60, 0.40,1.60 / 121 | 122 | &EXIT ID='RightExit', IOR=+1, 123 | FYI= 'Comment line', 124 | COLOR='BLUE', HEIGHT=2.0, SHOW=.TRUE., 125 | TIME_CLOSE=7.2,TIME_OPEN=12.2, 126 | XYZ= 11.40, 5.00, 1.00, 127 | XB= 11.60,11.60, 4.40,5.60, 0.40,1.60 / 128 | 129 | Smokeview tests: +y and -y direction exits also 130 | &EXIT ID='PlusY', IOR=+2, 131 | FYI= 'Comment line', 132 | COLOR='BLUE VIOLET', HEIGHT=2.0, 133 | XYZ= 6.50, 2.80, 1.00, 134 | XB= 6.0,7.0, 3.0,3.0, 0.40,1.60 / 135 | &EXIT ID='MinusY', IOR=-2, 136 | FYI= 'Comment line', 137 | COLOR='BANANA', HEIGHT=2.0, 138 | XYZ= 6.50, 7.20, 1.00, 139 | XB= 6.0,7.0, 7.0,7.0, 0.40,1.60 / 140 | &ENTR ID='DummyEntry', 141 | FYI='Comments',COLOR='GREEN', 142 | IOR=+2, HEIGHT=2.0, 143 | XB=1.20,2.20, 2.20,2.20, 0.40,1.60 / 144 | 145 | 146 | &EVSS ID='Incline1', IOR=-1, 147 | XB= 10.00,11.60, 4.40,5.60, 0.40,1.60, 148 | HEIGHT0=0.0, HEIGHT=2.0, 149 | FAC_V0_UP=0.4, FAC_V0_DOWN=0.6, FAC_V0_HORI=1.0, 150 | COLOR='GRAY' / 151 | 152 | &EVSS ID='Incline2', IOR=+1, 153 | XB= 2.00,4.60, 4.40,5.60, 0.40,1.60, 154 | HEIGHT0=0.0, HEIGHT=2.0, 155 | FAC_V0_UP=0.4, FAC_V0_DOWN=0.6, FAC_V0_HORI=1.0, 156 | COLOR='GRAY' / 157 | 158 | &EVSS ID='Incline3', IOR=+2, 159 | XB= 5.00,6.00, 2.00,5.00, 0.40,1.60, 160 | HEIGHT0=0.0, HEIGHT=2.0, 161 | FAC_V0_UP=0.4, FAC_V0_DOWN=0.6, FAC_V0_HORI=1.0, 162 | COLOR='GRAY' / 163 | 164 | &EVSS ID='Incline4', IOR=-2, 165 | XB= 5.00,6.00, 5.00,8.00, 0.40,1.60, 166 | HEIGHT0=0.0, HEIGHT=2.0, 167 | FAC_V0_UP=0.4, FAC_V0_DOWN=0.6, FAC_V0_HORI=1.0, 168 | COLOR='GRAY' / 169 | 170 | Next is just a counter, i.e., it just produces a column in 171 | the CHID_evac.csv file. 172 | &EXIT ID='RightCounter', IOR=+1, 173 | FYI= 'Comment line', 174 | COUNT_ONLY=.TRUE., 175 | XB= 10.00,10.00, 4.40,5.60, 0.40,1.60 / 176 | 177 | Second floor doors etc 178 | 179 | &DOOR ID='LeftDoor2nd', IOR=-1, 180 | FYI= 'Comment line', 181 | COLOR='RED', HEIGHT=2.0, SHOW=.TRUE., 182 | EXIT_SIGN=.TRUE., 183 | TO_NODE= 'LeftCorr', 184 | XYZ= 0.0, 8.00, 3.6, 185 | XB= -0.20,-0.20, 7.40,8.60, 3.0,4.2 / 186 | &CORR ID='LeftCorr', 187 | FYI='Comments', 188 | MAX_HUMANS_INSIDE=20, 189 | EFF_LENGTH= 8.5, 190 | FAC_SPEED=0.7, 191 | TO_NODE='LeftCorrExit' / 192 | &EXIT ID='LeftCorrExit', 193 | FYI='A dummy exit, the end point to a corridor object', 194 | SHOW=.FALSE., COUNT_ONLY=.TRUE., 195 | IOR=-1, 196 | XB= -0.40,-0.40, 7.40,8.60, 0.40,1.60 / 197 | 198 | &DOOR ID='RightDoor2nd', IOR=+1, 199 | FYI= 'Comment line', 200 | COLOR='GREEN', HEIGHT=2.0, SHOW=.TRUE., 201 | EXIT_SIGN=.TRUE., 202 | TO_NODE= 'RightCorr', 203 | XYZ=10.0, 3.00, 3.6, 204 | XB= 10.20,10.20, 2.40,3.60, 3.0,4.2 / 205 | &CORR ID='RightCorr', 206 | FYI='Comments', 207 | MAX_HUMANS_INSIDE=20, 208 | EFF_LENGTH= 8.5, 209 | FAC_SPEED=0.7, 210 | TO_NODE='RightEntry' / 211 | &ENTR ID='RightEntry', 212 | FYI='Comments', 213 | IOR=-1,COLOR='MAGENTA', 214 | XB=10.20,10.20, 1.00,2.20, 0.40,1.60 / 215 | &HOLE XB= 9.99,10.21, 0.99,2.21, 0.00,2.00, 216 | EVACUATION=.TRUE. / Left Door, 2nd 217 | &OBST XB=10.20,10.40, 1.00,2.20, 0.40,1.60, 218 | EVACUATION=.TRUE., RGB=30,150,20 / Left Door, 2nd 219 | 220 | &PERS ID='Adult', 221 | FYI='Male+Female diameter and velocity', 222 | DEFAULT_PROPERTIES='Adult', 223 | PRE_EVAC_DIST=1, 224 | PRE_LOW = 120.0, 225 | PRE_HIGH = 180.0, 226 | DET_EVAC_DIST=0, 227 | DET_MEAN = 120.0, 228 | TDET_SMOKE_DENS=0.2, 229 | HUMAN_SMOKE_HEIGHT=1.70, 230 | DENS_INIT= 4.0, 231 | NOT_RANDOM=.TRUE., 232 | OUTPUT_SPEED=.TRUE., 233 | OUTPUT_FED=.TRUE., 234 | COLOR_METHOD = 0 / 235 | 236 | &PERS ID='Male', 237 | FYI='Male diameter and velocity', 238 | DEFAULT_PROPERTIES='Male', 239 | PRE_EVAC_DIST=1, 240 | PRE_LOW = 120.0, 241 | PRE_HIGH = 180.0, 242 | DET_EVAC_DIST=0, 243 | DET_MEAN = 120.0, 244 | TDET_SMOKE_DENS=0.2, 245 | HUMAN_SMOKE_HEIGHT=1.80 / 246 | 247 | &PERS ID='Female', 248 | FYI='Female diameter and velocity', 249 | DEFAULT_PROPERTIES='Female', 250 | PRE_EVAC_DIST=1, 251 | PRE_LOW = 120.0, 252 | PRE_HIGH = 180.0, 253 | DET_EVAC_DIST=0, 254 | DET_MEAN = 120.0, 255 | TDET_SMOKE_DENS=0.1, 256 | HUMAN_SMOKE_HEIGHT=1.60 / 257 | 258 | 259 | &PERS ID='Child', 260 | FYI='Child diameter and velocity', 261 | DEFAULT_PROPERTIES='Child', 262 | PRE_EVAC_DIST=1, 263 | PRE_LOW = 120.0, 264 | PRE_HIGH = 180.0, 265 | DET_EVAC_DIST=0, 266 | DET_MEAN = 120.0, 267 | TDET_SMOKE_DENS=0.2, 268 | HUMAN_SMOKE_HEIGHT=1.40 / 269 | 270 | 271 | &PERS ID='Elderly', 272 | FYI='Elderly diameter and velocity', 273 | DEFAULT_PROPERTIES='Elderly', 274 | PRE_EVAC_DIST=1,PRE_LOW=1.0,PRE_HIGH=2.0, 275 | DET_EVAC_DIST=1,DET_MEAN=10.0, / 276 | 277 | EVAC ID = 'HumanLeftDoorKnown', 278 | NUMBER_INITIAL_PERSONS = 25, 279 | XB = 1.0,9.0, 1.0,9.0, 0.4,1.6 280 | AVATAR_COLOR = 'BLUE', 281 | PRE_EVAC_DIST=1, 282 | PRE_LOW = 60.0, 283 | PRE_HIGH =120.0, 284 | DET_EVAC_DIST=1, 285 | DET_LOW = 90.0, 286 | DET_HIGH =180.0, 287 | KNOWN_DOOR_NAMES = 'LeftExit', 288 | KNOWN_DOOR_PROBS = 1.0, 289 | PERS_ID = 'Male' / 290 | 291 | EVAC ID = 'HumanRightDoorKnown', 292 | NUMBER_INITIAL_PERSONS = 25, 293 | XB = 1.0,9.0, 1.0,9.0, 0.4,1.6 294 | KNOWN_DOOR_NAMES = 'RightExit', 295 | KNOWN_DOOR_PROBS = 1.0, 296 | PERS_ID = 'Female' / 297 | 298 | EVAC ID = 'HumanBothDoorsKnown', 299 | NUMBER_INITIAL_PERSONS = 25, 300 | XB = 1.0,9.0, 1.0,9.0, 0.4,1.6 301 | KNOWN_DOOR_NAMES = 'LeftExit','RightExit', 302 | KNOWN_DOOR_PROBS = 1.0,1.0, 303 | PERS_ID = 'Child' / 304 | 305 | EVAC ID = 'HumanNoDoorKnown', 306 | NUMBER_INITIAL_PERSONS = 25, 307 | XB = 1.0,9.0, 1.0,9.0, 0.4,1.6 308 | PERS_ID = 'Adult' / 309 | 310 | &EVAC ID = 'Human2ndFloor', 311 | NUMBER_INITIAL_PERSONS = 50, 312 | XB = 0.5,9.5, 0.5,9.5, 3.0,4.2 313 | KNOWN_DOOR_NAMES = 'LeftDoor2nd','RightDoor2nd','RightExit', 314 | KNOWN_DOOR_PROBS = 0.5,1.0,1.0, 315 | PERS_ID = 'Adult' / 316 | 317 | &EVHO ID = 'Evho_Fire', 318 | FYI = 'Do not put humans close to the fire', 319 | XB = 2.0,5.0, 2.0,5.0, 0.4,1.6 / 320 | &EVHO ID = 'Evho_2ndFloor', 321 | FYI = 'atrium space', 322 | XB = 2.0,8.0, 2.0,8.0, 3.0,4.2 / 323 | 324 | / Fire calculation output. 325 | &SLCF PBX=3.50, QUANTITY='TEMPERATURE' / 326 | &SLCF PBX=3.50, QUANTITY='VELOCITY' / 327 | 328 | Next lines could be used to plot the evacuation flow fields: 329 | &SLCF PBZ = 1.000, QUANTITY = 'VELOCITY', VECTOR = .TRUE., EVACUATION=.TRUE. / 330 | &SLCF PBZ = 3.600, QUANTITY = 'VELOCITY', VECTOR = .TRUE., EVACUATION=.TRUE. / 331 | 332 | &TAIL / 333 | -------------------------------------------------------------------------------- /examples/evac/fds_no_fed/stairs_evss_meshes.fds: -------------------------------------------------------------------------------- 1 | / ------------------------------------------------------------------- 2 | &HEAD CHID = "stairs_evss_meshes", 3 | TITLE = "case" / 4 | 5 | / ------------------------------------------------------------------- 6 | / Mesh - fire 7 | / ------------------------------------------------------------------- 8 | 9 | / ------------------------------------------------------------------- 10 | / Mesh - evac 11 | / ------------------------------------------------------------------- 12 | &MESH IJK = 165,100, 1, 13 | ID = "GroundFloor", 14 | XB = 0.0,16.5,-5.0, 5.0, 0.8, 1.6, 15 | EVACUATION = .TRUE., 16 | EVAC_HUMANS = .TRUE., 17 | EVAC_Z_OFFSET = 1.0 / 18 | 19 | &MESH IJK = 300,100, 1, 20 | ID = "FirstFloor", 21 | XB =16.5,46.5,-5.0, 5.0, 3.8, 4.6, 22 | EVACUATION = .TRUE., 23 | EVAC_HUMANS = .TRUE., 24 | EVAC_Z_OFFSET = 1.0 / 25 | 26 | / ------------------------------------------------------------------- 27 | / miscellaneous 28 | / ------------------------------------------------------------------- 29 | &MISC NO_EVACUATION = .FALSE., 30 | EVACUATION_ALL_STEPS = .TRUE. 31 | EVACUATION_DRILL = .TRUE., 32 | EVACUATION_WRITE_FED = .FALSE., 33 | EVAC_TIME_ITERATIONS = 50, 34 | EVAC_PRESSURE_ITERATIONS = 50 / 35 | 36 | &DUMP DT_PART = 0.5, 37 | DT_HRR = 0.1, 38 | DT_SLCF = 1000000.0, 39 | DT_PL3D = 1000000.0, 40 | DT_ISOF = 1000000.0 / 41 | 42 | / ------------------------------------------------------------------- 43 | / simulation time 44 | / ------------------------------------------------------------------- 45 | &TIME T_BEGIN = 0.0, 46 | T_END = 600.0, 47 | EVAC_DT_FLOWFIELD = 0.01 / 48 | 49 | / ------------------------------------------------------------------- 50 | / Evac - EXIT 51 | / ------------------------------------------------------------------- 52 | &EXIT ID = "EXIT", 53 | IOR = -1, 54 | COLOR = "GREEN", 55 | COUNT_ONLY = .FALSE., 56 | KNOWN_DOOR = .TRUE., 57 | XYZ = 0.2, 0.0, 2.0 58 | XB = 0.0, 0.0, -3.0, 3.0, 0.8, 1.6 / 59 | 60 | / ------------------------------------------------------------------- 61 | / Evac - stair 62 | / ------------------------------------------------------------------- 63 | 64 | &DOOR ID = 'T01', 65 | IOR = -1, 66 | EXIT_SIGN = .TRUE., 67 | COLOR = 'ORANGE', 68 | KNOWN_DOOR = .TRUE., 69 | XYZ =16.7, 0.0, 2.0, 70 | XB =16.5,16.5,-1.2, 1.2, 3.8, 4.6, 71 | TO_NODE = 'E00' / 72 | 73 | &ENTR ID = 'E00', 74 | XB =16.5,16.5,-1.2, 1.2, 0.8, 1.6, 75 | IOR = -1, 76 | COLOR = 'ORANGE' / 77 | 78 | &EVSS ID = 'stair', 79 | IOR = -1, 80 | SHOW = .TRUE., 81 | XB = 8.0,16.5,-1.2, 1.2, 0.8, 1.6, 82 | FAC_V0_UP = 0.5, 83 | FAC_V0_DOWN = 0.5, 84 | FAC_V0_HORI = 1.0, 85 | HEIGHT0 = 0.0, 86 | HEIGHT = 3.0, 87 | MESH_ID = 'GroundFloor' / 88 | 89 | / ------------------------------------------------------------------- 90 | / EVAC - counter 91 | / ------------------------------------------------------------------- 92 | &EXIT ID = 'C1', 93 | IOR = -1, 94 | COUNT_ONLY = .TRUE., 95 | XYZ = 6.2, 0.0, 2.0, 96 | XB = 6.0, 6.0,-0.7, 0.7, 0.8, 1.6 / 97 | 98 | DOOR ID = 'T02', 99 | IOR = -1, 100 | EXIT_SIGN = .TRUE., 101 | COLOR = 'ORANGE', 102 | KNOWN_DOOR = .TRUE., 103 | XYZ = 6.2, 0.0, 2.0, 104 | XB = 6.0, 6.0,-0.7, 0.7, 0.8, 1.6, 105 | TO_NODE = 'E02' / 106 | OBST XB = 5.8, 6.0,-0.7,-0.7, 0.0, 6.0, EVACUATION=.TRUE., COLOR='BRICK' / 107 | ENTR ID = 'E02', 108 | XB = 5.8, 5.8,-0.7, 0.7, 0.8, 1.6, 109 | IOR = -1, 110 | COLOR = 'ORANGE' / 111 | 112 | &EXIT ID = 'C2', 113 | IOR = -1, 114 | COUNT_ONLY = .TRUE., 115 | XYZ = 8.2, 0.0, 2.0, 116 | XB = 8.0, 8.0,-1.2, 1.2, 0.8, 1.6 / 117 | 118 | &EXIT ID = 'C3', 119 | IOR = -1, 120 | COUNT_ONLY = .TRUE., 121 | XYZ =16.7, 0.0, 2.0, 122 | XB =16.5,16.5,-1.2, 1.2, 3.8, 4.6 / 123 | 124 | &EXIT ID = 'C4', 125 | IOR = -1, 126 | COUNT_ONLY = .TRUE., 127 | XYZ =36.7, 0.0, 2.0, 128 | XB =36.5,36.5,-1.2, 1.2, 3.8, 4.6 / 129 | 130 | / ------------------------------------------------------------------- 131 | / EVAC - PERS 132 | / ------------------------------------------------------------------- 133 | &PERS ID = "ADULTS", 134 | DEFAULT_PROPERTIES = "Adult", 135 | PRE_EVAC_DIST = 0, 136 | PRE_MEAN = 0.0, 137 | DET_EVAC_DIST = 0, 138 | DET_MEAN = 0.0, 139 | VELOCITY_DIST = 0, 140 | VEL_MEAN = 1.00, 141 | DENS_INIT = 6.0, 142 | OUTPUT_FED = .FALSE., 143 | OUTPUT_SPEED = .TRUE., 144 | OUTPUT_CONTACT_FORCE = .FALSE., 145 | OUTPUT_TOTAL_FORCE = .FALSE., 146 | OUTPUT_DENSITY = .TRUE., 147 | OUTPUT_NERVOUSNESS = .FALSE., 148 | OUTPUT_ACCELERATION = .FALSE., 149 | COLOR_METHOD = 0 / 150 | 151 | &PERS ID = 'Students', 152 | DEFAULT_PROPERTIES = 'Adult', 153 | I_HERDING_TYPE = 2, 154 | VELOCITY_DIST = 2, 155 | VEL_LOW = 0.8, 156 | VEL_MEAN = 1.2, 157 | VEL_HIGH = 1.6, 158 | VEL_PARA = 0.20, 159 | DET_EVAC_DIST = 0, 160 | DET_MEAN = 0.00, 161 | PRE_EVAC_DIST = 0, 162 | PRE_MEAN = 0.00, 163 | SMOKE_MIN_SPEED = 0.10, 164 | DENS_INIT = 6.00, 165 | HUMAN_SMOKE_HEIGHT = 1.80, 166 | TDET_SMOKE_DENS = 0.00, 167 | COLOR_METHOD = 4 / 168 | 169 | &PERS ID = 'School children', 170 | DEFAULT_PROPERTIES = 'Child', 171 | I_HERDING_TYPE = 2, 172 | VELOCITY_DIST = 2, 173 | VEL_LOW = 0.6, 174 | VEL_MEAN = 0.9, 175 | VEL_HIGH = 1.3, 176 | VEL_PARA = 0.30, 177 | DET_EVAC_DIST = 0, 178 | DET_MEAN = 0.00, 179 | PRE_EVAC_DIST = 0, 180 | PRE_MEAN = 0.00, 181 | SMOKE_MIN_SPEED = 0.10, 182 | DENS_INIT = 6.00, 183 | HUMAN_SMOKE_HEIGHT = 1.80, 184 | TDET_SMOKE_DENS = 0.00, 185 | COLOR_METHOD = 4 / 186 | 187 | 188 | / ------------------------------------------------------------------- 189 | / EVAC - EVAC 190 | / ------------------------------------------------------------------- 191 | &EVAC ID = "Agents", 192 | NUMBER_INITIAL_PERSONS = 100, 193 | XB = 36.5, 46.5,-5.0, 5.0, 3.8, 4.6, 194 | AVATAR_COLOR = "BLUE", 195 | PERS_ID = "ADULTS" / 196 | 197 | &EVAC ID = "Students", 198 | NUMBER_INITIAL_PERSONS = 100, 199 | XB = 36.5, 46.5,-5.0, 5.0, 3.8, 4.6, 200 | AVATAR_COLOR = "BLUE", 201 | PERS_ID = "Students" / 202 | 203 | &EVAC ID = "Children", 204 | NUMBER_INITIAL_PERSONS = 100, 205 | XB = 36.5, 46.5,-5.0, 5.0, 3.8, 4.6, 206 | AVATAR_COLOR = "BLUE", 207 | PERS_ID = "School children" / 208 | 209 | / ------------------------------------------------------------------- 210 | / geometry 211 | / ------------------------------------------------------------------- 212 | 213 | &OBST XB= 0.0, 8.0, 3.0, 5.0, 0.0, 6.0, EVACUATION=.TRUE., COLOR='GRAY' / 214 | &OBST XB= 0.0, 8.0, -5.0,-3.0, 0.0, 6.0, EVACUATION=.TRUE., COLOR='GRAY' / 215 | 216 | &OBST XB= 8.0,16.5, -5.0,-1.2, 0.0, 6.0, EVACUATION=.TRUE., COLOR='GRAY' / 217 | &OBST XB= 8.0,16.5, 1.2, 5.0, 0.0, 6.0, EVACUATION=.TRUE., COLOR='GRAY' / 218 | 219 | &OBST XB=16.5,36.5, -5.0,-3.0, 0.0, 6.0, EVACUATION=.TRUE., COLOR='GRAY' / 220 | &OBST XB=16.5,36.5, 3.0, 5.0, 0.0, 6.0, EVACUATION=.TRUE., COLOR='GRAY' / 221 | 222 | &OBST XB= 5.8, 6.0, -3.0,-0.7, 0.0, 6.0, EVACUATION=.TRUE., COLOR='BRICK' / 223 | &OBST XB= 5.8, 6.0, 0.7, 3.0, 0.0, 6.0, EVACUATION=.TRUE., COLOR='BRICK' / 224 | 225 | / ------------------------------------------------------------------- 226 | / quantities 227 | / ------------------------------------------------------------------- 228 | &SLCF PBZ = 1.0, 229 | QUANTITY = "VELOCITY", 230 | VECTOR = .TRUE., 231 | EVACUATION = .TRUE. / 232 | 233 | &SLCF PBZ = 4.0, 234 | QUANTITY = "VELOCITY", 235 | VECTOR = .TRUE., 236 | EVACUATION = .TRUE. / 237 | 238 | &TAIL / 239 | -------------------------------------------------------------------------------- /examples/export/export_all.py: -------------------------------------------------------------------------------- 1 | import os 2 | import fdsreader as fds 3 | import fdsreader.export 4 | 5 | 6 | def main(): 7 | sim_path = "." 8 | sim = fds.Simulation(os.path.join(sim_path, "fds_data")) 9 | 10 | print(fds.export.export_sim(sim, os.path.join(sim_path, "fds_post"), ordering='F')) 11 | 12 | 13 | if __name__ == "__main__": 14 | main() 15 | -------------------------------------------------------------------------------- /examples/export/obst_export.py: -------------------------------------------------------------------------------- 1 | import os 2 | import fdsreader as fds 3 | import fdsreader.export 4 | 5 | 6 | def main(): 7 | base_path = "." 8 | case = "Apartment" 9 | sim = fds.Simulation(os.path.join(base_path, "fds_data", case)) 10 | 11 | for obst in sim.obstructions: 12 | print(obst.id) 13 | fds.export.export_obst_raw(obst, os.path.join(base_path, "fds_post", case, "obst"), 'F') 14 | 15 | 16 | if __name__ == "__main__": 17 | main() 18 | -------------------------------------------------------------------------------- /examples/export/slcf_export.py: -------------------------------------------------------------------------------- 1 | import os 2 | import fdsreader as fds 3 | import fdsreader.export 4 | 5 | 6 | def main(): 7 | base_path = "." 8 | case = "Apartment" 9 | sim = fds.Simulation(os.path.join(base_path, "fds_data", case)) 10 | 11 | for slc in sim.slices: 12 | fds.export.export_slcf_raw(slc, os.path.join(base_path, "fds_post", case, "slices", slc.quantity.name.replace(' ', '_').lower()), 'F') 13 | 14 | 15 | if __name__ == "__main__": 16 | main() 17 | -------------------------------------------------------------------------------- /examples/export/smoke3d_export.py: -------------------------------------------------------------------------------- 1 | import os 2 | import fdsreader as fds 3 | import fdsreader.export 4 | 5 | 6 | def main(): 7 | base_path = "." 8 | case = "Apartment" 9 | sim = fds.Simulation(os.path.join(base_path, "fds_data", case)) 10 | 11 | for smoke in sim.smoke_3d: 12 | print(smoke.quantity) 13 | fds.export.export_smoke_raw(smoke, os.path.join(base_path, "fds_post", case, "smoke", smoke.quantity.name.replace(' ', '_').lower()), 'F') 14 | 15 | 16 | if __name__ == "__main__": 17 | main() 18 | -------------------------------------------------------------------------------- /examples/geom/geom_example.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pyvista as pv 4 | from fdsreader import Simulation 5 | import numpy as np 6 | 7 | 8 | def main(): 9 | sim = Simulation("./fds_data") 10 | 11 | geom = sim.geom_data.filter_by_quantity("Radiative Heat Flux")[0] 12 | print(len(geom.data[-1])) 13 | exit() 14 | 15 | faces = np.hstack(np.append(np.full((geom.faces.shape[0], 1), 3), geom.faces, axis=1)) 16 | pv.PolyData(geom.vertices, faces).plot(scalars=geom.data[0]) 17 | 18 | 19 | if __name__ == "__main__": 20 | main() 21 | -------------------------------------------------------------------------------- /examples/geom/geom_slices.py: -------------------------------------------------------------------------------- 1 | from fdsreader import Simulation 2 | 3 | 4 | def main(): 5 | sim = Simulation("fds_geomslices") 6 | 7 | for slc in sim.geomslices: 8 | print(slc.vertices, slc.faces, slc.data) 9 | 10 | 11 | if __name__ == "__main__": 12 | main() 13 | -------------------------------------------------------------------------------- /examples/isof/fds_steckler/input_steckler.fds: -------------------------------------------------------------------------------- 1 | &HEAD CHID='Steckler_010', TITLE='Steckler Compartment, Test 10'/ 2 | 3 | &MESH IJK=36,28,22, XB=0.00,3.60,-1.40,1.40,0.00,2.13 / 4 | 5 | &TIME T_END=100.0, TIME_SHRINK_FACTOR=10. / 6 | &DUMP NFRAMES=100, DT_DEVC=10., DT_HRR=10., SIG_FIGS=4, SIG_FIGS_EXP=2 / 7 | 8 | &MISC TMPA=22. / 9 | 10 | &SURF ID='BURNER',HRRPUA=1048.3,TMP_FRONT=100.,COLOR='ORANGE' / 62.9 kW 11 | 12 | &HOLE XB=2.75,2.95,-0.10,0.15,0.00,1.83 / 2/6 Door 13 | 14 | &VENT XB=1.30,1.50,-0.10,0.10,0.00,0.00, SURF_ID='BURNER' / Position A 15 | &VENT XB=1.25,1.30,-0.05,0.05,0.00,0.00, SURF_ID='BURNER' / 16 | &VENT XB=1.50,1.55,-0.05,0.05,0.00,0.00, SURF_ID='BURNER' / 17 | &VENT XB=1.35,1.45,-0.15,-.10,0.00,0.00, SURF_ID='BURNER' / 18 | &VENT XB=1.35,1.45, 0.10,0.15,0.00,0.00, SURF_ID='BURNER' / 19 | 20 | &SURF ID='FIBER BOARD', DEFAULT=.TRUE., MATL_ID='INSULATION', THICKNESS=0.013, COLOR='BEIGE' / 21 | 22 | &REAC FUEL='METHANE',SOOT_YIELD=0. / 23 | 24 | &OBST XB=2.80,2.90,-1.40,1.40,0.00,2.13 / Wall with door or window 25 | 26 | &VENT MB='XMAX',SURF_ID='OPEN'/ 27 | &VENT XB= 2.90,3.60,-1.40,-1.40,0.00,2.13, SURF_ID='OPEN'/ 28 | &VENT XB= 2.90,3.60, 1.40, 1.40,0.00,2.13, SURF_ID='OPEN'/ 29 | &VENT XB= 2.90,3.60,-1.40, 1.40,2.13,2.13, SURF_ID='OPEN'/ 30 | 31 | &MATL ID = 'INSULATION' 32 | DENSITY = 200. 33 | CONDUCTIVITY = 0.1 34 | SPECIFIC_HEAT = 1. / 35 | 36 | 37 | &ISOF QUANTITY='TEMPERATURE', VALUE(1)=10., VALUE(2)=30., VALUE(3)=60. / 38 | &ISOF QUANTITY='U-VELOCITY' , VALUE(1)=0.08, QUANTITY2='TEMPERATURE' / 39 | 40 | &TAIL / 41 | -------------------------------------------------------------------------------- /examples/isof/isof_example.py: -------------------------------------------------------------------------------- 1 | import pyvista as pv 2 | from fdsreader import Simulation 3 | 4 | 5 | def main(): 6 | sim = Simulation("./fds_steckler") 7 | 8 | isosurface = sim.isosurfaces.filter_by_quantity("TEMP")[0] 9 | 10 | vertices, triangles, _ = isosurface.to_global(len(isosurface.times) - 1) 11 | 12 | print(triangles[-1][-1]) 13 | exit() 14 | # We ignore level 1 as it does not contain any vertices 15 | # level1 = isosurface.get_pyvista_mesh(vertices, triangles[0]) 16 | level2 = isosurface.get_pyvista_mesh(vertices, triangles[1]) 17 | level3 = isosurface.get_pyvista_mesh(vertices, triangles[2]) 18 | 19 | # Either plot both meshes directly... 20 | # isosurface.join_pyvista_meshes([level2, level3]).plot() 21 | 22 | # ...or plot them separately to adjust properties such as the color 23 | plotter = pv.Plotter() 24 | plotter.add_mesh(level2, color=[0, 0, 255, 255], opacity=0.95) 25 | plotter.add_mesh(level3, color=[255, 0, 0, 255], opacity=0.95) 26 | plotter.show() 27 | 28 | 29 | if __name__ == "__main__": 30 | main() 31 | -------------------------------------------------------------------------------- /examples/part/part_example.py: -------------------------------------------------------------------------------- 1 | import pyvista as pv 2 | import fdsreader as fds 3 | 4 | 5 | def main(): 6 | sim = fds.Simulation("./fds_data") 7 | 8 | # Get all particles with specified id 9 | particles = sim.particles["WATER PARTICLES"] 10 | 11 | # Get all data for the specified quantity 12 | # quantity = "PARTICLE VELOCITY" 13 | quantity = "PARTICLE DIAMETER" 14 | color_data = particles.data[quantity] 15 | t_0 = next(i for i, pos in enumerate(particles.positions) if pos.shape[0] != 0) 16 | 17 | # Create PyVista animation 18 | plotter = pv.Plotter(notebook=False, off_screen=True) 19 | plotter.open_movie("anim.mp4") 20 | 21 | actor = plotter.add_mesh(pv.PolyData(particles.positions[t_0]), scalars=color_data[t_0], 22 | render_points_as_spheres=True, point_size=15) 23 | plotter.add_scalar_bar(title=quantity) 24 | 25 | # Open a gif 26 | plotter.open_gif("particles.gif") 27 | 28 | for t in range(t_0 + 1, len(color_data)): 29 | plotter.remove_actor(actor) 30 | actor = plotter.add_mesh(pv.PolyData(particles.positions[t]), scalars=color_data[t], 31 | render_points_as_spheres=True, point_size=15) 32 | plotter.write_frame() 33 | 34 | # Closes and finalizes movie 35 | plotter.close() 36 | 37 | 38 | if __name__ == "__main__": 39 | main() 40 | -------------------------------------------------------------------------------- /examples/pl3d/fds_data/vort3d_80.fds: -------------------------------------------------------------------------------- 1 | &HEAD CHID='vort3d_80', TITLE='2D Vortex Simulation, Mesh Size = 80' / 2 | 3 | &TIME T_END=50.0/ 4 | 5 | &MESH ID='Mesh1', IJK=24,36,12, XB=-1.0, 5.0,-1.0,8.0,0.0,3.0/ 6 | &MESH ID='Mesh2', IJK=24,36,12, XB= 5.0,11.0,-1.0,8.0,0.0,3.0/ 7 | 8 | 9 | &REAC ID='POLYURETHANE', 10 | FYI='NFPA Babrauskas', 11 | FUEL='REAC_FUEL', 12 | C=6.3, 13 | H=7.1, 14 | O=2.1, 15 | N=1.0, 16 | SOOT_YIELD=0.1/ 17 | 18 | &SURF ID='FIRE', 19 | COLOR='RED', 20 | HRRPUA=50.0/ 21 | 22 | &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 23 | &OBST XB=-0.5,0.0,7.0,7.5,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 13D9 24 | &OBST XB=-0.5,10.5,7.5,8.0,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 13D9 25 | &OBST XB=10.0,10.5,7.0,7.5,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 13D9 26 | &OBST XB=-0.5,0.0,0.0,5.0,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 16F9 27 | &OBST XB=-0.5,0.0,5.5,6.0,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 16F9 28 | &OBST XB=-0.5,1.5,5.0,5.5,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 16F9 29 | &OBST XB=-0.5,10.5,-0.5,0.0,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 16F9 30 | &OBST XB=8.5,10.5,5.0,5.5,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 16F9 31 | &OBST XB=10.0,10.5,0.0,5.0,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 16F9 32 | &OBST XB=10.0,10.5,5.5,6.0,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 16F9 33 | &OBST XB=2.5,7.5,5.0,5.5,0.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 16FE 34 | &OBST XB=1.5,2.5,5.0,5.5,2.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 1445 35 | &OBST XB=7.5,8.5,5.0,5.5,2.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 1452 36 | &OBST XB=-0.5,0.0,6.0,7.0,2.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 1456 37 | &OBST XB=10.0,10.5,6.0,7.0,2.0,2.5, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 1463 38 | &OBST XB=-0.5,10.5,-0.5,8.0,2.5,2.75, RGB=91,91,91, SURF_ID='INERT'/ AcDb3dSolid - 1469 39 | 40 | &VENT SURF_ID='FIRE', XB=9.5,10.0,6.0,6.5,0.0,0.0/ Vent 41 | &VENT SURF_ID='OPEN', XB=11.0,11.0,-1.0,8.0,0.0,3.0/ Mesh Vent: Mesh1 [XMAX] 42 | &VENT SURF_ID='OPEN', XB=-1.0,-1.0,-1.0,8.0,0.0,3.0/ Mesh Vent: Mesh1 [XMIN] 43 | &VENT SURF_ID='OPEN', XB=-1.0,11.0,8.0,8.0,0.0,3.0/ Mesh Vent: Mesh1 [YMAX] 44 | &VENT SURF_ID='OPEN', XB=-1.0,11.0,-1.0,-1.0,0.0,3.0/ Mesh Vent: Mesh1 [YMIN] 45 | &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] 46 | 47 | &DUMP DT_PL3D=10, PLOT3D_QUANTITY(1:5)='TEMPERATURE', 'U-VELOCITY','V-VELOCITY','W-VELOCITY','HRRPUV' / 48 | 49 | &TAIL / -------------------------------------------------------------------------------- /examples/pl3d/pl3d_example.py: -------------------------------------------------------------------------------- 1 | import pyvista as pv 2 | import numpy as np 3 | import fdsreader as fds 4 | 5 | 6 | def main(): 7 | sim = fds.Simulation("./fds_data") 8 | 9 | # Load temperature 3D data 10 | quantity = "Temperature" 11 | pl_t1 = sim.data_3d.get_by_quantity(quantity) 12 | data, coordinates = pl_t1.to_global(masked=True, return_coordinates=True, fill=np.nan) 13 | 14 | # Select the last available timestep 15 | t = -1 16 | 17 | # Create 3D grid 18 | x_ = np.linspace(coordinates['x'][0], coordinates['x'][-1], len(coordinates['x'])) 19 | y_ = np.linspace(coordinates['y'][0], coordinates['y'][-1], len(coordinates['y'])) # y_ = np.array([29]) # when using a slice 20 | z_ = np.linspace(coordinates['z'][0], coordinates['z'][-1], len(coordinates['z'])) 21 | x, y, z = np.meshgrid(x_, y_, z_, indexing='ij') 22 | points = np.stack((x.flatten(), y.flatten(), z.flatten()), axis=1) 23 | 24 | color_data = data[t, :, :, :] 25 | # It is also possible to just plot a slice 26 | # color_data = pl_t1[mesh].data[t, :, 29:30, :] 27 | 28 | # Plot 3D data 29 | plotter = pv.Plotter() 30 | plotter.add_mesh(pv.PolyData(points), scalars=color_data.flatten(), 31 | opacity=0.3, render_points_as_spheres=False, point_size=25) 32 | plotter.add_scalar_bar(title=quantity) 33 | plotter.show() 34 | 35 | 36 | if __name__ == "__main__": 37 | main() 38 | -------------------------------------------------------------------------------- /examples/slcf/fds_data/vort2d_80.fds: -------------------------------------------------------------------------------- 1 | &HEAD CHID='vort2d_80', TITLE='2D Vortex Simulation, Mesh Size = 80' / 2 | 3 | &MESH ID='mesh1', IJK=40,5,80, XB=-0.155600,0.155600,-0.001945,0.001945,-0.155600,0.155600 / 4 | 5 | &TIME T_END=0.01, DT=5.5571428e-5, LOCK_TIME_STEP=.TRUE. / 6 | 7 | &DUMP DT_DEVC=1.11142850e-4 / 8 | 9 | &MISC DNS=.TRUE., 10 | STRATIFICATION=.FALSE., 11 | NOISE=.FALSE., 12 | CFL_MAX=0.5, 13 | GVEC(3)=0.0, 14 | PERIODIC_TEST=6 / 15 | 16 | &SPEC ID='AIR', VISCOSITY=0., BACKGROUND=.TRUE./ 17 | 18 | &RADI RADIATION=.FALSE./ 19 | 20 | &SLCF ID='Slice1' PBY=0.00, QUANTITY='PRESSURE' , CELL_CENTERED=.TRUE. / 21 | 22 | &VENT PBX=-0.155600, SURF_ID='PERIODIC' / 23 | &VENT PBX= 0.155600, SURF_ID='PERIODIC' / 24 | &VENT PBZ=-0.155600, SURF_ID='PERIODIC' / 25 | &VENT PBZ= 0.155600, SURF_ID='PERIODIC' / 26 | 27 | &OBST XB=0,0.1,0,0.001,0,0.1 28 | 29 | &TAIL / 30 | 31 | -------------------------------------------------------------------------------- /examples/slcf/fds_multimesh/multimesh.fds: -------------------------------------------------------------------------------- 1 | / ------------------------------------------------------------------- 2 | &HEAD CHID ='fdsreader_slcfs', 3 | TITLE ='' / 4 | / ------------------------------------------------------------------- 5 | / Mesh 6 | / ------------------------------------------------------------------- 7 | / Mesh size - dx = 0.1 8 | &MESH ID ='MESH_1', 9 | IJK = 10, 10, 10, 10 | XB = 0.0, 1.0, 0.0, 1.0, 0.0, 1.0 / 11 | &MESH ID ='MESH_2', 12 | IJK = 10, 10, 10, 13 | XB = 0.5, 1.5, 1.0, 2.0, 0.0, 1.0 / 14 | &MESH ID ='MESH_3', 15 | IJK = 20, 10, 10, 16 | XB = 1.0, 3.0, 0.0, 1.0, 0.0, 1.0 / 17 | &MESH ID ='MESH_4', 18 | IJK = 20, 10, 10, 19 | XB = 0.0, 2.0,-1.0, 0.0, 0.0, 1.0 / 20 | 21 | / ------------------------------------------------------------------- 22 | / simulation time 23 | / ------------------------------------------------------------------- 24 | &TIME T_BEGIN = 0.0, 25 | T_END = 120.0 / 26 | / ------------------------------------------------------------------- 27 | / boundary conditions 28 | / ------------------------------------------------------------------- 29 | &VENT MB ='XMIN', SURF_ID='OPEN' / 30 | &VENT MB ='XMAX', SURF_ID='OPEN' / 31 | &VENT MB ='YMIN', SURF_ID='OPEN' / 32 | &VENT MB ='YMAX', SURF_ID='OPEN' / 33 | VENT MB ='ZMIN', SURF_ID='OPEN' / 34 | &VENT MB ='ZMAX', SURF_ID='OPEN' / 35 | 36 | &DUMP DT_HRR = 1.0, 37 | DT_SLCF = 10.0, 38 | DT_DEVC = 10.0, 39 | DT_CTRL = 10.0, 40 | DT_PART = 10.0, 41 | DT_BNDF = 30.0, 42 | DT_SMOKE3D = 1.0, 43 | VELOCITY_ERROR_FILE = .FALSE., 44 | MASS_FILE = .FALSE. / 45 | 46 | / ------------------------------------------------------------------- 47 | / fire - reaction 48 | / ------------------------------------------------------------------- 49 | &REAC ID = 'METHANE' / 50 | 51 | / fire - area 52 | / ------------------------------------------------------------------- 53 | &SURF ID = 'fire', 54 | COLOR = 'RED', 55 | HRRPUA = 300.0, 56 | RAMP_Q = 'fire-ramp' / 57 | 58 | &RAMP ID='fire-ramp', T= 0.00, F=0.00 / 59 | &RAMP ID='fire-ramp', T= 30.00, F=1.00 / 60 | 61 | &OBST XB = 0.6, 0.7, 0.6, 0.7, 0.0, 0.0, SURF_ID='fire' / 62 | 63 | / ------------------------------------------------------------------- 64 | / quantities 65 | / ------------------------------------------------------------------- 66 | / Animated Planar Slices - SLCF 67 | / ----------------------------- 68 | &SLCF PBZ= 0.5, QUANTITY='TEMPERATURE' / 69 | &SLCF PBZ= 0.5, QUANTITY='TEMPERATURE' , CELL_CENTERED=.TRUE./ 70 | 71 | &TAIL / 72 | -------------------------------------------------------------------------------- /examples/slcf/fds_steckler/input_steckler.fds: -------------------------------------------------------------------------------- 1 | &HEAD CHID='Steckler', TITLE='Steckler Compartment'/ 2 | 3 | &MESH IJK=36,28,22, XB=0.00,3.60,-1.40,1.40,0.00,2.13 / 4 | 5 | &TIME T_END=100.0, TIME_SHRINK_FACTOR=10. / 6 | &DUMP NFRAMES=100, DT_DEVC=10., DT_HRR=10., SIG_FIGS=4, SIG_FIGS_EXP=2 / 7 | 8 | &MISC TMPA=22. / 9 | 10 | &SURF ID='BURNER',HRRPUA=1048.3,TMP_FRONT=100.,COLOR='ORANGE' / 62.9 kW 11 | 12 | &HOLE XB=2.75,2.95,-0.10,0.15,0.00,1.83 / 2/6 Door 13 | 14 | &VENT XB=1.30,1.50,-0.10,0.10,0.00,0.00, SURF_ID='BURNER' / Position A 15 | &VENT XB=1.25,1.30,-0.05,0.05,0.00,0.00, SURF_ID='BURNER' / 16 | &VENT XB=1.50,1.55,-0.05,0.05,0.00,0.00, SURF_ID='BURNER' / 17 | &VENT XB=1.35,1.45,-0.15,-.10,0.00,0.00, SURF_ID='BURNER' / 18 | &VENT XB=1.35,1.45, 0.10,0.15,0.00,0.00, SURF_ID='BURNER' / 19 | 20 | &SURF ID='FIBER BOARD', DEFAULT=.TRUE., MATL_ID='INSULATION', THICKNESS=0.013, COLOR='BEIGE' / 21 | 22 | &REAC FUEL='METHANE',SOOT_YIELD=0. / 23 | 24 | &OBST XB=2.80,2.90,-1.40,1.40,0.00,2.13 / Wall with door or window 25 | 26 | &VENT MB='XMAX',SURF_ID='OPEN'/ 27 | &VENT XB= 2.90,3.60,-1.40,-1.40,0.00,2.13, SURF_ID='OPEN'/ 28 | &VENT XB= 2.90,3.60, 1.40, 1.40,0.00,2.13, SURF_ID='OPEN'/ 29 | &VENT XB= 2.90,3.60,-1.40, 1.40,2.13,2.13, SURF_ID='OPEN'/ 30 | 31 | &MATL ID = 'INSULATION' 32 | DENSITY = 200. 33 | CONDUCTIVITY = 0.1 34 | SPECIFIC_HEAT = 1. / 35 | 36 | &SLCF PBY=0.0,QUANTITY='TEMPERATURE',VECTOR=.TRUE./ 37 | 38 | &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' / 39 | &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' / 40 | &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. / 41 | 42 | &DEVC ID='HGL Temp', XB=2.50,2.50,1.10,1.10,0.00,2.13, QUANTITY='UPPER TEMPERATURE' / 43 | &DEVC ID='HGL Height', XB=2.50,2.50,1.10,1.10,0.00,2.13, QUANTITY='LAYER HEIGHT' / 44 | 45 | &TAIL / 46 | -------------------------------------------------------------------------------- /examples/slcf/slcf_example_global.py: -------------------------------------------------------------------------------- 1 | from matplotlib import cm, pyplot as plt 2 | import numpy as np 3 | 4 | import fdsreader as fds 5 | 6 | 7 | def main(): 8 | sim = fds.Simulation("./fds_multimesh") 9 | 10 | # Get the first slice 11 | slc = sim.slices[1] 12 | data, coordinates = slc.to_global(masked=True, fill=np.nan, return_coordinates=True) 13 | 14 | # Set colormap 15 | cmap = cm.get_cmap('coolwarm').copy() 16 | # Set obsts color 17 | cmap.set_bad('white') 18 | 19 | # Plot the slice 20 | plt.imshow(data[-1].T, vmin=0, vmax=slc.vmax, origin="lower", cmap=cmap) 21 | plt.colorbar() 22 | plt.show() 23 | 24 | 25 | if __name__ == "__main__": 26 | main() 27 | -------------------------------------------------------------------------------- /examples/slcf/slcf_example_mask.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | 4 | import fdsreader as fds 5 | 6 | 7 | def main(): 8 | fds.settings.DEBUG = True 9 | sim = fds.Simulation("./fds_data") 10 | 11 | # Get the first mesh defined in fds file 12 | mesh = sim.meshes[0] 13 | # Get the Slice with name (id) "Slice1" 14 | slc = sim.slices.get_by_id("Slice1") 15 | 16 | # Get subslice that cuts through our mesh 17 | subslice = slc[mesh] 18 | 19 | # Timestep 20 | t = -1 21 | # Fill value for mask 22 | fill = 0 23 | # Mask the data 24 | mask = mesh.get_obstruction_mask_slice(subslice) 25 | sslc_data = np.where(np.squeeze(mask)[t], subslice.data[t], fill) 26 | 27 | # Plot the slice 28 | plt.imshow(sslc_data.T, origin="lower") 29 | plt.colorbar() 30 | plt.show() 31 | 32 | 33 | if __name__ == "__main__": 34 | main() 35 | -------------------------------------------------------------------------------- /examples/smoke3d/fds_steckler/input_steckler.fds: -------------------------------------------------------------------------------- 1 | &HEAD CHID='Steckler', TITLE='Steckler Compartment'/ 2 | 3 | &MESH IJK=36,28,22, XB=0.00,3.60,-1.40,1.40,0.00,2.13 / 4 | 5 | &TIME T_END=100.0, TIME_SHRINK_FACTOR=10. / 6 | &DUMP NFRAMES=100 / 7 | 8 | &MISC TMPA=22. / 9 | 10 | &SURF ID='BURNER',HRRPUA=1048.3,TMP_FRONT=100.,COLOR='ORANGE' / 62.9 kW 11 | 12 | &HOLE XB=2.75,2.95,-0.10,0.15,0.00,1.83 / 2/6 Door 13 | 14 | &VENT XB=1.30,1.50,-0.10,0.10,0.00,0.00, SURF_ID='BURNER' / Position A 15 | &VENT XB=1.25,1.30,-0.05,0.05,0.00,0.00, SURF_ID='BURNER' / 16 | &VENT XB=1.50,1.55,-0.05,0.05,0.00,0.00, SURF_ID='BURNER' / 17 | &VENT XB=1.35,1.45,-0.15,-.10,0.00,0.00, SURF_ID='BURNER' / 18 | &VENT XB=1.35,1.45, 0.10,0.15,0.00,0.00, SURF_ID='BURNER' / 19 | 20 | &SURF ID='FIBER BOARD', DEFAULT=.TRUE., MATL_ID='INSULATION', THICKNESS=0.013, COLOR='BEIGE' / 21 | 22 | &REAC FUEL='METHANE',SOOT_YIELD=0. / 23 | 24 | &OBST XB=2.80,2.90,-1.40,1.40,0.00,2.13 / Wall with door or window 25 | 26 | &VENT MB='XMAX',SURF_ID='OPEN'/ 27 | &VENT XB= 2.90,3.60,-1.40,-1.40,0.00,2.13, SURF_ID='OPEN'/ 28 | &VENT XB= 2.90,3.60, 1.40, 1.40,0.00,2.13, SURF_ID='OPEN'/ 29 | &VENT XB= 2.90,3.60,-1.40, 1.40,2.13,2.13, SURF_ID='OPEN'/ 30 | 31 | &MATL ID = 'INSULATION' 32 | DENSITY = 200. 33 | CONDUCTIVITY = 0.1 34 | SPECIFIC_HEAT = 1. / 35 | 36 | &TAIL / 37 | -------------------------------------------------------------------------------- /examples/smoke3d/smoke3d_example.py: -------------------------------------------------------------------------------- 1 | import pyvista as pv 2 | import numpy as np 3 | 4 | import fdsreader as fds 5 | 6 | 7 | def main(): 8 | sim = fds.Simulation("./fds_steckler/") 9 | 10 | # Get 3D smoke data for a specific quantity in one of the meshes 11 | quantity = "Temperature" # "SOOT MASS FRACTION" 12 | smoke = sim.smoke_3d.get_by_quantity(quantity) 13 | 14 | data, coordinates = smoke.to_global(masked=True, return_coordinates=True, fill=np.nan) 15 | 16 | # Create 3D grid 17 | x_ = np.linspace(coordinates['x'][0], coordinates['x'][-1], len(coordinates['x'])) 18 | y_ = np.linspace(coordinates['y'][0], coordinates['y'][-1], len(coordinates['y'])) 19 | z_ = np.linspace(coordinates['z'][0], coordinates['z'][-1], len(coordinates['z'])) 20 | x, y, z = np.meshgrid(x_, y_, z_, indexing='ij') 21 | points = np.stack((x.flatten(), y.flatten(), z.flatten()), axis=1) 22 | 23 | # Initialize data grid 24 | grid = pv.PolyData(points) 25 | 26 | # Plot 3D data 27 | plotter = pv.Plotter(notebook=False, off_screen=True) 28 | plotter.add_mesh(grid, scalars=data[0].flatten(), opacity=0.3, render_points_as_spheres=False, point_size=25) 29 | plotter.add_scalar_bar(title=quantity) 30 | 31 | # Open a gif 32 | plotter.open_gif("smoke.gif") 33 | 34 | for d in data: 35 | plotter.update_scalars(d.flatten(), render=False) 36 | plotter.write_frame() 37 | 38 | # Closes and finalizes movie 39 | plotter.close() 40 | 41 | 42 | if __name__ == "__main__": 43 | main() 44 | -------------------------------------------------------------------------------- /fdsreader/__init__.py: -------------------------------------------------------------------------------- 1 | from . import _version 2 | __version__ = str(_version.__version__.public()) 3 | 4 | from .simulation import Simulation 5 | 6 | from . import settings 7 | -------------------------------------------------------------------------------- /fdsreader/_version.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides fdsreader version information. 3 | """ 4 | 5 | # This file is auto-generated! Do not edit! 6 | # Use `python -m incremental.update fdsreader` to change this file. 7 | 8 | from incremental import Version 9 | 10 | __version__ = Version("fdsreader", 1, 11, 6) 11 | __all__ = ["__version__"] 12 | -------------------------------------------------------------------------------- /fdsreader/bndf/__init__.py: -------------------------------------------------------------------------------- 1 | from .obstruction import Obstruction, SubObstruction, Patch, Boundary 2 | 3 | from .obstruction_collection import ObstructionCollection 4 | -------------------------------------------------------------------------------- /fdsreader/bndf/obstruction_collection.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Tuple, List 2 | import numpy as np 3 | 4 | from fdsreader.bndf import Obstruction 5 | from fdsreader.utils.data import FDSDataCollection, Quantity 6 | 7 | 8 | class ObstructionCollection(FDSDataCollection): 9 | """Collection of :class:`Obstruction` objects. Offers extensive functionality for filtering and 10 | using obstructions as well as dependent such as :class:`Boundary`. 11 | """ 12 | 13 | def __init__(self, *obstructions: Iterable[Obstruction]): 14 | super().__init__(*obstructions) 15 | 16 | @property 17 | def quantities(self) -> List[Quantity]: 18 | qs = set() 19 | for obst in self._elements: 20 | for q in obst.quantities: 21 | qs.add(q) 22 | return list(qs) 23 | 24 | def filter_by_boundary_data(self): 25 | """Filters all obstructions for which output data exists. 26 | """ 27 | return ObstructionCollection(x for x in self._elements if x.has_boundary_data) 28 | 29 | def get_by_id(self, obst_id: str): 30 | """Get the obstruction with corresponding id if it exists. 31 | """ 32 | return next((obst for obst in self._elements if obst.id == obst_id), None) 33 | 34 | def get_nearest(self, x: float = None, y: float = None, z: float = None) -> Obstruction: 35 | """Filters the obstruction with the shortest distance to the given point. 36 | """ 37 | d_min = np.finfo(float).max 38 | obst_min = None 39 | 40 | for obst in self: 41 | for subobst in obst: 42 | dx = max(subobst.extent.x_start - x, 0, x - subobst.extent.x_end) if x is not None else 0 43 | dy = max(subobst.extent.y_start - y, 0, y - subobst.extent.y_end) if y is not None else 0 44 | dz = max(subobst.extent.z_start - z, 0, z - subobst.extent.z_end) if z is not None else 0 45 | d = np.sqrt(dx * dx + dy * dy + dz * dz) 46 | if d < d_min: 47 | d_min = d 48 | obst_min = obst 49 | 50 | return obst_min 51 | 52 | def __repr__(self): 53 | return "ObstructionCollection(" + super(ObstructionCollection, self).__repr__() + ")" 54 | -------------------------------------------------------------------------------- /fdsreader/bndf/utils.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fdsreader.bndf import Patch 4 | 5 | 6 | def sort_patches_cartesian(patches_in: List[Patch]): 7 | """Returns all patches (of same orientation!) sorted in cartesian coordinates. 8 | """ 9 | patches = patches_in.copy() 10 | if len(patches) != 0: 11 | patches_cart = [[patches[0]]] 12 | orientation = abs(patches[0].orientation) 13 | if orientation == 1: # x 14 | patches.sort(key=lambda p: (p.extent.y_start, p.extent.z_start)) 15 | elif orientation == 2: # y 16 | patches.sort(key=lambda p: (p.extent.x_start, p.extent.z_start)) 17 | elif orientation == 3: # z 18 | patches.sort(key=lambda p: (p.extent.x_start, p.extent.y_start)) 19 | 20 | if orientation == 1: 21 | for patch in patches[1:]: 22 | if patch.extent.y_start == patches_cart[-1][-1].extent.y_start: 23 | patches_cart[-1].append(patch) 24 | else: 25 | patches_cart.append([patch]) 26 | else: 27 | for patch in patches[1:]: 28 | if patch.extent.x_start == patches_cart[-1][-1].extent.x_start: 29 | patches_cart[-1].append(patch) 30 | else: 31 | patches_cart.append([patch]) 32 | return patches_cart 33 | return patches -------------------------------------------------------------------------------- /fdsreader/devc/__init__.py: -------------------------------------------------------------------------------- 1 | from .device import Device 2 | 3 | from .device_collection import DeviceCollection -------------------------------------------------------------------------------- /fdsreader/devc/device.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, Union 2 | 3 | from fdsreader.utils import Quantity 4 | 5 | 6 | class Device: 7 | """Represents a single Device. 8 | 9 | :ivar id: The id the device was given. 10 | :ivar quantity: The :class:`Quantity` the device measured. 11 | :ivar position: Position of the device in the simulation space. 12 | :ivar orientation: The direction the device was facing. 13 | :ivar data: All data the device measured. 14 | """ 15 | def __init__(self, device_id: str, quantity: Quantity, position: Tuple[float, float, float], 16 | orientation: Tuple[float, float, float]): 17 | self.id = device_id 18 | self.quantity = quantity 19 | self.position = position 20 | self.orientation = orientation 21 | self._data_callback = lambda: None 22 | 23 | @property 24 | def data(self): 25 | if not hasattr(self, "_data"): 26 | # While this design is suboptimal, there is no other way of doing it at this point in time. When a device 27 | # is encountered that does not have any data loaded yet, the device-loading function of the Simulation 28 | # class is called, which is needed to get the path to the device file as well as fill in the data for all 29 | # other devices as well as we are reading all the data anyway 30 | self._data_callback() 31 | return self._data 32 | 33 | @property 34 | def quantity_name(self): 35 | """Alias for :class:`Device`.quantity.name. 36 | """ 37 | return self.quantity.name 38 | 39 | @property 40 | def unit(self): 41 | """Alias for :class:`Device`.quantity.unit. 42 | """ 43 | return self.quantity.unit 44 | 45 | @property 46 | def xyz(self): 47 | """Alias for :class:`Device`.position. 48 | """ 49 | return self.position 50 | 51 | def clear_cache(self): 52 | """Remove all data from the internal cache that has been loaded so far to free memory. 53 | """ 54 | if hasattr(self, "_data"): 55 | del self._data 56 | 57 | def __eq__(self, other): 58 | if type(other) == str: 59 | return self.id == other 60 | return self.id == other.id 61 | 62 | def __repr__(self): 63 | return f"Device(id='{self.id}', xyz={self.position}, quantity={self.quantity})" 64 | 65 | -------------------------------------------------------------------------------- /fdsreader/devc/device_collection.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import Iterable, Union, List 3 | 4 | from fdsreader.devc import Device 5 | from fdsreader.utils.data import FDSDataCollection 6 | 7 | 8 | class DeviceCollection(FDSDataCollection): 9 | """Collection of :class:`Device` objects. Offers additional functionality for working on devices using pandas. 10 | """ 11 | 12 | def __init__(self, *devices: Iterable[Device]): 13 | super().__init__(*devices) 14 | 15 | def __getitem__(self, key) -> Union[Device, List[Device]]: 16 | if type(key) == int: 17 | return self._elements[key] 18 | else: 19 | return next(devc for devc in self._elements if (devc.id == key if type(devc) == Device else devc[0].id == key)) 20 | 21 | def __contains__(self, value: Union[Device, str]): 22 | id_matching = any((devc.id == value if type(devc) == Device else devc[0].id == value) for devc in self._elements) 23 | return value in self._elements or id_matching 24 | 25 | def to_pandas_dataframe(self): 26 | """Returns a pandas DataFrame with device-IDs as column names and device data as column values. 27 | """ 28 | import pandas as pd 29 | data = dict() 30 | for devc in self: 31 | if type(devc) == Device: 32 | data[devc.id] = devc.data 33 | elif type(devc) == list: 34 | # It might be the case that there are multiple devices with the same name 35 | for i, list_devc in enumerate(devc): 36 | data[list_devc.id + "_" + str(i)] = list_devc.data 37 | return pd.DataFrame(data) 38 | 39 | -------------------------------------------------------------------------------- /fdsreader/evac/__init__.py: -------------------------------------------------------------------------------- 1 | from .evacuation import Evacuation 2 | 3 | from .evac_collection import EvacCollection 4 | -------------------------------------------------------------------------------- /fdsreader/evac/evacuation.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple, Dict, Sequence, Union 2 | 3 | import numpy as np 4 | 5 | from fdsreader.fds_classes import Mesh 6 | from fdsreader.utils import Quantity 7 | 8 | 9 | # class Entrance: 10 | # def __init__(self): 11 | # self.id = "" 12 | # self.extent = Extent(1.0, 1.0, 1.0, 1.0, 1.0, 1.0) 13 | # self.orientation = 1 14 | # self.color = (0.0, 0.0, 0.0) 15 | 16 | 17 | # class Exit: 18 | # def __init__(self): 19 | # self.id = "" 20 | # self.extent = Extent(1.0, 1.0, 1.0, 1.0, 1.0, 1.0) 21 | # self.xyz = (1.0, 1.0, 1.0) 22 | # self.orientation = 1 23 | # self.color = (0.0, 0.0, 0.0) 24 | # self.count_only = True 25 | # self.known_door = True 26 | 27 | 28 | # class Door: 29 | # def __init__(self): 30 | # self.id = "" 31 | # self.extent = Extent(1.0, 1.0, 1.0, 1.0, 1.0, 1.0) 32 | # self.xyz = (1.0, 1.0, 1.0) 33 | # self.orientation = 1 34 | # self.color = (0.0, 0.0, 0.0) 35 | # self.to_node = Entrance() 36 | # self.known_door = True 37 | # self.exit_sign = True 38 | 39 | 40 | class Evacuation: 41 | """Container to store evac data from evac simulations with FDS. 42 | Note: Evac support was removed from FDS in all versions after 6.7.7! 43 | 44 | :ivar class_name: Name of the evac class defined in the FDS input-file. 45 | :ivar quantities: List of all quantities for which data has been written out. 46 | :ivar color: Color assigned to the evac. 47 | :ivar n_humans: Number of existing evacs for each timestep per mesh. 48 | :ivar lower_bounds: Dictionary with lower bounds for each timestep with quantities as keys. 49 | :ivar upper_bounds: Dictionary with upper bounds for each timestep with quantities as keys. 50 | """ 51 | 52 | def __init__(self, class_name: str, quantities: List[Quantity], color: Tuple[float, float, float]): 53 | self.class_name = class_name 54 | self.quantities = quantities 55 | self.color = color 56 | self.n_humans: Dict[str, List[int]] = dict() 57 | 58 | self._positions: List[np.ndarray] = list() 59 | self._body_angles: List[np.ndarray] = list() 60 | self._semi_major_axis: List[np.ndarray] = list() 61 | self._semi_minor_axis: List[np.ndarray] = list() 62 | self._agent_heights: List[np.ndarray] = list() 63 | self._tags: List[np.ndarray] = list() 64 | self._data: Dict[str, List[np.ndarray]] = {q.name: [] for q in self.quantities} 65 | self.times: Sequence[float] = list() 66 | 67 | self.lower_bounds = {q.name: [] for q in self.quantities} 68 | self.upper_bounds = {q.name: [] for q in self.quantities} 69 | 70 | self._init_callback = lambda: None 71 | 72 | @property 73 | def id(self): 74 | return self.class_name 75 | 76 | def has_quantity(self, quantity: Union[Quantity, str]): 77 | if type(quantity) == Quantity: 78 | quantity = quantity.name 79 | return any( 80 | q.name.lower() == quantity.lower() or q.short_name.lower() == quantity.lower() for q in self.quantities) 81 | 82 | def filter_by_tag(self, tag: int): 83 | """Filter all evacs by a single one with the specified tag. 84 | """ 85 | data = self.data 86 | tags = self.tags 87 | positions = self.positions 88 | evac = Evacuation(self.class_name, self.quantities, self.color) 89 | evac._tags = tag 90 | 91 | evac._data = {quantity: list() for quantity in data.keys()} 92 | evac._positions = list() 93 | evac.times = list() 94 | 95 | for t, tags in enumerate(tags): 96 | if tag in tags: 97 | idx = np.where(tags == tag)[0] 98 | 99 | for quantity in data.keys(): 100 | evac._data[quantity].append(data[quantity][t][idx][0]) 101 | evac._positions.append(positions[t][idx][0]) 102 | evac.times.append(self.times[t]) 103 | 104 | evac.lower_bounds = dict() 105 | evac.upper_bounds = dict() 106 | for q in self.quantities: 107 | if len(evac._positions) != 0: 108 | evac.lower_bounds[q.name] = np.min(evac._data[q.name]) 109 | evac.upper_bounds[q.name] = np.max(evac._data[q.name]) 110 | else: 111 | evac.lower_bounds[q.name] = 0 112 | evac.upper_bounds[q.name] = 0 113 | 114 | return evac 115 | 116 | @property 117 | def data(self) -> Dict[str, List[np.ndarray]]: 118 | """Dictionary with quantities as keys and a list with a numpy array for each timestep which 119 | contains data for each person in that timestep. 120 | """ 121 | if len(self._positions) == 0 and len(self._tags) == 0: 122 | self._init_callback() 123 | return self._data 124 | 125 | def get_data(self, quantity: Union[Quantity, str]) -> List[np.ndarray]: 126 | """Returns a list with a numpy array for each timestep which contains data about the specified quantity for 127 | each person in that timestep. 128 | """ 129 | if self.has_quantity(quantity): 130 | if type(quantity) == Quantity: 131 | quantity = quantity.name 132 | return self.data[quantity] 133 | return [] 134 | 135 | @property 136 | def tags(self) -> List[np.ndarray]: 137 | """List with a numpy array for each timestep which contains a tag for each evac in that 138 | timestep. 139 | """ 140 | if len(self._positions) == 0 and len(self._tags) == 0: 141 | self._init_callback() 142 | return self._tags 143 | 144 | @property 145 | def positions(self) -> List[np.ndarray]: 146 | """List with a numpy array for each timestep which contains the position of each evac in 147 | that timestep. 148 | """ 149 | if len(self._positions) == 0 and len(self._tags) == 0: 150 | self._init_callback() 151 | return self._positions 152 | 153 | @property 154 | def body_angles(self) -> List[np.ndarray]: 155 | """ 156 | """ 157 | if len(self._positions) == 0 and len(self._tags) == 0: 158 | self._init_callback() 159 | return self._body_angles 160 | 161 | @property 162 | def semi_major_axis(self) -> List[np.ndarray]: 163 | """ 164 | """ 165 | if len(self._positions) == 0 and len(self._tags) == 0: 166 | self._init_callback() 167 | return self._semi_major_axis 168 | 169 | @property 170 | def semi_minor_axis(self) -> List[np.ndarray]: 171 | """ 172 | """ 173 | if len(self._positions) == 0 and len(self._tags) == 0: 174 | self._init_callback() 175 | return self._semi_minor_axis 176 | 177 | @property 178 | def agent_heights(self) -> List[np.ndarray]: 179 | """ 180 | """ 181 | if len(self._positions) == 0 and len(self._tags) == 0: 182 | self._init_callback() 183 | return self._agent_heights 184 | 185 | def clear_cache(self): 186 | """Remove all data from the internal cache that has been loaded so far to free memory. 187 | """ 188 | if len(self._positions) != 0: 189 | del self._positions 190 | self._positions = list() 191 | del self._tags 192 | self._tags = list() 193 | del self._data 194 | self._data = {q.name: [] for q in self.quantities} 195 | 196 | def __repr__(self): 197 | return f"Evacuation(name={self.class_name}, quantities={self.quantities})" 198 | -------------------------------------------------------------------------------- /fdsreader/export/__init__.py: -------------------------------------------------------------------------------- 1 | from .smoke3d_exporter import export_smoke_raw 2 | from .slcf_exporter import export_slcf_raw 3 | from .obst_exporter import export_obst_raw 4 | from .sim_exporter import export_sim 5 | -------------------------------------------------------------------------------- /fdsreader/export/obst_exporter.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from typing import Dict, Union, Tuple 4 | 5 | import numpy as np 6 | from typing_extensions import Literal 7 | from ..bndf import Obstruction 8 | 9 | 10 | def export_obst_raw(obst: Obstruction, output_dir: str, ordering: Literal['C', 'F'] = 'C'): 11 | """Exports the 3d arrays to raw binary files with corresponding .yaml meta files. 12 | 13 | :param obst: The :class:`Obstruction` object to export including its :class:`Boundary` data. 14 | :param output_dir: The directory in which to save all files. 15 | :param ordering: Whether to write the data in C or Fortran ordering. 16 | """ 17 | if len(obst.quantities) == 0: 18 | return "" 19 | 20 | from pathos.pools import ProcessPool as Pool 21 | from multiprocess import Lock, Manager 22 | obst_filename_base = "obst-" + str(obst.id) 23 | 24 | bounding_box = obst.bounding_box.as_list() 25 | meta = {"BoundingBox": " ".join(f"{b:.6f}" for b in bounding_box), "NumQuantities": len(obst.quantities), 26 | "Orientations": list()} 27 | m = Manager() 28 | lock = m.Lock() 29 | meta["Quantities"] = m.list() 30 | 31 | random_bndf = next(iter(obst.get_boundary_data(obst.quantities[0]).values())) 32 | meta["TimeSteps"] = len(random_bndf.times) 33 | meta["NumOrientations"] = len(random_bndf.data) 34 | for orientation, face in random_bndf.data.items(): 35 | if abs(orientation) == 1: 36 | spacing1 = (bounding_box[3] - bounding_box[2]) / face.shape[0] 37 | spacing2 = (bounding_box[5] - bounding_box[4]) / face.shape[1] 38 | elif abs(orientation) == 2: 39 | spacing1 = (bounding_box[1] - bounding_box[0]) / face.shape[0] 40 | spacing2 = (bounding_box[5] - bounding_box[4]) / face.shape[1] 41 | else: 42 | spacing1 = (bounding_box[1] - bounding_box[0]) / face.shape[0] 43 | spacing2 = (bounding_box[3] - bounding_box[2]) / face.shape[1] 44 | 45 | meta["Orientations"].append({ 46 | "BoundaryOrientation": orientation, 47 | # "MeshPos": f"{meta['BoundingBox'][0]:.6f} {meta['BoundingBox'][2]:.6f} {meta['BoundingBox'][4]:.6f}", 48 | "Spacing": f"{random_bndf.times[1] - random_bndf.times[0]:.6f} {spacing1:.6f} {spacing2:.6f}", 49 | "DimSize": f"{face.shape[0]} {face.shape[1]}" 50 | }) 51 | 52 | def worker(quantity: str, faces: Dict[int, Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]], vmin: float, 53 | vmax: float): 54 | quantity_name = quantity.replace(" ", "_").replace(".", "-") 55 | filename = obst_filename_base + "_quantity-" + quantity_name + ".dat" 56 | 57 | out = { 58 | "BoundaryQuantity": quantity.replace(" ", "_").replace(".", "-"), 59 | "DataFile": os.path.join(quantity_name, filename), 60 | "DataValMax": vmax, 61 | "DataValMin": vmin, 62 | "ScaleFactor": 255.0 / vmax 63 | } 64 | 65 | # Abort if no useful data is available 66 | if out["DataValMax"] <= 0: 67 | return 68 | 69 | with open(os.path.join(output_dir, quantity_name, filename), 'wb') as rawfile: 70 | for face in faces.values(): # face for each orientation 71 | for d in (face * out["ScaleFactor"]).astype(np.uint8): 72 | if ordering == 'F': 73 | d = d.T 74 | d.tofile(rawfile) 75 | 76 | with lock: 77 | meta["Quantities"].append(out) 78 | 79 | worker_args = list() 80 | for i, bndf_quantity in enumerate(obst.quantities): 81 | worker_args.append((bndf_quantity.name, obst.get_global_boundary_data_arrays(bndf_quantity), 82 | obst.vmin(bndf_quantity, 0), obst.vmax(bndf_quantity, 0))) 83 | # Create all requested directories if they don't exist yet 84 | Path(os.path.join(output_dir, bndf_quantity.name.replace(" ", "_").replace(".", "-"))).mkdir(parents=True, 85 | exist_ok=True) 86 | 87 | with Pool(len(obst.quantities)) as pool: 88 | pool.map(lambda args: worker(*args), worker_args) 89 | 90 | meta["Quantities"] = list(meta["Quantities"]) 91 | 92 | meta_file_path = os.path.join(output_dir, obst_filename_base + ".yaml") 93 | with open(meta_file_path, 'w') as meta_file: 94 | import yaml 95 | yaml.dump(meta, meta_file) 96 | 97 | return meta_file_path 98 | -------------------------------------------------------------------------------- /fdsreader/export/sim_exporter.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing_extensions import Literal 3 | from . import export_slcf_raw, export_obst_raw, export_smoke_raw 4 | from .. import Simulation 5 | 6 | 7 | def export_sim(sim: Simulation, output_dir: str, ordering: Literal['C', 'F'] = 'C'): 8 | """Exports the 3d arrays to raw binary files with corresponding .yaml meta files. 9 | Warning: This method does not work for large simulations as some internal multiprocess buffers overflow after 10 | a few GB of data. Please export the simulation manually using the functions used in this method in separate 11 | python instances. 12 | 13 | :param sim: The :class:`Simulation` to export. 14 | :param output_dir: The directory in which to save all files. 15 | :param ordering: Whether to write the data in C or Fortran ordering. 16 | """ 17 | meta = {"Obstructions": list(), "Slices": list(), "Volumes": list(), "Hash": sim._hash} 18 | 19 | for obst in sim.obstructions: 20 | obst_path = export_obst_raw(obst, os.path.join(output_dir, "obst"), ordering) 21 | if obst_path: 22 | meta["Obstructions"].append(os.path.relpath(obst_path, output_dir).replace("\\", "/")) 23 | 24 | for slc in sim.slices: 25 | slice_path = export_slcf_raw(slc, os.path.join(output_dir, "slices", slc.quantity.name.replace(' ', '_').lower()), ordering) 26 | if slice_path: 27 | meta["Slices"].append(os.path.relpath(slice_path, output_dir).replace("\\", "/")) 28 | 29 | for smoke in sim.smoke_3d: 30 | volume_path = export_smoke_raw(smoke, os.path.join(output_dir, "smoke", smoke.quantity.name.replace(' ', '_').lower()), ordering) 31 | if volume_path: 32 | meta["Volumes"].append(os.path.relpath(volume_path, output_dir).replace("\\", "/")) 33 | 34 | meta["NumObstructions"] = len(meta["Obstructions"]) 35 | meta["NumSlices"] = len(meta["Slices"]) 36 | meta["NumVolumes"] = len(meta["Volumes"]) 37 | 38 | import yaml 39 | meta_file_path = os.path.join(output_dir, sim.chid + "-smv.yaml") 40 | with open(meta_file_path, 'w') as metafile: 41 | yaml.dump(meta, metafile) 42 | 43 | return meta_file_path 44 | -------------------------------------------------------------------------------- /fdsreader/export/slcf_exporter.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | import numpy as np 4 | from typing_extensions import Literal 5 | from ..slcf import Slice 6 | 7 | 8 | def export_slcf_raw(slc: Slice, output_dir: str, ordering: Literal['C', 'F'] = 'C'): 9 | """Exports the 3d arrays to raw binary files with corresponding .yaml meta files. 10 | 11 | :param slc: The :class:`Slice` object to export. 12 | :param output_dir: The directory in which to save all files. 13 | :param ordering: Whether to write the data in C or Fortran ordering. 14 | """ 15 | from pathos.pools import ProcessPool as Pool 16 | from multiprocess import Lock, Manager 17 | slc2d = slc.type == '2D' 18 | 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)), 19 | "MeshNum": len(slc.subslices), "Quantity": slc.quantity.name} 20 | 21 | filename_base = ("slice" + ("2D-" if slc2d else "3D-") + slc.id.lower()).replace(" ", "_").replace(".", "-") 22 | # Create all requested directories if they don't exist yet 23 | Path(os.path.join(output_dir, filename_base + "-data")).mkdir(parents=True, exist_ok=True) 24 | 25 | m = Manager() 26 | meta["Meshes"] = m.list() 27 | lock = m.Lock() 28 | 29 | def worker(mesh, subslice): 30 | mesh_id = mesh.id.replace(" ", "_").replace(".", "-") 31 | filename = filename_base + "_mesh-" + mesh_id + ".dat" 32 | 33 | data = ((subslice.data - meta["DataValMin"]) * meta["ScaleFactor"]).astype(np.uint8) 34 | shape = data.shape 35 | if slc2d: 36 | shape = shape[:subslice.orientation] + (1,) + shape[subslice.orientation:] 37 | 38 | with open(os.path.join(output_dir, filename_base + "-data", filename), 'wb') as rawfile: 39 | for d in data: 40 | if ordering == 'F': 41 | d = d.T 42 | d.tofile(rawfile) 43 | 44 | spacing = [slc.times[1] - slc.times[0], 45 | mesh.coordinates['x'][1] - mesh.coordinates['x'][0], 46 | mesh.coordinates['y'][1] - mesh.coordinates['y'][0], 47 | mesh.coordinates['z'][1] - mesh.coordinates['z'][0]] 48 | with lock: 49 | meta["Meshes"].append({ 50 | "Mesh": mesh_id, 51 | "DataFile": os.path.join(filename_base + "-data", filename), 52 | "MeshPos": f"{subslice.extent['x'][0]:.6} {subslice.extent['y'][0]:.6} {subslice.extent['z'][0]:.6}", 53 | "Spacing": f"{spacing[0]:.6} {spacing[1]:.6} {spacing[2]:.6} {spacing[3]:.6}", 54 | "DimSize": f"{shape[0]} {shape[1]} {shape[2]} {shape[3]}" 55 | }) 56 | 57 | with Pool() as pool: 58 | pool.map(lambda args: worker(*args), list(slc._subslices.items())) 59 | 60 | meta["Meshes"] = list(meta["Meshes"]) 61 | 62 | meta_file_path = os.path.join(output_dir, filename_base + ".yaml") 63 | with open(meta_file_path, 'w') as meta_file: 64 | import yaml 65 | yaml.dump(meta, meta_file) 66 | 67 | return meta_file_path 68 | -------------------------------------------------------------------------------- /fdsreader/export/smoke3d_exporter.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | import numpy as np 4 | from typing_extensions import Literal 5 | from ..smoke3d import Smoke3D 6 | 7 | 8 | def export_smoke_raw(smoke3d: Smoke3D, output_dir: str, ordering: Literal['C', 'F'] = 'C'): 9 | """Exports the 3d arrays to raw binary files with corresponding .yaml meta files. 10 | 11 | :param smoke3d: The :class:`Smoke3D` object to export. 12 | :param output_dir: The directory in which to save all files. 13 | :param ordering: Whether to write the data in C or Fortran ordering. 14 | """ 15 | from pathos.pools import ProcessPool as Pool 16 | from multiprocess import Lock, Manager 17 | filename_base = ("smoke-" + smoke3d.quantity.name.lower()).replace(" ", "_").replace(".", "-") 18 | # Create all requested directories if they don't exist yet 19 | Path(os.path.join(output_dir, filename_base + "-data")).mkdir(parents=True, exist_ok=True) 20 | 21 | meta = {"DataValMax": -100000., "DataValMin": 100000., "ScaleFactor": 1, "MeshNum": len(smoke3d.subsmokes), 22 | "Quantity": smoke3d.quantity.name} 23 | 24 | for subsmoke in smoke3d._subsmokes.values(): 25 | meta["DataValMax"] = max(meta["DataValMax"], np.max(subsmoke.data)) 26 | meta["DataValMin"] = min(meta["DataValMin"], np.min(subsmoke.data)) 27 | meta["DataValMax"] = float(meta["DataValMax"]) 28 | meta["DataValMin"] = float(meta["DataValMin"]) 29 | 30 | # Abort if no useful data is available 31 | if meta["DataValMax"] <= 0: 32 | return "" 33 | 34 | meta["ScaleFactor"] = 255.0 / meta["DataValMax"] 35 | 36 | m = Manager() 37 | meta["Meshes"] = m.list() 38 | lock = m.Lock() 39 | 40 | def worker(mesh, subsmoke): 41 | mesh_id = mesh.id.replace(" ", "_").replace(".", "-") 42 | filename = filename_base + "_mesh-" + mesh_id + ".dat" 43 | 44 | data = (subsmoke.data * meta["ScaleFactor"]).astype(np.uint8) 45 | 46 | with open(os.path.join(output_dir, filename_base + "-data", filename), 'wb') as rawfile: 47 | for d in data: 48 | if ordering == 'F': 49 | d = d.T 50 | d.tofile(rawfile) 51 | 52 | spacing = [smoke3d.times[1] - smoke3d.times[0], 53 | mesh.coordinates['x'][1] - mesh.coordinates['x'][0], 54 | mesh.coordinates['y'][1] - mesh.coordinates['y'][0], 55 | mesh.coordinates['z'][1] - mesh.coordinates['z'][0]] 56 | with lock: 57 | meta["Meshes"].append({ 58 | "Mesh": mesh_id, 59 | "DataFile": os.path.join(filename_base + "-data", filename), 60 | "MeshPos": f"{mesh.coordinates['x'][0]:.6} {mesh.coordinates['y'][0]:.6} {mesh.coordinates['z'][0]:.6}", 61 | "Spacing": f"{spacing[0]:.6} {spacing[1]:.6} {spacing[2]:.6} {spacing[3]:.6}", 62 | "DimSize": f"{data.shape[0]} {data.shape[1]} {data.shape[2]} {data.shape[3]}" 63 | }) 64 | 65 | with Pool() as pool: 66 | pool.map(lambda args: worker(*args), list(smoke3d._subsmokes.items())) 67 | 68 | meta["Meshes"] = list(meta["Meshes"]) 69 | 70 | meta_file_path = os.path.join(output_dir, filename_base + ".yaml") 71 | with open(meta_file_path, 'w') as meta_file: 72 | import yaml 73 | yaml.dump(meta, meta_file) 74 | 75 | return meta_file_path 76 | -------------------------------------------------------------------------------- /fdsreader/fds_classes/__init__.py: -------------------------------------------------------------------------------- 1 | from .mesh import Mesh 2 | 3 | from .mesh_collection import MeshCollection 4 | 5 | from .surface import Surface 6 | 7 | from .ventilation import Ventilation 8 | -------------------------------------------------------------------------------- /fdsreader/fds_classes/mesh.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Tuple, Sequence, List, Union 2 | from typing_extensions import Literal 3 | import numpy as np 4 | import math 5 | 6 | from fdsreader import settings 7 | from fdsreader.utils import Dimension, Extent, Quantity 8 | from fdsreader.bndf import Boundary, Patch 9 | 10 | 11 | class Mesh: 12 | """3-dimensional Mesh of fixed, defined size. 13 | 14 | :ivar coordinates: Coordinate values for each of the 3 dimension. 15 | :ivar dimensions: :class:`Dimension` describing the size of the 3 dimensions regarding indices. 16 | :ivar extent: :class:`Extent` object containing 3-dimensional extent information. 17 | :ivar n: Number of elements for each of the 3 dimensions. 18 | :ivar n_size: Total number of blocks in this mesh. 19 | :var id: Mesh id/short_name assigned to this mesh. 20 | """ 21 | 22 | def __init__(self, coordinates: Dict[Literal['x', 'y', 'z'], np.ndarray], 23 | extents: Dict[Literal['x', 'y', 'z'], Tuple[float, float]], mesh_id: str): 24 | """ 25 | :param coordinates: Coordinate values of the three axes. 26 | :param extents: Extent of the mesh in each dimension. 27 | :param mesh_id: ID of this mesh. 28 | """ 29 | self.id = mesh_id 30 | self.coordinates = coordinates 31 | self.dimension = Dimension( 32 | coordinates['x'].size if coordinates['x'].size > 0 else 1, 33 | coordinates['y'].size if coordinates['y'].size > 0 else 1, 34 | coordinates['z'].size if coordinates['z'].size > 0 else 1) 35 | 36 | self.n_size = self.dimension.size() 37 | self.extent = Extent(extents['x'][0], extents['x'][1], extents['y'][0], extents['y'][1], 38 | extents['z'][0], extents['z'][1]) 39 | 40 | self.obstructions = list() 41 | 42 | self._boundary_data: Dict[int, Boundary] = dict() 43 | 44 | def get_obstruction_mask(self, times: Sequence[float], cell_centered=False) -> np.ndarray: 45 | """Marks all cells which are blocked by an obstruction. 46 | 47 | :param times: All timesteps of the simulation. 48 | :returns: A 4-dimensional array with time as first and x,y,z as last dimensions. The array 49 | depends on time as obstructions may be hidden as specific points in time. 50 | """ 51 | shape = self.dimension.shape(cell_centered=cell_centered) 52 | mask = np.ones((len(times), shape[0], shape[1], shape[2]), dtype=bool) 53 | c = 1 if cell_centered else 0 54 | 55 | for obst in self.obstructions: 56 | subobst = obst[self] 57 | x1, x2 = subobst.bound_indices['x'] 58 | y1, y2 = subobst.bound_indices['y'] 59 | z1, z2 = subobst.bound_indices['z'] 60 | t_idx = 0 61 | for t in subobst.get_visible_times(times): 62 | while not np.isclose(t, times[t_idx]): 63 | t_idx += 1 64 | mask[t_idx, x1:max(x2 + c, x1 + 1), y1:max(y2 + c, y1 + 1), z1:max(z2 + c, z1 + 1)] = False 65 | return mask 66 | 67 | def get_obstruction_mask_slice(self, subslice): 68 | """Marks all cells of a single subslice which are blocked by an obstruction. 69 | 70 | :returns: A 4-dimensional array with time as first and x,y,z as last dimensions. 71 | The array depends on time as obstructions may be hidden at specific points in time. 72 | """ 73 | orientation = subslice.orientation 74 | value = subslice.extent[orientation][0] 75 | cell_centered = subslice.cell_centered 76 | 77 | slc_index = self.coordinate_to_index((value,), dimension=(orientation,), cell_centered=cell_centered)[0] 78 | 79 | mask_indices = [slice(None)] * 4 80 | mask_indices[orientation] = slice(slc_index, slc_index + 1, 1) 81 | mask_indices = tuple(mask_indices) 82 | 83 | return self.get_obstruction_mask(subslice.times, cell_centered=cell_centered)[mask_indices] 84 | 85 | def coordinate_to_index(self, coordinate: Tuple[float, ...], 86 | dimension: Tuple[Literal[1, 2, 3, 'x', 'y', 'z'], ...] = ('x', 'y', 'z'), 87 | cell_centered=False) -> Tuple[int, ...]: 88 | """Finds the nearest point in the mesh's grid and returns its indices. 89 | 90 | :param coordinate: Tuple of 3 floats. If the dimension parameter is supplied, up to 2 91 | dimensions can be left out from the tuple. 92 | :param dimension: The dimensions in which to return the indices (1=x, 2=y, 3=z). 93 | :param cell_centered: Instead of finding the nearest point on the mesh, find the center of the nearest cell. 94 | """ 95 | # Convert possible integer input to chars 96 | dimension = tuple(('x', 'y', 'z')[dim - 1] if type(dim) == int else dim for dim in dimension) 97 | 98 | ret = list() 99 | for i, dim in enumerate(dimension): 100 | co = coordinate[i] 101 | coords = self.coordinates[dim] 102 | if cell_centered: 103 | coords = coords[:-1] + (coords[1] - coords[0]) / 2 104 | idx = np.searchsorted(coords, co, side="left") 105 | if co > 0 and (idx == len(coords) or math.fabs(co - coords[idx - 1]) < math.fabs( 106 | co - coords[idx])): 107 | ret.append(idx - 1) 108 | else: 109 | ret.append(idx) 110 | return tuple(ret) 111 | 112 | def get_nearest_coordinate(self, coordinate: Tuple[float, ...], 113 | dimension: Tuple[Literal[1, 2, 3, 'x', 'y', 'z'], ...] = ('x', 'y', 'z'), 114 | cell_centered=False) -> Tuple[float, ...]: 115 | """Finds the nearest point in the mesh's grid. 116 | 117 | :param coordinate: Tuple of 3 floats. If the dimension parameter is supplied, up to 2 118 | dimensions can be left out from the tuple. 119 | :param dimension: The dimensions in which to return the indices (1=x, 2=y, 3=z). 120 | :param cell_centered: Instead of finding the nearest point on the mesh, find the center of the nearest cell. 121 | """ 122 | indices = self.coordinate_to_index(coordinate, dimension, cell_centered) 123 | ret = list() 124 | for i, dim in enumerate(dimension): 125 | coords = self.coordinates[dim] 126 | if cell_centered: 127 | coords = coords[:-1] + (coords[1] - coords[0]) / 2 128 | ret.append(coords[indices[i]]) 129 | return tuple(ret) 130 | 131 | def _add_patches(self, bid: int, cell_centered: bool, quantity: str, short_name: str, unit: str, 132 | patches: List[Patch], times: np.ndarray, lower_bounds: np.ndarray, 133 | upper_bounds: np.ndarray): 134 | if bid not in self._boundary_data: 135 | self._boundary_data[bid] = Boundary(Quantity(quantity, short_name, unit), cell_centered, times, 136 | patches, lower_bounds, upper_bounds) 137 | # Add reference to parent boundary class in patches 138 | for patch in patches: 139 | patch._boundary_parent = self._boundary_data[bid] 140 | 141 | if not settings.LAZY_LOAD: 142 | _ = self._boundary_data[bid].data 143 | 144 | def get_boundary_data(self, quantity: Union[str, Quantity]): 145 | if type(quantity) == Quantity: 146 | quantity = quantity.name 147 | return next(b for b in self._boundary_data.values() if 148 | b.quantity.name.lower() == quantity.lower() or b.quantity.short_name.lower() == quantity.lower()) 149 | 150 | def __getitem__(self, dimension: Literal[0, 1, 2, 'x', 'y', 'z']) -> np.ndarray: 151 | """Get all values in given dimension. 152 | 153 | :param dimension: The dimension in which to return all grid values (0=x, 1=y, 2=z). 154 | """ 155 | # Convert possible integer input to chars 156 | if type(dimension) == int: 157 | dimension = ('x', 'y', 'z')[dimension] 158 | return self.coordinates[dimension] 159 | 160 | def __eq__(self, other): 161 | return self.id == other.id 162 | 163 | def __repr__(self, *args, **kwargs): 164 | return f'Mesh(id="{self.id}", extent={str(self.extent)}, dimension={str(self.dimension)})' 165 | -------------------------------------------------------------------------------- /fdsreader/fds_classes/mesh_collection.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | 3 | from fdsreader.fds_classes import Mesh 4 | from fdsreader.utils.data import FDSDataCollection 5 | 6 | 7 | class MeshCollection(FDSDataCollection): 8 | """Collection of :class:`Obstruction` objects. Offers extensive functionality for filtering and 9 | using obstructions as well as dependent such as :class:`Boundary`. 10 | """ 11 | 12 | def __init__(self, *meshes: Iterable[Mesh]): 13 | super().__init__(*meshes) 14 | 15 | def get_by_id(self, mesh_id: str): 16 | """Get the mesh with corresponding id if it exists. 17 | """ 18 | return next((mesh for mesh in self if mesh.id == mesh_id), None) 19 | 20 | def __repr__(self): 21 | return "MeshCollection(" + super(MeshCollection, self).__repr__() + ")" 22 | -------------------------------------------------------------------------------- /fdsreader/fds_classes/surface.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Tuple 2 | 3 | 4 | class Surface: 5 | """Surface objects describe what bounding surfaces consist of. Boundary conditions for obstructions and vents are 6 | prescribed by referencing the appropriate surface. 7 | 8 | :ivar name: Name of the surface. 9 | :ivar material_emissivity: Emissivity of the material. 10 | :ivar surface_type: Type of the surface. 11 | :ivar texture_width:Width of the texture of the surface. 12 | :ivar texture_height: Height of the texture of the surface. 13 | :ivar texture_map: Path to the texture map used for the surface. 14 | :ivar rgb: Color of the surface in form of a 3-element tuple. 15 | :ivar transparency: Transparency of the color (alpha channel). 16 | """ 17 | 18 | def __init__(self, name: str, tmpm: float, material_emissivity: float, surface_type: int, 19 | texture_width: float, texture_height: float, texture_map: Optional[str], 20 | rgb: Tuple[float, float, float], transparency: float): 21 | self.name = name 22 | self.tmpm = tmpm 23 | self.material_emissivity = material_emissivity 24 | self.surface_type = surface_type 25 | self.texture_width = texture_width 26 | self.texture_height = texture_height 27 | self.texture_map = texture_map 28 | self.rgb = rgb 29 | self.transparency = transparency 30 | 31 | def id(self): 32 | return self.name 33 | 34 | def __eq__(self, other): 35 | return self.name == other.name 36 | 37 | def __repr__(self, *args, **kwargs): 38 | return f'Surface(name="{self.name}")' 39 | -------------------------------------------------------------------------------- /fdsreader/fds_classes/ventilation.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Tuple, Dict 2 | 3 | from fdsreader.fds_classes import Surface, Mesh 4 | from fdsreader.utils import Extent 5 | 6 | 7 | class SubVentilation: 8 | """Part of a :class:`Ventilation`. 9 | 10 | :ivar extent: :class:`Extent` object containing 3-dimensional extent information. 11 | :ivar mesh: The mesh that contains this part of the ventilation. 12 | """ 13 | 14 | def __init__(self, mesh: Mesh, extent: Extent): 15 | self.mesh = mesh 16 | self.extent = extent 17 | 18 | 19 | class Ventilation: 20 | """A ventilation can be used to model components of the ventilation system in a building, like a 21 | diffuser or a return. 22 | A ventilation can also be used as a means of applying a particular boundary condition to a 23 | rectangular patch on a solid surface. 24 | 25 | :ivar id: ID of the ventilation. 26 | :ivar extent: :class:`Extent` object containing 3-dimensional extent information. 27 | :ivar surface: Surface object used for the ventilation. 28 | :ivar bound_indices: Indices used to define ventilation bounds in terms of mesh locations. 29 | :ivar color_index: Type of coloring used to color ventilation. 30 | -99 or +99 - use default color 31 | -n or +n - use n’th palette color 32 | < 0 - do not draw boundary file over vent 33 | > 0 - draw boundary file over vent 34 | :ivar draw_type: Defines how the ventilation is drawn. 35 | 0 - solid surface 36 | 2 - outline 37 | -2 - hidden 38 | :ivar open_time: Point in time the ventilation should open. 39 | :ivar close_time: Point in time the ventilation should close. 40 | :ivar rgba: Color of the ventilation in form of a 4-element tuple (ranging from 0.0 to 1.0). 41 | :ivar texture_origin: Origin position of the texture provided by the surface. When the texture does have a pattern, 42 | for example windows or bricks, the texture_origin specifies where the pattern should begin. 43 | :ivar circular_vent_origin: Origin of the ventilation relative to bounding box. 44 | :ivar radius: Radius of the ventilation circle. 45 | """ 46 | 47 | def __init__(self, surface: Surface, bound_indices: Tuple[int, int, int, int, int, int], 48 | color_index: int, draw_type: int, 49 | rgba: Union[Tuple[()], Tuple[float, float, float, float]] = (), 50 | texture_origin: Union[Tuple[()], Tuple[float, float, float]] = (), 51 | circular_vent_origin: Union[Tuple[()], Tuple[float, float, float]] = (), 52 | radius: float = -1): 53 | self.surface = surface 54 | self.bound_indices = bound_indices 55 | self.color_index = color_index 56 | self.draw_type = draw_type 57 | self.open_time = -1.0 58 | self.close_time = -1.0 59 | 60 | if len(rgba) != 0: 61 | self.rgba = rgba 62 | if len(texture_origin) != 0: 63 | self.texture_origin = texture_origin 64 | if len(circular_vent_origin) != 0: 65 | self.circular_vent_origin = circular_vent_origin 66 | if radius != -1: 67 | self.radius = radius 68 | 69 | self._subventilations: Dict[str, SubVentilation] = dict() 70 | 71 | def _add_subventilation(self, mesh: Mesh, extent: Extent): 72 | self._subventilations[mesh.id] = SubVentilation(mesh, extent) 73 | 74 | def __repr__(self, *args, **kwargs): 75 | return f"Ventilation()" 76 | -------------------------------------------------------------------------------- /fdsreader/geom/__init__.py: -------------------------------------------------------------------------------- 1 | from .geometry import Geometry, GeomBoundary 2 | 3 | from .geometry_collection import GeometryCollection 4 | -------------------------------------------------------------------------------- /fdsreader/geom/geometry.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from typing import Tuple, Dict, Iterable 3 | 4 | import numpy as np 5 | 6 | from fdsreader.fds_classes import Surface 7 | from fdsreader.utils import Quantity 8 | import fdsreader.utils.fortran_data as fdtype 9 | 10 | 11 | class GeomBoundary: 12 | """Boundary data of a specific quantity for all geoms in the simulation. 13 | 14 | :ivar quantity: Quantity object containing information about the quantity calculated for geoms. 15 | :ivar times: Numpy array containing all times for which data has been recorded. 16 | :ivar n_t: Total number of time steps for which output data has been written. 17 | :ivar lower_bounds: Dictionary with lower bounds for each timestep per mesh. 18 | :ivar upper_bounds: Dictionary with upper bounds for each timestep per mesh. 19 | """ 20 | 21 | def __init__(self, quantity: Quantity, times: np.ndarray, n_t: int): 22 | self.quantity = quantity 23 | self.times = times 24 | self.n_t = n_t 25 | 26 | self.lower_bounds: Dict[int, np.ndarray] = dict() 27 | self.upper_bounds: Dict[int, np.ndarray] = dict() 28 | 29 | self.file_paths_be: Dict[int, str] = dict() 30 | self.file_paths_gbf: Dict[int, str] = dict() 31 | 32 | def _add_data(self, mesh: int, file_path_be: str, file_path_gbf: str, lower_bounds: np.ndarray, 33 | upper_bounds: np.ndarray): 34 | self.file_paths_be[mesh] = file_path_be 35 | self.file_paths_gbf[mesh] = file_path_gbf 36 | 37 | self.lower_bounds[mesh] = lower_bounds 38 | self.upper_bounds[mesh] = upper_bounds 39 | 40 | def _load_data(self): 41 | self._vertices: Dict[int, np.ndarray] = dict() 42 | self._faces: Dict[int, np.ndarray] = dict() 43 | self._data: Dict[int, np.ndarray] = dict() 44 | 45 | for mesh in self.file_paths_be.keys(): 46 | file_path_be = self.file_paths_be[mesh] 47 | file_path_gbf = self.file_paths_gbf[mesh] 48 | # Load .gbf 49 | with open(file_path_gbf, 'rb') as infile: 50 | offset = fdtype.INT.itemsize * 2 + fdtype.new( 51 | (('i', 3),)).itemsize + fdtype.FLOAT.itemsize 52 | infile.seek(offset) 53 | 54 | dtype_meta = fdtype.new((('i', 3),)) 55 | n_vertices, n_faces, _ = np.fromfile(infile, dtype_meta, 1)[0][1] 56 | 57 | dtype_vertices = fdtype.new((('f', 3 * n_vertices),)) 58 | vertices = np.fromfile(infile, dtype_vertices, 1)[0][1].reshape( 59 | (n_vertices, 3)).astype(float) 60 | 61 | dtype_faces = fdtype.new((('i', 3 * n_faces),)) 62 | faces = fdtype.read(infile, dtype_faces, 1)[0][0].reshape((n_faces, 3)).astype( 63 | int) - 1 64 | 65 | # Load .be 66 | dtype_faces = fdtype.new((('f', n_faces),)) 67 | dtype_meta = fdtype.new((('i', 4),)) 68 | data = np.empty((self.n_t, n_faces), dtype=np.float32) 69 | with open(file_path_be, 'rb') as infile: 70 | offset = fdtype.INT.itemsize * 2 71 | infile.seek(offset) 72 | for t in range(self.n_t): 73 | # Skip time value 74 | infile.seek(fdtype.FLOAT.itemsize, 1) 75 | assert fdtype.read(infile, dtype_meta, 1)[0][0][3] == n_faces, "Something went wrong, please" \ 76 | " submit an issue on Github" \ 77 | " attaching your fds case!" 78 | if n_faces > 0: 79 | data[t, :] = np.fromfile(infile, dtype_faces, 1)[0][1] 80 | 81 | self._vertices[mesh] = vertices 82 | self._faces[mesh] = faces 83 | self._data[mesh] = data 84 | 85 | # def _load_data2(self): 86 | # self._vertices: Dict[int, np.ndarray] = dict() 87 | # self._faces: Dict[int, np.ndarray] = dict() 88 | # self._data: Dict[int, np.ndarray] = dict() 89 | # 90 | # for mesh in self.file_paths_be.keys(): 91 | # file_path_be = self.file_paths_be[mesh] 92 | # file_path_gbf = self.file_paths_gbf[mesh] 93 | # # Load .gbf 94 | # with open(file_path_gbf, 'rb') as infile: 95 | # offset = fdtype.INT.itemsize * 2 + fdtype.new( 96 | # (('i', 3),)).itemsize + fdtype.FLOAT.itemsize 97 | # infile.seek(offset) 98 | # 99 | # dtype_meta = fdtype.new((('i', 3),)) 100 | # n_vertices, n_faces, _ = np.fromfile(infile, dtype_meta, 1)[0][1] 101 | # 102 | # dtype_vertices = fdtype.new((('f', 3 * n_vertices),)) 103 | # vertices = np.fromfile(infile, dtype_vertices, 1)[0][1].reshape( 104 | # (n_vertices, 3)).astype(float) 105 | # 106 | # dtype_faces = fdtype.new((('i', 3 * n_faces),)) 107 | # faces = fdtype.read(infile, dtype_faces, 1)[0][0].reshape((n_faces, 3)).astype( 108 | # int) - 1 109 | # 110 | # # Load .be 111 | # class BEReader(threading.Thread): 112 | # def __init__(self, path, thread_offset, n_t, data, t_start, n_faces): 113 | # threading.Thread.__init__(self) 114 | # self.path = path 115 | # self.n_faces = n_faces 116 | # self.offset = thread_offset 117 | # self.n_t = n_t 118 | # self.data = data 119 | # self.t_start = t_start 120 | # 121 | # def run(self): 122 | # dtype_faces = fdtype.new((('f', self.n_faces),)) 123 | # dtype_meta = fdtype.new((('i', 4),)) 124 | # with open(self.path, 'rb') as infile: 125 | # for t in range(self.n_t): 126 | # # Skip time and n_faces values 127 | # infile.seek(fdtype.FLOAT.itemsize + dtype_meta.itemsize, 1) 128 | # 129 | # if self.n_faces > 0: 130 | # self.data[self.t_start + t, :] = np.fromfile(infile, dtype_faces, 1)[0][1] 131 | # 132 | # dtype_faces = fdtype.new((('f', n_faces),)) 133 | # dtype_meta = fdtype.new((('i', 4),)) 134 | # data = np.empty((self.n_t, n_faces), dtype=np.float32) 135 | # 136 | # offset = fdtype.INT.itemsize * 2 137 | # stride = dtype_faces.itemsize + dtype_meta.itemsize + fdtype.FLOAT.itemsize 138 | # 139 | # times = list(range(0, self.n_t, self.n_t // 8)) 140 | # times.append(self.n_t) 141 | # threads = list() 142 | # for i in range(len(times)-1): 143 | # threads.append(BEReader(file_path_be, offset+stride*times[i], times[i+1] - times[i], data, times[i], n_faces)) 144 | # 145 | # for thread in threads: 146 | # thread.start() 147 | # 148 | # for thread in threads: 149 | # thread.join() 150 | # 151 | # self._vertices[mesh] = vertices 152 | # self._faces[mesh] = faces 153 | # self._data[mesh] = data 154 | 155 | @property 156 | def vertices(self) -> Iterable: 157 | """Returns a global array of the vertices from all meshes. 158 | """ 159 | if not hasattr(self, "_vertices"): 160 | self._load_data() 161 | 162 | size = sum(v.shape[0] for v in self._vertices.values()) 163 | ret = np.empty((size, 3), dtype=float) 164 | counter = 0 165 | 166 | for v in self._vertices.values(): 167 | size = v.shape[0] 168 | ret[counter:counter + size, :] = v 169 | counter += size 170 | 171 | return ret 172 | 173 | @property 174 | def faces(self) -> np.ndarray: 175 | """Returns a global array of the faces from all meshes. 176 | """ 177 | if not hasattr(self, "_faces"): 178 | self._load_data() 179 | 180 | size = sum(f.shape[0] for f in self._faces.values()) 181 | ret = np.empty((size, 3), dtype=int) 182 | counter = 0 183 | verts_counter = 0 184 | 185 | for m, f in self._faces.items(): 186 | size = f.shape[0] 187 | ret[counter:counter + size, :] = f + verts_counter 188 | counter += size 189 | verts_counter += self._vertices[m].shape[0] 190 | 191 | return ret 192 | 193 | @property 194 | def data(self) -> np.ndarray: 195 | """Returns a global array of the loaded data for the quantity with data from all meshes. 196 | """ 197 | if not hasattr(self, "_data"): 198 | self._load_data() 199 | 200 | size = sum(d[0].shape[0] for d in self._data.values()) 201 | ret = np.empty((self.n_t, size), dtype=np.float32) 202 | for t in range(self.n_t): 203 | counter = 0 204 | for d in self._data.values(): 205 | size = d[t].shape[0] 206 | ret[t, counter:counter + size] = d[t] 207 | counter += size 208 | 209 | return ret 210 | 211 | # @property 212 | # def data2(self) -> np.ndarray: 213 | # """Returns a global array of the loaded data for the quantity with data from all meshes. 214 | # """ 215 | # # if not hasattr(self, "_data"): 216 | # # self._load_data2() 217 | # self._load_data2() 218 | # 219 | # size = sum(d[0].shape[0] for d in self._data.values()) 220 | # ret = np.empty((self.n_t, size), dtype=np.float32) 221 | # for t in range(self.n_t): 222 | # counter = 0 223 | # for d in self._data.values(): 224 | # size = d[t].shape[0] 225 | # ret[t, counter:counter + size] = d[t] 226 | # counter += size 227 | # 228 | # return ret 229 | 230 | @property 231 | def vmin(self) -> float: 232 | """Minimum value of all faces at any time. 233 | """ 234 | curr_min = min(np.min(b) for b in self.lower_bounds.values()) 235 | if curr_min == 0.0: 236 | return min(min(np.min(p.data) for p in ps) for ps in self._faces.values()) 237 | return curr_min 238 | 239 | @property 240 | def vmax(self) -> float: 241 | """Maximum value of all faces at any time. 242 | """ 243 | curr_max = max(np.max(b) for b in self.upper_bounds.values()) 244 | if curr_max == np.float32(-1e33): 245 | return max(max(np.max(p.data) for p in ps) for ps in self._faces.values()) 246 | return curr_max 247 | 248 | def __repr__(self, *args, **kwargs): 249 | return f"GeomBoundary(quantity={self.quantity})" 250 | 251 | 252 | class Geometry: 253 | """Obstruction defined as a complex geometry. 254 | 255 | :ivar file_path: Path to the .ge file that defines the geom. 256 | :ivar texture_map: Path to the texture map of the geometry. 257 | :ivar texture_origin: Origin position of the texture provided by the surface. 258 | :ivar is_terrain: Indicates whether the geometry is a regular complex geom or terrain geometry. 259 | :ivar rgb: Color of the geometry in form of a 3-element tuple. 260 | :ivar surface: Surface object used for the geometry. 261 | """ 262 | 263 | def __init__(self, file_path: str, texture_mapping: str, 264 | texture_origin: Tuple[float, float, float], 265 | is_terrain: bool, rgb: Tuple[int, int, int], surface: Surface = None): 266 | self.file_path = file_path 267 | self.texture_map = texture_mapping 268 | self.texture_origin = texture_origin 269 | self.is_terrain = is_terrain 270 | self.rgb = rgb 271 | self.surface = surface 272 | 273 | def __repr__(self, *args, **kwargs): 274 | return f"Geometry(file_path={self.file_path})" 275 | -------------------------------------------------------------------------------- /fdsreader/geom/geometry_collection.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Union, List 2 | 3 | from fdsreader import settings 4 | from fdsreader.geom import GeomBoundary 5 | from fdsreader.utils.data import FDSDataCollection, Quantity 6 | 7 | 8 | class GeometryCollection(FDSDataCollection): 9 | """Collection of :class:`GeomBoundary` objects. Offers extensive functionality for filtering and 10 | using geometry data. 11 | """ 12 | 13 | def __init__(self, *geom_boundaries: Iterable[GeomBoundary]): 14 | super().__init__(*geom_boundaries) 15 | 16 | if not settings.LAZY_LOAD: 17 | for geom in self: 18 | geom._load_prt_data() 19 | 20 | @property 21 | def quantities(self) -> List[Quantity]: 22 | return list({geom.name for geom in self}) 23 | 24 | def filter_by_quantity(self, quantity: Union[str, Quantity]): 25 | """Filters all GeomBoundaries by a specific quantity. 26 | """ 27 | if type(quantity) == Quantity: 28 | quantity = quantity.name 29 | return GeometryCollection(x for x in self if 30 | x.quantity.name.lower() == quantity.lower() or x.quantity.short_name.lower() == quantity.lower()) 31 | 32 | def __repr__(self): 33 | return "GeometryCollection(" + super(GeometryCollection, self).__repr__() + ")" 34 | -------------------------------------------------------------------------------- /fdsreader/isof/__init__.py: -------------------------------------------------------------------------------- 1 | from .isosurface import Isosurface 2 | 3 | from .isosurface_collection import IsosurfaceCollection 4 | -------------------------------------------------------------------------------- /fdsreader/isof/isosurface_collection.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Union, List 2 | 3 | from fdsreader.isof import Isosurface 4 | from fdsreader.utils.data import FDSDataCollection, Quantity 5 | 6 | 7 | class IsosurfaceCollection(FDSDataCollection): 8 | """Collection of :class:`Isosurface` objects. Offers extensive functionality for filtering and 9 | using isosurfaces as well as its subclasses such as :class:`SubSurface`. 10 | """ 11 | 12 | def __init__(self, *isosurfaces: Iterable[Isosurface]): 13 | super().__init__(*isosurfaces) 14 | 15 | @property 16 | def quantities(self) -> List[Quantity]: 17 | return list({iso.name for iso in self}) 18 | 19 | def filter_by_quantity(self, quantity: Union[str, Quantity]): 20 | """Filters all isosurfaces by a specific quantity. 21 | """ 22 | if type(quantity) == Quantity: 23 | quantity = quantity.name 24 | return IsosurfaceCollection(x for x in self if 25 | x.quantity.name.lower() == quantity.lower() or x.quantity.short_name.lower() == quantity.lower()) 26 | 27 | def __repr__(self): 28 | return "IsosurfaceCollection(" + super(IsosurfaceCollection, self).__repr__() + ")" 29 | -------------------------------------------------------------------------------- /fdsreader/part/__init__.py: -------------------------------------------------------------------------------- 1 | from .particle import Particle 2 | 3 | from .particle_collection import ParticleCollection 4 | -------------------------------------------------------------------------------- /fdsreader/part/particle.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple, Dict, Sequence 2 | 3 | import numpy as np 4 | 5 | from fdsreader.fds_classes import Mesh 6 | from fdsreader.utils import Quantity 7 | 8 | 9 | class Particle: 10 | """Container to store particle data from particle simulations with FDS. 11 | 12 | :ivar class_name: Name of the particle class defined in the FDS input-file. 13 | :ivar quantities: List of all quantities for which data has been written out. 14 | :ivar color: Color assigned to the particle. 15 | :ivar n_particles: Number of existing particles for each timestep per mesh. 16 | :ivar lower_bounds: Dictionary with lower bounds for each timestep with quantities as keys. 17 | :ivar upper_bounds: Dictionary with upper bounds for each timestep with quantities as keys. 18 | """ 19 | 20 | def __init__(self, class_name: str, quantities: List[Quantity], color: Tuple[float, float, float]): 21 | self.class_name = class_name 22 | self.quantities = quantities 23 | self.color = color 24 | self.n_particles: Dict[str, List[int]] = dict() 25 | 26 | self._positions: List[np.ndarray] = list() 27 | self._tags: List[np.ndarray] = list() 28 | self._data: Dict[str, List[np.ndarray]] = {q.name: [] for q in self.quantities} 29 | self.times: Sequence[float] = list() 30 | 31 | self.lower_bounds = {q.name: [] for q in self.quantities} 32 | self.upper_bounds = {q.name: [] for q in self.quantities} 33 | 34 | self._init_callback = None 35 | 36 | @property 37 | def id(self): 38 | return self.class_name 39 | 40 | def filter_by_tag(self, tag: int): 41 | """Filter all particles by a single one with the specified tag. 42 | """ 43 | data = self.data 44 | tags = self.tags 45 | positions = self.positions 46 | part = Particle(self.class_name, self.quantities, self.color) 47 | part._tags = tag 48 | 49 | part._data = {quantity: list() for quantity in data.keys()} 50 | part._positions = list() 51 | part.times = list() 52 | 53 | for t, tags in enumerate(tags): 54 | if tag in tags: 55 | idx = np.where(tags == tag)[0] 56 | 57 | for quantity in data.keys(): 58 | part._data[quantity].append(data[quantity][t][idx][0]) 59 | part._positions.append(positions[t][idx][0]) 60 | part.times.append(self.times[t]) 61 | 62 | part.lower_bounds = dict() 63 | part.upper_bounds = dict() 64 | for q in self.quantities: 65 | if len(part._positions) != 0: 66 | part.lower_bounds[q.name] = np.min(part._data[q.name]) 67 | part.upper_bounds[q.name] = np.max(part._data[q.name]) 68 | else: 69 | part.lower_bounds[q.name] = 0 70 | part.upper_bounds[q.name] = 0 71 | 72 | return part 73 | 74 | @property 75 | def data(self) -> Dict[str, List[np.ndarray]]: 76 | """Dictionary with quantities as keys and a list with a numpy array for each timestep which 77 | contains data for each particle in that timestep. 78 | """ 79 | if len(self._positions) == 0 and len(self._tags) == 0: 80 | self._init_callback() 81 | return self._data 82 | 83 | @property 84 | def tags(self) -> List[np.ndarray]: 85 | """List with a numpy array for each timestep which contains a tag for each particle in that 86 | timestep. 87 | """ 88 | if len(self._positions) == 0 and len(self._tags) == 0: 89 | self._init_callback() 90 | return self._tags 91 | 92 | @property 93 | def positions(self) -> List[np.ndarray]: 94 | """List with a numpy array for each timestep which contains the position of each particle in 95 | that timestep. 96 | """ 97 | if len(self._positions) == 0 and len(self._tags) == 0: 98 | self._init_callback() 99 | return self._positions 100 | 101 | def clear_cache(self): 102 | """Remove all data from the internal cache that has been loaded so far to free memory. 103 | """ 104 | if len(self._positions) != 0: 105 | del self._positions 106 | self._positions = list() 107 | del self._tags 108 | self._tags = list() 109 | del self._data 110 | self._data = {q.name: [] for q in self.quantities} 111 | 112 | def __repr__(self): 113 | return f"Particle(name={self.class_name}, quantities={self.quantities})" 114 | -------------------------------------------------------------------------------- /fdsreader/part/particle_collection.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Dict, List 2 | import numpy as np 3 | 4 | from fdsreader.part import Particle 5 | from fdsreader.fds_classes import Mesh 6 | from fdsreader.utils.data import FDSDataCollection, Quantity 7 | import fdsreader.utils.fortran_data as fdtype 8 | from fdsreader import settings 9 | 10 | 11 | class ParticleCollection(FDSDataCollection): 12 | """Collection of :class:`Particle` objects. 13 | """ 14 | 15 | def __init__(self, times: Iterable[float], particles: Iterable[Particle]): 16 | super().__init__(particles) 17 | self.times = list(times) 18 | self._file_paths: Dict[str, str] = dict() 19 | for particle in particles: 20 | particle.times = times 21 | 22 | for particle in self: 23 | if settings.LAZY_LOAD: 24 | particle._init_callback = self._load_data 25 | else: 26 | self._load_data() 27 | 28 | @property 29 | def quantities(self) -> List[Quantity]: 30 | qs = set() 31 | for part in self: 32 | for q in part.quantities: 33 | qs.add(q) 34 | return list(qs) 35 | 36 | def _post_init(self): 37 | if not settings.LAZY_LOAD: 38 | self._load_data() 39 | 40 | def _load_data(self): 41 | """Function to read in all particle data for a simulation. 42 | """ 43 | particles = self 44 | pointer_location = {particle: [0] * len(self.times) for particle in particles} 45 | 46 | for particle in particles: 47 | if len(particle._positions) == 0 and len(particle._tags) == 0: 48 | for t in range(len(self.times)): 49 | size = 0 50 | for mesh in self._file_paths.keys(): 51 | size += particle.n_particles[mesh][t] 52 | for quantity in particle.quantities: 53 | particle._data[quantity.name].append(np.empty((size,), dtype=np.float32)) 54 | particle._positions.append(np.empty((size, 3), dtype=np.float32)) 55 | particle._tags.append(np.empty((size,), dtype=int)) 56 | 57 | for mesh, file_path in self._file_paths.items(): 58 | with open(file_path, 'rb') as infile: 59 | # Initial offset (ONE, fds version and number of particle classes) 60 | offset = 3 * fdtype.INT.itemsize 61 | # Number of quantities for each particle class (plus an INTEGER_ZERO) 62 | offset += fdtype.new((('i', 2),)).itemsize * len(particles) 63 | # 30-char long name and unit information for each quantity 64 | offset += fdtype.new((('c', 30),)).itemsize * 2 * sum( 65 | [len(particle.quantities) for particle in particles]) 66 | infile.seek(offset) 67 | 68 | for t in range(len(self.times)): 69 | # Skip time value 70 | infile.seek(fdtype.FLOAT.itemsize, 1) 71 | 72 | # Read data for each particle class 73 | for particle in particles: 74 | # Read number of particles in each class 75 | n_particles = fdtype.read(infile, fdtype.INT, 1)[0][0][0] 76 | offset = pointer_location[particle][t] 77 | # Read positions 78 | dtype_positions = fdtype.new((('f', 3 * n_particles),)) 79 | pos = fdtype.read(infile, dtype_positions, 1)[0][0] 80 | particle._positions[t][offset: offset + n_particles] = pos.reshape((n_particles, 3), 81 | order='F').astype(float) 82 | 83 | # Read tags 84 | dtype_tags = fdtype.new((('i', n_particles),)) 85 | particle._tags[t][offset: offset + n_particles] = fdtype.read(infile, dtype_tags, 1)[0][0] 86 | 87 | # Read actual quantity values 88 | if len(particle.quantities) > 0: 89 | dtype_data = fdtype.new( 90 | (('f', str((n_particles, len(particle.quantities)))),)) 91 | data_raw = fdtype.read(infile, dtype_data, 1)[0][0].reshape( 92 | (n_particles, len(particle.quantities)), order='F') 93 | 94 | for q, quantity in enumerate(particle.quantities): 95 | particle._data[quantity.name][t][ 96 | offset:offset + n_particles] = data_raw[:, q].astype(float) 97 | 98 | pointer_location[particle][t] += particle.n_particles[mesh][t] 99 | 100 | def __getitem__(self, key): 101 | if type(key) == int: 102 | return self._elements[key] 103 | for particle in self: 104 | if particle.class_name == key: 105 | return particle 106 | 107 | def __contains__(self, value): 108 | if value in self._elements: 109 | return True 110 | for particle in self: 111 | if particle.class_name == value: 112 | return True 113 | return False 114 | 115 | def __repr__(self): 116 | return "ParticleCollection(" + super(ParticleCollection, self).__repr__() + ")" 117 | -------------------------------------------------------------------------------- /fdsreader/pl3d/__init__.py: -------------------------------------------------------------------------------- 1 | from .pl3d import Plot3D 2 | 3 | from .plot3D_collection import Plot3DCollection 4 | -------------------------------------------------------------------------------- /fdsreader/pl3d/plot3D_collection.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Union, List 2 | import numpy as np 3 | 4 | from fdsreader.pl3d import Plot3D 5 | from fdsreader.utils.data import FDSDataCollection, Quantity 6 | 7 | 8 | class Plot3DCollection(FDSDataCollection): 9 | """Collection of :class:`Plot3D` objects. Offers extensive functionality for filtering and 10 | using plot3Ds as well as its subclasses such as :class:`SubPlot3D`. 11 | """ 12 | 13 | def __init__(self, *plot3ds: Iterable[Plot3D]): 14 | super().__init__(*plot3ds) 15 | 16 | @property 17 | def times(self) -> np.ndarray: 18 | return np.array(self._elements[0].times) 19 | 20 | @property 21 | def quantities(self) -> List[Quantity]: 22 | return [pl.quantity for pl in self._elements] 23 | 24 | def get_by_quantity(self, quantity: Union[str, Quantity]): 25 | """Filters all plot3d data by a specific quantity. 26 | """ 27 | if type(quantity) == Quantity: 28 | quantity = quantity.name 29 | return next(x for x in self._elements if 30 | x.quantity.name.lower() == quantity.lower() or x.quantity.short_name.lower() == quantity.lower()) 31 | 32 | def __repr__(self): 33 | return "Plot3DCollection(" + super(Plot3DCollection, self).__repr__() + ")" 34 | -------------------------------------------------------------------------------- /fdsreader/settings.py: -------------------------------------------------------------------------------- 1 | LAZY_LOAD = True 2 | 3 | ENABLE_CACHING = True 4 | 5 | DEBUG = False 6 | 7 | IGNORE_ERRORS = False 8 | 9 | # As the binary representation of raw data is compiler dependent, this information must be provided 10 | # by the user 11 | FORTRAN_DATA_TYPE_INTEGER = " 4-byte integer (little-endian) 12 | FORTRAN_DATA_TYPE_UINT8 = " 1-byte unsigned integer (little-endian) 13 | FORTRAN_DATA_TYPE_FLOAT = " 4-byte floating point (little-endian) 14 | FORTRAN_DATA_TYPE_CHAR = "S" # S -> 1-byte char 15 | FORTRAN_BACKWARD = True # Sets weather the blocks are ended with the size of the block 16 | -------------------------------------------------------------------------------- /fdsreader/slcf/__init__.py: -------------------------------------------------------------------------------- 1 | from .slice import Slice 2 | 3 | from .slice_collection import SliceCollection 4 | 5 | from .geomslice import GeomSlice 6 | 7 | from .geomslice_collection import GeomSliceCollection 8 | -------------------------------------------------------------------------------- /fdsreader/slcf/geomslice_collection.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, List, Union 2 | 3 | import numpy as np 4 | 5 | from fdsreader.slcf import GeomSlice 6 | from fdsreader.utils.data import FDSDataCollection, Quantity 7 | 8 | 9 | class GeomSliceCollection(FDSDataCollection): 10 | """Collection of :class:`GeomSlice` objects. Offers extensive functionality for filtering and 11 | using geomslices as well as its subclasses such as :class:`SubSlice`. 12 | """ 13 | 14 | def __init__(self, *geomslices: Iterable[GeomSlice]): 15 | super().__init__(*geomslices) 16 | 17 | @property 18 | def quantities(self) -> List[Quantity]: 19 | return list({slc.name for slc in self}) 20 | 21 | def filter_by_quantity(self, quantity: Union[str, Quantity]): 22 | """Filters all geomslices by a specific quantity. 23 | """ 24 | if type(quantity) == Quantity: 25 | quantity = quantity.name 26 | return GeomSliceCollection(x for x in self if x.quantity.name.lower() == quantity.lower() 27 | or x.quantity.short_name.lower() == quantity.lower()) 28 | 29 | def get_by_id(self, geomslice_id: str): 30 | """Get the geomslice with corresponding id if it exists. 31 | """ 32 | return next((slc for slc in self if slc.id == geomslice_id), None) 33 | 34 | def get_nearest(self, x: float = None, y: float = None, z: float = None) -> GeomSlice: 35 | """Filters the geomslice with the shortest distance to the given point. 36 | If there are multiple geomslices with the same distance, a random one will be selected. 37 | """ 38 | d_min = np.finfo(float).max 39 | geomslices_min = list() 40 | 41 | for slc in self: 42 | dx = max(slc.extent.x_start - x, 0, x - slc.extent.x_end) if x is not None else 0 43 | dy = max(slc.extent.y_start - y, 0, y - slc.extent.y_end) if y is not None else 0 44 | dz = max(slc.extent.z_start - z, 0, z - slc.extent.z_end) if z is not None else 0 45 | d = np.sqrt(dx * dx + dy * dy + dz * dz) 46 | if d <= d_min: 47 | d_min = d 48 | geomslices_min.append(slc) 49 | 50 | if x is not None: 51 | geomslices_min.sort(key=lambda slc: (slc.extent.x_end - slc.extent.x_start)) 52 | if y is not None: 53 | geomslices_min.sort(key=lambda slc: (slc.extent.y_end - slc.extent.y_start)) 54 | if z is not None: 55 | geomslices_min.sort(key=lambda slc: (slc.extent.z_end - slc.extent.z_start)) 56 | 57 | if len(geomslices_min) > 0: 58 | return geomslices_min[0] 59 | return None 60 | 61 | def __repr__(self): 62 | return "GeomSliceCollection(" + super(GeomSliceCollection, self).__repr__() + ")" 63 | -------------------------------------------------------------------------------- /fdsreader/slcf/slice_collection.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, List, Union 2 | 3 | import numpy as np 4 | 5 | from fdsreader.slcf import Slice 6 | from fdsreader.utils.data import FDSDataCollection, Quantity 7 | 8 | 9 | class SliceCollection(FDSDataCollection): 10 | """Collection of :class:`Slice` objects. Offers additional functionality for filtering and 11 | using slices as well as its subclasses such as :class:`SubSlice`. 12 | """ 13 | 14 | def __init__(self, *slices: Iterable[Slice]): 15 | super().__init__(*slices) 16 | 17 | @property 18 | def quantities(self) -> List[Quantity]: 19 | return list({slc.quantity.name for slc in self}) 20 | 21 | def filter_by_quantity(self, quantity: Union[str, Quantity]): 22 | """Filters all slices by a specific quantity. 23 | """ 24 | if type(quantity) == Quantity: 25 | quantity = quantity.name 26 | return SliceCollection(x for x in self if x.quantity.name.lower() == quantity.lower() 27 | or x.quantity.short_name.lower() == quantity.lower()) 28 | 29 | def get_by_id(self, slice_id: str): 30 | """Get the slice with corresponding id if it exists. 31 | """ 32 | return next((slc for slc in self if slc.id == slice_id), None) 33 | 34 | def get_nearest(self, x: float = None, y: float = None, z: float = None) -> Slice: 35 | """Filters the slice with the shortest distance to the given point. 36 | If there are multiple slices with the same distance, a random one will be selected. 37 | """ 38 | 39 | d_min = np.finfo(float).max 40 | slices_min = list() 41 | 42 | for slc in self: 43 | dx = max(slc.extent.x_start - x, 0, x - slc.extent.x_end) if x is not None else 0 44 | dy = max(slc.extent.y_start - y, 0, y - slc.extent.y_end) if y is not None else 0 45 | dz = max(slc.extent.z_start - z, 0, z - slc.extent.z_end) if z is not None else 0 46 | d = np.sqrt(dx * dx + dy * dy + dz * dz) 47 | if d <= d_min: 48 | d_min = d 49 | slices_min.append(slc) 50 | 51 | if x is not None: 52 | slices_min.sort(key=lambda slc: (slc.extent.x_end - slc.extent.x_start)) 53 | if y is not None: 54 | slices_min.sort(key=lambda slc: (slc.extent.y_end - slc.extent.y_start)) 55 | if z is not None: 56 | slices_min.sort(key=lambda slc: (slc.extent.z_end - slc.extent.z_start)) 57 | 58 | if len(slices_min) > 0: 59 | return slices_min[0] 60 | return None 61 | 62 | def __repr__(self): 63 | return "SliceCollection(" + super(SliceCollection, self).__repr__() + ")" 64 | -------------------------------------------------------------------------------- /fdsreader/smoke3d/__init__.py: -------------------------------------------------------------------------------- 1 | from .smoke3d import Smoke3D 2 | 3 | from .smoke3D_collection import Smoke3DCollection 4 | -------------------------------------------------------------------------------- /fdsreader/smoke3d/smoke3D_collection.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, List, Union 2 | 3 | from fdsreader.smoke3d import Smoke3D 4 | from fdsreader.utils.data import FDSDataCollection, Quantity 5 | 6 | 7 | class Smoke3DCollection(FDSDataCollection): 8 | """Collection of :class:`Smoke3D` objects. Offers extensive functionality for filtering and using Smoke3Ds as well 9 | as its subclasses such as :class:`SubSmoke3D`. 10 | """ 11 | 12 | def __init__(self, *smoke3ds: Iterable[Smoke3D]): 13 | super().__init__(*smoke3ds) 14 | 15 | @property 16 | def quantities(self) -> List[Quantity]: 17 | return [smoke3d.name for smoke3d in self._elements] 18 | 19 | def get_by_quantity(self, quantity: Union[Quantity, str]): 20 | """Gets the :class:`Smoke3D` with a specific quantity. 21 | """ 22 | if type(quantity) == Quantity: 23 | quantity = quantity.name 24 | return next(x for x in self._elements if 25 | x.quantity.name.lower() == quantity.lower() or x.quantity.short_name.lower() == quantity.lower()) 26 | 27 | def __repr__(self): 28 | return "Smoke3DCollection(" + super(Smoke3DCollection, self).__repr__() + ")" 29 | -------------------------------------------------------------------------------- /fdsreader/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .data import Quantity 2 | 3 | from fdsreader.utils.dimension import Dimension 4 | 5 | from fdsreader.utils.extent import Extent 6 | 7 | from.misc import * 8 | -------------------------------------------------------------------------------- /fdsreader/utils/data.py: -------------------------------------------------------------------------------- 1 | """ 2 | Collection of internal utilities (convenience functions and classes) for data handling. 3 | """ 4 | import glob 5 | import hashlib 6 | import os 7 | import numpy as np 8 | try: 9 | # Python <= 3.9 10 | from collections import Iterable 11 | except ImportError: 12 | # Python > 3.9 13 | from collections.abc import Iterable 14 | 15 | 16 | class Quantity: 17 | """Object containing information about a quantity with the corresponding short_name and unit. 18 | 19 | :ivar short_name: The short short_name representing the quantity. 20 | :ivar quantity: The name of the quantity. 21 | :ivar unit: The corresponding unit of the quantity. 22 | """ 23 | def __init__(self, quantity: str, short_name: str, unit: str): 24 | self.short_name = short_name 25 | self.unit = unit 26 | self.name = quantity 27 | 28 | def __eq__(self, other): 29 | return self.name == other.name and self.short_name == other.short_name and self.unit == other.unit 30 | 31 | @property 32 | def quantity(self): 33 | return self.name 34 | 35 | @property 36 | def label(self): 37 | return self.short_name 38 | 39 | def __hash__(self): 40 | return hash(self.short_name) 41 | 42 | def __repr__(self): 43 | return f"Quantity('{self.name}')" 44 | 45 | 46 | class Profile: 47 | """Class containing profile data. 48 | """ 49 | def __init__(self, profile_id: str, times: np.ndarray, npoints: np.ndarray, depths: np.ndarray, values: np.ndarray): 50 | self.id = profile_id 51 | self.times = times 52 | self.npoints = npoints 53 | self.depths = depths 54 | self.values = values 55 | 56 | def __eq__(self, other): 57 | return self.id == other.id 58 | 59 | def __repr__(self): 60 | return f"Profile(id='{self.id}', times={self.times}, depths={self.depths}, values={self.values})" 61 | 62 | 63 | def create_hash(path: str): 64 | """Returns the md5 hash as string for the given file. 65 | """ 66 | return str(hashlib.md5(open(path, 'rb').read()).hexdigest()) 67 | 68 | 69 | def scan_directory_smv(directory: str): 70 | """Scanning a directory non-recursively for smv-files. 71 | 72 | :param directory: The directory that will be scanned for smv files. 73 | :returns: A list containing the path to each smv-file found in the directory. 74 | """ 75 | return glob.glob(directory + "/**/*.smv", recursive=True) 76 | 77 | 78 | def get_smv_file(path: str): 79 | """Get the .smv file in a given directory. 80 | 81 | :param path: Either the path to the directory containing the simulation data or direct path 82 | to the .smv file for the simulation in case that multiple simulation output was written to 83 | the same directory. 84 | """ 85 | if os.path.isfile(path): 86 | return path 87 | elif os.path.isdir(path): 88 | files = scan_directory_smv(path) 89 | if len(files) > 1: 90 | raise IOError("There are multiple simulations in the directory: " + path) 91 | elif len(files) == 0: 92 | raise IOError("No simulations were found in the directory: " + path) 93 | return files[0] 94 | elif os.path.isfile(path + ".smv"): 95 | return path + ".smv" 96 | else: 97 | raise IOError("The given path does neither point to a directory nor a file: " + path) 98 | 99 | 100 | class FDSDataCollection: 101 | """(Abstract) Base class for any collection of FDS data. 102 | """ 103 | 104 | def __init__(self, *elements: Iterable): 105 | self._elements = tuple(*elements) 106 | 107 | def __getitem__(self, index): 108 | return self._elements[index] 109 | 110 | def __iter__(self): 111 | return self._elements.__iter__() 112 | 113 | def __len__(self): 114 | return len(self._elements) 115 | 116 | def __contains__(self, value): 117 | return value in self._elements 118 | 119 | def __repr__(self): 120 | return "[" + ",\n".join(str(e) for e in self._elements) + "]" 121 | 122 | def clear_cache(self): 123 | """Remove all data from the internal cache that has been loaded so far to free memory. 124 | """ 125 | for element in self._elements: 126 | element.clear_cache() 127 | -------------------------------------------------------------------------------- /fdsreader/utils/dimension.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | from operator import mul 3 | from typing import Tuple, List 4 | 5 | from typing_extensions import Literal 6 | 7 | 8 | class Dimension: 9 | """Three-dimensional index-based extent with support for a missing dimension (2D). 10 | 11 | :ivar x: Number of data points in x-direction (end is exclusive). 12 | :ivar y: Number of data points in y-direction (end is exclusive). 13 | :ivar z: Number of data points in z-direction (end is exclusive). 14 | """ 15 | 16 | def __init__(self, *args, skip_dimension: Literal['x', 1, 'y', 2, 'z', 3, ''] = ''): 17 | dimensions = list(args) 18 | 19 | if skip_dimension in ('x', 1): 20 | dimensions.insert(0, 1) 21 | elif skip_dimension in ('y', 2): 22 | dimensions.insert(1, 1) 23 | elif skip_dimension in ('z', 3): 24 | dimensions.append(1) 25 | 26 | self.x = dimensions[0] 27 | self.y = dimensions[1] 28 | self.z = dimensions[2] 29 | 30 | def __eq__(self, other): 31 | return self.x == other.x and self.y == other.y and self.z == other.z 32 | 33 | def __repr__(self, *args, **kwargs): 34 | return f"Dimension({self.x}, {self.y}, {self.z})" 35 | 36 | def __getitem__(self, dimension: Literal[0, 1, 2, 'x', 'y', 'z']): 37 | if type(dimension) == int: 38 | dimension = ('x', 'y', 'z')[dimension] 39 | return self.__dict__[dimension] 40 | 41 | def size(self, cell_centered=False): 42 | return reduce(mul, self.shape(cell_centered)) 43 | 44 | def shape(self, cell_centered=False) -> Tuple: 45 | """Method to get the actual number of data points per dimension. 46 | """ 47 | s = list() 48 | c = -1 if cell_centered else 0 49 | if self.x != 1: 50 | s.append(self.x + c) 51 | if self.y != 1: 52 | s.append(self.y + c) 53 | if self.z != 1: 54 | s.append(self.z + c) 55 | return tuple(s) 56 | 57 | def as_tuple(self, cell_centered=False, reduced=True) -> Tuple: 58 | """Gives the dimensions in tuple notation (optionally without empty dimensions). 59 | 60 | :param reduced: Whether to leave out empty dimensions (size of 1) or not. 61 | """ 62 | c = -1 if cell_centered else 0 63 | if reduced: 64 | if self.x == 1: 65 | return self.y + c, self.z + c 66 | elif self.y == 1: 67 | return self.x + c, self.z + c 68 | elif self.z == 1: 69 | return self.x + c, self.y + c 70 | return self.x + c, self.y + c, self.z + c 71 | 72 | def as_list(self, cell_centered=False, reduced=True) -> List: 73 | """Gives the dimension in list notation (without empty extents). 74 | 75 | :param reduced: Whether to leave out empty dimensions (size of 1) or not. 76 | """ 77 | return list(self.as_tuple(cell_centered, reduced)) 78 | -------------------------------------------------------------------------------- /fdsreader/utils/extent.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, List, Union 2 | 3 | from typing_extensions import Literal 4 | 5 | 6 | class Extent: 7 | """Three-dimensional value-based extent with support for a missing dimension (2D). 8 | """ 9 | 10 | def __init__(self, *args, skip_dimension: Literal['x', 1, 'y', 2, 'z', 3, ''] = ''): 11 | self._extents = list() 12 | 13 | if len(args) % 2 != 0: 14 | ValueError("An invalid number of arguments were passed to the constructor.") 15 | for i in range(0, len(args), 2): 16 | self._extents.append((float(args[i]), float(args[i + 1]))) 17 | 18 | if skip_dimension in ('x', 1): 19 | self._extents.insert(0, (0, 0)) 20 | elif skip_dimension in ('y', 2): 21 | self._extents.insert(1, (0, 0)) 22 | elif skip_dimension in ('z', 3): 23 | self._extents.append((0, 0)) 24 | 25 | def __eq__(self, other): 26 | return self._extents == other._extents 27 | 28 | def __repr__(self, *args, **kwargs): 29 | return "Extent([{:.2f}, {:.2f}] x [{:.2f}, {:.2f}] x [{:.2f}, {:.2f}])".format(self.x_start, self.x_end, 30 | self.y_start, self.y_end, 31 | self.z_start, self.z_end) 32 | 33 | def __getitem__(self, item: Union[Literal['x', 'y', 'z', 1, 2, 3]]): 34 | if item == 'x': 35 | return self.x_start, self.x_end 36 | elif item == 'y': 37 | return self.y_start, self.y_end 38 | elif item == 'z': 39 | return self.z_start, self.z_end 40 | return self._extents[item - 1] 41 | 42 | @property 43 | def x_start(self) -> float: 44 | """Gives the absolute extent in x-direction. 45 | """ 46 | return self._extents[0][0] 47 | 48 | @property 49 | def y_start(self) -> float: 50 | """Gives the absolute extent in y-direction. 51 | """ 52 | return self._extents[1][0] 53 | 54 | @property 55 | def z_start(self) -> float: 56 | """Gives the absolute extent in z-direction. 57 | """ 58 | return self._extents[2][0] 59 | 60 | @property 61 | def x_end(self) -> float: 62 | """Gives the absolute extent in x-direction. 63 | """ 64 | return self._extents[0][1] 65 | 66 | @property 67 | def y_end(self) -> float: 68 | """Gives the absolute extent in y-direction. 69 | """ 70 | return self._extents[1][1] 71 | 72 | @property 73 | def z_end(self) -> float: 74 | """Gives the absolute extent in z-direction. 75 | """ 76 | return self._extents[2][1] 77 | 78 | def as_tuple(self, reduced=True) -> Tuple[float, ...]: 79 | """Gives the extent in tuple notation (without empty extents). 80 | 81 | :param reduced: Whether to leave out empty extents or not. 82 | """ 83 | if reduced: 84 | if self.x_start == self.x_end: 85 | return self.y_start, self.y_end, self.z_start, self.z_end 86 | elif self.y_start == self.y_end: 87 | return self.x_start, self.x_end, self.z_start, self.z_end 88 | elif self.z_start == self.z_end: 89 | return self.x_start, self.x_end, self.y_start, self.y_end 90 | return self.x_start, self.x_end, self.y_start, self.y_end, self.z_start, self.z_end 91 | 92 | def as_list(self, reduced=True) -> List[float]: 93 | """Gives the extent in list notation (without empty extents). 94 | 95 | :param reduced: Whether to leave out empty extents or not. 96 | """ 97 | return list(self.as_tuple(reduced)) 98 | -------------------------------------------------------------------------------- /fdsreader/utils/fortran_data.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence, BinaryIO, Union, Tuple 2 | import numpy as np 3 | 4 | from fdsreader.settings import FORTRAN_DATA_TYPE_CHAR, FORTRAN_DATA_TYPE_FLOAT, FORTRAN_DATA_TYPE_INTEGER, \ 5 | FORTRAN_DATA_TYPE_UINT8, FORTRAN_BACKWARD 6 | 7 | _BASE_FORMAT = f"{FORTRAN_DATA_TYPE_INTEGER}, {{}}" + ( 8 | f", {FORTRAN_DATA_TYPE_INTEGER}" if FORTRAN_BACKWARD else "") 9 | 10 | _DATA_TYPES = {'i': FORTRAN_DATA_TYPE_INTEGER, 'f': FORTRAN_DATA_TYPE_FLOAT, 'c': FORTRAN_DATA_TYPE_CHAR, 11 | 'u': FORTRAN_DATA_TYPE_UINT8, '{}': "{}"} 12 | 13 | 14 | def _get_dtype_output_format(d, n): 15 | """Returns the correct output format needed to create a numpy dtype depending on input. 16 | """ 17 | if d == 'c': 18 | return str(n) 19 | if type(n) == int or type(n) == np.int32: 20 | return f"({n},)" 21 | return str(n) 22 | 23 | 24 | def new_raw(data_structure: Sequence[Tuple[str, Union[int, str]]]) -> str: 25 | """Creates the string definition for a fortran-compliant numpy dtype to read in binary fortran data. 26 | 27 | :param data_structure: Tuple consisting of tuples with 2 elements each where the first element 28 | is a char ('i', 'f', 'c' or '{}') representing the primitive data type to be used and the 29 | second element an integer representing the number of times this data type was written out in 30 | Fortran. 31 | :returns: The definition string for a fortran-compliant numpy dtype with the desired structure. 32 | """ 33 | return ", ".join([_get_dtype_output_format(d, n) + _DATA_TYPES[d] for d, n in data_structure]) 34 | 35 | 36 | def new(data_structure: Sequence[Tuple[str, Union[int, str]]]) -> np.dtype: 37 | """Creates a fortran-compliant numpy dtype to read in binary fortran data. 38 | 39 | :param data_structure: Tuple consisting of tuples with 2 elements each where the first element 40 | is a char ('i', 'f' or 'c') representing the primitive data type to be used and the second 41 | element an integer representing the number of times this data type was written out in Fortran. 42 | :returns: The newly created fortran-compliant numpy dtype with the desired structure. 43 | """ 44 | return np.dtype(_BASE_FORMAT.format(new_raw(data_structure))) 45 | 46 | 47 | def combine(*dtypes: np.dtype): 48 | """Combines multiple numpy dtypes into one. 49 | 50 | :param dtypes: An arbitrary amount of numpy dtype objects can be provided. 51 | :returns: The newly created numpy dtype. 52 | """ 53 | count = 0 54 | type_combination = list() 55 | for types in dtypes: 56 | for dtype in types.descr: 57 | type_combination.append(tuple(['f' + str(count)] + list(dtype[1:]))) 58 | count += 1 59 | return np.dtype(type_combination) 60 | 61 | 62 | # Commonly used datatypes 63 | CHAR = new((('c', 1),)) 64 | INT = new((('i', 1),)) 65 | FLOAT = new((('f', 1),)) 66 | # Border datatype to get the border of a fortran write 67 | PRE_BORDER = np.dtype(FORTRAN_DATA_TYPE_INTEGER) 68 | HAS_POST_BORDER = FORTRAN_BACKWARD 69 | 70 | 71 | def read(infile: BinaryIO, dtype: np.dtype, n: int): 72 | """Convenience function to read in binary data from a file using a numpy dtype. 73 | 74 | :param infile: Already opened binary IO stream. 75 | :param dtype: Numpy dtype object. 76 | :param n: The number of times a dtype object should be read in from the stream. 77 | :returns: Read in data. 78 | """ 79 | return np.array( 80 | [[t[i] for i in range(1, len(t), 3)] for t in np.fromfile(infile, dtype=dtype, count=n)], 81 | dtype=object) 82 | -------------------------------------------------------------------------------- /fdsreader/utils/misc.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from functools import wraps 3 | import sys 4 | from fdsreader import settings 5 | 6 | 7 | def log_error(module): 8 | def decorated(f): 9 | @wraps(f) 10 | def wrapped(*args, **kwargs): 11 | try: 12 | return f(*args, **kwargs) 13 | except Exception as e: 14 | if settings.DEBUG: 15 | raise e 16 | elif not settings.IGNORE_ERRORS: 17 | 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]) 18 | logging.warning(e) 19 | return wrapped 20 | return decorated 21 | -------------------------------------------------------------------------------- /paper.bib: -------------------------------------------------------------------------------- 1 | @misc{FDS, 2 | author = {Kevin McGrattan and Randall McDermott and Craig Weinschenk and Glenn Forney}, 3 | title = {Fire Dynamics Simulator Users Guide, Sixth Edition}, 4 | year = {2013}, 5 | month = {11}, 6 | publisher = {Special Publication (NIST SP), National Institute of Standards and Technology, Gaithersburg, MD}, 7 | doi = {10.6028/NIST.sp.1019}, 8 | language = {en}, 9 | } 10 | 11 | @misc{SMV, 12 | author = {Glenn Forney}, 13 | title = {Smokeview (Version 5) - A Tool for Visualizing Fire Dynamics Simulation Data, Volume I: User's Guide}, 14 | year = {2017}, 15 | month = {02}, 16 | publisher = {Special Publication (NIST SP), National Institute of Standards and Technology, Gaithersburg, MD}, 17 | language = {en}, 18 | } 19 | -------------------------------------------------------------------------------- /paper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'FDSReader' 3 | tags: 4 | - Python 5 | - TODO 6 | authors: 7 | - name: Jan Niklas Vogelsang 8 | orcid: 0000-0003-2336-6630 9 | affiliation: 1 10 | - name: Prof. Dr. Lukas Arnold 11 | orcid: 0000-0002-5939-8995 12 | affiliation: 2 13 | affiliations: 14 | - name: PGI-15, Forschungszentrum Jülich, Germany 15 | index: 1 16 | - name: IAS-7, Forschungszentrum Jülich, Germany 17 | index: 2 18 | date: 15 March 2024 19 | bibliography: paper.bib 20 | --- 21 | 22 | # Summary 23 | 24 | In recent years, Python has become the most popular programming language in the scientific community. It offers 25 | extensive functionality for preparing, post-processing and visualising data of all types and origins. By 26 | a large number of freely available packages, the programming environment can be flexibly adapted to your own 27 | needs. However, until now it was not possible to automatically import and process fire simulation data as 28 | produced by the fire dynamics simulator (FDS) [@FDS] for example. Using SmokeView [@SMV], it was 29 | already possible to visualize generated data, but it was not possible to process it directly or perform detailed 30 | analysis on the data itself. The introduction of the fdsreader made this possible in an uncomplicated and efficient way. 31 | 32 | # Statement of need 33 | 34 | Up until now, most working groups in both industry and research have had no solution for efficiently and 35 | automatically reading the binary data output by FDS to process it further. One would either have to use Fds2Ascii, a 36 | Fortran program to manually convert the binary output of FDS into a text file and read in the text file with their 37 | preferred language or write their own scripts to read the binary data output of FDS and further process the data. Many 38 | groups did end up with their own set of, typically Python-based, scripts to read the various types of data an FDS 39 | simulation can produce, e.g. slices, boundary data, etc. 40 | As the internal data structures of FDS are non-trivial and the source code is not trivial to understand for an outsider, 41 | the scripts usually require lots of time to write, ended up having strict limitations to the simulation parameters, 42 | e.g. only single mesh based simulations, and could not process all types of data. In addition, new versions of FDS 43 | often meant that some scripts no longer worked as expected, requiring even more time investment to maintain these scripts. 44 | The fdsreader Python package provides functionality to automatically import the full set of FDS outputs for both old and 45 | new versions of FDS. 46 | When first loading a simulation, all metadata is collected and cached to reduce subsequent load times. The actual 47 | data produced during the simulation such as the slice data for each time step, is not loaded until the data is accessed by the 48 | user via Python, which then triggers loading the data into memory in the background and converting it to equivalent 49 | Python data structures. This method minimizes initial loading time without sacrificing the ability to easily filter and 50 | select the desired data. The fdsreader collects all data of the same type into one collection and offers its own set 51 | functions for each type to easily select the data which should be further processed. These functions can even be called 52 | in an automated fashion, to run some predefined post-processing routines on simulation data without having to manually 53 | interact with the data, as one would have to do when using SmokeView or Fds2Ascii. 54 | The fdsreader is able to read all data types available in FDS including most of the additional metadata. To be concrete, 55 | the package contains modules for slices (slcf), boundary data (bndf), volumes (plot3d, smoke3d) particles (part), 56 | isosurfaces (isof), evacuations (evac), complex geometry boundary data (geom), devices (devc) and meshes. Slices, 57 | boundary data, isosurfaces and volumes are each respectively separated into multiple parts, one for each mesh. While FDS 58 | outputs the data separately for each mesh, the fdsreader provides methods to combine all these parts automatically and 59 | operate on data across meshes. 60 | 61 | # Acknowledgements 62 | TODO 63 | 64 | # References 65 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools", 4 | "wheel", 5 | "incremental>=24.7.2", # ← Add incremental as a build dependency 6 | ] 7 | build-backend = "setuptools.build_meta" 8 | 9 | [project] 10 | name = "fdsreader" 11 | dynamic = ["version"] 12 | description = "Python reader for data generated by FDS." 13 | readme = "README.md" 14 | requires-python = ">=3.6" 15 | license = {file = "LICENSE"} 16 | authors = [ 17 | {name = "FZJ IAS-7 (Jan Vogelsang, Prof. Dr. Lukas Arnold, Tristan Hehnen)", email = "j.vogelsang@fz-juelich.de"} 18 | ] 19 | classifiers=[ 20 | "Programming Language :: Python :: 3", 21 | "Operating System :: OS Independent", 22 | "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", 23 | ] 24 | dependencies = [ 25 | "incremental", 26 | "numpy", 27 | "typing_extensions" 28 | ] 29 | 30 | [project.urls] 31 | Repository = "https://github.com/FireDynamics/fdsreader" 32 | 33 | [project.optional-dependencies] 34 | dev = [ 35 | "pytest >=6.0" 36 | ] 37 | 38 | [tool.incremental] 39 | 40 | [tool.setuptools] 41 | 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"] 42 | 43 | [tool.pytest.ini_options] 44 | testpaths = ["tests"] 45 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.9.1 2 | aiosignal==1.3.1 3 | alabaster==0.7.13 4 | appdirs==1.4.4 5 | argcomplete==3.1.6 6 | argon2-cffi==23.1.0 7 | argon2-cffi-bindings==21.2.0 8 | asttokens==2.4.1 9 | async-timeout==4.0.3 10 | asynctest==0.13.0 11 | attrs==23.1.0 12 | autodocsumm==0.2.11 13 | Automat==22.10.0 14 | Babel==2.13.1 15 | backcall==0.2.0 16 | beautifulsoup4==4.12.2 17 | bleach==6.1.0 18 | bpyutils==0.5.8 19 | brotlipy==0.7.0 20 | certifi==2023.11.17 21 | cffi==1.16.0 22 | charset-normalizer==2.0.12 23 | click==8.1.7 24 | colorama==0.4.6 25 | comm==0.2.0 26 | constantly==23.10.4 27 | contourpy==1.2.0 28 | cryptography==41.0.7 29 | cycler==0.12.1 30 | debugpy==1.8.0 31 | decorator==5.1.1 32 | defusedxml==0.7.1 33 | dill==0.3.7 34 | docutils==0.20.1 35 | dpcpp-cpp-rt==2024.0.0 36 | entrypoints==0.4 37 | environment-kernels==1.2.0 38 | executing==2.0.1 39 | fastjsonschema==2.19.0 40 | fonttools==4.45.1 41 | frozenlist==1.4.0 42 | hyperlink==21.0.0 43 | idna==3.6 44 | imageio==2.33.0 45 | imageio-ffmpeg==0.4.9 46 | imagesize==1.4.1 47 | importlib-metadata==6.8.0 48 | importlib-resources==6.1.1 49 | incremental==22.10.0 50 | intel-cmplr-lib-rt==2024.0.0 51 | intel-cmplr-lic-rt==2024.0.0 52 | intel-opencl-rt==2024.0.0 53 | intel-openmp==2024.0.0 54 | ipykernel==6.27.1 55 | ipython==8.18.1 56 | ipython-genutils==0.2.0 57 | jaraco.classes==3.3.0 58 | jedi==0.19.1 59 | jeepney==0.8.0 60 | Jinja2==3.1.2 61 | jsonschema==4.20.0 62 | jsonschema-specifications==2023.11.1 63 | jupyter_client==8.6.0 64 | jupyter_core==5.5.0 65 | jupyterlab_pygments==0.3.0 66 | keyring==24.3.0 67 | kiwisolver==1.4.5 68 | markdown-it-py==3.0.0 69 | MarkupSafe==2.1.3 70 | matplotlib==3.8.2 71 | matplotlib-inline==0.1.6 72 | mdurl==0.1.2 73 | meshio==5.3.4 74 | mistune==3.0.2 75 | mkl==2024.0.0 76 | more-itertools==10.1.0 77 | multidict==6.0.4 78 | multiprocess==0.70.15 79 | nbclient==0.9.0 80 | nbconvert==7.11.0 81 | nbformat==5.9.2 82 | nbsphinx==0.9.3 83 | nest-asyncio==1.5.8 84 | nh3==0.2.14 85 | numpy==1.26.2 86 | olefile==0.46 87 | packaging==23.2 88 | pandocfilters==1.5.0 89 | parso==0.8.3 90 | pathos==0.3.1 91 | pexpect==4.9.0 92 | pickleshare==0.7.5 93 | Pillow==10.1.0 94 | pkginfo==1.9.6 95 | platformdirs==4.0.0 96 | pooch==1.8.0 97 | pox==0.3.3 98 | ppft==1.7.6.7 99 | prometheus-client==0.19.0 100 | prompt-toolkit==3.0.41 101 | psutil==5.9.6 102 | ptyprocess==0.7.0 103 | pure-eval==0.2.2 104 | pyasn1==0.5.1 105 | pyasn1-modules==0.3.0 106 | pycparser==2.21 107 | Pygments==2.17.2 108 | pyparsing==3.1.1 109 | PyQt5==5.15.10 110 | PyQt5-Qt5==5.15.2 111 | PyQt5-sip==12.13.0 112 | pyrsistent==0.20.0 113 | python-dateutil==2.8.2 114 | pytz==2023.3.post1 115 | pyvista==0.42.3 116 | pyvistaqt==0.11.0 117 | pywinpty==0.5.7 118 | PyYAML==6.0.1 119 | pyzmq==25.1.1 120 | QtPy==2.4.1 121 | readme-renderer==42.0 122 | referencing==0.31.0 123 | requests==2.31.0 124 | requests-toolbelt==1.0.0 125 | rfc3986==2.0.0 126 | rich==13.7.0 127 | rpds-py==0.13.1 128 | scooby==0.9.2 129 | SecretStorage==3.3.3 130 | setuptools-scm==8.0.4 131 | six==1.16.0 132 | snowballstemmer==2.2.0 133 | soupsieve==2.5 134 | Sphinx==7.2.6 135 | sphinx-rtd-theme==2.0.0 136 | sphinxcontrib-applehelp==1.0.7 137 | sphinxcontrib-devhelp==1.0.5 138 | sphinxcontrib-htmlhelp==2.0.4 139 | sphinxcontrib-jquery==4.1 140 | sphinxcontrib-jsmath==1.0.1 141 | sphinxcontrib-qthelp==1.0.6 142 | sphinxcontrib-serializinghtml==1.1.9 143 | stack-data==0.6.3 144 | tbb==2021.11.0 145 | terminado==0.18.0 146 | testpath==0.6.0 147 | tinycss2==1.2.1 148 | tomli==2.0.1 149 | tornado==6.4 150 | traitlets==5.14.0 151 | transforms3d==0.4.1 152 | twine==4.0.2 153 | Twisted==23.10.0 154 | typing_extensions==4.8.0 155 | urllib3==2.1.0 156 | vtk==9.3.0 157 | wcwidth==0.2.12 158 | webencodings==0.5.1 159 | wincertstore==0.2 160 | wslink==1.12.4 161 | yarl==1.9.3 162 | zipp==3.17.0 163 | zope.interface==6.1 164 | -------------------------------------------------------------------------------- /tests/acceptance_tests/test_bndf.py: -------------------------------------------------------------------------------- 1 | from fdsreader import Simulation 2 | 3 | 4 | def test_bndf(): 5 | sim = Simulation("./bndf_data") 6 | obst = sim.obstructions.get_nearest(-0.8, 1, 1) 7 | face = obst.get_global_boundary_data_arrays("Wall Temperature")[1] 8 | 9 | assert len(face[-1]) == 68 10 | -------------------------------------------------------------------------------- /tests/acceptance_tests/test_devc.py: -------------------------------------------------------------------------------- 1 | from fdsreader import Simulation 2 | 3 | 4 | def test_devc(): 5 | sim = Simulation("./steckler_data") 6 | assert abs(sim.devices["TC_Door"][0].data - 23.58) < 1e-6 7 | -------------------------------------------------------------------------------- /tests/acceptance_tests/test_geom.py: -------------------------------------------------------------------------------- 1 | from fdsreader import Simulation 2 | 3 | 4 | def test_geom(): 5 | sim = Simulation("./geom_data") 6 | geom = sim.geom_data.filter_by_quantity("Radiative Heat Flux")[0] 7 | 8 | assert len(geom.faces) == len(geom.data[-1]) == 19816 9 | assert len(geom.vertices) == 40624 10 | -------------------------------------------------------------------------------- /tests/acceptance_tests/test_isof.py: -------------------------------------------------------------------------------- 1 | from fdsreader import Simulation 2 | 3 | 4 | def test_isof(): 5 | sim = Simulation("./steckler_data") 6 | isosurface = sim.isosurfaces.filter_by_quantity("TEMP")[0] 7 | vertices, triangles, _ = isosurface.to_global(len(isosurface.times) - 1) 8 | 9 | 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 10 | 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 11 | -------------------------------------------------------------------------------- /tests/acceptance_tests/test_part.py: -------------------------------------------------------------------------------- 1 | from fdsreader import Simulation 2 | 3 | 4 | def test_part(): 5 | sim = Simulation("./part_data") 6 | particles = sim.particles["WATER PARTICLES"] 7 | position = particles.positions[-1][-1] 8 | color_data = particles.data["PARTICLE DIAMETER"] 9 | 10 | assert abs(position[0] + 0.04036335) < 1e-6 and abs(position[1] - 0.05389348) < 1e-6 and abs(position[2] - 13.596354) < 1e-6 11 | assert abs(color_data[-1][-1] - 2.2600818) < 1e-6 12 | -------------------------------------------------------------------------------- /tests/acceptance_tests/test_pl3d.py: -------------------------------------------------------------------------------- 1 | from fdsreader import Simulation 2 | 3 | 4 | def test_pl3d(): 5 | sim = Simulation("./pl3d_data") 6 | pl_t1 = sim.data_3d.get_by_quantity("Temperature") 7 | data, coordinates = pl_t1.to_global(masked=True, return_coordinates=True) 8 | 9 | assert abs(data[-1, 41, 27, 0] - 55.85966110229492) < 1e-6 10 | assert abs(coordinates['x'][41] - 9.25) < 1e-6 11 | -------------------------------------------------------------------------------- /tests/acceptance_tests/test_slcf.py: -------------------------------------------------------------------------------- 1 | from fdsreader import Simulation 2 | 3 | 4 | def test_slcf(): 5 | sim = Simulation("./steckler_data") 6 | data, coordinates = sim.slices[0].to_global(masked=True, return_coordinates=True) 7 | assert abs(data[-1, -1, -1] - 33.311744689941406) < 1e-6 8 | assert abs(coordinates['x'][0] - 0.) < 1e-6 and abs(coordinates['x'][-1] - 3.6) < 1e-6 9 | -------------------------------------------------------------------------------- /tests/acceptance_tests/test_smoke3d.py: -------------------------------------------------------------------------------- 1 | from fdsreader import Simulation 2 | 3 | 4 | def test_smoke3d(): 5 | sim = Simulation("./steckler_data") 6 | smoke = sim.smoke_3d.get_by_quantity("Temperature") 7 | data, coordinates = smoke.to_global(masked=True, return_coordinates=True) 8 | 9 | assert abs(data[-1, 13, 13, 1] - 77.) < 1e-6 10 | assert abs(coordinates['x'][13] - 1.3) < 1e-6 11 | -------------------------------------------------------------------------------- /tests/cases/bndf_data.tgz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f2000a5d9994efc8a7e9ca624d281d4542092e761ee9948f8cbcd4846d64d8c6 3 | size 1945025 4 | -------------------------------------------------------------------------------- /tests/cases/devc_data.tgz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:393ce55684c7a94cfa7dff49c7c5cb28e428d3d38f8a759c0c32452178f14801 3 | size 806007 4 | -------------------------------------------------------------------------------- /tests/cases/geom_data.tgz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a79ce8bd073418b84d628773d1bcef6ef5c4dcf9b7de1b6d2e0057a4291ed8a2 3 | size 1177213 4 | -------------------------------------------------------------------------------- /tests/cases/part_data.tgz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e330577e3eedadbf5816606eab25721ba3eb772bbcc02a3d26df308f9a17ea3e 3 | size 390399 4 | -------------------------------------------------------------------------------- /tests/cases/pl3d_data.tgz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d8f669c365ab6051a4ffa7b5262ee2062779c7953cef75f59a42871dbd2bfcdb 3 | size 1977899 4 | -------------------------------------------------------------------------------- /tests/cases/steckler_data.tgz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e03f1279106475da4eb9f24c974aa85e18fd577b9a0567228aacc01360ac0d62 3 | size 12519145 4 | -------------------------------------------------------------------------------- /tests/test.smv: -------------------------------------------------------------------------------- 1 | TITLE 2 | Test Simulation 3 | 4 | FDSVERSION 5 | FDS6.7.5-0-g71f025606-release 6 | 7 | REVISION 8 | Revisio 9 | 10 | CHID 11 | test 12 | 13 | SOLID_HT3D 14 | 0 15 | 16 | NMESHES 17 | 1 18 | 19 | VIEWTIMES 20 | 0.00 0.01 1000 21 | 22 | ALBEDO 23 | 0.30000 24 | 25 | IBLANK 26 | 1 27 | 28 | GVEC 29 | 0.00000 0.00000 0.00000 30 | 31 | SURFDEF 32 | INERT 33 | 34 | SURFACE 35 | INERT 36 | 5000.00 1.00 37 | 0 1.00000 1.00000 1.00000 0.80000 0.40000 1.00000 38 | null 39 | 40 | SURFACE 41 | OPEN 42 | 5000.00 1.00 43 | 2 1.00000 1.00000 1.00000 0.00000 1.00000 1.00000 44 | null 45 | 46 | SURFACE 47 | MIRROR 48 | 5000.00 1.00 49 | -2 1.00000 1.00000 1.00000 0.80000 0.40000 1.00000 50 | null 51 | 52 | SURFACE 53 | INTERPOLATED 54 | 5000.00 1.00 55 | 0 1.00000 1.00000 1.00000 0.80000 0.40000 1.00000 56 | null 57 | 58 | SURFACE 59 | PERIODIC 60 | 5000.00 1.00 61 | 0 1.00000 1.00000 1.00000 0.80000 0.40000 1.00000 62 | null 63 | 64 | SURFACE 65 | HVAC 66 | 5000.00 1.00 67 | 0 1.00000 1.00000 1.00000 0.80000 0.40000 1.00000 68 | null 69 | 70 | SURFACE 71 | MASSLESS TRACER 72 | 5000.00 1.00 73 | 0 1.00000 1.00000 1.00000 0.80000 0.40000 1.00000 74 | null 75 | 76 | SURFACE 77 | DROPLET 78 | 5000.00 1.00 79 | 0 1.00000 1.00000 1.00000 0.80000 0.40000 1.00000 80 | null 81 | 82 | SURFACE 83 | EVACUATION_OUTFLOW 84 | 5000.00 1.00 85 | 0 1.00000 1.00000 1.00000 0.80000 0.40000 1.00000 86 | null 87 | 88 | SURFACE 89 | MASSLESS TARGET 90 | 5000.00 1.00 91 | 0 1.00000 1.00000 1.00000 0.80000 0.40000 1.00000 92 | null 93 | 94 | SURFACE 95 | PERIODIC FLOW ONLY 96 | 5000.00 1.00 97 | 0 1.00000 1.00000 1.00000 0.80000 0.40000 1.00000 98 | null 99 | 100 | OUTLINE 101 | 12 102 | -0.1556 -0.0019 -0.1556 -0.1556 -0.0019 0.1556 103 | -0.1556 0.0019 -0.1556 -0.1556 0.0019 0.1556 104 | 0.1556 -0.0019 -0.1556 0.1556 -0.0019 0.1556 105 | 0.1556 0.0019 -0.1556 0.1556 0.0019 0.1556 106 | -0.1556 -0.0019 -0.1556 -0.1556 0.0019 -0.1556 107 | -0.1556 -0.0019 0.1556 -0.1556 0.0019 0.1556 108 | 0.1556 -0.0019 -0.1556 0.1556 0.0019 -0.1556 109 | 0.1556 -0.0019 0.1556 0.1556 0.0019 0.1556 110 | -0.1556 -0.0019 -0.1556 0.1556 -0.0019 -0.1556 111 | -0.1556 0.0019 -0.1556 0.1556 0.0019 -0.1556 112 | -0.1556 -0.0019 0.1556 0.1556 -0.0019 0.1556 113 | -0.1556 0.0019 0.1556 0.1556 0.0019 0.1556 114 | 115 | TOFFSET 116 | 0.00000 0.00000 0.00000 117 | 118 | HRRPUVCUT 119 | 1 120 | 200.00000 121 | 122 | RAMP 123 | 0 124 | 125 | PROP 126 | null 127 | 1 128 | sensor 129 | 0 130 | 131 | OFFSET 132 | 0.00000 0.00000 0.00000 133 | 134 | GRID mesh1 135 | 80 1 80 0 136 | 137 | PDIM 138 | -0.15560 0.15560 -0.00194 0.00194 -0.15560 0.15560 0.00000 0.00000 0.00000 139 | 140 | TRNX 141 | 0 142 | 0 -0.15560 143 | 1 -0.15171 144 | 2 -0.14782 145 | 3 -0.14393 146 | 4 -0.14004 147 | 5 -0.13615 148 | 6 -0.13226 149 | 7 -0.12837 150 | 8 -0.12448 151 | 9 -0.12059 152 | 10 -0.11670 153 | 11 -0.11281 154 | 12 -0.10892 155 | 13 -0.10503 156 | 14 -0.10114 157 | 15 -0.09725 158 | 16 -0.09336 159 | 17 -0.08947 160 | 18 -0.08558 161 | 19 -0.08169 162 | 20 -0.07780 163 | 21 -0.07391 164 | 22 -0.07002 165 | 23 -0.06613 166 | 24 -0.06224 167 | 25 -0.05835 168 | 26 -0.05446 169 | 27 -0.05057 170 | 28 -0.04668 171 | 29 -0.04279 172 | 30 -0.03890 173 | 31 -0.03501 174 | 32 -0.03112 175 | 33 -0.02723 176 | 34 -0.02334 177 | 35 -0.01945 178 | 36 -0.01556 179 | 37 -0.01167 180 | 38 -0.00778 181 | 39 -0.00389 182 | 40 0.00000 183 | 41 0.00389 184 | 42 0.00778 185 | 43 0.01167 186 | 44 0.01556 187 | 45 0.01945 188 | 46 0.02334 189 | 47 0.02723 190 | 48 0.03112 191 | 49 0.03501 192 | 50 0.03890 193 | 51 0.04279 194 | 52 0.04668 195 | 53 0.05057 196 | 54 0.05446 197 | 55 0.05835 198 | 56 0.06224 199 | 57 0.06613 200 | 58 0.07002 201 | 59 0.07391 202 | 60 0.07780 203 | 61 0.08169 204 | 62 0.08558 205 | 63 0.08947 206 | 64 0.09336 207 | 65 0.09725 208 | 66 0.10114 209 | 67 0.10503 210 | 68 0.10892 211 | 69 0.11281 212 | 70 0.11670 213 | 71 0.12059 214 | 72 0.12448 215 | 73 0.12837 216 | 74 0.13226 217 | 75 0.13615 218 | 76 0.14004 219 | 77 0.14393 220 | 78 0.14782 221 | 79 0.15171 222 | 80 0.15560 223 | 224 | TRNY 225 | 0 226 | 0 -0.00194 227 | 1 0.00194 228 | 229 | TRNZ 230 | 0 231 | 0 -0.15560 232 | 1 -0.15171 233 | 2 -0.14782 234 | 3 -0.14393 235 | 4 -0.14004 236 | 5 -0.13615 237 | 6 -0.13226 238 | 7 -0.12837 239 | 8 -0.12448 240 | 9 -0.12059 241 | 10 -0.11670 242 | 11 -0.11281 243 | 12 -0.10892 244 | 13 -0.10503 245 | 14 -0.10114 246 | 15 -0.09725 247 | 16 -0.09336 248 | 17 -0.08947 249 | 18 -0.08558 250 | 19 -0.08169 251 | 20 -0.07780 252 | 21 -0.07391 253 | 22 -0.07002 254 | 23 -0.06613 255 | 24 -0.06224 256 | 25 -0.05835 257 | 26 -0.05446 258 | 27 -0.05057 259 | 28 -0.04668 260 | 29 -0.04279 261 | 30 -0.03890 262 | 31 -0.03501 263 | 32 -0.03112 264 | 33 -0.02723 265 | 34 -0.02334 266 | 35 -0.01945 267 | 36 -0.01556 268 | 37 -0.01167 269 | 38 -0.00778 270 | 39 -0.00389 271 | 40 0.00000 272 | 41 0.00389 273 | 42 0.00778 274 | 43 0.01167 275 | 44 0.01556 276 | 45 0.01945 277 | 46 0.02334 278 | 47 0.02723 279 | 48 0.03112 280 | 49 0.03501 281 | 50 0.03890 282 | 51 0.04279 283 | 52 0.04668 284 | 53 0.05057 285 | 54 0.05446 286 | 55 0.05835 287 | 56 0.06224 288 | 57 0.06613 289 | 58 0.07002 290 | 59 0.07391 291 | 60 0.07780 292 | 61 0.08169 293 | 62 0.08558 294 | 63 0.08947 295 | 64 0.09336 296 | 65 0.09725 297 | 66 0.10114 298 | 67 0.10503 299 | 68 0.10892 300 | 69 0.11281 301 | 70 0.11670 302 | 71 0.12059 303 | 72 0.12448 304 | 73 0.12837 305 | 74 0.13226 306 | 75 0.13615 307 | 76 0.14004 308 | 77 0.14393 309 | 78 0.14782 310 | 79 0.15171 311 | 80 0.15560 312 | 313 | OBST 314 | 1 315 | 0.00000 0.10000 0.00000 0.00100 0.00000 0.10000 1 0 0 0 0 0 0 316 | 40 66 1 1 40 66 -1 -1 317 | 318 | VENT 319 | 6 0 320 | -0.15560 0.15560 -0.00194 -0.00194 -0.15560 0.15560 0 2 -999.00000 -999.00000 -999.00000 321 | -0.15560 0.15560 0.00194 0.00194 -0.15560 0.15560 0 2 -999.00000 -999.00000 -999.00000 322 | -0.15560 -0.15560 -0.00194 0.00194 -0.15560 0.15560 1 4 -999.00000 -999.00000 -999.00000 323 | 0.15560 0.15560 -0.00194 0.00194 -0.15560 0.15560 2 4 -999.00000 -999.00000 -999.00000 324 | -0.15560 0.15560 -0.00194 0.00194 -0.15560 -0.15560 3 4 -999.00000 -999.00000 -999.00000 325 | -0.15560 0.15560 -0.00194 0.00194 0.15560 0.15560 4 4 -999.00000 -999.00000 -999.00000 326 | 0 80 0 0 0 80 -99 -2 327 | 0 80 1 1 0 80 -99 -2 328 | 0 0 0 1 0 80 99 -2 329 | 80 80 0 1 0 80 99 -2 330 | 0 80 0 1 0 0 99 -2 331 | 0 80 0 1 80 80 99 -2 332 | 333 | CVENT 334 | 0 -------------------------------------------------------------------------------- /tests/test_version_compatibility.py: -------------------------------------------------------------------------------- 1 | """Basic tests to ensure version compatibility. 2 | """ 3 | 4 | import unittest 5 | 6 | from fdsreader import Simulation 7 | 8 | 9 | class SimTest(unittest.TestCase): 10 | def test_sim(self): 11 | sim = Simulation(".") 12 | self.assertEqual(sim.chid, "test") 13 | 14 | 15 | if __name__ == '__main__': 16 | unittest.main() 17 | --------------------------------------------------------------------------------