Spaces:
Sleeping
Sleeping
puppy2333
commited on
Commit
·
32e7816
1
Parent(s):
0e3c6b7
Web visualizer initial commit
Browse files- .gitignore +26 -0
- CONTRIBUTING.rst +8 -0
- LICENSE +29 -0
- README.rst +51 -0
- bundles/desktop/README.md +53 -0
- bundles/desktop/create_exe.bat +7 -0
- bundles/desktop/create_linux.sh +9 -0
- bundles/desktop/create_mac.sh +10 -0
- bundles/desktop/requirements.txt +2 -0
- bundles/desktop/run.py +7 -0
- bundles/docker/DEPLOY.md +58 -0
- bundles/docker/Dockerfile +11 -0
- bundles/docker/README.md +19 -0
- bundles/docker/captain-definition +4 -0
- bundles/docker/scripts/build_image.sh +6 -0
- bundles/docker/scripts/run_image.sh +2 -0
- bundles/docker/setup/apps.yml +4 -0
- bundles/docker/setup/initialize.sh +2 -0
- bundles/docker/setup/requirements.txt +2 -0
- pyproject.toml +99 -0
- src/wind_field_visualizer/__init__.py +1 -0
- src/wind_field_visualizer/app/__init__.py +5 -0
- src/wind_field_visualizer/app/main.py +242 -0
.gitignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.DS_Store
|
| 2 |
+
node_modules
|
| 3 |
+
.venv
|
| 4 |
+
|
| 5 |
+
# local env files
|
| 6 |
+
.env.local
|
| 7 |
+
.env.*.local
|
| 8 |
+
|
| 9 |
+
# Log files
|
| 10 |
+
npm-debug.log*
|
| 11 |
+
yarn-debug.log*
|
| 12 |
+
yarn-error.log*
|
| 13 |
+
pnpm-debug.log*
|
| 14 |
+
|
| 15 |
+
# Editor directories and files
|
| 16 |
+
.idea
|
| 17 |
+
.vscode
|
| 18 |
+
*.suo
|
| 19 |
+
*.ntvs*
|
| 20 |
+
*.njsproj
|
| 21 |
+
*.sln
|
| 22 |
+
*.sw?
|
| 23 |
+
|
| 24 |
+
__pycache__
|
| 25 |
+
*egg-info
|
| 26 |
+
*pyc
|
CONTRIBUTING.rst
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
=====================================
|
| 2 |
+
Contributing to Wind Field Visualizer
|
| 3 |
+
=====================================
|
| 4 |
+
|
| 5 |
+
#. Clone the repository using ``git clone``
|
| 6 |
+
#. Make changes to the code, and commit your changes to a separate branch
|
| 7 |
+
#. Create a fork of the repository on GitHub
|
| 8 |
+
#. Push your branch to your fork, and open a pull request
|
LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BSD 3-Clause
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025, Xuanchen Zhou
|
| 4 |
+
All rights reserved.
|
| 5 |
+
|
| 6 |
+
Redistribution and use in source and binary forms, with or without modification,
|
| 7 |
+
are permitted provided that the following conditions are met:
|
| 8 |
+
|
| 9 |
+
* Redistributions of source code must retain the above copyright notice, this
|
| 10 |
+
list of conditions and the following disclaimer.
|
| 11 |
+
|
| 12 |
+
* Redistributions in binary form must reproduce the above copyright notice, this
|
| 13 |
+
list of conditions and the following disclaimer in the documentation and/or
|
| 14 |
+
other materials provided with the distribution.
|
| 15 |
+
|
| 16 |
+
* Neither the name of the copyright holder nor the names of its
|
| 17 |
+
contributors may be used to endorse or promote products derived from this
|
| 18 |
+
software without specific prior written permission.
|
| 19 |
+
|
| 20 |
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
| 21 |
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
| 22 |
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
| 23 |
+
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
| 24 |
+
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
| 25 |
+
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
| 26 |
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
| 27 |
+
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
| 28 |
+
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
| 29 |
+
OF THE POSSIBILITY OF SUCH DAMAGE.
|
README.rst
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wind Field Visualizer
|
| 2 |
+
----------------------------------------
|
| 3 |
+
|
| 4 |
+
A web-based wind field visualizer
|
| 5 |
+
|
| 6 |
+
License
|
| 7 |
+
----------------------------------------
|
| 8 |
+
|
| 9 |
+
This library is OpenSource and follow the BSD License
|
| 10 |
+
|
| 11 |
+
Installation
|
| 12 |
+
----------------------------------------
|
| 13 |
+
|
| 14 |
+
Install the application/library
|
| 15 |
+
|
| 16 |
+
.. code-block:: console
|
| 17 |
+
|
| 18 |
+
pip install wind-field-visualizer
|
| 19 |
+
|
| 20 |
+
Run the application
|
| 21 |
+
|
| 22 |
+
.. code-block:: console
|
| 23 |
+
|
| 24 |
+
wind-field-visualizer
|
| 25 |
+
|
| 26 |
+
Developement setup
|
| 27 |
+
----------------------------------------
|
| 28 |
+
|
| 29 |
+
We recommend using uv for setting up and managing a virtual environement for your development.
|
| 30 |
+
|
| 31 |
+
.. code-block:: console
|
| 32 |
+
|
| 33 |
+
# Create venv and install all dependencies
|
| 34 |
+
uv sync --all-extras --dev
|
| 35 |
+
|
| 36 |
+
# Activate environement
|
| 37 |
+
source .venv/bin/activate
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# Allow live code edit
|
| 41 |
+
uv pip install -e .
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
Professional Support
|
| 47 |
+
----------------------------------------
|
| 48 |
+
|
| 49 |
+
* `Training <https://www.kitware.com/courses/trame/>`_: Learn how to confidently use trame from the expert developers at Kitware.
|
| 50 |
+
* `Support <https://www.kitware.com/trame/support/>`_: Our experts can assist your team as you build your web application and establish in-house expertise.
|
| 51 |
+
* `Custom Development <https://www.kitware.com/trame/support/>`_: Leverage Kitware’s 25+ years of experience to quickly build your web application.
|
bundles/desktop/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Desktop
|
| 2 |
+
|
| 3 |
+
This relies on [pyinstaller](https://pyinstaller.org/en/stable/) to bundle your trame application into a standalone desktop application.
|
| 4 |
+
|
| 5 |
+
In our current setup, we'll rely on the current virtual environment to provide the code content.
|
| 6 |
+
|
| 7 |
+
## Building the bundle
|
| 8 |
+
|
| 9 |
+
First, run `pip install -r requirements.txt`. This includes `pyinstaller` and `pywebview`, which is required for trame desktop applications.
|
| 10 |
+
|
| 11 |
+
Next, take a look at the `run.py` file. This should be just a regular python script that starts your trame desktop application. It can be tested via `python run.py`. Due to multiprocessing in some applications, `multiprocessing.freeze_support()` may be required to avoid an infinite recursion of running the script.
|
| 12 |
+
|
| 13 |
+
Once `python run.py` seems to be working properly, take a look at `create_exe.bat`. This is the pyinstaller command that we will use. Explanations for each option are provided:
|
| 14 |
+
|
| 15 |
+
1. `--hidden-import vtkmodules.all`: pyinstaller tries to find every dependency, but sometimes it is not able to. You can add additional python dependencies via the `--hidden-import` option, which may be specified multiple times.
|
| 16 |
+
|
| 17 |
+
For VTK applications, we can include all VTK modules, or to reduce the final binary size, only include the VTK modules that are actually used. The VTK hooks in pyinstaller should be updated in the future so that this is not necessary; see [pyinstaller/pyinstaller-hooks-contrib#327](https://github.com/pyinstaller/pyinstaller-hooks-contrib/issues/327) for details.
|
| 18 |
+
|
| 19 |
+
2. `--collect-data pywebvue`: normally, pyinstaller will only install python dependencies. For trame, however, we need some javascript files too. Adding this command will get pyinstaller to install the `pywebvue` javascript files as well.
|
| 20 |
+
|
| 21 |
+
3. `--onefile`: create a single executable for the output. `pyinstaller` by default creates a folder that contains the executable. The single executable is stand-alone and easy to move between computers. The only downside is that it takes a little longer to start and stop compared to the folder executable, because it must unpack itself before starting.
|
| 22 |
+
|
| 23 |
+
Note: if you are debugging your `pyinstaller` application, consider removing this option. It is much easier to see whether files/folders ended up in the right place in the folder executable mode.
|
| 24 |
+
|
| 25 |
+
4. `--windowed`: on Mac and Windows, this prevents the terminal/command prompt from being opened when the application is started.
|
| 26 |
+
|
| 27 |
+
5. `--icon wind-field-visualizer.ico`: the icon file to use for the application.
|
| 28 |
+
|
| 29 |
+
After running `pyinstaller`, the standalone application may be found inside the `dist` directory that is created.
|
| 30 |
+
|
| 31 |
+
## MacOS
|
| 32 |
+
|
| 33 |
+
```bash
|
| 34 |
+
pip install -r ./requirements.txt
|
| 35 |
+
./create_mac.sh
|
| 36 |
+
open ./dist/run.app
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
## Linux
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
pip install -r ./requirements.txt
|
| 43 |
+
./create_linux.sh
|
| 44 |
+
./dist/run
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
## Windows
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
pip install -r requirements.txt
|
| 51 |
+
./create_exe.bat
|
| 52 |
+
./dist/run/run.exe
|
| 53 |
+
```
|
bundles/desktop/create_exe.bat
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
python -m PyInstaller ^
|
| 2 |
+
--hidden-import pkgutil ^
|
| 3 |
+
--windowed ^
|
| 4 |
+
--collect-data trame_vuetify ^
|
| 5 |
+
--collect-data trame_vtk ^
|
| 6 |
+
--collect-data trame_client ^
|
| 7 |
+
.\run.py
|
bundles/desktop/create_linux.sh
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
python -m PyInstaller \
|
| 4 |
+
--clean --noconfirm \
|
| 5 |
+
--hidden-import pkgutil \
|
| 6 |
+
--collect-data trame_vuetify \
|
| 7 |
+
--collect-data trame_vtk \
|
| 8 |
+
--collect-data trame_client \
|
| 9 |
+
./run.py
|
bundles/desktop/create_mac.sh
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
python -m PyInstaller \
|
| 4 |
+
--clean --noconfirm \
|
| 5 |
+
--windowed \
|
| 6 |
+
--hidden-import pkgutil \
|
| 7 |
+
--collect-data trame_vuetify \
|
| 8 |
+
--collect-data trame_vtk \
|
| 9 |
+
--collect-data trame_client \
|
| 10 |
+
./run.py
|
bundles/desktop/requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
pyinstaller
|
| 2 |
+
pywebview
|
bundles/desktop/run.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import multiprocessing
|
| 2 |
+
|
| 3 |
+
from wind_field_visualizer.app.main import main
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
multiprocessing.freeze_support()
|
| 7 |
+
main(exec_mode="desktop")
|
bundles/docker/DEPLOY.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Trame deployment
|
| 2 |
+
|
| 3 |
+
Trame applications can be deployed following different patterns.
|
| 4 |
+
The one describe below is the simplest one and will only scale up to what the hardware is capable of handling.
|
| 5 |
+
For infinite scaling feel free to consult [Kitware](https://www.kitware.com/contact/) for more guidance.
|
| 6 |
+
|
| 7 |
+
## Docker
|
| 8 |
+
|
| 9 |
+
This directory provide the core infrastructure for building docker images that can then be deployed your own way.
|
| 10 |
+
But if you are looking for something more Heroku like we suggest using [CapRover](https://caprover.com/).
|
| 11 |
+
|
| 12 |
+
## Caprover
|
| 13 |
+
|
| 14 |
+
For that section we will assumed you've setup your own CapRover and you are just aiming to deploy your trame application using the caprover cli from npm.
|
| 15 |
+
|
| 16 |
+
Before any deployment, you need to create an application from the web interface and check the __Websocket Support__.
|
| 17 |
+
|
| 18 |
+
Then within the directory that contain this `DEPLOY.md`, you should run the following:
|
| 19 |
+
|
| 20 |
+
```bash
|
| 21 |
+
rm -rf ./server
|
| 22 |
+
./scripts/build_server.sh
|
| 23 |
+
tar -cvf trame-app.tar captain-definition Dockerfile server
|
| 24 |
+
caprover deploy -t trame-app.tar
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
It might be necessary to increase the nginx __client_max_body_size__ for your app upload.
|
| 28 |
+
To do that go in: __CapRover > Settings > NGINX Configurations > /etc/nginx/conf.d/captain-root.conf__
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
```json
|
| 32 |
+
# Captain dashboard at captain.captainroot.domain.com
|
| 33 |
+
server {
|
| 34 |
+
client_max_body_size 500m;
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
For GPU access you will need to create a `/etc/docker/daemon.json` with the following content where `GPU-f0ada461` come from the command line below
|
| 38 |
+
|
| 39 |
+
```bash
|
| 40 |
+
nvidia-smi -a | grep GPU-UUID
|
| 41 |
+
GPU UUID : GPU-f0ada461-5941-6211-dad3-a5003817fb59
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
=> `/etc/docker/daemon.json`
|
| 45 |
+
```json
|
| 46 |
+
{
|
| 47 |
+
"runtimes": {
|
| 48 |
+
"nvidia": {
|
| 49 |
+
"path": "/usr/bin/nvidia-container-runtime",
|
| 50 |
+
"runtimeArgs": []
|
| 51 |
+
}
|
| 52 |
+
},
|
| 53 |
+
"default-runtime": "nvidia",
|
| 54 |
+
"node-generic-resources": [
|
| 55 |
+
"NVIDIA-GPU=GPU-f0ada461"
|
| 56 |
+
]
|
| 57 |
+
}
|
| 58 |
+
```
|
bundles/docker/Dockerfile
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM kitware/trame:uv
|
| 2 |
+
|
| 3 |
+
# Set Python and Vue versions
|
| 4 |
+
ENV TRAME_PYTHON=3.13
|
| 5 |
+
ENV TRAME_CLIENT_TYPE=vue3
|
| 6 |
+
|
| 7 |
+
# Copy the local app and the deploy directory
|
| 8 |
+
COPY --chown=trame-user:trame-user . /local-app
|
| 9 |
+
COPY --chown=trame-user:trame-user ./bundles/docker /deploy
|
| 10 |
+
|
| 11 |
+
RUN /opt/trame/entrypoint.sh build
|
bundles/docker/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Docker Bundle
|
| 2 |
+
|
| 3 |
+
Docker bundle aims to provide an easy step to bundle and deploy your application into a Docker image that can easily be deployed in the cloud as a service and will naturally support multi-users.
|
| 4 |
+
|
| 5 |
+
## Building the server directory
|
| 6 |
+
|
| 7 |
+
The **server** directory capture all the key elements of your application and can be used with our generic docker image.
|
| 8 |
+
|
| 9 |
+
To generate that directory, just run the **scripts/build_server.sh** script.
|
| 10 |
+
|
| 11 |
+
## Building your docker image
|
| 12 |
+
|
| 13 |
+
Your docker image is simply bundling the **server** directory into a docker image to simplify possible deployment.
|
| 14 |
+
|
| 15 |
+
To generate that image, just run the **scripts/build_image.sh** script.
|
| 16 |
+
|
| 17 |
+
## Running your bundle
|
| 18 |
+
|
| 19 |
+
Just run either `scripts/run_server.sh` or `scripts/run_image.sh` and open your browser to `http://localhost:8080/wind-field-visualizer.html`
|
bundles/docker/captain-definition
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schemaVersion": 2,
|
| 3 |
+
"dockerfilePath": "./Dockerfile"
|
| 4 |
+
}
|
bundles/docker/scripts/build_image.sh
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
CURRENT_DIR=`dirname "$0"`
|
| 3 |
+
|
| 4 |
+
cd $CURRENT_DIR/../../..
|
| 5 |
+
|
| 6 |
+
docker build -t wind-field-visualizer -f ./bundles/docker/Dockerfile .
|
bundles/docker/scripts/run_image.sh
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
docker run -it --rm -p 8080:80 wind-field-visualizer
|
bundles/docker/setup/apps.yml
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
trame: # Default app under /index.html
|
| 2 |
+
app: wind-field-visualizer
|
| 3 |
+
wind-field-visualizer: # /wind-field-visualizer.html
|
| 4 |
+
app: wind-field-visualizer
|
bundles/docker/setup/initialize.sh
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Install app from local directory
|
| 2 |
+
uv pip install /local-app
|
bundles/docker/setup/requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use published version instead of local-app from initialize.sh
|
| 2 |
+
# wind-field-visualizer
|
pyproject.toml
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "wind-field-visualizer"
|
| 3 |
+
version = "1.0.0"
|
| 4 |
+
description = "A web-based wind field visualizer"
|
| 5 |
+
authors = [
|
| 6 |
+
{name = "Xuanchen Zhou"},
|
| 7 |
+
]
|
| 8 |
+
dependencies = [
|
| 9 |
+
"trame>=3.12",
|
| 10 |
+
"trame-vuetify",
|
| 11 |
+
"trame-vtk",
|
| 12 |
+
]
|
| 13 |
+
requires-python = ">=3.9"
|
| 14 |
+
readme = "README.rst"
|
| 15 |
+
license = {text = "BSD License"}
|
| 16 |
+
keywords = ["Python", "Interactive", "Web", "Application", "Framework"]
|
| 17 |
+
classifiers = [
|
| 18 |
+
"Development Status :: 4 - Beta",
|
| 19 |
+
"Environment :: Web Environment",
|
| 20 |
+
"License :: OSI Approved :: BSD License",
|
| 21 |
+
"Natural Language :: English",
|
| 22 |
+
"Operating System :: OS Independent",
|
| 23 |
+
"Programming Language :: Python :: 3 :: Only",
|
| 24 |
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
| 25 |
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
[project.optional-dependencies]
|
| 29 |
+
app = [
|
| 30 |
+
"pywebview",
|
| 31 |
+
]
|
| 32 |
+
jupyter = [
|
| 33 |
+
"jupyterlab",
|
| 34 |
+
]
|
| 35 |
+
dev = [
|
| 36 |
+
"pre-commit",
|
| 37 |
+
"ruff",
|
| 38 |
+
"pytest >=6",
|
| 39 |
+
"pytest-cov >=3",
|
| 40 |
+
"nox",
|
| 41 |
+
]
|
| 42 |
+
|
| 43 |
+
[project.scripts]
|
| 44 |
+
wind-field-visualizer = "wind_field_visualizer.app:main"
|
| 45 |
+
|
| 46 |
+
[build-system]
|
| 47 |
+
requires = ["hatchling"]
|
| 48 |
+
build-backend = "hatchling.build"
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
[tool.hatch.build]
|
| 52 |
+
include = [
|
| 53 |
+
"/src/wind_field_visualizer/**/*.py",
|
| 54 |
+
"/src/wind_field_visualizer/**/*.js",
|
| 55 |
+
"/src/wind_field_visualizer/**/*.css",
|
| 56 |
+
]
|
| 57 |
+
|
| 58 |
+
[tool.hatch.build.targets.wheel]
|
| 59 |
+
packages = [
|
| 60 |
+
"src/wind_field_visualizer",
|
| 61 |
+
]
|
| 62 |
+
|
| 63 |
+
[tool.ruff]
|
| 64 |
+
|
| 65 |
+
[tool.ruff.lint]
|
| 66 |
+
extend-select = [
|
| 67 |
+
"ARG", # flake8-unused-arguments
|
| 68 |
+
"B", # flake8-bugbear
|
| 69 |
+
"C4", # flake8-comprehensions
|
| 70 |
+
"EM", # flake8-errmsg
|
| 71 |
+
"EXE", # flake8-executable
|
| 72 |
+
"G", # flake8-logging-format
|
| 73 |
+
"I", # isort
|
| 74 |
+
"ICN", # flake8-import-conventions
|
| 75 |
+
"NPY", # NumPy specific rules
|
| 76 |
+
"PD", # pandas-vet
|
| 77 |
+
"PGH", # pygrep-hooks
|
| 78 |
+
"PIE", # flake8-pie
|
| 79 |
+
"PL", # pylint
|
| 80 |
+
"PT", # flake8-pytest-style
|
| 81 |
+
"PTH", # flake8-use-pathlib
|
| 82 |
+
"RET", # flake8-return
|
| 83 |
+
"RUF", # Ruff-specific
|
| 84 |
+
"SIM", # flake8-simplify
|
| 85 |
+
"T20", # flake8-print
|
| 86 |
+
"UP", # pyupgrade
|
| 87 |
+
"YTT", # flake8-2020
|
| 88 |
+
]
|
| 89 |
+
ignore = [
|
| 90 |
+
"PLR09", # Too many <...>
|
| 91 |
+
"PLR2004", # Magic value used in comparison
|
| 92 |
+
"ISC001", # Conflicts with formatter
|
| 93 |
+
]
|
| 94 |
+
isort.required-imports = []
|
| 95 |
+
|
| 96 |
+
[tool.ruff.lint.per-file-ignores]
|
| 97 |
+
"tests/**" = ["T20"]
|
| 98 |
+
"noxfile.py" = ["T20"]
|
| 99 |
+
"src/**" = ["SIM117"]
|
src/wind_field_visualizer/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
__version__ = "1.0.0"
|
src/wind_field_visualizer/app/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .main import main
|
| 2 |
+
|
| 3 |
+
__all__ = [
|
| 4 |
+
"main",
|
| 5 |
+
]
|
src/wind_field_visualizer/app/main.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from trame.app import get_server
|
| 3 |
+
from trame.ui.vuetify import SinglePageLayout
|
| 4 |
+
from trame.widgets import vuetify
|
| 5 |
+
from trame.widgets import vtk as vtk_widget
|
| 6 |
+
import vtk
|
| 7 |
+
import xarray as xr
|
| 8 |
+
import pyvista as pv
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
import matplotlib.pyplot as plt
|
| 12 |
+
from vtkmodules.vtkCommonCore import vtkLookupTable
|
| 13 |
+
|
| 14 |
+
from vtkmodules.vtkCommonColor import vtkNamedColors
|
| 15 |
+
from vtkmodules.vtkCommonDataModel import vtkPlane
|
| 16 |
+
from vtkmodules.vtkFiltersCore import vtkCutter
|
| 17 |
+
from vtkmodules.vtkFiltersModeling import vtkOutlineFilter
|
| 18 |
+
from vtkmodules.vtkRenderingCore import (
|
| 19 |
+
vtkActor,
|
| 20 |
+
vtkPolyDataMapper,
|
| 21 |
+
vtkRenderer,
|
| 22 |
+
vtkRenderWindow,
|
| 23 |
+
vtkRenderWindowInteractor,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
def main(server=None, **kwargs):
|
| 27 |
+
CURRENT_DIRECTORY = os.path.abspath(os.path.dirname(__file__))
|
| 28 |
+
|
| 29 |
+
# -----------------------------------------------------------------------------
|
| 30 |
+
# NetCDF to VTK
|
| 31 |
+
# -----------------------------------------------------------------------------
|
| 32 |
+
ds = xr.open_dataset(os.path.join(CURRENT_DIRECTORY, "../../../../../Data/Field_20251119_140509_BuildingGroupFloor.nc"))
|
| 33 |
+
|
| 34 |
+
# ----- Load fluid field -----
|
| 35 |
+
# Vtk uses right-handed coordinate system, while unity uses left-handed coordinate system. So the
|
| 36 |
+
# z asix needs to be reversed here.
|
| 37 |
+
velFieldRaw = ds["velField"][:, ::-1, :, :]
|
| 38 |
+
nTime, nz, ny, nx, _ = velFieldRaw.shape
|
| 39 |
+
print(f"Fluid field resolution: {nx=}, {ny=}, {nz=}")
|
| 40 |
+
|
| 41 |
+
# ----- Load dx -----
|
| 42 |
+
velXCoor = ds["velX"]
|
| 43 |
+
dx = float(velXCoor.values[1] - velXCoor.values[0])
|
| 44 |
+
print(f"{dx=}")
|
| 45 |
+
|
| 46 |
+
# ----- Load dt -----
|
| 47 |
+
timeList = ds["time"]
|
| 48 |
+
dt = float(timeList.values[1] - timeList.values[0])
|
| 49 |
+
dt = round(dt, 2)
|
| 50 |
+
print(f"{dt=}")
|
| 51 |
+
|
| 52 |
+
# ----- Fluid field at the first stored time step -----
|
| 53 |
+
velField0 = velFieldRaw[0, :, :, :, :3]
|
| 54 |
+
velFieldMag0 = np.linalg.norm(velField0.values, axis=-1).ravel()
|
| 55 |
+
|
| 56 |
+
# ----- Build vtk data -----
|
| 57 |
+
grid = pv.ImageData()
|
| 58 |
+
grid.dimensions = (nx, ny, nz)
|
| 59 |
+
grid.origin = (-(nx-1) * dx / 2, -(ny-1) * dx / 2 + 10, -(nz-1) * dx / 2)
|
| 60 |
+
grid.spacing = (dx, dx, dx)
|
| 61 |
+
grid.point_data["vel_mag"] = velFieldMag0
|
| 62 |
+
|
| 63 |
+
vtk_producer = vtk.vtkTrivialProducer()
|
| 64 |
+
vtk_producer.SetOutput(grid)
|
| 65 |
+
|
| 66 |
+
# -----------------------------------------------------------------------------
|
| 67 |
+
# PLY Reader (for building model)
|
| 68 |
+
# -----------------------------------------------------------------------------
|
| 69 |
+
|
| 70 |
+
modelReader = vtk.vtkPLYReader()
|
| 71 |
+
# modelReader.SetFileName("Data/SmallBuilding3.ply")
|
| 72 |
+
modelReader.SetFileName(os.path.join(CURRENT_DIRECTORY, "../../../../Data/Buildings-GroupAll.ply"))
|
| 73 |
+
modelReader.Update()
|
| 74 |
+
|
| 75 |
+
modelMapper = vtk.vtkPolyDataMapper()
|
| 76 |
+
modelMapper.SetInputConnection(modelReader.GetOutputPort())
|
| 77 |
+
|
| 78 |
+
modelActor = vtk.vtkActor()
|
| 79 |
+
modelActor.SetMapper(modelMapper)
|
| 80 |
+
|
| 81 |
+
transform = vtk.vtkTransform()
|
| 82 |
+
|
| 83 |
+
transform.PostMultiply()
|
| 84 |
+
|
| 85 |
+
# Small building 3
|
| 86 |
+
# transform.RotateX(-90.0)
|
| 87 |
+
# transform.RotateY(50.0)
|
| 88 |
+
|
| 89 |
+
# Building Group
|
| 90 |
+
transform.Translate(25, 0.2, -12)
|
| 91 |
+
transform.RotateX(180.0)
|
| 92 |
+
transform.RotateY(180.0)
|
| 93 |
+
|
| 94 |
+
modelActor.SetUserTransform(transform)
|
| 95 |
+
|
| 96 |
+
# -----------------------------------------------------------------------------
|
| 97 |
+
# Render VTK fluid field
|
| 98 |
+
# -----------------------------------------------------------------------------
|
| 99 |
+
|
| 100 |
+
renderer = vtkRenderer()
|
| 101 |
+
renderWindow = vtkRenderWindow()
|
| 102 |
+
renderWindow.AddRenderer(renderer)
|
| 103 |
+
|
| 104 |
+
renderWindowInteractor = vtkRenderWindowInteractor()
|
| 105 |
+
renderWindowInteractor.SetRenderWindow(renderWindow)
|
| 106 |
+
renderWindowInteractor.GetInteractorStyle().SetCurrentStyleToTrackballCamera()
|
| 107 |
+
|
| 108 |
+
# ----- Render outline -----
|
| 109 |
+
|
| 110 |
+
colors = vtkNamedColors()
|
| 111 |
+
|
| 112 |
+
outline = vtkOutlineFilter()
|
| 113 |
+
outline.SetInputConnection(vtk_producer.GetOutputPort())
|
| 114 |
+
|
| 115 |
+
outlineMapper = vtkPolyDataMapper()
|
| 116 |
+
outlineMapper.SetInputConnection(outline.GetOutputPort())
|
| 117 |
+
|
| 118 |
+
outlineActor = vtkActor()
|
| 119 |
+
outlineActor.SetMapper(outlineMapper)
|
| 120 |
+
outlineActor.GetProperty().SetColor(colors.GetColor3d("White"))
|
| 121 |
+
|
| 122 |
+
# ----- Colormap -----
|
| 123 |
+
def create_vtk_lut_from_matplotlib(cmap_name, num_colors=256):
|
| 124 |
+
mpl_cmap = plt.get_cmap(cmap_name)
|
| 125 |
+
|
| 126 |
+
lut = vtkLookupTable()
|
| 127 |
+
lut.SetNumberOfTableValues(num_colors)
|
| 128 |
+
|
| 129 |
+
for i in range(num_colors):
|
| 130 |
+
rgba = mpl_cmap(i / (num_colors - 1))
|
| 131 |
+
lut.SetTableValue(i, rgba[0], rgba[1], rgba[2], 1.0)
|
| 132 |
+
|
| 133 |
+
lut.SetRange(0, 15)
|
| 134 |
+
lut.Build()
|
| 135 |
+
return lut
|
| 136 |
+
|
| 137 |
+
turbo_lut = create_vtk_lut_from_matplotlib("turbo")
|
| 138 |
+
|
| 139 |
+
# ----- Render a slice -----
|
| 140 |
+
center_x = grid.center[0]
|
| 141 |
+
center_y = grid.center[1]
|
| 142 |
+
center_z = grid.center[2]
|
| 143 |
+
plane = vtkPlane()
|
| 144 |
+
plane.SetOrigin(center_x, center_y, center_z)
|
| 145 |
+
plane.SetNormal(0, 1, 0)
|
| 146 |
+
print(f"{grid.center[1]=}")
|
| 147 |
+
|
| 148 |
+
cutter = vtkCutter()
|
| 149 |
+
cutter.SetInputConnection(vtk_producer.GetOutputPort())
|
| 150 |
+
cutter.SetCutFunction(plane)
|
| 151 |
+
cutter.Update()
|
| 152 |
+
|
| 153 |
+
slice_mapper = vtkPolyDataMapper()
|
| 154 |
+
slice_mapper.SetInputConnection(cutter.GetOutputPort())
|
| 155 |
+
slice_mapper.SetScalarModeToUsePointFieldData()
|
| 156 |
+
slice_mapper.SelectColorArray("vel_mag")
|
| 157 |
+
slice_mapper.SetLookupTable(turbo_lut)
|
| 158 |
+
slice_mapper.SetScalarRange(0, 15)
|
| 159 |
+
|
| 160 |
+
slice_actor = vtkActor()
|
| 161 |
+
slice_actor.SetMapper(slice_mapper)
|
| 162 |
+
|
| 163 |
+
# Add the actors to the renderer
|
| 164 |
+
renderer.AddActor(outlineActor)
|
| 165 |
+
renderer.AddActor(slice_actor)
|
| 166 |
+
renderer.AddActor(modelActor)
|
| 167 |
+
renderer.ResetCamera()
|
| 168 |
+
renderWindow.Render()
|
| 169 |
+
|
| 170 |
+
# -----------------------------------------------------------------------------
|
| 171 |
+
# GUI
|
| 172 |
+
# -----------------------------------------------------------------------------
|
| 173 |
+
|
| 174 |
+
server = get_server(client_type = "vue2")
|
| 175 |
+
ctrl = server.controller
|
| 176 |
+
state = server.state
|
| 177 |
+
|
| 178 |
+
@state.change("time")
|
| 179 |
+
def update_time(time, **kwargs):
|
| 180 |
+
time_index = round(time / dt) - 1
|
| 181 |
+
newVelField = velFieldRaw[time_index, :, :, :, :3]
|
| 182 |
+
newVelMagFlat = np.linalg.norm(newVelField.values, axis=-1).ravel()
|
| 183 |
+
grid.point_data["vel_mag"] = newVelMagFlat
|
| 184 |
+
grid.Modified()
|
| 185 |
+
ctrl.view_update()
|
| 186 |
+
|
| 187 |
+
@state.change("ypos")
|
| 188 |
+
def update_ypos(ypos, **kwargs):
|
| 189 |
+
plane.SetOrigin(center_x, ypos, center_z)
|
| 190 |
+
ctrl.view_update()
|
| 191 |
+
|
| 192 |
+
def reset_ypos():
|
| 193 |
+
state.ypos = center_y
|
| 194 |
+
|
| 195 |
+
with SinglePageLayout(server) as layout:
|
| 196 |
+
layout.title.set_text("Fluid Field Visualizer")
|
| 197 |
+
|
| 198 |
+
with layout.toolbar:
|
| 199 |
+
vuetify.VSpacer()
|
| 200 |
+
|
| 201 |
+
# Time slider
|
| 202 |
+
vuetify.VSlider(
|
| 203 |
+
label="Time (s)",
|
| 204 |
+
v_model=("time", 0),
|
| 205 |
+
min=dt,
|
| 206 |
+
max=nTime * dt,
|
| 207 |
+
step=dt,
|
| 208 |
+
thumb_label=True,
|
| 209 |
+
hide_details=True,
|
| 210 |
+
dense=True,
|
| 211 |
+
style="max-width: 300px; margin-top: 40px; margin-bottom: 10px;",
|
| 212 |
+
)
|
| 213 |
+
vuetify.VDivider(vertical=True, classes="mx-2")
|
| 214 |
+
|
| 215 |
+
# Slice position silder
|
| 216 |
+
vuetify.VSlider(
|
| 217 |
+
label="Y Position (m)",
|
| 218 |
+
v_model=("ypos", center_y),
|
| 219 |
+
min=grid.bounds[2] + dx,
|
| 220 |
+
max=grid.bounds[3] - dx,
|
| 221 |
+
step=dx,
|
| 222 |
+
thumb_label=True,
|
| 223 |
+
hide_details=True,
|
| 224 |
+
dense=True,
|
| 225 |
+
style="max-width: 300px; margin-top: 40px; margin-bottom: 10px;",
|
| 226 |
+
)
|
| 227 |
+
with vuetify.VBtn(icon=True, click=reset_ypos):
|
| 228 |
+
vuetify.VIcon("mdi-restore")
|
| 229 |
+
vuetify.VDivider(vertical=True, classes="mx-2")
|
| 230 |
+
|
| 231 |
+
with layout.content:
|
| 232 |
+
with vuetify.VContainer(
|
| 233 |
+
fluid=True,
|
| 234 |
+
classes="pa-0 fill-height",
|
| 235 |
+
):
|
| 236 |
+
view = vtk_widget.VtkLocalView(renderWindow)
|
| 237 |
+
ctrl.view_update = view.update
|
| 238 |
+
|
| 239 |
+
server.start()
|
| 240 |
+
|
| 241 |
+
if __name__ == "__main__":
|
| 242 |
+
main()
|