diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 320624f..ea978ab 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.8 +current_version = 0.1.9 commit = True tag = True diff --git a/README.md b/README.md index a1a7b2a..9549822 100644 --- a/README.md +++ b/README.md @@ -3,70 +3,141 @@ [![Test](https://github.com/ayenpure/QuickView/actions/workflows/test.yml/badge.svg)](https://github.com/ayenpure/QuickView/actions/workflows/test.yml) [![Package and Release](https://github.com/ayenpure/QuickView/actions/workflows/package-and-release.yml/badge.svg)](https://github.com/ayenpure/QuickView/actions/workflows/package-and-release.yml) -A scientific visualization application for exploring atmospheric data from the -E3SM (Energy Exascale Earth System Model) project. +**QuickView** is an interactive visualization tool designed specifically for +atmospheric scientists working with E3SM (Energy Exascale Earth System Model) +data. Built on ParaView and Trame, it provides an intuitive interface for +exploring atmospheric simulation outputs without the steep learning curve of +general-purpose visualization tools. -## Setting up conda environment +![quickview](docs/images/main.png) -This trame application has two requirements. +## Why QuickView? -### Python version 3.10 +Traditional visualization tools like ParaView and VisIt, while powerful, often +require significant time investment to master their complex interfaces and may +lack atmospheric science-specific features out of the box. QuickView addresses +these challenges by: -Python can be installed using either `homebrew` if using macOS like +- **Reducing the learning curve** - Atmospheric scientists can start visualizing + their data immediately +- **Eliminating "last-mile" effort** - No need to write custom scripts or + plugins for common tasks +- **Accelerating insights** - Focus on science, not software configuration +- **Building on proven technology** - Leverages ParaView's robust data + processing with a tailored interface -`brew install python3.10` +## Installation -or `apt` if using Ubuntu Linux. +### Using Conda (Recommended) -`sudo apt install python3.10` +1. Create and activate a conda environment: -Alternatively, python can also be installed using anaconda/miniconda +```bash +conda env create -f quickview-env.yml +conda activate quickview +``` -`conda create --name eamapp python=3.10.0 conda activate eamapp` +2. Install QuickView: -However, this would require activating the conda environment before using the -app. +```bash +pip install . +``` -### ParaView 5.13 installed on the system +### Requirements -ParaView can be installed from the binaries found at -https://www.paraview.org/download +- Python 3.13 +- ParaView 5.13.3 (installed automatically with conda environment) +- Trame and other dependencies (installed automatically) -The additional requirements for the app are satisfied once the app is launched -for the very first time using Python virtual environments `venv`. An additional -step for the use is to provide the path to ParaView's Python client that is -distributed with the ParaView binaries. The `pvpython` binary is present in the -`bin` directory of ParaView, on macOS the path is something like -`/Applications/ParaView-5.13.0.app/Contents/bin/pvpython` +## Getting the Code -To clone this repository use the following commands +### Clone from GitHub +```bash +git clone https://github.com/ayenpure/QuickView.git +cd QuickView ``` -git clone https://gitlab.kitware.com/ayenpure/eamapp.git -cd eamapp -git lfs install -git lfs pull + +### Download as Archive + +```bash +wget https://github.com/ayenpure/QuickView/archive/main.tar.gz +tar -xvzf main.tar.gz +cd QuickView-main ``` -Alternatively, the code can also be downloaded as a tarball +## Running the Application + +To run QuickView with a data file: +```bash +python -m quickview.app --data data/aerosol_F2010.eam.h0.2014-12.nc ``` -wget https://gitlab.kitware.com/ayenpure/eamapp/-/archive/master/eamapp-master.tar.gz -tar -xvzf eamapp-master.tar.gz -cd eamapp-master + +### Command Line Options + +- `--data`: Path to the NetCDF data file (required) +- `--conn`: Path to the connectivity file (optional) +- `--port`: Server port (default: 8080) +- `--host`: Server host (default: localhost) + +### Example with Custom Files + +```bash +python -m quickview.app --data /path/to/your/data.nc --conn /path/to/your/connectivity.nc ``` -To run the app, execute +The application will start a Trame web server. Open your browser and navigate +to: ``` -python3 launch.py --data data/aerosol_F2010.eam.h0.2014-12.nc +http://localhost:8080 ``` -Please ensure that the `python3` is the correct version. In the above execution -the data file is provided as the sample data out of the repository. The -repository also contains the connectivity file that it uses by default. If -another connectivity and data files are to be used please specify the paths -using the `--conn` and `--data` options. +## Sample Data + +The repository includes sample data files in the `data/` directory for testing: + +- `aerosol_F2010.eam.h0.2014-12.nc` - Sample atmospheric data +- Default connectivity file is automatically loaded + +## Development + +For development setup and contribution guidelines, please see +[CONTRIBUTING.md](CONTRIBUTING.md). + +## Documentation + +Comprehensive documentation is available in the [docs/](docs/) directory, +including: + +- [User Guide](docs/userguide/launch.md) - Detailed usage instructions +- [Control Panel Guide](docs/userguide/control_panel.md) - Interface overview +- [Viewport Customization](docs/userguide/viewport.md) - Working with multiple + variables +- [Data Requirements](docs/data-requirements.md) - NetCDF file format and + required variables + +## About + +QuickView is developed by [Kitware, Inc.](https://www.kitware.com/) in +collaboration with +[Pacific Northwest National Laboratory](https://www.pnnl.gov/). It is supported +by the U.S. Department of Energy's +[Biological and Environmental Research (BER)](https://www.energy.gov/science/ber/biological-and-environmental-research) +and +[Advanced Scientific Computing Research (ASCR)](https://www.energy.gov/science/ascr/advanced-scientific-computing-research) +programs via the +[Scientific Discovery through Advanced Computing (SciDAC)](https://www.scidac.gov/) +program. + +### Contributors + +- **Lead Developer**: Abhishek Yenpure (Kitware, Inc.) +- **Key Contributors**: Berk Geveci, Sebastien Jourdain (Kitware, Inc.); Hui + Wan, Kai Zhang (PNNL) + +## License -The above command will start the Trame app server. On the browser proceed to -`http://localhost:8080` to use the app +This project is licensed under the Apache Software License - see the +[LICENSE](LICENSE) file for details. diff --git a/docs/README.md b/docs/README.md index 07f82fe..69ca30a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -51,6 +51,9 @@ To learn more about the installation of EAM QuickView, checkout the To learn more about using EAM QuickView, checkout the [brief overview.](tutorials/eamapp.md) +For information about data file requirements and supported formats, see the +[data requirements documentation](data-requirements.md) + ## Point of Contact The lead developer of EAM QuickView is diff --git a/docs/data-requirements.md b/docs/data-requirements.md new file mode 100644 index 0000000..74ef904 --- /dev/null +++ b/docs/data-requirements.md @@ -0,0 +1,154 @@ +# QuickView Data File Requirements + +This document describes the NetCDF file format and variable requirements for +QuickView to properly read and visualize E3SM atmospheric data. + +## Overview + +QuickView requires two NetCDF files: + +1. **Data File** - Contains the atmospheric variables and time-varying data +2. **Connectivity File** - Contains the mesh geometry and grid structure + +## Required Dimensions + +The following dimensions must be present in the data files: + +### In Data File: + +- `time` - Time dimension for temporal data +- `ncol` - Number of columns (horizontal grid points) +- `lev` - Number of vertical levels at layer midpoints +- `ilev` - Number of vertical levels at layer interfaces + +### In Connectivity File: + +- `grid_size` or `ncol` - Total number of grid cells + +## Required Variables + +### 1. Coordinate Variables + +#### Latitude and Longitude (Required) + +The connectivity file must contain corner coordinates for grid cells: + +- **Variables containing `corner_lat`** - Latitude coordinates of cell corners +- **Variables containing `corner_lon`** - Longitude coordinates of cell corners + +These are used to construct the unstructured grid geometry. + +#### Vertical Coordinate Variables + +For proper vertical level display, the data file should contain either: + +**Option A: Direct level values** + +- `lev` - Pressure levels at layer midpoints (hPa) +- `ilev` - Pressure levels at layer interfaces (hPa) + +**Option B: Hybrid coordinate coefficients** If `lev` and `ilev` are not +directly provided, they will be computed from: + +- Variables containing `hyam` - Hybrid A coefficient at layer midpoints +- Variables containing `hybm` - Hybrid B coefficient at layer midpoints +- Variables containing `hyai` - Hybrid A coefficient at layer interfaces +- Variables containing `hybi` - Hybrid B coefficient at layer interfaces + +The pressure levels are computed as: + +``` +pressure = (hyam * P0) + (hybm * PS0) +``` + +where P0 = 100000 Pa (reference pressure) and PS0 = 100000 Pa (surface +pressure). + +### 2. Time Variable (Required) + +- `time` - Time coordinate variable containing timestamps for each time step + +### 3. Area Variable (Optional but Recommended) + +- Variable containing `area` in its name - Grid cell areas used for computing + area-weighted averages + - If not present, simple arithmetic averaging will be used instead + +## Variable Types Supported + +QuickView categorizes variables based on their dimensions: + +### 1D Variables (Info Variables) + +- Dimensions: `(ncol)` +- Example: `area` +- These are typically time-invariant grid properties + +### 2D Variables (Surface Variables) + +- Dimensions: `(time, ncol)` +- Example: `TS` (surface temperature), `PS` (surface pressure) +- These represent surface or column-integrated quantities + +### 3D Midpoint Variables + +- Dimensions: `(time, lev, ncol)` or `(time, ncol, lev)` +- Example: `T` (temperature), `U` (zonal wind), `Q` (specific humidity) +- These are defined at layer midpoints + +### 3D Interface Variables + +- Dimensions: `(time, ilev, ncol)` or `(time, ncol, ilev)` +- Example: `OMEGA` (vertical velocity) +- These are defined at layer interfaces + +## Variable Attributes + +### Fill Values + +Variables should include the `_FillValue` attribute to indicate missing or +undefined data. QuickView will convert these to NaN values for proper handling. + +## Example Data Structure + +### Data File Structure: + +``` +dimensions: + time = 12 ; + ncol = 48602 ; + lev = 72 ; + ilev = 73 ; + +variables: + double time(time) ; + double T(time, lev, ncol) ; + T:_FillValue = 9.96920996838687e+36 ; + double TS(time, ncol) ; + TS:_FillValue = 9.96920996838687e+36 ; + double hyam(lev) ; + double hybm(lev) ; + double hyai(ilev) ; + double hybi(ilev) ; +``` + +### Connectivity File Structure: + +``` +dimensions: + grid_size = 48602 ; + grid_corners = 4 ; + +variables: + double grid_corner_lat(grid_size, grid_corners) ; + double grid_corner_lon(grid_size, grid_corners) ; +``` + +## Notes + +1. The reader automatically detects and categorizes variables based on their + dimensions +2. Variables with dimensions not matching the expected patterns are ignored +3. The reader caches geometry and special variables for performance +4. Time interpolation is handled automatically when requesting specific time + steps diff --git a/docs/images/main.png b/docs/images/main.png new file mode 100644 index 0000000..a8d4bbf --- /dev/null +++ b/docs/images/main.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4fbfe24c8789ee9ddbd09550d91f3ca683caf59eb1a3569faa4e64ab7c039a8 +size 1285101 diff --git a/pyproject.toml b/pyproject.toml index 7d8eada..f834a61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "quickview" -version = "0.1.8" +version = "0.1.9" description = "An application to explore/analyze data for atmosphere component for E3SM" authors = [ {name = "Kitware Inc."}, diff --git a/quickview/__init__.py b/quickview/__init__.py index 7bbf27b..9b9f9f9 100644 --- a/quickview/__init__.py +++ b/quickview/__init__.py @@ -1,5 +1,5 @@ """QuickView: Visual Analysis for E3SM Atmosphere Data.""" -__version__ = "0.1.8" +__version__ = "0.1.9" __author__ = "Kitware Inc." __license__ = "Apache-2.0" diff --git a/quickview/colorbar_cache.py b/quickview/colorbar_cache.py new file mode 100644 index 0000000..e2dbc41 --- /dev/null +++ b/quickview/colorbar_cache.py @@ -0,0 +1,65 @@ +# Auto-generated colorbar cache +# Generated using generate_colorbar_cache.py + +COLORBAR_CACHE = { + "oslo": { + "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAmUlEQVQokXWRWQ7EMAxCedz/zp2PeiEZVa0QEJzEDoAwtjAYiggGAUGjASkcWVB/c4RovzkR0ByhU3Yedof201xe99Th36t/ye70DmTXaRLml/QrXWSxHNzfFNwbeaQc5B2DUUgZ1QP0+NFwTimkTapMF3k+A8EjP2exS1cVeTG6EPezmMRu09PvORwH5pR8jXt4b2Z75AYKf0CgA/rlIrMBAAAAAElFTkSuQmCC", + "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAmElEQVQokW2RUQ7EQAhC6f3vzNuPjoqdTRMDQo2DD2ADviperLU0UB8lYTid7qeEqq9S1RXrAGQVQIxHIKTB7xwFDbyc0kw7nmf5/xjkwWzazlnMszBB93sjlk84GeeOeYJfWWKuYR+qoNrNltRNLamrxoYuZ/YPXmoYRP6l925JQe89w38mNFaP7TWoUb32ucH1FlcCxv4BzjllJjnD7IwAAAAASUVORK5CYII=", + }, + "batlow": { + "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAArklEQVQokW2RwZEEMQjEWk0UG9rmHwj3AGzmZj+ULBqbKsPniwVggTAgvVjFCEswICFASC4uyeKVYWeuBEntf9TV4so8x8VZYTckyN3NBqWvT1ThnKrFPVLJU1deXn515ecl77DZmWZ3prbVGF3uIyNlYckwsoACBgyW3V/qYxDGFpZ5gOmAza3TMgooMMQ/EEHvFf2UYnaJ2Tdqdy0/fPxPWRxH5ls+Z/PymYrEfz0jPL9tQdmmAAAAAElFTkSuQmCC", + "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAArklEQVQokW2RQQ4DIQzEnP9/rS+qxLgHCLDqXkaOGciB+n6+mDIwMBgcGEjZhpZrXFzr6CG55X33VR4/vLwGhgajA+LsEI0vEBmazJHkTqLOo5DghhgxZC6wIQe0IQfokR5pSZhjtVzsMjsrIrU5lIvL06nd2dIrd/nUHsn9iNfSTk7Tsq/oBtwrZPYVRRpEis2ymAUvyWLE9e4xt7y8h/O8mFUw3Y8KQfGP57/+AEbyxodkWlsHAAAAAElFTkSuQmCC", + }, + "vik": { + "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAuUlEQVQokXWOUXIGIQyCAfcEvWHvf4ZAH4xbbf1nMuwHQVfi65siSCz9Y1uv4U35qfxut7/MMklKFKWFIiWJWnZMu2CcsOlmx2X73O3H5Nlv5r9EHNIjDi77CxjkEAZ7BIgZgBDCSggzPbARw9Xqig1XXLCTysue7FTFhWzJ0blaJ0cCO7nbrsXrJb2CnX6Su7a4m/Gy5ZWnmr3Ytue3smzK201OGX4nJwTJYW0kcNjbGb4wt2YfDH4AjnNMdUGwXfEAAAAASUVORK5CYII=", + "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAwUlEQVQokXWOQZbDMAxCAW3minP/a1TQha20btM8P/xBODb/8SdCBAkRUkhIEEFhjxYI4mt66RwcfQehBInayhJV1PoW1HasksRhXqzNpHCxRBbeeO4oct1X6wh11Yqczp3F/OdIVL8sVaComtfW0dnPq/USqKACNapwFmQykMEGHBrozDI66aCddjq54OG0va3T9mOP/JlM7fHq3ydfBbfTfdpz6hN8ghM7GZtt1xY4cZDEwQUJxn5o7sKf+hXmDJ8lOkiIWk7JIwAAAABJRU5ErkJggg==", + }, + "roma": { + "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAxUlEQVQokW2PQQ7DIAwEd80j+tv+nJ0eDCRpK1nWMF6I4/dLHl5V9pCrWS67XBtcl9T96C+W7tKd1za2T+Bn6n/J/ZqOP+b6UFftTO2+QNU/c+RYcrNr6MHjGNWwjxnyv+5iM67uaNBehSsuVDSrUHefHjkNOPK9T/QANGnWXEclmg1o5jIJE82058hEExIlZJtcZgEhiJAISOAJ3AGRBQoAEUGsjHamzckICOrRuX5iPY0I65Etl1+B5w7Zu4UAWTsndH0AoR4v4nBAd8IAAAAASUVORK5CYII=", + "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAA0ElEQVQokWWPQY4gIRDD4vCi+e18lmQPTQM9K6GScQUoGD+/Nnth2WAMGCzMWQijxejtCrBkALz8kssA0gqfLjy3rdjKXAdPBt0zPJkN/oItjBHrXzI3yMboMeM1A9kaxmhYg0uiYRkNtI3RoK+skelAVm/41li1aoVPjRUaHm5opNBJgyaNuqoy6dTL/9dmKrOHs0xmF2dzEyU9Jk3bLdsPVO27bdO2emFL9Rilra6w2otXTOueP8ndPQ+9x093sfZI0Z4kh9XZpp1q2tnO/gOZg0urikp4YQAAAABJRU5ErkJggg==", + }, + "tokyo": { + "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAoUlEQVQokW2Syw3DMAxD+eh7J+gY3X+2HiRZih0EEEjq+R++nx/IwsgCyWDxCDlD2C2MzJPhWZ08IwfhEGVDxBSZSNXVsGqbcCd6dCXuVjNHIilm0+jedVheAN6BS/Tq99JcQ/pERU5s3M/J+4DBewgg+RbQ5K4onyyf8NQpnLU+XMZhQrnUGnZFXdtie5kKqXAKhTVqi8hEgC2QHdvtX89/EjgCyIkUtH0AAAAASUVORK5CYII=", + "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAq0lEQVQokXWSQQ6DMAwEp/9/Y9/QD5SdHuIEJ6gCWbvLYISd1+f7FiVRsekENakkYkxKJ2bZmHh1ca0kV4WZYVaNmcBto9M5WtZloqtqVIllm0aMqOGuQlRQwikEGSSOEITs1tmq3c0KnDw7xkbWIzayC5c9XuQfwAE8+nvw/vtu6X0CbH/nPi7bfGx2JpqRr9Om4jx8qby2Z2iLnZV723PJU4x9doZHyB3+APLS0HEVvfePAAAAAElFTkSuQmCC", + }, + "davos": { + "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAApklEQVQokYWSUa7DIAwEd0Z6l+v975N+2CbQNnqRsGbXGKMY8veKRqJABAxQEpEADRbWqhIAzQCQL6iqrfxMdV9WdjZwf9MhG5dqcMkVG3I4HxySQ36bCVynf0Gy+Amyqi5IPv1/5Hn+c5e6WwKXDxeWzWmZ+eu3Y4+ix95ynoAiOIA3qz8jKi6nti9gM7eDnSey2Jl6X2rAlN9RUilSZixIVWWZzW/flBj7cleqDQAAAABJRU5ErkJggg==", + "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAnElEQVQokX2SQRLDMAgDlf//ebcHQ0xdTy/MSgGiTHiAxMSEqFkSNYvFDV2DkvhWQ8S1QRSXX4yWpM0FgPryAEtuvlbH/F5dmVBTaViZUimNQ05Ho0/JbGcxg/Xpnhu0zPaPtrvMlJe2E3KG+Qrs+SF/TCfnNN2VlwesW2h2s8PvGRwN3U1v5/fRhnol9Kq+rq9x6jCXW+P07yf4AfjdPHOVKVO9AAAAAElFTkSuQmCC", + }, + "acton": { + "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAi0lEQVQokYWRyREDIQwEu+fvNByH84/KD/CuhKGWD6O5isP36+NYmTtqfsPCFwldPJWhBDfSKfKM2fJnI+FiUMOQEISAOvAcoai3tKj55y+/lWkgLW56bZrTxoyTi5h5OzIvvX8t2oeq8yWIFEMhU8kbGOMhm41/re2gNTuaWf3sVFhTXD2r4RkDfgEEQgLT9KY7/QAAAABJRU5ErkJggg==", + "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAjUlEQVQokZWSOxJDMQgDlfvfNFXqFNpXYByDPZ5JtyAJf1+f9xeQEBKIqPiHRxZzNrg2HZFdRZ4DF1tCMRQwR78R3v2AcBu7rWUyMg2zKhzLDxAC5LgPCQXLYnB2SqlhHtmUOowUTfoxvb/6D2puMlVi5/MvWHlebQe/3Mi5f2XfPOtbtEg327Vch7j1H1G+JZaGOgELAAAAAElFTkSuQmCC", + }, + "grayC": { + "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAASUlEQVQokbWSOw4AMAhCgfsfukOTxrRWTT8MBh6MEoAkSSRlRLITngrAq2U82LUuX2FK6rFY3fg0BjDgn+T+zwonYqPr68aN4zZHpwL88xPUcgAAAABJRU5ErkJggg==", + "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAR0lEQVQokbWSSQoAIAwD6///nHgQROxiEM2hZKY9tpEkCSCcGYql6B5Dk8l/MTPdb7LAV11fHVE0mSy8slUOLi591recGdgB5Ri1JqWVr6UAAAAASUVORK5CYII=", + }, + "navia": { + "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAr0lEQVQokXWS0Q3EIAxD/ZwRbojbf0LuwyGlrQ6h6OE4CVWhPl9sYYGwMOS4t+/HePzXw6PE05BhsMzotA62yIqh18mA2tVRzQLUoCcosEBS9LVFoRWQxrCECOhIjfmdOmqvJjr67ynTIXd4zVpM9qqK8yqBu/+hv5Qz+r/ijjrZ+XuzHSCxHKbApsBgXK24sKGwCb9ANuWt3I5qEdWGI+IM31dgsvvd5Emdyv4++AEbhyvIyFxhOAAAAABJRU5ErkJggg==", + "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAr0lEQVQokXWSQY7EIBADzf9fuP/Y41I1h24IzMxGqFU2dkgkxt/vj0E1LtgTorIdNgsdQNvfEzGzgOXgPLjlXPINpqBTgKlTaUcoKTrreNck2MslNTj0Yh0YF//n9Ex05MMpTi4/xbuSoUlXjnySs1KywzFHN7v1QHrrCew3xHzfeutex/VnP6f0L1TxE1xc0DMU99M35WE6V9dKCiJKBRafPnEt0EvmkN7ye+ZhIy9nOgFitgzqrgAAAABJRU5ErkJggg==", + }, + "bam": { + "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAvUlEQVQokW2Q0W3FMAwDSXm8ztH9F6jIfkhWlNcmgnFHEXAQfsdXnHMi4tQ0xon1vAPuVZBrwbUnm/kIWzjtYU4tyEoW7/Clp8LRP7AYh0/5kEGsQnOQh8MfgCBZAAZxuYEVAiQCLCD+GQwYhDFgwKWXawTYtiFDBXKxml1JyrJTlpwqUF7O4VRr+kdqTVWSnXywJGVaD0uS5AtSSvIVe4WexC91e2084vt207fZ3bXSbffdfa/v4fVh8zv0CzRYXG5OJkEEAAAAAElFTkSuQmCC", + "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAvUlEQVQokW2QQY7DMAwDSarX/dn+/zcme7DsCG2DQJ6RaMcI//4hsqQSJUkUVdLlBkl8Apwdsol8pmd0suQM8x4+k5pnnjvc72pcTCWJ1aDqysEqqYr1g3k7L11lqX/DZB0Vd0X1lSlCpIjLJBqAzSQ274rmoDXA1gBbf7xJAAMJgvjWwA1xcxJ4a/INaza9tQMrsXvaMd/84AG9cYPt2X/Un03bSRynyV+8/YHed3hgr+c5uj5W2+vIWl6tb9Q4Uct7Ls3FAAAAAElFTkSuQmCC", + }, + "lajolla": { + "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAArElEQVQokXWRUY5EIQgEq5pTzP0vuh+Co+/tJMY01YgKfj6ogrQAFVmw9QW9LJ/WkaNC8N8wAxfJT60SjIrxEFfIDjOud3i4bFgaePGLVPO9MznrnWxLiUbcbvcIQ6ZB04juhYEDdtE9g6zxLCEPuIb3zV93D7+TOcZwHX+m+So7K8ctd+X+dn/EGzqPl57o6k6mRyHRkGD87kVKQwrLFMYUFpYWv3SvjM6b/AE3AwLH88Pa5AAAAABJRU5ErkJggg==", + "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAqklEQVQokW2RWw7DMAzDuPsfbWeKuI84TrMGKAqZVl7yx3xhYHBoCbwQiw9y12ZgTPOQyYOZfxINRURNUKJKwAmnwIggRKd4QjEsrcIuKX9a+1pC70kOz8WWxynHtq9r5IBuXTdsMt/acH7R1aIScgawQpJgKq0S0fBsOR56df0jo8jJD6hn2WZPc5cusxJq+RrpRe/Z1rtXxq+yRrEFlZk94d3ir7XhnuEPk2frRoSfIrIAAAAASUVORK5CYII=", + }, + "Rainbow Desaturated": { + "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAf0lEQVQokb2MSwrDMAxExxkV52a5/7oncSxZXfSD6rgkIVAQjydpmLQs99asNR3SfXzfpbuNCwnkG7Kc43wy/6QkaIEVWHnJT67HYl3bGl+SLAsy8eEc/Aq3PQJAAQXqP0Qb1KC2I/VAppO6uTiSTBPfIyTjGpzk1xq/ZBcevh6R9SSE4xjKlAAAAABJRU5ErkJggg==", + "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAgUlEQVQokb2MwRLCIAxENyHI1/X/r35GM02CB2WYYq3Ug5c3L7sLdF8Wj/AIi/AId9+d36rX2aqDdl8RqiRIgjCekpvItOTpMTIggPxDDFCDeufqY/Ib3/+xmiAFqTTeuvfwlMOrs1mBVegGtWtcL+7VoBs5mBOzDCQ6CGdI9LF9AEoJAHMyopBpAAAAAElFTkSuQmCC", + }, + "Cool to Warm": { + "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAj0lEQVQokY2PwRHEIAwDJdq6Fq7oq0y6B2BsQib57Qhhr/n5/kg0giQbFjMYbFxMzrAXRnNxHXKckPI08GZC1otm1WDK0V4fMvjhkJy/PWTw/SHxEQAkyLY7wLK8hbYgQ7I9QIYF2RIcyexfwtKX4YC5zo69h3Dvh8xZ7+y8622e/Wms28MiPx2K58XhyfkPxwMk8uPUcnsAAAAASUVORK5CYII=", + "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAkElEQVQokY2PQRLDIBRCgelZev9FzydZfE3QmLY75onM+/y83hBJQYREEhJEimAF4YTXa4dRi74IimKE6M9w9EdIhyv0py384kxx8dzC0V/kS2/23MKz/6h3cxgQgI1m2HCD7Z4duVXBxXtu8dGOkVxAq18t8sy3C7G8aOTCohF6z4ekxs9Dkm8Xbgf+e0jlA67XGVo/7bonAAAAAElFTkSuQmCC", + }, + "Jet": { + "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAPElEQVQokb2MwQ0AIAjEamB/Fz7EFbwHJn22XbAhHeJJi2Xp7v7DPFFQid6Z9q0kuhAUyMHyR+eiz+ieC/Oio4VU6TUjAAAAAElFTkSuQmCC", + "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAAAPElEQVQokbXNwQ0AIAwCQGo7uIOX6AqUaMLzgNhAAjXJ1Mf6fDDy1jgjG6WEUKVd8TwaaihLw9vjPM8PLpPXnpUG7pGjAAAAAElFTkSuQmCC", + }, + "Yellow - Gray - Blue": { + "normal": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAABAklEQVQokW2NTVJAMQiDSQKMju49lfc/iW5eCy76fqojw6RfSUrx+f6RVtGVVtmV6MTSTnZ4pzq9wy3ULgs/NdTh5mq5yZeaByKXQgE6FGCADnNgtQinOSGYC05I5oTL3OFu7g/r1oAHFOZhSihtNQNKYxgTDGMYAwyD26MBhCGBMEuzbEuz/P56m/MY4xjjmPMYY4xxzDHGGHOMueqG31WzrsDGt7llatacVbOqLl51Q3VVdfd1WbDu/SfwT6yfJdukz0z3pc/WqZerX40OkiRBnOfOeoaPJWq9OV1Sp6vTuIdXcJtfrJu18C9fg32HpP0RpV9hEttXD0vUGUaIIYTzB37sE9oBp0aLAAAAAElFTkSuQmCC", + "inverted": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAP8AAAABCAIAAACe8u/0AAABCElEQVQokW2OQW7FIAxEbUM27f1P2Cu0amXPeLqA0OirFhoe9iPEPz6rqEIXVWxSZLOb7FVkd+uF15TkDTvJ1T53+ZSP+nLt1h+vHvM+PPt6NHWmbDVb+19P6mjPkTZYY/B78GetiAj3iPCIcPfwnbHPsfkoN9z9LbkfZ1v/aX9fG/c+Fo8Ym8dYvOrA2P354H9rzgNzzjHnnNecc4xrzmvOa4zr7f3LLN3SLM1SKtPKsi4TVqrLuqxLXdZpXWJalzGNKaYxDSWWUEIZIWAnINDw5BYptIGiDC202IIEGdRQl1hqiKVKoVTrBRhhhBMGesGKXnDQdtILlvCkFzzbU2tFytMjLcrjF8c3wYUVNTbdAAAAAElFTkSuQmCC", + }, +} diff --git a/quickview/interface.py b/quickview/interface.py index 2c3b9a1..c3cd8a3 100644 --- a/quickview/interface.py +++ b/quickview/interface.py @@ -18,6 +18,7 @@ from quickview.pipeline import EAMVisSource +from quickview import __version__ as version from quickview.ui.slice_selection import SliceSelection from quickview.ui.projection_selection import ProjectionSelection from quickview.ui.variable_selection import VariableSelection @@ -30,6 +31,11 @@ from paraview.simple import ImportPresets, GetLookupTableNames +from paraview.modules import vtkRemotingCore as rc + +rc.vtkProcessModule.GetProcessModule().UpdateProcessType( + rc.vtkProcessModule.PROCESS_BATCH, 0 +) # ----------------------------------------------------------------------------- # trame setup @@ -596,7 +602,7 @@ def ui(self) -> SinglePageWithDrawerLayout: self._ui = SinglePageWithDrawerLayout(self.server) with self._ui as layout: # layout.footer.clear() - layout.title.set_text("EAM QuickView v1.0") + layout.title.set_text(f"QuickView {version}") with layout.toolbar as toolbar: Toolbar( diff --git a/quickview/pipeline.py b/quickview/pipeline.py index d36ab64..2f6374e 100644 --- a/quickview/pipeline.py +++ b/quickview/pipeline.py @@ -219,13 +219,13 @@ def Update(self, data_file, conn_file, midpoint=0, interface=0): self.interfaces = data_wrapped.FieldData["ilev"].tolist() self.surface_vars = list( - np.asarray(self.data.GetProperty("a2DVariablesInfo"))[::2] + np.asarray(self.data.GetProperty("SurfaceVariablesInfo"))[::2] ) self.midpoint_vars = list( - np.asarray(self.data.GetProperty("a3DMiddleLayerVariablesInfo"))[::2] + np.asarray(self.data.GetProperty("MidpointVariablesInfo"))[::2] ) self.interface_vars = list( - np.asarray(self.data.GetProperty("a3DInterfaceLayerVariablesInfo"))[::2] + np.asarray(self.data.GetProperty("InterfaceVariablesInfo"))[::2] ) tk = GetTimeKeeper() @@ -311,9 +311,9 @@ def Update(self, data_file, conn_file, midpoint=0, interface=0): def LoadVariables(self, surf, mid, intf): if not self.valid: return - self.data.a2DVariables = surf - self.data.a3DMiddleLayerVariables = mid - self.data.a3DInterfaceLayerVariables = intf + self.data.SurfaceVariables = surf + self.data.MidpointVariables = mid + self.data.InterfaceVariables = intf self.vars["surface"] = surf self.vars["midpoint"] = mid self.vars["interface"] = intf diff --git a/quickview/plugins/eam_projection.py b/quickview/plugins/eam_projection.py index 048cdae..8e866b0 100644 --- a/quickview/plugins/eam_projection.py +++ b/quickview/plugins/eam_projection.py @@ -283,14 +283,23 @@ def RequestData(self, request, inInfo, outInfo): x = flat[0::3] - 180.0 if self.translate else flat[0::3] y = flat[1::3] - latlon = Proj(init="epsg:4326") - if self.project == 1: - proj = Proj(proj="robin") - elif self.project == 2: - proj = Proj(proj="moll") - - xformer = Transformer.from_proj(latlon, proj) - res = xformer.transform(x, y) + try: + # Use proj4 string for WGS84 instead of EPSG code to avoid database dependency + latlon = Proj(proj="latlong", datum="WGS84") + if self.project == 1: + proj = Proj(proj="robin") + elif self.project == 2: + proj = Proj(proj="moll") + else: + # Should not reach here, but return without transformation + return 1 + + xformer = Transformer.from_proj(latlon, proj, always_xy=True) + res = xformer.transform(x, y) + except Exception as e: + print(f"Projection error: {e}") + # If projection fails, return without modifying coordinates + return 1 flat[0::3] = np.array(res[0]) flat[1::3] = np.array(res[1]) diff --git a/quickview/plugins/eam_reader.py b/quickview/plugins/eam_reader.py index 1c154ee..c2c13c8 100644 --- a/quickview/plugins/eam_reader.py +++ b/quickview/plugins/eam_reader.py @@ -138,383 +138,6 @@ def _markmodified(*args, **kwars): return _markmodified -@smproxy.reader( - name="EAMSource", - label="EAM Data Reader", - extensions="nc", - file_description="NETCDF files for EAM", -) -@smproperty.xml("""""") -@smproperty.xml("""""") -@smproperty.xml("""""") -@smproperty.xml( - """ - - - Specify the NetCDF data file name. - - """ -) -@smproperty.xml( - """ - - - Specify the NetCDF connecticity file name. - - """ -) -class EAMSource(VTKPythonAlgorithmBase): - def __init__(self): - VTKPythonAlgorithmBase.__init__( - self, nInputPorts=0, nOutputPorts=3, outputType="vtkUnstructuredGrid" - ) - self._DataFileName = None - self._ConnFileName = None - # Variables for dimension sliders - self._time = 0 - self._lev = 0 - self._ilev = 0 - # Arrays to store field names in netCDF file - self._vars1D = [] - self._vars2D = [] - self._vars3Di = [] - self._vars3Dm = [] - self._timeSteps = [] - - # vtkDataArraySelection to allow users choice for fields - # to fetch from the netCDF data set - self._vars1Darr = vtkDataArraySelection() - self._vars2Darr = vtkDataArraySelection() - self._vars3Diarr = vtkDataArraySelection() - self._vars3Dmarr = vtkDataArraySelection() - # Cache for non temporal variables - # Store { names : data } - self._vars1DCacahe = {} - # Add observers for the selection arrays - self._vars1Darr.AddObserver("ModifiedEvent", createModifiedCallback(self)) - self._vars2Darr.AddObserver("ModifiedEvent", createModifiedCallback(self)) - self._vars3Diarr.AddObserver("ModifiedEvent", createModifiedCallback(self)) - self._vars3Dmarr.AddObserver("ModifiedEvent", createModifiedCallback(self)) - - # Storing Area as FieldData if available in file - self._areavar = False - - # Method to clear all the variable names - def _clear(self): - self._vars1D.clear() - self._vars2D.clear() - self._vars3Di.clear() - self._vars3Dm.clear() - - def _populate_variable_metadata(self): - if self._DataFileName is None: - return - vardata = netCDF4.Dataset(self._DataFileName, "r") - for name, info in vardata.variables.items(): - if "ncol_d" in info.dimensions: - continue - varmeta = VarMeta(name, info) - if varmeta.type == VarType._1D: - self._vars1D.append(varmeta) - if name == "area": - self._areavar = True - elif varmeta.type == VarType._2D: - self._vars2D.append(varmeta) - self._vars2Darr.AddArray(name) - elif varmeta.type == VarType._3Di: - self._vars3Di.append(varmeta) - self._vars3Diarr.AddArray(name) - elif varmeta.type == VarType._3Dm: - self._vars3Dm.append(varmeta) - self._vars3Dmarr.AddArray(name) - try: - fillval = info.getncattr("_FillValue") - varmeta.fillval = fillval - except Exception: - traceback.print_exc() - pass - self._vars2Darr.DisableAllArrays() - self._vars3Diarr.DisableAllArrays() - self._vars3Dmarr.DisableAllArrays() - timesteps = vardata["time"][:].data.flatten() - self._timeSteps.extend(timesteps) - self.timeDim = vardata.dimensions["time"].size - self.ilevDim = vardata.dimensions["ilev"].size - self.levDim = vardata.dimensions["lev"].size - - def SetDataFileName(self, fname): - if fname is not None and fname != "None": - if fname != self._DataFileName: - self._DataFileName = fname - self._clear() - self._populate_variable_metadata() - self.Modified() - - def SetConnFileName(self, fname): - if fname != self._ConnFileName: - self._ConnFileName = fname - self.Modified() - - @smproperty.doublevector( - name="TimestepValues", information_only="1", si_class="vtkSITimeStepsProperty" - ) - def GetTimestepValues(self): - return self._timeSteps - - # Array selection API is typical with readers in VTK - # This is intended to allow ability for users to choose which arrays to - # load. To expose that in ParaView, simply use the - # smproperty.dataarrayselection(). - # This method **must** return a `vtkDataArraySelection` instance. - @smproperty.dataarrayselection(name="2D Variables") - def Get2DDataArrays(self): - return self._vars2Darr - - @smproperty.dataarrayselection(name="3D Middle Layer Variables") - def Get3DmDataArrays(self): - return self._vars3Dmarr - - @smproperty.dataarrayselection(name="3D Interface Layer Variables") - def Get3DiDataArrays(self): - return self._vars3Diarr - - def RequestInformation(self, request, inInfo, outInfo): - executive = self.GetExecutive() - for i in range(3): - port = outInfo.GetInformationObject(i) - port.Remove(executive.TIME_STEPS()) - port.Remove(executive.TIME_RANGE()) - if self._timeSteps is not None and len(self._timeSteps) > 0: - for t in self._timeSteps: - port.Append(executive.TIME_STEPS(), t) - port.Append(executive.TIME_RANGE(), self._timeSteps[0]) - port.Append(executive.TIME_RANGE(), self._timeSteps[-1]) - return 1 - - # TODO : implement request extents - def RequestUpdateExtent(self, request, inInfo, outInfo): - return super().RequestUpdateExtent(request, inInfo, outInfo) - - def GetTimeIndex(self, time): - timeInd = 0 - if self._timeSteps is not None and len(self._timeSteps) > 1: - for t in self._timeSteps[1:]: - if time == t: - break - else: - timeInd = timeInd + 1 - return timeInd - return 0 - - def RequestData(self, request, inInfo, outInfo): - if ( - self._ConnFileName is None - or self._ConnFileName == "None" - or self._DataFileName is None - or self._DataFileName == "None" - ): - print_error( - "Either one or both, the data file or connectivity file, are not provided!" - ) - return 0 - global _has_deps - if not _has_deps: - print_error("Required Python module 'netCDF4' or 'numpy' missing!") - return 0 - - executive = self.GetExecutive() - from_port = request.Get(executive.FROM_OUTPUT_PORT()) - timeInfo = outInfo.GetInformationObject(from_port) - timeInd = 0 - if timeInfo.Has(executive.UPDATE_TIME_STEP()) and len(self._timeSteps) > 0: - time = timeInfo.Get(executive.UPDATE_TIME_STEP()) - timeInd = self.GetTimeIndex(time) - - meshdata = netCDF4.Dataset(self._ConnFileName, "r") - vardata = netCDF4.Dataset(self._DataFileName, "r") - - lat = meshdata["cell_corner_lat"][:].data.flatten() - lon = meshdata["cell_corner_lon"][:].data.flatten() - - output2D = dsa.WrapDataObject(vtkUnstructuredGrid.GetData(outInfo, 0)) - output3Dm = dsa.WrapDataObject(vtkUnstructuredGrid.GetData(outInfo, 1)) - output3Di = dsa.WrapDataObject(vtkUnstructuredGrid.GetData(outInfo, 2)) - - coords = np.empty((len(lat), 3), dtype=np.float64) - coords[:, 0] = lon - coords[:, 1] = lat - coords[:, 2] = 0.0 - _coords = dsa.numpyTovtkDataArray(coords) - vtk_coords = vtkPoints() - vtk_coords.SetData(_coords) - output2D.SetPoints(vtk_coords) - - ncells2D = meshdata["cell_corner_lat"][:].data.shape[0] - cellTypes = np.empty(ncells2D, dtype=np.uint8) - offsets = np.arange(0, (4 * ncells2D) + 1, 4, dtype=np.int64) - cells = np.arange(ncells2D * 4, dtype=np.int64) - cellTypes.fill(vtkConstants.VTK_QUAD) - cellTypes = numpy_support.numpy_to_vtk( - num_array=cellTypes.ravel(), - deep=True, - array_type=vtkConstants.VTK_UNSIGNED_CHAR, - ) - offsets = numpy_support.numpy_to_vtk( - num_array=offsets.ravel(), deep=True, array_type=vtkConstants.VTK_ID_TYPE - ) - cells = numpy_support.numpy_to_vtk( - num_array=cells.ravel(), deep=True, array_type=vtkConstants.VTK_ID_TYPE - ) - cellArray = vtkCellArray() - cellArray.SetData(offsets, cells) - output2D.VTKObject.SetCells(cellTypes, cellArray) - - gridAdapter2D = dsa.WrapDataObject(output2D) - for varmeta in self._vars2D: - if self._vars2Darr.ArrayIsEnabled(varmeta.name): - data = vardata[varmeta.name][:].data[timeInd].flatten() - data = np.where(data == varmeta.fillval, np.nan, data) - gridAdapter2D.CellData.append(data, varmeta.name) - - lev = None - try: - lev = FindSpecialVariable( - vardata, EAMConstants.LEV, EAMConstants.HYAM, EAMConstants.HYBM - ) - if lev is not None: - coords3Dm = np.empty((self.levDim, len(lat), 3), dtype=np.float64) - levInd = 0 - for z in lev: - coords = np.empty((len(lat), 3), dtype=np.float64) - coords[:, 0] = lon - coords[:, 1] = lat - coords[:, 2] = z - coords3Dm[levInd] = coords - levInd = levInd + 1 - coords3Dm = coords3Dm.flatten().reshape(self.levDim * len(lat), 3) - _coords = dsa.numpyTovtkDataArray(coords3Dm) - vtk_coords = vtkPoints() - vtk_coords.SetData(_coords) - output3Dm.SetPoints(vtk_coords) - cellTypesm = np.empty(ncells2D * self.levDim, dtype=np.uint8) - offsetsm = np.arange( - 0, (4 * ncells2D * self.levDim) + 1, 4, dtype=np.int64 - ) - cellsm = np.arange(ncells2D * self.levDim * 4, dtype=np.int64) - cellTypesm.fill(vtkConstants.VTK_QUAD) - cellTypesm = numpy_support.numpy_to_vtk( - num_array=cellTypesm.ravel(), - deep=True, - array_type=vtkConstants.VTK_UNSIGNED_CHAR, - ) - offsetsm = numpy_support.numpy_to_vtk( - num_array=offsetsm.ravel(), - deep=True, - array_type=vtkConstants.VTK_ID_TYPE, - ) - cellsm = numpy_support.numpy_to_vtk( - num_array=cellsm.ravel(), - deep=True, - array_type=vtkConstants.VTK_ID_TYPE, - ) - cellArraym = vtkCellArray() - cellArraym.SetData(offsetsm, cellsm) - output3Dm.VTKObject.SetCells(cellTypesm, cellArraym) - - gridAdapter3Dm = dsa.WrapDataObject(output3Dm) - for varmeta in self._vars3Dm: - if self._vars3Dmarr.ArrayIsEnabled(varmeta.name): - if not varmeta.transpose: - data = vardata[varmeta.name][:].data[timeInd].flatten() - else: - data = ( - vardata[varmeta.name][:] - .data[timeInd] - .transpose() - .flatten() - ) - data = np.where(data == varmeta.fillval, np.nan, data) - gridAdapter3Dm.CellData.append(data, varmeta.name) - gridAdapter3Dm.FieldData.append(self.levDim, "numlev") - gridAdapter3Dm.FieldData.append(lev, "lev") - except Exception as e: - print_error("Error occurred while processing middle layer variables :", e) - - ilev = None - try: - ilev = FindSpecialVariable( - vardata, EAMConstants.ILEV, EAMConstants.HYAI, EAMConstants.HYBI - ) - if ilev is not None: - coords3Di = np.empty((self.ilevDim, len(lat), 3), dtype=np.float64) - ilevInd = 0 - for z in ilev: - coords = np.empty((len(lat), 3), dtype=np.float64) - coords[:, 0] = lon - coords[:, 1] = lat - coords[:, 2] = z - coords3Di[ilevInd] = coords - ilevInd = ilevInd + 1 - coords3Di = coords3Di.flatten().reshape(self.ilevDim * len(lat), 3) - _coords = dsa.numpyTovtkDataArray(coords3Di) - vtk_coords = vtkPoints() - vtk_coords.SetData(_coords) - output3Di.SetPoints(vtk_coords) - cellTypesi = np.empty(ncells2D * self.ilevDim, dtype=np.uint8) - offsetsi = np.arange( - 0, (4 * ncells2D * self.ilevDim) + 1, 4, dtype=np.int64 - ) - cellsi = np.arange(ncells2D * self.ilevDim * 4, dtype=np.int64) - cellTypesi.fill(vtkConstants.VTK_QUAD) - cellTypesi = numpy_support.numpy_to_vtk( - num_array=cellTypesi.ravel(), - deep=True, - array_type=vtkConstants.VTK_UNSIGNED_CHAR, - ) - offsetsi = numpy_support.numpy_to_vtk( - num_array=offsetsi.ravel(), - deep=True, - array_type=vtkConstants.VTK_ID_TYPE, - ) - cellsi = numpy_support.numpy_to_vtk( - num_array=cellsi.ravel(), - deep=True, - array_type=vtkConstants.VTK_ID_TYPE, - ) - cellArrayi = vtkCellArray() - cellArrayi.SetData(offsetsi, cellsi) - output3Di.VTKObject.SetCells(cellTypesi, cellArrayi) - - gridAdapter3Di = dsa.WrapDataObject(output3Di) - for varmeta in self._vars3Di: - if self._vars3Diarr.ArrayIsEnabled(varmeta.name): - if not varmeta.transpose: - data = vardata[varmeta.name][:].data[timeInd].flatten() - else: - data = ( - vardata[varmeta.name][:] - .data[timeInd] - .transpose() - .flatten() - ) - data = np.where(data == varmeta.fillval, np.nan, data) - gridAdapter3Di.CellData.append(data, varmeta.name) - gridAdapter3Di.FieldData.append(self.ilevDim, "numilev") - gridAdapter3Di.FieldData.append(ilev, "ilev") - except Exception as e: - print_error( - "Error occurred while processing interface layer variables :", e - ) - - return 1 - - import traceback # noqa: E402 @@ -524,7 +147,7 @@ def RequestData(self, request, inInfo, outInfo): extensions="nc", file_description="NETCDF files for EAM", ) -@smproperty.xml("""""") +@smproperty.xml("""""") @smproperty.xml( """ -1) + | (np.char.find(mdims, "ncol") > -1) + )[0][0] + ] + ].size + self._cached_ncells2D = ncells2D + + # Find lat/lon dimensions + latdim = mvars[np.where(np.char.find(mvars, "corner_lat") > -1)][0] + londim = mvars[np.where(np.char.find(mvars, "corner_lon") > -1)][0] + + # Build coordinates + lat = meshdata[latdim][:].data.flatten() + lon = meshdata[londim][:].data.flatten() + + coords = np.empty((len(lat), 3), dtype=np.float64) + coords[:, 0] = lon + coords[:, 1] = lat + coords[:, 2] = 0.0 + + # Create VTK points + _coords = dsa.numpyTovtkDataArray(coords) + vtk_coords = vtkPoints() + vtk_coords.SetData(_coords) + self._cached_points = vtk_coords + + # Build cell arrays + cellTypes = np.empty(ncells2D, dtype=np.uint8) + cellTypes.fill(vtkConstants.VTK_QUAD) + self._cached_cell_types = numpy_support.numpy_to_vtk( + num_array=cellTypes.ravel(), + deep=True, + array_type=vtkConstants.VTK_UNSIGNED_CHAR, + ) + + offsets = np.arange(0, (4 * ncells2D) + 1, 4, dtype=np.int64) + self._cached_offsets = numpy_support.numpy_to_vtk( + num_array=offsets.ravel(), + deep=True, + array_type=vtkConstants.VTK_ID_TYPE, + ) + + cells = np.arange(ncells2D * 4, dtype=np.int64) + self._cached_cells = numpy_support.numpy_to_vtk( + num_array=cells.ravel(), deep=True, array_type=vtkConstants.VTK_ID_TYPE + ) def _populate_variable_metadata(self): if self._DataFileName is None: return - vardata = netCDF4.Dataset(self._DataFileName, "r") + vardata = self._get_var_dataset() for name, info in vardata.variables.items(): dims = set(info.dimensions) if not (dims == dims1 or dims == dims2 or dims == dims3m or dims == dims3i): continue varmeta = VarMeta(name, info) if varmeta.type == VarType._1D: - self._vars1D.append(varmeta) + self._info_vars.append(varmeta) if "area" in name: self._areavar = varmeta elif varmeta.type == VarType._2D: - self._vars2D.append(varmeta) - self._vars2Darr.AddArray(name) + self._surface_vars.append(varmeta) + self._surface_selection.AddArray(name) elif varmeta.type == VarType._3Dm: - self._vars3Dm.append(varmeta) - self._vars3Dmarr.AddArray(name) + self._midpoint_vars.append(varmeta) + self._midpoint_selection.AddArray(name) elif varmeta.type == VarType._3Di: - self._vars3Di.append(varmeta) - self._vars3Diarr.AddArray(name) + self._interface_vars.append(varmeta) + self._interface_selection.AddArray(name) try: fillval = info.getncattr("_FillValue") varmeta.fillval = fillval except Exception: pass - self._vars2Darr.DisableAllArrays() - self._vars3Diarr.DisableAllArrays() - self._vars3Dmarr.DisableAllArrays() + self._surface_selection.DisableAllArrays() + self._interface_selection.DisableAllArrays() + self._midpoint_selection.DisableAllArrays() timesteps = vardata["time"][:].data.flatten() self._timeSteps.extend(timesteps) @@ -653,10 +488,17 @@ def SetDataFileName(self, fname): if fname != self._DataFileName: self._DataFileName = fname self._dirty = True - self._2d_update = True - self._lev_update = True - self._ilev_update = True + self._surface_update = True + self._midpoint_update = True + self._interface_update = True self._clear() + # Close old dataset if filename changed + if self._cached_var_filename != fname and self._var_dataset is not None: + try: + self._var_dataset.close() + except Exception: + pass + self._var_dataset = None self._populate_variable_metadata() self.Modified() @@ -664,21 +506,30 @@ def SetConnFileName(self, fname): if fname != self._ConnFileName: self._ConnFileName = fname self._dirty = True - self._2d_update = True - self._lev_update = True - self._ilev_update = True + self._surface_update = True + self._midpoint_update = True + self._interface_update = True + # Close old dataset if filename changed + if self._cached_mesh_filename != fname and self._mesh_dataset is not None: + try: + self._mesh_dataset.close() + except Exception: + pass + self._mesh_dataset = None + # Clear geometry cache when connectivity file changes + self._clear_geometry_cache() self.Modified() def SetMiddleLayer(self, lev): if self._lev != lev: self._lev = lev - self._lev_update = True + self._midpoint_update = True self.Modified() def SetInterfaceLayer(self, ilev): if self._ilev != ilev: self._ilev = ilev - self._ilev_update = True + self._interface_update = True self.Modified() def SetCalculateAverages(self, calcavg): @@ -697,17 +548,17 @@ def GetTimestepValues(self): # load. To expose that in ParaView, simply use the # smproperty.dataarrayselection(). # This method **must** return a `vtkDataArraySelection` instance. - @smproperty.dataarrayselection(name="2D Variables") - def Get2DDataArrays(self): - return self._vars2Darr + @smproperty.dataarrayselection(name="Surface Variables") + def GetSurfaceVariables(self): + return self._surface_selection - @smproperty.dataarrayselection(name="3D Middle Layer Variables") - def Get3DmDataArrays(self): - return self._vars3Dmarr + @smproperty.dataarrayselection(name="Midpoint Variables") + def GetMidpointVariables(self): + return self._midpoint_selection - @smproperty.dataarrayselection(name="3D Interface Layer Variables") - def Get3DiDataArrays(self): - return self._vars3Diarr + @smproperty.dataarrayselection(name="Interface Variables") + def GetInterfaceVariables(self): + return self._interface_selection def RequestInformation(self, request, inInfo, outInfo): executive = self.GetExecutive() @@ -760,96 +611,60 @@ def RequestData(self, request, inInfo, outInfo): timeInd = self.get_time_index(outInfo, executive, from_port) if self._time != timeInd: self._time = timeInd - self._2d_update = True - self._lev_update = True - self._ilev_update = True + self._surface_update = True + self._midpoint_update = True + self._interface_update = True - meshdata = netCDF4.Dataset(self._ConnFileName, "r") - vardata = netCDF4.Dataset(self._DataFileName, "r") + meshdata = self._get_mesh_dataset() + vardata = self._get_var_dataset() + + # Build geometry if not cached + self._build_geometry(meshdata) + + output_mesh = dsa.WrapDataObject(self._output) - output2D = dsa.WrapDataObject(self._output) - dims = meshdata.dimensions - mdims = np.array(list(meshdata.dimensions.keys())) - mvars = np.array(list(meshdata.variables.keys())) - ncells2D = dims[ - mdims[ - np.where( - (np.char.find(mdims, "grid_size") > -1) - | (np.char.find(mdims, "ncol") > -1) - )[0][0] - ] - ].size if self._dirty: self._output = vtkUnstructuredGrid() - output2D = dsa.WrapDataObject(self._output) - - latdim = mvars[np.where(np.char.find(mvars, "corner_lat") > -1)][0] - londim = mvars[np.where(np.char.find(mvars, "corner_lon") > -1)][0] - - lat = meshdata[latdim][:].data.flatten() - lon = meshdata[londim][:].data.flatten() - - coords = np.empty((len(lat), 3), dtype=np.float64) - coords[:, 0] = lon - coords[:, 1] = lat - coords[:, 2] = 0.0 - _coords = dsa.numpyTovtkDataArray(coords) - vtk_coords = vtkPoints() - vtk_coords.SetData(_coords) - output2D.SetPoints(vtk_coords) - - cellTypes = np.empty(ncells2D, dtype=np.uint8) - offsets = np.arange(0, (4 * ncells2D) + 1, 4, dtype=np.int64) - cells = np.arange(ncells2D * 4, dtype=np.int64) - cellTypes.fill(vtkConstants.VTK_QUAD) - cellTypes = numpy_support.numpy_to_vtk( - num_array=cellTypes.ravel(), - deep=True, - array_type=vtkConstants.VTK_UNSIGNED_CHAR, - ) - offsets = numpy_support.numpy_to_vtk( - num_array=offsets.ravel(), - deep=True, - array_type=vtkConstants.VTK_ID_TYPE, - ) - cells = numpy_support.numpy_to_vtk( - num_array=cells.ravel(), deep=True, array_type=vtkConstants.VTK_ID_TYPE - ) + output_mesh = dsa.WrapDataObject(self._output) + + # Use cached geometry + output_mesh.SetPoints(self._cached_points) + + # Create cell array from cached data cellArray = vtkCellArray() - cellArray.SetData(offsets, cells) - output2D.VTKObject.SetCells(cellTypes, cellArray) + cellArray.SetData(self._cached_offsets, self._cached_cells) + output_mesh.VTKObject.SetCells(self._cached_cell_types, cellArray) self._dirty = False + # Use cached ncells2D + ncells2D = self._cached_ncells2D + # Needed to drop arrays from cached VTK Object to_remove = set() - last_num_arrays = output2D.CellData.GetNumberOfArrays() + last_num_arrays = output_mesh.CellData.GetNumberOfArrays() for i in range(last_num_arrays): - to_remove.add(output2D.CellData.GetArrayName(i)) + to_remove.add(output_mesh.CellData.GetArrayName(i)) - for varmeta in self._vars2D: - if self._vars2Darr.ArrayIsEnabled(varmeta.name): - if output2D.CellData.HasArray(varmeta.name): + for varmeta in self._surface_vars: + if self._surface_selection.ArrayIsEnabled(varmeta.name): + if output_mesh.CellData.HasArray(varmeta.name): to_remove.remove(varmeta.name) - if not output2D.CellData.HasArray(varmeta.name) or self._2d_update: - data = vardata[varmeta.name][:].data[timeInd].flatten() - data = np.where(data == varmeta.fillval, np.nan, data) - output2D.CellData.append(data, varmeta.name) - self._2d_update = False + if ( + not output_mesh.CellData.HasArray(varmeta.name) + or self._surface_update + ): + data = self._load_2d_variable(vardata, varmeta, timeInd) + output_mesh.CellData.append(data, varmeta.name) + self._surface_update = False try: lev_field_name = "lev" - has_lev_field = output2D.FieldData.HasArray(lev_field_name) - lev = ( - output2D.FieldData.GetArray(lev_field_name) - if has_lev_field - else FindSpecialVariable( - vardata, EAMConstants.LEV, EAMConstants.HYAM, EAMConstants.HYBM - ) - ) + has_lev_field = output_mesh.FieldData.HasArray(lev_field_name) + lev = self._get_cached_lev(vardata) if lev is not None: if not has_lev_field: - output2D.FieldData.append(lev, lev_field_name) + output_mesh.FieldData.append(lev, lev_field_name) if self._lev >= vardata.dimensions[lev_field_name].size: print_error( f"User provided input for middle layer {self._lev} larger than actual data {len(lev) - 1}" @@ -857,73 +672,49 @@ def RequestData(self, request, inInfo, outInfo): lstart = self._lev * ncells2D lend = lstart + ncells2D - for varmeta in self._vars3Dm: - if self._vars3Dmarr.ArrayIsEnabled(varmeta.name): - if output2D.CellData.HasArray(varmeta.name): + for varmeta in self._midpoint_vars: + if self._midpoint_selection.ArrayIsEnabled(varmeta.name): + if output_mesh.CellData.HasArray(varmeta.name): to_remove.remove(varmeta.name) if ( - not output2D.CellData.HasArray(varmeta.name) - or self._lev_update + not output_mesh.CellData.HasArray(varmeta.name) + or self._midpoint_update ): - if not varmeta.transpose: - data = ( - vardata[varmeta.name][:] - .data[timeInd] - .flatten()[lstart:lend] - ) - else: - data = ( - vardata[varmeta.name][:] - .data[timeInd] - .transpose() - .flatten()[lstart:lend] - ) - data = np.where(data == varmeta.fillval, np.nan, data) - output2D.CellData.append(data, varmeta.name) - self._lev_update = False + data = self._load_3d_slice( + vardata, varmeta, timeInd, lstart, lend + ) + output_mesh.CellData.append(data, varmeta.name) + self._midpoint_update = False except Exception as e: print_error("Error occurred while processing middle layer variables :", e) traceback.print_exc() try: ilev_field_name = "ilev" - has_ilev_field = output2D.FieldData.HasArray(ilev_field_name) - ilev = FindSpecialVariable( - vardata, EAMConstants.ILEV, EAMConstants.HYAI, EAMConstants.HYBI - ) + has_ilev_field = output_mesh.FieldData.HasArray(ilev_field_name) + ilev = self._get_cached_ilev(vardata) if ilev is not None: if not has_ilev_field: - output2D.FieldData.append(ilev, ilev_field_name) + output_mesh.FieldData.append(ilev, ilev_field_name) if self._ilev >= vardata.dimensions[ilev_field_name].size: print_error( f"User provided input for middle layer {self._ilev} larger than actual data {len(ilev) - 1}" ) ilstart = self._ilev * ncells2D ilend = ilstart + ncells2D - for varmeta in self._vars3Di: - if self._vars3Diarr.ArrayIsEnabled(varmeta.name): - if output2D.CellData.HasArray(varmeta.name): + for varmeta in self._interface_vars: + if self._interface_selection.ArrayIsEnabled(varmeta.name): + if output_mesh.CellData.HasArray(varmeta.name): to_remove.remove(varmeta.name) if ( - not output2D.CellData.HasArray(varmeta.name) - or self._ilev_update + not output_mesh.CellData.HasArray(varmeta.name) + or self._interface_update ): - if not varmeta.transpose: - data = ( - vardata[varmeta.name][:] - .data[timeInd] - .flatten()[ilstart:ilend] - ) - else: - data = ( - vardata[varmeta.name][:] - .data[timeInd] - .transpose() - .flatten()[ilstart:ilend] - ) - data = np.where(data == varmeta.fillval, np.nan, data) - output2D.CellData.append(data, varmeta.name) - self._ilev_update = False + data = self._load_3d_slice( + vardata, varmeta, timeInd, ilstart, ilend + ) + output_mesh.CellData.append(data, varmeta.name) + self._interface_update = False except Exception as e: print_error( "Error occurred while processing interface layer variables :", e @@ -931,15 +722,15 @@ def RequestData(self, request, inInfo, outInfo): traceback.print_exc() area_var_name = "area" - if self._areavar and not output2D.CellData.HasArray(area_var_name): - data = vardata[self._areavar.name][:].data.flatten() - data = np.where(data == self._areavar.fillval, np.nan, data) - output2D.CellData.append(data, area_var_name) + if self._areavar and not output_mesh.CellData.HasArray(area_var_name): + data = self._get_cached_area(vardata) + if data is not None: + output_mesh.CellData.append(data, area_var_name) if area_var_name in to_remove: to_remove.remove(area_var_name) for var_name in to_remove: - output2D.CellData.RemoveArray(var_name) + output_mesh.CellData.RemoveArray(var_name) output = vtkUnstructuredGrid.GetData(outInfo, 0) output.ShallowCopy(self._output) diff --git a/quickview/utilities.py b/quickview/utilities.py index f5914f5..f340b75 100644 --- a/quickview/utilities.py +++ b/quickview/utilities.py @@ -156,3 +156,29 @@ def build_colorbar_image(paraview_lut, log_scale=False, invert=False): # Convert to image return vtk_lut_to_image(vtk_lut) + + +def get_cached_colorbar_image(colormap_name, inverted=False): + """ + Get a cached colorbar image for a given colormap. + + Parameters: + ----------- + colormap_name : str + Name of the colormap (e.g., "Cool to Warm", "Rainbow Desaturated") + inverted : bool + Whether to get the inverted version + + Returns: + -------- + str + Base64-encoded PNG image as a data URI, or empty string if not found + """ + # Import the cache (will be added after running generate_colorbar_cache.py) + from quickview.colorbar_cache import COLORBAR_CACHE + + if colormap_name in COLORBAR_CACHE: + variant = "inverted" if inverted else "normal" + return COLORBAR_CACHE[colormap_name].get(variant, "") + + return "" diff --git a/quickview/view_manager.py b/quickview/view_manager.py index dafd83e..1e6b90f 100644 --- a/quickview/view_manager.py +++ b/quickview/view_manager.py @@ -18,7 +18,7 @@ ) from quickview.pipeline import EAMVisSource -from quickview.utilities import build_colorbar_image +from quickview.utilities import get_cached_colorbar_image from typing import Dict, List, Optional @@ -335,8 +335,8 @@ def sync_color_config_to_state(self, index, context: ViewContext): def generate_colorbar_image(self, index): """Generate colorbar image for a variable at given index. - This is a read-only operation that captures the current state of the - color transfer function without modifying it. + This uses the cached colorbar images based on the colormap name + and invert status. """ if index >= len(self.state.variables): return @@ -346,24 +346,16 @@ def generate_colorbar_image(self, index): if context is None: return - # Get the current ParaView color transfer function - already in correct state - coltrfunc = GetColorTransferFunction(var) - - # Generate the colorbar image from current state + # Get cached colorbar image based on colormap and invert status try: - # Note: We pass log_scale=False because the colorbar image should - # always show linear color gradients. The log scale affects data mapping, - # not the color gradient display. - image_data = build_colorbar_image( - coltrfunc, - log_scale=False, - invert=False, # Inversion is already applied to coltrfunc + image_data = get_cached_colorbar_image( + context.config.colormap, context.config.invert_colors ) - # Update state with the new image + # Update state with the cached image self.state.colorbar_images[index] = image_data self.state.dirty("colorbar_images") except Exception as e: - print(f"Error generating colorbar image for {var}: {e}") + print(f"Error getting cached colorbar image for {var}: {e}") def reset_camera(self, **kwargs): for widget in self.widgets: @@ -406,14 +398,29 @@ def compute_average(self, var, vtkdata=None): data = self.source.views["atmosphere_data"] vtkdata = sm.Fetch(data) vardata = vtkdata.GetCellData().GetArray(var) - area = np.array(vtkdata.GetCellData().GetArray("area")) - if np.isnan(vardata).any(): - mask = ~np.isnan(vardata) - if not np.any(mask): - return np.nan # all values are NaN - vardata = np.array(vardata)[mask] - area = np.array(area)[mask] - return np.average(vardata, weights=area) + + # Check if area variable exists + area_array = vtkdata.GetCellData().GetArray("area") + + if area_array is not None: + # Area-weighted averaging + area = np.array(area_array) + if np.isnan(vardata).any(): + mask = ~np.isnan(vardata) + if not np.any(mask): + return np.nan # all values are NaN + vardata = np.array(vardata)[mask] + area = np.array(area)[mask] + return np.average(vardata, weights=area) + else: + # Simple arithmetic averaging + vardata = np.array(vardata) + if np.isnan(vardata).any(): + mask = ~np.isnan(vardata) + if not np.any(mask): + return np.nan # all values are NaN + vardata = vardata[mask] + return np.mean(vardata) def compute_range(self, var, vtkdata=None): if vtkdata is None: @@ -595,14 +602,14 @@ def update_colormap(self, index, value): context: ViewContext = self.registry.get_view(var) context.config.colormap = value - # Generate new colorbar image BEFORE applying any transformations - self.generate_colorbar_image(index) - # Now apply the preset with current transformations + # Apply the preset coltrfunc.ApplyPreset(context.config.colormap, True) # Reapply inversion if it was enabled if context.config.invert_colors: coltrfunc.InvertTransferFunction() + # Generate new colorbar image with updated colormap + self.generate_colorbar_image(index) # Sync all color configuration changes back to state self.sync_color_config_to_state(index, context) self.render_view_by_index(index) @@ -643,10 +650,10 @@ def update_invert_colors(self, index, value): self.sync_color_config_to_state(index, context) self.render_view_by_index(index) - def update_scalar_bars(self, event): + def update_scalar_bars(self, event=None): # Always hide ParaView scalar bars - using custom HTML colorbar # The HTML colorbar is always visible, no toggle needed - for var, context in self.registry.items(): + for _, context in self.registry.items(): view = context.state.view_proxy context.state.data_representation.SetScalarBarVisibility(view, False) self.render_all_views() @@ -662,8 +669,7 @@ def set_manual_color_range(self, index, min, max): # Update color transfer function coltrfunc = GetColorTransferFunction(var) coltrfunc.RescaleTransferFunction(float(min), float(max)) - # Generate new colorbar image with updated range - self.generate_colorbar_image(index) + # Note: colorbar image doesn't change with range, only data mapping changes self.render_view_by_index(index) def revert_to_auto_color_range(self, index): @@ -680,8 +686,7 @@ def revert_to_auto_color_range(self, index): context.state.data_representation.RescaleTransferFunctionToDataRange( False, True ) - # Generate new colorbar image with updated range - self.generate_colorbar_image(index) + # Note: colorbar image doesn't change with range, only data mapping changes self.render_all_views() def zoom_in(self, index=0): diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..ffa92c7 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,23 @@ +# QuickView Scripts + +This directory contains utility scripts for maintaining and developing +QuickView. + +## Scripts + +### generate_colorbar_cache.py + +Generates the colorbar image cache for all supported colormaps. This +pre-generates base64-encoded PNG images for both normal and inverted versions of +each colormap, improving runtime performance. + +**Usage:** + +```bash +# Requires ParaView's pvpython +export EAMPVIEW=/path/to/paraview/bin/pvpython +$EAMPVIEW scripts/generate_colorbar_cache.py > quickview/colorbar_cache.py +``` + +This will update the `quickview/colorbar_cache.py` file with all colorbar +images. diff --git a/scripts/generate_colorbar_cache.py b/scripts/generate_colorbar_cache.py new file mode 100755 index 0000000..66c8302 --- /dev/null +++ b/scripts/generate_colorbar_cache.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +""" +Standalone script to generate colorbar cache for QuickView. +This script generates base64-encoded PNG images for all supported colormaps +in both normal and inverted forms. + +Usage: + python generate_colorbar_cache.py > colorbar_cache_output.py +""" + +import os +import sys +import xml.etree.ElementTree as ET + +# Add quickview to path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from paraview.simple import GetColorTransferFunction, ImportPresets, GetLookupTableNames +from quickview.utilities import build_colorbar_image + +# Define the colormaps to cache (matching interface.py) +noncvd = [ + { + "text": "Rainbow Desat.", + "value": "Rainbow Desaturated", + }, + { + "text": "Cool to Warm", + "value": "Cool to Warm", + }, + { + "text": "Jet", + "value": "Jet", + }, + { + "text": "Yellow-Gray-Blue", + "value": "Yellow - Gray - Blue", + }, +] + +# CVD-friendly colormaps will be loaded from XML files +cvd = [] + +# Load CVD presets from XML files +try: + existing = GetLookupTableNames() + presdir = os.path.join(os.path.dirname(__file__), "quickview", "presets") + presets = os.listdir(path=presdir) + for preset in presets: + prespath = os.path.abspath(os.path.join(presdir, preset)) + if os.path.isfile(prespath): + name = ET.parse(prespath).getroot()[0].attrib["name"] + if name not in existing: + ImportPresets(prespath) + cvd.append({"text": name.title(), "value": name}) +except Exception as e: + print(f"# Error loading presets: {e}", file=sys.stderr) + +# Combine all colormaps +all_colormaps = cvd + noncvd + +print("# Auto-generated colorbar cache") +print("# Generated using generate_colorbar_cache.py") +print() +print("COLORBAR_CACHE = {") + +for colormap in all_colormaps: + colormap_name = colormap["value"] + print(f' "{colormap_name}": {{') + + try: + # Get the color transfer function + lut = GetColorTransferFunction("dummy_var") + lut.ApplyPreset(colormap_name, True) + + # Generate normal colorbar + normal_image = build_colorbar_image(lut, log_scale=False, invert=False) + print(f' "normal": "{normal_image}",') + + # Invert the transfer function + lut.InvertTransferFunction() + + # Generate inverted colorbar + inverted_image = build_colorbar_image(lut, log_scale=False, invert=False) + print(f' "inverted": "{inverted_image}",') + + # Reset for next iteration + lut.InvertTransferFunction() # Revert back to normal + + except Exception as e: + print(f"# Error processing {colormap_name}: {e}", file=sys.stderr) + print(' "normal": "",') + print(' "inverted": "",') + + print(" },") + +print("}") diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 732e6f7..f53c1f7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "0.1.8" +version = "0.1.9" description = "QuickView: Visual Analyis for E3SM Atmosphere Data" authors = ["Kitware"] license = "" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 118e17d..2728d74 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -7,7 +7,7 @@ }, "package": { "productName": "QuickView", - "version": "0.1.8" + "version": "0.1.9" }, "tauri": { "allowlist": {