diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..9640858
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,141 @@
+
+name: 'Tests'
+
+on:
+ push:
+
+ pull_request:
+ types: [opened, synchronize] #labeled, assigned]
+
+jobs:
+ build-and-test:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: [3.6, 3.8] # 2.7,
+
+ steps:
+ # --- Install steps
+ - name: Checkout
+ uses: actions/checkout@v2
+ with:
+ submodules: recursive
+
+ # --- Versioning
+ - name: Versioning
+ id: versioning
+ run: |
+ git fetch --unshallow
+ export VERSION_TAG=""
+ export VERSION_NAME=`git describe | sed 's/\(.*\)-.*/\1/'`
+ echo "GIT DESCRIBE: `git describe`"
+ echo "GITHUB_REF: $GITHUB_REF"
+ echo "VERSION_NAME $VERSION_NAME"
+ if [[ $GITHUB_REF == *"tags"* ]]; then export VERSION_TAG=${GITHUB_REF/refs\/tags\//} ; fi
+ echo "VERSION_TAG $VERSION_TAG"
+ if [[ "$VERSION_TAG" == "" ]]; then export VERSION_TAG="vdev" ; fi
+ if [[ "$VERSION_TAG" != "vdev" ]]; then export VERSION_NAME=$VERSION_TAG ; fi
+ echo "VERSION_NAME: $VERSION_NAME"
+ echo "VERSION_TAG : $VERSION_TAG"
+ if [[ "$VERSION_TAG" == "vdev" ]]; then export VERSION_NAME=$VERSION_NAME"-dev" ; fi
+ if [[ "$VERSION_TAG" != "vdev" ]]; then export FULL_VERSION_NAME="version $VERSION_NAME" ; fi
+ if [[ "$VERSION_TAG" == "vdev" ]]; then export FULL_VERSION_NAME="latest dev. version $VERSION_NAME" ; fi
+ echo "VERSION_NAME: $VERSION_NAME"
+ echo "FULL_VERSION_NAME: $FULL_VERSION_NAME"
+ echo "::set-output name=FULL_VERSION_NAME::$FULL_VERSION_NAME"
+ echo "::set-output name=VERSION_NAME::$VERSION_NAME"
+ echo "::set-output name=VERSION_TAG::$VERSION_TAG"
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install python dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r _tools/travis_requirements.txt
+ pip install -r weio/requirements.txt
+
+ - name: System info
+ run: |
+ echo "Actor : $GITHUB_ACTOR"
+ echo "Branch: $GITHUB_REF"
+ pip list
+ ls
+
+ # --- Run Tests
+ - name: Tests
+ run: |
+ make test
+
+
+ # --- Check if Deployment if needed
+ - name: Check if deployment is needed
+ id: check_deploy
+ env:
+ PY_VERSION: ${{matrix.python-version}}
+ GH_EVENT : ${{github.event_name}}
+ run: |
+ echo "GH_EVENT : $GH_EVENT"
+ echo "PY_VERSION : $PY_VERSION"
+ export OK=0
+ if [[ $PY_VERSION == "3.6" ]]; then export OK=1 ; fi
+ if [[ "$OK" == "1" && $GH_EVENT == "push" ]]; then export OK=1 ; fi
+ echo "DEPLOY : $OK"
+ echo "::set-output name=GO::$OK"
+
+ # --- Run Deployments
+ - name: Install system dependencies
+ if: ${{ steps.check_deploy.outputs.GO == '1'}}
+ run: sudo apt-get install nsis
+
+
+ - name: Before deploy
+ if: ${{ steps.check_deploy.outputs.GO == '1'}}
+ id: before_deploy
+ env:
+ FULL_VERSION_NAME: ${{steps.versioning.outputs.FULL_VERSION_NAME}}
+ VERSION_NAME: ${{steps.versioning.outputs.VERSION_NAME}}
+ VERSION_TAG: ${{steps.versioning.outputs.VERSION_TAG}}
+ run: |
+ echo "FULL_VERSION_NAME: $FULL_VERSION_NAME"
+ echo "VERSION_NAME : $VERSION_NAME"
+ echo "VERSION_TAG : $VERSION_TAG"
+ pip install pynsist
+ pip install distlib
+ git clone https://github.com/takluyver/pynsist
+ mv pynsist/nsist nsist
+ sh _tools/setVersion.sh $VERSION_NAME
+ make installer
+ mv build/nsis/pyDatView_setup.exe "pyDatView_"$VERSION_NAME"_setup.exe"
+ mv _tools/pyDatView.cmd build/nsis/
+ mv _tools/pyDatView_Test.bat build/nsis/
+ mv _tools/pyDatView.exe build/nsis/
+ mv build/nsis build/pyDatView_$VERSION_NAME
+ cd build && zip -r "../pyDatView_"$VERSION_NAME"_portable.zip" pyDatView_$VERSION_NAME
+ cd ..
+ if [[ "$VERSION_TAG" == "vdev" ]]; then cp "pyDatView_"$VERSION_NAME"_setup.exe" "pyDatView_LatestVersion_setup.exe" ;fi
+ if [[ "$VERSION_TAG" == "vdev" ]]; then cp "pyDatView_"$VERSION_NAME"_portable.zip" "pyDatView_LatestVersion_portable.zip" ;fi
+ ls
+
+ - name: Deploy
+ if: ${{ steps.check_deploy.outputs.GO == '1'}}
+ env:
+ FULL_VERSION_NAME: ${{steps.versioning.outputs.FULL_VERSION_NAME}}
+ VERSION_NAME: ${{steps.versioning.outputs.VERSION_NAME}}
+ VERSION_TAG: ${{steps.versioning.outputs.VERSION_TAG}}
+ uses: svenstaro/upload-release-action@v2
+ with:
+ repo_token: ${{ secrets.GITHUB_TOKEN }}
+ file: pyDatView_*.*
+ release_name: ${{steps.versioning.outputs.FULL_VERSION_NAME}}
+ tag: ${{steps.versioning.outputs.VERSION_TAG}}
+ overwrite: true
+ file_glob: true
+ body: |
+ Different development versions are found in the "Assets" below.
+
+ Select the one with the highest number to get the latest development version.
+
+ Use a file labelled "setup" for a windows installer. No admin right is required for this installation, but the application is not signed. You may use a file labelled "portable" for a self contained zip files.
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 962d72d..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,58 +0,0 @@
-# Travis-CI file for pyDatView
-language: python
-
-python:
- - "2.7"
- - "3.6"
- - "3.7"
- - "3.8"
-os:
- - linux
-sudo: true
-
-install:
-# - sudo apt-get install python-wxgtk3.0
- - sudo apt-get install nsis
- - pip install -r _tools/travis_requirements.txt
- - pip install -r weio/requirements.txt
-
-script:
- - make test
- - if [[ "$TRAVIS_TAG" == "" ]]; then export TRAVIS_TAG="vdev" ; fi
-
-before_deploy:
- - git fetch --unshallow
- - export VERSION_NAME=`git describe | sed 's/\(.*\)-.*/\1/'`
- - export FULL_VERSION_NAME="version $VERSION_NAME"
- - if [[ "$TRAVIS_TAG" == "vdev" ]]; then export VERSION_NAME=$VERSION_NAME"-dev" ; fi
- - if [[ "$TRAVIS_TAG" == "vdev" ]]; then export FULL_VERSION_NAME="latest dev. version $VERSION_NAME" ; fi
- - echo $VERSION_NAME
- - pip install pynsist
- - pip install distlib
- - git clone https://github.com/takluyver/pynsist
- - mv pynsist/nsist nsist
- - make installer
- - mv build/nsis/pyDatView.exe "pyDatView_"$VERSION_NAME"_setup.exe"
- - mv _tools/pyDatView.cmd build/nsis/
- - mv _tools/pyDatView.exe build/nsis/
- - mv build/nsis build/pyDatView_$VERSION_NAME
- - cd build && zip -r "../pyDatView_"$VERSION_NAME"_portable.zip" pyDatView_$VERSION_NAME
- - cd ..
- - ls
-
-deploy:
- provider: releases
- api_key: $GITHUB_TOKEN
- file_glob: true
- overwrite: true
- skip_cleanup: true
- file:
- - pyDatView*.exe
- - pyDatView*.zip
- name: $FULL_VERSION_NAME
- target_commitish: $TRAVIS_COMMIT
- tag_name: $TRAVIS_TAG
- on:
- tags: true
- branch: master
- condition: $TRAVIS_PYTHON_VERSION = 3.6
diff --git a/Makefile b/Makefile
index 491d533..f79965f 100644
--- a/Makefile
+++ b/Makefile
@@ -1,85 +1,87 @@
-# --- Detecting OS
-ifeq '$(findstring ;,$(PATH))' ';'
- detected_OS := Windows
-else
- detected_OS := $(shell uname 2>/dev/null || echo Unknown)
- detected_OS := $(patsubst CYGWIN%,Cygwin,$(detected_OS))
- detected_OS := $(patsubst MSYS%,MSYS,$(detected_OS))
- detected_OS := $(patsubst MINGW%,MSYS,$(detected_OS))
-endif
-
-testfile= weio/weio/tests/example_files/FASTIn_arf_coords.txt
-all:
-ifeq ($(detected_OS),Darwin) # Mac OS X
- ./pythonmac pyDatView.py $(testfile)
-else
- python pyDatView.py $(testfile)
-endif
-
-
-
-
-deb:
- python DEBUG.py
-
-install:
- python setup.py install
-
-dep:
- python -m pip install -r requirements.txt
-
-pull:
- git pull --recurse-submodules
-update:pull
-
-
-help:
- @echo "Available rules:"
- @echo " all run the standalone program"
- @echo " install install the python package in the system"
- @echo " dep download the dependencies "
- @echo " pull download the latest version "
- @echo " test run the unit tests "
-
-test:
-ifeq ($(detected_OS),Darwin) # Mac OS X
- ./pythonmac -m unittest discover -v tests
-else
- python -m unittest discover -v tests
-endif
-
-prof:
- python -m cProfile -o tests/prof_all.prof tests/prof_all.py
- python -m pyprof2calltree -i tests/prof_all.prof -o tests/callgrind.prof_all.prof
- snakeviz tests/prof_all.prof
-
-
-exe:
- python -m nuitka --follow-imports --include-plugin-directory --include-plugin-files --show-progress --show-modules --output-dir=build-nuitka pyDatView.py
-
-exestd:
- python -m nuitka --python-flag=no_site --assume-yes-for-downloads --standalone --follow-imports --include-plugin-directory --include-plugin-files --show-progress --show-modules --output-dir=build-nuitka-std pyDatView.py
-
-clean:
- rm -rf __pycache__
- rm -rf *.egg-info
- rm -rf *.spec
- rm -rf build*
- rm -rf dist
-
-
-pyexe:
- pyinstaller --onedir pyDatView.py
-
-version:
-ifeq ($(OS),Windows_NT)
- @echo "Doing nothing"
-else
- @sh _tools/setVersion.sh
-endif
-
-installer: version
- python -m nsist installer.cfg
-
-
-
+# --- Detecting OS
+ifeq '$(findstring ;,$(PATH))' ';'
+ detected_OS := Windows
+else
+ detected_OS := $(shell uname 2>/dev/null || echo Unknown)
+ detected_OS := $(patsubst CYGWIN%,Cygwin,$(detected_OS))
+ detected_OS := $(patsubst MSYS%,MSYS,$(detected_OS))
+ detected_OS := $(patsubst MINGW%,MSYS,$(detected_OS))
+endif
+
+testfile= weio/weio/tests/example_files/FASTIn_arf_coords.txt
+all:
+ifeq ($(detected_OS),Darwin) # Mac OS X
+ ./pythonmac pyDatView.py $(testfile)
+else
+ python pyDatView.py $(testfile)
+endif
+
+
+
+
+deb:
+ python DEBUG.py
+
+install:
+ python setup.py install
+
+dep:
+ python -m pip install -r requirements.txt
+
+pull:
+ git pull --recurse-submodules
+update:pull
+
+
+help:
+ @echo "Available rules:"
+ @echo " all run the standalone program"
+ @echo " install install the python package in the system"
+ @echo " dep download the dependencies "
+ @echo " pull download the latest version "
+ @echo " test run the unit tests "
+
+test:
+ifeq ($(detected_OS),Darwin) # Mac OS X
+ ./pythonmac -m unittest discover -v tests
+ ./pythonmac -m unittest discover -v pydatview/plugins/tests
+else
+ python -m unittest discover -v tests
+ python -m unittest discover -v pydatview/plugins/tests
+endif
+
+prof:
+ python -m cProfile -o tests/prof_all.prof tests/prof_all.py
+ python -m pyprof2calltree -i tests/prof_all.prof -o tests/callgrind.prof_all.prof
+ snakeviz tests/prof_all.prof
+
+
+exe:
+ python -m nuitka --follow-imports --include-plugin-directory --include-plugin-files --show-progress --show-modules --output-dir=build-nuitka pyDatView.py
+
+exestd:
+ python -m nuitka --python-flag=no_site --assume-yes-for-downloads --standalone --follow-imports --include-plugin-directory --include-plugin-files --show-progress --show-modules --output-dir=build-nuitka-std pyDatView.py
+
+clean:
+ rm -rf __pycache__
+ rm -rf *.egg-info
+ rm -rf *.spec
+ rm -rf build*
+ rm -rf dist
+
+
+pyexe:
+ pyinstaller --onedir pyDatView.py
+
+version:
+ifeq ($(OS),Windows_NT)
+ @echo "Doing nothing"
+else
+ @sh _tools/setVersion.sh
+endif
+
+installer:
+ python -m nsist installer.cfg
+
+
+
diff --git a/README.md b/README.md
index 4e5c053..637ab40 100644
--- a/README.md
+++ b/README.md
@@ -1,205 +1,247 @@
-[![Build Status](https://travis-ci.com/ebranlard/pyDatView.svg?branch=master)](https://travis-ci.com/ebranlard/pyDatView)
-
-
-
-
-# pyDatView
-
-A crossplatform GUI to display tabulated data from files or python pandas dataframes. It's compatible Windows, Linux and MacOS, python 2 and python 3. Some of its features are: multiples plots, FFT plots, probability plots, export of figures...
-The file formats supported, are: CSV files and other formats present in the [weio](http://github.com/ebranlard/weio/) library.
-Additional file formats can easily be added.
-
-![Scatter](/../screenshots/screenshots/PlotScatter.png)
-
-## QuickStart
-For **Windows** users, an installer executable is available [here](https://github.com/ebranlard/pyDatView/releases) (look for the latest pyDatView\*.exe)
-
-**Linux** and **MacOS** users can use the command lines below. **Linux** users may need to install the package python-wxgtk\* (e.g. `python-gtk3.0`) from their distribution. **MacOS** users can use a `brew`, `anaconda` or `virtualenv` version of python and pip, but the final version of python that calls the script needs to have access to the screen (see [details for MacOS](#macos-installation)). The main commands for **Linux** and **MacOS** users are:
-```bash
-git clone --recurse-submodules https://github.com/ebranlard/pyDatView
-cd pyDatView
-python -m pip install --user -r requirements.txt
-make # executes: 'python pyDatView.py' (on linux) or './pythonmac pyDatView.py' (on Mac)
-```
-More information about the download, requirements and installation is provided [further down this page](#installation)
-
-
-## Usage
-### Launching the GUI
-Windows users that used a `setup.exe` file should be able to look for `pyDatView` in the Windows menu, then launch it, and pin the program to the taskbar for easier access.
-
-If you cloned this repository, the main script at the root (`pyDatView.py`) is executable and will open the GUI directly. A command line interface is provided, e.g.:
-```bash
-pyDatView file.csv
-```
-The python package can also be used directly from python/jupyter to display a dataframe or show the data in a file
-```python
-import pydatview
-pydatview.show(dataframe=df)
-# OR
-pydatview.show(filenames=['file.csv'])
-# OR
-pydatview.show('file.csv')
-```
-### Workflow
-Documentation is scarce for now, but here are some tips for using the program:
- - You can drag and drop files to the GUI directly to open them. Hold the Ctrl key to add.
- - You can open several files at once, with same or different filetypes. Upon opening multiple files, a new table appears with the list of open files.
- - To add multiple channels or data from multiple files to a plot, use `ctrl + click` or shift-click to make selections.
- - Look for the menus indicated by the "sandwich" symbol (3 horizontal bars ). These menus are also accessible with right clicks.
- - The menus will allow you to edit tables (rename, delete them), add or remove columns (for instance to convert a signal from one unit to another unit), or change the values displayed in the information table at the bottom.
- - Few options are also available in the menus `data` and `tools` located at the top of the program.
- - The modes and file format drop down menus at the top can usually be kept on `auto`. If a file cannot be read, pay attention to the file extension used, and possibly select a specific file format in the dropdown menu instead of `auto`.
-
-
-
-## Features
-Main features:
-- Plot of tabular data within a file
-- Automatic detection of fileformat (based on [weio](http://github.com/ebranlard/weio/) but possibility to add more formats)
-- Reload of data (e.g. on file change)
-- Display of statistics
-- Export figure as pdf, png, eps, svg
-
-Different kind of plots:
-- Scatter plots or line plots
-- Multiple plots using sub-figures or a different colors
-- Probability density function (PDF) plot
-- Fast Fourier Transform (FFT) plot
-
-Plot options:
-- Logarithmic scales on x and y axis
-- Scaling of data between 0 and 1 using min and max
-- Synchronization of the x-axis of the sub-figures while zooming
-
-Data manipulation options:
- - Remove columns in a table, add columns using a given formula, and export the table to csv
- - Mask part of the data (for instance selecting times above a certain value to remove the transient). Apply the mask temporarily, or create a new table from it
- - Estimate logarithmic decrement from a signal tthat is decaying
- - Extract radial data from OpenFAST input files
-
-
-
-## Screenshots
-
-Scatter plot (by selecting `Scatter`) and several plots on the same figure:
-
-![Scatter](/../screenshots/screenshots/PlotScatter.png)
-
-
-
-Fast Fourier Transform of the signals (by selecting `FFT`) and displaying several plots using subfigures (by selecting `Subplot`).
-
-![SubPlotFFT](/../screenshots/screenshots/SubPlotFFT.png)
-
-Probability density function:
-
-![PlotPDF](/../screenshots/screenshots/PlotPDF.png)
-
-Scaling all plots between 0 and 1 (by selecting `MinMax`)
-![PlotMinMax](/../screenshots/screenshots/PlotMinMax.png)
-
-
-
-## Installation
-
-### Windows installation
-For Windows users, installer executables are available [here](https://github.com/ebranlard/pyDatView/releases) (look for the latest pyDatView\*.exe)
-
-### Linux installation
-The script is compatible python 2.7 and python 3 and relies on the following python packages: `numpy` `matplotlib`, `pandas`, `wxpython`.
-To download the code and install the dependencies (with pip) run the following:
-```bash
-git clone --recurse-submodules https://github.com/ebranlard/pyDatView
-cd pyDatView
-python -m pip install --user -r requirements.txt
-```
-If the installation of `wxpython` fails, you may need to install the package python-wxgtk\* (e.g. `python-gtk3.0`) from your distribution. For Debian/Ubuntu systems, try:
-`sudo apt-get install python-wxgtk3.0`.
-For further troubleshooting you can check the [wxPython wiki page](https://wiki.wxpython.org/).
-
-If the requirements are successfully installed you can run pyDatView by typing:
-```bash
-python pyDatView.py
-```
-To easily access it later, you can add an alias to your `.bashrc` or install the pydatview module:
-```bash
-echo "alias pydat='python `pwd`/pyDatview.py'" >> ~/.bashrc
-# or
-python setup.py install
-```
-
-
-## MacOS installation
-The installation works with python2 and python3, with `brew` (with or without a `virtualenv`) or `anaconda`.
-First, download the source code:
-```bash
-git clone --recurse-submodules https://github.com/ebranlard/pyDatView
-cd pyDatView
-```
-Before installing the requirements, you need to be aware of the two following issues with MacOS:
-- If you are using the native version of python, there is an incompatibility between the native version of `matplotlib` on MacOS and the version of `wxpython`. The solution is to use `virtualenv`, `brew` or `anaconda`.
-- To use a GUI app, you need a python program that has access to the screen. These special python programs are in different locations. For the system-python, it's usually in `/System`, the `brew` versions are usually in `/usr/local/Cellar`, and the `anaconda` versions are usually called `python.app`.
-The script `pythonmac` provided in this repository attempt to find the correct python program depending if you are in a virtual environment, in a conda environment, a system-python or a python from brew or conda.
-
-Different solutions are provided below depending on your preferred way of working.
-
-### Brew-python version (outside of a virtualenv)
-If you have `brew` installed, and you installed python with `brew install python`, then the easiest is to use your `python3` version:
-```
-python3 -m pip install --user -r requirements.txt
-python3 pyDatView.py
-```
-
-### Brew-python version (inside a virtualenv)
-If you are inside a virtualenv, with python 2 or 3, use:
-```
-pip install -r requirements.txt
-./pythonmac pyDatView.py
-```
-If the `pythonmac` commands fails, contact the developer, and in the meantime try to replace it with something like:
-```
-$(brew --prefix)/Cellar/python/XXXXX/Frameworks/python.framework/Versions/XXXX/bin/pythonXXX
-```
-where the result from `brew --prefix` is usually `/usr/loca/` and the `XXX` above corresponds to the version of python you are using in your virtual environment.
-
-
-### Anaconda-python version (outside a virtualenv)
-The installation of anaconda sometimes replaces the system python with the anaconda version of python. You can see that by typing `which python`. Use the following:
-```
-python -m pip install --user -r requirements.txt
-./pythonmac pyDatView.py
-```
-If the `pythonmac` commands fails, contact the developer, and in the meantime try to replace it with a path similar to
-```bash
-/anaconda3/bin/python.app
-```
-where `/anaconda3/bin/` is the path that would be returned by the command `which conda`. Note the `.app` at the end. If you don't have `python.app`, try installing it with `conda install -c anaconda python.app`
-
-
-### Easy access
-To easily access the program later, you can add an alias to your `.bashrc` or install the pydatview module:
-```bash
-echo "alias pydat='python `pwd`/pyDatview.py'" >> ~/.bashrc
-# or
-python setup.py install
-```
-
-
-
-
-
-## Adding more file formats
-File formats can be added by implementing a subclass of `weio/File.py`, for instance `weio/VTKFile.py`. Existing examples are found in the folder `weio`.
-Once implemented the fileformat needs to be registered in `weio/__init__.py` by adding an import line at the beginning of this script and adding a line in the function `fileFormats()` of the form `formats.append(FileFormat(VTKFile))`
-
-
-
-## Contributing
-Any contributions to this project are welcome! If you find this project useful, you can also buy me a coffee (donate a small amount) with the link below:
-
-
-
-
-
-
+[![Build status](https://github.com/ebranlard/pyDatView/workflows/Tests/badge.svg)](https://github.com/ebranlard/pyDatView/actions?query=workflow%3A%22Tests%22)
+
+
+
+
+# pyDatView
+
+A crossplatform GUI to display tabulated data from files or python pandas dataframes. It's compatible Windows, Linux and MacOS, python 2 and python 3. Some of its features are: multiples plots, FFT plots, probability plots, export of figures...
+The file formats supported, are: CSV files and other formats present in the [weio](http://github.com/ebranlard/weio/) library.
+Additional file formats can easily be added.
+
+![Scatter](/../screenshots/screenshots/PlotScatter.png)
+
+## QuickStart
+For **Windows** users, an installer executable is available [here](https://github.com/ebranlard/pyDatView/releases) (look for the latest pyDatView\*.exe)
+
+**Linux** and **MacOS** users can use the command lines below. **Linux** users may need to install the package python-wxgtk\* (e.g. `python-gtk3.0`) from their distribution. **MacOS** users can use a `brew`, `anaconda` or `virtualenv` version of python and pip, but the final version of python that calls the script needs to have access to the screen (see [details for MacOS](#macos-installation)). The main commands for **Linux** and **MacOS** users are:
+```bash
+git clone --recurse-submodules https://github.com/ebranlard/pyDatView
+cd pyDatView
+python -m pip install --user -r requirements.txt
+make # executes: 'python pyDatView.py' (on linux) or './pythonmac pyDatView.py' (on Mac)
+echo "alias pydat='make -C `pwd`'" >> ~/.bashrc
+```
+More information about the download, requirements and installation is provided [further down this page](#installation)
+
+
+## Usage
+### Launching the GUI
+Windows users that used a `setup.exe` file should be able to look for `pyDatView` in the Windows menu, then launch it, and pin the program to the taskbar for easier access.
+
+If you cloned this repository, the main script at the root (`pyDatView.py`) is executable and will open the GUI directly. A command line interface is provided, e.g.:
+```bash
+pyDatView file.csv
+```
+The python package can also be used directly from python/jupyter to display a dataframe or show the data in a file
+```python
+import pydatview
+pydatview.show(dataframe=df)
+# OR
+pydatview.show(filenames=['file.csv'])
+# OR
+pydatview.show('file.csv')
+```
+
+### Quicklaunch/Shortcut
+**Windows**
+ - If you used the `setup.exe`, you will find the `pyDatView` App in the windows menu, you can launch it from there, pin it to start, pin it to the startbar, open the file location to
+ - If you used the portable version, you'll find `pyDatView.exe` at the root of the directory. You can launch it and pin it to your taskbar. You can also right click, and create a short cut to add to your desktop or start menu.
+ - If you clone the repository, you can create a shortcut at the root of the repository. In explorer, right click on an empty space, select New , Shortcut. Set the shortcut as follows:
+```
+ "C:\PYTHON_DIR\PythonXX\pythonw.exe" "C:\INSTALL_DIR\pyDatView\pyDatView.launch.pyw"
+```
+
+**Linux**
+
+You can add an alias to your bashrc as follows. Navigate to the root of the pyDatView repository, and type:
+```
+ echo "alias pydat='python `pwd`/pyDatView.py'" >> ~/.bashrc
+```
+Next time you open a terminal, you can type `pydat` to launch pyDatView.
+Adapt to another terminal like `.shrc`
+
+**MacOS**
+
+The procedure is the same as for linux, the small issue is that you need to find the "proper" python to call. When you run `./pythonmac` from the root of the directory, the script tries to find the right version for you and finishes by showing a line of the form: `[INFO] Using: /PATH/TO/PYTHON `. This line gives you the path to python. Add pydat as an alias by running the line below (after adapting the `PATH/TO/PYTHON`):
+```
+ echo "alias pydat='PATH/TO/PYTHON `pwd`/pyDatView.py'" >> ~/.zshrc
+```
+Next time you open a terminal, you can type `pydat` to launch pyDatView.
+
+
+
+### File association
+**Windows**
+
+To associate a given file type with pyDatView, follow the following steps:
+
+1. Locate `pyDatView.exe`. If you installed using `setup.exe` or the portable `zip`, you'll find `pyDatView.exe` at the root of the installation folder (default is `C:\Users\%USERNAME%\AppData\Local\pyDatView\`). If you cannot find the exe, download it from [the repository](/_tools/pyDatView.exe). If you cloned the repository, you'll find the executable in the subfolder `_tools\` of the repository.
+
+2. Verify that the exe works. Double click on the executable to verify that it lauches pyDatView. If it doesnt, run it from a terminal and look at the outputs.
+
+3. Add the file association. Right click on a file you want to associate pyDatView with. Select "Open With" > "More Apps" > scroll to "Look for another App on my PC" > Navigate to the location of `pyDatView.exe` mentioned above. If this works, repeat the operation and check the box "Always use this App for his filetype".
+
+
+### Workflow
+Documentation is scarce for now, but here are some tips for using the program:
+ - You can drag and drop files to the GUI directly to open them. Hold the Ctrl key to add.
+ - You can open several files at once, with same or different filetypes. Upon opening multiple files, a new table appears with the list of open files.
+ - To add multiple channels or data from multiple files to a plot, use `ctrl + click` or shift-click to make selections.
+ - Look for the menus indicated by the "sandwich" symbol (3 horizontal bars ). These menus are also accessible with right clicks.
+ - The menus will allow you to edit tables (rename, delete them), add or remove columns (for instance to convert a signal from one unit to another unit), or change the values displayed in the information table at the bottom.
+ - Few options are also available in the menus `data` and `tools` located at the top of the program.
+ - The modes and file format drop down menus at the top can usually be kept on `auto`. If a file cannot be read, pay attention to the file extension used, and possibly select a specific file format in the dropdown menu instead of `auto`.
+
+
+
+## Features
+Main features:
+- Plot of tabular data within a file
+- Automatic detection of fileformat (based on [weio](http://github.com/ebranlard/weio/) but possibility to add more formats)
+- Reload of data (e.g. on file change)
+- Display of statistics
+- Export figure as pdf, png, eps, svg
+
+Different kind of plots:
+- Scatter plots or line plots
+- Multiple plots using sub-figures or a different colors
+- Probability density function (PDF) plot
+- Fast Fourier Transform (FFT) plot
+
+Plot options:
+- Logarithmic scales on x and y axis
+- Scaling of data between 0 and 1 using min and max
+- Synchronization of the x-axis of the sub-figures while zooming
+
+Data manipulation options:
+ - Remove columns in a table, add columns using a given formula, and export the table to csv
+ - Mask part of the data (for instance selecting times above a certain value to remove the transient). Apply the mask temporarily, or create a new table from it
+ - Estimate logarithmic decrement from a signal tthat is decaying
+ - Extract radial data from OpenFAST input files
+
+
+
+## Screenshots
+
+Scatter plot (by selecting `Scatter`) and several plots on the same figure:
+
+![Scatter](/../screenshots/screenshots/PlotScatter.png)
+
+
+
+Fast Fourier Transform of the signals (by selecting `FFT`) and displaying several plots using subfigures (by selecting `Subplot`).
+
+![SubPlotFFT](/../screenshots/screenshots/SubPlotFFT.png)
+
+Probability density function:
+
+![PlotPDF](/../screenshots/screenshots/PlotPDF.png)
+
+Scaling all plots between 0 and 1 (by selecting `MinMax`)
+![PlotMinMax](/../screenshots/screenshots/PlotMinMax.png)
+
+
+
+## Installation
+
+### Windows installation
+For Windows users, installer executables are available [here](https://github.com/ebranlard/pyDatView/releases) (look for the latest pyDatView\*.exe)
+
+### Linux installation
+The script is compatible python 2.7 and python 3 and relies on the following python packages: `numpy` `matplotlib`, `pandas`, `wxpython`.
+To download the code and install the dependencies (with pip) run the following:
+```bash
+git clone --recurse-submodules https://github.com/ebranlard/pyDatView
+cd pyDatView
+python -m pip install --user -r requirements.txt
+```
+If the installation of `wxpython` fails, you may need to install the package python-wxgtk\* (e.g. `python-gtk3.0`) from your distribution. For Debian/Ubuntu systems, try:
+`sudo apt-get install python-wxgtk3.0`.
+For further troubleshooting you can check the [wxPython wiki page](https://wiki.wxpython.org/).
+
+If the requirements are successfully installed you can run pyDatView by typing:
+```bash
+python pyDatView.py
+```
+To easily access it later, you can add an alias to your `.bashrc` or install the pydatview module:
+```bash
+echo "alias pydat='python `pwd`/pyDatview.py'" >> ~/.bashrc
+# or
+python setup.py install
+```
+
+
+## MacOS installation
+The installation works with python2 and python3, with `brew` (with or without a `virtualenv`) or `anaconda`.
+First, download the source code:
+```bash
+git clone --recurse-submodules https://github.com/ebranlard/pyDatView
+cd pyDatView
+```
+Before installing the requirements, you need to be aware of the two following issues with MacOS:
+- If you are using the native version of python, there is an incompatibility between the native version of `matplotlib` on MacOS and the version of `wxpython`. The solution is to use `virtualenv`, `brew` or `anaconda`.
+- To use a GUI app, you need a python program that has access to the screen. These special python programs are in different locations. For the system-python, it's usually in `/System`, the `brew` versions are usually in `/usr/local/Cellar`, and the `anaconda` versions are usually called `python.app`.
+The script `pythonmac` provided in this repository attempt to find the correct python program depending if you are in a virtual environment, in a conda environment, a system-python or a python from brew or conda.
+
+Different solutions are provided below depending on your preferred way of working.
+
+### Brew-python version (outside of a virtualenv)
+If you have `brew` installed, and you installed python with `brew install python`, then the easiest is to use your `python3` version:
+```
+python3 -m pip install --user -r requirements.txt
+python3 pyDatView.py
+```
+
+### Brew-python version (inside a virtualenv)
+If you are inside a virtualenv, with python 2 or 3, use:
+```
+pip install -r requirements.txt
+./pythonmac pyDatView.py
+```
+If the `pythonmac` commands fails, contact the developer, and in the meantime try to replace it with something like:
+```
+$(brew --prefix)/Cellar/python/XXXXX/Frameworks/python.framework/Versions/XXXX/bin/pythonXXX
+```
+where the result from `brew --prefix` is usually `/usr/loca/` and the `XXX` above corresponds to the version of python you are using in your virtual environment.
+
+
+### Anaconda-python version (outside a virtualenv)
+The installation of anaconda sometimes replaces the system python with the anaconda version of python. You can see that by typing `which python`. Use the following:
+```
+python -m pip install --user -r requirements.txt
+./pythonmac pyDatView.py
+```
+If the `pythonmac` commands fails, contact the developer, and in the meantime try to replace it with a path similar to
+```bash
+/anaconda3/bin/python.app
+```
+where `/anaconda3/bin/` is the path that would be returned by the command `which conda`. Note the `.app` at the end. If you don't have `python.app`, try installing it with `conda install -c anaconda python.app`
+
+
+### Easy access
+To easily access the program later, you can add an alias to your `.bashrc` or install the pydatview module:
+```bash
+echo "alias pydat='python `pwd`/pyDatview.py'" >> ~/.bashrc
+# or
+python setup.py install
+```
+
+
+
+
+
+## Adding more file formats
+File formats can be added by implementing a subclass of `weio/File.py`, for instance `weio/VTKFile.py`. Existing examples are found in the folder `weio`.
+Once implemented the fileformat needs to be registered in `weio/__init__.py` by adding an import line at the beginning of this script and adding a line in the function `fileFormats()` of the form `formats.append(FileFormat(VTKFile))`
+
+
+
+## Contributing
+Any contributions to this project are welcome! If you find this project useful, you can also buy me a coffee (donate a small amount) with the link below:
+
+
+
+
+
+
diff --git a/_tools/Makefile b/_tools/Makefile
index 601adc2..b813bfe 100644
--- a/_tools/Makefile
+++ b/_tools/Makefile
@@ -1,8 +1,17 @@
-all: compile test
-
-compile:
- cl pyDatView.c
-
-test:
- @echo --------------------------------------------------
- ./pyDatView.exe AA BB CC
+all: compile test
+
+compile:
+ @echo --------------------------------------------------------------------------
+ @cl /nologo -c pyDatView.c
+ @echo --------------------------------------------------------------------------
+ @rc /nologo pyDatView.rc
+ @echo --------------------------------------------------------------------------
+ @link /nologo pyDatView.obj pyDatView.res /out:pyDatView.exe
+ @rm *.obj *.res
+
+test:
+ @echo --------------------------------------------------------------------------
+ ./pyDatView.exe C:\Bin\test.outb BB CC
+
+clean:
+ rm *.obj *.res pyDatView.exe
diff --git a/_tools/NewRelease.md b/_tools/NewRelease.md
new file mode 100644
index 0000000..4f14fc9
--- /dev/null
+++ b/_tools/NewRelease.md
@@ -0,0 +1,9 @@
+
+
+Steps:
+- Change PROG\_VERSION in pydateview/main.py
+- Change version in installler.cfg
+- Commit changes and push to pull request
+- Merge pull request from old to new version
+- Tag new version v0.x `git tag -a v0.x`
+- Push tags: `git push --tags`
diff --git a/_tools/pyDatView.c b/_tools/pyDatView.c
index 15f9cf9..c66b963 100644
--- a/_tools/pyDatView.c
+++ b/_tools/pyDatView.c
@@ -1,85 +1,244 @@
-#define _WIN32_WINNT 0x0500
-#define MAX 1024
-
-#include
-#include
-#include
-
-#include
-#pragma comment(lib,"User32.lib") // for static linking and using ShowWindow
-
-char* concat(const char *s1, const char *s2)
-{
- char *result = malloc(strlen(s1) + strlen(s2) + 1); // +1 for the null-terminator
- strcpy(result, s1);
- strcat(result, s2);
- return result;
-}
-
-void concatenate(char p[], char q[]) {
- int c, d;
- c = 0;
- while (p[c] != '\0') {
- c++;
- }
- d = 0;
- while (q[d] != '\0') {
- p[c] = q[d];
- d++;
- c++;
- }
- p[c] = '\0';
-}
-
-
-int main (int argc, char** argv) {
- char mainCommand[] = "\\Python\\pythonw.exe -c \"import pydatview; pydatview.show();\"";
- char wd[MAX];
- char args[MAX]="";
- char fullCommand[MAX]="";
- char path [MAX_PATH];
- char* pfullCommand ;
-
- // --- Hidding window
- HWND hWnd = GetConsoleWindow();
- ShowWindow( hWnd, SW_MINIMIZE ); //won't hide the window without SW_MINIMIZE
- //ShowWindow( hWnd, SW_HIDE );
-
- // --- List of argumenst to python list of string
- int i;
- concatenate(args, "[");
- for(i = 1; i <= argc-1; i++) {
- concatenate(args, "'");
- concatenate(args, argv[i]);
- concatenate(args, "'");
- if (i
+#include
+#include
+#include // stat
+#include // bool type
+
+
+#include
+#pragma comment(lib,"User32.lib") // for static linking and using ShowWindow
+
+
+/* True if file exists */
+bool file_exists (char *filename) {
+ struct stat buffer;
+ return (stat (filename, &buffer) == 0);
+}
+
+/* Returns path of current executable */
+char* getpath()
+{
+ TCHAR path[MAX];
+ DWORD length;
+ length = GetModuleFileName(NULL, path, 1000);
+ char *result = malloc(length+ 1); // +1 for the null-terminator
+ int c =0 ;
+ while (path[c] != '\0') {
+ result[c] = path[c] ;
+ c++;
+ }
+ result[c] = '\0';
+ //char *result = path;
+ return result;
+}
+
+
+/* Concatenate two strings */
+char* concat(const char *s1, const char *s2)
+{
+ char *result = malloc(strlen(s1) + strlen(s2) + 1); // +1 for the null-terminator
+ strcpy(result, s1);
+ strcat(result, s2);
+ return result;
+}
+
+/* Concatenate string2 to string1 */
+void concatenate(char p[], char q[]) {
+ int c, d;
+ c = 0;
+ while (p[c] != '\0') {
+ c++;
+ }
+ d = 0;
+ while (q[d] != '\0') {
+ p[c] = q[d];
+ d++;
+ c++;
+ }
+ p[c] = '\0';
+}
+
+
+/* Launch pydatview using local pythonw and command line arguments*/
+int main (int argc, char** argv) {
+ char wd[MAX];
+ char args[MAX]="";
+ char fullCommand[MAX]="";
+ char path [MAX_PATH]="";
+ char pydatpy[MAX_PATH]="";
+ char pythonwpath0[MAX_PATH]="";
+ char pythonwpath1[MAX_PATH]="";
+ char pythonwpath2[MAX_PATH]="";
+ char pythonwpath3[MAX_PATH]="";
+ char pythonwpath4[MAX_PATH]="";
+ char* pfullCommand ;
+ bool useImport = true;
+ int index=0;
+
+ // --- Hidding window
+ HWND hWnd = GetConsoleWindow();
+ //ShowWindow( hWnd, SW_MINIMIZE ); //won't hide the window without SW_MINIMIZE
+ ShowWindow( hWnd, SW_HIDE );
+
+ // --- Get user name (for AppData path)
+ char* user = getenv("USERNAME");
+ printf("Username : %s\n", user);
+
+ // --- Executable path
+ char * exename = getpath();
+ printf("Exe name : %s\n", exename);
+ char *exedir = exename;
+ exedir[strlen(exedir) - 13] = '\0'; // remove pyDatView.exe from path
+ char parentdir[7];
+ strncpy(parentdir, &exedir[strlen(exedir)-7],6);
+ printf("Exe dir : %s\n", exedir);
+ printf("Exe dir-7 : %s\n", parentdir);
+
+ // --- Current directory
+ wd[MAX-1] = '\0';
+ if(getcwd(wd, MAX-1) == NULL) {
+ printf ("[WARN] Can not get current working directory\n");
+ wd[0] = '.';
+ }
+ printf("Current Dir: %s\n", wd);
+
+ // --- Get PYDATPATH if defined as env variable
+ char* pydatpath = getenv("PYDATPATH");
+ if (pydatpath) {
+ printf("PYDATPATH : %s\n", pydatpath);
+ }else{
+ printf("PYDATPATH : (environmental variable not set)\n");
+ }
+
+ // --- Pythonw path (assuming it's in system path)
+ concatenate(pythonwpath1, "pythonw ");
+ printf("Pythonw1 : %s\n", pythonwpath1);
+
+
+ // --- Pythonw path (assuming close to current executable)
+ concatenate(pythonwpath2, exedir);
+ concatenate(pythonwpath2,"Python\\pythonw.exe");
+ printf("Pythonw2 : %s\n", pythonwpath2);
+
+ // --- Pythonw path (assuming user installed in AppData)
+ concatenate(pythonwpath3,"C:\\Users\\");
+ concatenate(pythonwpath3,user);
+ concatenate(pythonwpath3,"\\AppData\\Local\\pyDatView\\Python\\pythonw.exe");
+ printf("Pythonw3 : %s\n", pythonwpath3);
+
+ // --- Pythonw path (using PYDATPATH env variable)
+ if (pydatpath) {
+ concatenate(pythonwpath4, pydatpath);
+ concatenate(pythonwpath4,"\\Python\\pythonw.exe");
+ printf("Pythonw4 : %s\n", pythonwpath4);
+ }
+
+
+
+ // --- Selecting pythonw path that exist
+ if (strcmp(parentdir,"_tools")==0) {
+ exedir[strlen(exedir) - 7] = '\0'; // remove pyDatView.exe from path
+ printf("Repo dir : %s\n", exedir);
+ concatenate(pythonwpath0, pythonwpath1);
+ useImport =false;
+ printf(">>> Using Pythonw1\n");
+
+ } else if (file_exists(pythonwpath2)) {
+ concatenate(pythonwpath0, pythonwpath2);
+ printf(">>> Using Pythonw2\n");
+
+ } else if (file_exists(pythonwpath3)) {
+ concatenate(pythonwpath0, pythonwpath3);
+ printf(">>> Using Pythonw3\n");
+
+ } else if (file_exists(pythonwpath4)) {
+ concatenate(pythonwpath0, pythonwpath4);
+ printf(">>> Using Pythonw4\n");
+
+ } else {
+ ShowWindow( hWnd, SW_RESTORE);
+ printf("\n");
+ printf("[ERROR] Cannot find pythonw.exe. Try the following options: \n");
+ printf(" - place the program at the root of the installation directory\n");
+ printf(" - rename the program 'pyDatView.exe' \n");
+ printf(" - define the environmental variable PYDATPATH to the root install dir. \n");
+ printf(" If none of these options work. Contact the developper with the above outputs'\n");
+ printf("\n");
+ printf("Press any key to close this window\n");
+ getchar();
+ return -1;
+ }
+
+ // --- Convert List of argumenst to python list of string or space separated string
+ int i;
+ if (useImport) {
+ concatenate(args, "[");
+ for(i = 1; i <= argc-1; i++) {
+ concatenate(args, "'");
+ // replacing slashes
+ index=0;
+ while(argv[i][index])
+ {
+ if(argv[i][index] == '\\')
+ argv[i][index] = '/';
+ else
+ index++;
+ }
+ concatenate(args, argv[i]);
+ concatenate(args, "'");
+ if (i $INSTDIR
+ _tools/pyDatView_Test.bat > $INSTDIR
+ LICENSE.TXT > $INSTDIR
+
+pypi_wheels =
+ numpy==1.19.3
+ wxPython==4.0.3
+ matplotlib==3.0.0
+ pyparsing==2.2.2
+ cycler==0.10.0
+ six==1.11.0
+ python-dateutil==2.7.3
+ kiwisolver==1.0.1
+ pandas==0.23.4
+ pytz==2018.5
+ chardet==3.0.4
+ scipy==1.1.0
+ pyarrow==4.0.1
+
+# numpy==1.20.3
+# wxPython==4.0.7
+# matplotlib==3.4.2
+# pyparsing==2.4.7
+# cycler==0.10.0
+# six==1.11.0
+# python-dateutil==2.7.3
+# kiwisolver==1.0.1
+# pandas==1.1.5
+# pytz==2018.5
+# chardet==3.0.4
+# scipy==1.5.4
+
+# PyYAML==5.1.2
+
+packages=weio
+ future
+
+exclude=weio/.git*
+ weio/tests
+ pkgs/weio/examples
+ pkgs/weio/weio/tests
+ pkgs/numpy/core/include
+ pkgs/numpy/doc
+ pkgs/numpy/f2py
+ pkgs/numpy/core/lib
+ pkgs/numpy/tests
+ pkgs/numpy/*/tests
+ pkgs/pyarrow/include
+ pkgs/pyarrow/includes
+ pkgs/pyarrow/tensorflow
+ pkgs/pyarrow/tests
+ pkgs/pandas/tests
+ pkgs/matplotlib/sphinxext
+ pkgs/matplotlib/testing
+ pkgs/matplotlib/mpl-data/sample_data
+ pkgs/matplotlib/mpl-data/fonts
+ pkgs/matplotlib/mpl-data/images/*.pdf
+ pkgs/matplotlib/mpl-data/images/*.svg
+ pkgs/matplotlib/mpl-data/images/*.ppm
+ pkgs/matplotlib/mpl-data/stylelib/seaborn*.mplstyle
+ pkgs/matplotlib/backends/qt_editor
+ pkgs/matplotlib/backends/web_backend
+ pkgs/wx/locale
+ pkgs/wx/py
+ pkgs/wx/lib/agw
+ pkgs/wx/lib/analogclock
+ pkgs/wx/lib/art
+ pkgs/wx/lib/colourchooser
+ pkgs/wx/lib/editor
+ pkgs/wx/lib/floatcanvas
+ pkgs/wx/lib/gizmos
+ pkgs/wx/lib/masked
+ pkgs/wx/lib/ogl
+ pkgs/wx/lib/pdfviewer
+ pkgs/wx/lib/plot
+ pkgs/wx/lib/pubsub
+ pkgs/wx/lib/wxcairo
+ pkgs/scipy/cluster
+ pkgs/scipy/constants
+ pkgs/scipy/io/tests
+ pkgs/scipy/io/arff
+ pkgs/scipy/io/matlab/tests
+ pkgs/scipy/io/harwell_boieng/tests
+ pkgs/scipy/ndimage
+ pkgs/scipy/odr
+ pkgs/scipy/extra-dll/libbanded*
+ pkgs/scipy/extra-dll/libd_odr*
+ pkgs/scipy/extra-dll/libdcosqb*
+ pkgs/scipy/extra-dll/libdfft_sub*
+ pkgs/scipy/*/tests
+ pkgs/scipy/fftpack
+ pkgs/scipy/signal
+
+# pkgs\matplotlib\mpl-data
+##Click==7.0
+
+[Build]
+#directory=
+installer_name=pyDatView_setup.exe
diff --git a/pydatview/GUICommon.py b/pydatview/GUICommon.py
index 2ab907f..59470be 100644
--- a/pydatview/GUICommon.py
+++ b/pydatview/GUICommon.py
@@ -3,55 +3,58 @@
import os
import platform
+_MONOFONTSIZE=9
+_FONTSIZE=9
+
# --------------------------------------------------------------------------------}
-# ---
+# --- FONT
# --------------------------------------------------------------------------------{
-def getMonoFontAbs():
- #return wx.Font(9, wx.MODERN, wx.NORMAL, wx.NORMAL, False, u'Monospace')
- if os.name=='nt':
- font=wx.Font(9, wx.TELETYPE, wx.NORMAL, wx.NORMAL, False)
- elif os.name=='posix':
- font=wx.Font(10, wx.TELETYPE, wx.NORMAL, wx.NORMAL, False)
- else:
- font=wx.Font(8, wx.TELETYPE, wx.NORMAL, wx.NORMAL, False)
+def setMonoFontSize(fs):
+ global _MONOFONTSIZE
+ _MONOFONTSIZE=int(fs)
+
+def getMonoFontSize():
+ global _MONOFONTSIZE
+ return _MONOFONTSIZE
+
+def setFontSize(fs):
+ global _FONTSIZE
+ _FONTSIZE=int(fs)
+
+def getFontSize():
+ global _FONTSIZE
+ return _FONTSIZE
+
+
+def getFont(widget):
+ global _FONTSIZE
+ font = widget.GetFont()
+ #font.SetFamily(wx.TELETYPE)
+ font.SetPointSize(_FONTSIZE)
+ #font=wx.Font(_FONTSIZE-1, wx.TELETYPE, wx.NORMAL, wx.NORMAL, False)
return font
def getMonoFont(widget):
+ global _MONOFONTSIZE
font = widget.GetFont()
font.SetFamily(wx.TELETYPE)
- if platform.system()=='Windows':
- pass
- elif platform.system()=='Linux':
- pass
- elif platform.system()=='Darwin':
- font.SetPointSize(font.GetPointSize()-1)
- else:
- pass
+ font.SetPointSize(_MONOFONTSIZE)
return font
-
-
-# def getColumn(df,i):
-# if i == wx.NOT_FOUND or i == 0:
-# x = np.array(range(df.shape[0]))
-# c = None
-# isString = False
-# isDate = False
-# else:
-# c = df.iloc[:, i-1]
-# x = df.iloc[:, i-1].values
-# isString = c.dtype == np.object and isinstance(c.values[0], str)
-# if isString:
-# x=x.astype(str)
-# isDate = np.issubdtype(c.dtype, np.datetime64)
-# if isDate:
-# x=x.astype('datetime64[s]')
-#
-# return x,isString,isDate,c
-#
# --------------------------------------------------------------------------------}
# --- Helper functions
# --------------------------------------------------------------------------------{
+def About(parent, message):
+ class MessageBox(wx.Dialog):
+ def __init__(self, parent, title, message):
+ wx.Dialog.__init__(self, parent, title=title, style=wx.CAPTION|wx.CLOSE_BOX)
+ text = wx.TextCtrl(self, style=wx.TE_READONLY|wx.BORDER_NONE|wx.TE_MULTILINE|wx.TE_AUTO_URL)
+ text.SetValue(message)
+ text.SetBackgroundColour(wx.SystemSettings.GetColour(4))
+ self.ShowModal()
+ self.Destroy()
+ MessageBox(parent, 'About', message)
+
def YesNo(parent, question, caption = 'Yes or no?'):
dlg = wx.MessageDialog(parent, question, caption, wx.YES_NO | wx.ICON_QUESTION)
result = dlg.ShowModal() == wx.ID_YES
diff --git a/pydatview/GUIInfoPanel.py b/pydatview/GUIInfoPanel.py
index 5d80862..e198a78 100644
--- a/pydatview/GUIInfoPanel.py
+++ b/pydatview/GUIInfoPanel.py
@@ -59,11 +59,16 @@ def onSelChange(self,event):
self.parent._showStats(erase=True)
+def selectColumns(columnList, selectedNames):
+ """ set Columns as selected based on a list of column names """
+ for col in columnList:
+ col['s']=col['name'] in selectedNames
+
class InfoPanel(wx.Panel):
""" Display the list of the columns for the user to select """
#----------------------------------------------------------------------
- def __init__(self, parent):
+ def __init__(self, parent, data=None):
wx.Panel.__init__(self, parent, -1)
# ---
# List of dictionaries for available "statistical" signals. Dictionary keys:
@@ -71,130 +76,144 @@ def __init__(self, parent):
# al : alignement (L,R,C for left,right or center)
# f : function used to evaluate value
# s : selected or not
+ if data is None:
+ print('>>> Using default settings for info panel')
+ from .appdata import defaultInfoPanelData
+ data = defaultInfoPanelData()
+
+
+ # TODO TODO Consider using an OrderedDict instead, with 'name' as key, and maybe use function instead of string
self.ColsReg=[]
- self.ColsReg.append({'name':'Directory' , 'al':'L' , 'm':'baseDir' , 's':False})
- self.ColsReg.append({'name':'Filename' , 'al':'L' , 'm':'fileName', 's':False})
- self.ColsReg.append({'name':'Table' , 'al':'L' , 'm':'tabName' , 's':False})
- self.ColsReg.append({'name':'Column' , 'al':'L' , 'm':'yName' , 's':True})
- self.ColsReg.append({'name':'Median' , 'al':'R' , 'm':'yMedian' , 's' :False})
- self.ColsReg.append({'name':'Mean' , 'al':'R' , 'm':'y0Mean' , 's' :True})
- self.ColsReg.append({'name':'Std' , 'al':'R' , 'm':'y0Std' , 's' :True})
- self.ColsReg.append({'name':'Var' , 'al':'R' , 'm':'y0Var' , 's' :False})
- self.ColsReg.append({'name':'Std/Mean (TI)', 'al':'R' , 'm':'y0TI' , 's' :False})
- self.ColsReg.append({'name':'Min' , 'al':'R' , 'm':'y0Min' , 's' :True})
- self.ColsReg.append({'name':'Max' , 'al':'R' , 'm':'y0Max' , 's' :True})
- self.ColsReg.append({'name':'x@Min' , 'al':'R' , 'm':'xAtYMin' , 's' :False})
- self.ColsReg.append({'name':'x@Max' , 'al':'R' , 'm':'xAtYMax' , 's' :False})
- self.ColsReg.append({'name':'Abs. Max' , 'al':'R' , 'm':'yAbsMax', 's' :False})
- self.ColsReg.append({'name':'Range' , 'al':'R' , 'm':'yRange', 's' :True})
- self.ColsReg.append({'name':'dx' , 'al':'R' , 'm':'dx' , 's' :True})
- self.ColsReg.append({'name':'Meas 1' , 'al':'R' , 'm':'meas1' , 's' :False})
- self.ColsReg.append({'name':'Meas 2' , 'al':'R' , 'm':'meas2' , 's' :False})
- self.ColsReg.append({'name':'Mean (Meas)' , 'al':'R' , 'm':'yMeanMeas' , 's' :False})
- self.ColsReg.append({'name':'Min (Meas)' , 'al':'R' , 'm':'yMinMeas' , 's' :False})
- self.ColsReg.append({'name':'Max (Meas)' , 'al':'R' , 'm':'yMaxMeas' , 's' :False})
- self.ColsReg.append({'name':'x@Min (Meas)' , 'al':'R' , 'm':'xAtYMinMeas' , 's' :False})
- self.ColsReg.append({'name':'x@Max (Meas)' , 'al':'R' , 'm':'xAtYMaxMeas' , 's' :False})
- self.ColsReg.append({'name':'xMin' , 'al':'R' , 'm':'xMin' , 's' :False})
- self.ColsReg.append({'name':'xMax' , 'al':'R' , 'm':'xMax' , 's' :False})
- self.ColsReg.append({'name':'xRange' , 'al':'R' , 'm':'xRange', 's' :False})
- self.ColsReg.append({'name':u'\u222By' , 'al':'R' , 'm':'inty' , 's' :False})
- self.ColsReg.append({'name':u'\u222By/\u222Bdx', 'al':'R' , 'm':'intyintdx' , 's' :False})
- self.ColsReg.append({'name':u'\u222By.x ' , 'al':'R' , 'm':'intyx1' , 's' :False})
- self.ColsReg.append({'name':u'\u222By.x/\u222By','al':'R' , 'm':'intyx1_scaled' , 's' :False})
- self.ColsReg.append({'name':u'\u222By.x^2' , 'al':'R' , 'm':'intyx2' , 's' :False})
- self.ColsReg.append({'name':'L_eq(m=3)' , 'al':'R' , 'f':lambda x:x.leq(m=3) , 's' :False})
- self.ColsReg.append({'name':'L_eq(m=4)' , 'al':'R' , 'f':lambda x:x.leq(m=4) , 's' :False})
- self.ColsReg.append({'name':'L_eq(m=5)' , 'al':'R' , 'f':lambda x:x.leq(m=5) , 's' :False})
- self.ColsReg.append({'name':'L_eq(m=7)' , 'al':'R' , 'f':lambda x:x.leq(m=7) , 's' :False})
- self.ColsReg.append({'name':'L_eq(m=8)' , 'al':'R' , 'f':lambda x:x.leq(m=8) , 's' :False})
- self.ColsReg.append({'name':'L_eq(m=9)' , 'al':'R' , 'f':lambda x:x.leq(m=9) , 's' :False})
- self.ColsReg.append({'name':'L_eq(m=10)' , 'al':'R' , 'f':lambda x:x.leq(m=10), 's' :False})
- self.ColsReg.append({'name':'L_eq(m=12)' , 'al':'R' , 'f':lambda x:x.leq(m=12), 's' :False})
- self.ColsReg.append({'name':'n' , 'al':'R' , 'm':'ylen' , 's' :True})
+ self.ColsReg.append({'name':'Directory' , 'al':'L' , 'm':'baseDir' , 's' :False})
+ self.ColsReg.append({'name':'Filename' , 'al':'L' , 'm':'fileName', 's' :False})
+ self.ColsReg.append({'name':'Table' , 'al':'L' , 'm':'tabName' , 's' :False})
+ self.ColsReg.append({'name':'Column' , 'al':'L' , 'm':'yName' , 's' :True})
+ self.ColsReg.append({'name':'Median' , 'al':'R' , 'm':'yMedian' , 's' :False})
+ self.ColsReg.append({'name':'Mean' , 'al':'R' , 'm':'y0Mean' , 's' :True})
+ self.ColsReg.append({'name':'Std' , 'al':'R' , 'm':'y0Std' , 's' :True})
+ self.ColsReg.append({'name':'Var' , 'al':'R' , 'm':'y0Var' , 's' :False})
+ self.ColsReg.append({'name':'Std/Mean (TI)', 'al':'R' , 'm':'y0TI' , 's' :False})
+ self.ColsReg.append({'name':'Min' , 'al':'R' , 'm':'y0Min' , 's' :True})
+ self.ColsReg.append({'name':'Max' , 'al':'R' , 'm':'y0Max' , 's' :True})
+ self.ColsReg.append({'name':'x@Min' , 'al':'R' , 'm':'xAtYMin' , 's' :False})
+ self.ColsReg.append({'name':'x@Max' , 'al':'R' , 'm':'xAtYMax' , 's' :False})
+ self.ColsReg.append({'name':'Abs. Max' , 'al':'R' , 'm':'yAbsMax', 's' :False})
+ self.ColsReg.append({'name':'Range' , 'al':'R' , 'm':'yRange', 's' :True})
+ self.ColsReg.append({'name':'dx' , 'al':'R' , 'm':'dx' , 's' :True})
+ self.ColsReg.append({'name':'Meas 1' , 'al':'R' , 'm':'meas1' , 's' :False})
+ self.ColsReg.append({'name':'Meas 2' , 'al':'R' , 'm':'meas2' , 's' :False})
+ self.ColsReg.append({'name':'Mean (Meas)' , 'al':'R' , 'm':'yMeanMeas' , 's' :False})
+ self.ColsReg.append({'name':'Min (Meas)' , 'al':'R' , 'm':'yMinMeas' , 's' :False})
+ self.ColsReg.append({'name':'Max (Meas)' , 'al':'R' , 'm':'yMaxMeas' , 's' :False})
+ self.ColsReg.append({'name':'x@Min (Meas)' , 'al':'R' , 'm':'xAtYMinMeas' , 's' :False})
+ self.ColsReg.append({'name':'x@Max (Meas)' , 'al':'R' , 'm':'xAtYMaxMeas' , 's' :False})
+ self.ColsReg.append({'name':'xMin' , 'al':'R' , 'm':'xMin' , 's' :False})
+ self.ColsReg.append({'name':'xMax' , 'al':'R' , 'm':'xMax' , 's' :False})
+ self.ColsReg.append({'name':'xRange' , 'al':'R' , 'm':'xRange', 's' :False})
+ self.ColsReg.append({'name':u'\u222By' , 'al':'R' , 'm':'inty' , 's' :False})
+ self.ColsReg.append({'name':u'\u222By/\u222Bdx', 'al':'R' , 'm':'intyintdx' , 's' :False})
+ self.ColsReg.append({'name':u'\u222By.x ' , 'al':'R' , 'm':'intyx1' , 's' :False})
+ self.ColsReg.append({'name':u'\u222By.x/\u222By','al':'R' , 'm':'intyx1_scaled' , 's' :False})
+ self.ColsReg.append({'name':u'\u222By.x^2' , 'al':'R' , 'm':'intyx2' , 's' :False})
+ self.ColsReg.append({'name':'L_eq(m=3)' , 'al':'R' , 'f':lambda x:x.leq(m=3) , 's' :False})
+ self.ColsReg.append({'name':'L_eq(m=4)' , 'al':'R' , 'f':lambda x:x.leq(m=4) , 's' :False})
+ self.ColsReg.append({'name':'L_eq(m=5)' , 'al':'R' , 'f':lambda x:x.leq(m=5) , 's' :False})
+ self.ColsReg.append({'name':'L_eq(m=7)' , 'al':'R' , 'f':lambda x:x.leq(m=7) , 's' :False})
+ self.ColsReg.append({'name':'L_eq(m=8)' , 'al':'R' , 'f':lambda x:x.leq(m=8) , 's' :False})
+ self.ColsReg.append({'name':'L_eq(m=9)' , 'al':'R' , 'f':lambda x:x.leq(m=9) , 's' :False})
+ self.ColsReg.append({'name':'L_eq(m=10)' , 'al':'R' , 'f':lambda x:x.leq(m=10), 's' :False})
+ self.ColsReg.append({'name':'L_eq(m=12)' , 'al':'R' , 'f':lambda x:x.leq(m=12), 's' :False})
+ self.ColsReg.append({'name':'n' , 'al':'R' , 'm':'ylen' , 's' :True})
self.ColsFFT=[]
- self.ColsFFT.append({'name':'Directory' , 'al':'L' , 'm':'baseDir' , 's':False})
- self.ColsFFT.append({'name':'Filename' , 'al':'L' , 'm':'fileName', 's':False})
- self.ColsFFT.append({'name':'Table' , 'al':'L' , 'm':'tabName' , 's':False})
- self.ColsFFT.append({'name':'Column' , 'al':'L' , 'm':'yName' , 's':True})
- self.ColsFFT.append({'name':'Mean' , 'al':'R' , 'm':'y0Mean' , 's' :True})
- self.ColsFFT.append({'name':'Std' , 'al':'R' , 'm':'y0Std' , 's' :True})
- self.ColsFFT.append({'name':'Min' , 'al':'R' , 'm':'y0Min' , 's' :True})
- self.ColsFFT.append({'name':'Max' , 'al':'R' , 'm':'y0Max' , 's' :True})
- self.ColsFFT.append({'name':'x@Min' , 'al':'R' , 'm':'xAtYMin' , 's' :False})
- self.ColsFFT.append({'name':'x@Max' , 'al':'R' , 'm':'xAtYMax' , 's' :False})
- self.ColsFFT.append({'name':'Mean(FFT)' , 'al':'R' , 'm':'yMean' , 's' :False})
- self.ColsFFT.append({'name':'Std(FFT)' , 'al':'R' , 'm':'yStd' , 's' :False})
- self.ColsFFT.append({'name':'Min(FFT)' , 'al':'R' , 'm':'yMin' , 's' :True})
- self.ColsFFT.append({'name':'Max(FFT)' , 'al':'R' , 'm':'yMax' , 's' :True})
- self.ColsFFT.append({'name':'Var' , 'al':'R' , 'm':'y0Var' , 's' :False})
- self.ColsFFT.append({'name':u'\u222By(FFT)' , 'al':'R' , 'm':'inty' , 's' :True})
- self.ColsFFT.append({'name':'dx(FFT)' , 'al':'R' , 'm':'dx' , 's' :True})
- self.ColsFFT.append({'name':'xMax(FFT)' , 'al':'R' , 'm':'xMax' , 's' :True})
- self.ColsFFT.append({'name':'nOvlp(FFT)' , 'al':'R' , 'f':lambda x:x.Info('LOvlp') , 's' :False})
- self.ColsFFT.append({'name':'nSeg(FFT)' , 'al':'R' , 'f':lambda x:x.Info('LSeg') , 's' :False})
- self.ColsFFT.append({'name':'nWin(FFT)' , 'al':'R' , 'f':lambda x:x.Info('LWin') , 's' :False})
- self.ColsFFT.append({'name':'nFFT(FFT)' , 'al':'R' , 'f':lambda x:x.Info('nFFT') , 's' :False})
- self.ColsFFT.append({'name':'n(FFT)' , 'al':'R' , 'm':'ylen' , 's' :True})
- self.ColsFFT.append({'name':'Meas 1' , 'al':'R' , 'm':'meas1' , 's' :False})
- self.ColsFFT.append({'name':'Meas 2' , 'al':'R' , 'm':'meas2' , 's' :False})
- self.ColsFFT.append({'name':'Mean (Meas)' , 'al':'R' , 'm':'yMeanMeas' , 's' :False})
- self.ColsFFT.append({'name':'Min (Meas)' , 'al':'R' , 'm':'yMinMeas' , 's' :False})
- self.ColsFFT.append({'name':'Max (Meas)' , 'al':'R' , 'm':'yMaxMeas' , 's' :False})
- self.ColsFFT.append({'name':'x@Min (Meas)' , 'al':'R' , 'm':'xAtYMinMeas' , 's' :False})
- self.ColsFFT.append({'name':'x@Max (Meas)' , 'al':'R' , 'm':'xAtYMaxMeas' , 's' :False})
- self.ColsFFT.append({'name':'n ' , 'al':'R' , 'm':'n0' , 's' :True})
+ self.ColsFFT.append({'name':'Directory' , 'al':'L' , 'm':'baseDir' , 's' :False})
+ self.ColsFFT.append({'name':'Filename' , 'al':'L' , 'm':'fileName', 's' :False})
+ self.ColsFFT.append({'name':'Table' , 'al':'L' , 'm':'tabName' , 's' :False})
+ self.ColsFFT.append({'name':'Column' , 'al':'L' , 'm':'yName' , 's' :True})
+ self.ColsFFT.append({'name':'Mean' , 'al':'R' , 'm':'y0Mean' , 's' :True})
+ self.ColsFFT.append({'name':'Std' , 'al':'R' , 'm':'y0Std' , 's' :True})
+ self.ColsFFT.append({'name':'Min' , 'al':'R' , 'm':'y0Min' , 's' :True})
+ self.ColsFFT.append({'name':'Max' , 'al':'R' , 'm':'y0Max' , 's' :True})
+ self.ColsFFT.append({'name':'x@Min' , 'al':'R' , 'm':'xAtYMin' , 's' :False})
+ self.ColsFFT.append({'name':'x@Max' , 'al':'R' , 'm':'xAtYMax' , 's' :False})
+ self.ColsFFT.append({'name':'Mean(FFT)' , 'al':'R' , 'm':'yMean' , 's' :False})
+ self.ColsFFT.append({'name':'Std(FFT)' , 'al':'R' , 'm':'yStd' , 's' :False})
+ self.ColsFFT.append({'name':'Min(FFT)' , 'al':'R' , 'm':'yMin' , 's' :True})
+ self.ColsFFT.append({'name':'Max(FFT)' , 'al':'R' , 'm':'yMax' , 's' :True})
+ self.ColsFFT.append({'name':'Var' , 'al':'R' , 'm':'y0Var' , 's' :False})
+ self.ColsFFT.append({'name':u'\u222By(FFT)' , 'al':'R' , 'm':'inty' , 's' :True})
+ self.ColsFFT.append({'name':'dx(FFT)' , 'al':'R' , 'm':'dx' , 's' :True})
+ self.ColsFFT.append({'name':'xMax(FFT)' , 'al':'R' , 'm':'xMax' , 's' :True})
+ self.ColsFFT.append({'name':'nOvlp(FFT)' , 'al':'R' , 'f':lambda x:x.Info('LOvlp') , 's' :False})
+ self.ColsFFT.append({'name':'nSeg(FFT)' , 'al':'R' , 'f':lambda x:x.Info('LSeg') , 's' :False})
+ self.ColsFFT.append({'name':'nWin(FFT)' , 'al':'R' , 'f':lambda x:x.Info('LWin') , 's' :False})
+ self.ColsFFT.append({'name':'nFFT(FFT)' , 'al':'R' , 'f':lambda x:x.Info('nFFT') , 's' :False})
+ self.ColsFFT.append({'name':'n(FFT)' , 'al':'R' , 'm':'ylen' , 's' :True})
+ self.ColsFFT.append({'name':'Meas 1' , 'al':'R' , 'm':'meas1' , 's' :False})
+ self.ColsFFT.append({'name':'Meas 2' , 'al':'R' , 'm':'meas2' , 's' :False})
+ self.ColsFFT.append({'name':'Mean (Meas)' , 'al':'R' , 'm':'yMeanMeas' , 's' :False})
+ self.ColsFFT.append({'name':'Min (Meas)' , 'al':'R' , 'm':'yMinMeas' , 's' :False})
+ self.ColsFFT.append({'name':'Max (Meas)' , 'al':'R' , 'm':'yMaxMeas' , 's' :False})
+ self.ColsFFT.append({'name':'x@Min (Meas)' , 'al':'R' , 'm':'xAtYMinMeas' , 's' :False})
+ self.ColsFFT.append({'name':'x@Max (Meas)' , 'al':'R' , 'm':'xAtYMaxMeas' , 's' :False})
+ self.ColsFFT.append({'name':'n' , 'al':'R' , 'm':'n0' , 's' :True})
self.ColsMinMax=[]
- self.ColsMinMax.append({'name':'Directory' , 'al':'L' , 'm':'baseDir', 's':False})
- self.ColsMinMax.append({'name':'Filename' , 'al':'L' , 'm':'fileName', 's':False})
- self.ColsMinMax.append({'name':'Table' , 'al':'L' , 'm':'tabName' , 's':False})
- self.ColsMinMax.append({'name':'Column' , 'al':'L' , 'm':'yName' , 's':True})
- self.ColsMinMax.append({'name':'Mean' , 'al':'R' , 'm':'y0Mean' , 's' :True})
- self.ColsMinMax.append({'name':'Std' , 'al':'R' , 'm':'y0Std' , 's' :True})
- self.ColsMinMax.append({'name':'Min' , 'al':'R' , 'm':'y0Min' , 's' :True})
- self.ColsMinMax.append({'name':'Max' , 'al':'R' , 'm':'y0Max' , 's' :True})
- self.ColsMinMax.append({'name':'Mean(MinMax)' , 'al':'R' , 'm':'yMean' , 's' :True})
- self.ColsMinMax.append({'name':'Std(MinMax)' , 'al':'R' , 'm':'yStd' , 's' :True})
- self.ColsMinMax.append({'name':u'\u222By(MinMax)' , 'al':'R' , 'm':'inty' , 's' :True})
- self.ColsMinMax.append({'name':u'\u222By.x(MinMax) ' , 'al':'R' , 'm':'intyx1' , 's' :False})
+ self.ColsMinMax.append({'name':'Directory' , 'al':'L' , 'm':'baseDir', 's' :False})
+ self.ColsMinMax.append({'name':'Filename' , 'al':'L' , 'm':'fileName', 's' :False})
+ self.ColsMinMax.append({'name':'Table' , 'al':'L' , 'm':'tabName' , 's' :False})
+ self.ColsMinMax.append({'name':'Column' , 'al':'L' , 'm':'yName' , 's' :True})
+ self.ColsMinMax.append({'name':'Mean' , 'al':'R' , 'm':'y0Mean' , 's' :True})
+ self.ColsMinMax.append({'name':'Std' , 'al':'R' , 'm':'y0Std' , 's' :True})
+ self.ColsMinMax.append({'name':'Min' , 'al':'R' , 'm':'y0Min' , 's' :True})
+ self.ColsMinMax.append({'name':'Max' , 'al':'R' , 'm':'y0Max' , 's' :True})
+ self.ColsMinMax.append({'name':'Mean(MinMax)' , 'al':'R' , 'm':'yMean' , 's' :True})
+ self.ColsMinMax.append({'name':'Std(MinMax)' , 'al':'R' , 'm':'yStd' , 's' :True})
+ self.ColsMinMax.append({'name':u'\u222By(MinMax)' , 'al':'R' , 'm':'inty' , 's' :True})
+ self.ColsMinMax.append({'name':u'\u222By.x(MinMax) ' , 'al':'R' , 'm':'intyx1' , 's' :False})
self.ColsMinMax.append({'name':u'\u222By.x/\u222By(MinMax)' , 'al':'R' , 'm':'intyx1_scaled' , 's' :False})
- self.ColsMinMax.append({'name':u'\u222By.x^2(MinMax)' , 'al':'R' , 'm':'intyx2' , 's' :False})
- self.ColsMinMax.append({'name':'dx(MinMax)' , 'al':'R' , 'm':'dx' , 's' :False})
- self.ColsMinMax.append({'name':'n' , 'al':'R' , 'm':'ylen' , 's' :True})
+ self.ColsMinMax.append({'name':u'\u222By.x^2(MinMax)' , 'al':'R' , 'm':'intyx2' , 's' :False})
+ self.ColsMinMax.append({'name':'dx(MinMax)' , 'al':'R' , 'm':'dx' , 's' :False})
+ self.ColsMinMax.append({'name':'n' , 'al':'R' , 'm':'ylen' , 's' :True})
self.ColsPDF=[]
- self.ColsPDF.append({'name':'Directory' , 'al':'L' , 'm':'baseDir' , 's':False})
- self.ColsPDF.append({'name':'Filename' , 'al':'L' , 'm':'fileName', 's':False})
- self.ColsPDF.append({'name':'Table' , 'al':'L' , 'm':'tabName' , 's':False})
- self.ColsPDF.append({'name':'Column' , 'al':'L' , 'm':'yName' , 's':True})
- self.ColsPDF.append({'name':'Mean' , 'al':'R' , 'm':'y0Mean' , 's' :True})
- self.ColsPDF.append({'name':'Std' , 'al':'R' , 'm':'y0Std' , 's' :True})
- self.ColsPDF.append({'name':'Min' , 'al':'R' , 'm':'y0Min' , 's' :True})
- self.ColsPDF.append({'name':'Max' , 'al':'R' , 'm':'y0Max' , 's' :True})
- self.ColsPDF.append({'name':'x@Min' , 'al':'R' , 'm':'xAtYMin' , 's' :False})
- self.ColsPDF.append({'name':'x@Max' , 'al':'R' , 'm':'xAtYMax' , 's' :False})
- self.ColsPDF.append({'name':'Mean(PDF)' , 'al':'R' , 'm':'yMean' , 's' :False})
- self.ColsPDF.append({'name':'Std(PDF)' , 'al':'R' , 'm':'yStd' , 's' :False})
- self.ColsPDF.append({'name':'Min(PDF)' , 'al':'R' , 'm':'yMin' , 's' :True})
- self.ColsPDF.append({'name':'Max(PDF)' , 'al':'R' , 'm':'yMax' , 's' :True})
- self.ColsPDF.append({'name':u'\u222By(PDF)' , 'al':'R' , 'm':'inty' , 's' :True})
- self.ColsPDF.append({'name':'Meas 1' , 'al':'R' , 'm':'meas1' , 's' :False})
- self.ColsPDF.append({'name':'Meas 2' , 'al':'R' , 'm':'meas2' , 's' :False})
- self.ColsPDF.append({'name':'Mean (Meas)' , 'al':'R' , 'm':'yMeanMeas' , 's' :False})
- self.ColsPDF.append({'name':'Min (Meas)' , 'al':'R' , 'm':'yMinMeas' , 's' :False})
- self.ColsPDF.append({'name':'Max (Meas)' , 'al':'R' , 'm':'yMaxMeas' , 's' :False})
- self.ColsPDF.append({'name':'x@Min (Meas)' , 'al':'R' , 'm':'xAtYMinMeas' , 's' :False})
- self.ColsPDF.append({'name':'x@Max (Meas)' , 'al':'R' , 'm':'xAtYMaxMeas' , 's' :False})
- self.ColsPDF.append({'name':'n(PDF)' , 'al':'R' , 'm':'ylen' , 's' :True})
+ self.ColsPDF.append({'name':'Directory' , 'al':'L' , 'm':'baseDir' , 's' :False})
+ self.ColsPDF.append({'name':'Filename' , 'al':'L' , 'm':'fileName', 's' :False})
+ self.ColsPDF.append({'name':'Table' , 'al':'L' , 'm':'tabName' , 's' :False})
+ self.ColsPDF.append({'name':'Column' , 'al':'L' , 'm':'yName' , 's' :True})
+ self.ColsPDF.append({'name':'Mean' , 'al':'R' , 'm':'y0Mean' , 's' :True})
+ self.ColsPDF.append({'name':'Std' , 'al':'R' , 'm':'y0Std' , 's' :True})
+ self.ColsPDF.append({'name':'Min' , 'al':'R' , 'm':'y0Min' , 's' :True})
+ self.ColsPDF.append({'name':'Max' , 'al':'R' , 'm':'y0Max' , 's' :True})
+ self.ColsPDF.append({'name':'x@Min' , 'al':'R' , 'm':'xAtYMin' , 's' :False})
+ self.ColsPDF.append({'name':'x@Max' , 'al':'R' , 'm':'xAtYMax' , 's' :False})
+ self.ColsPDF.append({'name':'Mean(PDF)' , 'al':'R' , 'm':'yMean' , 's' :False})
+ self.ColsPDF.append({'name':'Std(PDF)' , 'al':'R' , 'm':'yStd' , 's' :False})
+ self.ColsPDF.append({'name':'Min(PDF)' , 'al':'R' , 'm':'yMin' , 's' :True})
+ self.ColsPDF.append({'name':'Max(PDF)' , 'al':'R' , 'm':'yMax' , 's' :True})
+ self.ColsPDF.append({'name':u'\u222By(PDF)' , 'al':'R' , 'm':'inty' , 's' :True})
+ self.ColsPDF.append({'name':'Meas 1' , 'al':'R' , 'm':'meas1' , 's' :False})
+ self.ColsPDF.append({'name':'Meas 2' , 'al':'R' , 'm':'meas2' , 's' :False})
+ self.ColsPDF.append({'name':'Mean (Meas)' , 'al':'R' , 'm':'yMeanMeas' , 's' :False})
+ self.ColsPDF.append({'name':'Min (Meas)' , 'al':'R' , 'm':'yMinMeas' , 's' :False})
+ self.ColsPDF.append({'name':'Max (Meas)' , 'al':'R' , 'm':'yMaxMeas' , 's' :False})
+ self.ColsPDF.append({'name':'x@Min (Meas)' , 'al':'R' , 'm':'xAtYMinMeas' , 's' :False})
+ self.ColsPDF.append({'name':'x@Max (Meas)' , 'al':'R' , 'm':'xAtYMaxMeas' , 's' :False})
+ self.ColsPDF.append({'name':'n(PDF)' , 'al':'R' , 'm':'ylen' , 's' :True})
self.ColsCmp=[]
- self.ColsCmp.append({'name':'Directory' , 'al':'L' , 'm':'baseDir' , 's':False})
- self.ColsCmp.append({'name':'Filename' , 'al':'L' , 'm':'fileName', 's':False})
- self.ColsCmp.append({'name':'Table' , 'al':'L' , 'm':'tabName' , 's':False})
- self.ColsCmp.append({'name':'Column' , 'al':'L' , 'm':'yName' ,'s':True})
- self.ColsCmp.append({'name':'Mean(Cmp)' , 'al':'R' , 'm':'yMean' , 's' :True})
- self.ColsCmp.append({'name':'Std(Cmp)' , 'al':'R' , 'm':'yStd' , 's' :True})
- self.ColsCmp.append({'name':'Min(Cmp)' , 'al':'R' , 'm':'yMin' , 's' :True})
- self.ColsCmp.append({'name':'Max(Cmp)' , 'al':'R' , 'm':'yMax' , 's' :True})
- self.ColsCmp.append({'name':'n(Cmp)' , 'al':'R' , 'm':'ylen' , 's' :True})
+ self.ColsCmp.append({'name':'Directory' , 'al':'L' , 'm':'baseDir' , 's' :False})
+ self.ColsCmp.append({'name':'Filename' , 'al':'L' , 'm':'fileName', 's' :False})
+ self.ColsCmp.append({'name':'Table' , 'al':'L' , 'm':'tabName' , 's' :False})
+ self.ColsCmp.append({'name':'Column' , 'al':'L' , 'm':'yName' , 's' :True})
+ self.ColsCmp.append({'name':'Mean(Cmp)' , 'al':'R' , 'm':'yMean' , 's' :True})
+ self.ColsCmp.append({'name':'Std(Cmp)' , 'al':'R' , 'm':'yStd' , 's' :True})
+ self.ColsCmp.append({'name':'Min(Cmp)' , 'al':'R' , 'm':'yMin' , 's' :True})
+ self.ColsCmp.append({'name':'Max(Cmp)' , 'al':'R' , 'm':'yMax' , 's' :True})
+ self.ColsCmp.append({'name':'n(Cmp)' , 'al':'R' , 'm':'ylen' , 's' :True})
+
+ # Select columns based on data
+ selectColumns(self.ColsReg , data['ColumnsRegular'])
+ selectColumns(self.ColsFFT , data['ColumnsFFT'])
+ selectColumns(self.ColsMinMax, data['ColumnsMinMax'])
+ selectColumns(self.ColsPDF , data['ColumnsPDF'])
+ selectColumns(self.ColsCmp , data['ColumnsCmp'])
self.menuReg=ColCheckMenu(self)
self.menuReg.setColumns(self.ColsReg)
diff --git a/pydatview/GUIPlotPanel.py b/pydatview/GUIPlotPanel.py
index 885cba4..d80e08c 100644
--- a/pydatview/GUIPlotPanel.py
+++ b/pydatview/GUIPlotPanel.py
@@ -1,1397 +1,1468 @@
-import os
-import numpy as np
-import wx
-import wx.lib.buttons as buttons
-import dateutil # required by matplotlib
-#from matplotlib import pyplot as plt
-import matplotlib
-matplotlib.use('wxAgg') # Important for Windows version of installer. NOTE: changed from Agg to wxAgg
-from matplotlib import rc as matplotlib_rc
-try:
- from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
-except Exception as e:
- print('')
- print('Error: problem importing `matplotlib.backends.backend_wx`.')
- import platform
- if platform.system()=='Darwin':
- print('')
- print('pyDatView help:')
- print(' This is a typical issue on MacOS, most likely you are')
- print(' using the native MacOS python with the native matplolib')
- print(' library, which is incompatible with `wxPython`.')
- print('')
- print(' You can solve this by either:')
- print(' - using python3, and pip3 e.g. installing it with brew')
- print(' - using a virtual environment with python 2 or 3')
- print(' - using anaconda with python 2 or 3');
- print('')
- import sys
- sys.exit(1)
- else:
- raise e
-from matplotlib.figure import Figure
-from matplotlib.pyplot import rcParams as pyplot_rc
-from matplotlib import font_manager
-from pandas.plotting import register_matplotlib_converters
-
-import gc
-
-from .common import * # unique, CHAR
-from .plotdata import PlotData, compareMultiplePD
-from .GUICommon import *
-from .GUIToolBox import MyMultiCursor, MyNavigationToolbar2Wx, TBAddTool, TBAddCheckTool
-from .GUIMeasure import GUIMeasure
-from . import icons
-
-font = {'size' : 8}
-matplotlib_rc('font', **font)
-pyplot_rc['agg.path.chunksize'] = 20000
-
-
-class PDFCtrlPanel(wx.Panel):
- def __init__(self, parent):
- super(PDFCtrlPanel,self).__init__(parent)
- self.parent = parent
- lb = wx.StaticText( self, -1, 'Number of bins:')
- self.scBins = wx.SpinCtrl(self, value='50',size=wx.Size(70,-1))
- self.scBins.SetRange(3, 10000)
- self.cbSmooth = wx.CheckBox(self, -1, 'Smooth',(10,10))
- self.cbSmooth.SetValue(False)
- dummy_sizer = wx.BoxSizer(wx.HORIZONTAL)
- dummy_sizer.Add(lb ,0, flag = wx.CENTER|wx.LEFT,border = 1)
- dummy_sizer.Add(self.scBins ,0, flag = wx.CENTER|wx.LEFT,border = 1)
- dummy_sizer.Add(self.cbSmooth ,0, flag = wx.CENTER|wx.LEFT,border = 6)
- self.SetSizer(dummy_sizer)
- self.Bind(wx.EVT_TEXT , self.onPDFOptionChange, self.scBins)
- self.Bind(wx.EVT_CHECKBOX, self.onPDFOptionChange)
- self.Hide()
-
- def onPDFOptionChange(self,event=None):
- self.parent.load_and_draw(); # DATA HAS CHANGED
-
-class MinMaxPanel(wx.Panel):
- def __init__(self, parent):
- super(MinMaxPanel,self).__init__(parent)
- self.parent = parent
- self.cbxMinMax = wx.CheckBox(self, -1, 'xMinMax',(10,10))
- self.cbyMinMax = wx.CheckBox(self, -1, 'yMinMax',(10,10))
- self.cbxMinMax.SetValue(False)
- self.cbyMinMax.SetValue(True)
- dummy_sizer = wx.BoxSizer(wx.HORIZONTAL)
- dummy_sizer.Add(self.cbxMinMax ,0, flag=wx.CENTER|wx.LEFT, border = 1)
- dummy_sizer.Add(self.cbyMinMax ,0, flag=wx.CENTER|wx.LEFT, border = 1)
- self.SetSizer(dummy_sizer)
- self.Bind(wx.EVT_CHECKBOX, self.onMinMaxChange)
- self.Hide()
-
- def onMinMaxChange(self,event=None):
- self.parent.load_and_draw(); # DATA HAS CHANGED
-
-class CompCtrlPanel(wx.Panel):
- def __init__(self, parent):
- super(CompCtrlPanel,self).__init__(parent)
- self.parent = parent
- lblList = ['Relative', '|Relative|','Ratio','Absolute','Y-Y']
- self.rbType = wx.RadioBox(self, label = 'Type', choices = lblList,
- majorDimension = 1, style = wx.RA_SPECIFY_ROWS)
- dummy_sizer = wx.BoxSizer(wx.HORIZONTAL)
- dummy_sizer.Add(self.rbType ,0, flag = wx.CENTER|wx.LEFT,border = 1)
- self.SetSizer(dummy_sizer)
- self.rbType.Bind(wx.EVT_RADIOBOX,self.onTypeChange)
- self.Hide()
-
- def onTypeChange(self,e):
- self.parent.load_and_draw(); # DATA HAS CHANGED
-
-
-class SpectralCtrlPanel(wx.Panel):
- def __init__(self, parent):
- super(SpectralCtrlPanel,self).__init__(parent)
- self.parent = parent
- # --- GUI widgets
- lb = wx.StaticText( self, -1, 'Type:')
- self.cbType = wx.ComboBox(self, choices=['PSD','f x PSD','Amplitude'] , style=wx.CB_READONLY)
- self.cbType.SetSelection(0)
- lbAveraging = wx.StaticText( self, -1, 'Avg.:')
- self.cbAveraging = wx.ComboBox(self, choices=['None','Welch'] , style=wx.CB_READONLY)
- self.cbAveraging.SetSelection(1)
- self.lbAveragingMethod = wx.StaticText( self, -1, 'Window:')
- self.cbAveragingMethod = wx.ComboBox(self, choices=['Hamming','Hann','Rectangular'] , style=wx.CB_READONLY)
- self.cbAveragingMethod.SetSelection(0)
- self.lbP2 = wx.StaticText( self, -1, '2^n:')
- self.scP2 = wx.SpinCtrl(self, value='11',size=wx.Size(40,-1))
- self.lbWinLength = wx.StaticText( self, -1, '(2048) ')
- self.scP2.SetRange(3, 19)
- lbMaxFreq = wx.StaticText( self, -1, 'Xlim:')
- self.tMaxFreq = wx.TextCtrl(self,size = (30,-1),style=wx.TE_PROCESS_ENTER)
- self.tMaxFreq.SetValue("-1")
- self.cbDetrend = wx.CheckBox(self, -1, 'Detrend',(10,10))
- lbX = wx.StaticText( self, -1, 'x:')
- self.cbTypeX = wx.ComboBox(self, choices=['1/x','2pi/x','x'] , style=wx.CB_READONLY)
- self.cbTypeX.SetSelection(0)
- # Layout
- dummy_sizer = wx.BoxSizer(wx.HORIZONTAL)
- dummy_sizer.Add(lb ,0, flag = wx.CENTER|wx.LEFT,border = 1)
- dummy_sizer.Add(self.cbType ,0, flag = wx.CENTER|wx.LEFT,border = 1)
- dummy_sizer.Add(lbAveraging ,0, flag = wx.CENTER|wx.LEFT,border = 6)
- dummy_sizer.Add(self.cbAveraging ,0, flag = wx.CENTER|wx.LEFT,border = 1)
- dummy_sizer.Add(self.lbAveragingMethod,0, flag = wx.CENTER|wx.LEFT,border = 6)
- dummy_sizer.Add(self.cbAveragingMethod,0, flag = wx.CENTER|wx.LEFT,border = 1)
- dummy_sizer.Add(self.lbP2 ,0, flag = wx.CENTER|wx.LEFT,border = 6)
- dummy_sizer.Add(self.scP2 ,0, flag = wx.CENTER|wx.LEFT,border = 1)
- dummy_sizer.Add(self.lbWinLength ,0, flag = wx.CENTER|wx.LEFT,border = 1)
- dummy_sizer.Add(lbMaxFreq ,0, flag = wx.CENTER|wx.LEFT,border = 6)
- dummy_sizer.Add(self.tMaxFreq ,0, flag = wx.CENTER|wx.LEFT,border = 1)
- dummy_sizer.Add(lbX ,0, flag = wx.CENTER|wx.LEFT,border = 6)
- dummy_sizer.Add(self.cbTypeX ,0, flag = wx.CENTER|wx.LEFT,border = 1)
- dummy_sizer.Add(self.cbDetrend ,0, flag = wx.CENTER|wx.LEFT,border = 7)
- self.SetSizer(dummy_sizer)
- self.Bind(wx.EVT_COMBOBOX ,self.onSpecCtrlChange)
- self.Bind(wx.EVT_TEXT ,self.onP2ChangeText ,self.scP2 )
- self.Bind(wx.EVT_TEXT_ENTER,self.onXlimChange ,self.tMaxFreq )
- self.Bind(wx.EVT_CHECKBOX ,self.onDetrendChange ,self.cbDetrend)
- self.Hide()
-
- def onXlimChange(self,event=None):
- self.parent.redraw_same_data();
- def onSpecCtrlChange(self,event=None):
- self.parent.load_and_draw() # Data changes
- def onDetrendChange(self,event=None):
- self.parent.load_and_draw() # Data changes
-
- def onP2ChangeText(self,event=None):
- nExp=self.scP2.GetValue()
- self.updateP2(nExp)
- self.parent.load_and_draw() # Data changes
-
- def updateP2(self,P2):
- self.lbWinLength.SetLabel("({})".format(2**P2))
-
-
-
-
-class PlotTypePanel(wx.Panel):
- def __init__(self, parent):
- # Superclass constructor
- super(PlotTypePanel,self).__init__(parent)
- #self.SetBackgroundColour('yellow')
- # data
- self.parent = parent
- # --- Ctrl Panel
- self.cbRegular = wx.RadioButton(self, -1, 'Regular',style=wx.RB_GROUP)
- self.cbPDF = wx.RadioButton(self, -1, 'PDF' , )
- self.cbFFT = wx.RadioButton(self, -1, 'FFT' , )
- self.cbMinMax = wx.RadioButton(self, -1, 'MinMax' , )
- self.cbCompare = wx.RadioButton(self, -1, 'Compare', )
- self.cbRegular.SetValue(True)
- self.Bind(wx.EVT_RADIOBUTTON, self.pdf_select , self.cbPDF )
- self.Bind(wx.EVT_RADIOBUTTON, self.fft_select , self.cbFFT )
- self.Bind(wx.EVT_RADIOBUTTON, self.minmax_select , self.cbMinMax )
- self.Bind(wx.EVT_RADIOBUTTON, self.compare_select, self.cbCompare)
- self.Bind(wx.EVT_RADIOBUTTON, self.regular_select, self.cbRegular)
- # LAYOUT
- cb_sizer = wx.FlexGridSizer(rows=5, cols=1, hgap=0, vgap=0)
- cb_sizer.Add(self.cbRegular , 0, flag=wx.ALL, border=1)
- cb_sizer.Add(self.cbPDF , 0, flag=wx.ALL, border=1)
- cb_sizer.Add(self.cbFFT , 0, flag=wx.ALL, border=1)
- cb_sizer.Add(self.cbMinMax , 0, flag=wx.ALL, border=1)
- cb_sizer.Add(self.cbCompare , 0, flag=wx.ALL, border=1)
- self.SetSizer(cb_sizer)
-
- def plotType(self):
- plotType='Regular'
- if self.cbMinMax.GetValue():
- plotType='MinMax'
- elif self.cbPDF.GetValue():
- plotType='PDF'
- elif self.cbFFT.GetValue():
- plotType='FFT'
- elif self.cbCompare.GetValue():
- plotType='Compare'
- return plotType
-
- def regular_select(self, event=None):
- self.clear_measures()
- self.parent.cbLogY.SetValue(False)
- #
- self.parent.spcPanel.Hide();
- self.parent.pdfPanel.Hide();
- self.parent.cmpPanel.Hide();
- self.parent.mmxPanel.Hide();
- self.parent.slEsth.Hide();
- self.parent.plotsizer.Layout()
- #
- self.parent.load_and_draw() # Data changes
-
- def compare_select(self, event=None):
- self.clear_measures()
- self.parent.cbLogY.SetValue(False)
- self.parent.show_hide(self.parent.cmpPanel, self.cbCompare.GetValue())
- self.parent.spcPanel.Hide();
- self.parent.pdfPanel.Hide();
- self.parent.mmxPanel.Hide();
- self.parent.plotsizer.Layout()
- self.parent.load_and_draw() # Data changes
-
- def fft_select(self, event=None):
- self.clear_measures()
- self.parent.show_hide(self.parent.spcPanel, self.cbFFT.GetValue())
- self.parent.cbLogY.SetValue(self.cbFFT.GetValue())
- self.parent.pdfPanel.Hide();
- self.parent.mmxPanel.Hide();
- self.parent.plotsizer.Layout()
- self.parent.load_and_draw() # Data changes
-
- def pdf_select(self, event=None):
- self.clear_measures()
- self.parent.cbLogX.SetValue(False)
- self.parent.cbLogY.SetValue(False)
- self.parent.show_hide(self.parent.pdfPanel, self.cbPDF.GetValue())
- self.parent.spcPanel.Hide();
- self.parent.cmpPanel.Hide();
- self.parent.mmxPanel.Hide();
- self.parent.plotsizer.Layout()
- self.parent.load_and_draw() # Data changes
-
- def minmax_select(self, event):
- self.clear_measures()
- self.parent.cbLogY.SetValue(False)
- self.parent.show_hide(self.parent.mmxPanel, self.cbMinMax.GetValue())
- self.parent.spcPanel.Hide();
- self.parent.pdfPanel.Hide();
- self.parent.cmpPanel.Hide();
- self.parent.plotsizer.Layout()
- self.parent.load_and_draw() # Data changes
-
- def clear_measures(self):
- self.parent.rightMeasure.clear()
- self.parent.leftMeasure.clear()
- self.parent.lbDeltaX.SetLabel('')
- self.parent.lbDeltaY.SetLabel('')
-
-class EstheticsPanel(wx.Panel):
- def __init__(self, parent):
- wx.Panel.__init__(self, parent)
- self.parent=parent
- #self.SetBackgroundColour('red')
-
- lbFont = wx.StaticText( self, -1, 'Font:')
- self.cbFont = wx.ComboBox(self, choices=['6','7','8','9','10','11','12','13','14','15','16','17','18'] , style=wx.CB_READONLY)
- self.cbFont.SetSelection(2)
- # NOTE: we don't offer "best" since best is slow
- lbLegend = wx.StaticText( self, -1, 'Legend:')
- self.cbLegend = wx.ComboBox(self, choices=['None','Upper right','Upper left','Lower left','Lower right','Right','Center left','Center right','Lower center','Upper center','Center'] , style=wx.CB_READONLY)
- self.cbLegend.SetSelection(1)
- lbLgdFont = wx.StaticText( self, -1, 'Legend font:')
- self.cbLgdFont = wx.ComboBox(self, choices=['6','7','8','9','10','11','12','13','14','15','16','17','18'] , style=wx.CB_READONLY)
- self.cbLgdFont.SetSelection(2)
- lbLW = wx.StaticText( self, -1, 'Line width:')
- self.cbLW = wx.ComboBox(self, choices=['0.5','1.0','1.5','2.0','2.5','3.0'] , style=wx.CB_READONLY)
- self.cbLW.SetSelection(2)
- lbMS = wx.StaticText( self, -1, 'Marker size:')
- self.cbMS= wx.ComboBox(self, choices=['0.5','1','2','3','4','5','6','7','8'] , style=wx.CB_READONLY)
- self.cbMS.SetSelection(2)
-
- # Layout
- #dummy_sizer = wx.BoxSizer(wx.HORIZONTAL)
- dummy_sizer = wx.WrapSizer(orient=wx.HORIZONTAL)
- dummy_sizer.Add(lbFont ,0, flag = wx.CENTER|wx.LEFT,border = 1)
- dummy_sizer.Add(self.cbFont ,0, flag = wx.CENTER|wx.LEFT,border = 1)
- dummy_sizer.Add(lbLW ,0, flag = wx.CENTER|wx.LEFT,border = 5)
- dummy_sizer.Add(self.cbLW ,0, flag = wx.CENTER|wx.LEFT,border = 1)
- dummy_sizer.Add(lbMS ,0, flag = wx.CENTER|wx.LEFT,border = 5)
- dummy_sizer.Add(self.cbMS ,0, flag = wx.CENTER|wx.LEFT,border = 1)
- dummy_sizer.Add(lbLegend ,0, flag = wx.CENTER|wx.LEFT,border = 5)
- dummy_sizer.Add(self.cbLegend ,0, flag = wx.CENTER|wx.LEFT,border = 1)
- dummy_sizer.Add(lbLgdFont ,0, flag = wx.CENTER|wx.LEFT,border = 5)
- dummy_sizer.Add(self.cbLgdFont ,0, flag = wx.CENTER|wx.LEFT,border = 1)
- self.SetSizer(dummy_sizer)
- self.Hide()
- # Callbacks
- self.Bind(wx.EVT_COMBOBOX ,self.onAnyEsthOptionChange)
- self.cbFont.Bind(wx.EVT_COMBOBOX ,self.onFontOptionChange)
-
- def onAnyEsthOptionChange(self,event=None):
- self.parent.redraw_same_data()
-
- def onFontOptionChange(self,event=None):
- matplotlib_rc('font', **{'size':int(self.cbFont.Value) }) # affect all (including ticks)
- self.onAnyEsthOptionChange()
-
-
-class PlotPanel(wx.Panel):
- def __init__(self, parent, selPanel,infoPanel=None, mainframe=None):
-
- # Superclass constructor
- super(PlotPanel,self).__init__(parent)
-
- # Font handling
- font = parent.GetFont()
- font.SetPointSize(font.GetPointSize()-1)
- self.SetFont(font)
- # Preparing a special font manager for chinese characters
- self.specialFont=None
- try:
- pyplot_path = matplotlib.get_data_path()
- except:
- pyplot_path = pyplot_rc['datapath']
- CH_F_PATHS = [
- os.path.join(pyplot_path, 'fonts/ttf/SimHei.ttf'),
- os.path.join(os.path.dirname(__file__),'../SimHei.ttf')]
- for fpath in CH_F_PATHS:
- if os.path.exists(fpath):
- fontP = font_manager.FontProperties(fname=fpath)
- fontP.set_size(font.GetPointSize())
- self.specialFont=fontP
- break
- # data
- self.selPanel = selPanel # <<< dependency with selPanel should be minimum
- self.selMode = ''
- self.infoPanel=infoPanel
- self.infoPanel.setPlotMatrixCallbacks(self._onPlotMatrixLeftClick, self._onPlotMatrixRightClick)
- self.parent = parent
- self.mainframe= mainframe
- self.plotData = []
- self.plotDataOptions=dict()
- if self.selPanel is not None:
- bg=self.selPanel.BackgroundColour
- self.SetBackgroundColour(bg) # sowhow, our parent has a wrong color
- #self.SetBackgroundColour('red')
- self.leftMeasure = GUIMeasure(1, 'firebrick')
- self.rightMeasure = GUIMeasure(2, 'darkgreen')
- self.xlim_prev = [[0, 1]]
- self.ylim_prev = [[0, 1]]
- # GUI
- self.fig = Figure(facecolor="white", figsize=(1, 1))
- register_matplotlib_converters()
- self.canvas = FigureCanvas(self, -1, self.fig)
- self.canvas.mpl_connect('motion_notify_event', self.onMouseMove)
- self.canvas.mpl_connect('button_press_event', self.onMouseClick)
- self.canvas.mpl_connect('button_release_event', self.onMouseRelease)
- self.canvas.mpl_connect('draw_event', self.onDraw)
- self.clickLocation = (None, 0, 0)
-
- self.navTBTop = MyNavigationToolbar2Wx(self.canvas, ['Home', 'Pan'])
- self.navTBBottom = MyNavigationToolbar2Wx(self.canvas, ['Subplots', 'Save'])
- TBAddCheckTool(self.navTBBottom,'', icons.chart.GetBitmap(), self.onEsthToggle)
- self.esthToggle=False
-
- self.navTBBottom.Realize()
-
- #self.navTB = wx.ToolBar(self, style=wx.TB_HORIZONTAL|wx.TB_HORZ_LAYOUT|wx.TB_NODIVIDER|wx.TB_FLAT)
- #self.navTB.SetMargins(0,0)
- #self.navTB.SetToolPacking(0)
- #self.navTB.AddCheckTool(-1, label='', bitmap1=icons.chart.GetBitmap())
- #self.navTB.Realize()
-
- self.toolbar_sizer = wx.BoxSizer(wx.VERTICAL)
- self.toolbar_sizer.Add(self.navTBTop)
- self.toolbar_sizer.Add(self.navTBBottom)
-
-
- # --- Tool Panel
- self.toolSizer= wx.BoxSizer(wx.VERTICAL)
- # --- PlotType Panel
- self.pltTypePanel= PlotTypePanel(self);
- # --- Plot type specific options
- self.spcPanel = SpectralCtrlPanel(self)
- self.pdfPanel = PDFCtrlPanel(self)
- self.cmpPanel = CompCtrlPanel(self)
- self.mmxPanel = MinMaxPanel(self)
- # --- Esthetics panel
- self.esthPanel = EstheticsPanel(self)
-
-
- # --- Ctrl Panel
- self.ctrlPanel= wx.Panel(self)
- #self.ctrlPanel.SetBackgroundColour('blue')
- # Check Boxes
- self.cbCurveType = wx.ComboBox(self.ctrlPanel, choices=['Plain','LS','Markers','Mix'] , style=wx.CB_READONLY)
- self.cbCurveType.SetSelection(1)
- self.cbSub = wx.CheckBox(self.ctrlPanel, -1, 'Subplot',(10,10))
- self.cbLogX = wx.CheckBox(self.ctrlPanel, -1, 'Log-x',(10,10))
- self.cbLogY = wx.CheckBox(self.ctrlPanel, -1, 'Log-y',(10,10))
- self.cbSync = wx.CheckBox(self.ctrlPanel, -1, 'Sync-x',(10,10))
- self.cbXHair = wx.CheckBox(self.ctrlPanel, -1, 'CrossHair',(10,10))
- self.cbPlotMatrix = wx.CheckBox(self.ctrlPanel, -1, 'Matrix',(10,10))
- self.cbAutoScale = wx.CheckBox(self.ctrlPanel, -1, 'AutoScale',(10,10))
- self.cbGrid = wx.CheckBox(self.ctrlPanel, -1, 'Grid',(10,10))
- self.cbStepPlot = wx.CheckBox(self.ctrlPanel, -1, 'StepPlot',(10,10))
- self.cbMeasure = wx.CheckBox(self.ctrlPanel, -1, 'Measure',(10,10))
- #self.cbSub.SetValue(True) # DEFAULT TO SUB?
- self.cbSync.SetValue(True)
- self.cbXHair.SetValue(True) # Have cross hair by default
- self.cbAutoScale.SetValue(True)
- # Callbacks
- self.Bind(wx.EVT_CHECKBOX, self.redraw_event , self.cbSub )
- self.Bind(wx.EVT_COMBOBOX, self.redraw_event , self.cbCurveType)
- self.Bind(wx.EVT_CHECKBOX, self.log_select , self.cbLogX )
- self.Bind(wx.EVT_CHECKBOX, self.log_select , self.cbLogY )
- self.Bind(wx.EVT_CHECKBOX, self.redraw_event , self.cbSync )
- self.Bind(wx.EVT_CHECKBOX, self.crosshair_event , self.cbXHair )
- self.Bind(wx.EVT_CHECKBOX, self.plot_matrix_select, self.cbPlotMatrix )
- self.Bind(wx.EVT_CHECKBOX, self.redraw_event , self.cbAutoScale )
- self.Bind(wx.EVT_CHECKBOX, self.redraw_event , self.cbGrid )
- self.Bind(wx.EVT_CHECKBOX, self.redraw_event , self.cbStepPlot )
- self.Bind(wx.EVT_CHECKBOX, self.measure_select , self.cbMeasure )
- self.Bind(wx.EVT_CHECKBOX, self.measure_select , self.cbMeasure )
- # LAYOUT
- cb_sizer = wx.FlexGridSizer(rows=4, cols=3, hgap=0, vgap=0)
- cb_sizer.Add(self.cbCurveType , 0, flag=wx.ALL, border=1)
- cb_sizer.Add(self.cbSub , 0, flag=wx.ALL, border=1)
- cb_sizer.Add(self.cbAutoScale , 0, flag=wx.ALL, border=1)
- cb_sizer.Add(self.cbLogX , 0, flag=wx.ALL, border=1)
- cb_sizer.Add(self.cbLogY , 0, flag=wx.ALL, border=1)
- cb_sizer.Add(self.cbStepPlot , 0, flag=wx.ALL, border=1)
- cb_sizer.Add(self.cbXHair , 0, flag=wx.ALL, border=1)
- cb_sizer.Add(self.cbGrid , 0, flag=wx.ALL, border=1)
- cb_sizer.Add(self.cbSync , 0, flag=wx.ALL, border=1)
- cb_sizer.Add(self.cbPlotMatrix, 0, flag=wx.ALL, border=1)
- cb_sizer.Add(self.cbMeasure , 0, flag=wx.ALL, border=1)
-
- self.ctrlPanel.SetSizer(cb_sizer)
-
- # --- Crosshair Panel
- crossHairPanel= wx.Panel(self)
- self.lbCrossHairX = wx.StaticText(crossHairPanel, -1, 'x = ... ')
- self.lbCrossHairY = wx.StaticText(crossHairPanel, -1, 'y = ... ')
- self.lbDeltaX = wx.StaticText(crossHairPanel, -1, ' ')
- self.lbDeltaY = wx.StaticText(crossHairPanel, -1, ' ')
- self.lbCrossHairX.SetFont(getMonoFont(self))
- self.lbCrossHairY.SetFont(getMonoFont(self))
- self.lbDeltaX.SetFont(getMonoFont(self))
- self.lbDeltaY.SetFont(getMonoFont(self))
- cbCH = wx.FlexGridSizer(rows=4, cols=1, hgap=0, vgap=0)
- cbCH.Add(self.lbCrossHairX , 0, flag=wx.ALL, border=1)
- cbCH.Add(self.lbCrossHairY , 0, flag=wx.ALL, border=1)
- cbCH.Add(self.lbDeltaX , 0, flag=wx.ALL, border=1)
- cbCH.Add(self.lbDeltaY , 0, flag=wx.ALL, border=1)
- crossHairPanel.SetSizer(cbCH)
-
- # --- layout of panels
- row_sizer = wx.BoxSizer(wx.HORIZONTAL)
- sl2 = wx.StaticLine(self, -1, size=wx.Size(1,-1), style=wx.LI_VERTICAL)
- sl3 = wx.StaticLine(self, -1, size=wx.Size(1,-1), style=wx.LI_VERTICAL)
- sl4 = wx.StaticLine(self, -1, size=wx.Size(1,-1), style=wx.LI_VERTICAL)
- row_sizer.Add(self.pltTypePanel , 0 , flag=wx.LEFT|wx.RIGHT|wx.CENTER , border=1)
- row_sizer.Add(sl2 , 0 , flag=wx.LEFT|wx.RIGHT|wx.EXPAND|wx.CENTER, border=0)
- row_sizer.Add(self.toolbar_sizer, 0 , flag=wx.LEFT|wx.RIGHT|wx.CENTER , border=1)
- row_sizer.Add(sl3 , 0 , flag=wx.LEFT|wx.RIGHT|wx.EXPAND|wx.CENTER, border=0)
- row_sizer.Add(self.ctrlPanel , 1 , flag=wx.LEFT|wx.RIGHT|wx.EXPAND|wx.CENTER, border=0)
- row_sizer.Add(sl4 , 0 , flag=wx.LEFT|wx.RIGHT|wx.EXPAND|wx.CENTER, border=0)
- row_sizer.Add(crossHairPanel , 0 , flag=wx.LEFT|wx.RIGHT|wx.EXPAND|wx.CENTER, border=1)
-
- plotsizer = wx.BoxSizer(wx.VERTICAL)
- self.slCtrl = wx.StaticLine(self, -1, size=wx.Size(-1,1), style=wx.LI_HORIZONTAL)
- self.slCtrl.Hide()
- self.slEsth = wx.StaticLine(self, -1, size=wx.Size(-1,1), style=wx.LI_HORIZONTAL)
- self.slEsth.Hide()
- sl1 = wx.StaticLine(self, -1, size=wx.Size(-1,1), style=wx.LI_HORIZONTAL)
- plotsizer.Add(self.toolSizer,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10)
- plotsizer.Add(self.canvas ,1,flag = wx.EXPAND,border = 5 )
- plotsizer.Add(sl1 ,0,flag = wx.EXPAND,border = 0)
- plotsizer.Add(self.spcPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10)
- plotsizer.Add(self.pdfPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10)
- plotsizer.Add(self.cmpPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10)
- plotsizer.Add(self.mmxPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10)
- plotsizer.Add(self.slEsth ,0,flag = wx.EXPAND,border = 0)
- plotsizer.Add(self.esthPanel,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10)
- plotsizer.Add(self.slCtrl ,0,flag = wx.EXPAND,border = 0)
- plotsizer.Add(row_sizer ,0,flag = wx.EXPAND|wx.NORTH ,border = 2)
-
- self.show_hide(self.spcPanel, self.pltTypePanel.cbFFT.GetValue())
- self.show_hide(self.cmpPanel, self.pltTypePanel.cbCompare.GetValue())
- self.show_hide(self.pdfPanel, self.pltTypePanel.cbPDF.GetValue())
- self.show_hide(self.mmxPanel, self.pltTypePanel.cbMinMax.GetValue())
-
- self.SetSizer(plotsizer)
- self.plotsizer=plotsizer;
- self.set_subplot_spacing(init=True)
-
- def onEsthToggle(self,event):
- self.esthToggle=not self.esthToggle
- if self.esthToggle:
- self.slCtrl.Show()
- self.esthPanel.Show()
- else:
- self.slCtrl.Hide()
- self.esthPanel.Hide()
- self.plotsizer.Layout()
- event.Skip()
-
- def set_subplot_spacing(self, init=False):
- """
- Handle default subplot spacing
-
- NOTE:
- - Tight fails when the ylabel is too long, especially for fft with multiplt signals
- - might need to change depending on window size/resizing
- - need to change if right axis needed
- - this will override the user settings
- """
- #self.fig.set_tight_layout(True) # NOTE: works almost fine, but problem with FFT multiple
- # TODO this is definitely not generic, but tight fails..
- if init:
- # NOTE: at init size is (20,20) because sizer is not initialized yet
- bottom = 0.12
- left = 0.12
- else:
- if self.Size[1]<300:
- bottom=0.20
- elif self.Size[1]<350:
- bottom=0.18
- elif self.Size[1]<430:
- bottom=0.16
- elif self.Size[1]<600:
- bottom=0.13
- elif self.Size[1]<800:
- bottom=0.09
- else:
- bottom=0.07
- if self.Size[0]<300:
- left=0.22
- elif self.Size[0]<450:
- left=0.20
- elif self.Size[0]<950:
- left=0.12
- else:
- left=0.06
- #print(self.Size,'bottom', bottom, 'left',left)
- if self.cbPlotMatrix.GetValue(): # TODO detect it
- self.fig.subplots_adjust(top=0.97,bottom=bottom,left=left,right=0.98-left)
- else:
- self.fig.subplots_adjust(top=0.97,bottom=bottom,left=left,right=0.98)
-
- def plot_matrix_select(self, event):
- self.infoPanel.togglePlotMatrix(self.cbPlotMatrix.GetValue())
- self.redraw_same_data()
-
- def measure_select(self, event):
- if self.cbMeasure.IsChecked():
- self.cbAutoScale.SetValue(False)
- self.redraw_same_data()
-
- def redraw_event(self, event):
- self.redraw_same_data()
-
- def log_select(self, event):
- if self.pltTypePanel.cbPDF.GetValue():
- self.cbLogX.SetValue(False)
- self.cbLogY.SetValue(False)
- else:
- self.redraw_same_data()
-
- def crosshair_event(self, event):
- try:
- self.multiCursors.vertOn =self.cbXHair.GetValue()
- self.multiCursors.horizOn=self.cbXHair.GetValue()
- self.multiCursors._update()
- except:
- pass
-
- def show_hide(self,panel,bShow):
- if bShow:
- panel.Show()
- self.slEsth.Show()
- else:
- self.slEsth.Hide()
- panel.Hide()
-
- @property
- def sharex(self):
- return self.cbSync.IsChecked() and (not self.pltTypePanel.cbPDF.GetValue())
-
- def set_subplots(self,nPlots):
- self.set_subplot_spacing()
- # Creating subplots
- for ax in self.fig.axes:
- self.fig.delaxes(ax)
- sharex=None
- for i in range(nPlots):
- # Vertical stack
- if i==0:
- ax=self.fig.add_subplot(nPlots,1,i+1)
- if self.sharex:
- sharex=ax
- else:
- ax=self.fig.add_subplot(nPlots,1,i+1,sharex=sharex)
- # Horizontal stack
- #self.fig.add_subplot(1,nPlots,i+1)
-
- def onMouseMove(self, event):
- if event.inaxes:
- x, y = event.xdata, event.ydata
- self.lbCrossHairX.SetLabel('x =' + self.formatLabelValue(x))
- self.lbCrossHairY.SetLabel('y =' + self.formatLabelValue(y))
-
- def onMouseClick(self, event):
- self.clickLocation = (event.inaxes, event.xdata, event.ydata)
-
- def onMouseRelease(self, event):
- if self.cbMeasure.GetValue():
- for ax, ax_idx in zip(self.fig.axes, range(len(self.fig.axes))):
- if event.inaxes == ax:
- x, y = event.xdata, event.ydata
- if self.clickLocation != (ax, x, y):
- # Ignore measurements for zoom-actions. Possibly add small tolerance.
- # Zoom-actions disable autoscale
- self.cbAutoScale.SetValue(False)
- return
- if event.button == 1:
- self.infoPanel.setMeasurements((x, y), None)
- self.leftMeasure.set(ax_idx, x, y)
- self.leftMeasure.plot(ax, ax_idx)
- elif event.button == 3:
- self.infoPanel.setMeasurements(None, (x, y))
- self.rightMeasure.set(ax_idx, x, y)
- self.rightMeasure.plot(ax, ax_idx)
- else:
- return
- if self.cbAutoScale.IsChecked() is False:
- self._restore_limits()
-
- if self.leftMeasure.axis_idx == self.rightMeasure.axis_idx and self.leftMeasure.axis_idx != -1:
- self.lbDeltaX.SetLabel('dx=' + self.formatLabelValue(self.rightMeasure.x - self.leftMeasure.x))
- self.lbDeltaY.SetLabel('dy=' + self.formatLabelValue(self.rightMeasure.y - self.leftMeasure.y))
- else:
- self.lbDeltaX.SetLabel('')
- self.lbDeltaY.SetLabel('')
- return
-
- def onDraw(self, event):
- self._store_limits()
-
- def formatLabelValue(self, value):
- try:
- if abs(value)<1000 and abs(value)>1e-4:
- s = '{:10.5f}'.format(value)
- else:
- s = '{:10.3e}'.format(value)
- except TypeError:
- s = ' '
- return s
-
- def removeTools(self,event=None,Layout=True):
- try:
- self.toolPanel.destroy() # call the "destroy" function which might clean up data
- except:
- pass
- try:
- # Python3
- self.toolSizer.Clear(delete_windows=True) # Delete Windows
- except:
- # Python2
- if hasattr(self,'toolPanel'):
- self.toolSizer.Remove(self.toolPanel)
- self.toolPanel.Destroy()
- del self.toolPanel
- self.toolSizer.Clear() # Delete Windows
- if Layout:
- self.plotsizer.Layout()
-
- def showTool(self,toolName=''):
- from .GUITools import TOOLS
- self.Freeze()
- self.removeTools(Layout=False)
- if toolName in TOOLS.keys():
- self.toolPanel=TOOLS[toolName](self) # calling the panel constructor
- else:
- raise Exception('Unknown tool {}'.format(toolName))
- self.toolSizer.Add(self.toolPanel, 0, wx.EXPAND|wx.ALL, 5)
- self.plotsizer.Layout()
- self.Thaw()
-
- def setPD_PDF(self,PD,c):
- """ Convert plot data to PDF data based on GUI options"""
- # ---PDF
- nBins = self.pdfPanel.scBins.GetValue()
- bSmooth = self.pdfPanel.cbSmooth.GetValue()
- nBins_out= PD.toPDF(nBins,bSmooth)
- if nBins_out!=nBins:
- self.pdfPanel.scBins.SetValue(nBins)
-
- def setPD_MinMax(self,PD):
- """ Convert plot data to MinMax data based on GUI options"""
- yScale=self.mmxPanel.cbyMinMax.IsChecked()
- xScale=self.mmxPanel.cbxMinMax.IsChecked()
- try:
- PD.toMinMax(xScale,yScale)
- except Exception as e:
- self.mmxPanel.cbxMinMax.SetValue(False)
- raise e # Used to be Warn
-
- def setPD_FFT(self,pd):
- """ Convert plot data to FFT data based on GUI options"""
- yType = self.spcPanel.cbType.GetStringSelection()
- xType = self.spcPanel.cbTypeX.GetStringSelection()
- avgMethod = self.spcPanel.cbAveraging.GetStringSelection()
- avgWindow = self.spcPanel.cbAveragingMethod.GetStringSelection()
- bDetrend = self.spcPanel.cbDetrend.IsChecked()
- nExp = self.spcPanel.scP2.GetValue()
- # Convert plotdata to FFT data
- try:
- Info = pd.toFFT(yType=yType, xType=xType, avgMethod=avgMethod, avgWindow=avgWindow, bDetrend=bDetrend, nExp=nExp)
- # Trigger
- if hasattr(Info,'nExp') and Info.nExp!=nExp:
- self.spcPanel.scP2.SetValue(Info.nExp)
- self.spcPanel.updateP2(Info.nExp)
- except Exception as e:
- self.spcPanel.Hide();
- self.plotsizer.Layout()
- raise e
-
-
- def transformPlotData(self,PD):
- """"
- Apply MinMax, PDF or FFT transform to plot based on GUI data
- """
- plotType=self.pltTypePanel.plotType()
- if plotType=='MinMax':
- self.setPD_MinMax(PD)
- elif plotType=='PDF':
- self.setPD_PDF(PD,PD.c)
- elif plotType=='FFT':
- self.setPD_FFT(PD)
-
- def getPlotData(self,plotType):
- ID,SameCol,selMode=self.selPanel.getPlotDataSelection()
- self.selMode=selMode # we store the selection mode
- del self.plotData
- self.plotData=[]
- tabs=self.selPanel.tabList.getTabs() # TODO, selPanel should just return the PlotData...
- try:
- for i,idx in enumerate(ID):
- # Initialize each plotdata based on selected table and selected id channels
- pd=PlotData();
- pd.fromIDs(tabs,i,idx,SameCol, self.plotDataOptions)
- # Possible change of data
- if plotType=='MinMax':
- self.setPD_MinMax(pd)
- elif plotType=='PDF':
- self.setPD_PDF(pd,pd.c)
- elif plotType=='FFT':
- self.setPD_FFT(pd)
- self.plotData.append(pd)
- except Exception as e:
- self.plotData=[]
- raise e
-
- def PD_Compare(self,mode):
- """ Perform comparison of the selected PlotData, returns new plotData with the comparison. """
- sComp = self.cmpPanel.rbType.GetStringSelection()
- try:
- self.plotData = compareMultiplePD(self.plotData,mode, sComp)
- except Exception as e:
- self.pltTypePanel.cbRegular.SetValue(True)
- raise e
-
- def _onPlotMatrixLeftClick(self, event):
- """Toggle plot-states from None, to left-axis, to right-axis.
- Left-click goes forwards, right-click goes backwards.
- IndexError to avoid "holes" in matrix with outer adjacent populated entries
- """
- btn = event.GetEventObject()
- label = btn.GetLabelText()
- if label == '-':
- btn.SetLabel('1')
- try:
- self.infoPanel.getPlotMatrix(self.plotData, self.cbSub.IsChecked())
- except IndexError:
- btn.SetLabel('-')
- elif label == '1':
- btn.SetLabel('2')
- else:
- btn.SetLabel('-')
- try:
- self.infoPanel.getPlotMatrix(self.plotData, self.cbSub.IsChecked())
- except IndexError:
- btn.SetLabel('1')
- self.redraw_same_data()
-
- def _onPlotMatrixRightClick(self, event):
- btn = event.GetEventObject()
- label = btn.GetLabelText()
- if label == '-':
- btn.SetLabel('2')
- try:
- self.infoPanel.getPlotMatrix(self.plotData, self.cbSub.IsChecked())
- except IndexError:
- btn.SetLabel('-')
- elif label == '1':
- btn.SetLabel('-')
- try:
- self.infoPanel.getPlotMatrix(self.plotData, self.cbSub.IsChecked())
- except IndexError:
- btn.SetLabel('2')
- else:
- btn.SetLabel('1')
- self.redraw_same_data()
-
- def set_axes_lim(self, PDs, axis):
- """
- It's usually faster to set the axis limits first (before plotting)
- and disable autoscaling. This way the limits are not recomputed when plot data are added.
- Also, we already have computed the min and max, so we leverage that.
- NOTE:
- doesnt not work with strings
- doesnt not work for FFT and compare
-
- INPUTS:
- PDs: list of plot data
- """
- # TODO option for tight axes
- tight=False
-
- plotType=self.pltTypePanel.plotType()
- if plotType in ['FFT','Compare']:
- axis.autoscale(True, axis='both', tight=tight)
- return
- vXString=[PDs[i].xIsString for i in axis.iPD]
- vYString=[PDs[i].yIsString for i in axis.iPD]
- if not any(vXString) and not self.cbLogX.IsChecked():
- try:
- xMin=np.min([PDs[i]._xMin[0] for i in axis.iPD])
- xMax=np.max([PDs[i]._xMax[0] for i in axis.iPD])
- if np.isclose(xMin,xMax):
- delta=1 if np.isclose(xMax,0) else 0.1*xMax
- else:
- if tight:
- delta=0
- else:
- delta = (xMax-xMin)*pyplot_rc['axes.xmargin']
- axis.set_xlim(xMin-delta,xMax+delta)
- axis.autoscale(False, axis='x', tight=False)
- except:
- pass
- if not any(vYString) and not self.cbLogY.IsChecked():
- try:
- yMin=np.min([PDs[i]._yMin[0] for i in axis.iPD])
- yMax=np.max([PDs[i]._yMax[0] for i in axis.iPD])
- delta = (yMax-yMin)*pyplot_rc['axes.ymargin']
- if np.isclose(yMin,yMax):
- delta=1 if np.isclose(yMax,0) else 0.1*yMax
- else:
- if tight:
- delta=0
- else:
- delta = (yMax-yMin)*pyplot_rc['axes.xmargin']
- axis.set_ylim(yMin-delta,yMax+delta)
- axis.autoscale(False, axis='y', tight=False)
- except:
- pass
-
- def plot_all(self, keep_limits=True):
- self.multiCursors=[]
-
- if self.cbMeasure.GetValue() is False:
- for measure in [self.leftMeasure, self.rightMeasure]:
- measure.clear()
- self.infoPanel.setMeasurements(None, None)
- self.lbDeltaX.SetLabel('')
- self.lbDeltaY.SetLabel('')
-
- axes=self.fig.axes
- PD=self.plotData
-
-
- # --- Plot options
- bStep = self.cbStepPlot.IsChecked()
- plot_options = dict()
- plot_options['lw']=float(self.esthPanel.cbLW.Value)
- plot_options['ms']=float(self.esthPanel.cbMS.Value)
- if self.cbCurveType.Value=='Plain':
- plot_options['LineStyles'] = ['-']
- plot_options['Markers'] = ['']
- elif self.cbCurveType.Value=='LS':
- plot_options['LineStyles'] = ['-','--','-.',':']
- plot_options['Markers'] = ['']
- elif self.cbCurveType.Value=='Markers':
- plot_options['LineStyles'] = ['']
- plot_options['Markers'] = ['o','d','v','^','s']
- elif self.cbCurveType.Value=='Mix': # NOTE, can be improved
- plot_options['LineStyles'] = ['-','--', '-','-','-']
- plot_options['Markers'] = ['' ,'' ,'o','^','s']
- else:
- # Combination of linestyles markers, colors, etc.
- # But at that stage, if the user really want this, then we can implement an option to set styles per plot. Not high priority.
- raise Exception('Not implemented')
-
-
-
- # --- Font options
- font_options = dict()
- font_options_legd = dict()
- font_options['size'] = int(self.esthPanel.cbFont.Value) # affect labels
- font_options_legd['fontsize'] = int(self.esthPanel.cbLgdFont.Value)
- needChineseFont = any([pd.needChineseFont for pd in PD])
- if needChineseFont and self.specialFont is not None:
- font_options['fontproperties']= self.specialFont
- font_options_legd['prop'] = self.specialFont
-
- # --- Loop on axes. Either use ax.iPD to chose the plot data, or rely on plotmatrix
- for axis_idx, ax_left in enumerate(axes):
- ax_right = None
- # Checks
- vDate=[PD[i].yIsDate for i in ax_left.iPD]
- if any(vDate) and len(vDate)>1:
- Error(self,'Cannot plot date and other value on the same axis')
- return
-
- # Set limit before plot when possible, for optimization
- self.set_axes_lim(PD, ax_left)
-
- # Actually plot
- pm = self.infoPanel.getPlotMatrix(PD, self.cbSub.IsChecked())
- __, bAllNegLeft = self.plotSignals(ax_left, axis_idx, PD, pm, 1, bStep, plot_options)
- ax_right, bAllNegRight = self.plotSignals(ax_left, axis_idx, PD, pm, 2, bStep, plot_options)
-
- self.infoPanel.setMeasurements(self.leftMeasure.get_xydata(), self.rightMeasure.get_xydata())
- for measure in [self.leftMeasure, self.rightMeasure]:
- measure.plot(ax_left, axis_idx)
-
- # Log Axes
- if self.cbLogX.IsChecked():
- try:
- ax_left.set_xscale("log", nonpositive='clip') # latest
- except:
- ax_left.set_xscale("log", nonposx='clip') # legacy
-
- if self.cbLogY.IsChecked():
- if bAllNegLeft is False:
- try:
- ax_left.set_yscale("log", nonpositive='clip') # latest
- except:
- ax_left.set_yscale("log", nonposy='clip')
- if bAllNegRight is False and ax_right is not None:
- try:
- ax_right.set_yscale("log", nonpositive='clip') # latest
- except:
- ax_left.set_yscale("log", nonposy='clip') # legacy
-
- # XLIM - TODO FFT ONLY NASTY
- if self.pltTypePanel.cbFFT.GetValue():
- try:
- xlim=float(self.spcPanel.tMaxFreq.GetLineText(0))
- if xlim>0:
- ax_left.set_xlim([0,xlim])
- pd=PD[ax_left.iPD[0]]
- I=pd.x 0 and len(yleft_labels) <= 3:
- ax_left.set_ylabel(' and '.join(yleft_labels), **font_options)
- elif ax_left is not None:
- ax_left.set_ylabel('')
- if len(yright_labels) > 0 and len(yright_labels) <= 3:
- ax_right.set_ylabel(' and '.join(yright_labels), **font_options)
- elif ax_right is not None:
- ax_right.set_ylabel('')
-
- # Legends
- lgdLoc = self.esthPanel.cbLegend.Value.lower()
- if (self.pltTypePanel.cbCompare.GetValue() or
- ((len(yleft_legends) + len(yright_legends)) > 1)):
- if lgdLoc !='none':
- if len(yleft_legends) > 0:
- ax_left.legend(fancybox=False, loc=lgdLoc, **font_options_legd)
- if ax_right is not None and len(yright_legends) > 0:
- ax_right.legend(fancybox=False, loc=4, **font_options_legd)
- elif len(axes)>1 and len(axes)==len(PD):
- # TODO: can this be removed? If there is only one unique signal
- # per subplot, normally only ylabel is displayed and no legend.
- # Special case when we have subplots and all plots have the same label
- if lgdLoc !='none':
- usy = unique([pd.sy for pd in PD])
- if len(usy)==1:
- for ax in axes:
- ax.legend(fancybox=False, loc=lgdLoc, **font_options_legd)
-
- axes[-1].set_xlabel(PD[axes[-1].iPD[0]].sx, **font_options)
-
- #print('sy :',[pd.sy for pd in PD])
- #print('syl:',[pd.syl for pd in PD])
-
- # --- Cursors for each individual plot
- # NOTE: cursors needs to be stored in the object!
- #for ax_left in self.fig.axes:
- # self.cursors.append(MyCursor(ax_left,horizOn=True, vertOn=False, useblit=True, color='gray', linewidth=0.5, linestyle=':'))
- # Vertical cusor for all, commonly
- bXHair = self.cbXHair.GetValue()
- self.multiCursors = MyMultiCursor(self.canvas, tuple(self.fig.axes), useblit=True, horizOn=bXHair, vertOn=bXHair, color='gray', linewidth=0.5, linestyle=':')
-
- def plotSignals(self, ax, axis_idx, PD, pm, left_right, is_step, opts):
- axis = None
- bAllNeg = True
- if pm is None:
- loop_range = ax.iPD
- else:
- loop_range = range(len(PD))
-
- iPlot=-1
- for signal_idx in loop_range:
- do_plot = False
- if left_right == 1 and (pm is None or pm[signal_idx][axis_idx] == left_right):
- do_plot = True
- axis = ax
- elif left_right == 2 and pm is not None and pm[signal_idx][axis_idx] == left_right:
- do_plot = True
- if axis is None:
- axis = ax.twinx()
- ax.set_zorder(axis.get_zorder()+1)
- ax.patch.set_visible(False)
- axis._get_lines.prop_cycler = ax._get_lines.prop_cycler
- pd=PD[signal_idx]
- if do_plot:
- iPlot+=1
- # --- styling per plot
- if len(pd.x)==1:
- marker='o'; ls=''
- else:
- # TODO allow PlotData to override for "per plot" options in the future
- marker = opts['Markers'][np.mod(iPlot,len(opts['Markers']))]
- ls = opts['LineStyles'][np.mod(iPlot,len(opts['LineStyles']))]
- if is_step:
- plot = axis.step
- else:
- plot = axis.plot
- plot(pd.x,pd.y,label=pd.syl,ms=opts['ms'], lw=opts['lw'], marker=marker, ls=ls)
- try:
- bAllNeg = bAllNeg and all(pd.y<=0)
- except:
- pass # Dates or strings
- return axis, bAllNeg
-
- def findPlotMode(self,PD):
- uTabs = unique([pd.it for pd in PD])
- usy = unique([pd.sy for pd in PD])
- uiy = unique([pd.iy for pd in PD])
- if len(uTabs)<=0:
- raise Exception('No Table. Contact developer')
- if len(uTabs)==1:
- mode='1Tab_nCols'
- else:
- if PD[0].SameCol:
- mode='nTabs_SameCols'
- else:
- # Now that we allow multiple selections detecting "simColumns" is more difficult
- if len(uTabs) == len(PD):
- mode='nTabs_1Col'
- elif self.selMode=='simColumnsMode':
- mode='nTabs_SimCols'
- else:
- mode='nTabs_mCols'
- return mode
-
- def findSubPlots(self,PD,mode):
- uTabs = unique([pd.it for pd in PD])
- usy = unique([pd.sy for pd in PD])
- bSubPlots = self.cbSub.IsChecked()
- bCompare = self.pltTypePanel.cbCompare.GetValue() # NOTE bCompare somehow always 1Tab_nCols
- nSubPlots=1
- spreadBy='none'
- self.infoPanel.setTabMode(mode)
- if mode=='1Tab_nCols':
- if bSubPlots:
- if bCompare or len(uTabs)==1:
- nSubPlots = self.infoPanel.getNumberOfSubplots(PD, bSubPlots)
- else:
- nSubPlots=len(usy)
- spreadBy='iy'
- elif mode=='nTabs_SameCols':
- if bSubPlots:
- if bCompare:
- print('>>>TODO ',mode,len(usy),len(uTabs))
- else:
- if len(usy)==1:
- # Temporary hack until we have an option for spread by tabs or col
- nSubPlots=len(uTabs)
- spreadBy='it'
- else:
- nSubPlots=len(usy)
- spreadBy='iy'
- elif mode=='nTabs_SimCols':
- if bSubPlots:
- if bCompare:
- print('>>>TODO ',mode,len(usy),len(uTabs))
- else:
- nSubPlots=int(len(PD)/len(uTabs))
- spreadBy='mod-ip'
- elif mode=='nTabs_mCols':
- if bSubPlots:
- if bCompare:
- print('>>>TODO ',mode,len(usy),len(uTabs))
- else:
- if bCompare or len(uTabs)==1:
- nSubPlots = self.infoPanel.getNumberOfSubplots(PD, bSubPlots)
- else:
- nSubPlots=len(PD)
- spreadBy='mod-ip'
- elif mode=='nTabs_1Col':
- if bSubPlots:
- if bCompare:
- print('>>> TODO',mode,len(uTabs))
- else:
- nSubPlots=len(uTabs)
- spreadBy='it'
- else:
- raise Exception('Unknown mode, contact developer.')
- return nSubPlots,spreadBy
-
- def distributePlots(self,mode,nSubPlots,spreadBy):
- """ Assigns plot data to axes and axes to plot data """
- axes=self.fig.axes
-
- # Link plot data to axes
- if nSubPlots==1 or spreadBy=='none':
- axes[0].iPD=[i for i in range(len(self.plotData))]
- else:
- for ax in axes:
- ax.iPD=[]
- PD=self.plotData
- uTabs=unique([pd.it for pd in PD])
- uiy=unique([pd.iy for pd in PD])
- if spreadBy=='iy':
- for ipd,pd in enumerate(PD):
- i=uiy.index(pd.iy)
- if i < len(axes):
- axes[i].iPD.append(ipd)
- elif spreadBy=='it':
- for ipd,pd in enumerate(PD):
- i=uTabs.index(pd.it)
- axes[i].iPD.append(ipd)
- elif spreadBy=='mod-ip':
- for ipd,pd in enumerate(PD):
- i=np.mod(ipd, nSubPlots)
- axes[i].iPD.append(ipd)
- else:
- raise Exception('Wrong spreadby value')
-
- def setLegendLabels(self,mode):
- """ Set labels for legend """
- if mode=='1Tab_nCols':
- for pd in self.plotData:
- if self.pltTypePanel.cbMinMax.GetValue():
- pd.syl = no_unit(pd.sy)
- else:
- pd.syl = pd.sy
-
- elif mode=='nTabs_SameCols':
- for pd in self.plotData:
- pd.syl=pd.st
-
- elif mode=='nTabs_1Col':
- usy=unique([pd.sy for pd in self.plotData])
- if len(usy)==1:
- for pd in self.plotData:
- pd.syl=pd.st
- else:
- for pd in self.plotData:
- if self.pltTypePanel.cbMinMax.GetValue():
- pd.syl=no_unit(pd.sy)
- else:
- pd.syl=pd.sy #pd.syl=pd.st + ' - '+pd.sy
- elif mode=='nTabs_SimCols':
- bSubPlots = self.cbSub.IsChecked()
- if bSubPlots: # spread by table name
- for pd in self.plotData:
- pd.syl=pd.st
- else:
- for pd in self.plotData:
- pd.syl=pd.st + ' - '+pd.sy
- elif mode=='nTabs_mCols':
- usy=unique([pd.sy for pd in self.plotData])
- bSubPlots = self.cbSub.IsChecked()
- if bSubPlots and len(usy)==1: # spread by table name
- for pd in self.plotData:
- pd.syl=pd.st
- else:
- for pd in self.plotData:
- pd.syl=pd.st + ' - '+pd.sy
- else:
- raise Exception('Unknown mode {}'.format(mode))
-
-
- def empty(self):
- self.cleanPlot()
-
- def clean_memory(self):
- if hasattr(self,'plotData'):
- del self.plotData
- self.plotData=[]
- for ax in self.fig.axes:
- ax.iPD=[]
- self.fig.delaxes(ax)
- gc.collect()
-
- def clean_memory_plot(self):
- pass
-
- def cleanPlot(self):
- for ax in self.fig.axes:
- if hasattr(ax,'iPD'):
- del ax.iPD
- self.fig.delaxes(ax)
- gc.collect()
- self.fig.add_subplot(111)
- ax = self.fig.axes[0]
- ax.set_axis_off()
- #ax.plot(1,1)
- self.canvas.draw()
- gc.collect()
-
- def load_and_draw(self):
- """ Full draw event:
- - Get plot data based on selection
- - Plot them
- - Trigger changes to infoPanel
-
- """
- self.clean_memory()
- self.getPlotData(self.pltTypePanel.plotType())
- if len(self.plotData)==0:
- self.cleanPlot();
- return
- mode=self.findPlotMode(self.plotData)
- if self.pltTypePanel.cbCompare.GetValue():
- self.PD_Compare(mode)
- if len(self.plotData)==0:
- self.cleanPlot();
- return
- self.redraw_same_data()
- if self.infoPanel is not None:
- self.infoPanel.showStats(self.plotData,self.pltTypePanel.plotType())
-
- def redraw_same_data(self, keep_limits=True):
- if len(self.plotData)==0:
- self.cleanPlot();
- return
- elif len(self.plotData) == 1:
- if self.plotData[0].xIsString or self.plotData[0].yIsString or self.plotData[0].xIsDate or self.plotData[0].yIsDate:
- self.cbAutoScale.SetValue(True)
- else:
- if len(self.xlim_prev)==0: # Might occur if some date didn't plot before (e.g. strings)
- self.cbAutoScale.SetValue(True)
- elif rectangleOverlap(self.plotData[0]._xMin[0], self.plotData[0]._yMin[0],
- self.plotData[0]._xMax[0], self.plotData[0]._yMax[0],
- self.xlim_prev[0][0], self.ylim_prev[0][0],
- self.xlim_prev[0][1], self.ylim_prev[0][1]):
- pass
- else:
- self.cbAutoScale.SetValue(True)
-
- mode=self.findPlotMode(self.plotData)
- nPlots,spreadBy=self.findSubPlots(self.plotData,mode)
-
- self.clean_memory_plot()
- self.set_subplots(nPlots)
- self.distributePlots(mode,nPlots,spreadBy)
-
- if not self.pltTypePanel.cbCompare.GetValue():
- self.setLegendLabels(mode)
-
- self.plot_all(keep_limits)
- self.canvas.draw()
-
-
- def _store_limits(self):
- self.xlim_prev = []
- self.ylim_prev = []
- for ax in self.fig.axes:
- self.xlim_prev.append(ax.get_xlim())
- self.ylim_prev.append(ax.get_ylim())
-
- def _restore_limits(self):
- for ax, xlim, ylim in zip(self.fig.axes, self.xlim_prev, self.ylim_prev):
- ax.set_xlim(xlim)
- ax.set_ylim(ylim)
-
-
-if __name__ == '__main__':
- import pandas as pd;
- from Tables import Table,TableList
-
- app = wx.App(False)
- self=wx.Frame(None,-1,"Title")
- self.SetSize((800, 600))
- #self.SetBackgroundColour('red')
- class FakeSelPanel(wx.Panel):
- def __init__(self, parent):
- super(FakeSelPanel,self).__init__(parent)
- d ={'ColA': np.linspace(0,1,100)+1,'ColB': np.random.normal(0,1,100)+0,'ColC':np.random.normal(0,1,100)+1}
- df = pd.DataFrame(data=d)
- self.tabList=TableList([Table(data=df)])
-
- def getPlotDataSelection(self):
- ID=[]
- ID.append([0,0,2,'x','ColB','tab'])
- ID.append([0,0,3,'x','ColC','tab'])
- return ID,True
-
- selpanel=FakeSelPanel(self)
- # selpanel.SetBackgroundColour('blue')
- p1=PlotPanel(self,selpanel)
- p1.load_and_draw()
- #p1=SpectralCtrlPanel(self)
- sizer = wx.BoxSizer(wx.VERTICAL)
- sizer.Add(selpanel,0, flag = wx.EXPAND|wx.ALL,border = 10)
- sizer.Add(p1,1, flag = wx.EXPAND|wx.ALL,border = 10)
- self.SetSizer(sizer)
-
- self.Center()
- self.Layout()
- self.SetSize((800, 600))
- self.Show()
- self.SendSizeEvent()
-
- #p1.showStats(None,[tab],[0],[0,1],tab.columns,0,erase=False)
-
- app.MainLoop()
-
-
+import os
+import numpy as np
+import wx
+import wx.lib.buttons as buttons
+import dateutil # required by matplotlib
+#from matplotlib import pyplot as plt
+import matplotlib
+matplotlib.use('wxAgg') # Important for Windows version of installer. NOTE: changed from Agg to wxAgg
+from matplotlib import rc as matplotlib_rc
+try:
+ from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
+except Exception as e:
+ print('')
+ print('Error: problem importing `matplotlib.backends.backend_wx`.')
+ import platform
+ if platform.system()=='Darwin':
+ print('')
+ print('pyDatView help:')
+ print(' This is a typical issue on MacOS, most likely you are')
+ print(' using the native MacOS python with the native matplolib')
+ print(' library, which is incompatible with `wxPython`.')
+ print('')
+ print(' You can solve this by either:')
+ print(' - using python3, and pip3 e.g. installing it with brew')
+ print(' - using a virtual environment with python 2 or 3')
+ print(' - using anaconda with python 2 or 3');
+ print('')
+ import sys
+ sys.exit(1)
+ else:
+ raise e
+from matplotlib.figure import Figure
+from matplotlib.pyplot import rcParams as pyplot_rc
+from matplotlib import font_manager
+from pandas.plotting import register_matplotlib_converters
+
+import gc
+
+from .common import * # unique, CHAR
+from .plotdata import PlotData, compareMultiplePD
+from .GUICommon import *
+from .GUIToolBox import MyMultiCursor, MyNavigationToolbar2Wx, TBAddTool, TBAddCheckTool
+from .GUIMeasure import GUIMeasure
+from . import icons
+
+font = {'size' : 8}
+matplotlib_rc('font', **font)
+pyplot_rc['agg.path.chunksize'] = 20000
+
+
+class PDFCtrlPanel(wx.Panel):
+ def __init__(self, parent):
+ super(PDFCtrlPanel,self).__init__(parent)
+ self.parent = parent
+ lb = wx.StaticText( self, -1, 'Number of bins:')
+ self.scBins = wx.SpinCtrl(self, value='51',size=wx.Size(70,-1), style=wx.TE_RIGHT)
+ self.scBins.SetRange(3, 10000)
+ self.cbSmooth = wx.CheckBox(self, -1, 'Smooth',(10,10))
+ self.cbSmooth.SetValue(False)
+ dummy_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ dummy_sizer.Add(lb ,0, flag = wx.CENTER|wx.LEFT,border = 1)
+ dummy_sizer.Add(self.scBins ,0, flag = wx.CENTER|wx.LEFT,border = 1)
+ dummy_sizer.Add(self.cbSmooth ,0, flag = wx.CENTER|wx.LEFT,border = 6)
+ self.SetSizer(dummy_sizer)
+ self.Bind(wx.EVT_TEXT , self.onPDFOptionChange, self.scBins)
+ self.Bind(wx.EVT_CHECKBOX, self.onPDFOptionChange)
+ self.Hide()
+
+ def onPDFOptionChange(self,event=None):
+ self.parent.load_and_draw(); # DATA HAS CHANGED
+
+class MinMaxPanel(wx.Panel):
+ def __init__(self, parent):
+ super(MinMaxPanel,self).__init__(parent)
+ self.parent = parent
+ self.cbxMinMax = wx.CheckBox(self, -1, 'xMinMax',(10,10))
+ self.cbyMinMax = wx.CheckBox(self, -1, 'yMinMax',(10,10))
+ self.cbxMinMax.SetValue(False)
+ self.cbyMinMax.SetValue(True)
+ dummy_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ dummy_sizer.Add(self.cbxMinMax ,0, flag=wx.CENTER|wx.LEFT, border = 1)
+ dummy_sizer.Add(self.cbyMinMax ,0, flag=wx.CENTER|wx.LEFT, border = 1)
+ self.SetSizer(dummy_sizer)
+ self.Bind(wx.EVT_CHECKBOX, self.onMinMaxChange)
+ self.Hide()
+
+ def onMinMaxChange(self,event=None):
+ self.parent.load_and_draw(); # DATA HAS CHANGED
+
+class CompCtrlPanel(wx.Panel):
+ def __init__(self, parent):
+ super(CompCtrlPanel,self).__init__(parent)
+ self.parent = parent
+ lblList = ['Relative', '|Relative|','Ratio','Absolute','Y-Y']
+ self.rbType = wx.RadioBox(self, label = 'Type', choices = lblList,
+ majorDimension = 1, style = wx.RA_SPECIFY_ROWS)
+ dummy_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ dummy_sizer.Add(self.rbType ,0, flag = wx.CENTER|wx.LEFT,border = 1)
+ self.SetSizer(dummy_sizer)
+ self.rbType.Bind(wx.EVT_RADIOBOX,self.onTypeChange)
+ self.Hide()
+
+ def onTypeChange(self,e):
+ self.parent.load_and_draw(); # DATA HAS CHANGED
+
+
+class SpectralCtrlPanel(wx.Panel):
+ def __init__(self, parent):
+ super(SpectralCtrlPanel,self).__init__(parent)
+ self.parent = parent
+ # --- GUI widgets
+ lb = wx.StaticText( self, -1, 'Type:')
+ self.cbType = wx.ComboBox(self, choices=['PSD','f x PSD','Amplitude'] , style=wx.CB_READONLY)
+ self.cbType.SetSelection(0)
+ lbAveraging = wx.StaticText( self, -1, 'Avg.:')
+ self.cbAveraging = wx.ComboBox(self, choices=['None','Welch','Binning'] , style=wx.CB_READONLY)
+ self.cbAveraging.SetSelection(1)
+ self.lbAveragingMethod = wx.StaticText( self, -1, 'Window:')
+ self.cbAveragingMethod = wx.ComboBox(self, choices=['Hamming','Hann','Rectangular'] , style=wx.CB_READONLY)
+ self.cbAveragingMethod.SetSelection(0)
+ self.lbP2 = wx.StaticText( self, -1, '2^n:')
+ self.scP2 = wx.SpinCtrl(self, value='11',size=wx.Size(40,-1))
+ self.lbWinLength = wx.StaticText( self, -1, '(2048) ')
+ self.scP2.SetRange(3, 50)
+ self.previousNExp = 8
+ self.previousNDec = 20
+ lbMaxFreq = wx.StaticText( self, -1, 'Xlim:')
+ self.tMaxFreq = wx.TextCtrl(self,size = (30,-1),style=wx.TE_PROCESS_ENTER)
+ self.tMaxFreq.SetValue("-1")
+ self.cbDetrend = wx.CheckBox(self, -1, 'Detrend',(10,10))
+ lbX = wx.StaticText( self, -1, 'x:')
+ self.cbTypeX = wx.ComboBox(self, choices=['1/x','2pi/x','x'] , style=wx.CB_READONLY)
+ self.cbTypeX.SetSelection(0)
+ # Layout
+ dummy_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ dummy_sizer.Add(lb ,0, flag = wx.CENTER|wx.LEFT,border = 1)
+ dummy_sizer.Add(self.cbType ,0, flag = wx.CENTER|wx.LEFT,border = 1)
+ dummy_sizer.Add(lbAveraging ,0, flag = wx.CENTER|wx.LEFT,border = 6)
+ dummy_sizer.Add(self.cbAveraging ,0, flag = wx.CENTER|wx.LEFT,border = 1)
+ dummy_sizer.Add(self.lbAveragingMethod,0, flag = wx.CENTER|wx.LEFT,border = 6)
+ dummy_sizer.Add(self.cbAveragingMethod,0, flag = wx.CENTER|wx.LEFT,border = 1)
+ dummy_sizer.Add(self.lbP2 ,0, flag = wx.CENTER|wx.LEFT,border = 6)
+ dummy_sizer.Add(self.scP2 ,0, flag = wx.CENTER|wx.LEFT,border = 1)
+ dummy_sizer.Add(self.lbWinLength ,0, flag = wx.CENTER|wx.LEFT,border = 1)
+ dummy_sizer.Add(lbMaxFreq ,0, flag = wx.CENTER|wx.LEFT,border = 6)
+ dummy_sizer.Add(self.tMaxFreq ,0, flag = wx.CENTER|wx.LEFT,border = 1)
+ dummy_sizer.Add(lbX ,0, flag = wx.CENTER|wx.LEFT,border = 6)
+ dummy_sizer.Add(self.cbTypeX ,0, flag = wx.CENTER|wx.LEFT,border = 1)
+ dummy_sizer.Add(self.cbDetrend ,0, flag = wx.CENTER|wx.LEFT,border = 7)
+ self.SetSizer(dummy_sizer)
+ self.Bind(wx.EVT_COMBOBOX ,self.onSpecCtrlChange)
+ self.Bind(wx.EVT_TEXT ,self.onP2ChangeText ,self.scP2 )
+ self.Bind(wx.EVT_TEXT_ENTER,self.onXlimChange ,self.tMaxFreq )
+ self.Bind(wx.EVT_CHECKBOX ,self.onDetrendChange ,self.cbDetrend)
+ self.Hide()
+
+ def onXlimChange(self,event=None):
+ self.parent.redraw_same_data();
+ def onSpecCtrlChange(self,event=None):
+ if self.cbAveraging.GetStringSelection()=='None':
+ self.scP2.Enable(False)
+ self.cbAveragingMethod.Enable(False)
+ self.lbP2.SetLabel('')
+ self.lbWinLength.SetLabel('')
+ elif self.cbAveraging.GetStringSelection()=='Binning':
+ self.previousNExp= self.scP2.GetValue()
+ self.scP2.SetValue(self.previousNDec)
+ self.scP2.Enable(True)
+ self.cbAveragingMethod.Enable(False)
+ self.lbP2.SetLabel('n:')
+ self.lbWinLength.SetLabel('')
+ else:
+ self.previousDec= self.scP2.GetValue()
+ self.scP2.SetValue(self.previousNExp)
+ self.lbP2.SetLabel('2^n:')
+ self.scP2.Enable(True)
+ self.cbAveragingMethod.Enable(True)
+ self.onP2ChangeText(event=None)
+ self.parent.load_and_draw() # Data changes
+
+ def onDetrendChange(self,event=None):
+ self.parent.load_and_draw() # Data changes
+
+ def onP2ChangeText(self,event=None):
+ if self.cbAveraging.GetStringSelection()=='Binning':
+ pass
+ else:
+ nExp=self.scP2.GetValue()
+ self.updateP2(nExp)
+ self.parent.load_and_draw() # Data changes
+
+ def updateP2(self,P2):
+ self.lbWinLength.SetLabel("({})".format(2**P2))
+
+
+
+
+class PlotTypePanel(wx.Panel):
+ def __init__(self, parent):
+ # Superclass constructor
+ super(PlotTypePanel,self).__init__(parent)
+ #self.SetBackgroundColour('yellow')
+ # data
+ self.parent = parent
+ # --- Ctrl Panel
+ self.cbRegular = wx.RadioButton(self, -1, 'Regular',style=wx.RB_GROUP)
+ self.cbPDF = wx.RadioButton(self, -1, 'PDF' , )
+ self.cbFFT = wx.RadioButton(self, -1, 'FFT' , )
+ self.cbMinMax = wx.RadioButton(self, -1, 'MinMax' , )
+ self.cbCompare = wx.RadioButton(self, -1, 'Compare', )
+ self.cbRegular.SetValue(True)
+ self.Bind(wx.EVT_RADIOBUTTON, self.pdf_select , self.cbPDF )
+ self.Bind(wx.EVT_RADIOBUTTON, self.fft_select , self.cbFFT )
+ self.Bind(wx.EVT_RADIOBUTTON, self.minmax_select , self.cbMinMax )
+ self.Bind(wx.EVT_RADIOBUTTON, self.compare_select, self.cbCompare)
+ self.Bind(wx.EVT_RADIOBUTTON, self.regular_select, self.cbRegular)
+ # LAYOUT
+ cb_sizer = wx.FlexGridSizer(rows=5, cols=1, hgap=0, vgap=0)
+ cb_sizer.Add(self.cbRegular , 0, flag=wx.ALL, border=1)
+ cb_sizer.Add(self.cbPDF , 0, flag=wx.ALL, border=1)
+ cb_sizer.Add(self.cbFFT , 0, flag=wx.ALL, border=1)
+ cb_sizer.Add(self.cbMinMax , 0, flag=wx.ALL, border=1)
+ cb_sizer.Add(self.cbCompare , 0, flag=wx.ALL, border=1)
+ self.SetSizer(cb_sizer)
+
+ def plotType(self):
+ plotType='Regular'
+ if self.cbMinMax.GetValue():
+ plotType='MinMax'
+ elif self.cbPDF.GetValue():
+ plotType='PDF'
+ elif self.cbFFT.GetValue():
+ plotType='FFT'
+ elif self.cbCompare.GetValue():
+ plotType='Compare'
+ return plotType
+
+ def regular_select(self, event=None):
+ self.clear_measures()
+ self.parent.cbLogY.SetValue(False)
+ #
+ self.parent.spcPanel.Hide();
+ self.parent.pdfPanel.Hide();
+ self.parent.cmpPanel.Hide();
+ self.parent.mmxPanel.Hide();
+ self.parent.slEsth.Hide();
+ self.parent.plotsizer.Layout()
+ #
+ self.parent.load_and_draw() # Data changes
+
+ def compare_select(self, event=None):
+ self.clear_measures()
+ self.parent.cbLogY.SetValue(False)
+ self.parent.show_hide(self.parent.cmpPanel, self.cbCompare.GetValue())
+ self.parent.spcPanel.Hide();
+ self.parent.pdfPanel.Hide();
+ self.parent.mmxPanel.Hide();
+ self.parent.plotsizer.Layout()
+ self.parent.load_and_draw() # Data changes
+
+ def fft_select(self, event=None):
+ self.clear_measures()
+ self.parent.show_hide(self.parent.spcPanel, self.cbFFT.GetValue())
+ self.parent.cbLogY.SetValue(self.cbFFT.GetValue())
+ self.parent.pdfPanel.Hide();
+ self.parent.mmxPanel.Hide();
+ self.parent.plotsizer.Layout()
+ self.parent.load_and_draw() # Data changes
+
+ def pdf_select(self, event=None):
+ self.clear_measures()
+ self.parent.cbLogX.SetValue(False)
+ self.parent.cbLogY.SetValue(False)
+ self.parent.show_hide(self.parent.pdfPanel, self.cbPDF.GetValue())
+ self.parent.spcPanel.Hide();
+ self.parent.cmpPanel.Hide();
+ self.parent.mmxPanel.Hide();
+ self.parent.plotsizer.Layout()
+ self.parent.load_and_draw() # Data changes
+
+ def minmax_select(self, event):
+ self.clear_measures()
+ self.parent.cbLogY.SetValue(False)
+ self.parent.show_hide(self.parent.mmxPanel, self.cbMinMax.GetValue())
+ self.parent.spcPanel.Hide();
+ self.parent.pdfPanel.Hide();
+ self.parent.cmpPanel.Hide();
+ self.parent.plotsizer.Layout()
+ self.parent.load_and_draw() # Data changes
+
+ def clear_measures(self):
+ self.parent.rightMeasure.clear()
+ self.parent.leftMeasure.clear()
+ self.parent.lbDeltaX.SetLabel('')
+ self.parent.lbDeltaY.SetLabel('')
+
+class EstheticsPanel(wx.Panel):
+ def __init__(self, parent, data):
+ wx.Panel.__init__(self, parent)
+ self.parent=parent
+ #self.SetBackgroundColour('red')
+
+ # Font
+ lbFont = wx.StaticText( self, -1, 'Font:')
+ fontChoices = ['6','7','8','9','10','11','12','13','14','15','16','17','18']
+ self.cbFont = wx.ComboBox(self, choices=fontChoices , style=wx.CB_READONLY)
+ try:
+ i = fontChoices.index(str(data['Font']))
+ except ValueError:
+ i = 2
+ self.cbFont.SetSelection(i)
+ # Legend
+ # NOTE: we don't offer "best" since best is slow
+ lbLegend = wx.StaticText( self, -1, 'Legend:')
+ lbChoices = ['None','Upper right','Upper left','Lower left','Lower right','Right','Center left','Center right','Lower center','Upper center','Center']
+ self.cbLegend = wx.ComboBox(self, choices=lbChoices, style=wx.CB_READONLY)
+ try:
+ i = lbChoices.index(str(data['LegendPosition']))
+ except ValueError:
+ i=1
+ self.cbLegend.SetSelection(i)
+ # Legend Font
+ lbLgdFont = wx.StaticText( self, -1, 'Legend font:')
+ self.cbLgdFont = wx.ComboBox(self, choices=fontChoices, style=wx.CB_READONLY)
+ try:
+ i = fontChoices.index(str(data['LegendFont']))
+ except ValueError:
+ i = 2
+ self.cbLgdFont.SetSelection(i)
+ # Line Width Font
+ lbLW = wx.StaticText( self, -1, 'Line width:')
+ LWChoices = ['0.5','1.0','1.25','1.5','2.0','2.5','3.0']
+ self.cbLW = wx.ComboBox(self, choices=LWChoices , style=wx.CB_READONLY)
+ try:
+ i = LWChoices.index(str(data['LineWidth']))
+ except ValueError:
+ i = 3
+ self.cbLW.SetSelection(i)
+ # Marker Size
+ lbMS = wx.StaticText( self, -1, 'Marker size:')
+ MSChoices = ['0.5','1','2','3','4','5','6','7','8']
+ self.cbMS= wx.ComboBox(self, choices=MSChoices, style=wx.CB_READONLY)
+ try:
+ i = MSChoices.index(str(data['MarkerSize']))
+ except ValueError:
+ i = 2
+ self.cbMS.SetSelection(i)
+
+ # Layout
+ #dummy_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ dummy_sizer = wx.WrapSizer(orient=wx.HORIZONTAL)
+ dummy_sizer.Add(lbFont ,0, flag = wx.CENTER|wx.LEFT,border = 1)
+ dummy_sizer.Add(self.cbFont ,0, flag = wx.CENTER|wx.LEFT,border = 1)
+ dummy_sizer.Add(lbLW ,0, flag = wx.CENTER|wx.LEFT,border = 5)
+ dummy_sizer.Add(self.cbLW ,0, flag = wx.CENTER|wx.LEFT,border = 1)
+ dummy_sizer.Add(lbMS ,0, flag = wx.CENTER|wx.LEFT,border = 5)
+ dummy_sizer.Add(self.cbMS ,0, flag = wx.CENTER|wx.LEFT,border = 1)
+ dummy_sizer.Add(lbLegend ,0, flag = wx.CENTER|wx.LEFT,border = 5)
+ dummy_sizer.Add(self.cbLegend ,0, flag = wx.CENTER|wx.LEFT,border = 1)
+ dummy_sizer.Add(lbLgdFont ,0, flag = wx.CENTER|wx.LEFT,border = 5)
+ dummy_sizer.Add(self.cbLgdFont ,0, flag = wx.CENTER|wx.LEFT,border = 1)
+ self.SetSizer(dummy_sizer)
+ self.Hide()
+ # Callbacks
+ self.Bind(wx.EVT_COMBOBOX ,self.onAnyEsthOptionChange)
+ self.cbFont.Bind(wx.EVT_COMBOBOX ,self.onFontOptionChange)
+
+ def onAnyEsthOptionChange(self,event=None):
+ self.parent.redraw_same_data()
+
+ def onFontOptionChange(self,event=None):
+ matplotlib_rc('font', **{'size':int(self.cbFont.Value) }) # affect all (including ticks)
+ self.onAnyEsthOptionChange()
+
+
+class PlotPanel(wx.Panel):
+ def __init__(self, parent, selPanel,infoPanel=None, mainframe=None):
+
+ # Superclass constructor
+ super(PlotPanel,self).__init__(parent)
+
+ # Font handling
+ font = parent.GetFont()
+ font.SetPointSize(font.GetPointSize()-1)
+ self.SetFont(font)
+ # Preparing a special font manager for chinese characters
+ self.specialFont=None
+ try:
+ pyplot_path = matplotlib.get_data_path()
+ except:
+ pyplot_path = pyplot_rc['datapath']
+ CH_F_PATHS = [
+ os.path.join(pyplot_path, 'fonts/ttf/SimHei.ttf'),
+ os.path.join(os.path.dirname(__file__),'../SimHei.ttf')]
+ for fpath in CH_F_PATHS:
+ if os.path.exists(fpath):
+ fontP = font_manager.FontProperties(fname=fpath)
+ fontP.set_size(font.GetPointSize())
+ self.specialFont=fontP
+ break
+ # data
+ self.selPanel = selPanel # <<< dependency with selPanel should be minimum
+ self.selMode = ''
+ self.infoPanel=infoPanel
+ self.infoPanel.setPlotMatrixCallbacks(self._onPlotMatrixLeftClick, self._onPlotMatrixRightClick)
+ self.parent = parent
+ self.mainframe= mainframe
+ self.plotData = []
+ self.plotDataOptions=dict()
+ try:
+ self.data = mainframe.data['plotPanel']
+ except:
+ print('>>> Using default settings for plot panel')
+ from .appdata import defaultPlotPanelData
+ self.data = defaultPlotPanelData()
+ if self.selPanel is not None:
+ bg=self.selPanel.BackgroundColour
+ self.SetBackgroundColour(bg) # sowhow, our parent has a wrong color
+ #self.SetBackgroundColour('red')
+ self.leftMeasure = GUIMeasure(1, 'firebrick')
+ self.rightMeasure = GUIMeasure(2, 'darkgreen')
+ self.xlim_prev = [[0, 1]]
+ self.ylim_prev = [[0, 1]]
+ # GUI
+ self.fig = Figure(facecolor="white", figsize=(1, 1))
+ register_matplotlib_converters()
+ self.canvas = FigureCanvas(self, -1, self.fig)
+ self.canvas.mpl_connect('motion_notify_event', self.onMouseMove)
+ self.canvas.mpl_connect('button_press_event', self.onMouseClick)
+ self.canvas.mpl_connect('button_release_event', self.onMouseRelease)
+ self.canvas.mpl_connect('draw_event', self.onDraw)
+ self.clickLocation = (None, 0, 0)
+
+ self.navTBTop = MyNavigationToolbar2Wx(self.canvas, ['Home', 'Pan'])
+ self.navTBBottom = MyNavigationToolbar2Wx(self.canvas, ['Subplots', 'Save'])
+ TBAddCheckTool(self.navTBBottom,'', icons.chart.GetBitmap(), self.onEsthToggle)
+ self.esthToggle=False
+
+ self.navTBBottom.Realize()
+
+ #self.navTB = wx.ToolBar(self, style=wx.TB_HORIZONTAL|wx.TB_HORZ_LAYOUT|wx.TB_NODIVIDER|wx.TB_FLAT)
+ #self.navTB.SetMargins(0,0)
+ #self.navTB.SetToolPacking(0)
+ #self.navTB.AddCheckTool(-1, label='', bitmap1=icons.chart.GetBitmap())
+ #self.navTB.Realize()
+
+ self.toolbar_sizer = wx.BoxSizer(wx.VERTICAL)
+ self.toolbar_sizer.Add(self.navTBTop)
+ self.toolbar_sizer.Add(self.navTBBottom)
+
+
+ # --- Tool Panel
+ self.toolSizer= wx.BoxSizer(wx.VERTICAL)
+ # --- PlotType Panel
+ self.pltTypePanel= PlotTypePanel(self);
+ # --- Plot type specific options
+ self.spcPanel = SpectralCtrlPanel(self)
+ self.pdfPanel = PDFCtrlPanel(self)
+ self.cmpPanel = CompCtrlPanel(self)
+ self.mmxPanel = MinMaxPanel(self)
+ # --- Esthetics panel
+ self.esthPanel = EstheticsPanel(self, data=self.data['plotStyle'])
+
+
+ # --- Ctrl Panel
+ self.ctrlPanel= wx.Panel(self)
+ #self.ctrlPanel.SetBackgroundColour('blue')
+ # Check Boxes
+ self.cbCurveType = wx.ComboBox(self.ctrlPanel, choices=['Plain','LS','Markers','Mix'] , style=wx.CB_READONLY)
+ self.cbCurveType.SetSelection(1)
+ self.cbSub = wx.CheckBox(self.ctrlPanel, -1, 'Subplot',(10,10))
+ self.cbLogX = wx.CheckBox(self.ctrlPanel, -1, 'Log-x',(10,10))
+ self.cbLogY = wx.CheckBox(self.ctrlPanel, -1, 'Log-y',(10,10))
+ self.cbSync = wx.CheckBox(self.ctrlPanel, -1, 'Sync-x',(10,10))
+ self.cbXHair = wx.CheckBox(self.ctrlPanel, -1, 'CrossHair',(10,10))
+ self.cbPlotMatrix = wx.CheckBox(self.ctrlPanel, -1, 'Matrix',(10,10))
+ self.cbAutoScale = wx.CheckBox(self.ctrlPanel, -1, 'AutoScale',(10,10))
+ self.cbGrid = wx.CheckBox(self.ctrlPanel, -1, 'Grid',(10,10))
+ self.cbStepPlot = wx.CheckBox(self.ctrlPanel, -1, 'StepPlot',(10,10))
+ self.cbMeasure = wx.CheckBox(self.ctrlPanel, -1, 'Measure',(10,10))
+ #self.cbSub.SetValue(True) # DEFAULT TO SUB?
+ self.cbSync.SetValue(True)
+ self.cbXHair.SetValue(self.data['CrossHair']) # Have cross hair by default
+ self.cbAutoScale.SetValue(True)
+ self.cbGrid.SetValue(self.data['Grid'])
+ # Callbacks
+ self.Bind(wx.EVT_CHECKBOX, self.redraw_event , self.cbSub )
+ self.Bind(wx.EVT_COMBOBOX, self.redraw_event , self.cbCurveType)
+ self.Bind(wx.EVT_CHECKBOX, self.log_select , self.cbLogX )
+ self.Bind(wx.EVT_CHECKBOX, self.log_select , self.cbLogY )
+ self.Bind(wx.EVT_CHECKBOX, self.redraw_event , self.cbSync )
+ self.Bind(wx.EVT_CHECKBOX, self.crosshair_event , self.cbXHair )
+ self.Bind(wx.EVT_CHECKBOX, self.plot_matrix_select, self.cbPlotMatrix )
+ self.Bind(wx.EVT_CHECKBOX, self.redraw_event , self.cbAutoScale )
+ self.Bind(wx.EVT_CHECKBOX, self.redraw_event , self.cbGrid )
+ self.Bind(wx.EVT_CHECKBOX, self.redraw_event , self.cbStepPlot )
+ self.Bind(wx.EVT_CHECKBOX, self.measure_select , self.cbMeasure )
+ self.Bind(wx.EVT_CHECKBOX, self.measure_select , self.cbMeasure )
+ # LAYOUT
+ cb_sizer = wx.FlexGridSizer(rows=4, cols=3, hgap=0, vgap=0)
+ cb_sizer.Add(self.cbCurveType , 0, flag=wx.ALL, border=1)
+ cb_sizer.Add(self.cbSub , 0, flag=wx.ALL, border=1)
+ cb_sizer.Add(self.cbAutoScale , 0, flag=wx.ALL, border=1)
+ cb_sizer.Add(self.cbLogX , 0, flag=wx.ALL, border=1)
+ cb_sizer.Add(self.cbLogY , 0, flag=wx.ALL, border=1)
+ cb_sizer.Add(self.cbStepPlot , 0, flag=wx.ALL, border=1)
+ cb_sizer.Add(self.cbXHair , 0, flag=wx.ALL, border=1)
+ cb_sizer.Add(self.cbGrid , 0, flag=wx.ALL, border=1)
+ cb_sizer.Add(self.cbSync , 0, flag=wx.ALL, border=1)
+ cb_sizer.Add(self.cbPlotMatrix, 0, flag=wx.ALL, border=1)
+ cb_sizer.Add(self.cbMeasure , 0, flag=wx.ALL, border=1)
+
+ self.ctrlPanel.SetSizer(cb_sizer)
+
+ # --- Crosshair Panel
+ crossHairPanel= wx.Panel(self)
+ self.lbCrossHairX = wx.StaticText(crossHairPanel, -1, 'x = ... ')
+ self.lbCrossHairY = wx.StaticText(crossHairPanel, -1, 'y = ... ')
+ self.lbDeltaX = wx.StaticText(crossHairPanel, -1, ' ')
+ self.lbDeltaY = wx.StaticText(crossHairPanel, -1, ' ')
+ self.lbCrossHairX.SetFont(getMonoFont(self))
+ self.lbCrossHairY.SetFont(getMonoFont(self))
+ self.lbDeltaX.SetFont(getMonoFont(self))
+ self.lbDeltaY.SetFont(getMonoFont(self))
+ cbCH = wx.FlexGridSizer(rows=4, cols=1, hgap=0, vgap=0)
+ cbCH.Add(self.lbCrossHairX , 0, flag=wx.ALL, border=1)
+ cbCH.Add(self.lbCrossHairY , 0, flag=wx.ALL, border=1)
+ cbCH.Add(self.lbDeltaX , 0, flag=wx.ALL, border=1)
+ cbCH.Add(self.lbDeltaY , 0, flag=wx.ALL, border=1)
+ crossHairPanel.SetSizer(cbCH)
+
+ # --- layout of panels
+ row_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ sl2 = wx.StaticLine(self, -1, size=wx.Size(1,-1), style=wx.LI_VERTICAL)
+ sl3 = wx.StaticLine(self, -1, size=wx.Size(1,-1), style=wx.LI_VERTICAL)
+ sl4 = wx.StaticLine(self, -1, size=wx.Size(1,-1), style=wx.LI_VERTICAL)
+ row_sizer.Add(self.pltTypePanel , 0 , flag=wx.LEFT|wx.RIGHT|wx.CENTER , border=1)
+ row_sizer.Add(sl2 , 0 , flag=wx.LEFT|wx.RIGHT|wx.EXPAND|wx.CENTER, border=0)
+ row_sizer.Add(self.toolbar_sizer, 0 , flag=wx.LEFT|wx.RIGHT|wx.CENTER , border=1)
+ row_sizer.Add(sl3 , 0 , flag=wx.LEFT|wx.RIGHT|wx.EXPAND|wx.CENTER, border=0)
+ row_sizer.Add(self.ctrlPanel , 1 , flag=wx.LEFT|wx.RIGHT|wx.EXPAND|wx.CENTER, border=0)
+ row_sizer.Add(sl4 , 0 , flag=wx.LEFT|wx.RIGHT|wx.EXPAND|wx.CENTER, border=0)
+ row_sizer.Add(crossHairPanel , 0 , flag=wx.LEFT|wx.RIGHT|wx.EXPAND|wx.CENTER, border=1)
+
+ plotsizer = wx.BoxSizer(wx.VERTICAL)
+ self.slCtrl = wx.StaticLine(self, -1, size=wx.Size(-1,1), style=wx.LI_HORIZONTAL)
+ self.slCtrl.Hide()
+ self.slEsth = wx.StaticLine(self, -1, size=wx.Size(-1,1), style=wx.LI_HORIZONTAL)
+ self.slEsth.Hide()
+ sl1 = wx.StaticLine(self, -1, size=wx.Size(-1,1), style=wx.LI_HORIZONTAL)
+ plotsizer.Add(self.toolSizer,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10)
+ plotsizer.Add(self.canvas ,1,flag = wx.EXPAND,border = 5 )
+ plotsizer.Add(sl1 ,0,flag = wx.EXPAND,border = 0)
+ plotsizer.Add(self.spcPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10)
+ plotsizer.Add(self.pdfPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10)
+ plotsizer.Add(self.cmpPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10)
+ plotsizer.Add(self.mmxPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10)
+ plotsizer.Add(self.slEsth ,0,flag = wx.EXPAND,border = 0)
+ plotsizer.Add(self.esthPanel,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10)
+ plotsizer.Add(self.slCtrl ,0,flag = wx.EXPAND,border = 0)
+ plotsizer.Add(row_sizer ,0,flag = wx.EXPAND|wx.NORTH ,border = 2)
+
+ self.show_hide(self.spcPanel, self.pltTypePanel.cbFFT.GetValue())
+ self.show_hide(self.cmpPanel, self.pltTypePanel.cbCompare.GetValue())
+ self.show_hide(self.pdfPanel, self.pltTypePanel.cbPDF.GetValue())
+ self.show_hide(self.mmxPanel, self.pltTypePanel.cbMinMax.GetValue())
+
+ self.SetSizer(plotsizer)
+ self.plotsizer=plotsizer;
+ self.set_subplot_spacing(init=True)
+
+ def onEsthToggle(self,event):
+ self.esthToggle=not self.esthToggle
+ if self.esthToggle:
+ self.slCtrl.Show()
+ self.esthPanel.Show()
+ else:
+ self.slCtrl.Hide()
+ self.esthPanel.Hide()
+ self.plotsizer.Layout()
+ event.Skip()
+
+ def set_subplot_spacing(self, init=False):
+ """
+ Handle default subplot spacing
+
+ NOTE:
+ - Tight fails when the ylabel is too long, especially for fft with multiplt signals
+ - might need to change depending on window size/resizing
+ - need to change if right axis needed
+ - this will override the user settings
+ """
+ #self.fig.set_tight_layout(True) # NOTE: works almost fine, but problem with FFT multiple
+ # TODO this is definitely not generic, but tight fails..
+ if init:
+ # NOTE: at init size is (20,20) because sizer is not initialized yet
+ bottom = 0.12
+ left = 0.12
+ else:
+ if self.Size[1]<300:
+ bottom=0.20
+ elif self.Size[1]<350:
+ bottom=0.18
+ elif self.Size[1]<430:
+ bottom=0.16
+ elif self.Size[1]<600:
+ bottom=0.13
+ elif self.Size[1]<800:
+ bottom=0.09
+ else:
+ bottom=0.07
+ if self.Size[0]<300:
+ left=0.22
+ elif self.Size[0]<450:
+ left=0.20
+ elif self.Size[0]<950:
+ left=0.12
+ else:
+ left=0.06
+ #print(self.Size,'bottom', bottom, 'left',left)
+ if self.cbPlotMatrix.GetValue(): # TODO detect it
+ self.fig.subplots_adjust(top=0.97,bottom=bottom,left=left,right=0.98-left)
+ else:
+ self.fig.subplots_adjust(top=0.97,bottom=bottom,left=left,right=0.98)
+
+ def plot_matrix_select(self, event):
+ self.infoPanel.togglePlotMatrix(self.cbPlotMatrix.GetValue())
+ self.redraw_same_data()
+
+ def measure_select(self, event):
+ if self.cbMeasure.IsChecked():
+ self.cbAutoScale.SetValue(False)
+ self.redraw_same_data()
+
+ def redraw_event(self, event):
+ self.redraw_same_data()
+
+ def log_select(self, event):
+ if self.pltTypePanel.cbPDF.GetValue():
+ self.cbLogX.SetValue(False)
+ self.cbLogY.SetValue(False)
+ else:
+ self.redraw_same_data()
+
+ def crosshair_event(self, event):
+ try:
+ self.multiCursors.vertOn =self.cbXHair.GetValue()
+ self.multiCursors.horizOn=self.cbXHair.GetValue()
+ self.multiCursors._update()
+ except:
+ pass
+
+ def show_hide(self,panel,bShow):
+ if bShow:
+ panel.Show()
+ self.slEsth.Show()
+ else:
+ self.slEsth.Hide()
+ panel.Hide()
+
+ @property
+ def sharex(self):
+ return self.cbSync.IsChecked() and (not self.pltTypePanel.cbPDF.GetValue())
+
+ def set_subplots(self,nPlots):
+ self.set_subplot_spacing()
+ # Creating subplots
+ for ax in self.fig.axes:
+ self.fig.delaxes(ax)
+ sharex=None
+ for i in range(nPlots):
+ # Vertical stack
+ if i==0:
+ ax=self.fig.add_subplot(nPlots,1,i+1)
+ if self.sharex:
+ sharex=ax
+ else:
+ ax=self.fig.add_subplot(nPlots,1,i+1,sharex=sharex)
+ # Horizontal stack
+ #self.fig.add_subplot(1,nPlots,i+1)
+
+ def onMouseMove(self, event):
+ if event.inaxes:
+ x, y = event.xdata, event.ydata
+ self.lbCrossHairX.SetLabel('x =' + self.formatLabelValue(x))
+ self.lbCrossHairY.SetLabel('y =' + self.formatLabelValue(y))
+
+ def onMouseClick(self, event):
+ self.clickLocation = (event.inaxes, event.xdata, event.ydata)
+
+ def onMouseRelease(self, event):
+ if self.cbMeasure.GetValue():
+ for ax, ax_idx in zip(self.fig.axes, range(len(self.fig.axes))):
+ if event.inaxes == ax:
+ x, y = event.xdata, event.ydata
+ if self.clickLocation != (ax, x, y):
+ # Ignore measurements for zoom-actions. Possibly add small tolerance.
+ # Zoom-actions disable autoscale
+ self.cbAutoScale.SetValue(False)
+ return
+ if event.button == 1:
+ self.infoPanel.setMeasurements((x, y), None)
+ self.leftMeasure.set(ax_idx, x, y)
+ self.leftMeasure.plot(ax, ax_idx)
+ elif event.button == 3:
+ self.infoPanel.setMeasurements(None, (x, y))
+ self.rightMeasure.set(ax_idx, x, y)
+ self.rightMeasure.plot(ax, ax_idx)
+ else:
+ return
+ if not self.cbAutoScale.IsChecked():
+ self._restore_limits()
+
+ if self.leftMeasure.axis_idx == self.rightMeasure.axis_idx and self.leftMeasure.axis_idx != -1:
+ self.lbDeltaX.SetLabel('dx=' + self.formatLabelValue(self.rightMeasure.x - self.leftMeasure.x))
+ self.lbDeltaY.SetLabel('dy=' + self.formatLabelValue(self.rightMeasure.y - self.leftMeasure.y))
+ else:
+ self.lbDeltaX.SetLabel('')
+ self.lbDeltaY.SetLabel('')
+ return
+
+ def onDraw(self, event):
+ self._store_limits()
+
+ def formatLabelValue(self, value):
+ try:
+ if abs(value)<1000 and abs(value)>1e-4:
+ s = '{:10.5f}'.format(value)
+ else:
+ s = '{:10.3e}'.format(value)
+ except TypeError:
+ s = ' '
+ return s
+
+ def removeTools(self,event=None,Layout=True):
+ try:
+ self.toolPanel.destroy() # call the "destroy" function which might clean up data
+ except:
+ pass
+ try:
+ # Python3
+ self.toolSizer.Clear(delete_windows=True) # Delete Windows
+ except:
+ # Python2
+ if hasattr(self,'toolPanel'):
+ self.toolSizer.Remove(self.toolPanel)
+ self.toolPanel.Destroy()
+ del self.toolPanel
+ self.toolSizer.Clear() # Delete Windows
+ if Layout:
+ self.plotsizer.Layout()
+
+ def showTool(self,toolName=''):
+ from .GUITools import TOOLS
+ if toolName in TOOLS.keys():
+ self.showToolPanel(TOOLS[toolName])
+ else:
+ raise Exception('Unknown tool {}'.format(toolName))
+
+ def showToolPanel(self, panelClass):
+ """ Show a tool panel based on a panel class (should inherit from GUIToolPanel)"""
+ from .GUITools import TOOLS
+ self.Freeze()
+ self.removeTools(Layout=False)
+ self.toolPanel=panelClass(parent=self) # calling the panel constructor
+ self.toolSizer.Add(self.toolPanel, 0, wx.EXPAND|wx.ALL, 5)
+ self.plotsizer.Layout()
+ self.Thaw()
+
+
+ def setPD_PDF(self,PD,c):
+ """ Convert plot data to PDF data based on GUI options"""
+ # ---PDF
+ nBins = self.pdfPanel.scBins.GetValue()
+ bSmooth = self.pdfPanel.cbSmooth.GetValue()
+ nBins_out= PD.toPDF(nBins,bSmooth)
+ if nBins_out!=nBins:
+ self.pdfPanel.scBins.SetValue(nBins)
+
+ def setPD_MinMax(self,PD):
+ """ Convert plot data to MinMax data based on GUI options"""
+ yScale=self.mmxPanel.cbyMinMax.IsChecked()
+ xScale=self.mmxPanel.cbxMinMax.IsChecked()
+ try:
+ PD.toMinMax(xScale,yScale)
+ except Exception as e:
+ self.mmxPanel.cbxMinMax.SetValue(False)
+ raise e # Used to be Warn
+
+ def setPD_FFT(self,pd):
+ """ Convert plot data to FFT data based on GUI options"""
+ yType = self.spcPanel.cbType.GetStringSelection()
+ xType = self.spcPanel.cbTypeX.GetStringSelection()
+ avgMethod = self.spcPanel.cbAveraging.GetStringSelection()
+ avgWindow = self.spcPanel.cbAveragingMethod.GetStringSelection()
+ bDetrend = self.spcPanel.cbDetrend.IsChecked()
+ nExp = self.spcPanel.scP2.GetValue()
+ nPerDecade = self.spcPanel.scP2.GetValue()
+ # Convert plotdata to FFT data
+ try:
+ Info = pd.toFFT(yType=yType, xType=xType, avgMethod=avgMethod, avgWindow=avgWindow, bDetrend=bDetrend, nExp=nExp, nPerDecade=nPerDecade)
+ # Trigger
+ if hasattr(Info,'nExp') and Info.nExp!=nExp:
+ self.spcPanel.scP2.SetValue(Info.nExp)
+ self.spcPanel.updateP2(Info.nExp)
+ except Exception as e:
+ self.spcPanel.Hide();
+ self.plotsizer.Layout()
+ raise e
+
+
+ def transformPlotData(self,PD):
+ """"
+ Apply MinMax, PDF or FFT transform to plot based on GUI data
+ """
+ plotType=self.pltTypePanel.plotType()
+ if plotType=='MinMax':
+ self.setPD_MinMax(PD)
+ elif plotType=='PDF':
+ self.setPD_PDF(PD,PD.c)
+ elif plotType=='FFT':
+ self.setPD_FFT(PD)
+
+ def getPlotData(self,plotType):
+ ID,SameCol,selMode=self.selPanel.getPlotDataSelection()
+ self.selMode=selMode # we store the selection mode
+ del self.plotData
+ self.plotData=[]
+ tabs=self.selPanel.tabList.getTabs() # TODO, selPanel should just return the PlotData...
+ try:
+ for i,idx in enumerate(ID):
+ # Initialize each plotdata based on selected table and selected id channels
+ pd=PlotData();
+ pd.fromIDs(tabs,i,idx,SameCol, self.plotDataOptions)
+ # Possible change of data
+ if plotType=='MinMax':
+ self.setPD_MinMax(pd)
+ elif plotType=='PDF':
+ self.setPD_PDF(pd,pd.c)
+ elif plotType=='FFT':
+ self.setPD_FFT(pd)
+ self.plotData.append(pd)
+ except Exception as e:
+ self.plotData=[]
+ raise e
+
+ def PD_Compare(self,mode):
+ """ Perform comparison of the selected PlotData, returns new plotData with the comparison. """
+ sComp = self.cmpPanel.rbType.GetStringSelection()
+ try:
+ self.plotData = compareMultiplePD(self.plotData,mode, sComp)
+ except Exception as e:
+ self.pltTypePanel.cbRegular.SetValue(True)
+ raise e
+
+ def _onPlotMatrixLeftClick(self, event):
+ """Toggle plot-states from None, to left-axis, to right-axis.
+ Left-click goes forwards, right-click goes backwards.
+ IndexError to avoid "holes" in matrix with outer adjacent populated entries
+ """
+ btn = event.GetEventObject()
+ label = btn.GetLabelText()
+ if label == '-':
+ btn.SetLabel('1')
+ try:
+ self.infoPanel.getPlotMatrix(self.plotData, self.cbSub.IsChecked())
+ except IndexError:
+ btn.SetLabel('-')
+ elif label == '1':
+ btn.SetLabel('2')
+ else:
+ btn.SetLabel('-')
+ try:
+ self.infoPanel.getPlotMatrix(self.plotData, self.cbSub.IsChecked())
+ except IndexError:
+ btn.SetLabel('1')
+ self.redraw_same_data()
+
+ def _onPlotMatrixRightClick(self, event):
+ btn = event.GetEventObject()
+ label = btn.GetLabelText()
+ if label == '-':
+ btn.SetLabel('2')
+ try:
+ self.infoPanel.getPlotMatrix(self.plotData, self.cbSub.IsChecked())
+ except IndexError:
+ btn.SetLabel('-')
+ elif label == '1':
+ btn.SetLabel('-')
+ try:
+ self.infoPanel.getPlotMatrix(self.plotData, self.cbSub.IsChecked())
+ except IndexError:
+ btn.SetLabel('2')
+ else:
+ btn.SetLabel('1')
+ self.redraw_same_data()
+
+ def set_axes_lim(self, PDs, axis):
+ """
+ It's usually faster to set the axis limits first (before plotting)
+ and disable autoscaling. This way the limits are not recomputed when plot data are added.
+ Also, we already have computed the min and max, so we leverage that.
+ NOTE:
+ doesnt not work with strings
+ doesnt not work for FFT and compare
+
+ INPUTS:
+ PDs: list of plot data
+ """
+ # TODO option for tight axes
+ tight=False
+
+ plotType=self.pltTypePanel.plotType()
+ if plotType in ['FFT','Compare']:
+ axis.autoscale(True, axis='both', tight=tight)
+ return
+ vXString=[PDs[i].xIsString for i in axis.iPD]
+ vYString=[PDs[i].yIsString for i in axis.iPD]
+ if not any(vXString) and not self.cbLogX.IsChecked():
+ try:
+ xMin=np.min([PDs[i]._xMin[0] for i in axis.iPD])
+ xMax=np.max([PDs[i]._xMax[0] for i in axis.iPD])
+ if np.isclose(xMin,xMax):
+ delta=1 if np.isclose(xMax,0) else 0.1*xMax
+ else:
+ if tight:
+ delta=0
+ else:
+ delta = (xMax-xMin)*pyplot_rc['axes.xmargin']
+ axis.set_xlim(xMin-delta,xMax+delta)
+ axis.autoscale(False, axis='x', tight=False)
+ except:
+ pass
+ if not any(vYString) and not self.cbLogY.IsChecked():
+ try:
+ yMin=np.min([PDs[i]._yMin[0] for i in axis.iPD])
+ yMax=np.max([PDs[i]._yMax[0] for i in axis.iPD])
+ delta = (yMax-yMin)*pyplot_rc['axes.ymargin']
+ if np.isclose(yMin,yMax):
+ delta=1 if np.isclose(yMax,0) else 0.1*yMax
+ else:
+ if tight:
+ delta=0
+ else:
+ delta = (yMax-yMin)*pyplot_rc['axes.xmargin']
+ axis.set_ylim(yMin-delta,yMax+delta)
+ axis.autoscale(False, axis='y', tight=False)
+ except:
+ pass
+
+ def plot_all(self, keep_limits=True):
+ self.multiCursors=[]
+
+ if self.cbMeasure.GetValue() is False:
+ for measure in [self.leftMeasure, self.rightMeasure]:
+ measure.clear()
+ self.infoPanel.setMeasurements(None, None)
+ self.lbDeltaX.SetLabel('')
+ self.lbDeltaY.SetLabel('')
+
+ axes=self.fig.axes
+ PD=self.plotData
+
+
+ # --- Plot options
+ bStep = self.cbStepPlot.IsChecked()
+ plot_options = dict()
+ plot_options['lw']=float(self.esthPanel.cbLW.Value)
+ plot_options['ms']=float(self.esthPanel.cbMS.Value)
+ if self.cbCurveType.Value=='Plain':
+ plot_options['LineStyles'] = ['-']
+ plot_options['Markers'] = ['']
+ elif self.cbCurveType.Value=='LS':
+ plot_options['LineStyles'] = ['-','--','-.',':']
+ plot_options['Markers'] = ['']
+ elif self.cbCurveType.Value=='Markers':
+ plot_options['LineStyles'] = ['']
+ plot_options['Markers'] = ['o','d','v','^','s']
+ elif self.cbCurveType.Value=='Mix': # NOTE, can be improved
+ plot_options['LineStyles'] = ['-','--', '-','-','-']
+ plot_options['Markers'] = ['' ,'' ,'o','^','s']
+ else:
+ # Combination of linestyles markers, colors, etc.
+ # But at that stage, if the user really want this, then we can implement an option to set styles per plot. Not high priority.
+ raise Exception('Not implemented')
+
+
+
+ # --- Font options
+ font_options = dict()
+ font_options_legd = dict()
+ font_options['size'] = int(self.esthPanel.cbFont.Value) # affect labels
+ font_options_legd['fontsize'] = int(self.esthPanel.cbLgdFont.Value)
+ needChineseFont = any([pd.needChineseFont for pd in PD])
+ if needChineseFont and self.specialFont is not None:
+ font_options['fontproperties']= self.specialFont
+ font_options_legd['prop'] = self.specialFont
+
+ # --- Loop on axes. Either use ax.iPD to chose the plot data, or rely on plotmatrix
+ for axis_idx, ax_left in enumerate(axes):
+ ax_right = None
+ # Checks
+ vDate=[PD[i].yIsDate for i in ax_left.iPD]
+ if any(vDate) and len(vDate)>1:
+ Error(self,'Cannot plot date and other value on the same axis')
+ return
+
+ # Set limit before plot when possible, for optimization
+ self.set_axes_lim(PD, ax_left)
+
+ # Actually plot
+ pm = self.infoPanel.getPlotMatrix(PD, self.cbSub.IsChecked())
+ __, bAllNegLeft = self.plotSignals(ax_left, axis_idx, PD, pm, 1, bStep, plot_options)
+ ax_right, bAllNegRight = self.plotSignals(ax_left, axis_idx, PD, pm, 2, bStep, plot_options)
+
+ self.infoPanel.setMeasurements(self.leftMeasure.get_xydata(), self.rightMeasure.get_xydata())
+ for measure in [self.leftMeasure, self.rightMeasure]:
+ measure.plot(ax_left, axis_idx)
+
+ # Log Axes
+ if self.cbLogX.IsChecked():
+ try:
+ ax_left.set_xscale("log", nonpositive='clip') # latest
+ except:
+ ax_left.set_xscale("log", nonposx='clip') # legacy
+
+ if self.cbLogY.IsChecked():
+ if bAllNegLeft is False:
+ try:
+ ax_left.set_yscale("log", nonpositive='clip') # latest
+ except:
+ ax_left.set_yscale("log", nonposy='clip')
+ if bAllNegRight is False and ax_right is not None:
+ try:
+ ax_right.set_yscale("log", nonpositive='clip') # latest
+ except:
+ ax_left.set_yscale("log", nonposy='clip') # legacy
+
+ # XLIM - TODO FFT ONLY NASTY
+ if self.pltTypePanel.cbFFT.GetValue():
+ try:
+ if self.cbAutoScale.IsChecked():
+ xlim=float(self.spcPanel.tMaxFreq.GetLineText(0))
+ if xlim>0:
+ ax_left.set_xlim([0,xlim])
+ pd=PD[ax_left.iPD[0]]
+ I=pd.x 0 and len(yleft_labels) <= 3:
+ ax_left.set_ylabel(' and '.join(yleft_labels), **font_options)
+ elif ax_left is not None:
+ ax_left.set_ylabel('')
+ if len(yright_labels) > 0 and len(yright_labels) <= 3:
+ ax_right.set_ylabel(' and '.join(yright_labels), **font_options)
+ elif ax_right is not None:
+ ax_right.set_ylabel('')
+
+ # Legends
+ lgdLoc = self.esthPanel.cbLegend.Value.lower()
+ if (self.pltTypePanel.cbCompare.GetValue() or
+ ((len(yleft_legends) + len(yright_legends)) > 1)):
+ if lgdLoc !='none':
+ if len(yleft_legends) > 0:
+ ax_left.legend(fancybox=False, loc=lgdLoc, **font_options_legd)
+ if ax_right is not None and len(yright_legends) > 0:
+ ax_right.legend(fancybox=False, loc=4, **font_options_legd)
+ elif len(axes)>1 and len(axes)==len(PD):
+ # TODO: can this be removed? If there is only one unique signal
+ # per subplot, normally only ylabel is displayed and no legend.
+ # Special case when we have subplots and all plots have the same label
+ if lgdLoc !='none':
+ usy = unique([pd.sy for pd in PD])
+ if len(usy)==1:
+ for ax in axes:
+ ax.legend(fancybox=False, loc=lgdLoc, **font_options_legd)
+
+ axes[-1].set_xlabel(PD[axes[-1].iPD[0]].sx, **font_options)
+
+ #print('sy :',[pd.sy for pd in PD])
+ #print('syl:',[pd.syl for pd in PD])
+
+ # --- Cursors for each individual plot
+ # NOTE: cursors needs to be stored in the object!
+ #for ax_left in self.fig.axes:
+ # self.cursors.append(MyCursor(ax_left,horizOn=True, vertOn=False, useblit=True, color='gray', linewidth=0.5, linestyle=':'))
+ # Vertical cusor for all, commonly
+ bXHair = self.cbXHair.GetValue()
+ self.multiCursors = MyMultiCursor(self.canvas, tuple(self.fig.axes), useblit=True, horizOn=bXHair, vertOn=bXHair, color='gray', linewidth=0.5, linestyle=':')
+
+ def plotSignals(self, ax, axis_idx, PD, pm, left_right, is_step, opts):
+ axis = None
+ bAllNeg = True
+ if pm is None:
+ loop_range = ax.iPD
+ else:
+ loop_range = range(len(PD))
+
+ iPlot=-1
+ for signal_idx in loop_range:
+ do_plot = False
+ if left_right == 1 and (pm is None or pm[signal_idx][axis_idx] == left_right):
+ do_plot = True
+ axis = ax
+ elif left_right == 2 and pm is not None and pm[signal_idx][axis_idx] == left_right:
+ do_plot = True
+ if axis is None:
+ axis = ax.twinx()
+ ax.set_zorder(axis.get_zorder()+1)
+ ax.patch.set_visible(False)
+ axis._get_lines.prop_cycler = ax._get_lines.prop_cycler
+ pd=PD[signal_idx]
+ if do_plot:
+ iPlot+=1
+ # --- styling per plot
+ if len(pd.x)==1:
+ marker='o'; ls=''
+ else:
+ # TODO allow PlotData to override for "per plot" options in the future
+ marker = opts['Markers'][np.mod(iPlot,len(opts['Markers']))]
+ ls = opts['LineStyles'][np.mod(iPlot,len(opts['LineStyles']))]
+ if is_step:
+ plot = axis.step
+ else:
+ plot = axis.plot
+ plot(pd.x,pd.y,label=pd.syl,ms=opts['ms'], lw=opts['lw'], marker=marker, ls=ls)
+ try:
+ bAllNeg = bAllNeg and all(pd.y<=0)
+ except:
+ pass # Dates or strings
+ return axis, bAllNeg
+
+ def findPlotMode(self,PD):
+ uTabs = unique([pd.it for pd in PD])
+ usy = unique([pd.sy for pd in PD])
+ uiy = unique([pd.iy for pd in PD])
+ if len(uTabs)<=0:
+ raise Exception('No Table. Contact developer')
+ if len(uTabs)==1:
+ mode='1Tab_nCols'
+ else:
+ if PD[0].SameCol:
+ mode='nTabs_SameCols'
+ else:
+ # Now that we allow multiple selections detecting "simColumns" is more difficult
+ if len(uTabs) == len(PD):
+ mode='nTabs_1Col'
+ elif self.selMode=='simColumnsMode':
+ mode='nTabs_SimCols'
+ else:
+ mode='nTabs_mCols'
+ return mode
+
+ def findSubPlots(self,PD,mode):
+ uTabs = unique([pd.it for pd in PD])
+ usy = unique([pd.sy for pd in PD])
+ bSubPlots = self.cbSub.IsChecked()
+ bCompare = self.pltTypePanel.cbCompare.GetValue() # NOTE bCompare somehow always 1Tab_nCols
+ nSubPlots=1
+ spreadBy='none'
+ self.infoPanel.setTabMode(mode)
+ if mode=='1Tab_nCols':
+ if bSubPlots:
+ if bCompare or len(uTabs)==1:
+ nSubPlots = self.infoPanel.getNumberOfSubplots(PD, bSubPlots)
+ else:
+ nSubPlots=len(usy)
+ spreadBy='iy'
+ elif mode=='nTabs_SameCols':
+ if bSubPlots:
+ if bCompare:
+ print('>>>TODO ',mode,len(usy),len(uTabs))
+ else:
+ if len(usy)==1:
+ # Temporary hack until we have an option for spread by tabs or col
+ nSubPlots=len(uTabs)
+ spreadBy='it'
+ else:
+ nSubPlots=len(usy)
+ spreadBy='iy'
+ elif mode=='nTabs_SimCols':
+ if bSubPlots:
+ if bCompare:
+ print('>>>TODO ',mode,len(usy),len(uTabs))
+ else:
+ nSubPlots=int(len(PD)/len(uTabs))
+ spreadBy='mod-ip'
+ elif mode=='nTabs_mCols':
+ if bSubPlots:
+ if bCompare:
+ print('>>>TODO ',mode,len(usy),len(uTabs))
+ else:
+ if bCompare or len(uTabs)==1:
+ nSubPlots = self.infoPanel.getNumberOfSubplots(PD, bSubPlots)
+ else:
+ nSubPlots=len(PD)
+ spreadBy='mod-ip'
+ elif mode=='nTabs_1Col':
+ if bSubPlots:
+ if bCompare:
+ print('>>> TODO',mode,len(uTabs))
+ else:
+ nSubPlots=len(uTabs)
+ spreadBy='it'
+ else:
+ raise Exception('Unknown mode, contact developer.')
+ return nSubPlots,spreadBy
+
+ def distributePlots(self,mode,nSubPlots,spreadBy):
+ """ Assigns plot data to axes and axes to plot data """
+ axes=self.fig.axes
+
+ # Link plot data to axes
+ if nSubPlots==1 or spreadBy=='none':
+ axes[0].iPD=[i for i in range(len(self.plotData))]
+ else:
+ for ax in axes:
+ ax.iPD=[]
+ PD=self.plotData
+ uTabs=unique([pd.it for pd in PD])
+ uiy=unique([pd.iy for pd in PD])
+ if spreadBy=='iy':
+ for ipd,pd in enumerate(PD):
+ i=uiy.index(pd.iy)
+ if i < len(axes):
+ axes[i].iPD.append(ipd)
+ elif spreadBy=='it':
+ for ipd,pd in enumerate(PD):
+ i=uTabs.index(pd.it)
+ axes[i].iPD.append(ipd)
+ elif spreadBy=='mod-ip':
+ for ipd,pd in enumerate(PD):
+ i=np.mod(ipd, nSubPlots)
+ axes[i].iPD.append(ipd)
+ else:
+ raise Exception('Wrong spreadby value')
+
+ def setLegendLabels(self,mode):
+ """ Set labels for legend """
+ if mode=='1Tab_nCols':
+ for pd in self.plotData:
+ if self.pltTypePanel.cbMinMax.GetValue():
+ pd.syl = no_unit(pd.sy)
+ else:
+ pd.syl = pd.sy
+
+ elif mode=='nTabs_SameCols':
+ for pd in self.plotData:
+ pd.syl=pd.st
+
+ elif mode=='nTabs_1Col':
+ usy=unique([pd.sy for pd in self.plotData])
+ if len(usy)==1:
+ for pd in self.plotData:
+ pd.syl=pd.st
+ else:
+ for pd in self.plotData:
+ if self.pltTypePanel.cbMinMax.GetValue():
+ pd.syl=no_unit(pd.sy)
+ else:
+ pd.syl=pd.sy #pd.syl=pd.st + ' - '+pd.sy
+ elif mode=='nTabs_SimCols':
+ bSubPlots = self.cbSub.IsChecked()
+ if bSubPlots: # spread by table name
+ for pd in self.plotData:
+ pd.syl=pd.st
+ else:
+ for pd in self.plotData:
+ pd.syl=pd.st + ' - '+pd.sy
+ elif mode=='nTabs_mCols':
+ usy=unique([pd.sy for pd in self.plotData])
+ bSubPlots = self.cbSub.IsChecked()
+ if bSubPlots and len(usy)==1: # spread by table name
+ for pd in self.plotData:
+ pd.syl=pd.st
+ else:
+ for pd in self.plotData:
+ pd.syl=pd.st + ' - '+pd.sy
+ else:
+ raise Exception('Unknown mode {}'.format(mode))
+
+
+ def empty(self):
+ self.cleanPlot()
+
+ def clean_memory(self):
+ if hasattr(self,'plotData'):
+ del self.plotData
+ self.plotData=[]
+ for ax in self.fig.axes:
+ ax.iPD=[]
+ self.fig.delaxes(ax)
+ gc.collect()
+
+ def clean_memory_plot(self):
+ pass
+
+ def cleanPlot(self):
+ for ax in self.fig.axes:
+ if hasattr(ax,'iPD'):
+ del ax.iPD
+ self.fig.delaxes(ax)
+ gc.collect()
+ self.fig.add_subplot(111)
+ ax = self.fig.axes[0]
+ ax.set_axis_off()
+ #ax.plot(1,1)
+ self.canvas.draw()
+ gc.collect()
+
+ def load_and_draw(self):
+ """ Full draw event:
+ - Get plot data based on selection
+ - Plot them
+ - Trigger changes to infoPanel
+
+ """
+ self.clean_memory()
+ self.getPlotData(self.pltTypePanel.plotType())
+ if len(self.plotData)==0:
+ self.cleanPlot();
+ return
+ mode=self.findPlotMode(self.plotData)
+ if self.pltTypePanel.cbCompare.GetValue():
+ self.PD_Compare(mode)
+ if len(self.plotData)==0:
+ self.cleanPlot();
+ return
+ self.redraw_same_data()
+ if self.infoPanel is not None:
+ self.infoPanel.showStats(self.plotData,self.pltTypePanel.plotType())
+
+ def redraw_same_data(self, keep_limits=True):
+ if len(self.plotData)==0:
+ self.cleanPlot();
+ return
+ elif len(self.plotData) == 1:
+ if self.plotData[0].xIsString or self.plotData[0].yIsString or self.plotData[0].xIsDate or self.plotData[0].yIsDate:
+ self.cbAutoScale.SetValue(True)
+ else:
+ if len(self.xlim_prev)==0: # Might occur if some date didn't plot before (e.g. strings)
+ self.cbAutoScale.SetValue(True)
+ elif rectangleOverlap(self.plotData[0]._xMin[0], self.plotData[0]._yMin[0],
+ self.plotData[0]._xMax[0], self.plotData[0]._yMax[0],
+ self.xlim_prev[0][0], self.ylim_prev[0][0],
+ self.xlim_prev[0][1], self.ylim_prev[0][1]):
+ pass
+ else:
+ self.cbAutoScale.SetValue(True)
+
+ mode=self.findPlotMode(self.plotData)
+ nPlots,spreadBy=self.findSubPlots(self.plotData,mode)
+
+ self.clean_memory_plot()
+ self.set_subplots(nPlots)
+ self.distributePlots(mode,nPlots,spreadBy)
+
+ if not self.pltTypePanel.cbCompare.GetValue():
+ self.setLegendLabels(mode)
+
+ self.plot_all(keep_limits)
+ self.canvas.draw()
+
+
+ def _store_limits(self):
+ self.xlim_prev = []
+ self.ylim_prev = []
+ for ax in self.fig.axes:
+ self.xlim_prev.append(ax.get_xlim())
+ self.ylim_prev.append(ax.get_ylim())
+
+ def _restore_limits(self):
+ for ax, xlim, ylim in zip(self.fig.axes, self.xlim_prev, self.ylim_prev):
+ ax.set_xlim(xlim)
+ ax.set_ylim(ylim)
+
+
+if __name__ == '__main__':
+ import pandas as pd;
+ from Tables import Table,TableList
+
+ app = wx.App(False)
+ self=wx.Frame(None,-1,"Title")
+ self.SetSize((800, 600))
+ #self.SetBackgroundColour('red')
+ class FakeSelPanel(wx.Panel):
+ def __init__(self, parent):
+ super(FakeSelPanel,self).__init__(parent)
+ d ={'ColA': np.linspace(0,1,100)+1,'ColB': np.random.normal(0,1,100)+0,'ColC':np.random.normal(0,1,100)+1}
+ df = pd.DataFrame(data=d)
+ self.tabList=TableList([Table(data=df)])
+
+ def getPlotDataSelection(self):
+ ID=[]
+ ID.append([0,0,2,'x','ColB','tab'])
+ ID.append([0,0,3,'x','ColC','tab'])
+ return ID,True
+
+ selpanel=FakeSelPanel(self)
+ # selpanel.SetBackgroundColour('blue')
+ p1=PlotPanel(self,selpanel)
+ p1.load_and_draw()
+ #p1=SpectralCtrlPanel(self)
+ sizer = wx.BoxSizer(wx.VERTICAL)
+ sizer.Add(selpanel,0, flag = wx.EXPAND|wx.ALL,border = 10)
+ sizer.Add(p1,1, flag = wx.EXPAND|wx.ALL,border = 10)
+ self.SetSizer(sizer)
+
+ self.Center()
+ self.Layout()
+ self.SetSize((800, 600))
+ self.Show()
+ self.SendSizeEvent()
+
+ #p1.showStats(None,[tab],[0],[0,1],tab.columns,0,erase=False)
+
+ app.MainLoop()
+
+
diff --git a/pydatview/GUISelectionPanel.py b/pydatview/GUISelectionPanel.py
index d7177aa..a623bee 100644
--- a/pydatview/GUISelectionPanel.py
+++ b/pydatview/GUISelectionPanel.py
@@ -1,1324 +1,1324 @@
-import wx
-import platform
-try:
- from .common import *
- from .GUICommon import *
- from .GUIMultiSplit import MultiSplit
- from .GUIToolBox import GetKeyString
-except:
- raise
-# from common import *
-# from GUICommon import *
-# from GUIMultiSplit import MultiSplit
-
-
-__all__ = ['ColumnPanel', 'TablePanel', 'SelectionPanel','SEL_MODES','SEL_MODES_ID','TablePopup','ColumnPopup']
-
-SEL_MODES = ['auto','Same tables' ,'Sim. tables' ,'2 tables','3 tables (exp.)' ]
-SEL_MODES_ID = ['auto','sameColumnsMode','simColumnsMode','twoColumnsMode' ,'threeColumnsMode' ]
-
-def ireplace(text, old, new):
- """ Replace case insensitive """
- try:
- index_l = text.lower().index(old.lower())
- return text[:index_l] + new + text[index_l + len(old):]
- except:
- return text
-
-
-# --------------------------------------------------------------------------------}
-# --- Formula diagog
-# --------------------------------------------------------------------------------{
-class FormulaDialog(wx.Dialog):
- def __init__(self, title='', name='', formula='',columns=[],unit='',xcol='',xunit=''):
- wx.Dialog.__init__(self, None, title=title)
- # --- Data
- self.unit=unit.strip().replace(' ','')
- self.columns=['{'+c+'}' for c in columns]
- self.xcol='{'+xcol+'}'
- self.xunit=xunit.strip().replace(' ','')
- if len(formula)==0:
- formula=' + '.join(self.columns)
- if len(name)==0:
- name=self.getDefaultName()
- self.formula_in=formula
-
-
- quick_lbl = wx.StaticText(self, label="Predefined: " )
- self.cbQuick = wx.ComboBox(self, choices=['None','x 1000','/ 1000','deg2rad','rad2deg','rpm2radps','radps2rpm','norm','squared','d/dx'], style=wx.CB_READONLY)
- self.cbQuick.SetSelection(0)
- self.cbQuick.Bind(wx.EVT_COMBOBOX ,self.onQuickFormula)
-
- # Formula info
- formula_lbl = wx.StaticText(self, label="Formula: ")
- self.formula = wx.TextCtrl(self)
- #self.formula.SetFont(getMonoFont(self))
-
- self.formula.SetValue(formula)
- formula_sizer = wx.BoxSizer(wx.HORIZONTAL)
- formula_sizer.Add(formula_lbl ,0,wx.ALL|wx.RIGHT|wx.CENTER,5)
- formula_sizer.Add(self.formula,1,wx.ALL|wx.EXPAND|wx.CENTER,5)
- formula_sizer.Add(quick_lbl ,0,wx.ALL|wx.CENTER,5)
- formula_sizer.Add(self.cbQuick,0,wx.ALL|wx.CENTER,5)
-
-
- # name info
- name_lbl = wx.StaticText(self, label="New name: " )
- self.name = wx.TextCtrl(self, size=wx.Size(200,-1))
- self.name.SetValue(name)
- #self.name.SetFont(getMonoFont(self))
- name_sizer = wx.BoxSizer(wx.HORIZONTAL)
- name_sizer.Add(name_lbl ,0,wx.ALL|wx.RIGHT|wx.CENTER,5)
- name_sizer.Add(self.name,0,wx.ALL|wx.CENTER,5)
-
- info ='The formula needs to have a valid python syntax for an array manipulation. The available arrays are \n'
- info+='the columns of the current table. The column names (without units) are surrounded by curly brackets.\n'
- info+='You have access to numpy using `np`.\n\n'
- info+='For instance, if you have two columns called `ColA [m]` and `ColB [m]` you can use:\n'
- info+=' - ` {ColA} + {ColB} `\n'
- info+=' - ` np.sqrt( {ColA}**2/1000 + 1/{ColB}**2 ) `\n'
- info+=' - ` np.sin ( {ColA}*2*np.pi + {ColB} ) `\n'
- help_lbl = wx.StaticText(self, label='Help: ')
- info_lbl = wx.StaticText(self, label=info)
- help_sizer = wx.BoxSizer(wx.HORIZONTAL)
- help_sizer.Add(help_lbl ,0,wx.ALL|wx.RIGHT|wx.TOP,5)
- help_sizer.Add(info_lbl ,0,wx.ALL|wx.TOP,5)
-
-
-
- self.btOK = wx.Button(self, wx.ID_OK)#, label = "OK" )
- btCL = wx.Button(self,label = "Cancel")
- bt_sizer = wx.BoxSizer(wx.HORIZONTAL)
- bt_sizer.Add(self.btOK, 0 ,wx.ALL,5)
- bt_sizer.Add(btCL, 0 ,wx.ALL,5)
- #btOK.Bind(wx.EVT_BUTTON,self.onOK )
- btCL.Bind(wx.EVT_BUTTON,self.onCancel)
-
-
- main_sizer = wx.BoxSizer(wx.VERTICAL)
- #main_sizer.Add(quick_sizer ,0,wx.ALL|wx.EXPAND,5)
- main_sizer.Add(formula_sizer,0,wx.ALL|wx.EXPAND,5)
- main_sizer.Add(name_sizer ,0,wx.ALL|wx.EXPAND,5)
- main_sizer.Add(help_sizer ,0 ,wx.ALL|wx.CENTER, 5)
- main_sizer.Add(bt_sizer ,0, wx.ALL|wx.CENTER, 5)
- self.SetSizer(main_sizer)
- self.Fit()
-
- def stripBrackets(self,s):
- return s.replace('{','').replace('}','')
-
- def getOneColName(self):
- if len(self.columns)>0:
- return self.columns[-1]
- else:
- return ''
-
- def get_unit(self):
- if len(self.unit)>0:
- return ' ['+self.unit+']'
- else:
- return ''
- def get_squared_unit(self):
- if len(self.unit)>0:
- if self.unit[0].lower()=='-':
- return ' [-]'
- else:
- return ' [('+self.unit+')^2]'
- else:
- return ''
- def get_kilo_unit(self):
- if len(self.unit)>0:
- if len(self.unit)>=1:
- if self.unit[0].lower()=='-':
- return ' [-]'
- elif self.unit[0].lower()=='G':
- r='T'
- elif self.unit[0].lower()=='M':
- r='G'
- elif self.unit[0]=='k':
- r='M'
- elif self.unit[0]=='m':
- if len(self.unit)==1:
- r='km'
- elif self.unit[1]=='/':
- r='km'
- else:
- r=''
- else:
- r='k'+self.unit[0]
- return ' ['+r+self.unit[1:]+']'
- else:
- return ' [k'+self.unit+']'
- else:
- return ''
- def get_milli_unit(self):
- if len(self.unit)>=1:
- if self.unit[0].lower()=='-':
- return ' [-]'
- elif self.unit[0].lower()=='T':
- r='G'
- elif self.unit[0]=='G':
- r='M'
- elif self.unit[0]=='M':
- r='k'
- elif self.unit[0].lower()=='k':
- r=''
- elif self.unit[0]=='m':
- if len(self.unit)==1:
- r='mm'
- elif self.unit[1]=='/':
- r='mm'
- else:
- r='mu'
- else:
- r='m'+self.unit[0]
-
- return ' ['+r+self.unit[1:]+']'
- else:
- return ''
- def get_deriv_unit(self):
- if self.unit==self.xunit:
- return ' [-]'
- else:
- return ' ['+self.unit+'/'+self.xunit+']'
-
- def getDefaultName(self):
- if len(self.columns)>0:
- return self.stripBrackets(self.getOneColName())+' New'+self.get_unit()
- else:
- return ''
-
- def onQuickFormula(self, event):
- i = self.cbQuick.GetSelection()
- s = self.cbQuick.GetStringSelection()
- if s=='None':
- self.formula.SetValue(self.formula_in)
- return
-
- #self.formula_in=self.formula.GetValue()
- c1 = self.getOneColName()
- n1 = self.stripBrackets(c1)
-
- if s=='x 1000':
- self.formula.SetValue(c1+' * 1000')
- self.name.SetValue(n1+'_x1000'+ self.get_milli_unit())
- elif s=='/ 1000':
- self.formula.SetValue(c1+' / 1000')
- self.name.SetValue(n1+'_/1000'+self.get_kilo_unit())
- elif s=='deg2rad':
- self.formula.SetValue(c1+' *np.pi/180')
- self.name.SetValue(n1+'_rad [rad]')
- elif s=='rad2deg':
- self.formula.SetValue(c1+' *180/np.pi')
- self.name.SetValue(n1+'_deg [deg]')
- elif s=='rpm2radps':
- self.formula.SetValue(c1+' *2*np.pi/60')
- self.name.SetValue(n1+'_radps [rad/s]')
- elif s=='radps2rpm':
- self.formula.SetValue(c1+' *60/(2*np.pi)')
- self.name.SetValue(n1+'_rpm [rpm]')
- elif s=='norm':
- self.formula.SetValue('np.sqrt( '+'**2 + '.join(self.columns)+'**2 )')
- self.name.SetValue(n1+'_norm'+self.get_unit())
- elif s=='squared':
- self.formula.SetValue('**2 + '.join(self.columns)+'**2 ')
- self.name.SetValue(n1+'^2'+self.get_squared_unit())
- elif s=='d/dx':
- self.formula.SetValue('np.gradient( '+'+'.join(self.columns)+ ', '+self.xcol+' )')
- nx = self.stripBrackets(self.xcol)
- bDoNewName=True
- if self.xunit=='s':
- if n1.lower().find('speed')>=0:
- n1=ireplace(n1,'speed','Acceleration')
- bDoNewName=False
- elif n1.lower().find('velocity')>=0:
- n1=ireplace(n1,'velocity','Acceleration')
- bDoNewName=False
- elif n1.lower().find('vel')>=0:
- n1=ireplace(n1,'vel','Acc')
- bDoNewName=False
- elif n1.lower().find('position')>=0:
- n1=ireplace(n1,'position','speed')
- bDoNewName=False
- elif n1.lower().find('pos')>=0:
- n1=ireplace(n1,'pos','Vel')
- bDoNewName=False
- else:
- n1='d('+n1+')/dt'
- else:
- n1='d('+n1+')/d('+nx+')'
- self.name.SetValue(n1+self.get_deriv_unit())
- else:
- raise Exception('Unknown quick formula {}'.s)
-
- def onCancel(self, event):
- self.Destroy()
-# --------------------------------------------------------------------------------}
-# --- Popup menus
-# --------------------------------------------------------------------------------{
-class TablePopup(wx.Menu):
- def __init__(self, mainframe, parent, fullmenu=False):
- wx.Menu.__init__(self)
- self.parent = parent # parent is listbox
- self.mainframe = mainframe
- self.ISel = self.parent.GetSelections()
-
- if fullmenu:
- self.itNameFile = wx.MenuItem(self, -1, "Naming: by file names", kind=wx.ITEM_CHECK)
- self.MyAppend(self.itNameFile)
- self.Bind(wx.EVT_MENU, self.OnNaming, self.itNameFile)
- self.Check(self.itNameFile.GetId(), self.parent.GetParent().tabList.Naming=='FileNames') # Checking the menu box
-
- item = wx.MenuItem(self, -1, "Sort by name")
- self.MyAppend(item)
- self.Bind(wx.EVT_MENU, self.OnSort, item)
-
- item = wx.MenuItem(self, -1, "Add")
- self.MyAppend(item)
- self.Bind(wx.EVT_MENU, self.mainframe.onAdd, item)
-
- if len(self.ISel)>0:
- item = wx.MenuItem(self, -1, "Delete")
- self.MyAppend(item)
- self.Bind(wx.EVT_MENU, self.OnDeleteTabs, item)
-
- if len(self.ISel)==1:
- tabPanel=self.parent.GetParent()
- if tabPanel.tabList.Naming!='FileNames':
- item = wx.MenuItem(self, -1, "Rename")
- self.MyAppend(item)
- self.Bind(wx.EVT_MENU, self.OnRenameTab, item)
-
- if len(self.ISel)==1:
- item = wx.MenuItem(self, -1, "Export")
- self.MyAppend(item)
- self.Bind(wx.EVT_MENU, self.OnExportTab, item)
-
- def MyAppend(self, item):
- try:
- self.Append(item) # python3
- except:
- self.AppendItem(item) # python2
-
- def OnNaming(self, event=None):
- tabPanel=self.parent.GetParent()
- if self.itNameFile.IsChecked():
- tabPanel.tabList.setNaming('FileNames')
- else:
- tabPanel.tabList.setNaming('Ellude')
-
- tabPanel.updateTabNames()
-
- def OnDeleteTabs(self, event):
- self.mainframe.deleteTabs(self.ISel)
-
- def OnRenameTab(self, event):
- oldName = self.parent.GetString(self.ISel[0])
- dlg = wx.TextEntryDialog(self.parent, 'New table name:', 'Rename table',oldName,wx.OK|wx.CANCEL)
- dlg.CentreOnParent()
- if dlg.ShowModal() == wx.ID_OK:
- newName=dlg.GetValue()
- self.mainframe.renameTable(self.ISel[0],newName)
-
- def OnExportTab(self, event):
- self.mainframe.exportTab(self.ISel[0]);
-
- def OnSort(self, event):
- self.mainframe.sortTabs()
-
-class ColumnPopup(wx.Menu):
- def __init__(self, parent, fullmenu=False):
- wx.Menu.__init__(self)
- self.parent = parent
- self.ISel = self.parent.lbColumns.GetSelections()
-
- self.itShowID = wx.MenuItem(self, -1, "Show ID", kind=wx.ITEM_CHECK)
- self.MyAppend(self.itShowID)
- self.Bind(wx.EVT_MENU, self.OnShowID, self.itShowID)
- self.Check(self.itShowID.GetId(), self.parent.bShowID)
-
- if self.parent.tab is not None: # TODO otherwise
- item = wx.MenuItem(self, -1, "Add")
- self.MyAppend(item)
- self.Bind(wx.EVT_MENU, self.OnAddColumn, item)
-
- if len(self.ISel)==1 and self.ISel[0]>=0:
- item = wx.MenuItem(self, -1, "Rename")
- self.MyAppend(item)
- self.Bind(wx.EVT_MENU, self.OnRenameColumn, item)
- if len(self.ISel) == 1 and any(
- f['pos'] == self.ISel[0] for f in self.parent.tab.formulas):
- item = wx.MenuItem(self, -1, "Edit")
- self.MyAppend(item)
- self.Bind(wx.EVT_MENU, self.OnEditColumn, item)
- if len(self.ISel)>=1 and self.ISel[0]>=0:
- item = wx.MenuItem(self, -1, "Delete")
- self.MyAppend(item)
- self.Bind(wx.EVT_MENU, self.OnDeleteColumn, item)
-
- def MyAppend(self, item):
- try:
- self.Append(item) # python3
- except:
- self.AppendItem(item) # python2
-
- def OnShowID(self, event=None):
- self.parent.bShowID=self.itShowID.IsChecked()
- xSel,ySel,_,_ = self.parent.getColumnSelection()
- self.parent.setGUIColumns(xSel=xSel, ySel=ySel)
-
- def OnRenameColumn(self, event=None):
- iFilt = self.ISel[0]
- if self.parent.bShowID:
- oldName = self.parent.lbColumns.GetString(iFilt)[4:]
- else:
- oldName = self.parent.lbColumns.GetString(iFilt)
- dlg = wx.TextEntryDialog(self.parent, 'New column name:', 'Rename column',oldName,wx.OK|wx.CANCEL)
- dlg.CentreOnParent()
- if dlg.ShowModal() == wx.ID_OK:
- newName=dlg.GetValue()
- main=self.parent.mainframe
- ITab,STab=main.selPanel.getSelectedTables()
- # TODO adapt me for Sim. tables mode
- iFull = self.parent.Filt2Full[iFilt]
- if iFull>0: # Important since -1 would rename last column of table
- if main.tabList.haveSameColumns(ITab):
- for iTab,sTab in zip(ITab,STab):
- main.tabList.get(iTab).renameColumn(iFull-1,newName)
- else:
- self.parent.tab.renameColumn(iFull-1,newName)
- self.parent.updateColumn(iFilt,newName) #faster
- self.parent.selPanel.updateLayout()
- # a trigger for the plot is required but skipped for now
-
- def OnEditColumn(self, event):
- main=self.parent.mainframe
- if len(self.ISel) != 1:
- raise ValueError('Only one signal can be edited!')
- ITab, STab = main.selPanel.getSelectedTables()
- for iTab,sTab in zip(ITab,STab):
- if sTab == self.parent.tab.active_name:
- for f in main.tabList.get(iTab).formulas:
- if f['pos'] == self.ISel[0]:
- sName = f['name']
- sFormula = f['formula']
- break
- else:
- raise ValueError('No formula found at {0} for table {1}!'.format(self.ISel[0], sTab))
- self.showFormulaDialog('Edit column', sName, sFormula)
-
- def OnDeleteColumn(self, event):
- main=self.parent.mainframe
- iX = self.parent.comboX.GetSelection()
- ITab,STab=main.selPanel.getSelectedTables()
- # TODO adapt me for Sim. tables mode
- IFull = [self.parent.Filt2Full[iFilt]-1 for iFilt in self.ISel]
- IFull = [iFull for iFull in IFull if iFull>=0]
- if main.tabList.haveSameColumns(ITab):
- for iTab,sTab in zip(ITab,STab):
- main.tabList.get(iTab).deleteColumns(IFull)
- else:
- self.parent.tab.deleteColumns(IFull)
- self.parent.setColumns()
- self.parent.setGUIColumns(xSel=iX)
- main.redraw()
-
- def OnAddColumn(self, event):
- main=self.parent.mainframe
- self.showFormulaDialog('Add a new column')
-
- def showFormulaDialog(self, title, name='', formula=''):
- bValid=False
- bCancelled=False
- main=self.parent.mainframe
- sName=name
- sFormula=formula
-
- if self.parent.bShowID:
- columns=[no_unit(self.parent.lbColumns.GetString(i)[4:]) for i in self.ISel]
- else:
- columns=[no_unit(self.parent.lbColumns.GetString(i)) for i in self.ISel]
- if len(self.ISel)>0:
- main_unit=unit(self.parent.lbColumns.GetString(self.ISel[-1]))
- else:
- main_unit=''
- xcol = self.parent.comboX.GetStringSelection()
- xunit = unit(xcol)
- xcol = no_unit(xcol)
-
- while (not bValid) and (not bCancelled):
- dlg = FormulaDialog(title=title,columns=columns,xcol=xcol,xunit=xunit,unit=main_unit,name=sName,formula=sFormula)
- dlg.CentreOnParent()
- if dlg.ShowModal()==wx.ID_OK:
- sName = dlg.name.GetValue()
- sFormula = dlg.formula.GetValue()
- dlg.Destroy()
- if len(self.ISel)>0:
- iFilt=self.ISel[-1]
- iFull=self.parent.Filt2Full[iFilt]
- else:
- iFull = -1
-
- ITab,STab=main.selPanel.getSelectedTables()
- #if main.tabList.haveSameColumns(ITab):
- sError=''
- nError=0
- haveSameColumns=main.tabList.haveSameColumns(ITab)
- for iTab,sTab in zip(ITab,STab):
- if haveSameColumns or self.parent.tab.active_name == sTab:
- # apply formula to all tables with same columns, otherwise only to active table
- if title.startswith('Edit'):
- bValid=main.tabList.get(iTab).setColumnByFormula(sName,sFormula,iFull)
- else:
- bValid=main.tabList.get(iTab).addColumnByFormula(sName,sFormula,iFull)
- if not bValid:
- sError+='The formula didn''t eval for table {}\n'.format(sTab)
- nError+=1
- if len(sError)>0:
- Error(self.parent,sError)
- if nErrorlen(self.columns):
- print('',self.Filt2Full)
- print('',self.columns)
- raise Exception('Error in Filt2Full')
- return self.columns[self.Filt2Full]
-
- def setReadOnly(self, tabLabel='', cols=[]):
- """ Set this list of columns as readonly and non selectable """
- self.tab=None
- self.bReadOnly=True
- self.lb.SetLabel(tabLabel)
- self.setColumns(columnNames=cols)
- self.setGUIColumns()
- self.lbColumns.Enable(True)
- self.comboX.Enable(False)
- self.lbColumns.SetSelection(-1)
- self.bt.Enable(False)
- self.bShowID=False
- self.btClear.Enable(False)
- self.btFilter.Enable(False)
- self.tFilter.Enable(False)
- self.tFilter.SetValue('')
-
- def setTab(self,tab=None,xSel=-1,ySel=[],colNames=None, tabLabel=''):
- """ Set the table used for the columns, update the GUI """
- self.tab=tab;
- self.lbColumns.Enable(True)
- self.comboX.Enable(True)
- self.bReadOnly=False
- self.btClear.Enable(True)
- self.btFilter.Enable(True)
- self.tFilter.Enable(True)
- self.bt.Enable(True)
- if tab is not None:
- self.Filt2Full=None # TODO
- if tab.active_name!='default':
- self.lb.SetLabel(' '+tab.active_name)
- self.setColumns()
- self.setGUIColumns(xSel=xSel, ySel=ySel)
- else:
- self.Filt2Full=None # TODO Decide whether filter should be applied...
- self.lb.SetLabel(tabLabel)
- self.setColumns(columnNames=colNames)
- self.setGUIColumns(xSel=xSel, ySel=ySel)
-
- def updateColumn(self,i,newName):
- """ Update of one column name
- i: index in GUI
- """
- iFull = self.Filt2Full[i]
- if self.bShowID:
- newName='{:03d} '.format(iFull)+newName
- self.lbColumns.SetString(i,newName)
- self.comboX.SetString (i,newName)
- self.columns[iFull] = newName
-
- def Full2Filt(self,iFull):
- try:
- return self.Filt2Full.index(iFull)
- except:
- return -1
-
- def setColumns(self, columnNames=None):
- # Get columns from user inputs, or table, or stored.
- if columnNames is not None:
- # Populating based on user inputs..
- columns=columnNames
- elif self.tab is None:
- columns=self.columns
- else:
- # Populating based on table (safest if table was updated)
- columns=['Index']+self.tab.columns
- # Storing columns, considered as "Full"
- self.columns=np.array(columns)
-
- def setGUIColumns(self, xSel=-1, ySel=[]):
- """ Set GUI columns based on self.columns and potential filter """
- # Filtering columns if neeed
- sFilt = self.tFilter.GetLineText(0).strip()
- if len(sFilt)>0:
- Lf, If = filter_list(self.columns, sFilt)
- self.Filt2Full = If
- else:
- self.Filt2Full = list(np.arange(len(self.columns)))
- columns=self.columns[self.Filt2Full]
-
- # GUI update
- self.Freeze()
- if self.bShowID:
- columnsY= ['{:03d} '.format(i)+c for i,c in enumerate(columns)]
- columnsX= ['{:03d} '.format(i)+c for i,c in enumerate(self.columns)]
- else:
- columnsY= columns
- columnsX= self.columns
- self.lbColumns.Set(columnsY) # potentially filterd
- # Slow line for many columns
- # NOTE: limiting to 300 for now.. I'm not sure anywant would want to scroll more than that
- # Consider adding a "more button"
- # see e.g. https://comp.soft-sys.wxwindows.narkive.com/gDfA1Ds5/long-load-time-in-wxpython
- self.comboX.Set(columnsX[:300]) # non filtered
-
- # Set selection for y, if any, and considering filtering
- for iFull in ySel:
- if iFull=0:
- iFilt = self.Full2Filt(iFull)
- if iFilt>0:
- self.lbColumns.SetSelection(iFilt)
- self.lbColumns.EnsureVisible(iFilt)
- if len(self.lbColumns.GetSelections())<=0:
- self.lbColumns.SetSelection(self.getDefaultColumnY(self.tab,len(columnsY)-1))
-
- # Set selection for x, if any, NOTE x is not filtered!
- if (xSel<0) or xSel>len(columnsX):
- self.comboX.SetSelection(self.getDefaultColumnX(self.tab,len(columnsX)-1))
- else:
- self.comboX.SetSelection(xSel)
- self.Thaw()
-
- def forceOneSelection(self):
- ISel=self.lbColumns.GetSelections()
- self.lbColumns.SetSelection(-1)
- if len(ISel)>0:
- self.lbColumns.SetSelection(ISel[0])
-
- def forceZeroSelection(self):
- self.lbColumns.SetSelection(-1)
-
- def empty(self):
- self.lbColumns.Clear()
- self.comboX.Clear()
- self.lb.SetLabel('')
- self.bReadOnly=False
- self.lbColumns.Enable(False)
- self.comboX.Enable(False)
- self.bt.Enable(False)
- self.tab=None
- self.columns=[]
- self.Filt2Full=None
- self.btClear.Enable(False)
- self.btFilter.Enable(False)
- self.tFilter.Enable(False)
- self.tFilter.SetValue('')
-
- def getColumnSelection(self):
- iX = self.comboX.GetSelection()
- if self.bShowID:
- sX = self.comboX.GetStringSelection()[4:]
- else:
- sX = self.comboX.GetStringSelection()
- IY = self.lbColumns.GetSelections()
- if self.bShowID:
- SY = [self.lbColumns.GetString(i)[4:] for i in IY]
- else:
- SY = [self.lbColumns.GetString(i) for i in IY]
- iXFull = iX # NOTE: x is always in full
- IYFull = [self.Filt2Full[iY] for iY in IY]
- return iXFull,IYFull,sX,SY
-
- def onClearFilter(self, event=None):
- self.tFilter.SetValue('')
- self.onFilterChange()
-
- def onFilterChange(self, event=None):
- xSel,ySel,_,_ = self.getColumnSelection() # (indices in full)
- self.setGUIColumns(xSel=xSel, ySel=ySel) # <<< Filtering done here
- self.triggerPlot() # Trigger a col selection event
-
- def onFilterKey(self, event=None):
- s=GetKeyString(event)
- if s=='ESCAPE' or s=='Ctrl+C':
- self.onClearFilter()
- event.Skip()
-
- def triggerPlot(self):
- event=wx.PyCommandEvent(wx.EVT_LISTBOX.typeId, self.lbColumns.GetId())
- wx.PostEvent(self.GetEventHandler(), event)
-
-
-# --------------------------------------------------------------------------------}
-# --- Selection Panel
-# --------------------------------------------------------------------------------{
-class SelectionPanel(wx.Panel):
- """ Display options for the user to select data """
- def __init__(self, parent, tabList, mode='auto',mainframe=None):
- # Superclass constructor
- super(SelectionPanel,self).__init__(parent)
- # DATA
- self.mainframe = mainframe
- self.tabList = None
- self.itabForCol = None
- self.parent = parent
- self.tabSelections = {}
- self.tabSelected = [] # NOTE only used to remember a selection after a reload
- self.modeRequested = mode
- self.currentMode = None
- self.nSplits = -1
-
- # GUI DATA
- self.splitter = MultiSplit(self, style=wx.SP_LIVE_UPDATE)
- self.splitter.SetMinimumPaneSize(70)
- self.tabPanel = TablePanel (self.splitter,mainframe, tabList)
- self.colPanel1 = ColumnPanel(self.splitter, self, mainframe);
- self.colPanel2 = ColumnPanel(self.splitter, self, mainframe);
- self.colPanel3 = ColumnPanel(self.splitter, self, mainframe);
- self.tabPanel.Hide()
- self.colPanel1.Hide()
- self.colPanel2.Hide()
- self.colPanel3.Hide()
-
- # Layout
- self.updateLayout()
- VertSizer = wx.BoxSizer(wx.VERTICAL)
- VertSizer.Add(self.splitter, 2, flag=wx.EXPAND, border=0)
- self.SetSizer(VertSizer)
-
- # TRIGGERS
- self.setTables(tabList)
-
- def updateLayout(self,mode=None):
- self.Freeze()
- if mode is None:
- mode=self.modeRequested
- else:
- self.modeRequested = mode
- if mode=='auto':
- self.autoMode()
- elif mode=='sameColumnsMode':
- self.sameColumnsMode()
- elif mode=='simColumnsMode':
- self.simColumnsMode()
- elif mode=='twoColumnsMode':
- self.twoColumnsMode()
- elif mode=='threeColumnsMode':
- self.threeColumnsMode()
- else:
- self.Thaw()
- raise Exception('Wrong mode for selection layout: {}'.format(mode))
- self.Thaw()
-
-
- def autoMode(self):
- ISel=self.tabPanel.lbTab.GetSelections()
- if self.tabList is not None:
- if self.tabList.len()<=0:
- self.nSplits=-1
- self.splitter.removeAll()
- elif self.tabList.haveSameColumns():
- self.sameColumnsMode()
- elif self.tabList.haveSameColumns(ISel):
- # We don't do same column because we know at least one table is different
- # to avoid "jumping" too much
- self.twoColumnsMode()
- else:
- # See if tables are quite similar
- IKeepPerTab, IMissPerTab, IDuplPerTab= getTabCommonColIndices([self.tabList.get(i) for i in ISel])
- if np.all(np.array([len(I) for I in IMissPerTab])<30) and np.all(np.array([len(I) for I in IKeepPerTab])>=2):
- self.simColumnsMode()
- elif len(ISel)==2:
- self.twoColumnsMode()
- elif len(ISel)==3:
- self.threeColumnsMode()
- else:
- #self.simColumnsMode(self)
- raise Exception('Too many panels selected with significant columns differences.')
-
- def sameColumnsMode(self):
- self.currentMode = 'sameColumnsMode'
- if self.nSplits==1:
- return
- if self.nSplits==0 and self.tabList.len()<=1:
- return
- self.splitter.removeAll()
- if self.tabList is not None:
- if self.tabList.len()>1:
- self.splitter.AppendWindow(self.tabPanel)
- self.splitter.AppendWindow(self.colPanel1)
- if self.mainframe is not None:
- self.mainframe.mainFrameUpdateLayout()
- if self.tabList is not None:
- if self.tabList.len()<=1:
- self.nSplits=0
- else:
- self.nSplits=1
- else:
- self.nSplits=0
-
- def simColumnsMode(self):
- self.currentMode = 'simColumnsMode'
- self.splitter.removeAll()
- self.splitter.AppendWindow(self.tabPanel)
- self.splitter.AppendWindow(self.colPanel2)
- self.splitter.AppendWindow(self.colPanel1)
- self.splitter.setEquiSash()
- if self.nSplits<2 and self.mainframe is not None:
- self.mainframe.mainFrameUpdateLayout()
- self.nSplits=2
-
- def twoColumnsMode(self):
- self.currentMode = 'twoColumnsMode'
- if self.nSplits==2:
- return
- self.splitter.removeAll()
- self.splitter.AppendWindow(self.tabPanel)
- self.splitter.AppendWindow(self.colPanel2)
- self.splitter.AppendWindow(self.colPanel1)
- self.splitter.setEquiSash()
- if self.nSplits<2 and self.mainframe is not None:
- self.mainframe.mainFrameUpdateLayout()
- self.nSplits=2
-
- def threeColumnsMode(self):
- self.currentMode = 'threeColumnsMode'
- if self.nSplits==3:
- return
- self.splitter.removeAll()
- self.splitter.AppendWindow(self.tabPanel)
- self.splitter.AppendWindow(self.colPanel3)
- self.splitter.AppendWindow(self.colPanel2)
- self.splitter.AppendWindow(self.colPanel1)
- self.splitter.setEquiSash()
- if self.mainframe is not None:
- self.mainframe.mainFrameUpdateLayout()
- self.nSplits=3
-
- def setTables(self,tabList,update=False):
- """ Set the list of tables. Keeping the selection if it's an update """
- # TODO PUT ME IN TABLE PANEL
- # Find a better way to remember selection
- #print('UPDATING TABLES')
- # Emptying GUI - TODO only if needed
- self.colPanel1.empty()
- self.colPanel2.empty()
- self.colPanel3.empty()
- # Adding
- self.tabList = tabList
- self.tabPanel.tabList = self.tabList
- tabnames = self.tabList.tabNames
- self.tabPanel.updateTabNames()
- for tn in tabnames:
- if tn not in self.tabSelections.keys():
- self.tabSelections[tn]={'xSel':-1,'ySel':[]}
- else:
- pass # do nothing
-
- # Reselecting
- if len(self.tabSelected)>0:
- # Removed line below since two column mode implemented
- #if not haveSameColumns(tabs,ISel):
- # ISel=[ISel[0]]
- for i in self.tabSelected:
- if i0:
- # Trigger - updating columns and layout
- ISel=self.tabPanel.lbTab.GetSelections()
- self.tabSelected=ISel
- if self.currentMode=='simColumnsMode':
- self.setColForSimTab(ISel)
- else:
- if len(ISel)==1:
- self.setTabForCol(ISel[0],1)
- elif len(ISel)==2:
- self.setTabForCol(ISel[0],1)
- self.setTabForCol(ISel[1],2)
- elif len(ISel)==3:
- self.setTabForCol(ISel[0],1)
- self.setTabForCol(ISel[1],2)
- self.setTabForCol(ISel[2],3)
- else: # Likely all tables have the same columns
- self.setTabForCol(ISel[0],1)
- self.updateLayout(self.modeRequested)
-
- def setTabForCol(self,iTabSel,iPanel):
- t = self.tabList.get(iTabSel)
- ts = self.tabSelections[t.name]
- if iPanel==1:
- self.colPanel1.setTab(t,ts['xSel'],ts['ySel'])
- elif iPanel==2:
- self.colPanel2.setTab(t,ts['xSel'],ts['ySel'])
- elif iPanel==3:
- self.colPanel3.setTab(t,ts['xSel'],ts['ySel'])
- else:
- raise Exception('Wrong ipanel')
-
- def setColForSimTab(self,ISel):
- """ Set column panels for similar tables """
- tabs = [self.tabList.get(i) for i in ISel]
- IKeepPerTab, IMissPerTab, IDuplPerTab = getTabCommonColIndices(tabs)
- LenMiss = np.array([len(I) for I in IMissPerTab])
- LenKeep = np.array([len(I) for I in IKeepPerTab])
- LenDupl = np.array([len(I) for I in IDuplPerTab])
-
- ColInfo = ['Sim. table mode ']
- ColInfo += ['']
- if self.tabList.haveSameColumns(ISel):
- if len(ISel)>1:
- ColInfo += ['Columns identical','']
- else:
- if (np.all(np.array(LenMiss)==0)):
- ColInfo += ['Columns identical']
- ColInfo += ['Order different!']
-
- ColInfo += ['','First difference:']
- ColInfo.append('----------------------------------')
- bFirst=True
- for it,t in enumerate(tabs):
- print('IKeep',IKeepPerTab[it])
- if it==0:
- continue
- INotOrdered=[ii for i,ii in enumerate(IKeepPerTab[it]) if ii!=IKeepPerTab[0][i]]
- print('INot',INotOrdered)
- if len(INotOrdered)>0:
- im=INotOrdered[0]
- if bFirst:
- ColInfo.append('{}:'.format(tabs[0].active_name))
- ColInfo.append('{:03d} {:s}'.format(im, tabs[0].columns[im]))
- bFirst=False
- ColInfo.append('{}:'.format(t.active_name))
- ColInfo.append('{:03d} {:s}'.format(im, t.columns[im]))
- ColInfo.append('----------------------------------')
-
- else:
- ColInfo += ['Columns different!']
- ColInfo += ['(similar: {})'.format(LenKeep[0])]
- ColInfo += ['','Missing columns:']
- ColInfo.append('----------------------------------')
- for it,t in enumerate(tabs):
- ColInfo.append('{}:'.format(t.active_name))
- if len(IMissPerTab[it])==0:
- ColInfo.append(' (None) ')
- for im in IMissPerTab[it]:
- ColInfo.append('{:03d} {:s}'.format(im, t.columns[im]))
- ColInfo.append('----------------------------------')
-
- if (np.any(np.array(LenDupl)>0)):
- if len(ISel)>1:
- ColInfo += ['','Common duplicates:']
- else:
- ColInfo += ['','Duplicates:']
- ColInfo.append('----------------------------------')
- for it,t in enumerate(tabs):
- ColInfo.append('{}:'.format(t.active_name))
- if len(IDuplPerTab[it])==0:
- ColInfo.append(' (None) ')
- for im in IDuplPerTab[it]:
- ColInfo.append('{:03d} {:s}'.format(im, t.columns[im]))
- ColInfo.append('----------------------------------')
-
-
- colNames = ['Index'] + [tabs[0].columns[i] for i in IKeepPerTab[0]]
- self.colPanel1.setTab(tab=None, colNames=colNames, tabLabel=' Tab. Intersection')
- self.colPanel2.setReadOnly(' Tab. Difference', ColInfo)
- self.IKeepPerTab=IKeepPerTab
-
-
-
- def selectDefaultTable(self):
- # Selecting the first table
- if self.tabPanel.lbTab.GetCount()>0:
- self.tabPanel.lbTab.SetSelection(0)
- self.tabSelected=[0]
- else:
- self.tabSelected=[]
-
- def tabSelectionChanged(self):
- # TODO This can be cleaned-up and merged with updateLayout
- #print('Tab selection change')
- # Storing the previous selection
- #self.printSelection()
- self.saveSelection() #
- #self.printSelection()
- ISel=self.tabPanel.lbTab.GetSelections()
- if len(ISel)>0:
- if self.modeRequested=='auto':
- self.autoMode()
- if self.currentMode=='simColumnsMode':# and len(ISel)>1:
- self.setColForSimTab(ISel)
- self.tabSelected=self.tabPanel.lbTab.GetSelections()
- return
-
- if self.tabList.haveSameColumns(ISel):
- # Setting tab
- self.setTabForCol(ISel[0],1)
- self.colPanel2.empty()
- self.colPanel3.empty()
- else:
- if self.nSplits==2:
- if len(ISel)>2:
- Error(self,'In this mode, only two tables can be selected. To compare three tables, uses the "3 different tables" mode. Otherwise the tables need to have the same columns.')
- ISel=ISel[0:2]
- self.tabPanel.lbTab.SetSelection(wx.NOT_FOUND)
- for isel in ISel:
- self.tabPanel.lbTab.SetSelection(isel)
- self.colPanel3.empty()
- elif self.nSplits==3:
- if len(ISel)>3:
- Error(self,'In this mode, only three tables can be selected. To compare more than three tables, the tables need to have the same columns.')
- ISel=ISel[0:3]
- self.tabPanel.lbTab.SetSelection(wx.NOT_FOUND)
- for isel in ISel:
- self.tabPanel.lbTab.SetSelection(isel)
- else:
- Error(self,'The tables selected have different columns.\n\nThis is not compatible with the "Same tables" mode. To compare them, chose one of the following mode: "2 tables", "3 tables" or "Sim. tables".')
- self.colPanel2.empty()
- self.colPanel3.empty()
- # unselect all and select only the first one
- ISel=[ISel[0]]
- self.tabPanel.lbTab.SetSelection(wx.NOT_FOUND)
- self.tabPanel.lbTab.SetSelection(ISel[0])
- for iPanel,iTab in enumerate(ISel):
- self.setTabForCol(iTab,iPanel+1)
- #print('>>>Updating tabSelected, from',self.tabSelected,'to',self.tabPanel.lbTab.GetSelections())
- self.tabSelected=self.tabPanel.lbTab.GetSelections()
-
- def colSelectionChanged(self):
- """ Simple triggers when column selection is changed, NOTE: does not redraw """
- if self.currentMode=='simColumnsMode':
- self.colPanel2.forceZeroSelection()
- else:
- if self.nSplits in [2,3]:
- ISel=self.tabPanel.lbTab.GetSelections()
- if self.tabList.haveSameColumns(ISel):
- pass # TODO: this test is identical to onTabSelectionChange. Unification.
- # elif len(ISel)==2:
- # self.colPanel1.forceOneSelection()
- # self.colPanel2.forceOneSelection()
- # elif len(ISel)==3:
- # self.colPanel1.forceOneSelection()
- # self.colPanel2.forceOneSelection()
- # self.colPanel3.forceOneSelection()
-
- def update_tabs(self, tabList):
- self.setTables(tabList, update=True)
-
- def renameTable(self,iTab, oldName, newName):
- #self.printSelection()
- self.tabSelections[newName] = self.tabSelections.pop(oldName)
- self.tabPanel.updateTabNames()
- #self.printSelection()
-
- def saveSelection(self):
- #self.ISel=self.tabPanel.lbTab.GetSelections()
- ISel=self.tabSelected #
- if self.tabList.haveSameColumns(ISel):
- for ii in ISel:
- t=self.tabList.get(ii)
- self.tabSelections[t.name]['xSel'] = self.colPanel1.comboX.GetSelection()
- self.tabSelections[t.name]['ySel'] = self.colPanel1.lbColumns.GetSelections()
- else:
- if len(ISel)>=1:
- t=self.tabList.get(ISel[0])
- self.tabSelections[t.name]['xSel'] = self.colPanel1.comboX.GetSelection()
- self.tabSelections[t.name]['ySel'] = self.colPanel1.lbColumns.GetSelections()
- if len(ISel)>=2:
- t=self.tabList.get(ISel[1])
- self.tabSelections[t.name]['xSel'] = self.colPanel2.comboX.GetSelection()
- self.tabSelections[t.name]['ySel'] = self.colPanel2.lbColumns.GetSelections()
- if len(ISel)>=3:
- t=self.tabList.get(ISel[2])
- self.tabSelections[t.name]['xSel'] = self.colPanel3.comboX.GetSelection()
- self.tabSelections[t.name]['ySel'] = self.colPanel3.lbColumns.GetSelections()
- self.tabSelected = self.tabPanel.lbTab.GetSelections();
-
- def printSelection(self):
- print('Number of tabSelections stored:',len(self.tabSelections))
- TS=self.tabSelections
- for i,tn in enumerate(self.tabList.tabNames):
- if tn not in TS.keys():
- print('Tab',i,'>>> Name {} not found in selection'.format(tn))
- else:
- print('Tab',i,'xSel:',TS[tn]['xSel'],'ySel:',TS[tn]['ySel'],'Name:',tn)
-
- def getPlotDataSelection(self):
- ID = []
- SameCol=False
- if self.tabList is not None and self.tabList.len()>0:
- ITab,STab = self.getSelectedTables()
- if self.currentMode=='simColumnsMode' and len(ITab)>1:
- iiX1,IY1,ssX1,SY1 = self.colPanel1.getColumnSelection()
- SameCol=False
- for i,(itab,stab) in enumerate(zip(ITab,STab)):
- IKeep=self.IKeepPerTab[i]
- for j,(iiy,ssy) in enumerate(zip(IY1,SY1)):
- if iiy==0:
- iy = 0
- sy = ssy
- else:
- iy = IKeep[iiy-1]+1
- sy = self.tabList.get(itab).columns[IKeep[iiy-1]]
- if iiX1==0:
- iX1 = 0
- sX1 = ssX1
- else:
- iX1 = IKeep[iiX1-1]+1
- sX1 = self.tabList.get(itab).columns[IKeep[iiX1-1]]
- ID.append([itab,iX1,iy,sX1,sy,stab])
- else:
- iX1,IY1,sX1,SY1 = self.colPanel1.getColumnSelection()
- SameCol=self.tabList.haveSameColumns(ITab)
- if self.nSplits in [0,1] or SameCol:
- for i,(itab,stab) in enumerate(zip(ITab,STab)):
- for j,(iy,sy) in enumerate(zip(IY1,SY1)):
- ID.append([itab,iX1,iy,sX1,sy,stab])
- elif self.nSplits in [2,3]:
- if len(ITab)>=1:
- for j,(iy,sy) in enumerate(zip(IY1,SY1)):
- ID.append([ITab[0],iX1,iy,sX1,sy,STab[0]])
- if len(ITab)>=2:
- iX2,IY2,sX2,SY2 = self.colPanel2.getColumnSelection()
- for j,(iy,sy) in enumerate(zip(IY2,SY2)):
- ID.append([ITab[1],iX2,iy,sX2,sy,STab[1]])
- if len(ITab)>=3:
- iX2,IY2,sX2,SY2 = self.colPanel3.getColumnSelection()
- for j,(iy,sy) in enumerate(zip(IY2,SY2)):
- ID.append([ITab[2],iX2,iy,sX2,sy,STab[2]])
- else:
- raise Exception('Wrong number of splits {}'.format(self.nSplits))
- return ID,SameCol,self.currentMode
-
- def getSelectedTables(self):
- I=self.tabPanel.lbTab.GetSelections()
- S=[self.tabPanel.lbTab.GetString(i) for i in I]
- return I,S
-
- def getAllTables(self):
- I=range(self.tabPanel.lbTab.GetCount())
- S=[self.tabPanel.lbTab.GetString(i) for i in I]
- return I,S
-
- def clean_memory(self):
- self.colPanel1.empty()
- self.colPanel2.empty()
- self.colPanel3.empty()
- self.tabPanel.empty()
- del self.tabList
- self.tabList=None
-
- @property
- def xCol(self):
- iX, _, sX, _ = self.colPanel1.getColumnSelection()
- return iX,sX
-
-
-if __name__ == '__main__':
- import pandas as pd;
- from Tables import Table
- import numpy as np
-
- def OnTabPopup(event):
- self.PopupMenu(TablePopup(self,selPanel.tabPanel.lbTab), event.GetPosition())
-
- app = wx.App(False)
- self=wx.Frame(None,-1,"Title")
- tab=Table(data=pd.DataFrame(data={'ColA': np.random.normal(0,1,100)+1,'ColB':np.random.normal(0,1,100)+2}))
- selPanel=SelectionPanel(self,[tab],mode='twoColumnsMode')
- self.SetSize((800, 600))
- self.Center()
- self.Show()
- selPanel.tabPanel.lbTab.Bind(wx.EVT_RIGHT_DOWN, OnTabPopup)
-
-
- app.MainLoop()
-
+import wx
+import platform
+try:
+ from .common import *
+ from .GUICommon import *
+ from .GUIMultiSplit import MultiSplit
+ from .GUIToolBox import GetKeyString
+except:
+ raise
+# from common import *
+# from GUICommon import *
+# from GUIMultiSplit import MultiSplit
+
+
+__all__ = ['ColumnPanel', 'TablePanel', 'SelectionPanel','SEL_MODES','SEL_MODES_ID','TablePopup','ColumnPopup']
+
+SEL_MODES = ['auto','Same tables' ,'Sim. tables' ,'2 tables','3 tables (exp.)' ]
+SEL_MODES_ID = ['auto','sameColumnsMode','simColumnsMode','twoColumnsMode' ,'threeColumnsMode' ]
+
+def ireplace(text, old, new):
+ """ Replace case insensitive """
+ try:
+ index_l = text.lower().index(old.lower())
+ return text[:index_l] + new + text[index_l + len(old):]
+ except:
+ return text
+
+
+# --------------------------------------------------------------------------------}
+# --- Formula diagog
+# --------------------------------------------------------------------------------{
+class FormulaDialog(wx.Dialog):
+ def __init__(self, title='', name='', formula='',columns=[],unit='',xcol='',xunit=''):
+ wx.Dialog.__init__(self, None, title=title)
+ # --- Data
+ self.unit=unit.strip().replace(' ','')
+ self.columns=['{'+c+'}' for c in columns]
+ self.xcol='{'+xcol+'}'
+ self.xunit=xunit.strip().replace(' ','')
+ if len(formula)==0:
+ formula=' + '.join(self.columns)
+ if len(name)==0:
+ name=self.getDefaultName()
+ self.formula_in=formula
+
+
+ quick_lbl = wx.StaticText(self, label="Predefined: " )
+ self.cbQuick = wx.ComboBox(self, choices=['None','x 1000','/ 1000','deg2rad','rad2deg','rpm2radps','radps2rpm','norm','squared','d/dx'], style=wx.CB_READONLY)
+ self.cbQuick.SetSelection(0)
+ self.cbQuick.Bind(wx.EVT_COMBOBOX ,self.onQuickFormula)
+
+ # Formula info
+ formula_lbl = wx.StaticText(self, label="Formula: ")
+ self.formula = wx.TextCtrl(self)
+ #self.formula.SetFont(getMonoFont(self))
+
+ self.formula.SetValue(formula)
+ formula_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ formula_sizer.Add(formula_lbl ,0,wx.ALL|wx.RIGHT|wx.CENTER,5)
+ formula_sizer.Add(self.formula,1,wx.ALL|wx.EXPAND|wx.CENTER,5)
+ formula_sizer.Add(quick_lbl ,0,wx.ALL|wx.CENTER,5)
+ formula_sizer.Add(self.cbQuick,0,wx.ALL|wx.CENTER,5)
+
+
+ # name info
+ name_lbl = wx.StaticText(self, label="New name: " )
+ self.name = wx.TextCtrl(self, size=wx.Size(200,-1))
+ self.name.SetValue(name)
+ #self.name.SetFont(getMonoFont(self))
+ name_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ name_sizer.Add(name_lbl ,0,wx.ALL|wx.RIGHT|wx.CENTER,5)
+ name_sizer.Add(self.name,0,wx.ALL|wx.CENTER,5)
+
+ info ='The formula needs to have a valid python syntax for an array manipulation. The available arrays are \n'
+ info+='the columns of the current table. The column names (without units) are surrounded by curly brackets.\n'
+ info+='You have access to numpy using `np`.\n\n'
+ info+='For instance, if you have two columns called `ColA [m]` and `ColB [m]` you can use:\n'
+ info+=' - ` {ColA} + {ColB} `\n'
+ info+=' - ` np.sqrt( {ColA}**2/1000 + 1/{ColB}**2 ) `\n'
+ info+=' - ` np.sin ( {ColA}*2*np.pi + {ColB} ) `\n'
+ help_lbl = wx.StaticText(self, label='Help: ')
+ info_lbl = wx.StaticText(self, label=info)
+ help_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ help_sizer.Add(help_lbl ,0,wx.ALL|wx.RIGHT|wx.TOP,5)
+ help_sizer.Add(info_lbl ,0,wx.ALL|wx.TOP,5)
+
+
+
+ self.btOK = wx.Button(self, wx.ID_OK)#, label = "OK" )
+ btCL = wx.Button(self,label = "Cancel")
+ bt_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ bt_sizer.Add(self.btOK, 0 ,wx.ALL,5)
+ bt_sizer.Add(btCL, 0 ,wx.ALL,5)
+ #btOK.Bind(wx.EVT_BUTTON,self.onOK )
+ btCL.Bind(wx.EVT_BUTTON,self.onCancel)
+
+
+ main_sizer = wx.BoxSizer(wx.VERTICAL)
+ #main_sizer.Add(quick_sizer ,0,wx.ALL|wx.EXPAND,5)
+ main_sizer.Add(formula_sizer,0,wx.ALL|wx.EXPAND,5)
+ main_sizer.Add(name_sizer ,0,wx.ALL|wx.EXPAND,5)
+ main_sizer.Add(help_sizer ,0 ,wx.ALL|wx.CENTER, 5)
+ main_sizer.Add(bt_sizer ,0, wx.ALL|wx.CENTER, 5)
+ self.SetSizer(main_sizer)
+ self.Fit()
+
+ def stripBrackets(self,s):
+ return s.replace('{','').replace('}','')
+
+ def getOneColName(self):
+ if len(self.columns)>0:
+ return self.columns[-1]
+ else:
+ return ''
+
+ def get_unit(self):
+ if len(self.unit)>0:
+ return ' ['+self.unit+']'
+ else:
+ return ''
+ def get_squared_unit(self):
+ if len(self.unit)>0:
+ if self.unit[0].lower()=='-':
+ return ' [-]'
+ else:
+ return ' [('+self.unit+')^2]'
+ else:
+ return ''
+ def get_kilo_unit(self):
+ if len(self.unit)>0:
+ if len(self.unit)>=1:
+ if self.unit[0].lower()=='-':
+ return ' [-]'
+ elif self.unit[0].lower()=='G':
+ r='T'
+ elif self.unit[0].lower()=='M':
+ r='G'
+ elif self.unit[0]=='k':
+ r='M'
+ elif self.unit[0]=='m':
+ if len(self.unit)==1:
+ r='km'
+ elif self.unit[1]=='/':
+ r='km'
+ else:
+ r=''
+ else:
+ r='k'+self.unit[0]
+ return ' ['+r+self.unit[1:]+']'
+ else:
+ return ' [k'+self.unit+']'
+ else:
+ return ''
+ def get_milli_unit(self):
+ if len(self.unit)>=1:
+ if self.unit[0].lower()=='-':
+ return ' [-]'
+ elif self.unit[0].lower()=='T':
+ r='G'
+ elif self.unit[0]=='G':
+ r='M'
+ elif self.unit[0]=='M':
+ r='k'
+ elif self.unit[0].lower()=='k':
+ r=''
+ elif self.unit[0]=='m':
+ if len(self.unit)==1:
+ r='mm'
+ elif self.unit[1]=='/':
+ r='mm'
+ else:
+ r='mu'
+ else:
+ r='m'+self.unit[0]
+
+ return ' ['+r+self.unit[1:]+']'
+ else:
+ return ''
+ def get_deriv_unit(self):
+ if self.unit==self.xunit:
+ return ' [-]'
+ else:
+ return ' ['+self.unit+'/'+self.xunit+']'
+
+ def getDefaultName(self):
+ if len(self.columns)>0:
+ return self.stripBrackets(self.getOneColName())+' New'+self.get_unit()
+ else:
+ return ''
+
+ def onQuickFormula(self, event):
+ i = self.cbQuick.GetSelection()
+ s = self.cbQuick.GetStringSelection()
+ if s=='None':
+ self.formula.SetValue(self.formula_in)
+ return
+
+ #self.formula_in=self.formula.GetValue()
+ c1 = self.getOneColName()
+ n1 = self.stripBrackets(c1)
+
+ if s=='x 1000':
+ self.formula.SetValue(c1+' * 1000')
+ self.name.SetValue(n1+'_x1000'+ self.get_milli_unit())
+ elif s=='/ 1000':
+ self.formula.SetValue(c1+' / 1000')
+ self.name.SetValue(n1+'_/1000'+self.get_kilo_unit())
+ elif s=='deg2rad':
+ self.formula.SetValue(c1+' *np.pi/180')
+ self.name.SetValue(n1+'_rad [rad]')
+ elif s=='rad2deg':
+ self.formula.SetValue(c1+' *180/np.pi')
+ self.name.SetValue(n1+'_deg [deg]')
+ elif s=='rpm2radps':
+ self.formula.SetValue(c1+' *2*np.pi/60')
+ self.name.SetValue(n1+'_radps [rad/s]')
+ elif s=='radps2rpm':
+ self.formula.SetValue(c1+' *60/(2*np.pi)')
+ self.name.SetValue(n1+'_rpm [rpm]')
+ elif s=='norm':
+ self.formula.SetValue('np.sqrt( '+'**2 + '.join(self.columns)+'**2 )')
+ self.name.SetValue(n1+'_norm'+self.get_unit())
+ elif s=='squared':
+ self.formula.SetValue('**2 + '.join(self.columns)+'**2 ')
+ self.name.SetValue(n1+'^2'+self.get_squared_unit())
+ elif s=='d/dx':
+ self.formula.SetValue('np.gradient( '+'+'.join(self.columns)+ ', '+self.xcol+' )')
+ nx = self.stripBrackets(self.xcol)
+ bDoNewName=True
+ if self.xunit=='s':
+ if n1.lower().find('speed')>=0:
+ n1=ireplace(n1,'speed','Acceleration')
+ bDoNewName=False
+ elif n1.lower().find('velocity')>=0:
+ n1=ireplace(n1,'velocity','Acceleration')
+ bDoNewName=False
+ elif n1.lower().find('vel')>=0:
+ n1=ireplace(n1,'vel','Acc')
+ bDoNewName=False
+ elif n1.lower().find('position')>=0:
+ n1=ireplace(n1,'position','speed')
+ bDoNewName=False
+ elif n1.lower().find('pos')>=0:
+ n1=ireplace(n1,'pos','Vel')
+ bDoNewName=False
+ else:
+ n1='d('+n1+')/dt'
+ else:
+ n1='d('+n1+')/d('+nx+')'
+ self.name.SetValue(n1+self.get_deriv_unit())
+ else:
+ raise Exception('Unknown quick formula {}'.s)
+
+ def onCancel(self, event):
+ self.Destroy()
+# --------------------------------------------------------------------------------}
+# --- Popup menus
+# --------------------------------------------------------------------------------{
+class TablePopup(wx.Menu):
+ def __init__(self, mainframe, parent, fullmenu=False):
+ wx.Menu.__init__(self)
+ self.parent = parent # parent is listbox
+ self.mainframe = mainframe
+ self.ISel = self.parent.GetSelections()
+
+ if fullmenu:
+ self.itNameFile = wx.MenuItem(self, -1, "Naming: by file names", kind=wx.ITEM_CHECK)
+ self.MyAppend(self.itNameFile)
+ self.Bind(wx.EVT_MENU, self.OnNaming, self.itNameFile)
+ self.Check(self.itNameFile.GetId(), self.parent.GetParent().tabList.Naming=='FileNames') # Checking the menu box
+
+ item = wx.MenuItem(self, -1, "Sort by name")
+ self.MyAppend(item)
+ self.Bind(wx.EVT_MENU, self.OnSort, item)
+
+ item = wx.MenuItem(self, -1, "Add")
+ self.MyAppend(item)
+ self.Bind(wx.EVT_MENU, self.mainframe.onAdd, item)
+
+ if len(self.ISel)>0:
+ item = wx.MenuItem(self, -1, "Delete")
+ self.MyAppend(item)
+ self.Bind(wx.EVT_MENU, self.OnDeleteTabs, item)
+
+ if len(self.ISel)==1:
+ tabPanel=self.parent.GetParent()
+ if tabPanel.tabList.Naming!='FileNames':
+ item = wx.MenuItem(self, -1, "Rename")
+ self.MyAppend(item)
+ self.Bind(wx.EVT_MENU, self.OnRenameTab, item)
+
+ if len(self.ISel)==1:
+ item = wx.MenuItem(self, -1, "Export")
+ self.MyAppend(item)
+ self.Bind(wx.EVT_MENU, self.OnExportTab, item)
+
+ def MyAppend(self, item):
+ try:
+ self.Append(item) # python3
+ except:
+ self.AppendItem(item) # python2
+
+ def OnNaming(self, event=None):
+ tabPanel=self.parent.GetParent()
+ if self.itNameFile.IsChecked():
+ tabPanel.tabList.setNaming('FileNames')
+ else:
+ tabPanel.tabList.setNaming('Ellude')
+
+ tabPanel.updateTabNames()
+
+ def OnDeleteTabs(self, event):
+ self.mainframe.deleteTabs(self.ISel)
+
+ def OnRenameTab(self, event):
+ oldName = self.parent.GetString(self.ISel[0])
+ dlg = wx.TextEntryDialog(self.parent, 'New table name:', 'Rename table',oldName,wx.OK|wx.CANCEL)
+ dlg.CentreOnParent()
+ if dlg.ShowModal() == wx.ID_OK:
+ newName=dlg.GetValue()
+ self.mainframe.renameTable(self.ISel[0],newName)
+
+ def OnExportTab(self, event):
+ self.mainframe.exportTab(self.ISel[0]);
+
+ def OnSort(self, event):
+ self.mainframe.sortTabs()
+
+class ColumnPopup(wx.Menu):
+ def __init__(self, parent, fullmenu=False):
+ wx.Menu.__init__(self)
+ self.parent = parent
+ self.ISel = self.parent.lbColumns.GetSelections()
+
+ self.itShowID = wx.MenuItem(self, -1, "Show ID", kind=wx.ITEM_CHECK)
+ self.MyAppend(self.itShowID)
+ self.Bind(wx.EVT_MENU, self.OnShowID, self.itShowID)
+ self.Check(self.itShowID.GetId(), self.parent.bShowID)
+
+ if self.parent.tab is not None: # TODO otherwise
+ item = wx.MenuItem(self, -1, "Add")
+ self.MyAppend(item)
+ self.Bind(wx.EVT_MENU, self.OnAddColumn, item)
+
+ if len(self.ISel)==1 and self.ISel[0]>=0:
+ item = wx.MenuItem(self, -1, "Rename")
+ self.MyAppend(item)
+ self.Bind(wx.EVT_MENU, self.OnRenameColumn, item)
+ if len(self.ISel) == 1 and any(
+ f['pos'] == self.ISel[0] for f in self.parent.tab.formulas):
+ item = wx.MenuItem(self, -1, "Edit")
+ self.MyAppend(item)
+ self.Bind(wx.EVT_MENU, self.OnEditColumn, item)
+ if len(self.ISel)>=1 and self.ISel[0]>=0:
+ item = wx.MenuItem(self, -1, "Delete")
+ self.MyAppend(item)
+ self.Bind(wx.EVT_MENU, self.OnDeleteColumn, item)
+
+ def MyAppend(self, item):
+ try:
+ self.Append(item) # python3
+ except:
+ self.AppendItem(item) # python2
+
+ def OnShowID(self, event=None):
+ self.parent.bShowID=self.itShowID.IsChecked()
+ xSel,ySel,_,_ = self.parent.getColumnSelection()
+ self.parent.setGUIColumns(xSel=xSel, ySel=ySel)
+
+ def OnRenameColumn(self, event=None):
+ iFilt = self.ISel[0]
+ if self.parent.bShowID:
+ oldName = self.parent.lbColumns.GetString(iFilt)[4:]
+ else:
+ oldName = self.parent.lbColumns.GetString(iFilt)
+ dlg = wx.TextEntryDialog(self.parent, 'New column name:', 'Rename column',oldName,wx.OK|wx.CANCEL)
+ dlg.CentreOnParent()
+ if dlg.ShowModal() == wx.ID_OK:
+ newName=dlg.GetValue()
+ main=self.parent.mainframe
+ ITab,STab=main.selPanel.getSelectedTables()
+ # TODO adapt me for Sim. tables mode
+ iFull = self.parent.Filt2Full[iFilt]
+ if iFull>0: # Important since -1 would rename last column of table
+ if main.tabList.haveSameColumns(ITab):
+ for iTab,sTab in zip(ITab,STab):
+ main.tabList.get(iTab).renameColumn(iFull-1,newName)
+ else:
+ self.parent.tab.renameColumn(iFull-1,newName)
+ self.parent.updateColumn(iFilt,newName) #faster
+ self.parent.selPanel.updateLayout()
+ # a trigger for the plot is required but skipped for now
+
+ def OnEditColumn(self, event):
+ main=self.parent.mainframe
+ if len(self.ISel) != 1:
+ raise ValueError('Only one signal can be edited!')
+ ITab, STab = main.selPanel.getSelectedTables()
+ for iTab,sTab in zip(ITab,STab):
+ if sTab == self.parent.tab.active_name:
+ for f in main.tabList.get(iTab).formulas:
+ if f['pos'] == self.ISel[0]:
+ sName = f['name']
+ sFormula = f['formula']
+ break
+ else:
+ raise ValueError('No formula found at {0} for table {1}!'.format(self.ISel[0], sTab))
+ self.showFormulaDialog('Edit column', sName, sFormula)
+
+ def OnDeleteColumn(self, event):
+ main=self.parent.mainframe
+ iX = self.parent.comboX.GetSelection()
+ ITab,STab=main.selPanel.getSelectedTables()
+ # TODO adapt me for Sim. tables mode
+ IFull = [self.parent.Filt2Full[iFilt]-1 for iFilt in self.ISel]
+ IFull = [iFull for iFull in IFull if iFull>=0]
+ if main.tabList.haveSameColumns(ITab):
+ for iTab,sTab in zip(ITab,STab):
+ main.tabList.get(iTab).deleteColumns(IFull)
+ else:
+ self.parent.tab.deleteColumns(IFull)
+ self.parent.setColumns()
+ self.parent.setGUIColumns(xSel=iX)
+ main.redraw()
+
+ def OnAddColumn(self, event):
+ main=self.parent.mainframe
+ self.showFormulaDialog('Add a new column')
+
+ def showFormulaDialog(self, title, name='', formula=''):
+ bValid=False
+ bCancelled=False
+ main=self.parent.mainframe
+ sName=name
+ sFormula=formula
+
+ if self.parent.bShowID:
+ columns=[no_unit(self.parent.lbColumns.GetString(i)[4:]) for i in self.ISel]
+ else:
+ columns=[no_unit(self.parent.lbColumns.GetString(i)) for i in self.ISel]
+ if len(self.ISel)>0:
+ main_unit=unit(self.parent.lbColumns.GetString(self.ISel[-1]))
+ else:
+ main_unit=''
+ xcol = self.parent.comboX.GetStringSelection()
+ xunit = unit(xcol)
+ xcol = no_unit(xcol)
+
+ while (not bValid) and (not bCancelled):
+ dlg = FormulaDialog(title=title,columns=columns,xcol=xcol,xunit=xunit,unit=main_unit,name=sName,formula=sFormula)
+ dlg.CentreOnParent()
+ if dlg.ShowModal()==wx.ID_OK:
+ sName = dlg.name.GetValue()
+ sFormula = dlg.formula.GetValue()
+ dlg.Destroy()
+ if len(self.ISel)>0:
+ iFilt=self.ISel[-1]
+ iFull=self.parent.Filt2Full[iFilt]
+ else:
+ iFull = -1
+
+ ITab,STab=main.selPanel.getSelectedTables()
+ #if main.tabList.haveSameColumns(ITab):
+ sError=''
+ nError=0
+ haveSameColumns=main.tabList.haveSameColumns(ITab)
+ for iTab,sTab in zip(ITab,STab):
+ if haveSameColumns or self.parent.tab.active_name == sTab:
+ # apply formula to all tables with same columns, otherwise only to active table
+ if title.startswith('Edit'):
+ bValid=main.tabList.get(iTab).setColumnByFormula(sName,sFormula,iFull)
+ else:
+ bValid=main.tabList.get(iTab).addColumnByFormula(sName,sFormula,iFull)
+ if not bValid:
+ sError+='The formula didn''t eval for table {}\n'.format(sTab)
+ nError+=1
+ if len(sError)>0:
+ Error(self.parent,sError)
+ if nErrorlen(self.columns):
+ print('',self.Filt2Full)
+ print('',self.columns)
+ raise Exception('Error in Filt2Full')
+ return self.columns[self.Filt2Full]
+
+ def setReadOnly(self, tabLabel='', cols=[]):
+ """ Set this list of columns as readonly and non selectable """
+ self.tab=None
+ self.bReadOnly=True
+ self.lb.SetLabel(tabLabel)
+ self.setColumns(columnNames=cols)
+ self.setGUIColumns()
+ self.lbColumns.Enable(True)
+ self.comboX.Enable(False)
+ self.lbColumns.SetSelection(-1)
+ self.bt.Enable(False)
+ self.bShowID=False
+ self.btClear.Enable(False)
+ self.btFilter.Enable(False)
+ self.tFilter.Enable(False)
+ self.tFilter.SetValue('')
+
+ def setTab(self,tab=None,xSel=-1,ySel=[],colNames=None, tabLabel=''):
+ """ Set the table used for the columns, update the GUI """
+ self.tab=tab;
+ self.lbColumns.Enable(True)
+ self.comboX.Enable(True)
+ self.bReadOnly=False
+ self.btClear.Enable(True)
+ self.btFilter.Enable(True)
+ self.tFilter.Enable(True)
+ self.bt.Enable(True)
+ if tab is not None:
+ self.Filt2Full=None # TODO
+ if tab.active_name!='default':
+ self.lb.SetLabel(' '+tab.active_name)
+ self.setColumns()
+ self.setGUIColumns(xSel=xSel, ySel=ySel)
+ else:
+ self.Filt2Full=None # TODO Decide whether filter should be applied...
+ self.lb.SetLabel(tabLabel)
+ self.setColumns(columnNames=colNames)
+ self.setGUIColumns(xSel=xSel, ySel=ySel)
+
+ def updateColumn(self,i,newName):
+ """ Update of one column name
+ i: index in GUI
+ """
+ iFull = self.Filt2Full[i]
+ if self.bShowID:
+ newName='{:03d} '.format(iFull)+newName
+ self.lbColumns.SetString(i,newName)
+ self.comboX.SetString (i,newName)
+ self.columns[iFull] = newName
+
+ def Full2Filt(self,iFull):
+ try:
+ return self.Filt2Full.index(iFull)
+ except:
+ return -1
+
+ def setColumns(self, columnNames=None):
+ # Get columns from user inputs, or table, or stored.
+ if columnNames is not None:
+ # Populating based on user inputs..
+ columns=columnNames
+ elif self.tab is None:
+ columns=self.columns
+ else:
+ # Populating based on table (safest if table was updated)
+ columns=['Index']+self.tab.columns
+ # Storing columns, considered as "Full"
+ self.columns=np.array(columns)
+
+ def setGUIColumns(self, xSel=-1, ySel=[]):
+ """ Set GUI columns based on self.columns and potential filter """
+ # Filtering columns if neeed
+ sFilt = self.tFilter.GetLineText(0).strip()
+ if len(sFilt)>0:
+ Lf, If = filter_list(self.columns, sFilt)
+ self.Filt2Full = If
+ else:
+ self.Filt2Full = list(np.arange(len(self.columns)))
+ columns=self.columns[self.Filt2Full]
+
+ # GUI update
+ self.Freeze()
+ if self.bShowID:
+ columnsY= ['{:03d} '.format(i)+c for i,c in enumerate(columns)]
+ columnsX= ['{:03d} '.format(i)+c for i,c in enumerate(self.columns)]
+ else:
+ columnsY= columns
+ columnsX= self.columns
+ self.lbColumns.Set(columnsY) # potentially filterd
+ # Slow line for many columns
+ # NOTE: limiting to 300 for now.. I'm not sure anywant would want to scroll more than that
+ # Consider adding a "more button"
+ # see e.g. https://comp.soft-sys.wxwindows.narkive.com/gDfA1Ds5/long-load-time-in-wxpython
+ self.comboX.Set(columnsX[:300]) # non filtered
+
+ # Set selection for y, if any, and considering filtering
+ for iFull in ySel:
+ if iFull=0:
+ iFilt = self.Full2Filt(iFull)
+ if iFilt>0:
+ self.lbColumns.SetSelection(iFilt)
+ self.lbColumns.EnsureVisible(iFilt)
+ if len(self.lbColumns.GetSelections())<=0:
+ self.lbColumns.SetSelection(self.getDefaultColumnY(self.tab,len(columnsY)-1))
+
+ # Set selection for x, if any, NOTE x is not filtered!
+ if (xSel<0) or xSel>len(columnsX):
+ self.comboX.SetSelection(self.getDefaultColumnX(self.tab,len(columnsX)-1))
+ else:
+ self.comboX.SetSelection(xSel)
+ self.Thaw()
+
+ def forceOneSelection(self):
+ ISel=self.lbColumns.GetSelections()
+ self.lbColumns.SetSelection(-1)
+ if len(ISel)>0:
+ self.lbColumns.SetSelection(ISel[0])
+
+ def forceZeroSelection(self):
+ self.lbColumns.SetSelection(-1)
+
+ def empty(self):
+ self.lbColumns.Clear()
+ self.comboX.Clear()
+ self.lb.SetLabel('')
+ self.bReadOnly=False
+ self.lbColumns.Enable(False)
+ self.comboX.Enable(False)
+ self.bt.Enable(False)
+ self.tab=None
+ self.columns=[]
+ self.Filt2Full=None
+ self.btClear.Enable(False)
+ self.btFilter.Enable(False)
+ self.tFilter.Enable(False)
+ self.tFilter.SetValue('')
+
+ def getColumnSelection(self):
+ iX = self.comboX.GetSelection()
+ if self.bShowID:
+ sX = self.comboX.GetStringSelection()[4:]
+ else:
+ sX = self.comboX.GetStringSelection()
+ IY = self.lbColumns.GetSelections()
+ if self.bShowID:
+ SY = [self.lbColumns.GetString(i)[4:] for i in IY]
+ else:
+ SY = [self.lbColumns.GetString(i) for i in IY]
+ iXFull = iX # NOTE: x is always in full
+ IYFull = [self.Filt2Full[iY] for iY in IY]
+ return iXFull,IYFull,sX,SY
+
+ def onClearFilter(self, event=None):
+ self.tFilter.SetValue('')
+ self.onFilterChange()
+
+ def onFilterChange(self, event=None):
+ xSel,ySel,_,_ = self.getColumnSelection() # (indices in full)
+ self.setGUIColumns(xSel=xSel, ySel=ySel) # <<< Filtering done here
+ self.triggerPlot() # Trigger a col selection event
+
+ def onFilterKey(self, event=None):
+ s=GetKeyString(event)
+ if s=='ESCAPE' or s=='Ctrl+C':
+ self.onClearFilter()
+ event.Skip()
+
+ def triggerPlot(self):
+ event=wx.PyCommandEvent(wx.EVT_LISTBOX.typeId, self.lbColumns.GetId())
+ wx.PostEvent(self.GetEventHandler(), event)
+
+
+# --------------------------------------------------------------------------------}
+# --- Selection Panel
+# --------------------------------------------------------------------------------{
+class SelectionPanel(wx.Panel):
+ """ Display options for the user to select data """
+ def __init__(self, parent, tabList, mode='auto',mainframe=None):
+ # Superclass constructor
+ super(SelectionPanel,self).__init__(parent)
+ # DATA
+ self.mainframe = mainframe
+ self.tabList = None
+ self.itabForCol = None
+ self.parent = parent
+ self.tabSelections = {}
+ self.tabSelected = [] # NOTE only used to remember a selection after a reload
+ self.modeRequested = mode
+ self.currentMode = None
+ self.nSplits = -1
+
+ # GUI DATA
+ self.splitter = MultiSplit(self, style=wx.SP_LIVE_UPDATE)
+ self.splitter.SetMinimumPaneSize(70)
+ self.tabPanel = TablePanel (self.splitter,mainframe, tabList)
+ self.colPanel1 = ColumnPanel(self.splitter, self, mainframe);
+ self.colPanel2 = ColumnPanel(self.splitter, self, mainframe);
+ self.colPanel3 = ColumnPanel(self.splitter, self, mainframe);
+ self.tabPanel.Hide()
+ self.colPanel1.Hide()
+ self.colPanel2.Hide()
+ self.colPanel3.Hide()
+
+ # Layout
+ self.updateLayout()
+ VertSizer = wx.BoxSizer(wx.VERTICAL)
+ VertSizer.Add(self.splitter, 2, flag=wx.EXPAND, border=0)
+ self.SetSizer(VertSizer)
+
+ # TRIGGERS
+ self.setTables(tabList)
+
+ def updateLayout(self,mode=None):
+ self.Freeze()
+ if mode is None:
+ mode=self.modeRequested
+ else:
+ self.modeRequested = mode
+ if mode=='auto':
+ self.autoMode()
+ elif mode=='sameColumnsMode':
+ self.sameColumnsMode()
+ elif mode=='simColumnsMode':
+ self.simColumnsMode()
+ elif mode=='twoColumnsMode':
+ self.twoColumnsMode()
+ elif mode=='threeColumnsMode':
+ self.threeColumnsMode()
+ else:
+ self.Thaw()
+ raise Exception('Wrong mode for selection layout: {}'.format(mode))
+ self.Thaw()
+
+
+ def autoMode(self):
+ ISel=self.tabPanel.lbTab.GetSelections()
+ if self.tabList is not None:
+ if self.tabList.len()<=0:
+ self.nSplits=-1
+ self.splitter.removeAll()
+ elif self.tabList.haveSameColumns():
+ self.sameColumnsMode()
+ elif self.tabList.haveSameColumns(ISel):
+ # We don't do same column because we know at least one table is different
+ # to avoid "jumping" too much
+ self.twoColumnsMode()
+ else:
+ # See if tables are quite similar
+ IKeepPerTab, IMissPerTab, IDuplPerTab, nCols = getTabCommonColIndices([self.tabList.get(i) for i in ISel])
+ if np.all(np.array([len(I) for I in IMissPerTab]))=2):
+ self.simColumnsMode()
+ elif len(ISel)==2:
+ self.twoColumnsMode()
+ elif len(ISel)==3:
+ self.threeColumnsMode()
+ else:
+ #self.simColumnsMode(self)
+ raise Exception('Too many panels selected with significant columns differences.')
+
+ def sameColumnsMode(self):
+ self.currentMode = 'sameColumnsMode'
+ if self.nSplits==1:
+ return
+ if self.nSplits==0 and self.tabList.len()<=1:
+ return
+ self.splitter.removeAll()
+ if self.tabList is not None:
+ if self.tabList.len()>1:
+ self.splitter.AppendWindow(self.tabPanel)
+ self.splitter.AppendWindow(self.colPanel1)
+ if self.mainframe is not None:
+ self.mainframe.mainFrameUpdateLayout()
+ if self.tabList is not None:
+ if self.tabList.len()<=1:
+ self.nSplits=0
+ else:
+ self.nSplits=1
+ else:
+ self.nSplits=0
+
+ def simColumnsMode(self):
+ self.currentMode = 'simColumnsMode'
+ self.splitter.removeAll()
+ self.splitter.AppendWindow(self.tabPanel)
+ self.splitter.AppendWindow(self.colPanel2)
+ self.splitter.AppendWindow(self.colPanel1)
+ self.splitter.setEquiSash()
+ if self.nSplits<2 and self.mainframe is not None:
+ self.mainframe.mainFrameUpdateLayout()
+ self.nSplits=2
+
+ def twoColumnsMode(self):
+ self.currentMode = 'twoColumnsMode'
+ if self.nSplits==2:
+ return
+ self.splitter.removeAll()
+ self.splitter.AppendWindow(self.tabPanel)
+ self.splitter.AppendWindow(self.colPanel2)
+ self.splitter.AppendWindow(self.colPanel1)
+ self.splitter.setEquiSash()
+ if self.nSplits<2 and self.mainframe is not None:
+ self.mainframe.mainFrameUpdateLayout()
+ self.nSplits=2
+
+ def threeColumnsMode(self):
+ self.currentMode = 'threeColumnsMode'
+ if self.nSplits==3:
+ return
+ self.splitter.removeAll()
+ self.splitter.AppendWindow(self.tabPanel)
+ self.splitter.AppendWindow(self.colPanel3)
+ self.splitter.AppendWindow(self.colPanel2)
+ self.splitter.AppendWindow(self.colPanel1)
+ self.splitter.setEquiSash()
+ if self.mainframe is not None:
+ self.mainframe.mainFrameUpdateLayout()
+ self.nSplits=3
+
+ def setTables(self,tabList,update=False):
+ """ Set the list of tables. Keeping the selection if it's an update """
+ # TODO PUT ME IN TABLE PANEL
+ # Find a better way to remember selection
+ #print('UPDATING TABLES')
+ # Emptying GUI - TODO only if needed
+ self.colPanel1.empty()
+ self.colPanel2.empty()
+ self.colPanel3.empty()
+ # Adding
+ self.tabList = tabList
+ self.tabPanel.tabList = self.tabList
+ tabnames = self.tabList.tabNames
+ self.tabPanel.updateTabNames()
+ for tn in tabnames:
+ if tn not in self.tabSelections.keys():
+ self.tabSelections[tn]={'xSel':-1,'ySel':[]}
+ else:
+ pass # do nothing
+
+ # Reselecting
+ if len(self.tabSelected)>0:
+ # Removed line below since two column mode implemented
+ #if not haveSameColumns(tabs,ISel):
+ # ISel=[ISel[0]]
+ for i in self.tabSelected:
+ if i0:
+ # Trigger - updating columns and layout
+ ISel=self.tabPanel.lbTab.GetSelections()
+ self.tabSelected=ISel
+ if self.currentMode=='simColumnsMode':
+ self.setColForSimTab(ISel)
+ else:
+ if len(ISel)==1:
+ self.setTabForCol(ISel[0],1)
+ elif len(ISel)==2:
+ self.setTabForCol(ISel[0],1)
+ self.setTabForCol(ISel[1],2)
+ elif len(ISel)==3:
+ self.setTabForCol(ISel[0],1)
+ self.setTabForCol(ISel[1],2)
+ self.setTabForCol(ISel[2],3)
+ else: # Likely all tables have the same columns
+ self.setTabForCol(ISel[0],1)
+ self.updateLayout(self.modeRequested)
+
+ def setTabForCol(self,iTabSel,iPanel):
+ t = self.tabList.get(iTabSel)
+ ts = self.tabSelections[t.name]
+ if iPanel==1:
+ self.colPanel1.setTab(t,ts['xSel'],ts['ySel'])
+ elif iPanel==2:
+ self.colPanel2.setTab(t,ts['xSel'],ts['ySel'])
+ elif iPanel==3:
+ self.colPanel3.setTab(t,ts['xSel'],ts['ySel'])
+ else:
+ raise Exception('Wrong ipanel')
+
+ def setColForSimTab(self,ISel):
+ """ Set column panels for similar tables """
+ tabs = [self.tabList.get(i) for i in ISel]
+ IKeepPerTab, IMissPerTab, IDuplPerTab, _ = getTabCommonColIndices(tabs)
+ LenMiss = np.array([len(I) for I in IMissPerTab])
+ LenKeep = np.array([len(I) for I in IKeepPerTab])
+ LenDupl = np.array([len(I) for I in IDuplPerTab])
+
+ ColInfo = ['Sim. table mode ']
+ ColInfo += ['']
+ if self.tabList.haveSameColumns(ISel):
+ if len(ISel)>1:
+ ColInfo += ['Columns identical','']
+ else:
+ if (np.all(np.array(LenMiss)==0)):
+ ColInfo += ['Columns identical']
+ ColInfo += ['Order different!']
+
+ ColInfo += ['','First difference:']
+ ColInfo.append('----------------------------------')
+ bFirst=True
+ for it,t in enumerate(tabs):
+ print('IKeep',IKeepPerTab[it])
+ if it==0:
+ continue
+ INotOrdered=[ii for i,ii in enumerate(IKeepPerTab[it]) if ii!=IKeepPerTab[0][i]]
+ print('INot',INotOrdered)
+ if len(INotOrdered)>0:
+ im=INotOrdered[0]
+ if bFirst:
+ ColInfo.append('{}:'.format(tabs[0].active_name))
+ ColInfo.append('{:03d} {:s}'.format(im, tabs[0].columns[im]))
+ bFirst=False
+ ColInfo.append('{}:'.format(t.active_name))
+ ColInfo.append('{:03d} {:s}'.format(im, t.columns[im]))
+ ColInfo.append('----------------------------------')
+
+ else:
+ ColInfo += ['Columns different!']
+ ColInfo += ['(similar: {})'.format(LenKeep[0])]
+ ColInfo += ['','Missing columns:']
+ ColInfo.append('----------------------------------')
+ for it,t in enumerate(tabs):
+ ColInfo.append('{}:'.format(t.active_name))
+ if len(IMissPerTab[it])==0:
+ ColInfo.append(' (None) ')
+ for im in IMissPerTab[it]:
+ ColInfo.append('{:03d} {:s}'.format(im, t.columns[im]))
+ ColInfo.append('----------------------------------')
+
+ if (np.any(np.array(LenDupl)>0)):
+ if len(ISel)>1:
+ ColInfo += ['','Common duplicates:']
+ else:
+ ColInfo += ['','Duplicates:']
+ ColInfo.append('----------------------------------')
+ for it,t in enumerate(tabs):
+ ColInfo.append('{}:'.format(t.active_name))
+ if len(IDuplPerTab[it])==0:
+ ColInfo.append(' (None) ')
+ for im in IDuplPerTab[it]:
+ ColInfo.append('{:03d} {:s}'.format(im, t.columns[im]))
+ ColInfo.append('----------------------------------')
+
+
+ colNames = ['Index'] + [tabs[0].columns[i] for i in IKeepPerTab[0]]
+ self.colPanel1.setTab(tab=None, colNames=colNames, tabLabel=' Tab. Intersection')
+ self.colPanel2.setReadOnly(' Tab. Difference', ColInfo)
+ self.IKeepPerTab=IKeepPerTab
+
+
+
+ def selectDefaultTable(self):
+ # Selecting the first table
+ if self.tabPanel.lbTab.GetCount()>0:
+ self.tabPanel.lbTab.SetSelection(0)
+ self.tabSelected=[0]
+ else:
+ self.tabSelected=[]
+
+ def tabSelectionChanged(self):
+ # TODO This can be cleaned-up and merged with updateLayout
+ #print('Tab selection change')
+ # Storing the previous selection
+ #self.printSelection()
+ self.saveSelection() #
+ #self.printSelection()
+ ISel=self.tabPanel.lbTab.GetSelections()
+ if len(ISel)>0:
+ if self.modeRequested=='auto':
+ self.autoMode()
+ if self.currentMode=='simColumnsMode':# and len(ISel)>1:
+ self.setColForSimTab(ISel)
+ self.tabSelected=self.tabPanel.lbTab.GetSelections()
+ return
+
+ if self.tabList.haveSameColumns(ISel):
+ # Setting tab
+ self.setTabForCol(ISel[0],1)
+ self.colPanel2.empty()
+ self.colPanel3.empty()
+ else:
+ if self.nSplits==2:
+ if len(ISel)>2:
+ Error(self,'In this mode, only two tables can be selected. To compare three tables, uses the "3 different tables" mode. Otherwise the tables need to have the same columns.')
+ ISel=ISel[0:2]
+ self.tabPanel.lbTab.SetSelection(wx.NOT_FOUND)
+ for isel in ISel:
+ self.tabPanel.lbTab.SetSelection(isel)
+ self.colPanel3.empty()
+ elif self.nSplits==3:
+ if len(ISel)>3:
+ Error(self,'In this mode, only three tables can be selected. To compare more than three tables, the tables need to have the same columns.')
+ ISel=ISel[0:3]
+ self.tabPanel.lbTab.SetSelection(wx.NOT_FOUND)
+ for isel in ISel:
+ self.tabPanel.lbTab.SetSelection(isel)
+ else:
+ Error(self,'The tables selected have different columns.\n\nThis is not compatible with the "Same tables" mode. To compare them, chose one of the following mode: "2 tables", "3 tables" or "Sim. tables".')
+ self.colPanel2.empty()
+ self.colPanel3.empty()
+ # unselect all and select only the first one
+ ISel=[ISel[0]]
+ self.tabPanel.lbTab.SetSelection(wx.NOT_FOUND)
+ self.tabPanel.lbTab.SetSelection(ISel[0])
+ for iPanel,iTab in enumerate(ISel):
+ self.setTabForCol(iTab,iPanel+1)
+ #print('>>>Updating tabSelected, from',self.tabSelected,'to',self.tabPanel.lbTab.GetSelections())
+ self.tabSelected=self.tabPanel.lbTab.GetSelections()
+
+ def colSelectionChanged(self):
+ """ Simple triggers when column selection is changed, NOTE: does not redraw """
+ if self.currentMode=='simColumnsMode':
+ self.colPanel2.forceZeroSelection()
+ else:
+ if self.nSplits in [2,3]:
+ ISel=self.tabPanel.lbTab.GetSelections()
+ if self.tabList.haveSameColumns(ISel):
+ pass # TODO: this test is identical to onTabSelectionChange. Unification.
+ # elif len(ISel)==2:
+ # self.colPanel1.forceOneSelection()
+ # self.colPanel2.forceOneSelection()
+ # elif len(ISel)==3:
+ # self.colPanel1.forceOneSelection()
+ # self.colPanel2.forceOneSelection()
+ # self.colPanel3.forceOneSelection()
+
+ def update_tabs(self, tabList):
+ self.setTables(tabList, update=True)
+
+ def renameTable(self,iTab, oldName, newName):
+ #self.printSelection()
+ self.tabSelections[newName] = self.tabSelections.pop(oldName)
+ self.tabPanel.updateTabNames()
+ #self.printSelection()
+
+ def saveSelection(self):
+ #self.ISel=self.tabPanel.lbTab.GetSelections()
+ ISel=self.tabSelected #
+ if self.tabList.haveSameColumns(ISel):
+ for ii in ISel:
+ t=self.tabList.get(ii)
+ self.tabSelections[t.name]['xSel'] = self.colPanel1.comboX.GetSelection()
+ self.tabSelections[t.name]['ySel'] = self.colPanel1.lbColumns.GetSelections()
+ else:
+ if len(ISel)>=1:
+ t=self.tabList.get(ISel[0])
+ self.tabSelections[t.name]['xSel'] = self.colPanel1.comboX.GetSelection()
+ self.tabSelections[t.name]['ySel'] = self.colPanel1.lbColumns.GetSelections()
+ if len(ISel)>=2:
+ t=self.tabList.get(ISel[1])
+ self.tabSelections[t.name]['xSel'] = self.colPanel2.comboX.GetSelection()
+ self.tabSelections[t.name]['ySel'] = self.colPanel2.lbColumns.GetSelections()
+ if len(ISel)>=3:
+ t=self.tabList.get(ISel[2])
+ self.tabSelections[t.name]['xSel'] = self.colPanel3.comboX.GetSelection()
+ self.tabSelections[t.name]['ySel'] = self.colPanel3.lbColumns.GetSelections()
+ self.tabSelected = self.tabPanel.lbTab.GetSelections();
+
+ def printSelection(self):
+ print('Number of tabSelections stored:',len(self.tabSelections))
+ TS=self.tabSelections
+ for i,tn in enumerate(self.tabList.tabNames):
+ if tn not in TS.keys():
+ print('Tab',i,'>>> Name {} not found in selection'.format(tn))
+ else:
+ print('Tab',i,'xSel:',TS[tn]['xSel'],'ySel:',TS[tn]['ySel'],'Name:',tn)
+
+ def getPlotDataSelection(self):
+ ID = []
+ SameCol=False
+ if self.tabList is not None and self.tabList.len()>0:
+ ITab,STab = self.getSelectedTables()
+ if self.currentMode=='simColumnsMode' and len(ITab)>1:
+ iiX1,IY1,ssX1,SY1 = self.colPanel1.getColumnSelection()
+ SameCol=False
+ for i,(itab,stab) in enumerate(zip(ITab,STab)):
+ IKeep=self.IKeepPerTab[i]
+ for j,(iiy,ssy) in enumerate(zip(IY1,SY1)):
+ if iiy==0:
+ iy = 0
+ sy = ssy
+ else:
+ iy = IKeep[iiy-1]+1
+ sy = self.tabList.get(itab).columns[IKeep[iiy-1]]
+ if iiX1==0:
+ iX1 = 0
+ sX1 = ssX1
+ else:
+ iX1 = IKeep[iiX1-1]+1
+ sX1 = self.tabList.get(itab).columns[IKeep[iiX1-1]]
+ ID.append([itab,iX1,iy,sX1,sy,stab])
+ else:
+ iX1,IY1,sX1,SY1 = self.colPanel1.getColumnSelection()
+ SameCol=self.tabList.haveSameColumns(ITab)
+ if self.nSplits in [0,1] or SameCol:
+ for i,(itab,stab) in enumerate(zip(ITab,STab)):
+ for j,(iy,sy) in enumerate(zip(IY1,SY1)):
+ ID.append([itab,iX1,iy,sX1,sy,stab])
+ elif self.nSplits in [2,3]:
+ if len(ITab)>=1:
+ for j,(iy,sy) in enumerate(zip(IY1,SY1)):
+ ID.append([ITab[0],iX1,iy,sX1,sy,STab[0]])
+ if len(ITab)>=2:
+ iX2,IY2,sX2,SY2 = self.colPanel2.getColumnSelection()
+ for j,(iy,sy) in enumerate(zip(IY2,SY2)):
+ ID.append([ITab[1],iX2,iy,sX2,sy,STab[1]])
+ if len(ITab)>=3:
+ iX2,IY2,sX2,SY2 = self.colPanel3.getColumnSelection()
+ for j,(iy,sy) in enumerate(zip(IY2,SY2)):
+ ID.append([ITab[2],iX2,iy,sX2,sy,STab[2]])
+ else:
+ raise Exception('Wrong number of splits {}'.format(self.nSplits))
+ return ID,SameCol,self.currentMode
+
+ def getSelectedTables(self):
+ I=self.tabPanel.lbTab.GetSelections()
+ S=[self.tabPanel.lbTab.GetString(i) for i in I]
+ return I,S
+
+ def getAllTables(self):
+ I=range(self.tabPanel.lbTab.GetCount())
+ S=[self.tabPanel.lbTab.GetString(i) for i in I]
+ return I,S
+
+ def clean_memory(self):
+ self.colPanel1.empty()
+ self.colPanel2.empty()
+ self.colPanel3.empty()
+ self.tabPanel.empty()
+ del self.tabList
+ self.tabList=None
+
+ @property
+ def xCol(self):
+ iX, _, sX, _ = self.colPanel1.getColumnSelection()
+ return iX,sX
+
+
+if __name__ == '__main__':
+ import pandas as pd;
+ from Tables import Table
+ import numpy as np
+
+ def OnTabPopup(event):
+ self.PopupMenu(TablePopup(self,selPanel.tabPanel.lbTab), event.GetPosition())
+
+ app = wx.App(False)
+ self=wx.Frame(None,-1,"Title")
+ tab=Table(data=pd.DataFrame(data={'ColA': np.random.normal(0,1,100)+1,'ColB':np.random.normal(0,1,100)+2}))
+ selPanel=SelectionPanel(self,[tab],mode='twoColumnsMode')
+ self.SetSize((800, 600))
+ self.Center()
+ self.Show()
+ selPanel.tabPanel.lbTab.Bind(wx.EVT_RIGHT_DOWN, OnTabPopup)
+
+
+ app.MainLoop()
+
diff --git a/pydatview/GUIToolBox.py b/pydatview/GUIToolBox.py
index 4606d36..a32141e 100644
--- a/pydatview/GUIToolBox.py
+++ b/pydatview/GUIToolBox.py
@@ -195,6 +195,11 @@ def _update(self,currentaxes=None):
class MyNavigationToolbar2Wx(NavigationToolbar2Wx):
+ """
+ Wrapped version of the Navigation toolbar from WX with the following features:
+ - Tools can be removed, if not in `keep_tools`
+ - Zoom is set by default, and the toggling between zoom and pan is handled internally
+ """
def __init__(self, canvas, keep_tools):
# Taken from matplotlib/backend_wx.py but added style:
self.VERSION = matplotlib.__version__
@@ -215,6 +220,8 @@ def __init__(self, canvas, keep_tools):
else:
NavigationToolbar2Wx.__init__(self, canvas)
+ self.pan_on=False
+
# Make sure we start in zoom mode
if 'Pan' in keep_tools:
self.zoom() # NOTE: #22 BREAK cursors #12!
@@ -225,32 +232,33 @@ def __init__(self, canvas, keep_tools):
if t.GetLabel() not in keep_tools:
self.DeleteToolByPos(i)
- def press_zoom(self, event):
- NavigationToolbar2Wx.press_zoom(self,event)
- #self.SetToolBitmapSize((22,22))
-
- def press_pan(self, event):
- NavigationToolbar2Wx.press_pan(self,event)
-
def zoom(self, *args):
- NavigationToolbar2Wx.zoom(self,*args)
+ # NEW - MPL>=3.0.0
+ if self.pan_on:
+ pass
+ else:
+ NavigationToolbar2.zoom(self,*args) # We skip wx and use the parent
+ # BEFORE
+ #NavigationToolbar2Wx.zoom(self,*args)
def pan(self, *args):
- try:
- #if self.VERSION[0]=='2' or self.VERSION[0]=='1':
- isPan = self._active=='PAN'
- except:
- try:
- from matplotlib.backend_bases import _Mode
- isPan = self.mode == _Mode.PAN
- except:
- raise Exception('Pan not found, report a pyDatView bug, with matplotlib version.')
- if isPan:
- NavigationToolbar2Wx.pan(self,*args)
+ self.pan_on=not self.pan_on
+ # NEW - MPL >= 3.0.0
+ NavigationToolbar2.pan(self, *args) # We skip wx and use to parent
+ if not self.pan_on:
self.zoom()
- else:
- NavigationToolbar2Wx.pan(self,*args)
-
+ # BEFORE
+ #try:
+ # isPan = self._active=='PAN'
+ #except:
+ # try:
+ # from matplotlib.backend_bases import _Mode
+ # isPan = self.mode == _Mode.PAN
+ # except:
+ # raise Exception('Pan not found, report a pyDatView bug, with matplotlib version.')
+ #NavigationToolbar2Wx.pan(self,*args)
+ #if isPan:
+ # self.zoom()
def home(self, *args):
"""Restore the original view."""
diff --git a/pydatview/GUITools.py b/pydatview/GUITools.py
index 86d2df9..3ff9d27 100644
--- a/pydatview/GUITools.py
+++ b/pydatview/GUITools.py
@@ -1,1071 +1,1145 @@
-from __future__ import absolute_import
-import wx
-import numpy as np
-import pandas as pd
-import copy
-import platform
-from collections import OrderedDict
-
-# For log dec tool
-from .common import CHAR, Error, pretty_num_short, Info
-from .plotdata import PlotData
-from pydatview.tools.damping import logDecFromDecay
-from pydatview.tools.curve_fitting import model_fit, extract_key_miscnum, extract_key_num, MODELS, FITTERS, set_common_keys
-
-
-TOOL_BORDER=15
-
-# --------------------------------------------------------------------------------}
-# --- Default class for tools
-# --------------------------------------------------------------------------------{
-class GUIToolPanel(wx.Panel):
- def __init__(self, parent):
- super(GUIToolPanel,self).__init__(parent)
- self.parent = parent
-
- def destroy(self,event=None):
- self.parent.removeTools()
-
- def getBtBitmap(self,par,label,Type=None,callback=None,bitmap=False):
- if Type is not None:
- label=CHAR[Type]+' '+label
-
- bt=wx.Button(par,wx.ID_ANY, label, style=wx.BU_EXACTFIT)
- #try:
- # if bitmap is not None:
- # bt.SetBitmapLabel(wx.ArtProvider.GetBitmap(bitmap)) #,size=(12,12)))
- # else:
- #except:
- # pass
- if callback is not None:
- par.Bind(wx.EVT_BUTTON, callback, bt)
- return bt
-
- def getToggleBtBitmap(self,par,label,Type=None,callback=None,bitmap=False):
- if Type is not None:
- label=CHAR[Type]+' '+label
- bt=wx.ToggleButton(par,wx.ID_ANY, label, style=wx.BU_EXACTFIT)
- if callback is not None:
- par.Bind(wx.EVT_TOGGLEBUTTON, callback, bt)
- return bt
-
-
-
-# --------------------------------------------------------------------------------}
-# --- Log Dec
-# --------------------------------------------------------------------------------{
-class LogDecToolPanel(GUIToolPanel):
- def __init__(self, parent):
- super(LogDecToolPanel,self).__init__(parent)
- btClose = self.getBtBitmap(self,'Close' ,'close' ,self.destroy )
- btComp = self.getBtBitmap(self,'Compute','compute',self.onCompute)
- self.lb = wx.StaticText( self, -1, ' ')
- self.sizer = wx.BoxSizer(wx.HORIZONTAL)
- self.sizer.Add(btClose ,0, flag = wx.LEFT|wx.CENTER,border = 1)
- self.sizer.Add(btComp ,0, flag = wx.LEFT|wx.CENTER,border = 5)
- self.sizer.Add(self.lb ,0, flag = wx.LEFT|wx.CENTER,border = 5)
- self.SetSizer(self.sizer)
-
- def onCompute(self,event=None):
- if len(self.parent.plotData)!=1:
- Error(self,'Log Dec tool only works with a single plot.')
- return
- pd =self.parent.plotData[0]
- try:
- logdec,DampingRatio,T,fn,fd,IPos,INeg,epos,eneg=logDecFromDecay(pd.y,pd.x)
- lab='LogDec.: {:.4f} - Damping ratio: {:.4f} - F_n: {:.4f} - F_d: {:.4f} - T:{:.3f}'.format(logdec,DampingRatio,fn,fd,T)
- self.lb.SetLabel(lab)
- self.sizer.Layout()
- ax=self.parent.fig.axes[0]
- ax.plot(pd.x[IPos],pd.y[IPos],'o')
- ax.plot(pd.x[INeg],pd.y[INeg],'o')
- ax.plot(pd.x ,epos,'k--')
- ax.plot(pd.x ,eneg,'k--')
- self.parent.canvas.draw()
- except:
- self.lb.SetLabel('Failed. The signal needs to look like the decay of a first order system.')
- #self.parent.load_and_draw(); # DATA HAS CHANGED
-
-# --------------------------------------------------------------------------------}
-# --- Outliers
-# --------------------------------------------------------------------------------{
-class OutlierToolPanel(GUIToolPanel):
- """
- A quick and dirty solution to manipulate plotData
- I need to think of a better way to do that
- """
- def __init__(self, parent):
- super(OutlierToolPanel,self).__init__(parent)
- self.parent = parent # parent is GUIPlotPanel
-
- # Setting default states to parent
- if 'RemoveOutliers' not in self.parent.plotDataOptions.keys():
- self.parent.plotDataOptions['RemoveOutliers']=False
- if 'OutliersMedianDeviation' not in self.parent.plotDataOptions.keys():
- self.parent.plotDataOptions['OutliersMedianDeviation']=5
-
- btClose = self.getBtBitmap(self,'Close','close',self.destroy)
- self.btComp = self.getToggleBtBitmap(self,'Apply','cloud',self.onToggleCompute)
-
- lb1 = wx.StaticText(self, -1, 'Median deviation:')
-# self.tMD = wx.TextCtrl(self, wx.ID_ANY,, size = (30,-1), style=wx.TE_PROCESS_ENTER)
- self.tMD = wx.SpinCtrlDouble(self, value='11', size=wx.Size(60,-1))
- self.tMD.SetValue(self.parent.plotDataOptions['OutliersMedianDeviation'])
- self.tMD.SetRange(0.0, 1000)
- self.tMD.SetIncrement(0.5)
-
- self.lb = wx.StaticText( self, -1, '')
- self.sizer = wx.BoxSizer(wx.HORIZONTAL)
- self.sizer.Add(btClose ,0,flag = wx.LEFT|wx.CENTER,border = 1)
- self.sizer.Add(self.btComp,0,flag = wx.LEFT|wx.CENTER,border = 5)
- self.sizer.Add(lb1 ,0,flag = wx.LEFT|wx.CENTER,border = 5)
- self.sizer.Add(self.tMD ,0,flag = wx.LEFT|wx.CENTER,border = 5)
- self.sizer.Add(self.lb ,0,flag = wx.LEFT|wx.CENTER,border = 5)
- self.SetSizer(self.sizer)
-
- self.Bind(wx.EVT_SPINCTRLDOUBLE, self.onMDChangeArrow, self.tMD)
- self.Bind(wx.EVT_TEXT_ENTER, self.onMDChangeEnter, self.tMD)
-
- if platform.system()=='Windows':
- # See issue https://github.com/wxWidgets/Phoenix/issues/1762
- self.spintxt = self.tMD.Children[0]
- assert isinstance(self.spintxt, wx.TextCtrl)
- self.spintxt.Bind(wx.EVT_CHAR_HOOK, self.onMDChangeChar)
-
- self.onToggleCompute(init=True)
-
- def destroy(self,event=None):
- self.parent.plotDataOptions['RemoveOutliers']=False
- super(OutlierToolPanel,self).destroy()
-
- def onToggleCompute(self,event=None, init=False):
- self.parent.plotDataOptions['OutliersMedianDeviation'] = float(self.tMD.Value)
-
- if not init:
- self.parent.plotDataOptions['RemoveOutliers']= not self.parent.plotDataOptions['RemoveOutliers']
-
- if self.parent.plotDataOptions['RemoveOutliers']:
- self.lb.SetLabel('Outliers are now removed on the fly. Click "Clear" to stop.')
- self.btComp.SetLabel(CHAR['sun']+' Clear')
- else:
- self.lb.SetLabel('Click on "Apply" to remove outliers on the fly for all new plot.')
- self.btComp.SetLabel(CHAR['cloud']+' Apply')
-
- if not init:
- self.parent.load_and_draw() # Data will change
-
- def onMDChange(self, event=None):
- #print(self.tMD.Value)
- self.parent.plotDataOptions['OutliersMedianDeviation'] = float(self.tMD.Value)
- if self.parent.plotDataOptions['RemoveOutliers']:
- self.parent.load_and_draw() # Data will change
-
- def onMDChangeArrow(self, event):
- self.onMDChange()
- event.Skip()
-
- def onMDChangeEnter(self, event):
- self.onMDChange()
- event.Skip()
-
- def onMDChangeChar(self, event):
- event.Skip()
- code = event.GetKeyCode()
- if code in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER]:
- #print(self.spintxt.Value)
- self.tMD.SetValue(self.spintxt.Value)
- self.onMDChangeEnter(event)
-
-
-
-# --------------------------------------------------------------------------------}
-# --- Moving Average
-# --------------------------------------------------------------------------------{
-class FilterToolPanel(GUIToolPanel):
- """
- Moving average/Filters
- A quick and dirty solution to manipulate plotData
- I need to think of a better way to do that
- """
- def __init__(self, parent):
- from pydatview.tools.signal import FILTERS
- super(FilterToolPanel,self).__init__(parent)
- self.parent = parent # parent is GUIPlotPanel
-
- self._DEFAULT_FILTERS=FILTERS
-
- # Setting default states to parent
- if 'Filter' not in self.parent.plotDataOptions.keys():
- self.parent.plotDataOptions['Filter']=None
- self._filterApplied = type(self.parent.plotDataOptions['Filter'])==dict
-
-
- btClose = self.getBtBitmap(self,'Close','close',self.destroy)
- self.btClear = self.getBtBitmap(self, 'Clear Plot','sun' , self.onClear)
- self.btComp = self.getToggleBtBitmap(self,'Apply','cloud',self.onToggleCompute)
- self.btPlot = self.getBtBitmap(self, 'Plot' ,'chart' , self.onPlot)
-
- lb1 = wx.StaticText(self, -1, 'Filter:')
- self.cbFilters = wx.ComboBox(self, choices=[filt['name'] for filt in self._DEFAULT_FILTERS], style=wx.CB_READONLY)
- self.lbParamName = wx.StaticText(self, -1, ' :')
- self.cbFilters.SetSelection(0)
- #self.tParam = wx.TextCtrl(self, wx.ID_ANY,, size = (30,-1), style=wx.TE_PROCESS_ENTER)
- self.tParam = wx.SpinCtrlDouble(self, value='11', size=wx.Size(60,-1))
- self.lbInfo = wx.StaticText( self, -1, '')
-
-
- # --- Layout
- btSizer = wx.FlexGridSizer(rows=3, cols=2, hgap=2, vgap=0)
- btSizer.Add(btClose ,0,flag = wx.ALL|wx.EXPAND, border = 1)
- btSizer.Add(self.btClear ,0,flag = wx.ALL|wx.EXPAND, border = 1)
- btSizer.Add(self.btComp,0,flag = wx.ALL|wx.EXPAND, border = 1)
- btSizer.Add(self.btPlot ,0,flag = wx.ALL|wx.EXPAND, border = 1)
- #btSizer.Add(btHelp ,0,flag = wx.ALL|wx.EXPAND, border = 1)
-
- horzSizer = wx.BoxSizer(wx.HORIZONTAL)
- horzSizer.Add(lb1 ,0,flag = wx.LEFT|wx.CENTER,border = 5)
- horzSizer.Add(self.cbFilters ,0,flag = wx.LEFT|wx.CENTER,border = 1)
- horzSizer.Add(self.lbParamName ,0,flag = wx.LEFT|wx.CENTER,border = 5)
- horzSizer.Add(self.tParam ,0,flag = wx.LEFT|wx.CENTER,border = 1)
-
- vertSizer = wx.BoxSizer(wx.VERTICAL)
- vertSizer.Add(self.lbInfo ,0, flag = wx.LEFT ,border = 5)
- vertSizer.Add(horzSizer ,1, flag = wx.LEFT|wx.EXPAND,border = 1)
-
- self.sizer = wx.BoxSizer(wx.HORIZONTAL)
- self.sizer.Add(btSizer ,0, flag = wx.LEFT ,border = 1)
- self.sizer.Add(vertSizer ,1, flag = wx.EXPAND|wx.LEFT ,border = 1)
- self.SetSizer(self.sizer)
-
- # --- Events
- self.cbFilters.Bind(wx.EVT_COMBOBOX, self.onSelectFilt)
- self.Bind(wx.EVT_SPINCTRLDOUBLE, self.onParamChangeArrow, self.tParam)
- self.Bind(wx.EVT_TEXT_ENTER, self.onParamChangeEnter, self.tParam)
- if platform.system()=='Windows':
- # See issue https://github.com/wxWidgets/Phoenix/issues/1762
- self.spintxt = self.tParam.Children[0]
- assert isinstance(self.spintxt, wx.TextCtrl)
- self.spintxt.Bind(wx.EVT_CHAR_HOOK, self.onParamChangeChar)
-
- self.onSelectFilt()
- self.onToggleCompute(init=True)
-
- def destroy(self,event=None):
- self.parent.plotDataOptions['Filter']=None
- super(FilterToolPanel,self).destroy()
-
-
- def onSelectFilt(self, event=None):
- """ Select the filter, but does not applied it to the plotData
- parentFilt is unchanged
- But if the parent already has
- """
- iFilt = self.cbFilters.GetSelection()
- filt = self._DEFAULT_FILTERS[iFilt]
- self.lbParamName.SetLabel(filt['paramName']+':')
- self.tParam.SetRange(filt['paramRange'][0], filt['paramRange'][1])
- self.tParam.SetIncrement(filt['increment'])
-
- parentFilt=self.parent.plotDataOptions['Filter']
- # Value
- if type(parentFilt)==dict and parentFilt['name']==filt['name']:
- self.tParam.SetValue(parentFilt['param'])
- else:
- self.tParam.SetValue(filt['param'])
-
- def onToggleCompute(self, event=None, init=False):
- """
- apply Filter based on GUI Data
- """
- parentFilt=self.parent.plotDataOptions['Filter']
- if not init:
- self._filterApplied = not self._filterApplied
-
- if self._filterApplied:
- self.parent.plotDataOptions['Filter'] =self._GUI2Filt()
- #print('Apply', self.parent.plotDataOptions['Filter'])
- self.lbInfo.SetLabel(
- 'Filter is now applied on the fly. Change parameter live. Click "Clear" to stop. '
- )
- self.btPlot.Enable(False)
- self.btClear.Enable(False)
- self.btComp.SetLabel(CHAR['sun']+' Clear')
- else:
- self.parent.plotDataOptions['Filter'] = None
- self.lbInfo.SetLabel(
- 'Click on "Apply" to set filter on the fly for all plots. '+
- 'Click on "Plot" to try a filter on the current plot.'
- )
- self.btPlot.Enable(True)
- self.btClear.Enable(True)
- self.btComp.SetLabel(CHAR['cloud']+' Apply')
-
- if not init:
- self.parent.load_and_draw() # Data will change
-
- pass
-
- def _GUI2Filt(self):
- iFilt = self.cbFilters.GetSelection()
- filt = self._DEFAULT_FILTERS[iFilt].copy()
- filt['param']=np.float(self.spintxt.Value)
- return filt
-
- def onPlot(self, event=None):
- """
- Overlay on current axis the filter
- """
- from pydatview.tools.signal import applyFilter
- if len(self.parent.plotData)!=1:
- Error(self,'Plotting only works for a single plot. Plot less data.')
- return
- filt=self._GUI2Filt()
-
- PD = self.parent.plotData[0]
- y_filt = applyFilter(PD.x0, PD.y0, filt)
- ax = self.parent.fig.axes[0]
-
- PD_new = PlotData()
- PD_new.fromXY(PD.x0, y_filt)
- self.parent.transformPlotData(PD_new)
- ax.plot(PD_new.x, PD_new.y, '-')
- self.parent.canvas.draw()
-
- def onClear(self, event):
- self.parent.load_and_draw() # Data will change
-
- def onParamChange(self, event=None):
- if self._filterApplied:
- self.parent.plotDataOptions['Filter'] =self._GUI2Filt()
- #print('OnParamChange', self.parent.plotDataOptions['Filter'])
- self.parent.load_and_draw() # Data will change
-
- def onParamChangeArrow(self, event):
- self.onParamChange()
- event.Skip()
-
- def onParamChangeEnter(self, event):
- self.onParamChange()
- event.Skip()
-
- def onParamChangeChar(self, event):
- event.Skip()
- code = event.GetKeyCode()
- if code in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER]:
- #print(self.spintxt.Value)
- self.tParam.SetValue(self.spintxt.Value)
- self.onParamChangeEnter(event)
-
-
-# --------------------------------------------------------------------------------}
-# --- Resample
-# --------------------------------------------------------------------------------{
-class ResampleToolPanel(GUIToolPanel):
- def __init__(self, parent):
- super(ResampleToolPanel,self).__init__(parent)
-
- # --- Data from other modules
- from pydatview.tools.signal import SAMPLERS
- self.parent = parent # parent is GUIPlotPanel
- self._SAMPLERS=SAMPLERS
- # Setting default states to parent
- if 'Sampler' not in self.parent.plotDataOptions.keys():
- self.parent.plotDataOptions['Sampler']=None
- self._applied = type(self.parent.plotDataOptions['Sampler'])==dict
-
-
- # --- GUI elements
- self.btClose = self.getBtBitmap(self, 'Close','close', self.destroy)
- self.btAdd = self.getBtBitmap(self, 'Add','add' , self.onAdd)
- self.btPlot = self.getBtBitmap(self, 'Plot' ,'chart' , self.onPlot)
- self.btClear = self.getBtBitmap(self, 'Clear Plot','sun', self.onClear)
- self.btApply = self.getToggleBtBitmap(self,'Apply','cloud',self.onToggleApply)
- self.btHelp = self.getBtBitmap(self, 'Help','help', self.onHelp)
-
- #self.lb = wx.StaticText( self, -1, """ Click help """)
- self.cbTabs = wx.ComboBox(self, -1, choices=[], style=wx.CB_READONLY)
- self.cbMethods = wx.ComboBox(self, -1, choices=[s['name'] for s in self._SAMPLERS], style=wx.CB_READONLY)
-
- self.lbNewX = wx.StaticText(self, -1, 'New x: ')
- self.textNewX = wx.TextCtrl(self, wx.ID_ANY, '', style = wx.TE_PROCESS_ENTER)
- self.textOldX = wx.TextCtrl(self, wx.ID_ANY|wx.TE_READONLY)
- self.textOldX.Enable(False)
-
- # --- Layout
- btSizer = wx.FlexGridSizer(rows=3, cols=2, hgap=2, vgap=0)
- btSizer.Add(self.btClose , 0, flag = wx.ALL|wx.EXPAND, border = 1)
- btSizer.Add(self.btClear , 0, flag = wx.ALL|wx.EXPAND, border = 1)
- btSizer.Add(self.btAdd , 0, flag = wx.ALL|wx.EXPAND, border = 1)
- btSizer.Add(self.btPlot , 0, flag = wx.ALL|wx.EXPAND, border = 1)
- btSizer.Add(self.btHelp , 0, flag = wx.ALL|wx.EXPAND, border = 1)
- btSizer.Add(self.btApply , 0, flag = wx.ALL|wx.EXPAND, border = 1)
-
- msizer = wx.FlexGridSizer(rows=2, cols=4, hgap=2, vgap=0)
- msizer.Add(wx.StaticText(self, -1, 'Table:') , 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM, 1)
- msizer.Add(self.cbTabs , 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM, 1)
- msizer.Add(wx.StaticText(self, -1, 'Current x: '), 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM, 1)
- msizer.Add(self.textOldX , 1, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND, 1)
- msizer.Add(wx.StaticText(self, -1, 'Method:') , 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM, 1)
- msizer.Add(self.cbMethods , 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM, 1)
- msizer.Add(self.lbNewX , 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM, 1)
- msizer.Add(self.textNewX , 1, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND, 1)
- msizer.AddGrowableCol(3,1)
-
- self.sizer = wx.BoxSizer(wx.HORIZONTAL)
- self.sizer.Add(btSizer ,0, flag = wx.LEFT ,border = 5)
- self.sizer.Add(msizer ,1, flag = wx.LEFT|wx.EXPAND ,border = TOOL_BORDER)
- self.SetSizer(self.sizer)
-
- # --- Events
- self.cbTabs.Bind (wx.EVT_COMBOBOX, self.onTabChange)
- self.cbMethods.Bind(wx.EVT_COMBOBOX, self.onMethodChange)
- self.textNewX.Bind(wx.EVT_TEXT_ENTER,self.onParamChange)
-
- # --- Init triggers
- self.cbMethods.SetSelection(3)
- self.onMethodChange(init=True)
- self.onToggleApply(init=True)
- self.updateTabList()
- self.textNewX.SetValue('2')
-
- def setCurrentX(self, x=None):
- if x is None:
- x= self.parent.plotData[0].x
- if len(x)<50:
- s=np.array2string(x, separator=', ')
- else:
- s =np.array2string(x[[0,1,2,3]], separator=', ')
- s+=', ..., '
- s+=np.array2string(x[[-3,-2,-1]], separator=', ')
- s=s.replace('[','').replace(']','').replace(' ','').replace(',',', ')
-
- self.textOldX.SetValue(s)
-
- def onMethodChange(self, event=None, init=True):
- """ Select the method, but does not applied it to the plotData
- User data and option is unchanged
- But if the user already has some options, they are used
- """
- iOpt = self.cbMethods.GetSelection()
- opt = self._SAMPLERS[iOpt]
- self.lbNewX.SetLabel(opt['paramName']+':')
-
- parentOpt=self.parent.plotDataOptions['Sampler']
- # Value
- if len(self.textNewX.Value)==0:
- if type(parentOpt)==dict:
- self.textNewX.SetValue(str(parentOpt['param'])[1:-1])
- else:
- self.textNewX.SetValue(str(opt['param'])[2:-2])
- self.onParamChange()
-
- def onParamChange(self, event=None):
- if self._applied:
- self.parent.plotDataOptions['Sampler'] =self._GUI2Data()
- self.parent.load_and_draw() # Data will change
- self.setCurrentX()
-
- def _GUI2Data(self):
- iOpt = self.cbMethods.GetSelection()
- opt = self._SAMPLERS[iOpt].copy()
- s= self.textNewX.Value.strip().replace('[','').replace(']','')
- if len(s)>0:
- if s.find(','):
- opt['param']=np.array(s.split(',')).astype(float)
- else:
- opt['param']=np.array(s.split('')).astype(float)
- return opt
-
- def onToggleApply(self, event=None, init=False):
- """
- apply sampler based on GUI Data
- """
- parentFilt=self.parent.plotDataOptions['Sampler']
- if not init:
- self._applied = not self._applied
-
- if self._applied:
- self.parent.plotDataOptions['Sampler'] =self._GUI2Data()
- #print('Apply', self.parent.plotDataOptions['Sampler'])
- #self.lbInfo.SetLabel(
- # 'Sampler is now applied on the fly. Change parameter live. Click "Clear" to stop. '
- # )
- self.btPlot.Enable(False)
- self.btClear.Enable(False)
- self.btApply.SetLabel(CHAR['sun']+' Clear')
- else:
- self.parent.plotDataOptions['Sampler'] = None
- #self.lbInfo.SetLabel(
- # 'Click on "Apply" to set filter on the fly for all plots. '+
- # 'Click on "Plot" to try a filter on the current plot.'
- # )
- self.btPlot.Enable(True)
- self.btClear.Enable(True)
- self.btApply.SetLabel(CHAR['cloud']+' Apply')
-
- if not init:
- self.parent.load_and_draw() # Data will change
- self.setCurrentX()
-
-
- def onAdd(self,event=None):
- iSel = self.cbTabs.GetSelection()
- tabList = self.parent.selPanel.tabList
- mainframe = self.parent.mainframe
- icol, colname = self.parent.selPanel.xCol
- print(icol,colname)
- opt = self._GUI2Data()
- errors=[]
- if iSel==0:
- dfs, names, errors = tabList.applyResampling(icol, opt, bAdd=True)
- mainframe.load_dfs(dfs,names,bAdd=True)
- else:
- df, name = tabList.get(iSel-1).applyResampling(icol, opt, bAdd=True)
- mainframe.load_df(df,name,bAdd=True)
- self.updateTabList()
-
- if len(errors)>0:
- raise Exception('Error: The resampling failed on some tables:\n\n'+'\n'.join(errors))
-
- def onPlot(self,event=None):
- from pydatview.tools.signal import applySampler
- if len(self.parent.plotData)!=1:
- Error(self,'Plotting only works for a single plot. Plot less data.')
- return
- opts=self._GUI2Data()
- PD = self.parent.plotData[0]
- x_new, y_new = applySampler(PD.x0, PD.y0, opts)
- ax = self.parent.fig.axes[0]
-
- PD_new = PlotData()
- PD_new.fromXY(x_new, y_new)
- self.parent.transformPlotData(PD_new)
- ax.plot(PD_new.x, PD_new.y, '-')
- self.setCurrentX(x_new)
-
- self.parent.canvas.draw()
-
- def onClear(self,event=None):
- self.parent.load_and_draw() # Data will change
- # Update Current X
- self.setCurrentX()
- # Update Table list
- self.updateTabList()
-
-
- def onTabChange(self,event=None):
- #tabList = self.parent.selPanel.tabList
- #iSel=self.cbTabs.GetSelection()
- pass
-
- def updateTabList(self,event=None):
- tabList = self.parent.selPanel.tabList
- tabListNames = ['All opened tables']+tabList.getDisplayTabNames()
- try:
- iSel=np.max([np.min([self.cbTabs.GetSelection(),len(tabListNames)]),0])
- self.cbTabs.Clear()
- [self.cbTabs.Append(tn) for tn in tabListNames]
- self.cbTabs.SetSelection(iSel)
- except RuntimeError:
- pass
-
- def onHelp(self,event=None):
- Info(self,"""Resampling.
-
-The resampling operation changes the "x" values of a table/plot and
-adapt the "y" values accordingly.
-
-To resample perform the following step:
-
-- Chose a resampling method:
- - replace: specify all the new x-values
- - insert : insert a list of x values to the existing ones
- - delete : delete a list of x values from the existing ones
- - every-n : use every n values
- - time-based: downsample using sample averaging or upsample using
- linear interpolation, x-axis must already be in seconds
- - delta x : specify a delta for uniform spacing of x values
-
-- Specify the x values as a space or comma separated list
-
-- Click on one of the following buttons:
- - Plot: will display the resampled data on the figure
- - Apply: will perform the resampling on the fly for all new plots
- - Add: will create new table(s) with resampled values for all
- signals. This process might take some time.
- Select a table or choose all (default)
-""")
-
-
-
-# --------------------------------------------------------------------------------}
-# --- Mask
-# --------------------------------------------------------------------------------{
-class MaskToolPanel(GUIToolPanel):
- def __init__(self, parent):
- super(MaskToolPanel,self).__init__(parent)
-
- tabList = self.parent.selPanel.tabList
- tabListNames = ['All opened tables']+tabList.getDisplayTabNames()
-
- allMask = tabList.commonMaskString
- if len(allMask)==0:
- allMask=self.guessMask(tabList) # no known mask, we guess one to help the user
- self.applied=False
- else:
- self.applied=True
-
- btClose = self.getBtBitmap(self, 'Close','close', self.destroy)
- btComp = self.getBtBitmap(self, u'Mask (add)','add' , self.onApply)
- if self.applied:
- self.btCompMask = self.getToggleBtBitmap(self, 'Clear','sun', self.onToggleApplyMask)
- self.btCompMask.SetValue(True)
- else:
- self.btCompMask = self.getToggleBtBitmap(self, 'Mask','cloud', self.onToggleApplyMask)
-
- self.lb = wx.StaticText( self, -1, """(Example of mask: "({Time}>100) && ({Time}<50) && ({WS}==5)" or "{Date} > '2018-10-01'")""")
- self.cbTabs = wx.ComboBox(self, choices=tabListNames, style=wx.CB_READONLY)
- self.cbTabs.SetSelection(0)
-
- self.textMask = wx.TextCtrl(self, wx.ID_ANY, allMask)
- #self.textMask.SetValue('({Time}>100) & ({Time}<400)')
- #self.textMask.SetValue("{Date} > '2018-10-01'")
-
- btSizer = wx.FlexGridSizer(rows=2, cols=2, hgap=2, vgap=0)
- btSizer.Add(btClose ,0,flag = wx.ALL|wx.EXPAND, border = 1)
- btSizer.Add(wx.StaticText(self, -1, '') ,0,flag = wx.ALL|wx.EXPAND, border = 1)
- btSizer.Add(btComp ,0,flag = wx.ALL|wx.EXPAND, border = 1)
- btSizer.Add(self.btCompMask ,0,flag = wx.ALL|wx.EXPAND, border = 1)
-
- row_sizer = wx.BoxSizer(wx.HORIZONTAL)
- row_sizer.Add(wx.StaticText(self, -1, 'Tab:') , 0, wx.CENTER|wx.LEFT, 0)
- row_sizer.Add(self.cbTabs , 0, wx.CENTER|wx.LEFT, 2)
- row_sizer.Add(wx.StaticText(self, -1, 'Mask:'), 0, wx.CENTER|wx.LEFT, 5)
- row_sizer.Add(self.textMask, 1, wx.CENTER|wx.LEFT|wx.EXPAND, 5)
-
- vert_sizer = wx.BoxSizer(wx.VERTICAL)
- vert_sizer.Add(self.lb ,0, flag = wx.ALIGN_LEFT|wx.TOP|wx.BOTTOM, border = 5)
- vert_sizer.Add(row_sizer ,1, flag = wx.EXPAND|wx.ALIGN_LEFT|wx.TOP|wx.BOTTOM, border = 5)
-
- self.sizer = wx.BoxSizer(wx.HORIZONTAL)
- self.sizer.Add(btSizer ,0, flag = wx.LEFT ,border = 5)
- self.sizer.Add(vert_sizer ,1, flag = wx.LEFT|wx.EXPAND ,border = TOOL_BORDER)
- self.SetSizer(self.sizer)
- self.Bind(wx.EVT_COMBOBOX, self.onTabChange, self.cbTabs )
-
- def onTabChange(self,event=None):
- tabList = self.parent.selPanel.tabList
- iSel=self.cbTabs.GetSelection()
- if iSel==0:
- maskString = tabList.commonMaskString
- else:
- maskString= tabList.get(iSel-1).maskString
- if len(maskString)>0:
- self.textMask.SetValue(maskString)
- #else:
- # self.textMask.SetValue('') # no known mask
- # self.textMask.SetValue(self.guessMask) # no known mask
-
- def guessMask(self,tabList):
- cols=[c.lower() for c in tabList.get(0).columns_clean]
- if 'time' in cols:
- return '{Time} > 100'
- elif 'date' in cols:
- return "{Date} > '2017-01-01"
- else:
- return ''
-
- def onClear(self,event=None):
- iSel = self.cbTabs.GetSelection()
- tabList = self.parent.selPanel.tabList
- mainframe = self.parent.mainframe
- if iSel==0:
- tabList.clearCommonMask()
- else:
- tabList.get(iSel-1).clearMask()
-
- mainframe.redraw()
- self.onTabChange()
-
- def onToggleApplyMask(self,event=None):
- self.applied = not self.applied
- if self.applied:
- self.btCompMask.SetLabel(CHAR['sun']+' Clear')
- else:
- self.btCompMask.SetLabel(CHAR['cloud']+' Mask')
-
- if self.applied:
- self.onApply(event,bAdd=False)
- else:
- self.onClear()
-
- def onApply(self,event=None,bAdd=True):
- maskString = self.textMask.GetLineText(0)
- iSel = self.cbTabs.GetSelection()
- tabList = self.parent.selPanel.tabList
- mainframe = self.parent.mainframe
- if iSel==0:
- dfs, names, errors = tabList.applyCommonMaskString(maskString, bAdd=bAdd)
- if bAdd:
- mainframe.load_dfs(dfs,names,bAdd=bAdd)
- else:
- mainframe.redraw()
- if len(errors)>0:
- raise Exception('Error: The mask failed on some tables:\n\n'+'\n'.join(errors))
- else:
- dfs, name = tabList.get(iSel-1).applyMaskString(maskString, bAdd=bAdd)
- if bAdd:
- mainframe.load_df(df,name,bAdd=bAdd)
- else:
- mainframe.redraw()
- self.updateTabList()
-
-
- def updateTabList(self,event=None):
- tabList = self.parent.selPanel.tabList
- tabListNames = ['All opened tables']+tabList.getDisplayTabNames()
- try:
- iSel=np.min([self.cbTabs.GetSelection(),len(tabListNames)])
- self.cbTabs.Clear()
- [self.cbTabs.Append(tn) for tn in tabListNames]
- self.cbTabs.SetSelection(iSel)
- except RuntimeError:
- pass
-
-# --------------------------------------------------------------------------------}
-# --- Radial
-# --------------------------------------------------------------------------------{
-sAVG_METHODS = ['Last `n` seconds','Last `n` periods']
-AVG_METHODS = ['constantwindow','periods']
-
-class RadialToolPanel(GUIToolPanel):
- def __init__(self, parent):
- super(RadialToolPanel,self).__init__(parent)
-
- tabList = self.parent.selPanel.tabList
- tabListNames = ['All opened tables']+tabList.getDisplayTabNames()
-
- btClose = self.getBtBitmap(self,'Close' ,'close' , self.destroy)
- btComp = self.getBtBitmap(self,'Average','compute', self.onApply) # ART_PLUS
-
- self.lb = wx.StaticText( self, -1, """Select tables, averaging method and average parameter (`Period` methods uses the `azimuth` signal) """)
- self.cbTabs = wx.ComboBox(self, choices=tabListNames, style=wx.CB_READONLY)
- self.cbMethod = wx.ComboBox(self, choices=sAVG_METHODS, style=wx.CB_READONLY)
- self.cbTabs.SetSelection(0)
- self.cbMethod.SetSelection(0)
-
- self.textAverageParam = wx.TextCtrl(self, wx.ID_ANY, '2',size = (36,-1), style=wx.TE_PROCESS_ENTER)
-
- btSizer = wx.FlexGridSizer(rows=2, cols=1, hgap=0, vgap=0)
- #btSizer = wx.BoxSizer(wx.VERTICAL)
- btSizer.Add(btClose ,0, flag = wx.ALL|wx.EXPAND, border = 1)
- btSizer.Add(btComp ,0, flag = wx.ALL|wx.EXPAND, border = 1)
-
- row_sizer = wx.BoxSizer(wx.HORIZONTAL)
- row_sizer.Add(wx.StaticText(self, -1, 'Tab:') , 0, wx.CENTER|wx.LEFT, 0)
- row_sizer.Add(self.cbTabs , 0, wx.CENTER|wx.LEFT, 2)
- row_sizer.Add(wx.StaticText(self, -1, 'Method:'), 0, wx.CENTER|wx.LEFT, 5)
- row_sizer.Add(self.cbMethod , 0, wx.CENTER|wx.LEFT, 2)
- row_sizer.Add(wx.StaticText(self, -1, 'Param:') , 0, wx.CENTER|wx.LEFT, 5)
- row_sizer.Add(self.textAverageParam , 0, wx.CENTER|wx.LEFT|wx.RIGHT| wx.EXPAND, 2)
-
- vert_sizer = wx.BoxSizer(wx.VERTICAL)
- vert_sizer.Add(self.lb ,0, flag =wx.ALIGN_LEFT|wx.TOP|wx.BOTTOM,border = 5)
- vert_sizer.Add(row_sizer ,0, flag =wx.ALIGN_LEFT|wx.TOP|wx.BOTTOM,border = 5)
-
- self.sizer = wx.BoxSizer(wx.HORIZONTAL)
- self.sizer.Add(btSizer ,0, flag = wx.LEFT ,border = 5)
- self.sizer.Add(vert_sizer ,0, flag = wx.LEFT|wx.EXPAND,border = TOOL_BORDER)
- self.SetSizer(self.sizer)
- self.Bind(wx.EVT_COMBOBOX, self.onTabChange, self.cbTabs )
-
- def onTabChange(self,event=None):
- tabList = self.parent.selPanel.tabList
-
- def onApply(self,event=None):
- try:
- avgParam = float(self.textAverageParam.GetLineText(0))
- except:
- raise Exception('Error: the averaging parameter needs to be an integer or a float')
- iSel = self.cbTabs.GetSelection()
- avgMethod = AVG_METHODS[self.cbMethod.GetSelection()]
- tabList = self.parent.selPanel.tabList
- mainframe = self.parent.mainframe
- if iSel==0:
- dfs, names, errors = tabList.radialAvg(avgMethod,avgParam)
- mainframe.load_dfs(dfs,names,bAdd=True)
- if len(errors)>0:
- raise Exception('Error: The mask failed on some tables:\n\n'+'\n'.join(errors))
- else:
- dfs, names = tabList.get(iSel-1).radialAvg(avgMethod,avgParam)
- mainframe.load_dfs(dfs,names,bAdd=True)
-
- self.updateTabList()
-
- def updateTabList(self,event=None):
- tabList = self.parent.selPanel.tabList
- tabListNames = ['All opened tables']+tabList.getDisplayTabNames()
- iSel=np.min([self.cbTabs.GetSelection(),len(tabListNames)])
- self.cbTabs.Clear()
- [self.cbTabs.Append(tn) for tn in tabListNames]
- self.cbTabs.SetSelection(iSel)
-
-
-# --------------------------------------------------------------------------------}
-# --- Curve Fitting
-# --------------------------------------------------------------------------------{
-MODELS_EXAMPLE =[
- {'label':'User defined model', 'id':'eval:',
- 'formula':'{a}*x**2 + {b}',
- 'coeffs':None,
- 'consts':None,
- 'bounds':None },
- ]
-MODELS_EXTRA =[
-# {'label':'Exponential decay', 'id':'eval:',
-# 'formula':'{A}*exp(-{k}*x)+{B}',
-# 'coeffs' :'k=1, A=1, B=0',
-# 'consts' :None,
-# 'bounds' :None},
-]
-
-class CurveFitToolPanel(GUIToolPanel):
- def __init__(self, parent):
- super(CurveFitToolPanel,self).__init__(parent)
-
- # Data
- self.x = None
- self.y_fit = None
-
- # GUI Objecst
- btClose = self.getBtBitmap(self, 'Close','close', self.destroy)
- btClear = self.getBtBitmap(self, 'Clear','sun', self.onClear) # DELETE
- btAdd = self.getBtBitmap(self, 'Add','add' , self.onAdd)
- btCompFit = self.getBtBitmap(self, 'Fit','check', self.onCurveFit)
- btHelp = self.getBtBitmap(self, 'Help','help', self.onHelp)
-
- boldFont = self.GetFont().Bold()
- lbOutputs = wx.StaticText(self, -1, 'Outputs')
- lbInputs = wx.StaticText(self, -1, 'Inputs ')
- lbOutputs.SetFont(boldFont)
- lbInputs.SetFont(boldFont)
-
- self.textFormula = wx.TextCtrl(self, wx.ID_ANY, '')
- self.textGuess = wx.TextCtrl(self, wx.ID_ANY, '')
- self.textBounds = wx.TextCtrl(self, wx.ID_ANY, '')
- self.textConstants = wx.TextCtrl(self, wx.ID_ANY, '')
-
- self.textFormulaNum = wx.TextCtrl(self, wx.ID_ANY, '', style=wx.TE_READONLY)
- self.textCoeffs = wx.TextCtrl(self, wx.ID_ANY, '', style=wx.TE_READONLY)
- self.textInfo = wx.TextCtrl(self, wx.ID_ANY, '', style=wx.TE_READONLY)
-
-
- self.Models=copy.deepcopy(MODELS_EXAMPLE) + copy.deepcopy(FITTERS) + copy.deepcopy(MODELS) + copy.deepcopy(MODELS_EXTRA)
- sModels=[d['label'] for d in self.Models]
-
-
- self.cbModels = wx.ComboBox(self, choices=sModels, style=wx.CB_READONLY)
- self.cbModels.SetSelection(0)
-
- btSizer = wx.FlexGridSizer(rows=3, cols=2, hgap=2, vgap=0)
- btSizer.Add(btClose ,0,flag = wx.ALL|wx.EXPAND, border = 1)
- btSizer.Add(btClear ,0,flag = wx.ALL|wx.EXPAND, border = 1)
- btSizer.Add(btAdd ,0,flag = wx.ALL|wx.EXPAND, border = 1)
- btSizer.Add(btCompFit ,0,flag = wx.ALL|wx.EXPAND, border = 1)
- btSizer.Add(btHelp ,0,flag = wx.ALL|wx.EXPAND, border = 1)
-
- inputSizer = wx.FlexGridSizer(rows=5, cols=2, hgap=0, vgap=0)
- inputSizer.Add(lbInputs ,0, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
- inputSizer.Add(self.cbModels ,1, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND,border = 1)
- inputSizer.Add(wx.StaticText(self, -1, 'Formula:') ,0, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
- inputSizer.Add(self.textFormula ,1, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND,border = 1)
- inputSizer.Add(wx.StaticText(self, -1, 'Guess:') ,0, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
- inputSizer.Add(self.textGuess ,1, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND,border = 1)
- inputSizer.Add(wx.StaticText(self, -1, 'Bounds:') ,0, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
- inputSizer.Add(self.textBounds ,1, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND,border = 1)
- inputSizer.Add(wx.StaticText(self, -1, 'Constants:'),0, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
- inputSizer.Add(self.textConstants ,1, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND,border = 1)
- inputSizer.AddGrowableCol(1,1)
-
- outputSizer = wx.FlexGridSizer(rows=5, cols=2, hgap=0, vgap=0)
- outputSizer.Add(lbOutputs ,0 , flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
- outputSizer.Add(wx.StaticText(self, -1, '') ,0 , flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
- outputSizer.Add(wx.StaticText(self, -1, 'Formula:'),0 , flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
- outputSizer.Add(self.textFormulaNum ,1 , flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND,border = 1)
- outputSizer.Add(wx.StaticText(self, -1, 'Parameters:') ,0 , flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
- outputSizer.Add(self.textCoeffs ,1 , flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND,border = 1)
- outputSizer.Add(wx.StaticText(self, -1, 'Accuracy:') ,0 , flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
- outputSizer.Add(self.textInfo ,1 , flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND,border = 1)
- outputSizer.AddGrowableCol(1,0.5)
-
- horzSizer = wx.BoxSizer(wx.HORIZONTAL)
- horzSizer.Add(inputSizer ,1.0, flag = wx.LEFT|wx.EXPAND,border = 2)
- horzSizer.Add(outputSizer ,1.0, flag = wx.LEFT|wx.EXPAND,border = 9)
-
- vertSizer = wx.BoxSizer(wx.VERTICAL)
-# vertSizer.Add(self.lbHelp ,0, flag = wx.LEFT ,border = 1)
- vertSizer.Add(horzSizer ,1, flag = wx.LEFT|wx.EXPAND,border = 1)
-
- self.sizer = wx.BoxSizer(wx.HORIZONTAL)
- self.sizer.Add(btSizer ,0, flag = wx.LEFT ,border = 1)
-# self.sizer.Add(vertSizerCB ,0, flag = wx.LEFT ,border = 1)
- self.sizer.Add(vertSizer ,1, flag = wx.EXPAND|wx.LEFT ,border = 1)
- self.SetSizer(self.sizer)
-
- self.Bind(wx.EVT_COMBOBOX, self.onModelChange, self.cbModels)
-
- self.onModelChange()
-
- def onModelChange(self,event=None):
- iModel = self.cbModels.GetSelection()
- d = self.Models[iModel]
- self.textFormula.SetEditable(True)
-
- if d['id'].find('fitter:')==0 :
- self.textGuess.Enable(False)
- self.textGuess.SetValue('')
- self.textFormula.Enable(False)
- self.textFormula.SetValue(d['formula'])
- self.textBounds.Enable(False)
- self.textBounds.SetValue('')
- self.textConstants.Enable(True)
- # NOTE: conversion to string works with list, and tuples, not numpy array
- val = ', '.join([k+'='+str(v) for k,v in d['consts'].items()])
- self.textConstants.SetValue(val)
- else:
- # Formula
- if d['id'].find('eval:')==0 :
- self.textFormula.Enable(True)
- self.textFormula.SetEditable(True)
- else:
- #self.textFormula.Enable(False)
- self.textFormula.Enable(True)
- self.textFormula.SetEditable(False)
- self.textFormula.SetValue(d['formula'])
-
- # Guess
- if d['coeffs'] is None:
- self.textGuess.SetValue('')
- else:
- self.textGuess.SetValue(d['coeffs'])
-
- # Constants
- if d['consts'] is None or len(d['consts'].strip())==0:
- self.textConstants.Enable(False)
- self.textConstants.SetValue('')
- else:
- self.textConstants.Enable(True)
- self.textConstants.SetValue(d['consts'])
-
- # Bounds
- self.textBounds.Enable(True)
- if d['bounds'] is None or len(d['bounds'].strip())==0:
- self.textBounds.SetValue('all=(-np.inf, np.inf)')
- else:
- self.textBounds.SetValue(d['bounds'])
-
- # Outputs
- self.textFormulaNum.SetValue('(Click on Fit)')
- self.textCoeffs.SetValue('')
- self.textInfo.SetValue('')
-
- def onCurveFit(self,event=None):
- self.x = None
- self.y_fit = None
- if len(self.parent.plotData)!=1:
- Error(self,'Curve fitting tool only works with a single curve. Plot less data.')
- return
- PD =self.parent.plotData[0]
-
- iModel = self.cbModels.GetSelection()
- d = self.Models[iModel]
-
- if d['id'].find('fitter:')==0 :
- sFunc=d['id']
- p0=None
- bounds=None
- fun_kwargs=extract_key_miscnum(self.textConstants.GetLineText(0).replace('np.inf','inf'))
- else:
- # Formula
- sFunc=d['id']
- if sFunc=='eval:':
- sFunc+=self.textFormula.GetLineText(0)
- # Bounds
- bounds=self.textBounds.GetLineText(0).replace('np.inf','inf')
- # Guess
- p0=self.textGuess.GetLineText(0).replace('np.inf','inf')
- fun_kwargs=extract_key_num(self.textConstants.GetLineText(0).replace('np.inf','inf'))
- #print('>>> Model fit sFunc :',sFunc )
- #print('>>> Model fit p0 :',p0 )
- #print('>>> Model fit bounds:',bounds )
- #print('>>> Model fit kwargs:',fun_kwargs)
- # Performing fit
- y_fit, pfit, fitter = model_fit(sFunc, PD.x, PD.y, p0=p0, bounds=bounds,**fun_kwargs)
-
- formatter = lambda x: pretty_num_short(x, digits=3)
- formula_num = fitter.formula_num(fmt=formatter)
- # Update info
- self.textFormulaNum.SetValue(formula_num)
- self.textCoeffs.SetValue(', '.join(['{}={:s}'.format(k,formatter(v)) for k,v in fitter.model['coeffs'].items()]))
- self.textInfo.SetValue('R2 = {:.3f} '.format(fitter.model['R2']))
-
- # Saving
- d['formula'] = self.textFormula.GetLineText(0)
- d['bounds'] = self.textBounds.GetLineText(0).replace('np.inf','inf')
- d['coeffs'] = self.textGuess.GetLineText(0).replace('np.inf','inf')
- if d['id'].find('fitter:')==0 :
- d['consts'], _ = set_common_keys(d['consts'],fun_kwargs)
- else:
- d['consts']= self.textConstants.GetLineText(0).replace('np.inf','inf')
-
-
- # Plot
- ax=self.parent.fig.axes[0]
- ax.plot(PD.x,y_fit,'o', ms=4)
- self.parent.canvas.draw()
-
- self.x=PD.x
- self.y_fit=y_fit
- self.sx=PD.sx
- self.sy=PD.sy
-
- def onClear(self,event=None):
- self.parent.load_and_draw() # DATA HAS CHANGED
- self.onModelChange()
-
- def onAdd(self,event=None):
- name='model_fit'
- if self.x is not None and self.y_fit is not None:
- df=pd.DataFrame({self.sx:self.x, self.sy:self.y_fit})
- self.parent.mainframe.load_df(df,name,bAdd=True)
-
- def onHelp(self,event=None):
- Info(self,"""Curve fitting is still in beta.
-
-To perform a curve fit, adjusts the "Inputs section on the left":
-- Select a predefined equation to fit, using the scrolldown menu.
-- Adjust the initial gues for the parameters (if wanted)
-- (Only for few models: set constants values)
-- Click on "Fit"
-
-If you select a user-defined model:
-- Equation parameters are specified using curly brackets
-- Numpy functions are available using "np"
-
-Buttons:
-- Clear: remove the fit from the plot
-- Add: add the fit data to the list of tables (can then be exported)
-
-""")
-
-
-
-TOOLS={
- 'LogDec': LogDecToolPanel,
- 'Outlier': OutlierToolPanel,
- 'Filter': FilterToolPanel,
- 'Resample': ResampleToolPanel,
- 'Mask': MaskToolPanel,
- 'FASTRadialAverage': RadialToolPanel,
- 'CurveFitting': CurveFitToolPanel,
-}
+from __future__ import absolute_import
+import wx
+import numpy as np
+import pandas as pd
+import copy
+import platform
+from collections import OrderedDict
+
+# For log dec tool
+from .common import CHAR, Error, pretty_num_short, Info
+from .plotdata import PlotData
+from pydatview.tools.damping import logDecFromDecay
+from pydatview.tools.curve_fitting import model_fit, extract_key_miscnum, extract_key_num, MODELS, FITTERS, set_common_keys
+
+
+TOOL_BORDER=15
+
+# --------------------------------------------------------------------------------}
+# --- Default class for tools
+# --------------------------------------------------------------------------------{
+class GUIToolPanel(wx.Panel):
+ def __init__(self, parent):
+ super(GUIToolPanel,self).__init__(parent)
+ self.parent = parent
+
+ def destroy(self,event=None):
+ self.parent.removeTools()
+
+ def getBtBitmap(self,par,label,Type=None,callback=None,bitmap=False):
+ if Type is not None:
+ label=CHAR[Type]+' '+label
+
+ bt=wx.Button(par,wx.ID_ANY, label, style=wx.BU_EXACTFIT)
+ #try:
+ # if bitmap is not None:
+ # bt.SetBitmapLabel(wx.ArtProvider.GetBitmap(bitmap)) #,size=(12,12)))
+ # else:
+ #except:
+ # pass
+ if callback is not None:
+ par.Bind(wx.EVT_BUTTON, callback, bt)
+ return bt
+
+ def getToggleBtBitmap(self,par,label,Type=None,callback=None,bitmap=False):
+ if Type is not None:
+ label=CHAR[Type]+' '+label
+ bt=wx.ToggleButton(par,wx.ID_ANY, label, style=wx.BU_EXACTFIT)
+ if callback is not None:
+ par.Bind(wx.EVT_TOGGLEBUTTON, callback, bt)
+ return bt
+
+
+
+# --------------------------------------------------------------------------------}
+# --- Log Dec
+# --------------------------------------------------------------------------------{
+class LogDecToolPanel(GUIToolPanel):
+ def __init__(self, parent):
+ super(LogDecToolPanel,self).__init__(parent)
+ btClose = self.getBtBitmap(self,'Close' ,'close' ,self.destroy )
+ btComp = self.getBtBitmap(self,'Compute','compute',self.onCompute)
+ self.lb = wx.StaticText( self, -1, ' ')
+ self.sizer = wx.BoxSizer(wx.HORIZONTAL)
+ self.sizer.Add(btClose ,0, flag = wx.LEFT|wx.CENTER,border = 1)
+ self.sizer.Add(btComp ,0, flag = wx.LEFT|wx.CENTER,border = 5)
+ self.sizer.Add(self.lb ,0, flag = wx.LEFT|wx.CENTER,border = 5)
+ self.SetSizer(self.sizer)
+
+ def onCompute(self,event=None):
+ if len(self.parent.plotData)!=1:
+ Error(self,'Log Dec tool only works with a single plot.')
+ return
+ pd =self.parent.plotData[0]
+ try:
+ logdec,DampingRatio,T,fn,fd,IPos,INeg,epos,eneg=logDecFromDecay(pd.y,pd.x)
+ lab='LogDec.: {:.4f} - Damping ratio: {:.4f} - F_n: {:.4f} - F_d: {:.4f} - T:{:.3f}'.format(logdec,DampingRatio,fn,fd,T)
+ self.lb.SetLabel(lab)
+ self.sizer.Layout()
+ ax=self.parent.fig.axes[0]
+ ax.plot(pd.x[IPos],pd.y[IPos],'o')
+ ax.plot(pd.x[INeg],pd.y[INeg],'o')
+ ax.plot(pd.x ,epos,'k--')
+ ax.plot(pd.x ,eneg,'k--')
+ self.parent.canvas.draw()
+ except:
+ self.lb.SetLabel('Failed. The signal needs to look like the decay of a first order system.')
+ #self.parent.load_and_draw(); # DATA HAS CHANGED
+
+# --------------------------------------------------------------------------------}
+# --- Outliers
+# --------------------------------------------------------------------------------{
+class OutlierToolPanel(GUIToolPanel):
+ """
+ A quick and dirty solution to manipulate plotData
+ I need to think of a better way to do that
+ """
+ def __init__(self, parent):
+ super(OutlierToolPanel,self).__init__(parent)
+ self.parent = parent # parent is GUIPlotPanel
+
+ # Setting default states to parent
+ if 'RemoveOutliers' not in self.parent.plotDataOptions.keys():
+ self.parent.plotDataOptions['RemoveOutliers']=False
+ if 'OutliersMedianDeviation' not in self.parent.plotDataOptions.keys():
+ self.parent.plotDataOptions['OutliersMedianDeviation']=5
+
+ btClose = self.getBtBitmap(self,'Close','close',self.destroy)
+ self.btComp = self.getToggleBtBitmap(self,'Apply','cloud',self.onToggleCompute)
+
+ lb1 = wx.StaticText(self, -1, 'Median deviation:')
+# self.tMD = wx.TextCtrl(self, wx.ID_ANY,, size = (30,-1), style=wx.TE_PROCESS_ENTER)
+ self.tMD = wx.SpinCtrlDouble(self, value='11', size=wx.Size(60,-1))
+ self.tMD.SetValue(self.parent.plotDataOptions['OutliersMedianDeviation'])
+ self.tMD.SetRange(0.0, 1000)
+ self.tMD.SetIncrement(0.5)
+
+ self.lb = wx.StaticText( self, -1, '')
+ self.sizer = wx.BoxSizer(wx.HORIZONTAL)
+ self.sizer.Add(btClose ,0,flag = wx.LEFT|wx.CENTER,border = 1)
+ self.sizer.Add(self.btComp,0,flag = wx.LEFT|wx.CENTER,border = 5)
+ self.sizer.Add(lb1 ,0,flag = wx.LEFT|wx.CENTER,border = 5)
+ self.sizer.Add(self.tMD ,0,flag = wx.LEFT|wx.CENTER,border = 5)
+ self.sizer.Add(self.lb ,0,flag = wx.LEFT|wx.CENTER,border = 5)
+ self.SetSizer(self.sizer)
+
+ self.Bind(wx.EVT_SPINCTRLDOUBLE, self.onMDChangeArrow, self.tMD)
+ self.Bind(wx.EVT_TEXT_ENTER, self.onMDChangeEnter, self.tMD)
+
+ if platform.system()=='Windows':
+ # See issue https://github.com/wxWidgets/Phoenix/issues/1762
+ self.spintxt = self.tMD.Children[0]
+ assert isinstance(self.spintxt, wx.TextCtrl)
+ self.spintxt.Bind(wx.EVT_CHAR_HOOK, self.onMDChangeChar)
+
+ self.onToggleCompute(init=True)
+
+ def destroy(self,event=None):
+ self.parent.plotDataOptions['RemoveOutliers']=False
+ super(OutlierToolPanel,self).destroy()
+
+ def onToggleCompute(self,event=None, init=False):
+ self.parent.plotDataOptions['OutliersMedianDeviation'] = float(self.tMD.Value)
+
+ if not init:
+ self.parent.plotDataOptions['RemoveOutliers']= not self.parent.plotDataOptions['RemoveOutliers']
+
+ if self.parent.plotDataOptions['RemoveOutliers']:
+ self.lb.SetLabel('Outliers are now removed on the fly. Click "Clear" to stop.')
+ self.btComp.SetLabel(CHAR['sun']+' Clear')
+ else:
+ self.lb.SetLabel('Click on "Apply" to remove outliers on the fly for all new plot.')
+ self.btComp.SetLabel(CHAR['cloud']+' Apply')
+
+ if not init:
+ self.parent.load_and_draw() # Data will change
+
+ def onMDChange(self, event=None):
+ #print(self.tMD.Value)
+ self.parent.plotDataOptions['OutliersMedianDeviation'] = float(self.tMD.Value)
+ if self.parent.plotDataOptions['RemoveOutliers']:
+ self.parent.load_and_draw() # Data will change
+
+ def onMDChangeArrow(self, event):
+ self.onMDChange()
+ event.Skip()
+
+ def onMDChangeEnter(self, event):
+ self.onMDChange()
+ event.Skip()
+
+ def onMDChangeChar(self, event):
+ event.Skip()
+ code = event.GetKeyCode()
+ if code in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER]:
+ #print(self.spintxt.Value)
+ self.tMD.SetValue(self.spintxt.Value)
+ self.onMDChangeEnter(event)
+
+
+
+# --------------------------------------------------------------------------------}
+# --- Filter Tool
+# --------------------------------------------------------------------------------{
+class FilterToolPanel(GUIToolPanel):
+ """
+ Moving average/Filters
+ A quick and dirty solution to manipulate plotData
+ I need to think of a better way to do that
+ """
+ def __init__(self, parent):
+ from pydatview.tools.signal import FILTERS
+ super(FilterToolPanel,self).__init__(parent)
+ self.parent = parent # parent is GUIPlotPanel
+
+ self._DEFAULT_FILTERS=FILTERS
+
+ # Setting default states to parent
+ if 'Filter' not in self.parent.plotDataOptions.keys():
+ self.parent.plotDataOptions['Filter']=None
+ self._filterApplied = type(self.parent.plotDataOptions['Filter'])==dict
+
+
+ self.btClose = self.getBtBitmap(self,'Close','close',self.destroy)
+ self.btAdd = self.getBtBitmap(self, 'Add','add' , self.onAdd)
+ self.btHelp = self.getBtBitmap(self, 'Help','help', self.onHelp)
+ self.btClear = self.getBtBitmap(self, 'Clear Plot','sun' , self.onClear)
+ self.btApply = self.getToggleBtBitmap(self,'Apply','cloud',self.onToggleApply)
+ self.btPlot = self.getBtBitmap(self, 'Plot' ,'chart' , self.onPlot)
+
+ self.cbTabs = wx.ComboBox(self, -1, choices=[], style=wx.CB_READONLY)
+
+ self.cbFilters = wx.ComboBox(self, choices=[filt['name'] for filt in self._DEFAULT_FILTERS], style=wx.CB_READONLY)
+ self.lbParamName = wx.StaticText(self, -1, ' :')
+ self.cbFilters.SetSelection(0)
+ self.tParam = wx.SpinCtrlDouble(self, value='11', size=wx.Size(60,-1))
+ self.lbInfo = wx.StaticText( self, -1, '')
+
+
+ # --- Layout
+ btSizer = wx.FlexGridSizer(rows=3, cols=2, hgap=2, vgap=0)
+ btSizer.Add(self.btClose , 0, flag = wx.ALL|wx.EXPAND, border = 1)
+ btSizer.Add(self.btClear , 0, flag = wx.ALL|wx.EXPAND, border = 1)
+ btSizer.Add(self.btAdd , 0, flag = wx.ALL|wx.EXPAND, border = 1)
+ btSizer.Add(self.btPlot , 0, flag = wx.ALL|wx.EXPAND, border = 1)
+ btSizer.Add(self.btHelp , 0, flag = wx.ALL|wx.EXPAND, border = 1)
+ btSizer.Add(self.btApply , 0, flag = wx.ALL|wx.EXPAND, border = 1)
+
+
+ horzSizerT = wx.BoxSizer(wx.HORIZONTAL)
+ horzSizerT.Add(wx.StaticText(self, -1, 'Table:') , 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.LEFT, 5)
+ horzSizerT.Add(self.cbTabs , 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.LEFT, 1)
+
+ horzSizer = wx.BoxSizer(wx.HORIZONTAL)
+ horzSizer.Add(wx.StaticText(self, -1, 'Filter:') , 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.LEFT, 5)
+ horzSizer.Add(self.cbFilters , 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.LEFT, 1)
+ horzSizer.Add(self.lbParamName , 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.LEFT, 5)
+ horzSizer.Add(self.tParam , 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.LEFT, 1)
+
+
+ vertSizer = wx.BoxSizer(wx.VERTICAL)
+ vertSizer.Add(self.lbInfo ,0, flag = wx.LEFT ,border = 5)
+ vertSizer.Add(horzSizerT ,1, flag = wx.LEFT|wx.EXPAND,border = 1)
+ vertSizer.Add(horzSizer ,1, flag = wx.LEFT|wx.EXPAND,border = 1)
+
+ self.sizer = wx.BoxSizer(wx.HORIZONTAL)
+ self.sizer.Add(btSizer ,0, flag = wx.LEFT ,border = 1)
+ self.sizer.Add(vertSizer ,1, flag = wx.EXPAND|wx.LEFT ,border = 1)
+ self.SetSizer(self.sizer)
+
+ # --- Events
+ self.cbTabs.Bind (wx.EVT_COMBOBOX, self.onTabChange)
+ self.cbFilters.Bind(wx.EVT_COMBOBOX, self.onSelectFilt)
+ self.Bind(wx.EVT_SPINCTRLDOUBLE, self.onParamChangeArrow, self.tParam)
+ self.Bind(wx.EVT_TEXT_ENTER, self.onParamChangeEnter, self.tParam)
+ if platform.system()=='Windows':
+ # See issue https://github.com/wxWidgets/Phoenix/issues/1762
+ self.spintxt = self.tParam.Children[0]
+ assert isinstance(self.spintxt, wx.TextCtrl)
+ self.spintxt.Bind(wx.EVT_CHAR_HOOK, self.onParamChangeChar)
+
+ # --- Init triggers
+ self.updateTabList()
+ self.onSelectFilt()
+ self.onToggleApply(init=True)
+
+ def destroy(self,event=None):
+ super(FilterToolPanel,self).destroy()
+
+ def onSelectFilt(self, event=None):
+ """ Select the filter, but does not applied it to the plotData
+ parentFilt is unchanged
+ But if the parent already has
+ """
+ iFilt = self.cbFilters.GetSelection()
+ filt = self._DEFAULT_FILTERS[iFilt]
+ self.lbParamName.SetLabel(filt['paramName']+':')
+ self.tParam.SetRange(filt['paramRange'][0], filt['paramRange'][1])
+ self.tParam.SetIncrement(filt['increment'])
+
+ parentFilt=self.parent.plotDataOptions['Filter']
+ # Value
+ if type(parentFilt)==dict and parentFilt['name']==filt['name']:
+ self.tParam.SetValue(parentFilt['param'])
+ else:
+ self.tParam.SetValue(filt['param'])
+
+ def _GUI2Data(self):
+ iFilt = self.cbFilters.GetSelection()
+ filt = self._DEFAULT_FILTERS[iFilt].copy()
+ try:
+ filt['param']=np.float(self.spintxt.Value)
+ except:
+ print('[WARN] pyDatView: Issue on Mac: GUITools.py/_GUI2Data. Help needed.')
+ return filt
+
+ def onToggleApply(self, event=None, init=False):
+ """
+ apply Filter based on GUI Data
+ """
+ parentFilt=self.parent.plotDataOptions['Filter']
+ if not init:
+ self._filterApplied = not self._filterApplied
+
+ if self._filterApplied:
+ self.parent.plotDataOptions['Filter'] =self._GUI2Data()
+ #print('Apply', self.parent.plotDataOptions['Filter'])
+ self.lbInfo.SetLabel(
+ 'Filter is now applied on the fly. Change parameter live. Click "Clear" to stop. '
+ )
+ self.btPlot.Enable(False)
+ self.btClear.Enable(False)
+ self.btApply.SetLabel(CHAR['sun']+' Clear')
+ else:
+ self.parent.plotDataOptions['Filter'] = None
+ self.lbInfo.SetLabel(
+ 'Click on "Apply" to set filter on the fly for all plots. '+
+ 'Click on "Plot" to try a filter on the current plot.'
+ )
+ self.btPlot.Enable(True)
+ self.btClear.Enable(True)
+ self.btApply.SetLabel(CHAR['cloud']+' Apply')
+
+ if not init:
+ self.parent.load_and_draw() # Data will change
+ self.updateTabList()
+
+ def onAdd(self,event=None):
+ iSel = self.cbTabs.GetSelection()
+ tabList = self.parent.selPanel.tabList
+ mainframe = self.parent.mainframe
+ icol, colname = self.parent.selPanel.xCol
+ opt = self._GUI2Data()
+ errors=[]
+ if iSel==0:
+ dfs, names, errors = tabList.applyFiltering(icol, opt, bAdd=True)
+ mainframe.load_dfs(dfs,names,bAdd=True)
+ else:
+ df, name = tabList.get(iSel-1).applyFiltering(icol, opt, bAdd=True)
+ mainframe.load_df(df,name,bAdd=True)
+ self.updateTabList()
+
+ if len(errors)>0:
+ raise Exception('Error: The resampling failed on some tables:\n\n'+'\n'.join(errors))
+
+ def onPlot(self, event=None):
+ """
+ Overlay on current axis the filter
+ """
+ from pydatview.tools.signal import applyFilter
+ if len(self.parent.plotData)!=1:
+ Error(self,'Plotting only works for a single plot. Plot less data.')
+ return
+ filt=self._GUI2Data()
+
+ PD = self.parent.plotData[0]
+ y_filt = applyFilter(PD.x0, PD.y0, filt)
+ ax = self.parent.fig.axes[0]
+
+ PD_new = PlotData()
+ PD_new.fromXY(PD.x0, y_filt)
+ self.parent.transformPlotData(PD_new)
+ ax.plot(PD_new.x, PD_new.y, '-')
+ self.parent.canvas.draw()
+
+ def onClear(self, event):
+ self.parent.load_and_draw() # Data will change
+
+ def onParamChange(self, event=None):
+ if self._filterApplied:
+ self.parent.plotDataOptions['Filter'] =self._GUI2Data()
+ #print('OnParamChange', self.parent.plotDataOptions['Filter'])
+ self.parent.load_and_draw() # Data will change
+
+ def onParamChangeArrow(self, event):
+ self.onParamChange()
+ event.Skip()
+
+ def onParamChangeEnter(self, event):
+ self.onParamChange()
+ event.Skip()
+
+ def onParamChangeChar(self, event):
+ event.Skip()
+ code = event.GetKeyCode()
+ if code in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER]:
+ #print(self.spintxt.Value)
+ self.tParam.SetValue(self.spintxt.Value)
+ self.onParamChangeEnter(event)
+
+ def onTabChange(self,event=None):
+ #tabList = self.parent.selPanel.tabList
+ #iSel=self.cbTabs.GetSelection()
+ pass
+
+ def updateTabList(self,event=None):
+ tabList = self.parent.selPanel.tabList
+ tabListNames = ['All opened tables']+tabList.getDisplayTabNames()
+ try:
+ iSel=np.max([np.min([self.cbTabs.GetSelection(),len(tabListNames)]),0])
+ self.cbTabs.Clear()
+ [self.cbTabs.Append(tn) for tn in tabListNames]
+ self.cbTabs.SetSelection(iSel)
+ except RuntimeError:
+ pass
+
+ def onHelp(self,event=None):
+ Info(self,"""Filtering.
+
+The filtering operation changes the "y" values of a table/plot,
+applying a given filter (typically cutting off some frequencies).
+
+To filter perform the following step:
+
+- Chose a filtering method:
+ - Moving average: apply a moving average filter, with
+ a length specified by the window size (in indices)
+ - High pass 1st order: apply a first oder high-pass filter,
+ passing the frequencies above the cutoff frequency parameter.
+ - Low pass 1st order: apply a first oder low-pass filter,
+ passing the frequencies below the cutoff frequency parameter.
+
+- Click on one of the following buttons:
+ - Plot: will display the filtered data on the figure
+ - Apply: will perform the filtering on the fly for all new plots
+ - Add: will create new table(s) with filtered values for all
+ signals. This process might take some time.
+ Currently done for all tables.
+""")
+
+
+# --------------------------------------------------------------------------------}
+# --- Resample
+# --------------------------------------------------------------------------------{
+class ResampleToolPanel(GUIToolPanel):
+ def __init__(self, parent):
+ super(ResampleToolPanel,self).__init__(parent)
+
+ # --- Data from other modules
+ from pydatview.tools.signal import SAMPLERS
+ self.parent = parent # parent is GUIPlotPanel
+ self._SAMPLERS=SAMPLERS
+ # Setting default states to parent
+ if 'Sampler' not in self.parent.plotDataOptions.keys():
+ self.parent.plotDataOptions['Sampler']=None
+ self._applied = type(self.parent.plotDataOptions['Sampler'])==dict
+
+
+ # --- GUI elements
+ self.btClose = self.getBtBitmap(self, 'Close','close', self.destroy)
+ self.btAdd = self.getBtBitmap(self, 'Add','add' , self.onAdd)
+ self.btHelp = self.getBtBitmap(self, 'Help','help', self.onHelp)
+ self.btClear = self.getBtBitmap(self, 'Clear Plot','sun', self.onClear)
+ self.btPlot = self.getBtBitmap(self, 'Plot' ,'chart' , self.onPlot)
+ self.btApply = self.getToggleBtBitmap(self,'Apply','cloud',self.onToggleApply)
+
+ #self.lb = wx.StaticText( self, -1, """ Click help """)
+ self.cbTabs = wx.ComboBox(self, -1, choices=[], style=wx.CB_READONLY)
+ self.cbMethods = wx.ComboBox(self, -1, choices=[s['name'] for s in self._SAMPLERS], style=wx.CB_READONLY)
+
+ self.lbNewX = wx.StaticText(self, -1, 'New x: ')
+ self.textNewX = wx.TextCtrl(self, wx.ID_ANY, '', style = wx.TE_PROCESS_ENTER)
+ self.textOldX = wx.TextCtrl(self, wx.ID_ANY|wx.TE_READONLY)
+ self.textOldX.Enable(False)
+
+ # --- Layout
+ btSizer = wx.FlexGridSizer(rows=3, cols=2, hgap=2, vgap=0)
+ btSizer.Add(self.btClose , 0, flag = wx.ALL|wx.EXPAND, border = 1)
+ btSizer.Add(self.btClear , 0, flag = wx.ALL|wx.EXPAND, border = 1)
+ btSizer.Add(self.btAdd , 0, flag = wx.ALL|wx.EXPAND, border = 1)
+ btSizer.Add(self.btPlot , 0, flag = wx.ALL|wx.EXPAND, border = 1)
+ btSizer.Add(self.btHelp , 0, flag = wx.ALL|wx.EXPAND, border = 1)
+ btSizer.Add(self.btApply , 0, flag = wx.ALL|wx.EXPAND, border = 1)
+
+ msizer = wx.FlexGridSizer(rows=2, cols=4, hgap=2, vgap=0)
+ msizer.Add(wx.StaticText(self, -1, 'Table:') , 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM, 1)
+ msizer.Add(self.cbTabs , 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM, 1)
+ msizer.Add(wx.StaticText(self, -1, 'Current x: '), 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM, 1)
+ msizer.Add(self.textOldX , 1, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND, 1)
+ msizer.Add(wx.StaticText(self, -1, 'Method:') , 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM, 1)
+ msizer.Add(self.cbMethods , 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM, 1)
+ msizer.Add(self.lbNewX , 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM, 1)
+ msizer.Add(self.textNewX , 1, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND, 1)
+ msizer.AddGrowableCol(3,1)
+
+ self.sizer = wx.BoxSizer(wx.HORIZONTAL)
+ self.sizer.Add(btSizer ,0, flag = wx.LEFT ,border = 5)
+ self.sizer.Add(msizer ,1, flag = wx.LEFT|wx.EXPAND ,border = TOOL_BORDER)
+ self.SetSizer(self.sizer)
+
+ # --- Events
+ self.cbTabs.Bind (wx.EVT_COMBOBOX, self.onTabChange)
+ self.cbMethods.Bind(wx.EVT_COMBOBOX, self.onMethodChange)
+ self.textNewX.Bind(wx.EVT_TEXT_ENTER,self.onParamChange)
+
+ # --- Init triggers
+ self.cbMethods.SetSelection(3)
+ self.onMethodChange(init=True)
+ self.onToggleApply(init=True)
+ self.updateTabList()
+ self.textNewX.SetValue('2')
+
+ def setCurrentX(self, x=None):
+ if x is None:
+ x= self.parent.plotData[0].x
+ if len(x)<50:
+ s=np.array2string(x, separator=', ')
+ else:
+ s =np.array2string(x[[0,1,2,3]], separator=', ')
+ s+=', ..., '
+ s+=np.array2string(x[[-3,-2,-1]], separator=', ')
+ s=s.replace('[','').replace(']','').replace(' ','').replace(',',', ')
+
+ self.textOldX.SetValue(s)
+
+ def onMethodChange(self, event=None, init=True):
+ """ Select the method, but does not applied it to the plotData
+ User data and option is unchanged
+ But if the user already has some options, they are used
+ """
+ iOpt = self.cbMethods.GetSelection()
+ opt = self._SAMPLERS[iOpt]
+ self.lbNewX.SetLabel(opt['paramName']+':')
+
+ parentOpt=self.parent.plotDataOptions['Sampler']
+ # Value
+ if len(self.textNewX.Value)==0:
+ if type(parentOpt)==dict:
+ self.textNewX.SetValue(str(parentOpt['param'])[1:-1])
+ else:
+ self.textNewX.SetValue(str(opt['param'])[2:-2])
+ self.onParamChange()
+
+ def onParamChange(self, event=None):
+ if self._applied:
+ self.parent.plotDataOptions['Sampler'] =self._GUI2Data()
+ self.parent.load_and_draw() # Data will change
+ self.setCurrentX()
+
+ def _GUI2Data(self):
+ iOpt = self.cbMethods.GetSelection()
+ opt = self._SAMPLERS[iOpt].copy()
+ s= self.textNewX.Value.strip().replace('[','').replace(']','')
+ if len(s)>0:
+ if s.find(','):
+ opt['param']=np.array(s.split(',')).astype(float)
+ else:
+ opt['param']=np.array(s.split('')).astype(float)
+ return opt
+
+ def onToggleApply(self, event=None, init=False):
+ """
+ apply sampler based on GUI Data
+ """
+ parentFilt=self.parent.plotDataOptions['Sampler']
+ if not init:
+ self._applied = not self._applied
+
+ if self._applied:
+ self.parent.plotDataOptions['Sampler'] =self._GUI2Data()
+ #print('Apply', self.parent.plotDataOptions['Sampler'])
+ #self.lbInfo.SetLabel(
+ # 'Sampler is now applied on the fly. Change parameter live. Click "Clear" to stop. '
+ # )
+ self.btPlot.Enable(False)
+ self.btClear.Enable(False)
+ self.btApply.SetLabel(CHAR['sun']+' Clear')
+ else:
+ self.parent.plotDataOptions['Sampler'] = None
+ #self.lbInfo.SetLabel(
+ # 'Click on "Apply" to set filter on the fly for all plots. '+
+ # 'Click on "Plot" to try a filter on the current plot.'
+ # )
+ self.btPlot.Enable(True)
+ self.btClear.Enable(True)
+ self.btApply.SetLabel(CHAR['cloud']+' Apply')
+
+ if not init:
+ self.parent.load_and_draw() # Data will change
+ self.setCurrentX()
+
+ def onAdd(self,event=None):
+ iSel = self.cbTabs.GetSelection()
+ tabList = self.parent.selPanel.tabList
+ mainframe = self.parent.mainframe
+ icol, colname = self.parent.selPanel.xCol
+ print(icol,colname)
+ opt = self._GUI2Data()
+ errors=[]
+ if iSel==0:
+ dfs, names, errors = tabList.applyResampling(icol, opt, bAdd=True)
+ mainframe.load_dfs(dfs,names,bAdd=True)
+ else:
+ df, name = tabList.get(iSel-1).applyResampling(icol, opt, bAdd=True)
+ mainframe.load_df(df,name,bAdd=True)
+ self.updateTabList()
+
+ if len(errors)>0:
+ raise Exception('Error: The resampling failed on some tables:\n\n'+'\n'.join(errors))
+
+ def onPlot(self,event=None):
+ from pydatview.tools.signal import applySampler
+ if len(self.parent.plotData)!=1:
+ Error(self,'Plotting only works for a single plot. Plot less data.')
+ return
+ opts=self._GUI2Data()
+ PD = self.parent.plotData[0]
+ x_new, y_new = applySampler(PD.x0, PD.y0, opts)
+ ax = self.parent.fig.axes[0]
+
+ PD_new = PlotData()
+ PD_new.fromXY(x_new, y_new)
+ self.parent.transformPlotData(PD_new)
+ ax.plot(PD_new.x, PD_new.y, '-')
+ self.setCurrentX(x_new)
+
+ self.parent.canvas.draw()
+
+ def onClear(self,event=None):
+ self.parent.load_and_draw() # Data will change
+ # Update Current X
+ self.setCurrentX()
+ # Update Table list
+ self.updateTabList()
+
+
+ def onTabChange(self,event=None):
+ #tabList = self.parent.selPanel.tabList
+ #iSel=self.cbTabs.GetSelection()
+ pass
+
+ def updateTabList(self,event=None):
+ tabList = self.parent.selPanel.tabList
+ tabListNames = ['All opened tables']+tabList.getDisplayTabNames()
+ try:
+ iSel=np.max([np.min([self.cbTabs.GetSelection(),len(tabListNames)]),0])
+ self.cbTabs.Clear()
+ [self.cbTabs.Append(tn) for tn in tabListNames]
+ self.cbTabs.SetSelection(iSel)
+ except RuntimeError:
+ pass
+
+ def onHelp(self,event=None):
+ Info(self,"""Resampling.
+
+The resampling operation changes the "x" values of a table/plot and
+adapt the "y" values accordingly.
+
+To resample perform the following step:
+
+- Chose a resampling method:
+ - replace: specify all the new x-values
+ - insert : insert a list of x values to the existing ones
+ - delete : delete a list of x values from the existing ones
+ - every-n : use every n values
+ - time-based: downsample using sample averaging or upsample using
+ linear interpolation, x-axis must already be in seconds
+ - delta x : specify a delta for uniform spacing of x values
+
+- Specify the x values as a space or comma separated list
+
+- Click on one of the following buttons:
+ - Plot: will display the resampled data on the figure
+ - Apply: will perform the resampling on the fly for all new plots
+ - Add: will create new table(s) with resampled values for all
+ signals. This process might take some time.
+ Select a table or choose all (default)
+""")
+
+
+
+# --------------------------------------------------------------------------------}
+# --- Mask
+# --------------------------------------------------------------------------------{
+class MaskToolPanel(GUIToolPanel):
+ def __init__(self, parent):
+ super(MaskToolPanel,self).__init__(parent)
+
+ tabList = self.parent.selPanel.tabList
+ tabListNames = ['All opened tables']+tabList.getDisplayTabNames()
+
+ allMask = tabList.commonMaskString
+ if len(allMask)==0:
+ allMask=self.guessMask(tabList) # no known mask, we guess one to help the user
+ self.applied=False
+ else:
+ self.applied=True
+
+ btClose = self.getBtBitmap(self, 'Close','close', self.destroy)
+ btComp = self.getBtBitmap(self, u'Mask (add)','add' , self.onApply)
+ if self.applied:
+ self.btCompMask = self.getToggleBtBitmap(self, 'Clear','sun', self.onToggleApplyMask)
+ self.btCompMask.SetValue(True)
+ else:
+ self.btCompMask = self.getToggleBtBitmap(self, 'Mask','cloud', self.onToggleApplyMask)
+
+ self.lb = wx.StaticText( self, -1, """(Example of mask: "({Time}>100) && ({Time}<50) && ({WS}==5)" or "{Date} > '2018-10-01'")""")
+ self.cbTabs = wx.ComboBox(self, choices=tabListNames, style=wx.CB_READONLY)
+ self.cbTabs.SetSelection(0)
+
+ self.textMask = wx.TextCtrl(self, wx.ID_ANY, allMask)
+ #self.textMask.SetValue('({Time}>100) & ({Time}<400)')
+ #self.textMask.SetValue("{Date} > '2018-10-01'")
+
+ btSizer = wx.FlexGridSizer(rows=2, cols=2, hgap=2, vgap=0)
+ btSizer.Add(btClose ,0,flag = wx.ALL|wx.EXPAND, border = 1)
+ btSizer.Add(wx.StaticText(self, -1, '') ,0,flag = wx.ALL|wx.EXPAND, border = 1)
+ btSizer.Add(btComp ,0,flag = wx.ALL|wx.EXPAND, border = 1)
+ btSizer.Add(self.btCompMask ,0,flag = wx.ALL|wx.EXPAND, border = 1)
+
+ row_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ row_sizer.Add(wx.StaticText(self, -1, 'Tab:') , 0, wx.CENTER|wx.LEFT, 0)
+ row_sizer.Add(self.cbTabs , 0, wx.CENTER|wx.LEFT, 2)
+ row_sizer.Add(wx.StaticText(self, -1, 'Mask:'), 0, wx.CENTER|wx.LEFT, 5)
+ row_sizer.Add(self.textMask, 1, wx.CENTER|wx.LEFT|wx.EXPAND, 5)
+
+ vert_sizer = wx.BoxSizer(wx.VERTICAL)
+ vert_sizer.Add(self.lb ,0, flag = wx.ALIGN_LEFT|wx.TOP|wx.BOTTOM, border = 5)
+ vert_sizer.Add(row_sizer ,1, flag = wx.EXPAND|wx.ALIGN_LEFT|wx.TOP|wx.BOTTOM, border = 5)
+
+ self.sizer = wx.BoxSizer(wx.HORIZONTAL)
+ self.sizer.Add(btSizer ,0, flag = wx.LEFT ,border = 5)
+ self.sizer.Add(vert_sizer ,1, flag = wx.LEFT|wx.EXPAND ,border = TOOL_BORDER)
+ self.SetSizer(self.sizer)
+ self.Bind(wx.EVT_COMBOBOX, self.onTabChange, self.cbTabs )
+
+ def onTabChange(self,event=None):
+ tabList = self.parent.selPanel.tabList
+ iSel=self.cbTabs.GetSelection()
+ if iSel==0:
+ maskString = tabList.commonMaskString
+ else:
+ maskString= tabList.get(iSel-1).maskString
+ if len(maskString)>0:
+ self.textMask.SetValue(maskString)
+ #else:
+ # self.textMask.SetValue('') # no known mask
+ # self.textMask.SetValue(self.guessMask) # no known mask
+
+ def guessMask(self,tabList):
+ cols=[c.lower() for c in tabList.get(0).columns_clean]
+ if 'time' in cols:
+ return '{Time} > 100'
+ elif 'date' in cols:
+ return "{Date} > '2017-01-01"
+ else:
+ return ''
+
+ def onClear(self,event=None):
+ iSel = self.cbTabs.GetSelection()
+ tabList = self.parent.selPanel.tabList
+ mainframe = self.parent.mainframe
+ if iSel==0:
+ tabList.clearCommonMask()
+ else:
+ tabList.get(iSel-1).clearMask()
+
+ mainframe.redraw()
+ self.onTabChange()
+
+ def onToggleApplyMask(self,event=None):
+ self.applied = not self.applied
+ if self.applied:
+ self.btCompMask.SetLabel(CHAR['sun']+' Clear')
+ else:
+ self.btCompMask.SetLabel(CHAR['cloud']+' Mask')
+
+ if self.applied:
+ self.onApply(event,bAdd=False)
+ else:
+ self.onClear()
+
+ def onApply(self,event=None,bAdd=True):
+ maskString = self.textMask.GetLineText(0)
+ iSel = self.cbTabs.GetSelection()
+ tabList = self.parent.selPanel.tabList
+ mainframe = self.parent.mainframe
+ if iSel==0:
+ dfs, names, errors = tabList.applyCommonMaskString(maskString, bAdd=bAdd)
+ if bAdd:
+ mainframe.load_dfs(dfs,names,bAdd=bAdd)
+ else:
+ mainframe.redraw()
+ if len(errors)>0:
+ raise Exception('Error: The mask failed on some tables:\n\n'+'\n'.join(errors))
+ else:
+ dfs, name = tabList.get(iSel-1).applyMaskString(maskString, bAdd=bAdd)
+ if bAdd:
+ mainframe.load_df(df,name,bAdd=bAdd)
+ else:
+ mainframe.redraw()
+ self.updateTabList()
+
+
+ def updateTabList(self,event=None):
+ tabList = self.parent.selPanel.tabList
+ tabListNames = ['All opened tables']+tabList.getDisplayTabNames()
+ try:
+ iSel=np.min([self.cbTabs.GetSelection(),len(tabListNames)])
+ self.cbTabs.Clear()
+ [self.cbTabs.Append(tn) for tn in tabListNames]
+ self.cbTabs.SetSelection(iSel)
+ except RuntimeError:
+ pass
+
+# --------------------------------------------------------------------------------}
+# --- Radial
+# --------------------------------------------------------------------------------{
+sAVG_METHODS = ['Last `n` seconds','Last `n` periods']
+AVG_METHODS = ['constantwindow','periods']
+
+class RadialToolPanel(GUIToolPanel):
+ def __init__(self, parent):
+ super(RadialToolPanel,self).__init__(parent)
+
+ tabList = self.parent.selPanel.tabList
+ tabListNames = ['All opened tables']+tabList.getDisplayTabNames()
+
+ btClose = self.getBtBitmap(self,'Close' ,'close' , self.destroy)
+ btComp = self.getBtBitmap(self,'Average','compute', self.onApply) # ART_PLUS
+
+ self.lb = wx.StaticText( self, -1, """Select tables, averaging method and average parameter (`Period` methods uses the `azimuth` signal) """)
+ self.cbTabs = wx.ComboBox(self, choices=tabListNames, style=wx.CB_READONLY)
+ self.cbMethod = wx.ComboBox(self, choices=sAVG_METHODS, style=wx.CB_READONLY)
+ self.cbTabs.SetSelection(0)
+ self.cbMethod.SetSelection(0)
+
+ self.textAverageParam = wx.TextCtrl(self, wx.ID_ANY, '2',size = (36,-1), style=wx.TE_PROCESS_ENTER)
+
+ btSizer = wx.FlexGridSizer(rows=2, cols=1, hgap=0, vgap=0)
+ #btSizer = wx.BoxSizer(wx.VERTICAL)
+ btSizer.Add(btClose ,0, flag = wx.ALL|wx.EXPAND, border = 1)
+ btSizer.Add(btComp ,0, flag = wx.ALL|wx.EXPAND, border = 1)
+
+ row_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ row_sizer.Add(wx.StaticText(self, -1, 'Tab:') , 0, wx.CENTER|wx.LEFT, 0)
+ row_sizer.Add(self.cbTabs , 0, wx.CENTER|wx.LEFT, 2)
+ row_sizer.Add(wx.StaticText(self, -1, 'Method:'), 0, wx.CENTER|wx.LEFT, 5)
+ row_sizer.Add(self.cbMethod , 0, wx.CENTER|wx.LEFT, 2)
+ row_sizer.Add(wx.StaticText(self, -1, 'Param:') , 0, wx.CENTER|wx.LEFT, 5)
+ row_sizer.Add(self.textAverageParam , 0, wx.CENTER|wx.LEFT|wx.RIGHT| wx.EXPAND, 2)
+
+ vert_sizer = wx.BoxSizer(wx.VERTICAL)
+ vert_sizer.Add(self.lb ,0, flag =wx.ALIGN_LEFT|wx.TOP|wx.BOTTOM,border = 5)
+ vert_sizer.Add(row_sizer ,0, flag =wx.ALIGN_LEFT|wx.TOP|wx.BOTTOM,border = 5)
+
+ self.sizer = wx.BoxSizer(wx.HORIZONTAL)
+ self.sizer.Add(btSizer ,0, flag = wx.LEFT ,border = 5)
+ self.sizer.Add(vert_sizer ,0, flag = wx.LEFT|wx.EXPAND,border = TOOL_BORDER)
+ self.SetSizer(self.sizer)
+ self.Bind(wx.EVT_COMBOBOX, self.onTabChange, self.cbTabs )
+
+ def onTabChange(self,event=None):
+ tabList = self.parent.selPanel.tabList
+
+ def onApply(self,event=None):
+ try:
+ avgParam = float(self.textAverageParam.GetLineText(0))
+ except:
+ raise Exception('Error: the averaging parameter needs to be an integer or a float')
+ iSel = self.cbTabs.GetSelection()
+ avgMethod = AVG_METHODS[self.cbMethod.GetSelection()]
+ tabList = self.parent.selPanel.tabList
+ mainframe = self.parent.mainframe
+ if iSel==0:
+ dfs, names, errors = tabList.radialAvg(avgMethod,avgParam)
+ mainframe.load_dfs(dfs,names,bAdd=True)
+ if len(errors)>0:
+ raise Exception('Error: The mask failed on some tables:\n\n'+'\n'.join(errors))
+ else:
+ dfs, names = tabList.get(iSel-1).radialAvg(avgMethod,avgParam)
+ mainframe.load_dfs(dfs,names,bAdd=True)
+
+ self.updateTabList()
+
+ def updateTabList(self,event=None):
+ tabList = self.parent.selPanel.tabList
+ tabListNames = ['All opened tables']+tabList.getDisplayTabNames()
+ iSel=np.min([self.cbTabs.GetSelection(),len(tabListNames)])
+ self.cbTabs.Clear()
+ [self.cbTabs.Append(tn) for tn in tabListNames]
+ self.cbTabs.SetSelection(iSel)
+
+
+# --------------------------------------------------------------------------------}
+# --- Curve Fitting
+# --------------------------------------------------------------------------------{
+MODELS_EXAMPLE =[
+ {'label':'User defined model', 'id':'eval:',
+ 'formula':'{a}*x**2 + {b}',
+ 'coeffs':None,
+ 'consts':None,
+ 'bounds':None },
+ ]
+MODELS_EXTRA =[
+# {'label':'Exponential decay', 'id':'eval:',
+# 'formula':'{A}*exp(-{k}*x)+{B}',
+# 'coeffs' :'k=1, A=1, B=0',
+# 'consts' :None,
+# 'bounds' :None},
+]
+
+class CurveFitToolPanel(GUIToolPanel):
+ def __init__(self, parent):
+ super(CurveFitToolPanel,self).__init__(parent)
+
+ # Data
+ self.x = None
+ self.y_fit = None
+
+ # GUI Objecst
+ btClose = self.getBtBitmap(self, 'Close','close', self.destroy)
+ btClear = self.getBtBitmap(self, 'Clear','sun', self.onClear) # DELETE
+ btAdd = self.getBtBitmap(self, 'Add','add' , self.onAdd)
+ btCompFit = self.getBtBitmap(self, 'Fit','check', self.onCurveFit)
+ btHelp = self.getBtBitmap(self, 'Help','help', self.onHelp)
+
+ boldFont = self.GetFont().Bold()
+ lbOutputs = wx.StaticText(self, -1, 'Outputs')
+ lbInputs = wx.StaticText(self, -1, 'Inputs ')
+ lbOutputs.SetFont(boldFont)
+ lbInputs.SetFont(boldFont)
+
+ self.textFormula = wx.TextCtrl(self, wx.ID_ANY, '')
+ self.textGuess = wx.TextCtrl(self, wx.ID_ANY, '')
+ self.textBounds = wx.TextCtrl(self, wx.ID_ANY, '')
+ self.textConstants = wx.TextCtrl(self, wx.ID_ANY, '')
+
+ self.textFormulaNum = wx.TextCtrl(self, wx.ID_ANY, '', style=wx.TE_READONLY)
+ self.textCoeffs = wx.TextCtrl(self, wx.ID_ANY, '', style=wx.TE_READONLY)
+ self.textInfo = wx.TextCtrl(self, wx.ID_ANY, '', style=wx.TE_READONLY)
+
+
+ self.Models=copy.deepcopy(MODELS_EXAMPLE) + copy.deepcopy(FITTERS) + copy.deepcopy(MODELS) + copy.deepcopy(MODELS_EXTRA)
+ sModels=[d['label'] for d in self.Models]
+
+
+ self.cbModels = wx.ComboBox(self, choices=sModels, style=wx.CB_READONLY)
+ self.cbModels.SetSelection(0)
+
+ btSizer = wx.FlexGridSizer(rows=3, cols=2, hgap=2, vgap=0)
+ btSizer.Add(btClose ,0,flag = wx.ALL|wx.EXPAND, border = 1)
+ btSizer.Add(btClear ,0,flag = wx.ALL|wx.EXPAND, border = 1)
+ btSizer.Add(btAdd ,0,flag = wx.ALL|wx.EXPAND, border = 1)
+ btSizer.Add(btCompFit ,0,flag = wx.ALL|wx.EXPAND, border = 1)
+ btSizer.Add(btHelp ,0,flag = wx.ALL|wx.EXPAND, border = 1)
+
+ inputSizer = wx.FlexGridSizer(rows=5, cols=2, hgap=0, vgap=0)
+ inputSizer.Add(lbInputs ,0, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
+ inputSizer.Add(self.cbModels ,1, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND,border = 1)
+ inputSizer.Add(wx.StaticText(self, -1, 'Formula:') ,0, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
+ inputSizer.Add(self.textFormula ,1, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND,border = 1)
+ inputSizer.Add(wx.StaticText(self, -1, 'Guess:') ,0, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
+ inputSizer.Add(self.textGuess ,1, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND,border = 1)
+ inputSizer.Add(wx.StaticText(self, -1, 'Bounds:') ,0, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
+ inputSizer.Add(self.textBounds ,1, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND,border = 1)
+ inputSizer.Add(wx.StaticText(self, -1, 'Constants:'),0, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
+ inputSizer.Add(self.textConstants ,1, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND,border = 1)
+ inputSizer.AddGrowableCol(1,1)
+
+ outputSizer = wx.FlexGridSizer(rows=5, cols=2, hgap=0, vgap=0)
+ outputSizer.Add(lbOutputs ,0 , flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
+ outputSizer.Add(wx.StaticText(self, -1, '') ,0 , flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
+ outputSizer.Add(wx.StaticText(self, -1, 'Formula:'),0 , flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
+ outputSizer.Add(self.textFormulaNum ,1 , flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND,border = 1)
+ outputSizer.Add(wx.StaticText(self, -1, 'Parameters:') ,0 , flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
+ outputSizer.Add(self.textCoeffs ,1 , flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND,border = 1)
+ outputSizer.Add(wx.StaticText(self, -1, 'Accuracy:') ,0 , flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM,border = 1)
+ outputSizer.Add(self.textInfo ,1 , flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM|wx.EXPAND,border = 1)
+ outputSizer.AddGrowableCol(1,0.5)
+
+ horzSizer = wx.BoxSizer(wx.HORIZONTAL)
+ horzSizer.Add(inputSizer ,1.0, flag = wx.LEFT|wx.EXPAND,border = 2)
+ horzSizer.Add(outputSizer ,1.0, flag = wx.LEFT|wx.EXPAND,border = 9)
+
+ vertSizer = wx.BoxSizer(wx.VERTICAL)
+# vertSizer.Add(self.lbHelp ,0, flag = wx.LEFT ,border = 1)
+ vertSizer.Add(horzSizer ,1, flag = wx.LEFT|wx.EXPAND,border = 1)
+
+ self.sizer = wx.BoxSizer(wx.HORIZONTAL)
+ self.sizer.Add(btSizer ,0, flag = wx.LEFT ,border = 1)
+# self.sizer.Add(vertSizerCB ,0, flag = wx.LEFT ,border = 1)
+ self.sizer.Add(vertSizer ,1, flag = wx.EXPAND|wx.LEFT ,border = 1)
+ self.SetSizer(self.sizer)
+
+ self.Bind(wx.EVT_COMBOBOX, self.onModelChange, self.cbModels)
+
+ self.onModelChange()
+
+ def onModelChange(self,event=None):
+ iModel = self.cbModels.GetSelection()
+ d = self.Models[iModel]
+ self.textFormula.SetEditable(True)
+
+ if d['id'].find('fitter:')==0 :
+ self.textGuess.Enable(False)
+ self.textGuess.SetValue('')
+ self.textFormula.Enable(False)
+ self.textFormula.SetValue(d['formula'])
+ self.textBounds.Enable(False)
+ self.textBounds.SetValue('')
+ self.textConstants.Enable(True)
+ # NOTE: conversion to string works with list, and tuples, not numpy array
+ val = ', '.join([k+'='+str(v) for k,v in d['consts'].items()])
+ self.textConstants.SetValue(val)
+ else:
+ # Formula
+ if d['id'].find('eval:')==0 :
+ self.textFormula.Enable(True)
+ self.textFormula.SetEditable(True)
+ else:
+ #self.textFormula.Enable(False)
+ self.textFormula.Enable(True)
+ self.textFormula.SetEditable(False)
+ self.textFormula.SetValue(d['formula'])
+
+ # Guess
+ if d['coeffs'] is None:
+ self.textGuess.SetValue('')
+ else:
+ self.textGuess.SetValue(d['coeffs'])
+
+ # Constants
+ if d['consts'] is None or len(d['consts'].strip())==0:
+ self.textConstants.Enable(False)
+ self.textConstants.SetValue('')
+ else:
+ self.textConstants.Enable(True)
+ self.textConstants.SetValue(d['consts'])
+
+ # Bounds
+ self.textBounds.Enable(True)
+ if d['bounds'] is None or len(d['bounds'].strip())==0:
+ self.textBounds.SetValue('all=(-np.inf, np.inf)')
+ else:
+ self.textBounds.SetValue(d['bounds'])
+
+ # Outputs
+ self.textFormulaNum.SetValue('(Click on Fit)')
+ self.textCoeffs.SetValue('')
+ self.textInfo.SetValue('')
+
+ def onCurveFit(self,event=None):
+ self.x = None
+ self.y_fit = None
+ if len(self.parent.plotData)!=1:
+ Error(self,'Curve fitting tool only works with a single curve. Plot less data.')
+ return
+ PD =self.parent.plotData[0]
+ ax =self.parent.fig.axes[0]
+ # Restricting data to axes visible bounds on the x axis
+ xlim= ax.get_xlim()
+ b=np.logical_and(PD.x>=xlim[0], PD.x<=xlim[1])
+
+ iModel = self.cbModels.GetSelection()
+ d = self.Models[iModel]
+
+ if d['id'].find('fitter:')==0 :
+ sFunc=d['id']
+ p0=None
+ bounds=None
+ fun_kwargs=extract_key_miscnum(self.textConstants.GetLineText(0).replace('np.inf','inf'))
+ else:
+ # Formula
+ sFunc=d['id']
+ if sFunc=='eval:':
+ sFunc+=self.textFormula.GetLineText(0)
+ # Bounds
+ bounds=self.textBounds.GetLineText(0).replace('np.inf','inf')
+ # Guess
+ p0=self.textGuess.GetLineText(0).replace('np.inf','inf')
+ fun_kwargs=extract_key_num(self.textConstants.GetLineText(0).replace('np.inf','inf'))
+ #print('>>> Model fit sFunc :',sFunc )
+ #print('>>> Model fit p0 :',p0 )
+ #print('>>> Model fit bounds:',bounds )
+ #print('>>> Model fit kwargs:',fun_kwargs)
+ # Performing fit
+ y_fit, pfit, fitter = model_fit(sFunc, PD.x[b], PD.y[b], p0=p0, bounds=bounds,**fun_kwargs)
+
+ formatter = lambda x: pretty_num_short(x, digits=3)
+ formula_num = fitter.formula_num(fmt=formatter)
+ # Update info
+ self.textFormulaNum.SetValue(formula_num)
+ self.textCoeffs.SetValue(', '.join(['{}={:s}'.format(k,formatter(v)) for k,v in fitter.model['coeffs'].items()]))
+ self.textInfo.SetValue('R2 = {:.3f} '.format(fitter.model['R2']))
+
+ # Saving
+ d['formula'] = self.textFormula.GetLineText(0)
+ d['bounds'] = self.textBounds.GetLineText(0).replace('np.inf','inf')
+ d['coeffs'] = self.textGuess.GetLineText(0).replace('np.inf','inf')
+ if d['id'].find('fitter:')==0 :
+ d['consts'], _ = set_common_keys(d['consts'],fun_kwargs)
+ else:
+ d['consts']= self.textConstants.GetLineText(0).replace('np.inf','inf')
+
+
+ # Plot
+ ax=self.parent.fig.axes[0]
+ ax.plot(PD.x[b],y_fit,'o', ms=4)
+ self.parent.canvas.draw()
+
+ self.x=PD.x[b]
+ self.y_fit=y_fit
+ self.sx=PD.sx
+ self.sy=PD.sy
+
+ def onClear(self,event=None):
+ self.parent.load_and_draw() # DATA HAS CHANGED
+ self.onModelChange()
+
+ def onAdd(self,event=None):
+ name='model_fit'
+ if self.x is not None and self.y_fit is not None:
+ df=pd.DataFrame({self.sx:self.x, self.sy:self.y_fit})
+ self.parent.mainframe.load_df(df,name,bAdd=True)
+
+ def onHelp(self,event=None):
+ Info(self,"""Curve fitting is still in beta.
+
+To perform a curve fit, adjusts the "Inputs section on the left":
+- Select a predefined equation to fit, using the scrolldown menu.
+- Adjust the initial gues for the parameters (if wanted)
+- (Only for few models: set constants values)
+- Click on "Fit"
+
+If you select a user-defined model:
+- Equation parameters are specified using curly brackets
+- Numpy functions are available using "np"
+
+Buttons:
+- Clear: remove the fit from the plot
+- Add: add the fit data to the list of tables (can then be exported)
+
+""")
+
+
+
+TOOLS={
+ 'LogDec': LogDecToolPanel,
+ 'Outlier': OutlierToolPanel,
+ 'Filter': FilterToolPanel,
+ 'Resample': ResampleToolPanel,
+ 'Mask': MaskToolPanel,
+ 'FASTRadialAverage': RadialToolPanel,
+ 'CurveFitting': CurveFitToolPanel,
+}
diff --git a/pydatview/Tables.py b/pydatview/Tables.py
index e77525b..7974b75 100644
--- a/pydatview/Tables.py
+++ b/pydatview/Tables.py
@@ -1,627 +1,731 @@
-import numpy as np
-import os.path
-from dateutil import parser
-import pandas as pd
-import pydatview.fast.fastlib as fastlib
-import pydatview.fast.fastfarm as fastfarm
-try:
- from .common import no_unit, ellude_common, getDt
-except:
- from common import no_unit, ellude_common, getDt
-try:
- import weio # File Formats and File Readers
-except:
- print('')
- print('Error: the python package `weio` was not imported successfully.\n')
- print('Most likely the submodule `weio` was not cloned with `pyDatView`')
- print('Type the following command to retrieve it:\n')
- print(' git submodule update --init --recursive\n')
- print('Alternatively re-clone this repository into a separate folder:\n')
- print(' git clone --recurse-submodules https://github.com/ebranlard/pyDatView\n')
- sys.exit(-1)
-
-
-
-# --------------------------------------------------------------------------------}
-# --- TabList
-# --------------------------------------------------------------------------------{
-class TableList(object): # todo inherit list
- def __init__(self,tabs=[]):
- self._tabs=tabs
- self.Naming='Ellude'
-
- def append(self,t):
- if isinstance(t,list):
- self._tabs += t
- else:
- self._tabs += [t]
-
-
- def from_dataframes(self, dataframes=[], names=[], bAdd=False):
- if not bAdd:
- self.clean() # TODO figure it out
- # Returning a list of tables
- for df,name in zip(dataframes, names):
- if df is not None:
- self.append(Table(data=df, name=name))
-
- def load_tables_from_files(self, filenames=[], fileformat=None, bAdd=False):
- """ load multiple files, only trigger the plot at the end """
- if not bAdd:
- self.clean() # TODO figure it out
- warnList=[]
- for f in filenames:
- if f in self.unique_filenames:
- warn.append('Warn: Cannot add a file already opened ' + f)
- elif len(f)==0:
- pass
- # warn+= 'Warn: an empty filename was skipped' +'\n'
- else:
- tabs, warnloc = self._load_file_tabs(f,fileformat=fileformat)
- if len(warnloc)>0:
- warnList.append(warnloc)
- self.append(tabs)
-
- return warnList
-
- def _load_file_tabs(self,filename,fileformat=None):
- """ load a single file, adds table, and potentially trigger plotting """
- # Returning a list of tables
- tabs=[]
- warn=''
- if not os.path.isfile(filename):
- warn = 'Error: File not found: `'+filename+'`\n'
- return tabs, warn
- try:
- F = weio.read(filename,fileformat = fileformat)
- dfs = F.toDataFrame()
- except weio.FileNotFoundError as e:
- warn = 'Error: A file was not found!\n\n While opening:\n\n {}\n\n the following file was not found:\n\n {}\n'.format(filename, e.filename)
- except IOError:
- warn = 'Error: IO Error thrown while opening file: '+filename+'\n'
- except MemoryError:
- warn='Error: Insufficient memory!\n\nFile: '+filename+'\n\nTry closing and reopening the program, or use a 64 bit version of this program (i.e. of python).\n'
- except weio.EmptyFileError:
- warn='Error: File empty!\n\nFile is empty: '+filename+'\n\nOpen a different file.\n'
- except weio.FormatNotDetectedError:
- warn='Error: File format not detected!\n\nFile: '+filename+'\n\nUse an explicit file-format from the list\n'
- except weio.WrongFormatError as e:
- warn='Error: Wrong file format!\n\nFile: '+filename+'\n\n' \
- 'The file parser for the selected format failed to open the file.\n\n'+ \
- 'The reported error was:\n'+e.args[0]+'\n\n' + \
- 'Double-check your file format and report this error if you think it''s a bug.\n'
- except weio.BrokenFormatError as e:
- warn = 'Error: Inconsistency in the file format!\n\nFile: '+filename+'\n\n' \
- 'The reported error was:\n\n'+e.args[0]+'\n\n' + \
- 'Double-check your file format and report this error if you think it''s a bug.'
- except:
- raise
- if len(warn)>0:
- return tabs, warn
-
- if dfs is None:
- pass
- elif not isinstance(dfs,dict):
- if len(dfs)>0:
- tabs=[Table(data=dfs, filename=filename, fileformat=F.formatName())]
- else:
- for k in list(dfs.keys()):
- if len(dfs[k])>0:
- tabs.append(Table(data=dfs[k], name=str(k), filename=filename, fileformat=F.formatName()))
- if len(tabs)<=0:
- warn='Warn: No dataframe found in file: '+filename+'\n'
- return tabs, warn
-
- def getTabs(self):
- # TODO remove me later
- return self._tabs
-
- def len(self):
- return len(self._tabs)
-
- def haveSameColumns(self,I=None):
- if I is None:
- I=list(range(len(self._tabs)))
- A=[len(self._tabs[i].columns)==len(self._tabs[I[0]].columns) for i in I ]
- if all(A):
- B=[self._tabs[i].columns_clean==self._tabs[I[0]].columns_clean for i in I] #list comparison
- return all(B)
- else:
- return False
-
- def renameTable(self, iTab, newName):
- oldName = self._tabs[iTab].name
- if newName in [t.name for t in self._tabs]:
- raise Exception('Error: This table already exist, choose a different name.')
- # Renaming table
- self._tabs[iTab].rename(newName)
- return oldName
-
- def sort(self, method='byName'):
- if method=='byName':
- tabnames_display=self.getDisplayTabNames()
- self._tabs = [t for _,t in sorted(zip(tabnames_display,self._tabs))]
- else:
- raise Exception('Sorting method unknown: `{}`'.format(method))
-
- def deleteTabs(self, I):
- self._tabs = [t for i,t in enumerate(self._tabs) if i not in I]
-
- def setActiveNames(self,names):
- for t,tn in zip(self._tabs,names):
- t.active_name=tn
-
- def setNaming(self,naming):
- self.Naming=naming
-
- def getDisplayTabNames(self):
- if self.Naming=='Ellude':
- # Temporary hack, using last names if all last names are unique
- names = [t.raw_name for t in self._tabs]
- last_names=[n.split('|')[-1] for n in names]
- if len(np.unique(last_names)) == len(names):
- return ellude_common(last_names)
- else:
- return ellude_common(names)
- elif self.Naming=='FileNames':
- return [os.path.splitext(os.path.basename(t.filename))[0] for t in self._tabs]
- else:
- raise Exception('Table naming unknown: {}'.format(self.Naming))
-
- @property
- def tabNames(self):
- return [t.name for t in self._tabs]
-
- @property
- def filenames(self):
- return [t.filename for t in self._tabs]
-
- @property
- def unique_filenames(self):
- return list(set([t.filename for t in self._tabs]))
-
- def clean(self):
- del self._tabs
- self._tabs=[]
-
- def __repr__(self):
- return '\n'.join([t.__repr__() for t in self._tabs])
-
- # --- Mask related
- @property
- def maskStrings(self):
- return [t.maskString for t in self._tabs]
-
- @property
- def commonMaskString(self):
- maskStrings=set(self.maskStrings)
- if len(maskStrings) == 1:
- return next(iter(maskStrings))
- else:
- return ''
-
- def clearCommonMask(self):
- for t in self._tabs:
- t.clearMask()
-
- def applyCommonMaskString(self,maskString,bAdd=True):
- dfs_new = []
- names_new = []
- errors=[]
- for i,t in enumerate(self._tabs):
- try:
- df_new, name_new = t.applyMaskString(maskString, bAdd=bAdd)
- if df_new is not None:
- # we don't append when string is empty
- dfs_new.append(df_new)
- names_new.append(name_new)
- except:
- errors.append('Mask failed for table: '+t.active_name) # TODO
-
- return dfs_new, names_new, errors
-
- def applyResampling(self,iCol,sampDict,bAdd=True):
- dfs_new = []
- names_new = []
- errors=[]
- for i,t in enumerate(self._tabs):
-# try:
- df_new, name_new = t.applyResampling(iCol,sampDict, bAdd=bAdd)
- if df_new is not None:
- # we don't append when string is empty
- dfs_new.append(df_new)
- names_new.append(name_new)
-# except:
-# errors.append('Resampling failed for table: '+t.active_name) # TODO
-
- return dfs_new, names_new, errors
-
-
-
-
- # --- Radial average related
- def radialAvg(self,avgMethod,avgParam):
- dfs_new = []
- names_new = []
- errors=[]
- for i,t in enumerate(self._tabs):
- dfs, names = t.radialAvg(avgMethod,avgParam)
- for df,n in zip(dfs,names):
- if df is not None:
- dfs_new.append(df)
- names_new.append(n)
- return dfs_new, names_new, errors
-
-
- # --- Element--related functions
- def get(self,i):
- return self._tabs[i]
-
-
-
-# --------------------------------------------------------------------------------}
-# --- Table
-# --------------------------------------------------------------------------------{
-# TODO sort out the naming
-#
-# Main naming concepts:
-# name :
-# active_name :
-# raw_name :
-# filename :
-class Table(object):
- def __init__(self,data=None,name='',filename='',columns=[],fileformat=''):
- # Default init
- self.maskString=''
- self.mask=None
-
- self.filename = filename
- self.fileformat = fileformat
- self.formulas = []
-
- if not isinstance(data,pd.DataFrame):
- # ndarray??
- raise NotImplementedError('Tables that are not dataframe not implemented.')
- else:
- # --- Pandas DataFrame
- self.data = data
- self.columns = self.columnsFromDF(data)
- # --- Trying to figure out how to name this table
- if name is None or len(str(name))==0:
- if data.columns.name is not None:
- name=data.columns.name
-
- self.setupName(name=str(name))
-
- self.convertTimeColumns()
-
-
- def setupName(self,name=''):
- # Creates a "codename": path | basename | name | ext
- splits=[]
- ext=''
- if len(self.filename)>0:
- base_dir = os.path.dirname(self.filename)
- if len(base_dir)==0:
- base_dir= os.getcwd()
- self.filename=os.path.join(base_dir,self.filename)
- splits.append(base_dir.replace('/','|').replace('\\','|'))
- basename,ext=os.path.splitext(os.path.basename(self.filename))
- if len(basename)>0:
- splits.append(basename)
- if name is not None and len(name)>0:
- splits.append(name)
- #if len(ext)>0:
- # splits.append(ext)
- self.extension=ext
- name='|'.join(splits)
- if len(name)==0:
- name='default'
- self.name=name
- self.active_name=self.name
-
-
- def __repr__(self):
- return 'Tab {} ({}x{}) (raw: {}, active: {}, file: {})'.format(self.name,self.nCols,self.nRows,self.raw_name, self.active_name,self.filename)
-
- def columnsFromDF(self,df):
- return [s.replace('_',' ') for s in df.columns.values.astype(str)]
-
-
- def clearMask(self):
- self.maskString=''
- self.mask=None
-
- def addLabelToName(self,label):
- print('raw_name',self.raw_name)
- raw_name=self.raw_name
- sp=raw_name.split('|')
- print(sp)
-
- def applyMaskString(self,maskString,bAdd=True):
- df = self.data
- Index = np.array(range(df.shape[0]))
- sMask=maskString.replace('{Index}','Index')
- for i,c in enumerate(self.columns):
- c_no_unit = no_unit(c).strip()
- c_in_df = df.columns[i]
- # TODO sort out the mess with asarray (introduced to have and/or
- # as array won't work with date comparison
- # NOTE: using iloc to avoid duplicates column issue
- if isinstance(df.iloc[0,i], pd._libs.tslibs.timestamps.Timestamp):
- sMask=sMask.replace('{'+c_no_unit+'}','df[\''+c_in_df+'\']')
- else:
- sMask=sMask.replace('{'+c_no_unit+'}','np.asarray(df[\''+c_in_df+'\'])')
- df_new = None
- name_new = None
- if len(sMask.strip())>0 and sMask.strip().lower()!='no mask':
- try:
- mask = np.asarray(eval(sMask))
- if bAdd:
- df_new = df[mask]
- name_new=self.raw_name+'_masked'
- else:
- self.mask=mask
- self.maskString=maskString
- except:
- raise Exception('Error: The mask failed for table: '+self.name)
- return df_new, name_new
-
- def applyResampling(self,iCol,sampDict,bAdd=True):
- from pydatview.tools.signal import applySamplerDF
- if iCol==0:
- raise Exception('Cannot resample based on index')
- colName=self.data.columns[iCol-1]
- df_new =applySamplerDF(self.data, colName, sampDict=sampDict)
- df_new
- if bAdd:
- name_new=self.raw_name+'_resampled'
- else:
- name_new=None
- self.data=df_new
- return df_new, name_new
-
-
- def radialAvg(self,avgMethod, avgParam):
- df = self.data
- base,out_ext = os.path.splitext(self.filename)
-
- # --- Detect if it's a FAST Farm file
- sCols = ''.join(df.columns)
- if sCols.find('WkDf')>1 or sCols.find('CtT')>0:
- # --- FAST FARM files
- Files=[base+ext for ext in ['.fstf','.FSTF','.Fstf','.fmas','.FMAS','.Fmas'] if os.path.exists(base+ext)]
- if len(Files)==0:
- fst_in=None
- #raise Exception('Error: No .fstf file found with name: '+base+'.fstf')
- else:
- fst_in=Files[0]
-
- dfRad,_,dfDiam = fastfarm.spanwisePostProFF(fst_in,avgMethod=avgMethod,avgParam=avgParam,D=1,df=df)
- dfs_new = [dfRad,dfDiam]
- names_new=[self.raw_name+'_rad',self.raw_name+'_diam']
- else:
- # --- FAST files
-
- # HACK for AD file to find the right .fst file
- iDotAD=base.lower().find('.ad')
- if iDotAD>1:
- base=base[:iDotAD]
- #
- Files=[base+ext for ext in ['.fst','.FST','.Fst','.dvr','.Dvr','.DVR'] if os.path.exists(base+ext)]
- if len(Files)==0:
- fst_in=None
- #raise Exception('Error: No .fst file found with name: '+base+'.fst')
- else:
- fst_in=Files[0]
-
-
- dfRadED, dfRadAD, dfRadBD= fastlib.spanwisePostPro(fst_in, avgMethod=avgMethod, avgParam=avgParam, out_ext=out_ext, df = self.data)
- dfs_new = [dfRadAD, dfRadED, dfRadBD]
- names_new=[self.raw_name+'_AD', self.raw_name+'_ED', self.raw_name+'_BD']
- return dfs_new, names_new
-
- def convertTimeColumns(self):
- if len(self.data)>0:
- for i,c in enumerate(self.data.columns.values):
- y = self.data.iloc[:,i]
- if y.dtype == np.object:
- if isinstance(y.values[0], str):
- # tring to convert to date
- try:
- parser.parse(y.values[0])
- isDate=True
- except:
- if y.values[0]=='NaT':
- isDate=True
- else:
- isDate=False
- if isDate:
- try:
- self.data[c]=pd.to_datetime(self.data[c].values).to_pydatetime()
- print('Column {} converted to datetime'.format(c))
- except:
- # Happens if values are e.g. "Monday, Tuesday"
- print('Conversion to datetime failed, column {} inferred as string'.format(c))
- else:
- print('Column {} inferred as string'.format(c))
- elif isinstance(y.values[0], (float, int)):
- try:
- self.data[c]=self.data[c].astype(float)
- print('Column {} converted to float (likely nan)'.format(c))
- except:
- self.data[c]=self.data[c].astype(str)
- print('Column {} inferred and converted to string'.format(c))
- else :
- print('>> Unknown type:',type(y.values[0]))
- #print(self.data.dtypes)
-
- def renameColumn(self,iCol,newName):
- self.columns[iCol]=newName
- self.data.columns.values[iCol]=newName
-
- def deleteColumns(self,ICol):
- """ Delete columns by index, not column names which can have duplicates"""
- IKeep =[i for i in np.arange(self.data.shape[1]) if i not in ICol]
- self.data = self.data.iloc[:, IKeep] # Drop won't work for duplicates
- for i in sorted(ICol, reverse=True):
- del(self.columns[i])
- for f in self.formulas:
- if f['pos'] == (i + 1):
- self.formulas.remove(f)
- break
- for f in self.formulas:
- if f['pos'] > (i + 1):
- f['pos'] = f['pos'] - 1
-
- def rename(self,new_name):
- self.name='>'+new_name
-
- def addColumn(self,sNewName,NewCol,i=-1,sFormula=''):
- if i<0:
- i=self.data.shape[1]
- self.data.insert(int(i),sNewName,NewCol)
- self.columns=self.columnsFromDF(self.data)
- for f in self.formulas:
- if f['pos'] > i:
- f['pos'] = f['pos'] + 1
- self.formulas.append({'pos': i+1, 'formula': sFormula, 'name': sNewName})
-
- def setColumn(self,sNewName,NewCol,i,sFormula=''):
- if i<1:
- raise ValueError('Cannot set column at position ' + str(i))
- self.data = self.data.drop(columns=self.data.columns[i-1])
- self.data.insert(int(i-1),sNewName,NewCol)
- self.columns=self.columnsFromDF(self.data)
- for f in self.formulas:
- if f['pos'] == i:
- f['name'] = sNewName
- f['formula'] = sFormula
-
- def getColumn(self,i):
- """ Return column of data, where i=0 is the index column
- If a mask exist, the mask is applied
- """
- if i <= 0 :
- x = np.array(range(self.data.shape[0]))
- if self.mask is not None:
- x=x[self.mask]
-
- c = None
- isString = False
- isDate = False
- else:
- if self.mask is not None:
- c = self.data.iloc[self.mask, i-1]
- x = self.data.iloc[self.mask, i-1].values
- else:
- c = self.data.iloc[:, i-1]
- x = self.data.iloc[:, i-1].values
-
- isString = c.dtype == np.object and isinstance(c.values[0], str)
- if isString:
- x=x.astype(str)
- isDate = np.issubdtype(c.dtype, np.datetime64)
- if isDate:
- dt=getDt(x)
- if dt>1:
- x=x.astype('datetime64[s]')
- else:
- x=x.astype('datetime64')
- return x,isString,isDate,c
-
-
-
- def evalFormula(self,sFormula):
- df = self.data
- Index = np.array(range(df.shape[0]))
- sFormula=sFormula.replace('{Index}','Index')
- for i,c in enumerate(self.columns):
- c_no_unit = no_unit(c).strip()
- c_in_df = df.columns[i]
- sFormula=sFormula.replace('{'+c_no_unit+'}','df[\''+c_in_df+'\']')
- #print(sFormula)
- try:
- NewCol=eval(sFormula)
- return NewCol
- except:
- return None
-
- def addColumnByFormula(self,sNewName,sFormula,i=-1):
- NewCol=self.evalFormula(sFormula)
- if NewCol is None:
- return False
- else:
- self.addColumn(sNewName,NewCol,i,sFormula)
- return True
-
- def setColumnByFormula(self,sNewName,sFormula,i=-1):
- NewCol=self.evalFormula(sFormula)
- if NewCol is None:
- return False
- else:
- self.setColumn(sNewName,NewCol,i,sFormula)
- return True
-
-
- def export(self,path):
- if isinstance(self.data, pd.DataFrame):
- try:
- self.data.to_csv(path,sep=',',index=False) #python3
- except:
- self.data.to_csv(path,sep=str(u',').encode('utf-8'),index=False) #python 2.
- else:
- raise NotImplementedError('Export of data that is not a dataframe')
-
-
-
- @property
- def basename(self):
- return os.path.splitext(os.path.basename(self.filename))[0]
-
- @property
- def shapestring(self):
- return '{}x{}'.format(self.nCols, self.nRows)
-
- @property
- def shape(self):
- return (self.nRows, self.nCols)
-
-
- @property
- def columns_clean(self):
- return [no_unit(s) for s in self.columns]
-
- @property
- def name(self):
- if len(self.__name)<=0:
- return ''
- if self.__name[0]=='>':
- return self.__name[1:]
- else:
- return self.__name
-
- @property
- def raw_name(self):
- return self.__name
-
- @name.setter
- def name(self,new_name):
- self.__name=new_name
-
- @property
- def nCols(self):
- return len(self.columns)
-
- @property
- def nRows(self):
- return len(self.data.iloc[:,0]) # TODO if not panda
-
-
-if __name__ == '__main__':
- import pandas as pd;
- from Tables import Table
- import numpy as np
-
- def OnTabPopup(event):
- self.PopupMenu(TablePopup(self,selPanel.tabPanel.lbTab), event.GetPosition())
+import numpy as np
+import os.path
+from dateutil import parser
+import pandas as pd
+try:
+ from .common import no_unit, ellude_common, getDt
+except:
+ from common import no_unit, ellude_common, getDt
+try:
+ import weio.weio as weio# File Formats and File Readers
+except:
+ print('')
+ print('Error: the python package `weio` was not imported successfully.\n')
+ print('Most likely the submodule `weio` was not cloned with `pyDatView`')
+ print('Type the following command to retrieve it:\n')
+ print(' git submodule update --init --recursive\n')
+ print('Alternatively re-clone this repository into a separate folder:\n')
+ print(' git clone --recurse-submodules https://github.com/ebranlard/pyDatView\n')
+ sys.exit(-1)
+
+
+
+# --------------------------------------------------------------------------------}
+# --- TabList
+# --------------------------------------------------------------------------------{
+class TableList(object): # todo inherit list
+ def __init__(self,tabs=[]):
+ self._tabs=tabs
+ self.Naming='Ellude'
+
+ # --- behaves like a list...
+ def __iter__(self):
+ self.__n = 0
+ return self
+
+ def __next__(self):
+ if self.__n < len(self._tabs):
+ self.__n += 1
+ return self._tabs[self.__n-1]
+ else:
+ raise StopIteration
+
+ def append(self,t):
+ if isinstance(t,list):
+ self._tabs += t
+ else:
+ self._tabs += [t]
+
+ # --- Main high level methods
+ def from_dataframes(self, dataframes=[], names=[], bAdd=False):
+ if not bAdd:
+ self.clean() # TODO figure it out
+ # Returning a list of tables
+ for df,name in zip(dataframes, names):
+ if df is not None:
+ self.append(Table(data=df, name=name))
+
+ def load_tables_from_files(self, filenames=[], fileformats=None, bAdd=False):
+ """ load multiple files into table list"""
+ if not bAdd:
+ self.clean() # TODO figure it out
+
+ if fileformats is None:
+ fileformats=[None]*len(filenames)
+ assert type(fileformats) ==list, 'fileformats must be a list'
+
+ # Loop through files, appending tables within files
+ warnList=[]
+ for f,ff in zip(filenames, fileformats):
+ if f in self.unique_filenames:
+ warnList.append('Warn: Cannot add a file already opened ' + f)
+ elif len(f)==0:
+ pass
+ # warn+= 'Warn: an empty filename was skipped' +'\n'
+ else:
+ tabs, warnloc = self._load_file_tabs(f,fileformat=ff)
+ if len(warnloc)>0:
+ warnList.append(warnloc)
+ self.append(tabs)
+
+ return warnList
+
+ def _load_file_tabs(self, filename, fileformat=None):
+ """ load a single file, adds table """
+ # Returning a list of tables
+ tabs=[]
+ warn=''
+ if not os.path.isfile(filename):
+ warn = 'Error: File not found: `'+filename+'`\n'
+ return tabs, warn
+ try:
+ #F = weio.read(filename, fileformat = fileformat)
+ # --- Expanded version of weio.read
+ F = None
+ if fileformat is None:
+ fileformat, F = weio.detectFormat(filename)
+ # Reading the file with the appropriate class if necessary
+ if not isinstance(F, fileformat.constructor):
+ F=fileformat.constructor(filename=filename)
+ dfs = F.toDataFrame()
+ except weio.FileNotFoundError as e:
+ warn = 'Error: A file was not found!\n\n While opening:\n\n {}\n\n the following file was not found:\n\n {}\n'.format(filename, e.filename)
+ except IOError:
+ warn = 'Error: IO Error thrown while opening file: '+filename+'\n'
+ except MemoryError:
+ warn='Error: Insufficient memory!\n\nFile: '+filename+'\n\nTry closing and reopening the program, or use a 64 bit version of this program (i.e. of python).\n'
+ except weio.EmptyFileError:
+ warn='Error: File empty!\n\nFile is empty: '+filename+'\n\nOpen a different file.\n'
+ except weio.FormatNotDetectedError:
+ warn='Error: File format not detected!\n\nFile: '+filename+'\n\nUse an explicit file-format from the list\n'
+ except weio.WrongFormatError as e:
+ warn='Error: Wrong file format!\n\nFile: '+filename+'\n\n' \
+ 'The file parser for the selected format failed to open the file.\n\n'+ \
+ 'The reported error was:\n'+e.args[0]+'\n\n' + \
+ 'Double-check your file format and report this error if you think it''s a bug.\n'
+ except weio.BrokenFormatError as e:
+ warn = 'Error: Inconsistency in the file format!\n\nFile: '+filename+'\n\n' \
+ 'The reported error was:\n\n'+e.args[0]+'\n\n' + \
+ 'Double-check your file format and report this error if you think it''s a bug.'
+ except:
+ raise
+ if len(warn)>0:
+ return tabs, warn
+
+ if dfs is None:
+ pass
+ elif not isinstance(dfs,dict):
+ if len(dfs)>0:
+ tabs=[Table(data=dfs, filename=filename, fileformat=fileformat)]
+ else:
+ for k in list(dfs.keys()):
+ if len(dfs[k])>0:
+ tabs.append(Table(data=dfs[k], name=str(k), filename=filename, fileformat=fileformat))
+ if len(tabs)<=0:
+ warn='Warn: No dataframe found in file: '+filename+'\n'
+ return tabs, warn
+
+ def getTabs(self):
+ # TODO remove me later
+ return self._tabs
+
+ def len(self):
+ return len(self._tabs)
+
+ def haveSameColumns(self,I=None):
+ if I is None:
+ I=list(range(len(self._tabs)))
+ A=[len(self._tabs[i].columns)==len(self._tabs[I[0]].columns) for i in I ]
+ if all(A):
+ B=[self._tabs[i].columns_clean==self._tabs[I[0]].columns_clean for i in I] #list comparison
+ return all(B)
+ else:
+ return False
+
+ def renameTable(self, iTab, newName):
+ oldName = self._tabs[iTab].name
+ if newName in [t.name for t in self._tabs]:
+ raise Exception('Error: This table already exist, choose a different name.')
+ # Renaming table
+ self._tabs[iTab].rename(newName)
+ return oldName
+
+ def sort(self, method='byName'):
+ if method=='byName':
+ tabnames_display=self.getDisplayTabNames()
+ self._tabs = [t for _,t in sorted(zip(tabnames_display,self._tabs))]
+ else:
+ raise Exception('Sorting method unknown: `{}`'.format(method))
+
+ def deleteTabs(self, I):
+ self._tabs = [t for i,t in enumerate(self._tabs) if i not in I]
+
+ def setActiveNames(self,names):
+ for t,tn in zip(self._tabs,names):
+ t.active_name=tn
+
+ def setNaming(self,naming):
+ self.Naming=naming
+
+ def getDisplayTabNames(self):
+ if self.Naming=='Ellude':
+ # Temporary hack, using last names if all last names are unique
+ names = [t.raw_name for t in self._tabs]
+ last_names=[n.split('|')[-1] for n in names]
+ if len(np.unique(last_names)) == len(names):
+ return ellude_common(last_names)
+ else:
+ return ellude_common(names)
+ elif self.Naming=='FileNames':
+ return [os.path.splitext(os.path.basename(t.filename))[0] for t in self._tabs]
+ else:
+ raise Exception('Table naming unknown: {}'.format(self.Naming))
+
+ # --- Properties
+ @property
+ def tabNames(self):
+ return [t.name for t in self._tabs]
+
+ @property
+ def filenames(self):
+ return [t.filename for t in self._tabs]
+
+ @property
+ def fileformats(self):
+ return [t.fileformat for t in self._tabs]
+
+ @property
+ def unique_filenames(self):
+ return list(set([t.filename for t in self._tabs]))
+
+ @property
+ def filenames_and_formats(self):
+ """ return unique list of filenames with associated fileformats """
+ filenames = []
+ fileformats = []
+ for t in self._tabs:
+ if t.filename not in filenames:
+ filenames.append(t.filename)
+ fileformats.append(t.fileformat)
+ return filenames, fileformats
+
+ def clean(self):
+ del self._tabs
+ self._tabs=[]
+
+ def __repr__(self):
+ return '\n'.join([t.__repr__() for t in self._tabs])
+
+ # --- Mask related
+ @property
+ def maskStrings(self):
+ return [t.maskString for t in self._tabs]
+
+ @property
+ def commonMaskString(self):
+ maskStrings=set(self.maskStrings)
+ if len(maskStrings) == 1:
+ return next(iter(maskStrings))
+ else:
+ return ''
+
+ def clearCommonMask(self):
+ for t in self._tabs:
+ t.clearMask()
+
+ def applyCommonMaskString(self,maskString,bAdd=True):
+ dfs_new = []
+ names_new = []
+ errors=[]
+ for i,t in enumerate(self._tabs):
+ try:
+ df_new, name_new = t.applyMaskString(maskString, bAdd=bAdd)
+ if df_new is not None:
+ # we don't append when string is empty
+ dfs_new.append(df_new)
+ names_new.append(name_new)
+ except:
+ errors.append('Mask failed for table: '+t.active_name) # TODO
+
+ return dfs_new, names_new, errors
+
+ # --- Resampling TODO MOVE THIS OUT OF HERE OR UNIFY
+ def applyResampling(self,iCol,sampDict,bAdd=True):
+ dfs_new = []
+ names_new = []
+ errors=[]
+ for i,t in enumerate(self._tabs):
+# try:
+ df_new, name_new = t.applyResampling(iCol,sampDict, bAdd=bAdd)
+ if df_new is not None:
+ # we don't append when string is empty
+ dfs_new.append(df_new)
+ names_new.append(name_new)
+# except:
+# errors.append('Resampling failed for table: '+t.active_name) # TODO
+ return dfs_new, names_new, errors
+
+ # --- Filtering TODO MOVE THIS OUT OF HERE OR UNIFY
+ def applyFiltering(self,iCol,options,bAdd=True):
+ dfs_new = []
+ names_new = []
+ errors=[]
+ for i,t in enumerate(self._tabs):
+# try:
+ df_new, name_new = t.applyFiltering(iCol, options, bAdd=bAdd)
+ if df_new is not None:
+ # we don't append when string is empty
+ dfs_new.append(df_new)
+ names_new.append(name_new)
+# except:
+# errors.append('Resampling failed for table: '+t.active_name) # TODO
+ return dfs_new, names_new, errors
+
+
+
+
+ # --- Radial average related
+ def radialAvg(self,avgMethod,avgParam):
+ dfs_new = []
+ names_new = []
+ errors=[]
+ for i,t in enumerate(self._tabs):
+ dfs, names = t.radialAvg(avgMethod,avgParam)
+ for df,n in zip(dfs,names):
+ if df is not None:
+ dfs_new.append(df)
+ names_new.append(n)
+ return dfs_new, names_new, errors
+
+
+ # --- Element--related functions
+ def get(self,i):
+ return self._tabs[i]
+
+
+
+# --------------------------------------------------------------------------------}
+# --- Table
+# --------------------------------------------------------------------------------{
+#
+class Table(object):
+ """
+ Main attributes:
+ - data
+ - columns
+ - name
+ - raw_name
+ - active_name
+ - filename
+ - fileformat
+ - fileformat_name
+ - nCols x nRows
+ - mask
+ - maskString
+ - formulas
+ """
+ # TODO sort out the naming
+ # Main naming concepts:
+ # name :
+ # active_name :
+ # raw_name :
+ # filename :
+ def __init__(self,data=None,name='',filename='',columns=[], fileformat=None):
+ # Default init
+ self.maskString=''
+ self.mask=None
+
+ self.filename = filename
+ self.fileformat = fileformat
+ if fileformat is not None:
+ self.fileformat_name = fileformat.name
+ else:
+ self.fileformat_name = ''
+ self.formulas = []
+
+ if not isinstance(data,pd.DataFrame):
+ # ndarray??
+ raise NotImplementedError('Tables that are not dataframe not implemented.')
+ else:
+ # --- Pandas DataFrame
+ self.data = data
+ self.columns = self.columnsFromDF(data)
+ # --- Trying to figure out how to name this table
+ if name is None or len(str(name))==0:
+ if data.columns.name is not None:
+ name=data.columns.name
+
+ self.setupName(name=str(name))
+
+ self.convertTimeColumns()
+
+
+ def setupName(self,name=''):
+ # Creates a "codename": path | basename | name | ext
+ splits=[]
+ ext=''
+ if len(self.filename)>0:
+ base_dir = os.path.dirname(self.filename)
+ if len(base_dir)==0:
+ base_dir= os.getcwd()
+ self.filename=os.path.join(base_dir,self.filename)
+ splits.append(base_dir.replace('/','|').replace('\\','|'))
+ basename,ext=os.path.splitext(os.path.basename(self.filename))
+ if len(basename)>0:
+ splits.append(basename)
+ if name is not None and len(name)>0:
+ splits.append(name)
+ #if len(ext)>0:
+ # splits.append(ext)
+ self.extension=ext
+ name='|'.join(splits)
+ if len(name)==0:
+ name='default'
+ self.name=name
+ self.active_name=self.name
+
+
+ def __repr__(self):
+ s='Table object:\n'
+ s+=' - name: {}\n'.format(self.name)
+ s+=' - raw_name : {}\n'.format(self.raw_name)
+ s+=' - active_name: {}\n'.format(self.raw_name)
+ s+=' - filename : {}\n'.format(self.filename)
+ s+=' - fileformat : {}\n'.format(self.fileformat)
+ s+=' - fileformat_name : {}\n'.format(self.fileformat_name)
+ s+=' - columns : {}\n'.format(self.columns)
+ s+=' - nCols x nRows: {}x{}\n'.format(self.nCols, self.nRows)
+ return s
+
+ def columnsFromDF(self,df):
+ return [s.replace('_',' ') for s in df.columns.values.astype(str)]
+
+ # --- Mask
+ def clearMask(self):
+ self.maskString=''
+ self.mask=None
+
+ def applyMaskString(self,maskString,bAdd=True):
+ df = self.data
+ Index = np.array(range(df.shape[0]))
+ sMask=maskString.replace('{Index}','Index')
+ for i,c in enumerate(self.columns):
+ c_no_unit = no_unit(c).strip()
+ c_in_df = df.columns[i]
+ # TODO sort out the mess with asarray (introduced to have and/or
+ # as array won't work with date comparison
+ # NOTE: using iloc to avoid duplicates column issue
+ if isinstance(df.iloc[0,i], pd._libs.tslibs.timestamps.Timestamp):
+ sMask=sMask.replace('{'+c_no_unit+'}','df[\''+c_in_df+'\']')
+ else:
+ sMask=sMask.replace('{'+c_no_unit+'}','np.asarray(df[\''+c_in_df+'\'])')
+ df_new = None
+ name_new = None
+ if len(sMask.strip())>0 and sMask.strip().lower()!='no mask':
+ try:
+ mask = np.asarray(eval(sMask))
+ if bAdd:
+ df_new = df[mask]
+ name_new=self.raw_name+'_masked'
+ else:
+ self.mask=mask
+ self.maskString=maskString
+ except:
+ raise Exception('Error: The mask failed for table: '+self.name)
+ return df_new, name_new
+
+ # --- Important manipulation TODO MOVE THIS OUT OF HERE OR UNIFY
+ def applyResampling(self, iCol, sampDict, bAdd=True):
+ from pydatview.tools.signal import applySamplerDF
+ if iCol==0:
+ raise Exception('Cannot resample based on index')
+ colName=self.data.columns[iCol-1]
+ df_new =applySamplerDF(self.data, colName, sampDict=sampDict)
+ df_new
+ if bAdd:
+ name_new=self.raw_name+'_resampled'
+ else:
+ name_new=None
+ self.data=df_new
+ return df_new, name_new
+
+ def applyFiltering(self, iCol, options, bAdd=True):
+ from pydatview.tools.signal import applyFilterDF
+ if iCol==0:
+ raise Exception('Cannot filter based on index')
+ colName=self.data.columns[iCol-1]
+ df_new =applyFilterDF(self.data, colName, options)
+ df_new
+ if bAdd:
+ name_new=self.raw_name+'_filtered'
+ else:
+ name_new=None
+ self.data=df_new
+ return df_new, name_new
+
+
+ def radialAvg(self,avgMethod, avgParam):
+ import pydatview.fast.fastlib as fastlib
+ import pydatview.fast.fastfarm as fastfarm
+ df = self.data
+ base,out_ext = os.path.splitext(self.filename)
+
+ # --- Detect if it's a FAST Farm file
+ sCols = ''.join(df.columns)
+ if sCols.find('WkDf')>1 or sCols.find('CtT')>0:
+ # --- FAST FARM files
+ Files=[base+ext for ext in ['.fstf','.FSTF','.Fstf','.fmas','.FMAS','.Fmas'] if os.path.exists(base+ext)]
+ if len(Files)==0:
+ fst_in=None
+ #raise Exception('Error: No .fstf file found with name: '+base+'.fstf')
+ else:
+ fst_in=Files[0]
+
+ dfRad,_,dfDiam = fastfarm.spanwisePostProFF(fst_in,avgMethod=avgMethod,avgParam=avgParam,D=1,df=df)
+ dfs_new = [dfRad,dfDiam]
+ names_new=[self.raw_name+'_rad',self.raw_name+'_diam']
+ else:
+ # --- FAST files
+
+ # HACK for AD file to find the right .fst file
+ iDotAD=base.lower().find('.ad')
+ if iDotAD>1:
+ base=base[:iDotAD]
+ #
+ Files=[base+ext for ext in ['.fst','.FST','.Fst','.dvr','.Dvr','.DVR'] if os.path.exists(base+ext)]
+ if len(Files)==0:
+ fst_in=None
+ #raise Exception('Error: No .fst file found with name: '+base+'.fst')
+ else:
+ fst_in=Files[0]
+
+
+ dfRadED, dfRadAD, dfRadBD= fastlib.spanwisePostPro(fst_in, avgMethod=avgMethod, avgParam=avgParam, out_ext=out_ext, df = self.data)
+ dfs_new = [dfRadAD, dfRadED, dfRadBD]
+ names_new=[self.raw_name+'_AD', self.raw_name+'_ED', self.raw_name+'_BD']
+ return dfs_new, names_new
+
+ def changeUnits(self, flavor='WE'):
+ """ Change units of the table """
+ # NOTE: moved to a plugin, but interface kept
+ from pydatview.plugins.data_standardizeUnits import changeUnits
+ changeUnits(self, flavor=flavor)
+
+ def convertTimeColumns(self):
+ if len(self.data)>0:
+ for i,c in enumerate(self.data.columns.values):
+ y = self.data.iloc[:,i]
+ if y.dtype == object:
+ if isinstance(y.values[0], str):
+ # tring to convert to date
+ try:
+ parser.parse(y.values[0])
+ isDate=True
+ except:
+ if y.values[0]=='NaT':
+ isDate=True
+ else:
+ isDate=False
+ if isDate:
+ try:
+ self.data[c]=pd.to_datetime(self.data[c].values).to_pydatetime()
+ print('Column {} converted to datetime'.format(c))
+ except:
+ # Happens if values are e.g. "Monday, Tuesday"
+ print('Conversion to datetime failed, column {} inferred as string'.format(c))
+ else:
+ print('Column {} inferred as string'.format(c))
+ elif isinstance(y.values[0], (float, int)):
+ try:
+ self.data[c]=self.data[c].astype(float)
+ print('Column {} converted to float (likely nan)'.format(c))
+ except:
+ self.data[c]=self.data[c].astype(str)
+ print('Column {} inferred and converted to string'.format(c))
+ else :
+ print('>> Unknown type:',type(y.values[0]))
+ #print(self.data.dtypes)
+
+
+ # --- Column manipulations
+ def renameColumn(self,iCol,newName):
+ self.columns[iCol]=newName
+ self.data.columns.values[iCol]=newName
+
+ def deleteColumns(self,ICol):
+ """ Delete columns by index, not column names which can have duplicates"""
+ IKeep =[i for i in np.arange(self.data.shape[1]) if i not in ICol]
+ self.data = self.data.iloc[:, IKeep] # Drop won't work for duplicates
+ for i in sorted(ICol, reverse=True):
+ del(self.columns[i])
+ for f in self.formulas:
+ if f['pos'] == (i + 1):
+ self.formulas.remove(f)
+ break
+ for f in self.formulas:
+ if f['pos'] > (i + 1):
+ f['pos'] = f['pos'] - 1
+
+ def rename(self,new_name):
+ self.name='>'+new_name
+
+ def addColumn(self,sNewName,NewCol,i=-1,sFormula=''):
+ if i<0:
+ i=self.data.shape[1]
+ elif i>self.data.shape[1]+1:
+ i=self.data.shape[1]
+ self.data.insert(int(i),sNewName,NewCol)
+ self.columns=self.columnsFromDF(self.data)
+ for f in self.formulas:
+ if f['pos'] > i:
+ f['pos'] = f['pos'] + 1
+ self.formulas.append({'pos': i+1, 'formula': sFormula, 'name': sNewName})
+
+ def setColumn(self,sNewName,NewCol,i,sFormula=''):
+ if i<1:
+ raise ValueError('Cannot set column at position ' + str(i))
+ self.data = self.data.drop(columns=self.data.columns[i-1])
+ self.data.insert(int(i-1),sNewName,NewCol)
+ self.columns=self.columnsFromDF(self.data)
+ for f in self.formulas:
+ if f['pos'] == i:
+ f['name'] = sNewName
+ f['formula'] = sFormula
+
+ def getColumn(self,i):
+ """ Return column of data, where i=0 is the index column
+ If a mask exist, the mask is applied
+
+ TODO TODO TODO get rid of this!
+ """
+ if i <= 0 :
+ x = np.array(range(self.data.shape[0]))
+ if self.mask is not None:
+ x=x[self.mask]
+
+ c = None
+ isString = False
+ isDate = False
+ else:
+ if self.mask is not None:
+ c = self.data.iloc[self.mask, i-1]
+ x = self.data.iloc[self.mask, i-1].values
+ else:
+ c = self.data.iloc[:, i-1]
+ x = self.data.iloc[:, i-1].values
+
+ isString = c.dtype == np.object and isinstance(c.values[0], str)
+ if isString:
+ x=x.astype(str)
+ isDate = np.issubdtype(c.dtype, np.datetime64)
+ if isDate:
+ dt=getDt(x)
+ if dt>1:
+ x=x.astype('datetime64[s]')
+ else:
+ x=x.astype('datetime64')
+ return x,isString,isDate,c
+
+
+ def evalFormula(self,sFormula):
+ df = self.data
+ Index = np.array(range(df.shape[0]))
+ sFormula=sFormula.replace('{Index}','Index')
+ for i,c in enumerate(self.columns):
+ c_no_unit = no_unit(c).strip()
+ c_in_df = df.columns[i]
+ sFormula=sFormula.replace('{'+c_no_unit+'}','df[\''+c_in_df+'\']')
+ try:
+ NewCol=eval(sFormula)
+ return NewCol
+ except:
+ return None
+
+ def addColumnByFormula(self,sNewName,sFormula,i=-1):
+ NewCol=self.evalFormula(sFormula)
+ if NewCol is None:
+ return False
+ else:
+ self.addColumn(sNewName,NewCol,i,sFormula)
+ return True
+
+ def setColumnByFormula(self,sNewName,sFormula,i=-1):
+ NewCol=self.evalFormula(sFormula)
+ if NewCol is None:
+ return False
+ else:
+ self.setColumn(sNewName,NewCol,i,sFormula)
+ return True
+
+
+ def export(self,path):
+ if isinstance(self.data, pd.DataFrame):
+ try:
+ self.data.to_csv(path,sep=',',index=False) #python3
+ except:
+ self.data.to_csv(path,sep=str(u',').encode('utf-8'),index=False) #python 2.
+ else:
+ raise NotImplementedError('Export of data that is not a dataframe')
+
+
+
+ # --- Properties
+ @property
+ def basename(self):
+ return os.path.splitext(os.path.basename(self.filename))[0]
+
+ @property
+ def shapestring(self):
+ return '{}x{}'.format(self.nCols, self.nRows)
+
+ @property
+ def shape(self):
+ return (self.nRows, self.nCols)
+
+ @property
+ def columns_clean(self):
+ return [no_unit(s) for s in self.columns]
+
+ @property
+ def name(self):
+ if len(self.__name)<=0:
+ return ''
+ if self.__name[0]=='>':
+ return self.__name[1:]
+ else:
+ return self.__name
+
+ @property
+ def raw_name(self):
+ return self.__name
+
+ @name.setter
+ def name(self,new_name):
+ self.__name=new_name
+
+ @property
+ def nCols(self):
+ return len(self.columns)
+
+ @property
+ def nRows(self):
+ return len(self.data.iloc[:,0]) # TODO if not panda
+
+
+if __name__ == '__main__':
+ import pandas as pd;
+ from Tables import Table
+ import numpy as np
+
+ def OnTabPopup(event):
+ self.PopupMenu(TablePopup(self,selPanel.tabPanel.lbTab), event.GetPosition())
diff --git a/pydatview/appdata.py b/pydatview/appdata.py
new file mode 100644
index 0000000..f801565
--- /dev/null
+++ b/pydatview/appdata.py
@@ -0,0 +1,147 @@
+import json
+import os
+from weio.weio import defaultUserDataDir
+from .GUICommon import Error
+
+
+def configFilePath():
+ return os.path.join(defaultUserDataDir(), 'pyDatView', 'pyDatView.json')
+
+def loadAppData(mainframe):
+ configFile = configFilePath()
+ os.makedirs(os.path.dirname(configFile), exist_ok=True)
+ data = defaultAppData(mainframe)
+ #print('>>> configFile', configFile)
+ #print('Default Data content:\n')
+ #for k,v in data.items():
+ # print('{:20s}: {}\n'.format(k,v))
+ if os.path.exists(configFile):
+ sError=''
+ try:
+ with open(configFile) as f:
+ data2 = json.load(f)
+ except:
+ sError='Error: pyDatView config file is not properly formatted.\n\n'
+ sError+='The config file was at the following location:\n {}\n\n'.format(configFile)
+
+ configFileBkp = configFile+'_bkp'
+ import shutil
+ try:
+ shutil.copy2(configFile, configFileBkp)
+ sError+='A backup of the file was made at the following location:\n {}\n\n'.format(configFileBkp)
+ backup=True
+ except:
+ backup=False
+ if backup:
+ try:
+ os.remove(configFile)
+ except:
+ sError+='To solve this issue, the config file was deleted.\n\n'
+ else:
+ sError+='A backup of the file could not be made and the file was not deleted\n\n'
+ sError+='If the problem persists, post an issue on the github repository\n'
+ #raise Exception(sError)
+ Error(mainframe, sError)
+ if len(sError)==0:
+ # Merging only what overlaps between default and user file
+ # --- Level 1
+ for k1,v1 in data2.items():
+ if k1 in data.keys():
+ if type(data[k1]) is dict:
+ # --- Level 2
+ for k2,v2 in v1.items():
+ if k2 in data[k1].keys():
+ if type(data[k1][k2]) is dict:
+ # --- Level 3
+ for k3,v3 in v2.items():
+ if k3 in data[k1][k2].keys():
+ data[k1][k2][k3]=v3
+ else:
+ data[k1][k2]=v2
+ else:
+ data[k1]=v1
+ #print('Data content on load:\n')
+ #for k,v in data.items():
+ # print('{:20s}: {}\n'.format(k,v))
+ return data
+
+def saveAppData(mainFrame, data):
+ from .GUICommon import getFontSize, getMonoFontSize
+ if not mainFrame.datareset:
+ # --- Import data from GUI
+ data['fontSize'] = int(getFontSize())
+ data['monoFontSize'] = int(getMonoFontSize())
+ if hasattr(mainFrame, 'plotPanel'):
+ savePlotPanelData(data['plotPanel'], mainFrame.plotPanel)
+ if hasattr(mainFrame, 'plotPanel'):
+ saveInfoPanelData(data['infoPanel'], mainFrame.infoPanel)
+
+ # --- Write config file
+ configFile = configFilePath()
+ #print('>>> Writing configFile', configFile)
+ #print('Data content on close:\n')
+ #for k,v in data.items():
+ # print('{:20s}: {}\n'.format(k,v))
+ try:
+ os.makedirs(os.path.dirname(configFile), exist_ok=True)
+ with open(configFile, 'w') as f:
+ json.dump(data, f, indent=2)
+ except:
+ pass
+
+def defaultAppData(mainframe):
+ data={}
+ # --- Main frame data
+ data['windowSize'] = (900,700)
+ data['monoFontSize'] = mainframe.systemFontSize
+ data['fontSize'] = mainframe.systemFontSize
+ #SIDE_COL = [160,160,300,420,530]
+ #SIDE_COL_LARGE = [200,200,360,480,600]
+ #BOT_PANL =85
+ data['plotPanel']=defaultPlotPanelData()
+ data['infoPanel']=defaultInfoPanelData()
+ return data
+
+# --- Plot Panel
+# TODO put this into plotPanel file?
+def savePlotPanelData(data, plotPanel):
+ data['Grid'] = plotPanel.cbGrid.IsChecked()
+ data['CrossHair'] = plotPanel.cbXHair.IsChecked()
+ data['plotStyle']['Font'] = plotPanel.esthPanel.cbFont.GetValue()
+ data['plotStyle']['LegendFont'] = plotPanel.esthPanel.cbLgdFont.GetValue()
+ data['plotStyle']['LegendPosition'] = plotPanel.esthPanel.cbLegend.GetValue()
+ data['plotStyle']['LineWidth'] = plotPanel.esthPanel.cbLW.GetValue()
+ data['plotStyle']['MarkerSize'] = plotPanel.esthPanel.cbMS.GetValue()
+
+def defaultPlotPanelData():
+ data={}
+ data['CrossHair']=True
+ data['Grid']=False
+ plotStyle = dict()
+ plotStyle['Font'] = '11'
+ plotStyle['LegendFont'] = '11'
+ plotStyle['LegendPosition'] = 'Upper right'
+ plotStyle['LineWidth'] = '1.5'
+ plotStyle['MarkerSize'] = '2'
+ data['plotStyle']= plotStyle
+ return data
+
+# --- Info Panel
+# TODO put this into infoPanel file?
+def saveInfoPanelData(data, infoPanel):
+ data['ColumnsRegular'] = [c['name'] for c in infoPanel.ColsReg if c['s']]
+ data['ColumnsFFT'] = [c['name'] for c in infoPanel.ColsFFT if c['s']]
+ data['ColumnsMinMax'] = [c['name'] for c in infoPanel.ColsMinMax if c['s']]
+ data['ColumnsPDF'] = [c['name'] for c in infoPanel.ColsPDF if c['s']]
+ data['ColumnsCmp'] = [c['name'] for c in infoPanel.ColsCmp if c['s']]
+
+def defaultInfoPanelData():
+ data={}
+ data['ColumnsRegular'] = ['Column','Mean','Std','Min','Max','Range','dx','n']
+ data['ColumnsFFT'] = ['Column','Mean','Std','Min','Max','Min(FFT)','Max(FFT)',u'\u222By(FFT)','dx(FFT)','xMax(FFT)','n(FFT)','n']
+ data['ColumnsMinMax'] = ['Column','Mean','Std','Min','Max','Mean(MinMax)','Std(MinMax)',u'\u222By(MinMax)','n']
+ data['ColumnsPDF'] = ['Column','Mean','Std','Min','Max','Min(PDF)','Max(PDF)',u'\u222By(PDF)','n(PDF)']
+ data['ColumnsCmp'] = ['Column','Mean(Cmp)','Std(Cmp)','Min(Cmp)','Max(Cmp)','n(Cmp)']
+ return data
+
+
diff --git a/pydatview/common.py b/pydatview/common.py
index 8903ec3..59f1340 100644
--- a/pydatview/common.py
+++ b/pydatview/common.py
@@ -1,454 +1,440 @@
-import numpy as np
-import pandas as pd
-import os
-import platform
-import datetime
-import re
-
-CHAR={
-'menu' : u'\u2630',
-'tridot' : u'\u26EC',
-'apply' : u'\u1809',
-'compute' : u'\u2699', # gear
-'close' : u'\u274C',
-'add' : u'\u2795',
-'add_small': u'\ufe62',
-'clear' : u'-',
-'sun' : u'\u2600',
-'suncloud' : u'\u26C5',
-'cloud' : u'\u2601',
-'check' : u'\u2714',
-'help' : u'\u2753',
-'pencil' : u'\u270f', # draw
-'pick' : u'\u26cf',
-'hammer' : u'\U0001f528',
-'wrench' : u'\U0001f527',
-'ruler' : u'\U0001F4CF', # measure
-'control_knobs' : u'\U0001F39b',
-'python' : u'\U0001F40D',
-'chart' : u'\U0001F4c8',
-'chart_small': u'\U0001F5e0',
-}
-# --------------------------------------------------------------------------------}
-# --- ellude
-# --------------------------------------------------------------------------------{
-def common_start(*strings):
- """ Returns the longest common substring
- from the beginning of the `strings`
- """
- if len(strings)==1:
- strings=tuple(strings[0])
- def _iter():
- for z in zip(*strings):
- if z.count(z[0]) == len(z): # check all elements in `z` are the same
- yield z[0]
- else:
- return
- return ''.join(_iter())
-
-def common_end(*strings):
- if len(strings)==1:
- strings=strings[0]
- else:
- strings=list(strings)
- strings = [s[-1::-1] for s in strings]
- return common_start(strings)[-1::-1]
-
-def find_leftstop(s):
- for i,c in enumerate(reversed(s)):
- if c in ['.','_','|']:
- i=i+1
- return s[:len(s)-i]
- return s
-
-def ellude_common(strings,minLength=2):
- """
- ellude the common parts of two strings
-
- minLength:
- if -1, string might be elluded up until there are of 0 length
- if 0 , if a string of zero length is obtained, it will be tried to be extended until a stop character is found
-
- """
- # Selecting only the strings that do not start with the safe '>' char
- S = [s for i,s in enumerate(strings) if ((len(s)>0) and (s[0]!= '>'))]
- if len(S)==0:
- pass
- elif len(S)==1:
- ns=S[0].rfind('|')+1
- ne=0;
- else:
- ss = common_start(S)
- se = common_end(S)
- iu = ss[:-1].rfind('_')
- ip = ss[:-1].rfind('_')
- if iu > 0:
- if ip>0:
- if iu>ip:
- ss=ss[:iu+1]
- else:
- ss=ss[:iu+1]
-
- iu = se[:-1].find('_')
- if iu > 0:
- se=se[iu:]
- iu = se[:-1].find('.')
- if iu > 0:
- se=se[iu:]
- ns=len(ss)
- ne=len(se)
-
- # Reduce start length if some strings end up empty
- # Look if any of the strings will end up empty
- SSS=[len(s[ns:-ne].lstrip('_') if ne>0 else s[ns:].lstrip('_')) for s in S]
- currentMinLength=np.min(SSS)
- if currentMinLength0:
- ss=ss[:-delta]
- ns=len(ss)
- #print('ss',ss)
- ss=find_leftstop(ss)
- #print('ss',ss)
- if len(ss)==ns:
- ns=0
- else:
- ns=len(ss)+1
-
- for i,s in enumerate(strings):
- if len(s)>0 and s[0]=='>':
- strings[i]=s[1:]
- else:
- s=s[ns:-ne] if ne>0 else s[ns:]
- strings[i]=s.lstrip('_')
- if len(strings[i])==0:
- strings[i]='tab{}'.format(i)
- return strings
-
-
-# --------------------------------------------------------------------------------}
-# --- Key value
-# --------------------------------------------------------------------------------{
-def extract_key_tuples(text):
- """
- all=(0.1,-2),b=(inf,0), c=(-inf,0.3e+10)
- """
- regex = re.compile(r'(?P[\w\-]+)=\((?P[0-9+epinf.-]*?),(?P[0-9+epinf.-]*?)\)($|,)')
- return {match.group("key"): (np.float(match.group("value1")),np.float(match.group("value2"))) for match in regex.finditer(text.replace(' ',''))}
-
-
-def extract_key_num(text):
- """
- all=0.1, b=inf, c=-0.3e+10
- """
- regex = re.compile(r'(?P[\w\-]+)=(?P[0-9+epinf.-]*?)($|,)')
- return {match.group("key"): np.float(match.group("value")) for match in regex.finditer(text.replace(' ',''))}
-
-# --------------------------------------------------------------------------------}
-# ---
-# --------------------------------------------------------------------------------{
-# def getMonoFontAbs():
-# import wx
-# #return wx.Font(9, wx.MODERN, wx.NORMAL, wx.NORMAL, False, u'Monospace')
-# if os.name=='nt':
-# font=wx.Font(9, wx.TELETYPE, wx.NORMAL, wx.NORMAL, False)
-# elif os.name=='posix':
-# font=wx.Font(10, wx.TELETYPE, wx.NORMAL, wx.NORMAL, False)
-# else:
-# font=wx.Font(8, wx.TELETYPE, wx.NORMAL, wx.NORMAL, False)
-# return font
-#
-# def getMonoFont(widget):
-# import wx
-# font = widget.GetFont()
-# font.SetFamily(wx.TELETYPE)
-# if platform.system()=='Windows':
-# pass
-# elif platform.system()=='Linux':
-# pass
-# elif platform.system()=='Darwin':
-# font.SetPointSize(font.GetPointSize()-1)
-# else:
-# pass
-# return font
-
-def getDt(x):
- """ returns dt in s """
- def myisnat(dt):
- if isinstance(dt,pd._libs.tslibs.timedeltas.Timedelta):
- try:
- dt=pd.to_timedelta(dt) # pandas 1.0
- except:
- dt=pd.to_timedelta(dt,box=False) # backward compatibility
-
- elif isinstance(dt,datetime.timedelta):
- dt=np.array([dt],dtype='timedelta64')[0]
- return pd.isna(dt)
-# try:
-# print('>>>', dt,type(dt))
-# isnat=np.isnat(dt)
-# except:
-# print(type(dt),type(dx))
-# isnat=False
-# raise
-# return isnat
-
-
-
- if len(x)<=1:
- return np.NaN
- if isinstance(x[0],float):
- return x[1]-x[0]
- if isinstance(x[0],int) or isinstance(x[0],np.int32) or isinstance(x[0],np.int64):
- return x[1]-x[0]
- # first try with seconds
- #print('')
- #print('getDT: dx:',x[1]-x[0])
- dx = x[1]-x[0]
- #print(type(dx))
- if myisnat(dx):
- # we try the last values (or while loop, but may take a while)
- dx = x[-1]-x[-2]
- if myisnat(dx):
- return np.nan
- dt=np.timedelta64(dx,'s').item().total_seconds()
- if dt<1:
- # try higher resolution
- dt=np.timedelta64(dx,'ns').item()/10.**9
- # TODO if dt> int res... do something
- return dt
-
-def getTabCommonColIndices(tabs):
- cleanedColLists = [ [cleanCol(s) for s in t.columns] for t in tabs]
- commonCols = cleanedColLists[0]
- for i in np.arange(1,len(cleanedColLists)):
- commonCols = list( set(commonCols) & set( cleanedColLists[i]))
- # Keep original order
- commonCols =[c for c in cleanedColLists[0] if c in commonCols] # Might have duplicates..
- IMissPerTab=[]
- IKeepPerTab=[]
- IDuplPerTab=[] # Duplicates amongst the "common"
- for cleanedCols in cleanedColLists:
- IKeep=[]
- IMiss=[]
- IDupl=[]
- # Ugly for loop here since we have to account for dupplicates
- for comcol in commonCols:
- I = [i for i, c in enumerate(cleanedCols) if c == comcol]
- if len(I)==0:
- pass
- else:
- if I[0] not in IKeep:
- IKeep.append(I[0])
- if len(I)>1:
- IDupl=IDupl+I[1:]
- IMiss=[i for i,_ in enumerate(cleanedCols) if (i not in IKeep) and (i not in IDupl)]
- IMissPerTab.append(IMiss)
- IKeepPerTab.append(IKeep)
- IDuplPerTab.append(IDupl)
- return IKeepPerTab, IMissPerTab, IDuplPerTab
-
-
-def cleanCol(s):
- s=no_unit(s).strip()
- s=no_unit(s.replace('(',' [').replace(')',']'))
- s=s.lower().strip().replace('_','').replace(' ','').replace('-','')
- return s
-
-def no_unit(s):
- s=s.replace('_[',' [')
- iu=s.rfind(' [')
- if iu>1:
- return s[:iu]
- else:
- return s
-
-def unit(s):
- iu=s.rfind('[')
- if iu>1:
- return s[iu+1:].replace(']','')
- else:
- return ''
-
-def inverse_unit(s):
- u=unit(s).strip()
- if u=='':
- return ''
- elif u=='-':
- return '-'
- elif len(u)==1:
- return '1/'+u;
- elif u=='m/s':
- return 's/m';
- elif u=='deg':
- return '1/deg';
- else:
- return '1/('+u+')'
-
-def filter_list(L, string):
- """ simple (not regex or fuzzy) filtering of a list of strings
- Returns matched indices and strings
- """
- ignore_case = string==string.lower()
- if ignore_case:
- I=[i for i,s in enumerate(L) if string in s.lower()]
- else:
- I=[i for i,s in enumerate(L) if string in s]
- L_found =np.array(L)[I]
- return L_found, I
-
-def unique(l):
- """ Return unique values of a list"""
- used=set()
- return [x for x in l if x not in used and (used.add(x) or True)]
-
-# --------------------------------------------------------------------------------}
-# --- geometry
-# --------------------------------------------------------------------------------{
-def rectangleOverlap(BLx1, BLy1, TRx1, TRy1, BLx2, BLy2, TRx2, TRy2):
- """ returns true if two rectangles overlap
- BL: Bottom left
- TR: top right
- "1" rectangle 1
- "2" rectangle 2
- """
- return not (TRx1 < BLx2 or BLx1 > TRx2 or TRy1 < BLy2 or BLy1> TRy2)
-# --------------------------------------------------------------------------------}
-# ---
-# --------------------------------------------------------------------------------{
-def pretty_time(t):
- # fPrettyTime: returns a 6-characters string corresponding to the input time in seconds.
- # fPrettyTime(612)=='10m12s'
- # AUTHOR: E. Branlard
- if np.isnan(t):
- return 'NaT';
- if(t<0):
- return '------';
- elif (t<1) :
- c=np.floor(t*100);
- s='{:2d}.{:02d}s'.format(0,int(c))
- elif(t<60) :
- s=np.floor(t);
- c=np.floor((t-s)*100);
- s='{:2d}.{:02d}s'.format(int(s),int(c))
- elif(t<3600) :
- m=np.floor(t/60);
- s=np.mod( np.floor(t), 60);
- s='{:2d}m{:02d}s'.format(int(m),int(s))
- elif(t<86400) :
- h=np.floor(t/3600);
- m=np.floor(( np.mod( np.floor(t) , 3600))/60);
- s='{:2d}h{:02d}m'.format(int(h),int(m))
- elif(t<8553600) : #below 3month
- d=np.floor(t/86400);
- h=np.floor( np.mod(np.floor(t), 86400)/3600);
- s='{:2d}d{:02d}h'.format(int(d),int(h))
- elif(t<31536000):
- m=t/(3600*24*30.5);
- s='{:4.1f}mo'.format(m)
- #s='+3mon.';
- else:
- y=t/(3600*24*365.25);
- s='{:.1f}y'.format(y)
- return s
-
-def pretty_num(x):
- if abs(x)<1000 and abs(x)>1e-4:
- return "{:9.4f}".format(x)
- else:
- return '{:.3e}'.format(x)
-
-def pretty_num_short(x,digits=3):
- if digits==4:
- if abs(x)<1000 and abs(x)>1e-1:
- return "{:.4f}".format(x)
- else:
- return "{:.4e}".format(x)
- elif digits==3:
- if abs(x)<1000 and abs(x)>1e-1:
- return "{:.3f}".format(x)
- else:
- return "{:.3e}".format(x)
- elif digits==2:
- if abs(x)<1000 and abs(x)>1e-1:
- return "{:.2f}".format(x)
- else:
- return "{:.2e}".format(x)
-
-# --------------------------------------------------------------------------------}
-# --- Chinese characters
-# --------------------------------------------------------------------------------{
-cjk_ranges = [
- ( 0x4E00, 0x62FF),
- ( 0x6300, 0x77FF),
- ( 0x7800, 0x8CFF),
- ( 0x8D00, 0x9FCC),
- ( 0x3400, 0x4DB5),
- (0x20000, 0x215FF),
- (0x21600, 0x230FF),
- (0x23100, 0x245FF),
- (0x24600, 0x260FF),
- (0x26100, 0x275FF),
- (0x27600, 0x290FF),
- (0x29100, 0x2A6DF),
- (0x2A700, 0x2B734),
- (0x2B740, 0x2B81D),
- (0x2B820, 0x2CEAF),
- (0x2CEB0, 0x2EBEF),
- (0x2F800, 0x2FA1F)
- ]
-
-def has_chinese_char(s):
- def is_cjk(char):
- char = ord(char)
- for bottom, top in cjk_ranges:
- if char >= bottom and char <= top:
- return True
- return False
- for c in s:
- char=ord(c)
- for bottom, top in cjk_ranges:
- if char >= bottom and char <= top:
- return True
- return False
-
-
-# --------------------------------------------------------------------------------}
-# --- Helper functions
-# --------------------------------------------------------------------------------{
-def YesNo(parent, question, caption = 'Yes or no?'):
- import wx
- dlg = wx.MessageDialog(parent, question, caption, wx.YES_NO | wx.ICON_QUESTION)
- result = dlg.ShowModal() == wx.ID_YES
- dlg.Destroy()
- return result
-def Info(parent, message, caption = 'Info'):
- import wx
- dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_INFORMATION)
- dlg.ShowModal()
- dlg.Destroy()
-def Warn(parent, message, caption = 'Warning!'):
- import wx
- dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_WARNING)
- dlg.ShowModal()
- dlg.Destroy()
-def Error(parent, message, caption = 'Error!'):
- import wx
- dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_ERROR)
- dlg.ShowModal()
- dlg.Destroy()
-
-
-
-# --------------------------------------------------------------------------------}
-# ---
-# --------------------------------------------------------------------------------{
-
-def isString(x):
- b = x.dtype == np.object and isinstance(x.values[0], str)
- return b
-
-def isDate(x):
- return np.issubdtype(x.dtype, np.datetime64)
-
+import numpy as np
+import pandas as pd
+import os
+import platform
+import datetime
+import re
+
+CHAR={
+'menu' : u'\u2630',
+'tridot' : u'\u26EC',
+'apply' : u'\u1809',
+'compute' : u'\u2699', # gear
+'close' : u'\u274C',
+'add' : u'\u2795',
+'add_small': u'\ufe62',
+'clear' : u'-',
+'sun' : u'\u2600',
+'suncloud' : u'\u26C5',
+'cloud' : u'\u2601',
+'check' : u'\u2714',
+'help' : u'\u2753',
+'pencil' : u'\u270f', # draw
+'pick' : u'\u26cf',
+'hammer' : u'\U0001f528',
+'wrench' : u'\U0001f527',
+'ruler' : u'\U0001F4CF', # measure
+'control_knobs' : u'\U0001F39b',
+'python' : u'\U0001F40D',
+'chart' : u'\U0001F4c8',
+'chart_small': u'\U0001F5e0',
+}
+# --------------------------------------------------------------------------------}
+# --- ellude
+# --------------------------------------------------------------------------------{
+def common_start(*strings):
+ """ Returns the longest common substring
+ from the beginning of the `strings`
+ """
+ if len(strings)==1:
+ strings=tuple(strings[0])
+ def _iter():
+ for z in zip(*strings):
+ if z.count(z[0]) == len(z): # check all elements in `z` are the same
+ yield z[0]
+ else:
+ return
+ return ''.join(_iter())
+
+def common_end(*strings):
+ if len(strings)==1:
+ strings=strings[0]
+ else:
+ strings=list(strings)
+ strings = [s[-1::-1] for s in strings]
+ return common_start(strings)[-1::-1]
+
+def find_leftstop(s):
+ for i,c in enumerate(reversed(s)):
+ if c in ['.','_','|']:
+ i=i+1
+ return s[:len(s)-i]
+ return s
+
+def ellude_common(strings,minLength=2):
+ """
+ ellude the common parts of two strings
+
+ minLength:
+ if -1, string might be elluded up until there are of 0 length
+ if 0 , if a string of zero length is obtained, it will be tried to be extended until a stop character is found
+
+ """
+ # Selecting only the strings that do not start with the safe '>' char
+ S = [s for i,s in enumerate(strings) if ((len(s)>0) and (s[0]!= '>'))]
+ if len(S)==0:
+ pass
+ elif len(S)==1:
+ ns=S[0].rfind('|')+1
+ ne=0;
+ else:
+ ss = common_start(S)
+ se = common_end(S)
+ iu = ss[:-1].rfind('_')
+ ip = ss[:-1].rfind('_')
+ if iu > 0:
+ if ip>0:
+ if iu>ip:
+ ss=ss[:iu+1]
+ else:
+ ss=ss[:iu+1]
+
+ iu = se[:-1].find('_')
+ if iu > 0:
+ se=se[iu:]
+ iu = se[:-1].find('.')
+ if iu > 0:
+ se=se[iu:]
+ ns=len(ss)
+ ne=len(se)
+
+ # Reduce start length if some strings end up empty
+ # Look if any of the strings will end up empty
+ SSS=[len(s[ns:-ne].lstrip('_') if ne>0 else s[ns:].lstrip('_')) for s in S]
+ currentMinLength=np.min(SSS)
+ if currentMinLength0:
+ ss=ss[:-delta]
+ ns=len(ss)
+ #print('ss',ss)
+ ss=find_leftstop(ss)
+ #print('ss',ss)
+ if len(ss)==ns:
+ ns=0
+ else:
+ ns=len(ss)+1
+
+ for i,s in enumerate(strings):
+ if len(s)>0 and s[0]=='>':
+ strings[i]=s[1:]
+ else:
+ s=s[ns:-ne] if ne>0 else s[ns:]
+ strings[i]=s.lstrip('_')
+ if len(strings[i])==0:
+ strings[i]='tab{}'.format(i)
+ return strings
+
+
+# --------------------------------------------------------------------------------}
+# --- Key value
+# --------------------------------------------------------------------------------{
+def extract_key_tuples(text):
+ """
+ all=(0.1,-2),b=(inf,0), c=(-inf,0.3e+10)
+ """
+ regex = re.compile(r'(?P[\w\-]+)=\((?P[0-9+epinf.-]*?),(?P[0-9+epinf.-]*?)\)($|,)')
+ return {match.group("key"): (np.float(match.group("value1")),np.float(match.group("value2"))) for match in regex.finditer(text.replace(' ',''))}
+
+
+def extract_key_num(text):
+ """
+ all=0.1, b=inf, c=-0.3e+10
+ """
+ regex = re.compile(r'(?P[\w\-]+)=(?P[0-9+epinf.-]*?)($|,)')
+ return {match.group("key"): np.float(match.group("value")) for match in regex.finditer(text.replace(' ',''))}
+
+def getDt(x):
+ """ returns dt in s """
+ def myisnat(dt):
+ if isinstance(dt,pd._libs.tslibs.timedeltas.Timedelta):
+ try:
+ dt=pd.to_timedelta(dt) # pandas 1.0
+ except:
+ dt=pd.to_timedelta(dt,box=False) # backward compatibility
+
+ elif isinstance(dt,datetime.timedelta):
+ dt=np.array([dt],dtype='timedelta64')[0]
+ return pd.isna(dt)
+# try:
+# print('>>>', dt,type(dt))
+# isnat=np.isnat(dt)
+# except:
+# print(type(dt),type(dx))
+# isnat=False
+# raise
+# return isnat
+
+
+
+ if len(x)<=1:
+ return np.NaN
+ if isinstance(x[0],float):
+ return x[1]-x[0]
+ if isinstance(x[0],int) or isinstance(x[0],np.int32) or isinstance(x[0],np.int64):
+ return x[1]-x[0]
+ # first try with seconds
+ #print('')
+ #print('getDT: dx:',x[1]-x[0])
+ dx = x[1]-x[0]
+ #print(type(dx))
+ if myisnat(dx):
+ # we try the last values (or while loop, but may take a while)
+ dx = x[-1]-x[-2]
+ if myisnat(dx):
+ return np.nan
+ dt=np.timedelta64(dx,'s').item().total_seconds()
+ if dt<1:
+ # try higher resolution
+ dt=np.timedelta64(dx,'ns').item()/10.**9
+ # TODO if dt> int res... do something
+ return dt
+
+def getTabCommonColIndices(tabs):
+ cleanedColLists = [ [cleanCol(s) for s in t.columns] for t in tabs]
+ nCols = np.array([len(cols) for cols in cleanedColLists])
+ # Common columns between all column lists
+ commonCols = cleanedColLists[0]
+ for i in np.arange(1,len(cleanedColLists)):
+ commonCols = list( set(commonCols) & set( cleanedColLists[i]))
+ # Keep original order
+ commonCols =[c for c in cleanedColLists[0] if c in commonCols] # Might have duplicates..
+ IMissPerTab=[]
+ IKeepPerTab=[]
+ IDuplPerTab=[] # Duplicates amongst the "common"
+ for cleanedCols in cleanedColLists:
+ IKeep=[]
+ IMiss=[]
+ IDupl=[]
+ # Ugly for loop here since we have to account for dupplicates
+ for comcol in commonCols:
+ I = [i for i, c in enumerate(cleanedCols) if c == comcol]
+ if len(I)==0:
+ pass
+ else:
+ if I[0] not in IKeep:
+ IKeep.append(I[0])
+ if len(I)>1:
+ IDupl=IDupl+I[1:]
+ IMiss=[i for i,_ in enumerate(cleanedCols) if (i not in IKeep) and (i not in IDupl)]
+ IMissPerTab.append(IMiss)
+ IKeepPerTab.append(IKeep)
+ IDuplPerTab.append(IDupl)
+ return IKeepPerTab, IMissPerTab, IDuplPerTab, nCols
+
+
+# --------------------------------------------------------------------------------}
+# --- Units
+# --------------------------------------------------------------------------------{
+def cleanCol(s):
+ s=no_unit(s).strip()
+ s=no_unit(s.replace('(',' [').replace(')',']'))
+ s=s.lower().strip().replace('_','').replace(' ','').replace('-','')
+ return s
+
+def no_unit(s):
+ s=s.replace('_[',' [')
+ iu=s.rfind(' [')
+ if iu>1:
+ return s[:iu]
+ else:
+ return s
+
+def unit(s):
+ iu=s.rfind('[')
+ if iu>1:
+ return s[iu+1:].replace(']','')
+ else:
+ return ''
+
+def splitunit(s):
+ iu=s.rfind('[')
+ if iu>1:
+ return s[:iu], s[iu+1:].replace(']','')
+ else:
+ return s, ''
+
+def inverse_unit(s):
+ u=unit(s).strip()
+ if u=='':
+ return ''
+ elif u=='-':
+ return '-'
+ elif len(u)==1:
+ return '1/'+u;
+ elif u=='m/s':
+ return 's/m';
+ elif u=='deg':
+ return '1/deg';
+ else:
+ return '1/('+u+')'
+
+
+
+def filter_list(L, string):
+ """ simple (not regex or fuzzy) filtering of a list of strings
+ Returns matched indices and strings
+ """
+ ignore_case = string==string.lower()
+ if ignore_case:
+ I=[i for i,s in enumerate(L) if string in s.lower()]
+ else:
+ I=[i for i,s in enumerate(L) if string in s]
+ L_found =np.array(L)[I]
+ return L_found, I
+
+def unique(l):
+ """ Return unique values of a list"""
+ used=set()
+ return [x for x in l if x not in used and (used.add(x) or True)]
+
+# --------------------------------------------------------------------------------}
+# --- geometry
+# --------------------------------------------------------------------------------{
+def rectangleOverlap(BLx1, BLy1, TRx1, TRy1, BLx2, BLy2, TRx2, TRy2):
+ """ returns true if two rectangles overlap
+ BL: Bottom left
+ TR: top right
+ "1" rectangle 1
+ "2" rectangle 2
+ """
+ return not (TRx1 < BLx2 or BLx1 > TRx2 or TRy1 < BLy2 or BLy1> TRy2)
+# --------------------------------------------------------------------------------}
+# ---
+# --------------------------------------------------------------------------------{
+def pretty_time(t):
+ # fPrettyTime: returns a 6-characters string corresponding to the input time in seconds.
+ # fPrettyTime(612)=='10m12s'
+ # AUTHOR: E. Branlard
+ if np.isnan(t):
+ return 'NaT';
+ if(t<0):
+ return '------';
+ elif (t<1) :
+ c=np.floor(t*100);
+ s='{:2d}.{:02d}s'.format(0,int(c))
+ elif(t<60) :
+ s=np.floor(t);
+ c=np.floor((t-s)*100);
+ s='{:2d}.{:02d}s'.format(int(s),int(c))
+ elif(t<3600) :
+ m=np.floor(t/60);
+ s=np.mod( np.floor(t), 60);
+ s='{:2d}m{:02d}s'.format(int(m),int(s))
+ elif(t<86400) :
+ h=np.floor(t/3600);
+ m=np.floor(( np.mod( np.floor(t) , 3600))/60);
+ s='{:2d}h{:02d}m'.format(int(h),int(m))
+ elif(t<8553600) : #below 3month
+ d=np.floor(t/86400);
+ h=np.floor( np.mod(np.floor(t), 86400)/3600);
+ s='{:2d}d{:02d}h'.format(int(d),int(h))
+ elif(t<31536000):
+ m=t/(3600*24*30.5);
+ s='{:4.1f}mo'.format(m)
+ #s='+3mon.';
+ else:
+ y=t/(3600*24*365.25);
+ s='{:.1f}y'.format(y)
+ return s
+
+def pretty_num(x):
+ if abs(x)<1000 and abs(x)>1e-4:
+ return "{:9.4f}".format(x)
+ else:
+ return '{:.3e}'.format(x)
+
+def pretty_num_short(x,digits=3):
+ if digits==4:
+ if abs(x)<1000 and abs(x)>1e-1:
+ return "{:.4f}".format(x)
+ else:
+ return "{:.4e}".format(x)
+ elif digits==3:
+ if abs(x)<1000 and abs(x)>1e-1:
+ return "{:.3f}".format(x)
+ else:
+ return "{:.3e}".format(x)
+ elif digits==2:
+ if abs(x)<1000 and abs(x)>1e-1:
+ return "{:.2f}".format(x)
+ else:
+ return "{:.2e}".format(x)
+
+# --------------------------------------------------------------------------------}
+# --- Chinese characters
+# --------------------------------------------------------------------------------{
+cjk_ranges = [
+ ( 0x4E00, 0x62FF),
+ ( 0x6300, 0x77FF),
+ ( 0x7800, 0x8CFF),
+ ( 0x8D00, 0x9FCC),
+ ( 0x3400, 0x4DB5),
+ (0x20000, 0x215FF),
+ (0x21600, 0x230FF),
+ (0x23100, 0x245FF),
+ (0x24600, 0x260FF),
+ (0x26100, 0x275FF),
+ (0x27600, 0x290FF),
+ (0x29100, 0x2A6DF),
+ (0x2A700, 0x2B734),
+ (0x2B740, 0x2B81D),
+ (0x2B820, 0x2CEAF),
+ (0x2CEB0, 0x2EBEF),
+ (0x2F800, 0x2FA1F)
+ ]
+
+def has_chinese_char(s):
+ def is_cjk(char):
+ char = ord(char)
+ for bottom, top in cjk_ranges:
+ if char >= bottom and char <= top:
+ return True
+ return False
+ for c in s:
+ char=ord(c)
+ for bottom, top in cjk_ranges:
+ if char >= bottom and char <= top:
+ return True
+ return False
+
+
+# --------------------------------------------------------------------------------}
+# --- Helper functions
+# --------------------------------------------------------------------------------{
+def YesNo(parent, question, caption = 'Yes or no?'):
+ import wx
+ dlg = wx.MessageDialog(parent, question, caption, wx.YES_NO | wx.ICON_QUESTION)
+ result = dlg.ShowModal() == wx.ID_YES
+ dlg.Destroy()
+ return result
+def Info(parent, message, caption = 'Info'):
+ import wx
+ dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_INFORMATION)
+ dlg.ShowModal()
+ dlg.Destroy()
+def Warn(parent, message, caption = 'Warning!'):
+ import wx
+ dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_WARNING)
+ dlg.ShowModal()
+ dlg.Destroy()
+def Error(parent, message, caption = 'Error!'):
+ import wx
+ dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_ERROR)
+ dlg.ShowModal()
+ dlg.Destroy()
+
+
+
+# --------------------------------------------------------------------------------}
+# ---
+# --------------------------------------------------------------------------------{
+
+def isString(x):
+ b = x.dtype == object and isinstance(x.values[0], str)
+ return b
+
+def isDate(x):
+ return np.issubdtype(x.dtype, np.datetime64)
+
diff --git a/pydatview/fast/case_gen.py b/pydatview/fast/case_gen.py
index a22c090..f9c53f0 100644
--- a/pydatview/fast/case_gen.py
+++ b/pydatview/fast/case_gen.py
@@ -1,586 +1,593 @@
-from __future__ import division, print_function
-import os
-import collections
-import glob
-import pandas as pd
-import numpy as np
-import shutil
-import stat
-import re
-
-# --- Misc fast libraries
-import weio.weio.fast_input_file as fi
-import pydatview.fast.runner as runner
-import pydatview.fast.postpro as postpro
-#import pyFAST.input_output.fast_input_file as fi
-#import pyFAST.case_generation.runner as runner
-#import pyFAST.input_output.postpro as postpro
-
-
-# --------------------------------------------------------------------------------}
-# --- Template replace
-# --------------------------------------------------------------------------------{
-def handleRemoveReadonlyWin(func, path, exc_info):
- """
- Error handler for ``shutil.rmtree``.
- If the error is due to an access error (read only file)
- it attempts to add write permission and then retries.
- Usage : ``shutil.rmtree(path, onerror=onerror)``
- """
- if not os.access(path, os.W_OK):
- # Is the error an access error ?
- os.chmod(path, stat.S_IWUSR)
- func(path)
- else:
- raise
-
-
-def copyTree(src, dst):
- """
- Copy a directory to another one, overwritting files if necessary.
- copy_tree from distutils and copytree from shutil fail on Windows (in particular on git files)
- """
- def forceMergeFlatDir(srcDir, dstDir):
- if not os.path.exists(dstDir):
- os.makedirs(dstDir)
- for item in os.listdir(srcDir):
- srcFile = os.path.join(srcDir, item)
- dstFile = os.path.join(dstDir, item)
- forceCopyFile(srcFile, dstFile)
-
- def forceCopyFile (sfile, dfile):
- # ---- Handling error due to wrong mod
- if os.path.isfile(dfile):
- if not os.access(dfile, os.W_OK):
- os.chmod(dfile, stat.S_IWUSR)
- #print(sfile, ' > ', dfile)
- shutil.copy2(sfile, dfile)
-
- def isAFlatDir(sDir):
- for item in os.listdir(sDir):
- sItem = os.path.join(sDir, item)
- if os.path.isdir(sItem):
- return False
- return True
-
- for item in os.listdir(src):
- s = os.path.join(src, item)
- d = os.path.join(dst, item)
- if os.path.isfile(s):
- if not os.path.exists(dst):
- os.makedirs(dst)
- forceCopyFile(s,d)
- if os.path.isdir(s):
- isRecursive = not isAFlatDir(s)
- if isRecursive:
- copyTree(s, d)
- else:
- forceMergeFlatDir(s, d)
-
-
-def templateReplaceGeneral(PARAMS, templateDir=None, outputDir=None, main_file=None, removeAllowed=False, removeRefSubFiles=False, oneSimPerDir=False):
- """ Generate inputs files by replacing different parameters from a template file.
- The generated files are placed in the output directory `outputDir`
- The files are read and written using the library `weio`.
- The template file is read and its content can be changed like a dictionary.
- Each item of `PARAMS` correspond to a set of parameters that will be replaced
- in the template file to generate one input file.
-
- For "FAST" input files, parameters can be changed recursively.
-
-
- INPUTS:
- PARAMS: list of dictionaries. Each key of the dictionary should be a key present in the
- template file when read with `weio` (see: weio.read(main_file).keys() )
-
- PARAMS[0]={'DT':0.1, 'EDFile|GBRatio':1, 'ServoFile|GenEff':0.8}
-
- templateDir: if provided, this directory and its content will be copied to `outputDir`
- before doing the parametric substitution
-
- outputDir : directory where files will be generated.
- """
- # --- Helper functions
- def rebase_rel(wd,s,sid):
- split = os.path.splitext(s)
- return os.path.join(wd,split[0]+sid+split[1])
-
- def get_strID(p) :
- if '__name__' in p.keys():
- strID=p['__name__']
- else:
- raise Exception('When calling `templateReplace`, provide the key `__name_` in the parameter dictionaries')
- return strID
-
- def splitAddress(sAddress):
- sp = sAddress.split('|')
- if len(sp)==1:
- return sp[0],[]
- else:
- return sp[0],sp[1:]
-
- def rebaseFileName(org_filename, workDir, strID):
- new_filename_full = rebase_rel(workDir, org_filename,'_'+strID)
- new_filename = os.path.relpath(new_filename_full,workDir).replace('\\','/')
- return new_filename, new_filename_full
-
- def replaceRecurse(templatename_or_newname, FileKey, ParamKey, ParamValue, Files, strID, workDir, TemplateFiles):
- """
- FileKey: a single key defining which file we are currently modifying e.g. :'AeroFile', 'EDFile','FVWInputFileName'
- ParamKey: the address key of the parameter to be changed, relative to the current FileKey
- e.g. 'EDFile|IntMethod' (if FileKey is '')
- 'IntMethod' (if FileKey is 'EDFile')
- ParamValue: the value to be used
- Files: dict of files, as returned by weio, keys are "FileKeys"
- """
- # --- Special handling for the root
- if FileKey=='':
- FileKey='Root'
- # --- Open (or get if already open) file where a parameter needs to be changed
- if FileKey in Files.keys():
- # The file was already opened, it's stored
- f = Files[FileKey]
- newfilename_full = f.filename
- newfilename = os.path.relpath(newfilename_full,workDir).replace('\\','/')
-
- else:
- templatefilename = templatename_or_newname
- templatefilename_full = os.path.join(workDir,templatefilename)
- TemplateFiles.append(templatefilename_full)
- if FileKey=='Root':
- # Root files, we start from strID
- ext = os.path.splitext(templatefilename)[-1]
- newfilename_full = os.path.join(wd,strID+ext)
- newfilename = strID+ext
- else:
- newfilename, newfilename_full = rebaseFileName(templatefilename, workDir, strID)
- #print('--------------------------------------------------------------')
- #print('TemplateFile :', templatefilename)
- #print('TemplateFileFull:', templatefilename_full)
- #print('NewFile :', newfilename)
- #print('NewFileFull :', newfilename_full)
- shutil.copyfile(templatefilename_full, newfilename_full)
- f= fi.FASTInputFile(newfilename_full) # open the template file for that filekey
- Files[FileKey]=f # store it
-
- # --- Changing parameters in that file
- NewFileKey_or_Key, ChildrenKeys = splitAddress(ParamKey)
- if len(ChildrenKeys)==0:
- # A simple parameter is changed
- Key = NewFileKey_or_Key
- #print('Setting', FileKey, '|',Key, 'to',ParamValue)
- if Key=='OutList':
- OutList=f[Key]
- f[Key]=addToOutlist(OutList, ParamValue)
- else:
- f[Key] = ParamValue
- else:
- # Parameters needs to be changed in subfiles (children)
- NewFileKey = NewFileKey_or_Key
- ChildrenKey = '|'.join(ChildrenKeys)
- child_templatefilename = f[NewFileKey].strip('"') # old filename that will be used as a template
- baseparent = os.path.dirname(newfilename)
- #print('Child templatefilename:',child_templatefilename)
- #print('Parent base dir :',baseparent)
- workDir = os.path.join(workDir, baseparent)
-
- #
- newchildFilename, Files = replaceRecurse(child_templatefilename, NewFileKey, ChildrenKey, ParamValue, Files, strID, workDir, TemplateFiles)
- #print('Setting', FileKey, '|',NewFileKey, 'to',newchildFilename)
- f[NewFileKey] = '"'+newchildFilename+'"'
-
- return newfilename, Files
-
-
- # --- Safety checks
- if templateDir is None and outputDir is None:
- raise Exception('Provide at least a template directory OR an output directory')
-
- if templateDir is not None:
- if not os.path.exists(templateDir):
- raise Exception('Template directory does not exist: '+templateDir)
-
- # Default value of outputDir if not provided
- if templateDir[-1]=='/' or templateDir[-1]=='\\' :
- templateDir=templateDir[0:-1]
- if outputDir is None:
- outputDir=templateDir+'_Parametric'
-
- # --- Main file use as "master"
- if templateDir is not None:
- main_file=os.path.join(outputDir, os.path.basename(main_file))
- else:
- main_file=main_file
-
- # Params need to be a list
- if not isinstance(PARAMS,list):
- PARAMS=[PARAMS]
-
- if oneSimPerDir:
- workDirS=[os.path.join(outputDir,get_strID(p)) for p in PARAMS]
- else:
- workDirS=[outputDir]*len(PARAMS)
- # --- Creating outputDir - Copying template folder to outputDir if necessary
- # Copying template folder to workDir
- for wd in list(set(workDirS)):
- if removeAllowed:
- removeFASTOuputs(wd)
- if os.path.exists(wd) and removeAllowed:
- shutil.rmtree(wd, ignore_errors=False, onerror=handleRemoveReadonlyWin)
- copyTree(templateDir, wd)
- if removeAllowed:
- removeFASTOuputs(wd)
-
-
- TemplateFiles=[]
- files=[]
- for ip,(wd,p) in enumerate(zip(workDirS,PARAMS)):
- if '__index__' not in p.keys():
- p['__index__']=ip
-
- main_file_base = os.path.basename(main_file)
- strID = get_strID(p)
- # --- Setting up files for this simulation
- Files=dict()
- for k,v in p.items():
- if k =='__index__' or k=='__name__':
- continue
- new_mainFile, Files = replaceRecurse(main_file_base, '', k, v, Files, strID, wd, TemplateFiles)
-
- # --- Writting files
- for k,f in Files.items():
- if k=='Root':
- files.append(f.filename)
- f.write()
-
- # --- Remove extra files at the end
- if removeRefSubFiles:
- TemplateFiles, nCounts = np.unique(TemplateFiles, return_counts=True)
- if not oneSimPerDir:
- # we can only detele template files that were used by ALL simulations
- TemplateFiles=[t for nc,t in zip(nCounts, TemplateFiles) if nc==len(PARAMS)]
- for tf in TemplateFiles:
- try:
- os.remove(tf)
- except:
- print('[FAIL] Removing '+tf)
- pass
- return files
-
-def templateReplace(PARAMS, templateDir, outputDir=None, main_file=None, removeAllowed=False, removeRefSubFiles=False, oneSimPerDir=False):
- """ Replace parameters in a fast folder using a list of dictionaries where the keys are for instance:
- 'DT', 'EDFile|GBRatio', 'ServoFile|GenEff'
- """
- # --- For backward compatibility, remove "FAST|" from the keys
- for p in PARAMS:
- old_keys=[ k for k,_ in p.items() if k.find('FAST|')==0]
- for k_old in old_keys:
- k_new=k_old.replace('FAST|','')
- p[k_new] = p.pop(k_old)
-
- return templateReplaceGeneral(PARAMS, templateDir, outputDir=outputDir, main_file=main_file,
- removeAllowed=removeAllowed, removeRefSubFiles=removeRefSubFiles, oneSimPerDir=oneSimPerDir)
-
-def removeFASTOuputs(workDir):
- # Cleaning folder
- for f in glob.glob(os.path.join(workDir,'*.out')):
- os.remove(f)
- for f in glob.glob(os.path.join(workDir,'*.outb')):
- os.remove(f)
- for f in glob.glob(os.path.join(workDir,'*.ech')):
- os.remove(f)
- for f in glob.glob(os.path.join(workDir,'*.sum')):
- os.remove(f)
-
-# --------------------------------------------------------------------------------}
-# --- Tools for template replacement
-# --------------------------------------------------------------------------------{
-def paramsSteadyAero(p=dict()):
- p['AeroFile|AFAeroMod']=1 # remove dynamic effects dynamic
- p['AeroFile|WakeMod']=1 # remove dynamic inflow dynamic
- p['AeroFile|TwrPotent']=0 # remove tower shadow
- return p
-
-def paramsNoGen(p=dict()):
- p['EDFile|GenDOF' ] = 'False'
- return p
-
-def paramsGen(p=dict()):
- p['EDFile|GenDOF' ] = 'True'
- return p
-
-def paramsNoController(p=dict()):
- p['ServoFile|PCMode'] = 0;
- p['ServoFile|VSContrl'] = 0;
- p['ServoFile|YCMode'] = 0;
- return p
-
-def paramsControllerDLL(p=dict()):
- p['ServoFile|PCMode'] = 5;
- p['ServoFile|VSContrl'] = 5;
- p['ServoFile|YCMode'] = 5;
- p['EDFile|GenDOF'] = 'True';
- return p
-
-
-def paramsStiff(p=dict()):
- p['EDFile|FlapDOF1'] = 'False'
- p['EDFile|FlapDOF2'] = 'False'
- p['EDFile|EdgeDOF' ] = 'False'
- p['EDFile|TeetDOF' ] = 'False'
- p['EDFile|DrTrDOF' ] = 'False'
- p['EDFile|YawDOF' ] = 'False'
- p['EDFile|TwFADOF1'] = 'False'
- p['EDFile|TwFADOF2'] = 'False'
- p['EDFile|TwSSDOF1'] = 'False'
- p['EDFile|TwSSDOF2'] = 'False'
- p['EDFile|PtfmSgDOF'] = 'False'
- p['EDFile|PtfmSwDOF'] = 'False'
- p['EDFile|PtfmHvDOF'] = 'False'
- p['EDFile|PtfmRDOF'] = 'False'
- p['EDFile|PtfmPDOF'] = 'False'
- p['EDFile|PtfmYDOF'] = 'False'
- return p
-
-def paramsWS_RPM_Pitch(WS, RPM, Pitch, baseDict=None, flatInputs=False):
- """
- Generate OpenFAST "parameters" (list of dictionaries with "address")
- chaing the inputs in ElastoDyn, InflowWind for different wind speed, RPM and Pitch
- """
- # --- Ensuring everythin is an iterator
- def iterify(x):
- if not isinstance(x, collections.Iterable): x = [x]
- return x
- WS = iterify(WS)
- RPM = iterify(RPM)
- Pitch = iterify(Pitch)
- # --- If inputs are not flat but different vectors to length through, we flatten them (TODO: meshgrid and ravel?)
- if not flatInputs :
- WS_flat = []
- Pitch_flat = []
- RPM_flat = []
- for pitch in Pitch:
- for rpm in RPM:
- for ws in WS:
- WS_flat.append(ws)
- RPM_flat.append(rpm)
- Pitch_flat.append(pitch)
- else:
- WS_flat, Pitch_flat, RPM_flat = WS, Pitch, RPM
-
- # --- Defining the parametric study
- PARAMS=[]
- i=0
- for ws,rpm,pitch in zip(WS_flat,RPM_flat,Pitch_flat):
- if baseDict is None:
- p=dict()
- else:
- p = baseDict.copy()
- p['EDFile|RotSpeed'] = rpm
- p['InflowFile|HWindSpeed'] = ws
- p['InflowFile|WindType'] = 1 # Setting steady wind
- p['EDFile|BlPitch(1)'] = pitch
- p['EDFile|BlPitch(2)'] = pitch
- p['EDFile|BlPitch(3)'] = pitch
-
- p['__index__'] = i
- p['__name__'] = '{:03d}_ws{:04.1f}_pt{:04.2f}_om{:04.2f}'.format(p['__index__'],p['InflowFile|HWindSpeed'],p['EDFile|BlPitch(1)'],p['EDFile|RotSpeed'])
- i=i+1
- PARAMS.append(p)
- return PARAMS
-
-def paramsLinearTrim(p=dict()):
-
- # Set a few DOFs, move this to main file
- p['Linearize'] = True
- p['CalcSteady'] = True
- p['TrimGain'] = 1e-4
- p['TrimTol'] = 1e-5
- p['CompMooring'] = 0
- p['CompHydro'] = 0
- p['LinOutJac'] = False
- p['LinOutMod'] = False
- p['OutFmt'] = '"ES20.12E3"' # Important for decent resolution
-
- p['AeroFile|AFAeroMod'] = 1
- p['AeroFile|CavitCheck'] = 'False'
- p['AeroFile|CompAA'] = 'False'
-
- p['ServoFile|PCMode'] = 0
- p['ServoFile|VSContrl'] = 1
-
- p['ServoFile|CompNTMD'] = 'False'
- p['ServoFile|CompTTMD'] = 'False'
-
- # Set all DOFs off, enable as desired
- p['EDFile|FlapDOF1'] = 'False'
- p['EDFile|FlapDOF2'] = 'False'
- p['EDFile|EdgeDOF'] = 'False'
- p['EDFile|TeetDOF'] = 'False'
- p['EDFile|DrTrDOF'] = 'False'
- p['EDFile|GenDOF'] = 'False'
- p['EDFile|YawDOF'] = 'False'
- p['EDFile|TwFADOF1'] = 'False'
- p['EDFile|TwFADOF2'] = 'False'
- p['EDFile|TwSSDOF1'] = 'False'
- p['EDFile|TwSSDOF2'] = 'False'
- p['EDFile|PtfmSgDOF'] = 'False'
- p['EDFile|PtfmSwDOF'] = 'False'
- p['EDFile|PtfmHvDOF'] = 'False'
- p['EDFile|PtfmRDOF'] = 'False'
- p['EDFile|PtfmPDOF'] = 'False'
- p['EDFile|PtfmYDOF'] = 'False'
-
-
- return p
-
-# --------------------------------------------------------------------------------}
-# ---
-# --------------------------------------------------------------------------------{
-def createStepWind(filename,WSstep=1,WSmin=3,WSmax=25,tstep=100,dt=0.5,tmin=0,tmax=999):
- f = weio.FASTWndFile()
- Steps= np.arange(WSmin,WSmax+WSstep,WSstep)
- print(Steps)
- nCol = len(f.colNames)
- nRow = len(Steps)*2
- M = np.zeros((nRow,nCol));
- M[0,0] = tmin
- M[0,1] = WSmin
- for i,s in enumerate(Steps[:-1]):
- M[2*i+1,0] = tmin + (i+1)*tstep-dt
- M[2*i+2,0] = tmin + (i+1)*tstep
- M[2*i+1,1] = Steps[i]
- if i0:
- main_fastfile=os.path.basename(main_fastfile)
-
- # --- Reading main fast file to get rotor radius
- fst = fi.FASTInputFile(os.path.join(refdir,main_fastfile))
- ed = fi.FASTInputFile(os.path.join(refdir,fst['EDFile'].replace('"','')))
- R = ed['TipRad']
-
- # --- Making sure we have
- if (Omega is not None):
- if (Lambda is not None):
- WS = np.ones(Omega.shape)*WS_default
- elif (WS is not None):
- if len(WS)!=len(Omega):
- raise Exception('When providing Omega and WS, both vectors should have the same dimension')
- else:
- WS = np.ones(Omega.shape)*WS_default
- else:
- Omega = WS_default * Lambda/R*60/(2*np.pi) # TODO, use more realistic combinations of WS and Omega
- WS = np.ones(Omega.shape)*WS_default
-
-
- # --- Defining flat vectors of operating conditions
- WS_flat = []
- RPM_flat = []
- Pitch_flat = []
- for pitch in Pitch:
- for (rpm,ws) in zip(Omega,WS):
- WS_flat.append(ws)
- RPM_flat.append(rpm)
- Pitch_flat.append(pitch)
- # --- Setting up default options
- baseDict={'TMax': TMax, 'DT': 0.01, 'DT_Out': 0.1} # NOTE: Tmax should be at least 2pi/Omega
- baseDict = paramsNoController(baseDict)
- if bStiff:
- baseDict = paramsStiff(baseDict)
- if bNoGen:
- baseDict = paramsNoGen(baseDict)
- if bSteadyAero:
- baseDict = paramsSteadyAero(baseDict)
-
- # --- Creating set of parameters to be changed
- # TODO: verify that RtAeroCp and RtAeroCt are present in AeroDyn outlist
- PARAMS = paramsWS_RPM_Pitch(WS_flat,RPM_flat,Pitch_flat,baseDict=baseDict, FlatInputs=True)
-
- # --- Generating all files in a workDir
- workDir = refdir.strip('/').strip('\\')+'_CPLambdaPitch'
- print('>>> Generating inputs files in {}'.format(workDir))
- RemoveAllowed=reRun # If the user want to rerun, we can remove, otherwise we keep existing simulations
- fastFiles=templateReplace(PARAMS, refdir, outputDir=workDir,removeRefSubFiles=True,removeAllowed=RemoveAllowed,main_file=main_fastfile)
-
- # --- Running fast simulations
- print('>>> Running {} simulations...'.format(len(fastFiles)))
- runner.run_fastfiles(fastFiles, showOutputs=showOutputs, fastExe=fastExe, nCores=nCores, reRun=reRun)
-
- # --- Postpro - Computing averages at the end of the simluation
- print('>>> Postprocessing...')
- outFiles = [os.path.splitext(f)[0]+'.outb' for f in fastFiles]
- # outFiles = glob.glob(os.path.join(workDir,'*.outb'))
- ColKeepStats = ['RotSpeed_[rpm]','BldPitch1_[deg]','RtAeroCp_[-]','RtAeroCt_[-]','Wind1VelX_[m/s]']
- result = postpro.averagePostPro(outFiles,avgMethod='periods',avgParam=1,ColKeep=ColKeepStats,ColSort='RotSpeed_[rpm]')
- # print(result)
-
- # --- Adding lambda, sorting and keeping only few columns
- result['lambda_[-]'] = result['RotSpeed_[rpm]']*R*2*np.pi/60/result['Wind1VelX_[m/s]']
- result.sort_values(['lambda_[-]','BldPitch1_[deg]'],ascending=[True,True],inplace=True)
- ColKeepFinal=['lambda_[-]','BldPitch1_[deg]','RtAeroCp_[-]','RtAeroCt_[-]']
- result=result[ColKeepFinal]
- print('>>> Done')
-
- # --- Converting to a matrices
- CP = result['RtAeroCp_[-]'].values
- CT = result['RtAeroCt_[-]'].values
- MCP =CP.reshape((len(Lambda),len(Pitch)))
- MCT =CT.reshape((len(Lambda),len(Pitch)))
- LAMBDA, PITCH = np.meshgrid(Lambda, Pitch)
- # --- CP max
- i,j = np.unravel_index(MCP.argmax(), MCP.shape)
- MaxVal={'CP_max':MCP[i,j],'lambda_opt':LAMBDA[j,i],'pitch_opt':PITCH[j,i]}
-
- return MCP,MCT,Lambda,Pitch,MaxVal,result
-
-
-if __name__=='__main__':
- # --- Test of templateReplace
- PARAMS = {}
- PARAMS['TMax'] = 10
- PARAMS['__name__'] = 'MyName'
- PARAMS['DT'] = 0.01
- PARAMS['DT_Out'] = 0.1
- PARAMS['EDFile|RotSpeed'] = 100
- PARAMS['EDFile|BlPitch(1)'] = 1
- PARAMS['EDFile|GBoxEff'] = 0.92
- PARAMS['ServoFile|VS_Rgn2K'] = 0.00038245
- PARAMS['ServoFile|GenEff'] = 0.95
- PARAMS['InflowFile|HWindSpeed'] = 8
- templateReplace(PARAMS,refDir,RemoveRefSubFiles=True)
-
+from __future__ import division, print_function
+import os
+import collections
+import glob
+import pandas as pd
+import numpy as np
+import shutil
+import stat
+import re
+
+# --- Misc fast libraries
+import weio.weio.fast_input_file as fi
+import pydatview.fast.runner as runner
+import pydatview.fast.postpro as postpro
+#import pyFAST.input_output.fast_input_file as fi
+#import pyFAST.case_generation.runner as runner
+#import pyFAST.input_output.postpro as postpro
+
+
+# --------------------------------------------------------------------------------}
+# --- Template replace
+# --------------------------------------------------------------------------------{
+def handleRemoveReadonlyWin(func, path, exc_info):
+ """
+ Error handler for ``shutil.rmtree``.
+ If the error is due to an access error (read only file)
+ it attempts to add write permission and then retries.
+ Usage : ``shutil.rmtree(path, onerror=onerror)``
+ """
+ if not os.access(path, os.W_OK):
+ # Is the error an access error ?
+ os.chmod(path, stat.S_IWUSR)
+ func(path)
+ else:
+ raise
+
+
+def forceCopyFile (sfile, dfile):
+ # ---- Handling error due to wrong mod
+ if os.path.isfile(dfile):
+ if not os.access(dfile, os.W_OK):
+ os.chmod(dfile, stat.S_IWUSR)
+ #print(sfile, ' > ', dfile)
+ shutil.copy2(sfile, dfile)
+
+def copyTree(src, dst):
+ """
+ Copy a directory to another one, overwritting files if necessary.
+ copy_tree from distutils and copytree from shutil fail on Windows (in particular on git files)
+ """
+ def forceMergeFlatDir(srcDir, dstDir):
+ if not os.path.exists(dstDir):
+ os.makedirs(dstDir)
+ for item in os.listdir(srcDir):
+ srcFile = os.path.join(srcDir, item)
+ dstFile = os.path.join(dstDir, item)
+ forceCopyFile(srcFile, dstFile)
+
+ def isAFlatDir(sDir):
+ for item in os.listdir(sDir):
+ sItem = os.path.join(sDir, item)
+ if os.path.isdir(sItem):
+ return False
+ return True
+
+ for item in os.listdir(src):
+ s = os.path.join(src, item)
+ d = os.path.join(dst, item)
+ if os.path.isfile(s):
+ if not os.path.exists(dst):
+ os.makedirs(dst)
+ forceCopyFile(s,d)
+ if os.path.isdir(s):
+ isRecursive = not isAFlatDir(s)
+ if isRecursive:
+ copyTree(s, d)
+ else:
+ forceMergeFlatDir(s, d)
+
+
+def templateReplaceGeneral(PARAMS, templateDir=None, outputDir=None, main_file=None, removeAllowed=False, removeRefSubFiles=False, oneSimPerDir=False):
+ """ Generate inputs files by replacing different parameters from a template file.
+ The generated files are placed in the output directory `outputDir`
+ The files are read and written using the library `weio`.
+ The template file is read and its content can be changed like a dictionary.
+ Each item of `PARAMS` correspond to a set of parameters that will be replaced
+ in the template file to generate one input file.
+
+ For "FAST" input files, parameters can be changed recursively.
+
+
+ INPUTS:
+ PARAMS: list of dictionaries. Each key of the dictionary should be a key present in the
+ template file when read with `weio` (see: weio.read(main_file).keys() )
+
+ PARAMS[0]={'DT':0.1, 'EDFile|GBRatio':1, 'ServoFile|GenEff':0.8}
+
+ templateDir: if provided, this directory and its content will be copied to `outputDir`
+ before doing the parametric substitution
+
+ outputDir : directory where files will be generated.
+ """
+ # --- Helper functions
+ def rebase_rel(wd,s,sid):
+ split = os.path.splitext(s)
+ return os.path.join(wd,split[0]+sid+split[1])
+
+ def get_strID(p) :
+ if '__name__' in p.keys():
+ strID=p['__name__']
+ else:
+ raise Exception('When calling `templateReplace`, provide the key `__name_` in the parameter dictionaries')
+ return strID
+
+ def splitAddress(sAddress):
+ sp = sAddress.split('|')
+ if len(sp)==1:
+ return sp[0],[]
+ else:
+ return sp[0],sp[1:]
+
+ def rebaseFileName(org_filename, workDir, strID):
+ new_filename_full = rebase_rel(workDir, org_filename,'_'+strID)
+ new_filename = os.path.relpath(new_filename_full,workDir).replace('\\','/')
+ return new_filename, new_filename_full
+
+ def replaceRecurse(templatename_or_newname, FileKey, ParamKey, ParamValue, Files, strID, workDir, TemplateFiles):
+ """
+ FileKey: a single key defining which file we are currently modifying e.g. :'AeroFile', 'EDFile','FVWInputFileName'
+ ParamKey: the address key of the parameter to be changed, relative to the current FileKey
+ e.g. 'EDFile|IntMethod' (if FileKey is '')
+ 'IntMethod' (if FileKey is 'EDFile')
+ ParamValue: the value to be used
+ Files: dict of files, as returned by weio, keys are "FileKeys"
+ """
+ # --- Special handling for the root
+ if FileKey=='':
+ FileKey='Root'
+ # --- Open (or get if already open) file where a parameter needs to be changed
+ if FileKey in Files.keys():
+ # The file was already opened, it's stored
+ f = Files[FileKey]
+ newfilename_full = f.filename
+ newfilename = os.path.relpath(newfilename_full,workDir).replace('\\','/')
+
+ else:
+ templatefilename = templatename_or_newname
+ templatefilename_full = os.path.join(workDir,templatefilename)
+ TemplateFiles.append(templatefilename_full)
+ if FileKey=='Root':
+ # Root files, we start from strID
+ ext = os.path.splitext(templatefilename)[-1]
+ newfilename_full = os.path.join(wd,strID+ext)
+ newfilename = strID+ext
+ else:
+ newfilename, newfilename_full = rebaseFileName(templatefilename, workDir, strID)
+ #print('--------------------------------------------------------------')
+ #print('TemplateFile :', templatefilename)
+ #print('TemplateFileFull:', templatefilename_full)
+ #print('NewFile :', newfilename)
+ #print('NewFileFull :', newfilename_full)
+ shutil.copyfile(templatefilename_full, newfilename_full)
+ f= fi.FASTInputFile(newfilename_full) # open the template file for that filekey
+ Files[FileKey]=f # store it
+
+ # --- Changing parameters in that file
+ NewFileKey_or_Key, ChildrenKeys = splitAddress(ParamKey)
+ if len(ChildrenKeys)==0:
+ # A simple parameter is changed
+ Key = NewFileKey_or_Key
+ #print('Setting', FileKey, '|',Key, 'to',ParamValue)
+ if Key=='OutList':
+ OutList=f[Key]
+ f[Key]=addToOutlist(OutList, ParamValue)
+ else:
+ f[Key] = ParamValue
+ else:
+ # Parameters needs to be changed in subfiles (children)
+ NewFileKey = NewFileKey_or_Key
+ ChildrenKey = '|'.join(ChildrenKeys)
+ child_templatefilename = f[NewFileKey].strip('"') # old filename that will be used as a template
+ baseparent = os.path.dirname(newfilename)
+ #print('Child templatefilename:',child_templatefilename)
+ #print('Parent base dir :',baseparent)
+ workDir = os.path.join(workDir, baseparent)
+
+ #
+ newchildFilename, Files = replaceRecurse(child_templatefilename, NewFileKey, ChildrenKey, ParamValue, Files, strID, workDir, TemplateFiles)
+ #print('Setting', FileKey, '|',NewFileKey, 'to',newchildFilename)
+ f[NewFileKey] = '"'+newchildFilename+'"'
+
+ return newfilename, Files
+
+
+ # --- Safety checks
+ if templateDir is None and outputDir is None:
+ raise Exception('Provide at least a template directory OR an output directory')
+
+ if templateDir is not None:
+ if not os.path.exists(templateDir):
+ raise Exception('Template directory does not exist: '+templateDir)
+
+ # Default value of outputDir if not provided
+ if templateDir[-1]=='/' or templateDir[-1]=='\\' :
+ templateDir=templateDir[0:-1]
+ if outputDir is None:
+ outputDir=templateDir+'_Parametric'
+
+ # --- Main file use as "master"
+ if templateDir is not None:
+ main_file=os.path.join(outputDir, os.path.basename(main_file))
+ else:
+ main_file=main_file
+
+ # Params need to be a list
+ if not isinstance(PARAMS,list):
+ PARAMS=[PARAMS]
+
+ if oneSimPerDir:
+ workDirS=[os.path.join(outputDir,get_strID(p)) for p in PARAMS]
+ else:
+ workDirS=[outputDir]*len(PARAMS)
+ # --- Creating outputDir - Copying template folder to outputDir if necessary
+ # Copying template folder to workDir
+ for wd in list(set(workDirS)):
+ if removeAllowed:
+ removeFASTOuputs(wd)
+ if os.path.exists(wd) and removeAllowed:
+ shutil.rmtree(wd, ignore_errors=False, onerror=handleRemoveReadonlyWin)
+ copyTree(templateDir, wd)
+ if removeAllowed:
+ removeFASTOuputs(wd)
+
+
+ TemplateFiles=[]
+ files=[]
+ for ip,(wd,p) in enumerate(zip(workDirS,PARAMS)):
+ if '__index__' not in p.keys():
+ p['__index__']=ip
+
+ main_file_base = os.path.basename(main_file)
+ strID = get_strID(p)
+ # --- Setting up files for this simulation
+ Files=dict()
+ for k,v in p.items():
+ if k =='__index__' or k=='__name__':
+ continue
+ new_mainFile, Files = replaceRecurse(main_file_base, '', k, v, Files, strID, wd, TemplateFiles)
+
+ # --- Writting files
+ for k,f in Files.items():
+ if k=='Root':
+ files.append(f.filename)
+ f.write()
+
+ # --- Remove extra files at the end
+ if removeRefSubFiles:
+ TemplateFiles, nCounts = np.unique(TemplateFiles, return_counts=True)
+ if not oneSimPerDir:
+ # we can only detele template files that were used by ALL simulations
+ TemplateFiles=[t for nc,t in zip(nCounts, TemplateFiles) if nc==len(PARAMS)]
+ for tf in TemplateFiles:
+ try:
+ os.remove(tf)
+ except:
+ print('[FAIL] Removing '+tf)
+ pass
+ return files
+
+def templateReplace(PARAMS, templateDir, outputDir=None, main_file=None, removeAllowed=False, removeRefSubFiles=False, oneSimPerDir=False):
+ """ Replace parameters in a fast folder using a list of dictionaries where the keys are for instance:
+ 'DT', 'EDFile|GBRatio', 'ServoFile|GenEff'
+ """
+ # --- For backward compatibility, remove "FAST|" from the keys
+ for p in PARAMS:
+ old_keys=[ k for k,_ in p.items() if k.find('FAST|')==0]
+ for k_old in old_keys:
+ k_new=k_old.replace('FAST|','')
+ p[k_new] = p.pop(k_old)
+
+ return templateReplaceGeneral(PARAMS, templateDir, outputDir=outputDir, main_file=main_file,
+ removeAllowed=removeAllowed, removeRefSubFiles=removeRefSubFiles, oneSimPerDir=oneSimPerDir)
+
+def removeFASTOuputs(workDir):
+ # Cleaning folder
+ for f in glob.glob(os.path.join(workDir,'*.out')):
+ os.remove(f)
+ for f in glob.glob(os.path.join(workDir,'*.outb')):
+ os.remove(f)
+ for f in glob.glob(os.path.join(workDir,'*.ech')):
+ os.remove(f)
+ for f in glob.glob(os.path.join(workDir,'*.sum')):
+ os.remove(f)
+
+# --------------------------------------------------------------------------------}
+# --- Tools for template replacement
+# --------------------------------------------------------------------------------{
+def paramsSteadyAero(p=None):
+ p = dict() if p is None else p
+ p['AeroFile|AFAeroMod']=1 # remove dynamic effects dynamic
+ p['AeroFile|WakeMod']=1 # remove dynamic inflow dynamic
+ p['AeroFile|TwrPotent']=0 # remove tower shadow
+ return p
+
+def paramsNoGen(p=None):
+ p = dict() if p is None else p
+ p['EDFile|GenDOF' ] = 'False'
+ return p
+
+def paramsGen(p=None):
+ p = dict() if p is None else p
+ p['EDFile|GenDOF' ] = 'True'
+ return p
+
+def paramsNoController(p=None):
+ p = dict() if p is None else p
+ p['ServoFile|PCMode'] = 0;
+ p['ServoFile|VSContrl'] = 0;
+ p['ServoFile|YCMode'] = 0;
+ return p
+
+def paramsControllerDLL(p=None):
+ p = dict() if p is None else p
+ p['ServoFile|PCMode'] = 5;
+ p['ServoFile|VSContrl'] = 5;
+ p['ServoFile|YCMode'] = 5;
+ p['EDFile|GenDOF'] = 'True';
+ return p
+
+
+def paramsStiff(p=None):
+ p = dict() if p is None else p
+ p['EDFile|FlapDOF1'] = 'False'
+ p['EDFile|FlapDOF2'] = 'False'
+ p['EDFile|EdgeDOF' ] = 'False'
+ p['EDFile|TeetDOF' ] = 'False'
+ p['EDFile|DrTrDOF' ] = 'False'
+ p['EDFile|YawDOF' ] = 'False'
+ p['EDFile|TwFADOF1'] = 'False'
+ p['EDFile|TwFADOF2'] = 'False'
+ p['EDFile|TwSSDOF1'] = 'False'
+ p['EDFile|TwSSDOF2'] = 'False'
+ p['EDFile|PtfmSgDOF'] = 'False'
+ p['EDFile|PtfmSwDOF'] = 'False'
+ p['EDFile|PtfmHvDOF'] = 'False'
+ p['EDFile|PtfmRDOF'] = 'False'
+ p['EDFile|PtfmPDOF'] = 'False'
+ p['EDFile|PtfmYDOF'] = 'False'
+ return p
+
+def paramsWS_RPM_Pitch(WS, RPM, Pitch, baseDict=None, flatInputs=False):
+ """
+ Generate OpenFAST "parameters" (list of dictionaries with "address")
+ chaing the inputs in ElastoDyn, InflowWind for different wind speed, RPM and Pitch
+ """
+ # --- Ensuring everythin is an iterator
+ def iterify(x):
+ if not isinstance(x, collections.Iterable): x = [x]
+ return x
+ WS = iterify(WS)
+ RPM = iterify(RPM)
+ Pitch = iterify(Pitch)
+ # --- If inputs are not flat but different vectors to length through, we flatten them (TODO: meshgrid and ravel?)
+ if not flatInputs :
+ WS_flat = []
+ Pitch_flat = []
+ RPM_flat = []
+ for pitch in Pitch:
+ for rpm in RPM:
+ for ws in WS:
+ WS_flat.append(ws)
+ RPM_flat.append(rpm)
+ Pitch_flat.append(pitch)
+ else:
+ WS_flat, Pitch_flat, RPM_flat = WS, Pitch, RPM
+
+ # --- Defining the parametric study
+ PARAMS=[]
+ i=0
+ for ws,rpm,pitch in zip(WS_flat,RPM_flat,Pitch_flat):
+ if baseDict is None:
+ p=dict()
+ else:
+ p = baseDict.copy()
+ p['EDFile|RotSpeed'] = rpm
+ p['InflowFile|HWindSpeed'] = ws
+ p['InflowFile|WindType'] = 1 # Setting steady wind
+ p['EDFile|BlPitch(1)'] = pitch
+ p['EDFile|BlPitch(2)'] = pitch
+ p['EDFile|BlPitch(3)'] = pitch
+
+ p['__index__'] = i
+ p['__name__'] = '{:03d}_ws{:04.1f}_pt{:04.2f}_om{:04.2f}'.format(p['__index__'],p['InflowFile|HWindSpeed'],p['EDFile|BlPitch(1)'],p['EDFile|RotSpeed'])
+ i=i+1
+ PARAMS.append(p)
+ return PARAMS
+
+def paramsLinearTrim(p=None):
+ p = dict() if p is None else p
+
+ # Set a few DOFs, move this to main file
+ p['Linearize'] = True
+ p['CalcSteady'] = True
+ p['TrimGain'] = 1e-4
+ p['TrimTol'] = 1e-5
+ p['CompMooring'] = 0
+ p['CompHydro'] = 0
+ p['LinOutJac'] = False
+ p['LinOutMod'] = False
+ p['OutFmt'] = '"ES20.12E3"' # Important for decent resolution
+
+ p['AeroFile|AFAeroMod'] = 1
+ p['AeroFile|CavitCheck'] = 'False'
+ p['AeroFile|CompAA'] = 'False'
+
+ p['ServoFile|PCMode'] = 0
+ p['ServoFile|VSContrl'] = 1
+
+ p['ServoFile|CompNTMD'] = 'False'
+ p['ServoFile|CompTTMD'] = 'False'
+
+ # Set all DOFs off, enable as desired
+ p['EDFile|FlapDOF1'] = 'False'
+ p['EDFile|FlapDOF2'] = 'False'
+ p['EDFile|EdgeDOF'] = 'False'
+ p['EDFile|TeetDOF'] = 'False'
+ p['EDFile|DrTrDOF'] = 'False'
+ p['EDFile|GenDOF'] = 'False'
+ p['EDFile|YawDOF'] = 'False'
+ p['EDFile|TwFADOF1'] = 'False'
+ p['EDFile|TwFADOF2'] = 'False'
+ p['EDFile|TwSSDOF1'] = 'False'
+ p['EDFile|TwSSDOF2'] = 'False'
+ p['EDFile|PtfmSgDOF'] = 'False'
+ p['EDFile|PtfmSwDOF'] = 'False'
+ p['EDFile|PtfmHvDOF'] = 'False'
+ p['EDFile|PtfmRDOF'] = 'False'
+ p['EDFile|PtfmPDOF'] = 'False'
+ p['EDFile|PtfmYDOF'] = 'False'
+
+
+ return p
+
+# --------------------------------------------------------------------------------}
+# ---
+# --------------------------------------------------------------------------------{
+def createStepWind(filename,WSstep=1,WSmin=3,WSmax=25,tstep=100,dt=0.5,tmin=0,tmax=999):
+ f = weio.FASTWndFile()
+ Steps= np.arange(WSmin,WSmax+WSstep,WSstep)
+ print(Steps)
+ nCol = len(f.colNames)
+ nRow = len(Steps)*2
+ M = np.zeros((nRow,nCol));
+ M[0,0] = tmin
+ M[0,1] = WSmin
+ for i,s in enumerate(Steps[:-1]):
+ M[2*i+1,0] = tmin + (i+1)*tstep-dt
+ M[2*i+2,0] = tmin + (i+1)*tstep
+ M[2*i+1,1] = Steps[i]
+ if i0:
+ main_fastfile=os.path.basename(main_fastfile)
+
+ # --- Reading main fast file to get rotor radius
+ fst = fi.FASTInputFile(os.path.join(refdir,main_fastfile))
+ ed = fi.FASTInputFile(os.path.join(refdir,fst['EDFile'].replace('"','')))
+ R = ed['TipRad']
+
+ # --- Making sure we have
+ if (Omega is not None):
+ if (Lambda is not None):
+ WS = np.ones(Omega.shape)*WS_default
+ elif (WS is not None):
+ if len(WS)!=len(Omega):
+ raise Exception('When providing Omega and WS, both vectors should have the same dimension')
+ else:
+ WS = np.ones(Omega.shape)*WS_default
+ else:
+ Omega = WS_default * Lambda/R*60/(2*np.pi) # TODO, use more realistic combinations of WS and Omega
+ WS = np.ones(Omega.shape)*WS_default
+
+
+ # --- Defining flat vectors of operating conditions
+ WS_flat = []
+ RPM_flat = []
+ Pitch_flat = []
+ for pitch in Pitch:
+ for (rpm,ws) in zip(Omega,WS):
+ WS_flat.append(ws)
+ RPM_flat.append(rpm)
+ Pitch_flat.append(pitch)
+ # --- Setting up default options
+ baseDict={'TMax': TMax, 'DT': 0.01, 'DT_Out': 0.1} # NOTE: Tmax should be at least 2pi/Omega
+ baseDict = paramsNoController(baseDict)
+ if bStiff:
+ baseDict = paramsStiff(baseDict)
+ if bNoGen:
+ baseDict = paramsNoGen(baseDict)
+ if bSteadyAero:
+ baseDict = paramsSteadyAero(baseDict)
+
+ # --- Creating set of parameters to be changed
+ # TODO: verify that RtAeroCp and RtAeroCt are present in AeroDyn outlist
+ PARAMS = paramsWS_RPM_Pitch(WS_flat,RPM_flat,Pitch_flat,baseDict=baseDict, FlatInputs=True)
+
+ # --- Generating all files in a workDir
+ workDir = refdir.strip('/').strip('\\')+'_CPLambdaPitch'
+ print('>>> Generating inputs files in {}'.format(workDir))
+ RemoveAllowed=reRun # If the user want to rerun, we can remove, otherwise we keep existing simulations
+ fastFiles=templateReplace(PARAMS, refdir, outputDir=workDir,removeRefSubFiles=True,removeAllowed=RemoveAllowed,main_file=main_fastfile)
+
+ # --- Running fast simulations
+ print('>>> Running {} simulations...'.format(len(fastFiles)))
+ runner.run_fastfiles(fastFiles, showOutputs=showOutputs, fastExe=fastExe, nCores=nCores, reRun=reRun)
+
+ # --- Postpro - Computing averages at the end of the simluation
+ print('>>> Postprocessing...')
+ outFiles = [os.path.splitext(f)[0]+'.outb' for f in fastFiles]
+ # outFiles = glob.glob(os.path.join(workDir,'*.outb'))
+ ColKeepStats = ['RotSpeed_[rpm]','BldPitch1_[deg]','RtAeroCp_[-]','RtAeroCt_[-]','Wind1VelX_[m/s]']
+ result = postpro.averagePostPro(outFiles,avgMethod='periods',avgParam=1,ColKeep=ColKeepStats,ColSort='RotSpeed_[rpm]')
+ # print(result)
+
+ # --- Adding lambda, sorting and keeping only few columns
+ result['lambda_[-]'] = result['RotSpeed_[rpm]']*R*2*np.pi/60/result['Wind1VelX_[m/s]']
+ result.sort_values(['lambda_[-]','BldPitch1_[deg]'],ascending=[True,True],inplace=True)
+ ColKeepFinal=['lambda_[-]','BldPitch1_[deg]','RtAeroCp_[-]','RtAeroCt_[-]']
+ result=result[ColKeepFinal]
+ print('>>> Done')
+
+ # --- Converting to a matrices
+ CP = result['RtAeroCp_[-]'].values
+ CT = result['RtAeroCt_[-]'].values
+ MCP =CP.reshape((len(Lambda),len(Pitch)))
+ MCT =CT.reshape((len(Lambda),len(Pitch)))
+ LAMBDA, PITCH = np.meshgrid(Lambda, Pitch)
+ # --- CP max
+ i,j = np.unravel_index(MCP.argmax(), MCP.shape)
+ MaxVal={'CP_max':MCP[i,j],'lambda_opt':LAMBDA[j,i],'pitch_opt':PITCH[j,i]}
+
+ return MCP,MCT,Lambda,Pitch,MaxVal,result
+
+
+if __name__=='__main__':
+ # --- Test of templateReplace
+ PARAMS = {}
+ PARAMS['TMax'] = 10
+ PARAMS['__name__'] = 'MyName'
+ PARAMS['DT'] = 0.01
+ PARAMS['DT_Out'] = 0.1
+ PARAMS['EDFile|RotSpeed'] = 100
+ PARAMS['EDFile|BlPitch(1)'] = 1
+ PARAMS['EDFile|GBoxEff'] = 0.92
+ PARAMS['ServoFile|VS_Rgn2K'] = 0.00038245
+ PARAMS['ServoFile|GenEff'] = 0.95
+ PARAMS['InflowFile|HWindSpeed'] = 8
+ templateReplace(PARAMS,refDir,RemoveRefSubFiles=True)
+
diff --git a/pydatview/fast/fastfarm.py b/pydatview/fast/fastfarm.py
index b09df03..1c939c0 100644
--- a/pydatview/fast/fastfarm.py
+++ b/pydatview/fast/fastfarm.py
@@ -1,482 +1,489 @@
-import os
-import glob
-import numpy as np
-import pandas as pd
-try:
- import weio
-except:
- raise Exception('Python package `weio` not found, please install it from https://github.com/ebranlard/weio ')
-
-from . import fastlib
-
-# --------------------------------------------------------------------------------}
-# --- Small helper functions
-# --------------------------------------------------------------------------------{
-def insertTN(s,i,nWT=1000):
- """ insert turbine number in name """
- if nWT<10:
- fmt='{:d}'
- elif nWT<100:
- fmt='{:02d}'
- else:
- fmt='{:03d}'
- if s.find('T1')>=0:
- s=s.replace('T1','T'+fmt.format(i))
- else:
- sp=os.path.splitext(s)
- s=sp[0]+'_T'+fmt.format(i)+sp[1]
- return s
-def forceCopyFile (sfile, dfile):
- # ---- Handling error due to wrong mod
- if os.path.isfile(dfile):
- if not os.access(dfile, os.W_OK):
- os.chmod(dfile, stat.S_IWUSR)
- #print(sfile, ' > ', dfile)
- shutil.copy2(sfile, dfile)
-
-# --------------------------------------------------------------------------------}
-# --- Tools to create fast farm simulations
-# --------------------------------------------------------------------------------{
-def writeFSTandDLL(FstT1Name, nWT):
- """
- Write FST files for each turbine, with different ServoDyn files and DLL
- FST files, ServoFiles, and DLL files will be written next to their turbine 1
- files, with name Ti.
-
- FstT1Name: absolute or relative path to the Turbine FST file
- """
-
- FstT1Full = os.path.abspath(FstT1Name).replace('\\','/')
- FstDir = os.path.dirname(FstT1Full)
-
- fst=weio.read(FstT1Name)
- SrvT1Name = fst['ServoFile'].strip('"')
- SrvT1Full = os.path.join(FstDir, SrvT1Name).replace('\\','/')
- SrvDir = os.path.dirname(SrvT1Full)
- SrvT1RelFst = os.path.relpath(SrvT1Full,FstDir)
- if os.path.exists(SrvT1Full):
- srv=weio.read(SrvT1Full)
- DLLT1Name = srv['DLL_FileName'].strip('"')
- DLLT1Full = os.path.join(SrvDir, DLLT1Name)
- if os.path.exists(DLLT1Full):
- servo=True
- else:
- print('[Info] DLL file not found, not copying servo and dll files ({})'.format(DLLT1Full))
- servo=False
- else:
- print('[Info] ServoDyn file not found, not copying servo and dll files ({})'.format(SrvT1Full))
- servo=False
-
- #print(FstDir)
- #print(FstT1Full)
- #print(SrvT1Name)
- #print(SrvT1Full)
- #print(SrvT1RelFst)
-
- for i in np.arange(2,nWT+1):
- FstName = insertTN(FstT1Name,i,nWT)
- if servo:
- # TODO handle the case where T1 not present
- SrvName = insertTN(SrvT1Name,i,nWT)
- DLLName = insertTN(DLLT1Name,i,nWT)
- DLLFullName = os.path.join(SrvDir, DLLName)
-
- print('')
- print('FstName: ',FstName)
- if servo:
- print('SrvName: ',SrvName)
- print('DLLName: ',DLLName)
- print('DLLFull: ',DLLFullName)
-
- # Changing main file
- if servo:
- fst['ServoFile']='"'+SrvName+'"'
- fst.write(FstName)
- if servo:
- # Changing servo file
- srv['DLL_FileName']='"'+DLLName+'"'
- srv.write(SrvName)
- # Copying dll
- forceCopyFile(DLLT1Full, DLLFullName)
-
-
-
-def rectangularLayoutSubDomains(D,Lx,Ly):
- """ Retuns position of turbines in a rectangular layout
- TODO, unfinished function parameters
- """
- # --- Parameters
- D = 112 # turbine diameter [m]
- Lx = 3840 # x dimension of precusor
- Ly = 3840 # y dimension of precusor
- Height = 0 # Height above ground, likely 0 [m]
- nDomains_x = 2 # number of domains in x
- nDomains_y = 2 # number of domains in y
- # --- 36 WT
- nx = 3 # number of turbines to be placed along x in one precursor domain
- ny = 3 # number of turbines to be placed along y in one precursor domain
- StartX = 1/2 # How close do we start from the x boundary
- StartY = 1/2 # How close do we start from the y boundary
- # --- Derived parameters
- Lx_Domain = Lx * nDomains_x # Full domain size
- Ly_Domain = Ly * nDomains_y
- DeltaX = Lx / (nx) # Turbine spacing
- DeltaY = Ly / (ny)
- xWT = np.arange(DeltaX*StartX,Lx_Domain,DeltaX) # Turbine positions
- yWT = np.arange(DeltaY*StartY,Ly_Domain,DeltaY)
-
- print('Full domain size [D] : {:.2f} x {:.2f} '.format(Lx_Domain/D, Ly_Domain/D))
- print('Turbine spacing [D] : {:.2f} x {:.2f} '.format(DeltaX/D,DeltaX/D))
- print('Number of turbines : {:d} x {:d} = {:d}'.format(len(xWT),len(yWT),len(xWT)*len(yWT)))
-
- XWT,YWT=np.meshgrid(xWT,yWT)
- ZWT=XWT*0+Height
-
- # --- Export coordinates only
- M=np.column_stack((XWT.ravel(),YWT.ravel(),ZWT.ravel()))
- np.savetxt('Farm_Coordinates.csv', M, delimiter=',',header='X_[m], Y_[m], Z_[m]')
- print(M)
-
- return XWT, YWT, ZWT
-
-def fastFarmTurbSimExtent(TurbSimFile, HubHeight, D, xWT, yWT, Cmeander=1.9, Chord_max=3, extent_X=1.2, extent_Y=1.2):
- """
- Determines "Ambient Wind" box parametesr for FastFarm, based on a TurbSimFile ('bts')
- """
- # --- TurbSim data
- ts = weio.read(TurbSimFile)
- iy,iz = ts.closestPoint(y=0,z=HubHeight)
- meanU = ts['u'][0,:,iy,iz].mean()
- dY_High = ts['y'][1]-ts['y'][0]
- dZ_High = ts['z'][1]-ts['z'][0]
- Z0_Low = ts['z'][0]
- Z0_High = ts['z'][0] # we start at lowest to include tower
- Width = ts['y'][-1]-ts['y'][0]
- Height = ts['z'][-1]-ts['z'][0]
- dT_High = ts['dt']
- effSimLength = ts['t'][-1]-ts['t'][0] + Width/meanU
-
- # Desired resolution, rule of thumbs
- dX_High_desired = Chord_max
- dX_Low_desired = Cmeander*D*meanU/150.0
- dt_des = Cmeander*D/(10.0*meanU)
-
- # --- High domain
- ZMax_High = HubHeight+extent_Y*D/2.0
- # high-box extent in x and y [D]
- Xdist_High = extent_X*D
- Ydist_High = extent_Y*D
- Zdist_High = ZMax_High-Z0_High # we include the tower
- X0_rel = Xdist_High/2.0
- Y0_rel = Ydist_High/2.0
- Length = effSimLength*meanU
- nx = int(round(effSimLength/dT_High))
- dx_TS = Length/(nx-1)
- dX_High = round(dX_High_desired/dx_TS)*dx_TS
-
- nX_High = int(round(Xdist_High/dX_High)+1)
- nY_High = int(round(Ydist_High/dY_High)+1)
- nZ_High = int(round(Zdist_High/dZ_High)+1)
-
- # --- High extent per turbine
- nTurbs = len(xWT)
- X0_des = np.asarray(xWT)-X0_rel
- Y0_des = np.asarray(yWT)-Y0_rel
- X0_High = np.around(np.round(X0_des/dX_High)*dX_High,3)
- Y0_High = np.around(np.round(Y0_des/dY_High)*dY_High,3)
-
- # --- Low domain
- dT_Low = round(dt_des/dT_High)*dT_High
- dx_des = dX_Low_desired
- dy_des = dX_Low_desired
- dz_des = dX_Low_desired
- X0_Low = min(xWT)-2*D
- Y0_Low = -Width/2
- dX_Low = round(dx_des/dX_High)*dX_High
- dY_Low = round(dy_des/dY_High)*dY_High
- dZ_Low = round(dz_des/dZ_High)*dZ_High
- Xdist = max(xWT)+8.0*D-X0_Low # Maximum extent
- Ydist = Width
- Zdist = Height
-
- nX_Low = int(Xdist/dX_Low)+1;
- nY_Low = int(Ydist/dY_Low)+1;
- nZ_Low = int(Zdist/dZ_Low)+1;
-
- if (nX_Low*dX_Low>Xdist):
- nX_Low=nX_Low-1
- if (nY_Low*dY_Low>Ydist):
- nY_Low=nY_Low-1
- if (nZ_Low*dZ_Low>Zdist):
- nZ_Low=nZ_Low-1
-
- d = dict()
- d['DT'] = np.around(dT_Low ,3)
- d['DT_High'] = np.around(dT_High,3)
- d['NX_Low'] = int(nX_Low)
- d['NY_Low'] = int(nY_Low)
- d['NZ_Low'] = int(nZ_Low)
- d['X0_Low'] = np.around(X0_Low,3)
- d['Y0_Low'] = np.around(Y0_Low,3)
- d['Z0_Low'] = np.around(Z0_Low,3)
- d['dX_Low'] = np.around(dX_Low,3)
- d['dY_Low'] = np.around(dY_Low,3)
- d['dZ_Low'] = np.around(dZ_Low,3)
- d['NX_High'] = int(nX_High)
- d['NY_High'] = int(nY_High)
- d['NZ_High'] = int(nZ_High)
- # --- High extent info for turbine outputs
- d['dX_High'] = np.around(dX_High,3)
- d['dY_High'] = np.around(dY_High,3)
- d['dZ_High'] = np.around(dZ_High,3)
- d['X0_High'] = X0_High
- d['Y0_High'] = Y0_High
- d['Z0_High'] = np.around(Z0_High,3)
-
- return d
-
-def writeFastFarm(outputFile, templateFile, xWT, yWT, zWT, FFTS=None, OutListT1=None):
- """ Write FastFarm input file based on a template, a TurbSimFile and the Layout
-
- outputFile: .fstf file to be written
- templateFile: .fstf file that will be used to generate the output_file
- XWT,YWT,ZWT: positions of turbines
- FFTS: FastFarm TurbSim parameters as returned by fastFarmTurbSimExtent
- """
- # --- Read template fast farm file
- fst=weio.FASTInputFile(templateFile)
- # --- Replace box extent values
- if FFTS is not None:
- fst['Mod_AmbWind'] = 2
- for k in ['DT', 'DT_High', 'NX_Low', 'NY_Low', 'NZ_Low', 'X0_Low', 'Y0_Low', 'Z0_Low', 'dX_Low', 'dY_Low', 'dZ_Low', 'NX_High', 'NY_High', 'NZ_High']:
- if isinstance(FFTS[k],int):
- fst[k] = FFTS[k]
- else:
- fst[k] = np.around(FFTS[k],3)
- fst['WrDisDT'] = FFTS['DT']
-
- # --- Set turbine names, position, and box extent
- nWT = len(xWT)
- fst['NumTurbines'] = nWT
- if FFTS is not None:
- nCol= 10
- else:
- nCol = 4
- ref_path = fst['WindTurbines'][0,3]
- WT = np.array(['']*nWT*nCol,dtype='object').reshape((nWT,nCol))
- for iWT,(x,y,z) in enumerate(zip(xWT,yWT,zWT)):
- WT[iWT,0]=x
- WT[iWT,1]=y
- WT[iWT,2]=z
- WT[iWT,3]=insertTN(ref_path,iWT+1,nWT)
- if FFTS is not None:
- WT[iWT,4]=FFTS['X0_High'][iWT]
- WT[iWT,5]=FFTS['Y0_High'][iWT]
- WT[iWT,6]=FFTS['Z0_High']
- WT[iWT,7]=FFTS['dX_High']
- WT[iWT,8]=FFTS['dY_High']
- WT[iWT,9]=FFTS['dZ_High']
- fst['WindTurbines']=WT
-
- fst.write(outputFile)
- if OutListT1 is not None:
- setFastFarmOutputs(outputFile, OutListT1)
-
-def setFastFarmOutputs(fastFarmFile, OutListT1):
- """ Duplicate the output list, by replacing "T1" with T1->Tn """
- fst = weio.read(fastFarmFile)
- nWTOut = min(fst['NumTurbines'],9) # Limited to 9 turbines
- OutList=['']
- for s in OutListT1:
- s=s.strip('"')
- if s.find('T1'):
- OutList+=['"'+s.replace('T1','T{:d}'.format(iWT+1))+'"' for iWT in np.arange(nWTOut) ]
- else:
- OutList+='"'+s+'"'
- fst['OutList']=OutList
- fst.write(fastFarmFile)
-
-
-def plotFastFarmSetup(fastFarmFile):
- """ """
- import matplotlib.pyplot as plt
- fst=weio.FASTInputFile(fastFarmFile)
-
- fig = plt.figure(figsize=(13.5,10))
- ax = fig.add_subplot(111,aspect="equal")
-
- WT=fst['WindTurbines']
- x = WT[:,0].astype(float)
- y = WT[:,1].astype(float)
-
- if fst['Mod_AmbWind'] == 2:
- xmax_low = fst['X0_Low']+fst['DX_Low']*fst['NX_Low']
- ymax_low = fst['Y0_Low']+fst['DY_Low']*fst['NY_Low']
- # low-res box
- ax.plot([fst['X0_Low'],xmax_low,xmax_low,fst['X0_Low'],fst['X0_Low']],
- [fst['Y0_Low'],fst['Y0_Low'],ymax_low,ymax_low,fst['Y0_Low']],'--k',lw=2,label='Low')
- X0_High = WT[:,4].astype(float)
- Y0_High = WT[:,5].astype(float)
- dX_High = WT[:,7].astype(float)[0]
- dY_High = WT[:,8].astype(float)[0]
- nX_High = fst['NX_High']
- nY_High = fst['NY_High']
- # high-res boxes
- for wt in range(len(x)):
- xmax_high = X0_High[wt]+dX_High*nX_High
- ymax_high = Y0_High[wt]+dY_High*nY_High
- ax.plot([X0_High[wt],xmax_high,xmax_high,X0_High[wt],X0_High[wt]],
- [Y0_High[wt],Y0_High[wt],ymax_high,ymax_high,Y0_High[wt]],
- '-',
- label="HighT{0}".format(wt+1))
- ax.plot(x[wt],y[wt],'x',ms=8,mew=2,label="WT{0}".format(wt+1))
- else:
- for wt in range(len(x)):
- ax.plot(x[wt],y[wt],'x',ms=8,mew=2,label="WT{0}".format(wt+1))
- #
- plt.legend(bbox_to_anchor=(1.05,1.015),frameon=False)
- ax.set_xlabel("x-location [m]")
- ax.set_ylabel("y-location [m]")
- fig.tight_layout
- # fig.savefig('FFarmLayout.pdf',bbox_to_inches='tight',dpi=500)
-
-# --------------------------------------------------------------------------------}
-# --- Tools for postpro
-# --------------------------------------------------------------------------------{
-
-def spanwiseColFastFarm(Cols, nWT=9, nD=9):
- """ Return column info, available columns and indices that contain AD spanwise data"""
- FFSpanMap=dict()
- for i in np.arange(nWT):
- FFSpanMap['^CtT{:d}N(\d*)_\[-\]'.format(i+1)]='CtT{:d}_[-]'.format(i+1)
- for i in np.arange(nWT):
- for k in np.arange(nD):
- FFSpanMap['^WkDfVxT{:d}N(\d*)D{:d}_\[m/s\]'.format(i+1,k+1) ]='WkDfVxT{:d}D{:d}_[m/s]'.format(i+1, k+1)
- for i in np.arange(nWT):
- for k in np.arange(nD):
- FFSpanMap['^WkDfVrT{:d}N(\d*)D{:d}_\[m/s\]'.format(i+1,k+1) ]='WkDfVrT{:d}D{:d}_[m/s]'.format(i+1, k+1)
-
- return fastlib.find_matching_columns(Cols, FFSpanMap)
-
-def diameterwiseColFastFarm(Cols, nWT=9):
- """ Return column info, available columns and indices that contain AD spanwise data"""
- FFDiamMap=dict()
- for i in np.arange(nWT):
- for x in ['X','Y','Z']:
- FFDiamMap['^WkAxs{}T{:d}D(\d*)_\[-\]'.format(x,i+1)] ='WkAxs{}T{:d}_[-]'.format(x,i+1)
- for i in np.arange(nWT):
- for x in ['X','Y','Z']:
- FFDiamMap['^WkPos{}T{:d}D(\d*)_\[m\]'.format(x,i+1)] ='WkPos{}T{:d}_[m]'.format(x,i+1)
- for i in np.arange(nWT):
- for x in ['X','Y','Z']:
- FFDiamMap['^WkVel{}T{:d}D(\d*)_\[m/s\]'.format(x,i+1)] ='WkVel{}T{:d}_[m/s]'.format(x,i+1)
- for i in np.arange(nWT):
- for x in ['X','Y','Z']:
- FFDiamMap['^WkDiam{}T{:d}D(\d*)_\[m\]'.format(x,i+1)] ='WkDiam{}T{:d}_[m]'.format(x,i+1)
- return fastlib.find_matching_columns(Cols, FFDiamMap)
-
-def SensorsFARMRadial(nWT=3,nD=10,nR=30,signals=None):
- """ Returns a list of FASTFarm sensors that are used for the radial distribution
- of quantities (e.g. Ct, Wake Deficits).
- If `signals` is provided, the output is the list of sensors within the list `signals`.
- """
- WT = np.arange(nWT)
- r = np.arange(nR)
- D = np.arange(nD)
- sens=[]
- sens+=['CtT{:d}N{:02d}_[-]'.format(i+1,j+1) for i in WT for j in r]
- sens+=['WkDfVxT{:d}N{:02d}D{:d}_[m/s]'.format(i+1,j+1,k+1) for i in WT for j in r for k in D]
- sens+=['WkDfVrT{:d}N{:02d}D{:d}_[m/s]'.format(i+1,j+1,k+1) for i in WT for j in r for k in D]
- if signals is not None:
- sens = [c for c in sens if c in signals]
- return sens
-
-def SensorsFARMDiam(nWT,nD):
- """ Returns a list of FASTFarm sensors that contain quantities at different downstream diameters
- (e.g. WkAxs, WkPos, WkVel, WkDiam)
- If `signals` is provided, the output is the list of sensors within the list `signals`.
- """
- WT = np.arange(nWT)
- D = np.arange(nD)
- XYZ = ['X','Y','Z']
- sens=[]
- sens+=['WkAxs{}T{:d}D{:d}_[-]'.format(x,i+1,j+1) for x in XYZ for i in WT for j in D]
- sens+=['WkPos{}T{:d}D{:d}_[m]'.format(x,i+1,j+1) for x in XYZ for i in WT for j in D]
- sens+=['WkVel{}T{:d}D{:d}_[m/s]'.format(x,i+1,j+1) for x in XYZ for i in WT for j in D]
- sens+=['WkDiam{}T{:d}D{:d}_[m]'.format(x,i+1,j+1) for x in XYZ for i in WT for j in D]
- if signals is not None:
- sens = [c for c in sens if c in signals]
- return sens
-
-
-def extractFFRadialData(fastfarm_out,fastfarm_input,avgMethod='constantwindow',avgParam=30,D=1,df=None):
- # LEGACY
- return spanwisePostProFF(fastfarm_input,avgMethod=avgMethod,avgParam=avgParam,D=D,df=df,fastfarm_out=fastfarm_out)
-
-
-def spanwisePostProFF(fastfarm_input,avgMethod='constantwindow',avgParam=30,D=1,df=None,fastfarm_out=None):
- """
- Opens a FASTFarm output file, extract the radial data, average them and returns spanwise data
-
- D: diameter TODO, extract it from the main file
-
- See faslibt.averageDF for `avgMethod` and `avgParam`.
- """
- # --- Opening ouputfile
- if df is None:
- df=weio.read(fastfarm_out).toDataFrame()
-
- # --- Opening input file and extracting inportant variables
- if fastfarm_input is None:
- # We don't have an input file, guess numbers of turbine, diameters, Nodes...
- cols, sIdx = fastlib.find_matching_pattern(df.columns.values, 'T(\d+)')
- nWT = np.array(sIdx).astype(int).max()
- cols, sIdx = fastlib.find_matching_pattern(df.columns.values, 'D(\d+)')
- nD = np.array(sIdx).astype(int).max()
- cols, sIdx = fastlib.find_matching_pattern(df.columns.values, 'N(\d+)')
- nr = np.array(sIdx).astype(int).max()
- vr=None
- vD=None
- D=0
- else:
- main=weio.FASTInputFile(fastfarm_input)
- iOut = main['OutRadii']
- dr = main['dr'] # Radial increment of radial finite-difference grid (m)
- OutDist = main['OutDist'] # List of downstream distances for wake output for an individual rotor
- WT = main['WindTurbines']
- nWT = len(WT)
- vr = dr*np.array(iOut)
- vD = np.array(OutDist)
- nr=len(iOut)
- nD=len(vD)
-
-
- # --- Extracting time series of radial data only
- colRadial = SensorsFARMRadial(nWT=nWT,nD=nD,nR=nr,signals=df.columns.values)
- colRadial=['Time_[s]']+colRadial
- dfRadialTime = df[colRadial] # TODO try to do some magic with it, display it with a slider
-
- # --- Averaging data
- dfAvg = fastlib.averageDF(df,avgMethod=avgMethod,avgParam=avgParam)
-
- # --- Extract radial data
- ColsInfo, nrMax = spanwiseColFastFarm(df.columns.values, nWT=nWT, nD=nD)
- dfRad = fastlib.extract_spanwise_data(ColsInfo, nrMax, df=None, ts=dfAvg.iloc[0])
- #dfRad = fastlib.insert_radial_columns(dfRad, vr)
- if vr is None:
- dfRad.insert(0, 'i_[#]', np.arange(nrMax)+1)
- else:
- dfRad.insert(0, 'r_[m]', vr[:nrMax])
- dfRad['i/n_[-]']=np.arange(nrMax)/nrMax
-
- # --- Extract downstream data
- ColsInfo, nDMax = diameterwiseColFastFarm(df.columns.values, nWT=nWT)
- dfDiam = fastlib.extract_spanwise_data(ColsInfo, nDMax, df=None, ts=dfAvg.iloc[0])
- #dfDiam = fastlib.insert_radial_columns(dfDiam)
- if vD is None:
- dfDiam.insert(0, 'i_[#]', np.arange(nDMax)+1)
- else:
- dfDiam.insert(0, 'x_[m]', vD[:nDMax])
- dfDiam['i/n_[-]'] = np.arange(nDMax)/nDMax
- return dfRad, dfRadialTime, dfDiam
-
+import os
+import glob
+import numpy as np
+import pandas as pd
+from weio.weio.fast_input_file import FASTInputFile
+from weio.weio.fast_output_file import FASTOutputFile
+from weio.weio.turbsim_file import TurbSimFile
+
+from . import fastlib
+
+# --------------------------------------------------------------------------------}
+# --- Small helper functions
+# --------------------------------------------------------------------------------{
+def insertTN(s,i,nWT=1000):
+ """ insert turbine number in name """
+ if nWT<10:
+ fmt='{:d}'
+ elif nWT<100:
+ fmt='{:02d}'
+ else:
+ fmt='{:03d}'
+ if s.find('T1')>=0:
+ s=s.replace('T1','T'+fmt.format(i))
+ else:
+ sp=os.path.splitext(s)
+ s=sp[0]+'_T'+fmt.format(i)+sp[1]
+ return s
+def forceCopyFile (sfile, dfile):
+ # ---- Handling error due to wrong mod
+ if os.path.isfile(dfile):
+ if not os.access(dfile, os.W_OK):
+ os.chmod(dfile, stat.S_IWUSR)
+ #print(sfile, ' > ', dfile)
+ shutil.copy2(sfile, dfile)
+
+# --------------------------------------------------------------------------------}
+# --- Tools to create fast farm simulations
+# --------------------------------------------------------------------------------{
+def writeFSTandDLL(FstT1Name, nWT):
+ """
+ Write FST files for each turbine, with different ServoDyn files and DLL
+ FST files, ServoFiles, and DLL files will be written next to their turbine 1
+ files, with name Ti.
+
+ FstT1Name: absolute or relative path to the Turbine FST file
+ """
+
+ FstT1Full = os.path.abspath(FstT1Name).replace('\\','/')
+ FstDir = os.path.dirname(FstT1Full)
+
+ fst=FASTInputFile(FstT1Name)
+ SrvT1Name = fst['ServoFile'].strip('"')
+ SrvT1Full = os.path.join(FstDir, SrvT1Name).replace('\\','/')
+ SrvDir = os.path.dirname(SrvT1Full)
+ SrvT1RelFst = os.path.relpath(SrvT1Full,FstDir)
+ if os.path.exists(SrvT1Full):
+ srv=FASTInputFile(SrvT1Full)
+ DLLT1Name = srv['DLL_FileName'].strip('"')
+ DLLT1Full = os.path.join(SrvDir, DLLT1Name)
+ if os.path.exists(DLLT1Full):
+ servo=True
+ else:
+ print('[Info] DLL file not found, not copying servo and dll files ({})'.format(DLLT1Full))
+ servo=False
+ else:
+ print('[Info] ServoDyn file not found, not copying servo and dll files ({})'.format(SrvT1Full))
+ servo=False
+
+ #print(FstDir)
+ #print(FstT1Full)
+ #print(SrvT1Name)
+ #print(SrvT1Full)
+ #print(SrvT1RelFst)
+
+ for i in np.arange(2,nWT+1):
+ FstName = insertTN(FstT1Name,i,nWT)
+ if servo:
+ # TODO handle the case where T1 not present
+ SrvName = insertTN(SrvT1Name,i,nWT)
+ DLLName = insertTN(DLLT1Name,i,nWT)
+ DLLFullName = os.path.join(SrvDir, DLLName)
+
+ print('')
+ print('FstName: ',FstName)
+ if servo:
+ print('SrvName: ',SrvName)
+ print('DLLName: ',DLLName)
+ print('DLLFull: ',DLLFullName)
+
+ # Changing main file
+ if servo:
+ fst['ServoFile']='"'+SrvName+'"'
+ fst.write(FstName)
+ if servo:
+ # Changing servo file
+ srv['DLL_FileName']='"'+DLLName+'"'
+ srv.write(SrvName)
+ # Copying dll
+ forceCopyFile(DLLT1Full, DLLFullName)
+
+
+
+def rectangularLayoutSubDomains(D,Lx,Ly):
+ """ Retuns position of turbines in a rectangular layout
+ TODO, unfinished function parameters
+ """
+ # --- Parameters
+ D = 112 # turbine diameter [m]
+ Lx = 3840 # x dimension of precusor
+ Ly = 3840 # y dimension of precusor
+ Height = 0 # Height above ground, likely 0 [m]
+ nDomains_x = 2 # number of domains in x
+ nDomains_y = 2 # number of domains in y
+ # --- 36 WT
+ nx = 3 # number of turbines to be placed along x in one precursor domain
+ ny = 3 # number of turbines to be placed along y in one precursor domain
+ StartX = 1/2 # How close do we start from the x boundary
+ StartY = 1/2 # How close do we start from the y boundary
+ # --- Derived parameters
+ Lx_Domain = Lx * nDomains_x # Full domain size
+ Ly_Domain = Ly * nDomains_y
+ DeltaX = Lx / (nx) # Turbine spacing
+ DeltaY = Ly / (ny)
+ xWT = np.arange(DeltaX*StartX,Lx_Domain,DeltaX) # Turbine positions
+ yWT = np.arange(DeltaY*StartY,Ly_Domain,DeltaY)
+
+ print('Full domain size [D] : {:.2f} x {:.2f} '.format(Lx_Domain/D, Ly_Domain/D))
+ print('Turbine spacing [D] : {:.2f} x {:.2f} '.format(DeltaX/D,DeltaX/D))
+ print('Number of turbines : {:d} x {:d} = {:d}'.format(len(xWT),len(yWT),len(xWT)*len(yWT)))
+
+ XWT,YWT=np.meshgrid(xWT,yWT)
+ ZWT=XWT*0+Height
+
+ # --- Export coordinates only
+ M=np.column_stack((XWT.ravel(),YWT.ravel(),ZWT.ravel()))
+ np.savetxt('Farm_Coordinates.csv', M, delimiter=',',header='X_[m], Y_[m], Z_[m]')
+ print(M)
+
+ return XWT, YWT, ZWT
+
+def fastFarmTurbSimExtent(TurbSimFilename, HubHeight, D, xWT, yWT, Cmeander=1.9, Chord_max=3, extent_X=1.2, extent_Y=1.2):
+ """
+ Determines "Ambient Wind" box parametesr for FastFarm, based on a TurbSimFile ('bts')
+ """
+ # --- TurbSim data
+ ts = TurbSimFile(TurbSimFilename)
+ #iy,iz = ts.closestPoint(y=0,z=HubHeight)
+ #iy,iz = ts.closestPoint(y=0,z=HubHeight)
+ zMid, uMid = ts.midValues()
+ #print('uMid',uMid)
+ #meanU = ts['u'][0,:,iy,iz].mean()
+ meanU = uMid
+ dY_High = ts['y'][1]-ts['y'][0]
+ dZ_High = ts['z'][1]-ts['z'][0]
+ Z0_Low = ts['z'][0]
+ Z0_High = ts['z'][0] # we start at lowest to include tower
+ Width = ts['y'][-1]-ts['y'][0]
+ Height = ts['z'][-1]-ts['z'][0]
+ dT_High = ts['dt']
+ #effSimLength = ts['t'][-1]-ts['t'][0] + Width/meanU
+ effSimLength = ts['t'][-1]-ts['t'][0]
+
+ # Desired resolution, rule of thumbs
+ dX_High_desired = Chord_max
+ dX_Low_desired = Cmeander*D*meanU/150.0
+ dt_des = Cmeander*D/(10.0*meanU)
+
+ # --- High domain
+ ZMax_High = HubHeight+extent_Y*D/2.0
+ # high-box extent in x and y [D]
+ Xdist_High = extent_X*D
+ Ydist_High = extent_Y*D
+ Zdist_High = ZMax_High-Z0_High # we include the tower
+ X0_rel = Xdist_High/2.0
+ Y0_rel = Ydist_High/2.0
+ Length = effSimLength*meanU
+ nx = int(round(effSimLength/dT_High))
+ dx_TS = Length/(nx-1)
+ #print('dx_TS',dx_TS)
+ dX_High = round(dX_High_desired/dx_TS)*dx_TS
+ #print('dX_High_desired',dX_High_desired, dX_High)
+
+ nX_High = int(round(Xdist_High/dX_High)+1)
+ nY_High = int(round(Ydist_High/dY_High)+1)
+ nZ_High = int(round(Zdist_High/dZ_High)+1)
+
+ # --- High extent per turbine
+ nTurbs = len(xWT)
+ X0_des = np.asarray(xWT)-X0_rel
+ Y0_des = np.asarray(yWT)-Y0_rel
+ X0_High = np.around(np.round(X0_des/dX_High)*dX_High,3)
+ Y0_High = np.around(np.round(Y0_des/dY_High)*dY_High,3)
+
+ # --- Low domain
+ dT_Low = round(dt_des/dT_High)*dT_High
+ dx_des = dX_Low_desired
+ dy_des = dX_Low_desired
+ dz_des = dX_Low_desired
+ X0_Low = round( (min(xWT)-2*D)/dX_High) *dX_High
+ Y0_Low = round( -Width/2 /dY_High) *dY_High
+ dX_Low = round( dx_des /dX_High)*dX_High
+ dY_Low = round( dy_des /dY_High)*dY_High
+ dZ_Low = round( dz_des /dZ_High)*dZ_High
+ Xdist = max(xWT)+8.0*D-X0_Low # Maximum extent
+ Ydist = Width
+ Zdist = Height
+ #print('dX_Low',dX_Low, dX_Low/dx_TS, dX_High/dx_TS)
+
+ nX_Low = int(Xdist/dX_Low)+1;
+ nY_Low = int(Ydist/dY_Low)+1;
+ nZ_Low = int(Zdist/dZ_Low)+1;
+
+ if (nX_Low*dX_Low>Xdist):
+ nX_Low=nX_Low-1
+ if (nY_Low*dY_Low>Ydist):
+ nY_Low=nY_Low-1
+ if (nZ_Low*dZ_Low>Zdist):
+ nZ_Low=nZ_Low-1
+
+ d = dict()
+ d['DT'] = np.around(dT_Low ,3)
+ d['DT_High'] = np.around(dT_High,3)
+ d['NX_Low'] = int(nX_Low)
+ d['NY_Low'] = int(nY_Low)
+ d['NZ_Low'] = int(nZ_Low)
+ d['X0_Low'] = np.around(X0_Low,3)
+ d['Y0_Low'] = np.around(Y0_Low,3)
+ d['Z0_Low'] = np.around(Z0_Low,3)
+ d['dX_Low'] = np.around(dX_Low,3)
+ d['dY_Low'] = np.around(dY_Low,3)
+ d['dZ_Low'] = np.around(dZ_Low,3)
+ d['NX_High'] = int(nX_High)
+ d['NY_High'] = int(nY_High)
+ d['NZ_High'] = int(nZ_High)
+ # --- High extent info for turbine outputs
+ d['dX_High'] = np.around(dX_High,3)
+ d['dY_High'] = np.around(dY_High,3)
+ d['dZ_High'] = np.around(dZ_High,3)
+ d['X0_High'] = X0_High
+ d['Y0_High'] = Y0_High
+ d['Z0_High'] = np.around(Z0_High,3)
+
+ return d
+
+def writeFastFarm(outputFile, templateFile, xWT, yWT, zWT, FFTS=None, OutListT1=None):
+ """ Write FastFarm input file based on a template, a TurbSimFile and the Layout
+
+ outputFile: .fstf file to be written
+ templateFile: .fstf file that will be used to generate the output_file
+ XWT,YWT,ZWT: positions of turbines
+ FFTS: FastFarm TurbSim parameters as returned by fastFarmTurbSimExtent
+ """
+ # --- Read template fast farm file
+ fst=FASTInputFile(templateFile)
+ # --- Replace box extent values
+ if FFTS is not None:
+ fst['Mod_AmbWind'] = 2
+ for k in ['DT', 'DT_High', 'NX_Low', 'NY_Low', 'NZ_Low', 'X0_Low', 'Y0_Low', 'Z0_Low', 'dX_Low', 'dY_Low', 'dZ_Low', 'NX_High', 'NY_High', 'NZ_High']:
+ if isinstance(FFTS[k],int):
+ fst[k] = FFTS[k]
+ else:
+ fst[k] = np.around(FFTS[k],3)
+ fst['WrDisDT'] = FFTS['DT']
+
+ # --- Set turbine names, position, and box extent
+ nWT = len(xWT)
+ fst['NumTurbines'] = nWT
+ if FFTS is not None:
+ nCol= 10
+ else:
+ nCol = 4
+ ref_path = fst['WindTurbines'][0,3]
+ WT = np.array(['']*nWT*nCol,dtype='object').reshape((nWT,nCol))
+ for iWT,(x,y,z) in enumerate(zip(xWT,yWT,zWT)):
+ WT[iWT,0]=x
+ WT[iWT,1]=y
+ WT[iWT,2]=z
+ WT[iWT,3]=insertTN(ref_path,iWT+1,nWT)
+ if FFTS is not None:
+ WT[iWT,4]=FFTS['X0_High'][iWT]
+ WT[iWT,5]=FFTS['Y0_High'][iWT]
+ WT[iWT,6]=FFTS['Z0_High']
+ WT[iWT,7]=FFTS['dX_High']
+ WT[iWT,8]=FFTS['dY_High']
+ WT[iWT,9]=FFTS['dZ_High']
+ fst['WindTurbines']=WT
+
+ fst.write(outputFile)
+ if OutListT1 is not None:
+ setFastFarmOutputs(outputFile, OutListT1)
+
+def setFastFarmOutputs(fastFarmFile, OutListT1):
+ """ Duplicate the output list, by replacing "T1" with T1->Tn """
+ fst = FASTInputFile(fastFarmFile)
+ nWTOut = min(fst['NumTurbines'],9) # Limited to 9 turbines
+ OutList=['']
+ for s in OutListT1:
+ s=s.strip('"')
+ if s.find('T1'):
+ OutList+=['"'+s.replace('T1','T{:d}'.format(iWT+1))+'"' for iWT in np.arange(nWTOut) ]
+ else:
+ OutList+='"'+s+'"'
+ fst['OutList']=OutList
+ fst.write(fastFarmFile)
+
+
+def plotFastFarmSetup(fastFarmFile):
+ """ """
+ import matplotlib.pyplot as plt
+ fst=FASTInputFile(fastFarmFile)
+
+ fig = plt.figure(figsize=(13.5,10))
+ ax = fig.add_subplot(111,aspect="equal")
+
+ WT=fst['WindTurbines']
+ x = WT[:,0].astype(float)
+ y = WT[:,1].astype(float)
+
+ if fst['Mod_AmbWind'] == 2:
+ xmax_low = fst['X0_Low']+fst['DX_Low']*fst['NX_Low']
+ ymax_low = fst['Y0_Low']+fst['DY_Low']*fst['NY_Low']
+ # low-res box
+ ax.plot([fst['X0_Low'],xmax_low,xmax_low,fst['X0_Low'],fst['X0_Low']],
+ [fst['Y0_Low'],fst['Y0_Low'],ymax_low,ymax_low,fst['Y0_Low']],'--k',lw=2,label='Low')
+ X0_High = WT[:,4].astype(float)
+ Y0_High = WT[:,5].astype(float)
+ dX_High = WT[:,7].astype(float)[0]
+ dY_High = WT[:,8].astype(float)[0]
+ nX_High = fst['NX_High']
+ nY_High = fst['NY_High']
+ # high-res boxes
+ for wt in range(len(x)):
+ xmax_high = X0_High[wt]+dX_High*nX_High
+ ymax_high = Y0_High[wt]+dY_High*nY_High
+ ax.plot([X0_High[wt],xmax_high,xmax_high,X0_High[wt],X0_High[wt]],
+ [Y0_High[wt],Y0_High[wt],ymax_high,ymax_high,Y0_High[wt]],
+ '-',
+ label="HighT{0}".format(wt+1))
+ ax.plot(x[wt],y[wt],'x',ms=8,mew=2,label="WT{0}".format(wt+1))
+ else:
+ for wt in range(len(x)):
+ ax.plot(x[wt],y[wt],'x',ms=8,mew=2,label="WT{0}".format(wt+1))
+ #
+ plt.legend(bbox_to_anchor=(1.05,1.015),frameon=False)
+ ax.set_xlabel("x-location [m]")
+ ax.set_ylabel("y-location [m]")
+ fig.tight_layout
+ # fig.savefig('FFarmLayout.pdf',bbox_to_inches='tight',dpi=500)
+
+# --------------------------------------------------------------------------------}
+# --- Tools for postpro
+# --------------------------------------------------------------------------------{
+
+def spanwiseColFastFarm(Cols, nWT=9, nD=9):
+ """ Return column info, available columns and indices that contain AD spanwise data"""
+ FFSpanMap=dict()
+ for i in np.arange(nWT):
+ FFSpanMap['^CtT{:d}N(\d*)_\[-\]'.format(i+1)]='CtT{:d}_[-]'.format(i+1)
+ for i in np.arange(nWT):
+ for k in np.arange(nD):
+ FFSpanMap['^WkDfVxT{:d}N(\d*)D{:d}_\[m/s\]'.format(i+1,k+1) ]='WkDfVxT{:d}D{:d}_[m/s]'.format(i+1, k+1)
+ for i in np.arange(nWT):
+ for k in np.arange(nD):
+ FFSpanMap['^WkDfVrT{:d}N(\d*)D{:d}_\[m/s\]'.format(i+1,k+1) ]='WkDfVrT{:d}D{:d}_[m/s]'.format(i+1, k+1)
+
+ return fastlib.find_matching_columns(Cols, FFSpanMap)
+
+def diameterwiseColFastFarm(Cols, nWT=9):
+ """ Return column info, available columns and indices that contain AD spanwise data"""
+ FFDiamMap=dict()
+ for i in np.arange(nWT):
+ for x in ['X','Y','Z']:
+ FFDiamMap['^WkAxs{}T{:d}D(\d*)_\[-\]'.format(x,i+1)] ='WkAxs{}T{:d}_[-]'.format(x,i+1)
+ for i in np.arange(nWT):
+ for x in ['X','Y','Z']:
+ FFDiamMap['^WkPos{}T{:d}D(\d*)_\[m\]'.format(x,i+1)] ='WkPos{}T{:d}_[m]'.format(x,i+1)
+ for i in np.arange(nWT):
+ for x in ['X','Y','Z']:
+ FFDiamMap['^WkVel{}T{:d}D(\d*)_\[m/s\]'.format(x,i+1)] ='WkVel{}T{:d}_[m/s]'.format(x,i+1)
+ for i in np.arange(nWT):
+ for x in ['X','Y','Z']:
+ FFDiamMap['^WkDiam{}T{:d}D(\d*)_\[m\]'.format(x,i+1)] ='WkDiam{}T{:d}_[m]'.format(x,i+1)
+ return fastlib.find_matching_columns(Cols, FFDiamMap)
+
+def SensorsFARMRadial(nWT=3,nD=10,nR=30,signals=None):
+ """ Returns a list of FASTFarm sensors that are used for the radial distribution
+ of quantities (e.g. Ct, Wake Deficits).
+ If `signals` is provided, the output is the list of sensors within the list `signals`.
+ """
+ WT = np.arange(nWT)
+ r = np.arange(nR)
+ D = np.arange(nD)
+ sens=[]
+ sens+=['CtT{:d}N{:02d}_[-]'.format(i+1,j+1) for i in WT for j in r]
+ sens+=['WkDfVxT{:d}N{:02d}D{:d}_[m/s]'.format(i+1,j+1,k+1) for i in WT for j in r for k in D]
+ sens+=['WkDfVrT{:d}N{:02d}D{:d}_[m/s]'.format(i+1,j+1,k+1) for i in WT for j in r for k in D]
+ if signals is not None:
+ sens = [c for c in sens if c in signals]
+ return sens
+
+def SensorsFARMDiam(nWT,nD):
+ """ Returns a list of FASTFarm sensors that contain quantities at different downstream diameters
+ (e.g. WkAxs, WkPos, WkVel, WkDiam)
+ If `signals` is provided, the output is the list of sensors within the list `signals`.
+ """
+ WT = np.arange(nWT)
+ D = np.arange(nD)
+ XYZ = ['X','Y','Z']
+ sens=[]
+ sens+=['WkAxs{}T{:d}D{:d}_[-]'.format(x,i+1,j+1) for x in XYZ for i in WT for j in D]
+ sens+=['WkPos{}T{:d}D{:d}_[m]'.format(x,i+1,j+1) for x in XYZ for i in WT for j in D]
+ sens+=['WkVel{}T{:d}D{:d}_[m/s]'.format(x,i+1,j+1) for x in XYZ for i in WT for j in D]
+ sens+=['WkDiam{}T{:d}D{:d}_[m]'.format(x,i+1,j+1) for x in XYZ for i in WT for j in D]
+ if signals is not None:
+ sens = [c for c in sens if c in signals]
+ return sens
+
+
+def extractFFRadialData(fastfarm_out,fastfarm_input,avgMethod='constantwindow',avgParam=30,D=1,df=None):
+ # LEGACY
+ return spanwisePostProFF(fastfarm_input,avgMethod=avgMethod,avgParam=avgParam,D=D,df=df,fastfarm_out=fastfarm_out)
+
+
+def spanwisePostProFF(fastfarm_input,avgMethod='constantwindow',avgParam=30,D=1,df=None,fastfarm_out=None):
+ """
+ Opens a FASTFarm output file, extract the radial data, average them and returns spanwise data
+
+ D: diameter TODO, extract it from the main file
+
+ See faslibt.averageDF for `avgMethod` and `avgParam`.
+ """
+ # --- Opening ouputfile
+ if df is None:
+ df=FASTOutputFile(fastfarm_out).toDataFrame()
+
+ # --- Opening input file and extracting inportant variables
+ if fastfarm_input is None:
+ # We don't have an input file, guess numbers of turbine, diameters, Nodes...
+ cols, sIdx = fastlib.find_matching_pattern(df.columns.values, 'T(\d+)')
+ nWT = np.array(sIdx).astype(int).max()
+ cols, sIdx = fastlib.find_matching_pattern(df.columns.values, 'D(\d+)')
+ nD = np.array(sIdx).astype(int).max()
+ cols, sIdx = fastlib.find_matching_pattern(df.columns.values, 'N(\d+)')
+ nr = np.array(sIdx).astype(int).max()
+ vr=None
+ vD=None
+ D=0
+ else:
+ main=FASTInputFile(fastfarm_input)
+ iOut = main['OutRadii']
+ dr = main['dr'] # Radial increment of radial finite-difference grid (m)
+ OutDist = main['OutDist'] # List of downstream distances for wake output for an individual rotor
+ WT = main['WindTurbines']
+ nWT = len(WT)
+ vr = dr*np.array(iOut)
+ vD = np.array(OutDist)
+ nr=len(iOut)
+ nD=len(vD)
+
+
+ # --- Extracting time series of radial data only
+ colRadial = SensorsFARMRadial(nWT=nWT,nD=nD,nR=nr,signals=df.columns.values)
+ colRadial=['Time_[s]']+colRadial
+ dfRadialTime = df[colRadial] # TODO try to do some magic with it, display it with a slider
+
+ # --- Averaging data
+ dfAvg = fastlib.averageDF(df,avgMethod=avgMethod,avgParam=avgParam)
+
+ # --- Extract radial data
+ ColsInfo, nrMax = spanwiseColFastFarm(df.columns.values, nWT=nWT, nD=nD)
+ dfRad = fastlib.extract_spanwise_data(ColsInfo, nrMax, df=None, ts=dfAvg.iloc[0])
+ #dfRad = fastlib.insert_radial_columns(dfRad, vr)
+ if vr is None:
+ dfRad.insert(0, 'i_[#]', np.arange(nrMax)+1)
+ else:
+ dfRad.insert(0, 'r_[m]', vr[:nrMax])
+ dfRad['i/n_[-]']=np.arange(nrMax)/nrMax
+
+ # --- Extract downstream data
+ ColsInfo, nDMax = diameterwiseColFastFarm(df.columns.values, nWT=nWT)
+ dfDiam = fastlib.extract_spanwise_data(ColsInfo, nDMax, df=None, ts=dfAvg.iloc[0])
+ #dfDiam = fastlib.insert_radial_columns(dfDiam)
+ if vD is None:
+ dfDiam.insert(0, 'i_[#]', np.arange(nDMax)+1)
+ else:
+ dfDiam.insert(0, 'x_[m]', vD[:nDMax])
+ dfDiam['i/n_[-]'] = np.arange(nDMax)/nDMax
+ return dfRad, dfRadialTime, dfDiam
+
diff --git a/pydatview/fast/postpro.py b/pydatview/fast/postpro.py
index b596633..8555d11 100644
--- a/pydatview/fast/postpro.py
+++ b/pydatview/fast/postpro.py
@@ -1,1183 +1,1449 @@
-# --- For cmd.py
-from __future__ import division, print_function
-import os
-import pandas as pd
-import numpy as np
-import re
-
-# --- fast libraries
-from weio.weio.fast_input_file import FASTInputFile
-from weio.weio.fast_output_file import FASTOutputFile
-from weio.weio.fast_input_deck import FASTInputDeck
-# from pyFAST.input_output.fast_input_file import FASTInputFile
-# from pyFAST.input_output.fast_output_file import FASTOutputFile
-# from pyFAST.input_output.fast_input_deck import FASTInputDeck
-
-# --------------------------------------------------------------------------------}
-# --- Tools for IO
-# --------------------------------------------------------------------------------{
-def ED_BldStations(ED):
- """ Returns ElastoDyn Blade Station positions, useful to know where the outputs are.
- INPUTS:
- - ED: either:
- - a filename of a ElastoDyn input file
- - an instance of FileCl, as returned by reading the file, ED = weio.read(ED_filename)
-
- OUTUPTS:
- - bld_fract: fraction of the blade length were stations are defined
- - r_nodes: spanwise position from the rotor apex of the Blade stations
- """
- if hasattr(ED,'startswith'): # if string
- ED = FASTInputFile(ED)
-
- nBldNodes = ED['BldNodes']
- bld_fract = np.arange(1./nBldNodes/2., 1, 1./nBldNodes)
- r_nodes = bld_fract*(ED['TipRad']-ED['HubRad']) + ED['HubRad']
- return bld_fract, r_nodes
-
-def ED_TwrStations(ED):
- """ Returns ElastoDyn Tower Station positions, useful to know where the outputs are.
- INPUTS:
- - ED: either:
- - a filename of a ElastoDyn input file
- - an instance of FileCl, as returned by reading the file, ED = weio.read(ED_filename)
-
- OUTPUTS:
- - r_fract: fraction of the towet length were stations are defined
- - h_nodes: height from the *ground* of the stations (not from the Tower base)
- """
- if hasattr(ED,'startswith'): # if string
- ED = FASTInputFile(ED)
-
- nTwrNodes = ED['TwrNodes']
- twr_fract = np.arange(1./nTwrNodes/2., 1, 1./nTwrNodes)
- h_nodes = twr_fract*(ED['TowerHt']-ED['TowerBsHt']) + ED['TowerBsHt']
- return twr_fract, h_nodes
-
-def ED_BldGag(ED):
- """ Returns the radial position of ElastoDyn blade gages
- INPUTS:
- - ED: either:
- - a filename of a ElastoDyn input file
- - an instance of FileCl, as returned by reading the file, ED = weio.read(ED_filename)
- OUTPUTS:
- - r_gag: The radial positions of the gages, given from the rotor apex
- """
- if hasattr(ED,'startswith'): # if string
- ED = FASTInputFile(ED)
- _,r_nodes= ED_BldStations(ED)
-
- # if ED.hasNodal:
- # return r_nodes, None
- nOuts = ED['NBlGages']
- if nOuts<=0:
- return np.array([]), np.array([])
- if type(ED['BldGagNd']) is list:
- Inodes = np.asarray(ED['BldGagNd'])
- else:
- Inodes = np.array([ED['BldGagNd']])
- r_gag = r_nodes[ Inodes[:nOuts] -1]
- return r_gag, Inodes
-
-def ED_TwrGag(ED):
- """ Returns the heights of ElastoDyn blade gages
- INPUTS:
- - ED: either:
- - a filename of a ElastoDyn input file
- - an instance of FileCl, as returned by reading the file, ED = weio.read(ED_filename)
- OUTPUTS:
- - h_gag: The heights of the gages, given from the ground height (tower base + TowerBsHt)
- """
- if hasattr(ED,'startswith'): # if string
- ED = FASTInputFile(ED)
- _,h_nodes= ED_TwrStations(ED)
- nOuts = ED['NTwGages']
- if nOuts<=0:
- return np.array([])
- if type(ED['TwrGagNd']) is list:
- Inodes = np.asarray(ED['TwrGagNd'])
- else:
- Inodes = np.array([ED['TwrGagNd']])
- h_gag = h_nodes[ Inodes[:nOuts] -1]
- return h_gag
-
-
-def AD14_BldGag(AD):
- """ Returns the radial position of AeroDyn 14 blade gages (based on "print" in column 6)
- INPUTS:
- - AD: either:
- - a filename of a AeroDyn input file
- - an instance of FileCl, as returned by reading the file, AD = weio.read(AD_filename)
- OUTPUTS:
- - r_gag: The radial positions of the gages, given from the blade root
- """
- if hasattr(ED,'startswith'): # if string
- AD = FASTInputFile(AD)
-
- Nodes=AD['BldAeroNodes']
- if Nodes.shape[1]==6:
- doPrint= np.array([ n.lower().find('p')==0 for n in Nodes[:,5]])
- else:
- doPrint=np.array([ True for n in Nodes[:,0]])
-
- r_gag = Nodes[doPrint,0].astype(float)
- IR = np.arange(1,len(Nodes)+1)[doPrint]
- return r_gag, IR
-
-def AD_BldGag(AD,AD_bld,chordOut=False):
- """ Returns the radial position of AeroDyn blade gages
- INPUTS:
- - AD: either:
- - a filename of a AeroDyn input file
- - an instance of FileCl, as returned by reading the file, AD = weio.read(AD_filename)
- - AD_bld: either:
- - a filename of a AeroDyn Blade input file
- - an instance of FileCl, as returned by reading the file, AD_bld = weio.read(AD_bld_filename)
- OUTPUTS:
- - r_gag: The radial positions of the gages, given from the blade root
- """
- if hasattr(AD,'startswith'): # if string
- AD = FASTInputFile(AD)
- if hasattr(AD_bld,'startswith'): # if string
- AD_bld = FASTInputFile(AD_bld)
- #print(AD_bld.keys())
-
- nOuts=AD['NBlOuts']
- if nOuts<=0:
- if chordOut:
- return np.array([]), np.array([])
- else:
- return np.array([])
- INodes = np.array(AD['BlOutNd'][:nOuts])
- r_gag = AD_bld['BldAeroNodes'][INodes-1,0]
- if chordOut:
- chord_gag = AD_bld['BldAeroNodes'][INodes-1,5]
- return r_gag,chord_gag
- else:
- return r_gag
-
-def BD_BldGag(BD):
- """ Returns the radial position of BeamDyn blade gages
- INPUTS:
- - BD: either:
- - a filename of a BeamDyn input file
- - an instance of FileCl, as returned by reading the file, BD = weio.read(BD_filename)
- OUTPUTS:
- - r_gag: The radial positions of the gages, given from the rotor apex
- """
- if hasattr(BD,'startswith'): # if string
- BD = FASTInputFile(BD)
-
- M = BD['MemberGeom']
- r_nodes = M[:,2] # NOTE: we select the z axis here, and we don't take curvilenear coord
- nOuts = BD['NNodeOuts']
- if nOuts<=0:
- nOuts=0
- if type(BD['OutNd']) is list:
- Inodes = np.asarray(BD['OutNd'])
- else:
- Inodes = np.array([BD['OutNd']])
- r_gag = r_nodes[ Inodes[:nOuts] -1]
- return r_gag, Inodes, r_nodes
-
-#
-#
-# 1, 7, 14, 21, 30, 36, 43, 52, 58 BldGagNd List of blade nodes that have strain gages [1 to BldNodes] (-) [unused if NBlGages=0]
-
-# --------------------------------------------------------------------------------}
-# --- Helper functions for radial data
-# --------------------------------------------------------------------------------{
-def _HarmonizeSpanwiseData(Name, Columns, vr, R, IR=None) :
- """ helper function to use with spanwiseAD and spanwiseED """
- # --- Data present
- data = [c for _,c in Columns if c is not None]
- ColNames = [n for n,_ in Columns if n is not None]
- Lengths = [len(d) for d in data]
- if len(data)<=0:
- print('[WARN] No spanwise data for '+Name)
- return None, None, None
-
- # --- Harmonize data so that they all have the same length
- nrMax = np.max(Lengths)
- ids=np.arange(nrMax)
- if vr is None:
- bFakeVr=True
- vr_bar = ids/(nrMax-1)
- else:
- vr_bar=vr/R
- bFakeVr=False
- if (nrMax)len(vr_bar):
- raise Exception('Inconsitent length between radial stations and max index present in output chanels')
-
- for i in np.arange(len(data)):
- d=data[i]
- if len(d)len(vr_bar):
- print(vr_bar)
- raise Exception('Inconsitent length between radial stations ({:d}) and max index present in output chanels ({:d})'.format(len(vr_bar),nrMax))
- df.insert(0, 'r/R_[-]', vr_bar)
-
- if IR is not None:
- df['Node_[#]']=IR[:nrMax]
- df['i_[#]']=ids+1
- if vr is not None:
- df['r_[m]'] = vr[:nrMax]
- return df
-
-def find_matching_columns(Cols, PatternMap):
- ColsInfo=[]
- nrMax=0
- for colpattern,colmap in PatternMap.items():
- # Extracting columns matching pattern
- cols, sIdx = find_matching_pattern(Cols, colpattern)
- if len(cols)>0:
- # Sorting by ID
- cols = np.asarray(cols)
- Idx = np.array([int(s) for s in sIdx])
- Isort = np.argsort(Idx)
- Idx = Idx[Isort]
- cols = cols[Isort]
- col={'name':colmap,'Idx':Idx,'cols':cols}
- nrMax=max(nrMax,np.max(Idx))
- ColsInfo.append(col)
- return ColsInfo,nrMax
-
-def extract_spanwise_data(ColsInfo, nrMax, df=None,ts=None):
- """
- Extract spanwise data based on some column info
- ColsInfo: see find_matching_columns
- """
- nCols = len(ColsInfo)
- if nCols==0:
- return None
- if ts is not None:
- Values = np.zeros((nrMax,nCols))
- Values[:] = np.nan
- elif df is not None:
- raise NotImplementedError()
-
- ColNames =[c['name'] for c in ColsInfo]
-
- for ic,c in enumerate(ColsInfo):
- Idx, cols, colname = c['Idx'], c['cols'], c['name']
- for idx,col in zip(Idx,cols):
- Values[idx-1,ic]=ts[col]
- nMissing = np.sum(np.isnan(Values[:,ic]))
- if len(cols)nrMax:
- print('[WARN] More values found for {}, found {}/{}'.format(colname,len(cols),nrMax))
- df = pd.DataFrame(data=Values, columns=ColNames)
- df = df.reindex(sorted(df.columns), axis=1)
- return df
-
-def spanwiseColBD(Cols):
- """ Return column info, available columns and indices that contain BD spanwise data"""
- BDSpanMap=dict()
- for sB in ['B1','B2','B3']:
- BDSpanMap['^'+sB+r'N(\d)TDxr_\[m\]']=sB+'TDxr_[m]'
- BDSpanMap['^'+sB+r'N(\d)TDyr_\[m\]']=sB+'TDyr_[m]'
- BDSpanMap['^'+sB+r'N(\d)TDzr_\[m\]']=sB+'TDzr_[m]'
- return find_matching_columns(Cols, BDSpanMap)
-
-def spanwiseColED(Cols):
- """ Return column info, available columns and indices that contain ED spanwise data"""
- EDSpanMap=dict()
- # All Outs
- for sB in ['B1','B2','B3']:
- EDSpanMap['^[A]*'+sB+r'N(\d*)ALx_\[m/s^2\]' ] = sB+'ALx_[m/s^2]'
- EDSpanMap['^[A]*'+sB+r'N(\d*)ALy_\[m/s^2\]' ] = sB+'ALy_[m/s^2]'
- EDSpanMap['^[A]*'+sB+r'N(\d*)ALz_\[m/s^2\]' ] = sB+'ALz_[m/s^2]'
- EDSpanMap['^[A]*'+sB+r'N(\d*)TDx_\[m\]' ] = sB+'TDx_[m]'
- EDSpanMap['^[A]*'+sB+r'N(\d*)TDy_\[m\]' ] = sB+'TDy_[m]'
- EDSpanMap['^[A]*'+sB+r'N(\d*)TDz_\[m\]' ] = sB+'TDz_[m]'
- EDSpanMap['^[A]*'+sB+r'N(\d*)RDx_\[deg\]' ] = sB+'RDx_[deg]'
- EDSpanMap['^[A]*'+sB+r'N(\d*)RDy_\[deg\]' ] = sB+'RDy_[deg]'
- EDSpanMap['^[A]*'+sB+r'N(\d*)RDz_\[deg\]' ] = sB+'RDz_[deg]'
- EDSpanMap['^[A]*'+sB+r'N(\d*)MLx_\[kN-m\]' ] = sB+'MLx_[kN-m]'
- EDSpanMap['^[A]*'+sB+r'N(\d*)MLy_\[kN-m\]' ] = sB+'MLy_[kN-m]'
- EDSpanMap['^[A]*'+sB+r'N(\d*)MLz_\[kN-m\]' ] = sB+'MLz_[kN-m]'
- EDSpanMap['^[A]*'+sB+r'N(\d*)FLx_\[kN\]' ] = sB+'FLx_[kN]'
- EDSpanMap['^[A]*'+sB+r'N(\d*)FLy_\[kN\]' ] = sB+'FLy_[kN]'
- EDSpanMap['^[A]*'+sB+r'N(\d*)FLz_\[kN\]' ] = sB+'FLz_[kN]'
- EDSpanMap['^[A]*'+sB+r'N(\d*)FLxNT_\[kN\]' ] = sB+'FLxNT_[kN]'
- EDSpanMap['^[A]*'+sB+r'N(\d*)FLyNT_\[kN\]' ] = sB+'FLyNT_[kN]'
- EDSpanMap['^[A]*'+sB+r'N(\d*)FlyNT_\[kN\]' ] = sB+'FLyNT_[kN]' # <<< Unfortunate
- EDSpanMap['^[A]*'+sB+r'N(\d*)MLxNT_\[kN-m\]'] = sB+'MLxNT_[kN-m]'
- EDSpanMap['^[A]*'+sB+r'N(\d*)MLyNT_\[kN-m\]'] = sB+'MLyNT_[kN-m]'
- # Old
- for sB in ['b1','b2','b3']:
- SB=sB.upper()
- EDSpanMap[r'^Spn(\d)ALx'+sB+r'_\[m/s^2\]']=SB+'ALx_[m/s^2]'
- EDSpanMap[r'^Spn(\d)ALy'+sB+r'_\[m/s^2\]']=SB+'ALy_[m/s^2]'
- EDSpanMap[r'^Spn(\d)ALz'+sB+r'_\[m/s^2\]']=SB+'ALz_[m/s^2]'
- EDSpanMap[r'^Spn(\d)TDx'+sB+r'_\[m\]' ]=SB+'TDx_[m]'
- EDSpanMap[r'^Spn(\d)TDy'+sB+r'_\[m\]' ]=SB+'TDy_[m]'
- EDSpanMap[r'^Spn(\d)TDz'+sB+r'_\[m\]' ]=SB+'TDz_[m]'
- EDSpanMap[r'^Spn(\d)RDx'+sB+r'_\[deg\]' ]=SB+'RDx_[deg]'
- EDSpanMap[r'^Spn(\d)RDy'+sB+r'_\[deg\]' ]=SB+'RDy_[deg]'
- EDSpanMap[r'^Spn(\d)RDz'+sB+r'_\[deg\]' ]=SB+'RDz_[deg]'
- EDSpanMap[r'^Spn(\d)FLx'+sB+r'_\[kN\]' ]=SB+'FLx_[kN]'
- EDSpanMap[r'^Spn(\d)FLy'+sB+r'_\[kN\]' ]=SB+'FLy_[kN]'
- EDSpanMap[r'^Spn(\d)FLz'+sB+r'_\[kN\]' ]=SB+'FLz_[kN]'
- EDSpanMap[r'^Spn(\d)MLy'+sB+r'_\[kN-m\]' ]=SB+'MLx_[kN-m]'
- EDSpanMap[r'^Spn(\d)MLx'+sB+r'_\[kN-m\]' ]=SB+'MLy_[kN-m]'
- EDSpanMap[r'^Spn(\d)MLz'+sB+r'_\[kN-m\]' ]=SB+'MLz_[kN-m]'
- return find_matching_columns(Cols, EDSpanMap)
-
-def spanwiseColAD(Cols):
- """ Return column info, available columns and indices that contain AD spanwise data"""
- ADSpanMap=dict()
- for sB in ['B1','B2','B3']:
- ADSpanMap['^[A]*'+sB+r'N(\d*)Alpha_\[deg\]']=sB+'Alpha_[deg]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)AOA_\[deg\]' ]=sB+'Alpha_[deg]' # DBGOuts
- ADSpanMap['^[A]*'+sB+r'N(\d*)AxInd_\[-\]' ]=sB+'AxInd_[-]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)TnInd_\[-\]' ]=sB+'TnInd_[-]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)AIn_\[deg\]' ]=sB+'AxInd_[-]' # DBGOuts NOTE BUG Unit
- ADSpanMap['^[A]*'+sB+r'N(\d*)ApI_\[deg\]' ]=sB+'TnInd_[-]' # DBGOuts NOTE BUG Unit
- ADSpanMap['^[A]*'+sB+r'N(\d*)AIn_\[-\]' ]=sB+'AxInd_[-]' # DBGOuts
- ADSpanMap['^[A]*'+sB+r'N(\d*)ApI_\[-\]' ]=sB+'TnInd_[-]' # DBGOuts
- ADSpanMap['^[A]*'+sB+r'N(\d*)Uin_\[m/s\]' ]=sB+'Uin_[m/s]' # DBGOuts
- ADSpanMap['^[A]*'+sB+r'N(\d*)Uit_\[m/s\]' ]=sB+'Uit_[m/s]' # DBGOuts
- ADSpanMap['^[A]*'+sB+r'N(\d*)Uir_\[m/s\]' ]=sB+'Uir_[m/s]' # DBGOuts
- ADSpanMap['^[A]*'+sB+r'N(\d*)Cl_\[-\]' ]=sB+'Cl_[-]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Cd_\[-\]' ]=sB+'Cd_[-]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Cm_\[-\]' ]=sB+'Cm_[-]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Cx_\[-\]' ]=sB+'Cx_[-]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Cy_\[-\]' ]=sB+'Cy_[-]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Cn_\[-\]' ]=sB+'Cn_[-]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Ct_\[-\]' ]=sB+'Ct_[-]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Re_\[-\]' ]=sB+'Re_[-]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Vrel_\[m/s\]' ]=sB+'Vrel_[m/s]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Theta_\[deg\]']=sB+'Theta_[deg]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Phi_\[deg\]' ]=sB+'Phi_[deg]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Twst_\[deg\]' ]=sB+'Twst_[deg]' #DBGOuts
- ADSpanMap['^[A]*'+sB+r'N(\d*)Curve_\[deg\]']=sB+'Curve_[deg]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Vindx_\[m/s\]']=sB+'Vindx_[m/s]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Vindy_\[m/s\]']=sB+'Vindy_[m/s]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Fx_\[N/m\]' ]=sB+'Fx_[N/m]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Fy_\[N/m\]' ]=sB+'Fy_[N/m]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Fl_\[N/m\]' ]=sB+'Fl_[N/m]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Fd_\[N/m\]' ]=sB+'Fd_[N/m]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Fn_\[N/m\]' ]=sB+'Fn_[N/m]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Ft_\[N/m\]' ]=sB+'Ft_[N/m]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)VUndx_\[m/s\]']=sB+'VUndx_[m/s]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)VUndy_\[m/s\]']=sB+'VUndy_[m/s]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)VUndz_\[m/s\]']=sB+'VUndz_[m/s]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)VDisx_\[m/s\]']=sB+'VDisx_[m/s]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)VDisy_\[m/s\]']=sB+'VDisy_[m/s]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)VDisz_\[m/s\]']=sB+'VDisz_[m/s]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)STVx_\[m/s\]' ]=sB+'STVx_[m/s]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)STVy_\[m/s\]' ]=sB+'STVy_[m/s]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)STVz_\[m/s\]' ]=sB+'STVz_[m/s]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Vx_\[m/s\]' ]=sB+'Vx_[m/s]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Vy_\[m/s\]' ]=sB+'Vy_[m/s]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Vz_\[m/s\]' ]=sB+'Vz_[m/s]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)DynP_\[Pa\]' ]=sB+'DynP_[Pa]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)M_\[-\]' ]=sB+'M_[-]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Mm_\[N-m/m\]' ]=sB+'Mm_[N-m/m]'
- ADSpanMap['^[A]*'+sB+r'N(\d*)Gam_\[' ]=sB+'Gam_[m^2/s]' #DBGOuts
- # --- AD 14
- ADSpanMap[r'^Alpha(\d*)_\[deg\]' ]='Alpha_[deg]'
- ADSpanMap[r'^DynPres(\d*)_\[Pa\]' ]='DynPres_[Pa]'
- ADSpanMap[r'^CLift(\d*)_\[-\]' ]='CLift_[-]'
- ADSpanMap[r'^CDrag(\d*)_\[-\]' ]='CDrag_[-]'
- ADSpanMap[r'^CNorm(\d*)_\[-\]' ]='CNorm_[-]'
- ADSpanMap[r'^CTang(\d*)_\[-\]' ]='CTang_[-]'
- ADSpanMap[r'^CMomt(\d*)_\[-\]' ]='CMomt_[-]'
- ADSpanMap[r'^Pitch(\d*)_\[deg\]' ]='Pitch_[deg]'
- ADSpanMap[r'^AxInd(\d*)_\[-\]' ]='AxInd_[-]'
- ADSpanMap[r'^TanInd(\d*)_\[-\]' ]='TanInd_[-]'
- ADSpanMap[r'^ForcN(\d*)_\[N\]' ]='ForcN_[N]'
- ADSpanMap[r'^ForcT(\d*)_\[N\]' ]='ForcT_[N]'
- ADSpanMap[r'^Pmomt(\d*)_\[N-m\]' ]='Pmomt_[N-N]'
- ADSpanMap[r'^ReNum(\d*)_\[x10^6\]']='ReNum_[x10^6]'
- ADSpanMap[r'^Gamma(\d*)_\[m^2/s\]']='Gamma_[m^2/s]'
-
- return find_matching_columns(Cols, ADSpanMap)
-
-def insert_extra_columns_AD(dfRad, tsAvg, vr=None, rho=None, R=None, nB=None, chord=None):
- # --- Compute additional values (AD15 only)
- if dfRad is None:
- return None
- if dfRad.shape[1]==0:
- return dfRad
- if chord is not None:
- if vr is not None:
- chord =chord[0:len(dfRad)]
- for sB in ['B1','B2','B3']:
- try:
- vr_bar=vr/R
- Fx = dfRad[sB+'Fx_[N/m]']
- U0 = tsAvg['Wind1VelX_[m/s]']
- Ct=nB*Fx/(0.5 * rho * 2 * U0**2 * np.pi * vr)
- Ct[vr<0.01*R] = 0
- dfRad[sB+'Ctloc_[-]'] = Ct
- CT=2*np.trapz(vr_bar*Ct,vr_bar)
- dfRad[sB+'CtAvg_[-]']= CT*np.ones(vr.shape)
- except:
- pass
- try:
- dfRad[sB+'Gamma_[m^2/s]'] = 1/2 * chord* dfRad[sB+'Vrel_[m/s]'] * dfRad[sB+'Cl_[-]']
- except:
- pass
- try:
- if not sB+'Vindx_[m/s]' in dfRad.columns:
- dfRad[sB+'Vindx_[m/s]']= -dfRad[sB+'AxInd_[-]'].values * dfRad[sB+'Vx_[m/s]'].values
- dfRad[sB+'Vindy_[m/s]']= dfRad[sB+'TnInd_[-]'].values * dfRad[sB+'Vy_[m/s]'].values
- except:
- pass
- return dfRad
-
-
-
-def spanwisePostPro(FST_In=None,avgMethod='constantwindow',avgParam=5,out_ext='.outb',df=None):
- """
- Postprocess FAST radial data
-
- INPUTS:
- - FST_IN: Fast .fst input file
- - avgMethod='periods', avgParam=2: average over 2 last periods, Needs Azimuth sensors!!!
- - avgMethod='constantwindow', avgParam=5: average over 5s of simulation
- - postprofile: outputfile to write radial data
- """
- # --- Opens Fast output and performs averaging
- if df is None:
- df = FASTOutputFile(FST_In.replace('.fst',out_ext).replace('.dvr',out_ext)).toDataFrame()
- returnDF=True
- else:
- returnDF=False
- # NOTE: spanwise script doest not support duplicate columns
- df = df.loc[:,~df.columns.duplicated()]
- dfAvg = averageDF(df,avgMethod=avgMethod ,avgParam=avgParam) # NOTE: average 5 last seconds
-
- # --- Extract info (e.g. radial positions) from Fast input file
- # We don't have a .fst input file, so we'll rely on some default values for "r"
- rho = 1.225
- chord = None
- # --- Extract radial positions of output channels
- r_AD, r_ED, r_BD, IR_AD, IR_ED, IR_BD, R, r_hub, fst = FASTRadialOutputs(FST_In, OutputCols=df.columns.values)
- if R is None:
- R=1
- try:
- chord = fst.AD.Bld1['BldAeroNodes'][:,5] # Full span
- except:
- pass
- try:
- rho = fst.AD['Rho']
- except:
- try:
- rho = fst.AD['AirDens']
- except:
- pass
- #print('r_AD:', r_AD)
- #print('r_ED:', r_ED)
- #print('r_BD:', r_BD)
- #print('I_AD:', IR_AD)
- #print('I_ED:', IR_ED)
- #print('I_BD:', IR_BD)
- # --- Extract radial data and export to csv if needed
- dfRad_AD = None
- dfRad_ED = None
- dfRad_BD = None
- Cols=dfAvg.columns.values
- # --- AD
- ColsInfoAD, nrMaxAD = spanwiseColAD(Cols)
- dfRad_AD = extract_spanwise_data(ColsInfoAD, nrMaxAD, df=None, ts=dfAvg.iloc[0])
- dfRad_AD = insert_extra_columns_AD(dfRad_AD, dfAvg.iloc[0], vr=r_AD, rho=rho, R=R, nB=3, chord=chord)
- dfRad_AD = insert_radial_columns(dfRad_AD, r_AD, R=R, IR=IR_AD)
- # --- ED
- ColsInfoED, nrMaxED = spanwiseColED(Cols)
- dfRad_ED = extract_spanwise_data(ColsInfoED, nrMaxED, df=None, ts=dfAvg.iloc[0])
- dfRad_ED = insert_radial_columns(dfRad_ED, r_ED, R=R, IR=IR_ED)
- # --- BD
- ColsInfoBD, nrMaxBD = spanwiseColBD(Cols)
- dfRad_BD = extract_spanwise_data(ColsInfoBD, nrMaxBD, df=None, ts=dfAvg.iloc[0])
- dfRad_BD = insert_radial_columns(dfRad_BD, r_BD, R=R, IR=IR_BD)
- if returnDF:
- return dfRad_ED , dfRad_AD, dfRad_BD, df
- else:
- return dfRad_ED , dfRad_AD, dfRad_BD
-
-
-
-def spanwisePostProRows(df, FST_In=None):
- """
- Returns a 3D matrix: n x nSpan x nColumn where df is of size n x nColumn
-
- NOTE: this is really not optimal. Spanwise columns should be extracted only once..
- """
- # --- Extract info (e.g. radial positions) from Fast input file
- # We don't have a .fst input file, so we'll rely on some default values for "r"
- rho = 1.225
- chord = None
- # --- Extract radial positions of output channels
- r_AD, r_ED, r_BD, IR_AD, IR_ED, IR_BD, R, r_hub, fst = FASTRadialOutputs(FST_In, OutputCols=df.columns.values)
- #print('r_AD:', r_AD)
- #print('r_ED:', r_ED)
- #print('r_BD:', r_BD)
- if R is None:
- R=1
- try:
- chord = fst.AD.Bld1['BldAeroNodes'][:,5] # Full span
- except:
- pass
- try:
- rho = fst.AD['Rho']
- except:
- try:
- rho = fst.AD['AirDens']
- except:
- pass
- # --- Extract radial data for each azimuthal average
- M_AD=None
- M_ED=None
- M_BD=None
- Col_AD=None
- Col_ED=None
- Col_BD=None
- v = df.index.values
-
- # --- Getting Column info
- Cols=df.columns.values
- if r_AD is not None:
- ColsInfoAD, nrMaxAD = spanwiseColAD(Cols)
- if r_ED is not None:
- ColsInfoED, nrMaxED = spanwiseColED(Cols)
- if r_BD is not None:
- ColsInfoBD, nrMaxBD = spanwiseColBD(Cols)
- for i,val in enumerate(v):
- if r_AD is not None:
- dfRad_AD = extract_spanwise_data(ColsInfoAD, nrMaxAD, df=None, ts=df.iloc[i])
- dfRad_AD = insert_extra_columns_AD(dfRad_AD, df.iloc[i], vr=r_AD, rho=rho, R=R, nB=3, chord=chord)
- dfRad_AD = insert_radial_columns(dfRad_AD, r_AD, R=R, IR=IR_AD)
- if i==0:
- M_AD = np.zeros((len(v), len(dfRad_AD), len(dfRad_AD.columns)))
- Col_AD=dfRad_AD.columns.values
- M_AD[i, :, : ] = dfRad_AD.values
- if r_ED is not None and len(r_ED)>0:
- dfRad_ED = extract_spanwise_data(ColsInfoED, nrMaxED, df=None, ts=df.iloc[i])
- dfRad_ED = insert_radial_columns(dfRad_ED, r_ED, R=R, IR=IR_ED)
- if i==0:
- M_ED = np.zeros((len(v), len(dfRad_ED), len(dfRad_ED.columns)))
- Col_ED=dfRad_ED.columns.values
- M_ED[i, :, : ] = dfRad_ED.values
- if r_BD is not None and len(r_BD)>0:
- dfRad_BD = extract_spanwise_data(ColsInfoBD, nrMaxBD, df=None, ts=df.iloc[i])
- dfRad_BD = insert_radial_columns(dfRad_BD, r_BD, R=R, IR=IR_BD)
- if i==0:
- M_BD = np.zeros((len(v), len(dfRad_BD), len(dfRad_BD.columns)))
- Col_BD=dfRad_BD.columns.values
- M_BD[i, :, : ] = dfRad_BD.values
- return M_AD, Col_AD, M_ED, Col_ED, M_BD, Col_BD
-
-
-def FASTRadialOutputs(FST_In, OutputCols=None):
- """ Returns radial positions where FAST has outputs
- INPUTS:
- FST_In: fast input file (.fst)
- OUTPUTS:
- r_AD: radial positions of FAST Outputs from the rotor center
- """
- R = None
- r_hub =0
- r_AD = None
- r_ED = None
- r_BD = None
- IR_ED = None
- IR_AD = None
- IR_BD = None
- fst=None
- if FST_In is not None:
- fst = FASTInputDeck(FST_In, readlist=['AD','ADbld','ED','BD'])
- # NOTE: all this below should be in FASTInputDeck
- if fst.version == 'F7':
- # --- FAST7
- if not hasattr(fst,'AD'):
- raise Exception('The AeroDyn file couldn''t be found or read, from main file: '+FST_In)
- r_AD,IR_AD = AD14_BldGag(fst.AD)
- R = fst.fst['TipRad']
- try:
- rho = fst.AD['Rho']
- except:
- rho = fst.AD['AirDens']
- else:
- # --- OpenFAST 2
- R = None
-
- # --- ElastoDyn
- if 'NumTurbines' in fst.fst.keys():
- # AeroDyn driver...
- if 'HubRad(1)' in fst.fst.keys():
- r_hub = fst.fst['HubRad(1)']
- else:
- r_hub = fst.fst['BldHubRad_bl(1_1)']
-
- elif not hasattr(fst,'ED'):
- print('[WARN] The Elastodyn file couldn''t be found or read, from main file: '+FST_In)
- #raise Exception('The Elastodyn file couldn''t be found or read, from main file: '+FST_In)
- else:
- R = fst.ED['TipRad']
- r_hub = fst.ED['HubRad']
- if fst.ED.hasNodal:
- _, r_ED = ED_BldStations(fst.ED)
- IR_ED =None
- else:
- r_ED, IR_ED = ED_BldGag(fst.ED)
-
- # --- BeamDyn
- if fst.BD is not None:
- r_BD, IR_BD, r_BD_All = BD_BldGag(fst.BD)
- r_BD= r_BD+r_hub
- if R is None:
- R = r_BD_All[-1] # just in case ED file missing
-
- # --- AeroDyn
- if fst.AD is None:
- print('[WARN] The AeroDyn file couldn''t be found or read, from main file: '+FST_In)
- #raise Exception('The AeroDyn file couldn''t be found or read, from main file: '+FST_In)
- else:
- if fst.ADversion == 'AD15':
- if fst.AD.Bld1 is None:
- raise Exception('The AeroDyn blade file couldn''t be found or read, from main file: '+FST_In)
-
- if 'B1N001Cl_[-]' in OutputCols or np.any(np.char.find(list(OutputCols),'AB1N')==0):
- # This was compiled with all outs
- r_AD = fst.AD.Bld1['BldAeroNodes'][:,0] # Full span
- r_AD += r_hub
- IR_AD = None
- else:
- r_AD,_ = AD_BldGag(fst.AD,fst.AD.Bld1, chordOut = True) # Only at Gages locations
- r_AD += r_hub
-
- if R is None:
- # ElastoDyn was not read, we use R from AD
- R = fst.AD.Bld1['BldAeroNodes'][-1,0]
-
- elif fst.ADversion == 'AD14':
- r_AD,IR_AD = AD14_BldGag(fst.AD)
-
- else:
- raise Exception('AeroDyn version unknown')
- return r_AD, r_ED, r_BD, IR_AD, IR_ED, IR_BD, R, r_hub, fst
-
-
-
-def addToOutlist(OutList, Signals):
- if not isinstance(Signals,list):
- raise Exception('Signals must be a list')
- for s in Signals:
- ss=s.split()[0].strip().strip('"').strip('\'')
- AlreadyIn = any([o.find(ss)==1 for o in OutList ])
- if not AlreadyIn:
- OutList.append(s)
- return OutList
-
-
-
-# --------------------------------------------------------------------------------}
-# --- Generic df
-# --------------------------------------------------------------------------------{
-def remap_df(df, ColMap, bColKeepNewOnly=False, inPlace=False):
- """ Add/rename columns of a dataframe, potentially perform operations between columns
-
- Example:
-
- ColumnMap={
- 'WS_[m/s]' : '{Wind1VelX_[m/s]}' , # create a new column from existing one
- 'RtTSR_[-]' : '{RtTSR_[-]} * 2 + {RtAeroCt_[-]}' , # change value of column
- 'RotSpeed_[rad/s]' : '{RotSpeed_[rpm]} * 2*np.pi/60 ', # new column [rpm] -> [rad/s]
- }
- # Read
- df = weio.read('FASTOutBin.outb').toDataFrame()
- # Change columns based on formulae, potentially adding new columns
- df = fastlib.remap_df(df, ColumnMap, inplace=True)
-
- """
- if not inPlace:
- df=df.copy()
- ColMapMiss=[]
- ColNew=[]
- RenameMap=dict()
- for k0,v in ColMap.items():
- k=k0.strip()
- v=v.strip()
- if v.find('{')>=0:
- search_results = re.finditer(r'\{.*?\}', v)
- expr=v
- # For more advanced operations, we use an eval
- bFail=False
- for item in search_results:
- col=item.group(0)[1:-1]
- if col not in df.columns:
- ColMapMiss.append(col)
- bFail=True
- expr=expr.replace(item.group(0),'df[\''+col+'\']')
- #print(k0, '=', expr)
- if not bFail:
- df[k]=eval(expr)
- ColNew.append(k)
- else:
- print('[WARN] Column not present in dataframe, cannot evaluate: ',expr)
- else:
- #print(k0,'=',v)
- if v not in df.columns:
- ColMapMiss.append(v)
- print('[WARN] Column not present in dataframe: ',v)
- else:
- RenameMap[k]=v
-
- # Applying renaming only now so that expressions may be applied in any order
- for k,v in RenameMap.items():
- k=k.strip()
- iCol = list(df.columns).index(v)
- df.columns.values[iCol]=k
- ColNew.append(k)
- df.columns = df.columns.values # Hack to ensure columns are updated
-
- if len(ColMapMiss)>0:
- print('[FAIL] The following columns were not found in the dataframe:',ColMapMiss)
- #print('Available columns are:',df.columns.values)
-
- if bColKeepNewOnly:
- ColNew = [c for c,_ in ColMap.items() if c in ColNew]# Making sure we respec order from user
- ColKeepSafe = [c for c in ColNew if c in df.columns.values]
- ColKeepMiss = [c for c in ColNew if c not in df.columns.values]
- if len(ColKeepMiss)>0:
- print('[WARN] Signals missing and omitted for ColKeep:\n '+'\n '.join(ColKeepMiss))
- df=df[ColKeepSafe]
- return df
-
-
-# --------------------------------------------------------------------------------}
-# --- Tools for PostProcessing one or several simulations
-# --------------------------------------------------------------------------------{
-def _zero_crossings(y,x=None,direction=None):
- """
- Find zero-crossing points in a discrete vector, using linear interpolation.
- direction: 'up' or 'down', to select only up-crossings or down-crossings
- Returns:
- x values xzc such that y(yzc)==0
- indexes izc, such that the zero is between y[izc] (excluded) and y[izc+1] (included)
- if direction is not provided, also returns:
- sign, equal to 1 for up crossing
- """
- y=np.asarray(y)
- if x is None:
- x=np.arange(len(y))
-
- if np.any((x[1:] - x[0:-1]) <= 0.0):
- raise Exception('x values need to be in ascending order')
-
- # Indices before zero-crossing
- iBef = np.where(y[1:]*y[0:-1] < 0.0)[0]
-
- # Find the zero crossing by linear interpolation
- xzc = x[iBef] - y[iBef] * (x[iBef+1] - x[iBef]) / (y[iBef+1] - y[iBef])
-
- # Selecting points that are exactly 0 and where neighbor change sign
- iZero = np.where(y == 0.0)[0]
- iZero = iZero[np.where((iZero > 0) & (iZero < x.size-1))]
- iZero = iZero[np.where(y[iZero-1]*y[iZero+1] < 0.0)]
-
- # Concatenate
- xzc = np.concatenate((xzc, x[iZero]))
- iBef = np.concatenate((iBef, iZero))
-
- # Sort
- iSort = np.argsort(xzc)
- xzc, iBef = xzc[iSort], iBef[iSort]
-
- # Return up-crossing, down crossing or both
- sign = np.sign(y[iBef+1]-y[iBef])
- if direction == 'up':
- I= np.where(sign==1)[0]
- return xzc[I],iBef[I]
- elif direction == 'down':
- I= np.where(sign==-1)[0]
- return xzc[I],iBef[I]
- elif direction is not None:
- raise Exception('Direction should be either `up` or `down`')
- return xzc, iBef, sign
-
-def find_matching_pattern(List, pattern):
- """ Return elements of a list of strings that match a pattern
- and return the first matching group
- """
- reg_pattern=re.compile(pattern)
- MatchedElements=[]
- MatchedStrings=[]
- for l in List:
- match=reg_pattern.search(l)
- if match:
- MatchedElements.append(l)
- if len(match.groups(1))>0:
- MatchedStrings.append(match.groups(1)[0])
- else:
- MatchedStrings.append('')
- return MatchedElements, MatchedStrings
-
-
-
-def extractSpanTSReg(ts, col_pattern, colname, IR=None):
- """ Helper function to extract spanwise results, like B1N1Cl B1N2Cl etc.
-
- Example
- col_pattern: 'B1N(\d*)Cl_\[-\]'
- colname : 'B1Cl_[-]'
- """
- # Extracting columns matching pattern
- cols, sIdx = find_matching_pattern(ts.keys(), col_pattern)
- if len(cols) ==0:
- return (None,None)
-
- # Sorting by ID
- cols = np.asarray(cols)
- Idx = np.array([int(s) for s in sIdx])
- Isort = np.argsort(Idx)
- Idx = Idx[Isort]
- cols = cols[Isort]
-
- nrMax = np.max(Idx)
- Values = np.zeros((nrMax,1))
- Values[:] = np.nan
-# if IR is None:
-# cols = [col_pattern.format(ir+1) for ir in range(nr)]
-# else:
-# cols = [col_pattern.format(ir) for ir in IR]
- for idx,col in zip(Idx,cols):
- Values[idx-1]=ts[col]
- nMissing = np.sum(np.isnan(Values))
- if nMissing==nrMax:
- return (None,None)
- if len(cols)nrMax:
- print('[WARN] More values found for {}, found {}/{}'.format(colname,len(cols),nrMax))
- return (colname,Values)
-
-def extractSpanTS(ts, nr, col_pattern, colname, IR=None):
- """ Helper function to extract spanwise results, like B1N1Cl B1N2Cl etc.
-
- Example
- col_pattern: 'B1N{:d}Cl_[-]'
- colname : 'B1Cl_[-]'
- """
- Values=np.zeros((nr,1))
- if IR is None:
- cols = [col_pattern.format(ir+1) for ir in range(nr)]
- else:
- cols = [col_pattern.format(ir) for ir in IR]
- colsExist = [c for c in cols if c in ts.keys() ]
- if len(colsExist)==0:
- return (None,None)
-
- Values = [ts[c] if c in ts.keys() else np.nan for c in cols ]
- nMissing = np.sum(np.isnan(Values))
- #Values = ts[cols].T
- #nCoun=len(Values)
- if nMissing==nr:
- return (None,None)
- if len(colsExist)nr:
- print('[WARN] More values found for {}, found {}/{}'.format(colname,len(cols),nr))
- return (colname,Values)
-
-def radialInterpTS(df, r, varName, r_ref, blade=1, bldFmt='AB{:d}', ndFmt='N{:03d}', method='interp'):
- """
- Interpolate a time series at a given radial position for a given variable (varName)
- INPUTS:
- - df : a dataframe (typically with OpenFAST time series)
- - r : radial positions of node where data is to be interpolated
- - varName: variable name (and unit) to be interpolated.
- The dataframe column will be assumed to be "BldFmt"+"ndFmt"+varName
- - r_ref : radial position of nodal data present in the dataframe
- - bldFmt : format for blade number, e.g. 'B{:d}' or 'AB{:d}'
- - ndFmt : format for node number, e.g. 'N{:d}' or 'N{:03d}'
- OUTPUT:
- - interpolated time series
- """
- # --- Sanity checks
- r_ref = np.asarray(r_ref)
- if not np.all(r_ref[:-1] <= r_ref[1:]):
- raise Exception('This function only works for ascending radial values')
-
- # No extrapolation
- if rnp.max(r_ref):
- raise Exception('Extrapolation not supported')
-
- # Exactly on first or last nodes
- if r==r_ref[0]:
- col=bldFmt.format(blade) + ndFmt.format(1) + varName
- if col in df.columns.values:
- return df[col]
- else:
- raise Exception('Column {} not found in dataframe'.format(col))
- elif r==r_ref[-1]:
- col=bldFmt.format(blade) + ndFmt.format(len(r_ref)+1) + varName
- if col in df.columns.values:
- return df[col]
- else:
- raise Exception('Column {} not found in dataframe'.format(col))
-
- if method=='interp':
- # Interpolation
- iBef = np.where(r_reftStart].copy()
-
- dfPsi= bin_mean_DF(df, psiBin, colPsi)
- if np.any(dfPsi['Counts']<1):
- print('[WARN] some bins have no data! Increase the bin size.')
-
- return dfPsi
-
-
-def averageDF(df,avgMethod='periods',avgParam=None,ColMap=None,ColKeep=None,ColSort=None,stats=['mean']):
- """
- See average PostPro for documentation, same interface, just does it for one dataframe
- """
- def renameCol(x):
- for k,v in ColMap.items():
- if x==v:
- return k
- return x
- # Before doing the colomn map we store the time
- time = df['Time_[s]'].values
- timenoNA = time[~np.isnan(time)]
- # Column mapping
- if ColMap is not None:
- ColMapMiss = [v for _,v in ColMap.items() if v not in df.columns.values]
- if len(ColMapMiss)>0:
- print('[WARN] Signals missing and omitted for ColMap:\n '+'\n '.join(ColMapMiss))
- df.rename(columns=renameCol,inplace=True)
- ## Defining a window for stats (start time and end time)
- if avgMethod.lower()=='constantwindow':
- tEnd = timenoNA[-1]
- if avgParam is None:
- tStart=timenoNA[0]
- else:
- tStart =tEnd-avgParam
- elif avgMethod.lower()=='periods':
- # --- Using azimuth to find periods
- if 'Azimuth_[deg]' not in df.columns:
- raise Exception('The sensor `Azimuth_[deg]` does not appear to be in the output file. You cannot use the averaging method by `periods`, use `constantwindow` instead.')
- # NOTE: potentially we could average over each period and then average
- psi=df['Azimuth_[deg]'].values
- _,iBef = _zero_crossings(psi-psi[-10],direction='up')
- if len(iBef)==0:
- _,iBef = _zero_crossings(psi-180,direction='up')
- if len(iBef)==0:
- print('[WARN] Not able to find a zero crossing!')
- tEnd = time[-1]
- iBef=[0]
- else:
- tEnd = time[iBef[-1]]
-
- if avgParam is None:
- tStart=time[iBef[0]]
- else:
- avgParam=int(avgParam)
- if len(iBef)-10:
- print('[WARN] Signals missing and omitted for ColKeep:\n '+'\n '.join(ColKeepMiss))
- df=df[ColKeepSafe]
- if tStart