diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 683d64f5..28714196 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,7 @@ -# This workflow will upload a Python Package using Twine when a release is created +# This workflow will upload new releases to PyPi +# Could write it explicitely ourself but this was neat. +# Consider moving the docs dependencies to dev to speed up (probably not a problem). +# reference: https://github.com/marketplace/actions/publish-python-poetry-package name: Upload Python Package to PyPi @@ -12,20 +15,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 + - name: Check out repository + uses: actions/checkout@v4 + - name: Build and publish to pypi + uses: JRubics/poetry-publish@v1.17 with: - python-version: '3.10' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install wheel twine - - name: Build package - run: python setup.py sdist bdist_wheel - - name: Publish package - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: | - twine upload dist/* + python_version: "3.11" + plugins: "poetry-dynamic-versioning[plugin]" + ignore_dev_requirements: "yes" + pypi_token: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd3cb338..e3525bbf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# This workflow will lint, run unit tests, test CLI and build with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Test Python package @@ -10,47 +10,98 @@ on: branches: [ master ] jobs: - build: - - name: Test with Python ${{ matrix.python-version }} on Ubuntu + linting: + name: Linting + # linting has nothing to do with multiple versions of os and python + # (the result will be the same) and therefore separated to save time runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Load pip cache if cache exists + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip + restore-keys: ${{ runner.os }}-pip + - name: Install linters + run: python -m pip install black flake8 isort + - name: Lint code + run: | + flake8 ./qats/ --exit-zero --max-complexity=10 --statistics + black . + isort . + + testing: + needs: linting + name: Test with Python ${{ matrix.python-version }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + os: ["ubuntu-latest"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + # https://stackoverflow.com/questions/75549995/why-do-the-pyside6-qt-modules-cause-tox-to-fail-during-a-github-action + - name: Install missing libraries on GitHub agent + run: sudo apt update && sudo apt install -y libegl1-mesa-dev + - name: Check out repository + uses: actions/checkout@v4 + - name: Set up python ${{ matrix.python-version }} + id: setup-python + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: Load cached Poetry installation + id: cached-poetry + uses: actions/cache@v3 + with: + path: ~/.local # the path depends on the OS + key: poetry-0 # increment to reset cache + - name: Install and configure Poetry + # install if not cached. Create venv in project to simplify caching + if: steps.cached-poetry.outputs.cache-hit != 'true' + uses: snok/install-poetry@v1 + with: + # Consider specifying version (version: 1.7.1 on Python 3.11) + virtualenvs-create: true + virtualenvs-in-project: true #.venv in current directory + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v3 + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + - name: Create venv and install project dependencies + # install if not cached + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + - name: Install project + # required to test the CLI, show working directory for debugging run: | - python -m pip install --upgrade pip - python -m pip install flake8 - # python -m pip install setuptools-scm flake8 wheel pyside2 h5py pymatreader matplotlib npTDMS numpy openpyxl pandas scipy - # changed 27.12.2023: install from requirements.txt, so that incompatibilities in requirements lead to failed tests - python -m pip install -r requirements.txt - - name: Lint with flake8 + poetry install --no-interaction + ls -a + - name: Run unit tests run: | - # stop the build if there are Python syntax errors or undefined names - # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Run tests + source .venv/bin/activate + pytest test/ + - name: Test CLI env: - QT_API: pyside2 + QT_API: pyside6 run: | - python -m unittest discover - python setup.py install - # test running qats via entry points + source .venv/bin/activate qats -h qats app -h qats config -h - # test running qats via __main__ hook python -m qats -h - # test that building documentation does not fail + - name: Test building package + run: poetry build + - name: Test building documentation + run: | + source .venv/bin/activate sphinx-build -b html docs/source docs/_build diff --git a/.gitignore b/.gitignore index 432077e5..3cce0622 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,9 @@ code-snippets/ # local test folders tmp*/* + +# version file written by poetry dynamic versioning +*/_version.py + +# Poetry +poetry.lock diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 1eb176e7..15663797 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,22 +8,20 @@ version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-20.04 - tools: - # minimum supported Python version - python: "3.7" + tools: {python: "3.11"} + # A solution to build docs with Poetry and avoid an additional (out of sync) requirements.txt + # https://github.com/Robpol86/sphinx-multi-theme/blob/28aac0ac3bd54387bd51bfb9c673c2208394c729/.readthedocs.yaml + jobs: + pre_create_environment: + - asdf plugin add poetry + - asdf install poetry latest + - asdf global poetry latest + - poetry config virtualenvs.create false + post_install: + - poetry install # Build documentation in the docs/ directory with Sphinx sphinx: builder: html configuration: docs/source/conf.py fail_on_warning: true - -# Optionally declare the Python requirements required to build your docs -python: - # install qats first, then requirements - # (because RTD uses `--upgrade-strategy eager` to install the package, - # which installs the latest possible version of all dependencies) - install: - - method: pip - path: . - - requirements: requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d3f09a2..ae1cf63a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ # Changelog + +> **Note:** +> After version 4.12.1 this changelog is deprecated. Releases will in future be documented in [Github's releases page](https://github.com/dnvgl/qats/releases). + + All notable changes to the project will be documented in this file, with format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). We apply the *"major.minor.micro"* versioning scheme defined in [PEP 440](https://www.python.org/dev/peps/pep-0440). diff --git a/README.md b/README.md index 4e1df40a..3c9b6bf8 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,6 @@ perfect for inspecting, comparing and reporting: ![QATS GUI](https://raw.githubusercontent.com/dnvgl/qats/master/docs/source/demo.gif) -## Python version support - -QATS supports Python 3.7, 3.8, 3.9 and 3.10. - ## Getting started ### Installation @@ -56,21 +52,20 @@ python -m pip install --upgrade qats You may now import qats in your own scripts: ```python -from qats import TsDB, TimeSeries +>>> from qats import TsDB, TimeSeries ``` -... or use the GUI to inspect time series. Note that as of version 4.2.0 you are quite free to choose which -[Qt](https://www.qt.io) binding you would like to use for the GUI: [PyQt5](https://pypi.org/project/PyQt5/) or -[PySide2](https://pypi.org/project/PySide2/), or even [PyQt4](https://pypi.org/project/PyQt4/) / -[PySide](https://pypi.org/project/PySide/). +... or use the GUI to inspect time series. -Install the chosen binding (here PyQt5 as an example): +_New in version 5.0.0._ +The [Qt](https://www.qt.io) binding [PySide6](https://pypi.org/project/PySide6/) is installed with `qats`. +If you would rather like to use [PyQt6](https://pypi.org/project/PyQt6/), run ```console -python -m pip install pyqt5 +python -m pip install pyqt6 ``` -If multiple Qt bindinds are installed, the one to use may be controlled by setting the environmental variable `QT_API` to the desired package. Accepted values include `pyqt5` (to use PyQt5) and `pyside2` (PySide2). For more details, see [README file for qtpy](https://github.com/spyder-ide/qtpy/blob/master/README.md). +If multiple Qt bindinds are installed, the one to use may be controlled by setting the environmental variable `QT_API` to the desired package. Accepted values include `pyqt6` (to use PyQt6) and `pyside6` (PySide6). For more details, see [README file for qtpy](https://github.com/spyder-ide/qtpy/blob/master/README.md). The GUI may now be launched by: @@ -97,7 +92,7 @@ python -m qats config --link-app * [**Source**](https://github.com/dnvgl/qats) * [**Issues**](https://github.com/dnvgl/qats/issues) -* [**Changelog**](https://github.com/dnvgl/qats/blob/master/CHANGELOG.md) +* [**Changelog**](https://github.com/dnvgl/qats/releases) * [**Documentation**](https://qats.readthedocs.io) * [**Download**](https://pypi.org/project/qats/) @@ -108,7 +103,13 @@ purposes. See deployment for notes on how to deploy the project on a live system ### Prerequisites -Install Python version 3.7 or later from either https://www.python.org or https://www.anaconda.com. +Install Python version 3.8 or later from either https://www.python.org or https://www.anaconda.com. + +Install Poetry with [the official installer](https://python-poetry.org/docs/#installing-with-the-official-installer). + +Install the [poetry-dynamic-versioning](https://pypi.org/project/poetry-dynamic-versioning/) plugin: + +```poetry self add "poetry-dynamic-versioning[plugin]"``` ### Clone the source code repository @@ -118,35 +119,22 @@ At the desired location, run: ### Installing -To get the development environment running: - -... create an isolated Python environment and activate it, - -```console -python -m venv /path/to/new/virtual/environment - -/path/to/new/virtual/environment/Scripts/activate -``` - -... install the dev dependencies in [requirements.txt](requirements.txt), - -```console -python -m pip install -r requirements.txt -``` - -.. and install the package in development ("editable") mode. +To get the development environment running... ```console -python -m pip install -e . +poetry install ``` -_Note: This is similar to the "legacy" development installation command ``python setup.py develop``, see the [setuptools page on development mode](https://setuptools.pypa.io/en/latest/userguide/development_mode.html)._ +This will +- create an isolated environment +- install all dependencies including those required for development, testing and building documentation +- and install the package in development ("editable") mode You should now be able to import the package in the Python console, ```python -import qats -help(qats) +>>> import qats +>>> help(qats) ``` ... and use the command line interface (CLI). @@ -171,13 +159,13 @@ python -m unittest discover ### Building the package -Build tarball and wheel distributions by: +Build tarball and wheel distributions by ```console -python setup.py sdist bdist_wheel +poetry build ``` -The distribution file names adhere to the [PEP 0427](https://www.python.org/dev/peps/pep-0427/#file-name-convention) +The builds appear in the `.dist` folder. The distributions adhere to the [PEP 0427](https://www.python.org/dev/peps/pep-0427/#file-name-convention) convention `{distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl`. ### Building the documentation @@ -194,16 +182,19 @@ To force a build to read/write all files (always read all files and don't use a sphinx-build -a -E -b html docs\source docs\_build ``` -### Deployment -Packaging, unit testing and deployment to [PyPi](https://pypi.org/project/qats/) is automated using [GitHub Actions](https://docs.github.com/en/actions). - ### Versioning We apply the "major.minor.micro" versioning scheme defined in [PEP 440](https://www.python.org/dev/peps/pep-0440/). See also [Scheme choices](https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#scheme-choices) on https://packaging.python.org/. -We cut a new version by applying a Git tag like `3.0.1` at the desired commit and then -[setuptools_scm](https://github.com/pypa/setuptools_scm/#setup-py-usage) takes care of the rest. For the versions -available, see the [tags on this repository](https://github.com/dnvgl/qats/tags). +Versions are tagged with Git tags like `v3.0.1`. See the [tags on this repository](https://github.com/dnvgl/qats/tags). + +### Release and deployment + +We use the [release cycle in GitHub](https://github.com/dnvgl/qats/releases) to cut new releases. + +Once a new release is published, [GitHub Actions](https://docs.github.com/en/actions) takes care of the packaging, unit testing and deployment to [PyPi](https://pypi.org/project/qats/). + +The workflows for continuous integration and deployment are found in [.github/workflows](.github/workflows/). ## Authors diff --git a/data/model_test_data.dat b/data/model_test_data.dat index 9c8ce569..4c7475a9 100644 --- a/data/model_test_data.dat +++ b/data/model_test_data.dat @@ -1,3 +1,5 @@ +# test comment +# another comment time[s] WaveC[m] Wave-F[m] Wave-S[m] Surge[m] Sway[m] Heave[m] Roll[deg] Pitch[deg] Yaw[deg] Acc-X[m/s^2] Acc-Y[m/s^3] Acc-Z[m/s^4] Ten-S-FP[kN] Ten-S-FS[kN] Ten-S-AS[kN] Ten-S-AP[kN] RW1[m] RW3[m] RW5[m] RW7[m] RW13[m] RW14[m] RW2[m] RW4[m] RW6[m] RW8[m] RW9[m] RW10[m] RW11[m] RW12[m] RW15[m] RW16[m] RW17[m] RW18[m] RW19[m] Ten-P-FP[kN] Ten-P-FS[kN] Ten-P-AS[kN] Ten-P-AP[kN] 0.0000000e+00 1.1468169e+00 -6.2024932e+00 1.5963418e+00 -1.2440454e+01 1.8175390e+00 5.8588385e-01 3.3933087e-01 1.6214460e+00 4.1363153e-01 1.1196024e-01 -2.3473288e-02 1.5040265e-01 3.6432728e+03 3.7840381e+03 2.5385340e+03 2.3443660e+03 -9.3945101e-01 -1.5887219e+00 -2.6872322e+00 -2.7754727e+00 9.6520604e-01 2.1473762e+00 -1.4565005e+00 -2.1295738e+00 -2.5923946e+00 -1.2597798e+00 -2.6924590e+00 -7.5948454e-01 1.4100411e+00 1.4251543e+00 -1.1114424e+00 -1.2456250e+00 1.1438377e+00 3.4769380e-01 -5.0987183e-01 3.6178672e+03 3.6655819e+03 2.5026818e+03 2.3855970e+03 7.0710678e-02 1.3147447e+00 -6.1294752e+00 1.5299688e+00 -1.2476236e+01 1.8165961e+00 6.0005881e-01 3.3398525e-01 1.6583331e+00 4.2669511e-01 1.1747680e-01 -2.1960561e-02 1.7126258e-01 3.6428042e+03 3.7912919e+03 2.5447336e+03 2.3366768e+03 -9.1152887e-01 -1.5730936e+00 -2.6601386e+00 -2.7932502e+00 1.1041916e+00 2.1576044e+00 -1.4025477e+00 -2.1228321e+00 -2.6254164e+00 -1.2523734e+00 -2.7433894e+00 -7.6519707e-01 1.4704902e+00 1.4266390e+00 -1.0990665e+00 -1.2547405e+00 1.1563448e+00 4.1997923e-01 -3.6905164e-01 3.6210090e+03 3.6738023e+03 2.5028865e+03 2.3816678e+03 diff --git a/docs/source/conf.py b/docs/source/conf.py index 57bfccde..30378fc3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,6 +10,7 @@ # import os import sys + import qats # which theme to use @@ -267,8 +268,10 @@ def setup(app): try: import inspect - from sphinx.ext.autosummary import Autosummary + from docutils.parsers.rst import directives + from sphinx.ext.autosummary import Autosummary + # import sphinx.ext.autodoc # from sphinx.ext.autosummary import get_documenter # from sphinx.util.inspect import safe_getattr diff --git a/docs/source/examples/mooring_fatigue.py b/docs/source/examples/mooring_fatigue.py index 8f295c60..22c7f4ef 100644 --- a/docs/source/examples/mooring_fatigue.py +++ b/docs/source/examples/mooring_fatigue.py @@ -2,8 +2,10 @@ Calculate mooring line fatigue. """ import os -import numpy as np from math import pi + +import numpy as np + from qats import TsDB from qats.fatigue.sn import SNCurve, minersum diff --git a/docs/source/examples/plot.py b/docs/source/examples/plot.py index 23e9f496..55e5b38c 100644 --- a/docs/source/examples/plot.py +++ b/docs/source/examples/plot.py @@ -2,6 +2,7 @@ Example of using the time series database class """ import os + from qats import TsDB db = TsDB() diff --git a/docs/source/examples/rainflow.py b/docs/source/examples/rainflow.py index 5d39dcaf..f6bffa0e 100644 --- a/docs/source/examples/rainflow.py +++ b/docs/source/examples/rainflow.py @@ -2,6 +2,7 @@ Example on working with cycle range and range-mean distributions. """ import os + from qats import TsDB # locate time series file diff --git a/docs/source/examples/tail_fitting.py b/docs/source/examples/tail_fitting.py index 6e15f0fa..3b27c67c 100644 --- a/docs/source/examples/tail_fitting.py +++ b/docs/source/examples/tail_fitting.py @@ -2,10 +2,10 @@ Example showing tail fitting with Weibull """ import os + from qats import TsDB from qats.stats.weibull import lse, plot_fit - # locate time series file file_name = os.path.join("..", "..", "..", "data", "mooring.ts") db = TsDB.fromfile(file_name) diff --git a/docs/source/examples/timeseries_filter.py b/docs/source/examples/timeseries_filter.py index 5cedcbfb..b0889cd1 100644 --- a/docs/source/examples/timeseries_filter.py +++ b/docs/source/examples/timeseries_filter.py @@ -2,7 +2,9 @@ Example showing how to directly initiate the database with time series from file and then filter the time series. """ import os + import matplotlib.pyplot as plt + from qats import TsDB # locate time series file diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index d418ade6..633c65aa 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -6,29 +6,19 @@ Getting started Prerequisites ************* -You need Python version 3.7 or later (up to 3.10 is supported). You can find it at https://www.python.org or https://www.anaconda.com. +You need Python version 3.8 or later. Versions up to and including 3.11 are tested, version 3.12 is not tested on deployment +with Github Actions but successfully tested locally. + +You can install Python from https://www.python.org or https://www.anaconda.com. Installation ************ -.. note:: - As of version 4.2.0, you must install the desired qt binding yourself (needed for the GUI to work). - Supported packages are: PyQt5, Pyside2, PyQt4 and Pyside. See installation instructions below. - QATS is installed from PyPI by using `pip`: .. code-block:: console - python -m pip install qats - -In order to use the GUI, you must also install a Python package with qt bindings (here, `PyQt5` is used as an -example): - -.. code-block:: - - python -m pip install pyqt5 - -Supported qt bindings are: PyQt5, Pyside2, PyQt4 and Pyside. + $ python -m pip install qats Now you should be able to import the package in the Python console @@ -59,11 +49,11 @@ Now you should be able to import the package in the Python console ... >>> -and the command line interface (CLI). +and run the command line interface (CLI). .. code-block:: console - qats -h + $ qats -h usage: qats [-h] [--version] {app,config} ... @@ -79,42 +69,44 @@ and the command line interface (CLI). config Configure the package +.. note:: + As of version 5.0.0, qats installs the `Qt `_ binding `PySide6 `_. + Although not recommended, you can choose a different qt binding yourself by installing the package and setting the + environmental variable :code:`QT_API`. Accepted values include :code:`pyqt6` (to use PyQt6) and :code:`pyside6` (PySide6). For more details, + see `README file for qtpy `_. -Launching the GUI -***************** +.. note:: + As of version 4.11.0, the CLI is also available through the ``python -m`` switch, for example: -The GUI is launched via the CLI: + .. code-block:: -.. code-block:: + $ python -m qats -h + $ python -m qats app - qats app +.. $ python -m qats config --link-app -Or, you may add a shortcut for launching the QATS GUI to your Windows Start menu and on the Desktop by running the command: -.. code-block:: +.. :code:`python -m qats config --link-app-no-exe`. - qats config --link-app -.. note:: - As of version 4.11.0, the CLI is also available through the ``python -m`` switch, for example: +.. :code:`python -m qats -h` or :code:`python -m qats app`. - .. code-block:: - python -m qats -h - python -m qats app - To add a Windows Start menu shortcut that utilizes this to launch the GUI without invoking the qats executable - (i.e., does not call ``qats.exe``), use +Launching the GUI +***************** - .. code-block:: +The GUI is launched via the CLI: - python -m qats config --link-app-no-exe +.. code-block:: + $ qats app -.. :code:`python -m qats config --link-app-no-exe`. +If using qats on **Windows**, you may add a shortcut for launching the qats GUI to your Windows Start menu and on the Desktop by running the command: +.. code-block:: -.. :code:`python -m qats -h` or :code:`python -m qats app`. + C:\> qats config --link-app Your first script @@ -127,5 +119,5 @@ Import the time series database, load data to it from file and plot it all. :linenos: :lines: 1-17 -Take a look at :ref:`examples` and the :ref:`api` to learn how to use QATS and build it into your code. +Take a look at :ref:`examples` and the :ref:`api` to learn how to use :code:`qats` and build it into your code. diff --git a/docs/source/gui.rst b/docs/source/gui.rst index 2a794783..2c97990a 100644 --- a/docs/source/gui.rst +++ b/docs/source/gui.rst @@ -13,7 +13,7 @@ Create link to GUI in start menu and desktop :figclass: align-center :target: _images/link_app.gif - In the terminal, run ``qats config --link-app`` to create start menu and desktop links to QATS. Pin the start menu + In the terminal, run :code:`qats config --link-app` to create start menu and desktop links to qats. Pin the start menu link to the taskbar to further ease accessibility. diff --git a/docs/source/index.rst b/docs/source/index.rst index 4ac8ec34..e2335f2b 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -49,16 +49,15 @@ inspecting, quality assurance and reporting. Use the library for more advanced o Python version support ********************** -QATS currently supports Python version 3.7, 3.8, 3.9, and 3.10. +QATS currently supports Python version 3.8 and later. Note that version 3.12 is not properly tested but should work. + Source code, Issue tracker and Changelog **************************************** The `source code `_, `issue tracker `_ and -`changelog `_ are hosted on GitHub. - -A copy of the changelog is rendered within this documentation, see :ref:`Changelog`. +`changelog `_ are hosted on GitHub. Downloads diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..08d3aea0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,91 @@ +[tool.poetry] +name = "qats" +version = "0.0.0" # overridden by dynamic versioning, must still be specified +description = "Library for efficient processing and visualization of time series" +authors = [ + "Per Voie ", + "Erling Lone " + ] +# Repo and docs are optional fields in the project section. Consider to keep it here or to gather +# all URLs in the url section +repository = "https://github.com/dnvgl/qats" +documentation = "https://qats.readthedocs.io" +license = "MIT" +readme = "README.md" +include = [ + "LICENSE", + "CHANGELOG.md" +] + +[tool.poetry.dependencies] +python = ">=3.8.1,<3.13" +h5py = ">=3.5.0" +pymatreader = ">=0.0.24" +matplotlib = ">=3.3.3" +nptdms = ">=1.1.0" +numpy = [ + {version = ">=1.21.6", python = "<3.12"}, + {version = ">=1.26.0", python = ">=3.12"} +] +openpyxl = ">=3.0.5" +pandas = ">=1.1.4" +qtpy = ">=1.9.0" +scipy = [ + {version = ">=1.9.0", python = "<3.12"}, + {version = ">=1.11.1", python = ">=3.12"} +] +pywin32 = {version = "^306", markers = "platform_system == 'Windows'"} # MUST check the appropriate constraint +pyside6 = "^6.6.0" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.3" +flake8 = "^6.1.0" +black = "^23.11.0" +isort = "^5.12.0" + +[tool.poetry.group.docs.dependencies] +sphinx = ">=6.1" +furo = ">=2023.9.10" +myst-parser = ">=2.0.0" + +[tool.poetry.scripts] +qats = "qats.cli:main" + + +[tool.poetry.urls] +Source = "https://github.com/dnvgl/qats" +Documentation = "https://qats.readthedocs.io" +Download = "https://pypi.org/project/qats/" +Issues = "https://github.com/dnvgl/qats/issues" + +# enable dynamic versioning using Git tags +# https://sam.hooke.me/note/2023/08/poetry-automatically-generated-package-version-from-git-commit/ +# https://pypi.org/project/poetry-dynamic-versioning/ +[tool.poetry-dynamic-versioning] +enable = true +vcs = "git" +pattern = "^(?P\\d+\\.\\d+\\.\\d+)(-?((?P[a-zA-Z]+)\\.?(?P\\d+)?))?" +format-jinja = """ + {%- if distance == 0 -%} + {{- base -}} + {%- else -%} + {{- base }}.dev{{ distance }}+g{{commit}} + {%- endif -%} +""" +format-jinja-imports = [ + { module = "datetime", item = "datetime" } +] + +[tool.poetry-dynamic-versioning.files."qats/_version.py"] +persistent-substitution = true +initial-content = """ +# file generated by poetry dynamic versioning +# don't change, don't track in version control +__version__ = "0.0.0" +__version_tuple__ = (0, 0, 0) +""" + +[build-system] +# https://pypi.org/project/poetry-dynamic-versioning/ +requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.1.1,<2.0.0"] +build-backend = "poetry_dynamic_versioning.backend" # "poetry.core.masonry.api" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..53b38d73 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +; [pytest] +; filterwarnings = +; ignore::Warning diff --git a/qats/__init__.py b/qats/__init__.py index 96be850a..20d13c69 100644 --- a/qats/__init__.py +++ b/qats/__init__.py @@ -2,17 +2,14 @@ """ Library for efficient processing and visualization of time series. """ -from .tsdb import TsDB from .ts import TimeSeries -from pkg_resources import get_distribution, DistributionNotFound +from .tsdb import TsDB -# get version +# version string try: - # version at runtime from distribution/package info - __version__ = get_distribution("qats").version -except DistributionNotFound: - # package is not installed - __version__ = "" + from ._version import __version__ +except ImportError: + __version__ = "0.0.0" +# summary __summary__ = __doc__ - diff --git a/qats/app/exceptions.py b/qats/app/exceptions.py index 42ef73b9..441a0aa5 100644 --- a/qats/app/exceptions.py +++ b/qats/app/exceptions.py @@ -6,6 +6,7 @@ import sys import traceback import webbrowser + from qtpy.QtWidgets import QMessageBox diff --git a/qats/app/funcs.py b/qats/app/funcs.py index 068a369d..6bc8dd89 100644 --- a/qats/app/funcs.py +++ b/qats/app/funcs.py @@ -2,9 +2,10 @@ Module with functions for handling file operations and calculations. Made for multithreading. """ import numpy as np -from ..tsdb import TsDB -from ..stats.gumbel import pwm as gumbel_pwm + from ..fatigue.rainflow import rebin as rebin_cycles +from ..stats.gumbel import pwm as gumbel_pwm +from ..tsdb import TsDB def calculate_psd(container, twin, fargs, nperseg, normalize): diff --git a/qats/app/gui.py b/qats/app/gui.py index aa1abe9c..71e90e94 100644 --- a/qats/app/gui.py +++ b/qats/app/gui.py @@ -5,39 +5,43 @@ @author: perl """ +import json import logging -import sys import os +import sys from itertools import cycle + import numpy as np +from matplotlib.backends.backend_qt5agg import \ + FigureCanvasQTAgg as FigureCanvas +from matplotlib.backends.backend_qt5agg import \ + NavigationToolbar2QT as NavigationToolbar +from matplotlib.figure import Figure +from pkg_resources import resource_filename from qtpy import API_NAME as QTPY_API_NAME from qtpy.QtCore import * from qtpy.QtGui import * -from qtpy.QtWidgets import QMainWindow, QFileDialog, QMessageBox, QWidget, QHBoxLayout, \ - QListView, QGroupBox, QLabel, QRadioButton, QCheckBox, QSpinBox, QDoubleSpinBox, QVBoxLayout, QPushButton, \ - QLineEdit, QComboBox, QSplitter, QFrame, QTabBar, QHeaderView, QDialog, QAction, QDialogButtonBox -from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas -from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar -from matplotlib.figure import Figure -import json -from pkg_resources import resource_filename, get_distribution, DistributionNotFound +from qtpy.QtWidgets import (QAction, QCheckBox, QComboBox, QDialog, + QDialogButtonBox, QDoubleSpinBox, QFileDialog, + QFrame, QGroupBox, QHBoxLayout, QHeaderView, + QLabel, QLineEdit, QListView, QMainWindow, + QMessageBox, QPushButton, QRadioButton, QSpinBox, + QSplitter, QTabBar, QVBoxLayout, QWidget) + +from ..stats.empirical import empirical_cdf +from ..tsdb import TsDB +from .funcs import (calculate_gumbel_fit, calculate_psd, calculate_rfc, + calculate_stats, calculate_trace, export_to_file, + import_from_file, read_timeseries) from .logger import QLogger from .threading import Worker -from .models import CustomSortFilterProxyModel -from .widgets import CustomTabWidget, CustomTableWidgetItem, CustomTableWidget -from ..tsdb import TsDB -from ..stats.empirical import empirical_cdf -from .funcs import ( - export_to_file, - import_from_file, - read_timeseries, - calculate_trace, - calculate_psd, - calculate_rfc, - calculate_gumbel_fit, - calculate_stats -) - +from .widgets import CustomTableWidget, CustomTableWidgetItem, CustomTabWidget +# version string +# from .._version import __version__ +try: + from .._version import __version__ +except ImportError: + __version__ = "0.0.0" LOGGING_LEVELS = dict( debug=logging.DEBUG, @@ -85,12 +89,11 @@ "Weibull distribution fitted to sample maxima/minima."), } -# TODO: New method that generalize threading -# TODO: Explore how to create consecutive threads without handshake in main loop +# todo: New method that generalize threading +# todo: Explore how to create consecutive threads without handshake in main loop # todo: add technical guidance and result interpretation to help menu, link docs website # todo: add 'export' option to file menu: response statistics summary (mean, std, skew, kurt, tz, weibull distributions, # gumbel distributions etc.) -# todo: read orcaflex time series files class Qats(QMainWindow): @@ -99,10 +102,10 @@ class Qats(QMainWindow): Contain widgets for plotting time series, power spectra and statistics. - Series of data are loaded from a .ts file, and their names are displayed in a checkable list view. The user can - select the series it wants from the list and plot them on a matplotlib canvas. The prodlinelib python package is - used for loading time series from file, perform signal processing, calculating power spectra and statistics and - plotting. + Series of data are loaded from a time series file (e.g., .ts), and their names are displayed in a checkable + list view. The user can select the series it wants from the list and plot them on a matplotlib canvas. The + base library is used to load time series from file (`qats.io`), perform signal processing (`qats.signal`), + calculating power spectra and statistics (`qats.stats`) and plotting. """ def __init__(self, parent=None, files_on_init=None, logging_level="info"): @@ -241,7 +244,7 @@ def __init__(self, parent=None, files_on_init=None, logging_level="info"): # initiate time series data base and checkable model and view with filter self.db = TsDB() self.db_source_model = QStandardItemModel() - self.db_proxy_model = CustomSortFilterProxyModel() + self.db_proxy_model = QSortFilterProxyModel() self.db_proxy_model.setDynamicSortFilter(True) self.db_proxy_model.setSourceModel(self.db_source_model) self.db_view = QListView() @@ -252,9 +255,9 @@ def __init__(self, parent=None, files_on_init=None, logging_level="info"): self.db_view_filter_pattern.setPlaceholderText("type filter text") self.db_view_filter_pattern.setText("") self.db_view_filter_syntax = QComboBox() - self.db_view_filter_syntax.addItem("Wildcard", QRegExp.Wildcard) - self.db_view_filter_syntax.addItem("Regular expression", QRegExp.RegExp) - self.db_view_filter_syntax.addItem("Fixed string", QRegExp.FixedString) + self.db_view_filter_syntax.addItem("Wildcard", "wildcard") + self.db_view_filter_syntax.addItem("Regular expression", "regexp") + self.db_view_filter_syntax.addItem("Fixed string", "fixedstring") self.db_view_filter_pattern.textChanged.connect(self.model_view_filter_changed) self.db_view_filter_syntax.currentIndexChanged.connect(self.model_view_filter_changed) self.db_view_filter_casesensitivity.toggled.connect(self.model_view_filter_changed) @@ -617,23 +620,61 @@ def model_view_filter_changed(self): """ Apply filter changes to db proxy model """ - syntax = QRegExp.PatternSyntax(self.db_view_filter_syntax.itemData(self.db_view_filter_syntax.currentIndex())) - case_sensitivity = (self.db_view_filter_casesensitivity.isChecked() and Qt.CaseSensitive or Qt.CaseInsensitive) - reg_exp = QRegExp(self.db_view_filter_pattern.text(), case_sensitivity, syntax) - self.db_proxy_model.setFilterRegExp(reg_exp) + # type of filter selected in drop-down menu + filter_index = self.db_view_filter_syntax.currentIndex() + filter_type = self.db_view_filter_syntax.itemData(filter_index) + # text input by user + pattern = self.db_view_filter_pattern.text() + # case sensitivity (if 'Case sensitive filter' is checked) + case_sensitive = self.db_view_filter_casesensitivity.isChecked() + + # the code below works for python qt5 (pyside2/pyqt5) and qt6 (pyside6/pyqt6) + # pyside6: see the following links for documentation on QRegularExpression and the filter model (QSortFilterProxyModel) + # https://doc.qt.io/qtforpython-6/PySide6/QtCore/QRegularExpression.html + # https://doc-snapshots.qt.io/qtforpython-6.2/PySide6/QtCore/QSortFilterProxyModel.html#filtering + # https://doc-snapshots.qt.io/qtforpython-6.2/PySide6/QtCore/QSortFilterProxyModel.html#PySide6.QtCore.QSortFilterProxyModel.filterAcceptsRow + + # notes on the methods available for self.db_proxy_model (type: QSortFilterProxyModel) + # .setFilterCaseSensitivity(Qt.CaseSensitive | Qt.CaseInsensitive) may be used with .setFilterWildcard(pattern) and .setFilterFixedString(pattern) + # .setFilterRegularExpression(QRegularExpression) may not be used with .setFilterCaseSensitivity(...) + # * setting a new regular expression propagates its case sensitivity to .filterCaseSensitivity (-> breaks the binding to what previously set) + # * setting a filter case sensitivity afterwards breaks the binding to the regular expression + + # construct regexp string that may be used to initiate QRegularExpression instance + if filter_type == "wildcard": + # pad with wildcard ('*') to get expected behaviour + reg_exp_pattern = QRegularExpression.wildcardToRegularExpression(f"*{pattern}*") + elif filter_type == "regexp": + # pattern string should be interpreted as a regexp pattern + reg_exp_pattern = pattern + elif filter_type == "fixedstring": + # according to https://doc.qt.io/qt-6/qregexp.html, a fixed string is + # equivalent to using the regexp pattern on a string in which all + # metacharacters are escaped using escape() + reg_exp_pattern = QRegularExpression.escape(pattern) + else: + raise ValueError(f"Unsupported filter type: {filter_type}") + + # options (for case sensitivity) to QRegularExpression construction + if case_sensitive: + # case sensitive is default for QRegularExpression => no options + options = {} + else: + # case insensitive => must be specified + options = {"options": QRegularExpression.CaseInsensitiveOption} + + # initiate QRegularExpression instance + reg_exp = QRegularExpression(reg_exp_pattern, **options) + + # assign reg exp to proxy model filter + # (but only if reg exp is valid, which is not always the case when user is still typing) + if reg_exp.isValid(): + self.db_proxy_model.setFilterRegularExpression(reg_exp) def on_about(self): """ Show information about the application """ - # get distribution version - try: - # version at runtime from distribution/package info - version = get_distribution("qats").version - except DistributionNotFound: - # package is not installed - version = "" - msg = "This is a low threshold tool for inspection of time series, power spectra and statistics. " \ "Its main objective is to ease self-check, quality assurance and reporting.

" \ "Import qats Python package and use the API " \ @@ -648,7 +689,7 @@ def on_about(self): msgbox.setIcon(QMessageBox.Information) msgbox.setTextFormat(Qt.RichText) msgbox.setText(msg.strip()) - msgbox.setWindowTitle(f"About QATS - version {version}") + msgbox.setWindowTitle(f"About QATS - version {__version__}") msgbox.exec_() def on_clear(self): @@ -1204,7 +1245,14 @@ def set_status(self, message=None, msecs=None): if not msecs: msecs = 0 # statusbar.showMessage() does not accept NoneType - self.statusBar().showMessage(message, msecs=msecs) + # self.statusBar().showMessage(message, msecs=msecs) + + # pyside6 uses 'timeout' keyword, pyqt6 uses 'msecs' keyword + # solve pragmatically by try-except + try: + self.statusBar().showMessage(message, timeout=msecs) + except TypeError: + self.statusBar().showMessage(message, msecs=msecs) def selected_series(self): """ diff --git a/qats/app/logger.py b/qats/app/logger.py index 723e10c8..f57c4150 100644 --- a/qats/app/logger.py +++ b/qats/app/logger.py @@ -6,6 +6,7 @@ @author: perl """ import logging + from qtpy.QtWidgets import QTextBrowser diff --git a/qats/app/models.py b/qats/app/models.py deleted file mode 100644 index d70e4b04..00000000 --- a/qats/app/models.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Module containing models such as item models, proxy models etc tailored for the QATS application - -@author: perl -""" - -from qtpy.QtCore import QSortFilterProxyModel - - -class CustomSortFilterProxyModel(QSortFilterProxyModel): - """ - Customized proxy model to filter items in a standard item model based on item names - """ - - def __init__(self, parent=None): - super(CustomSortFilterProxyModel, self).__init__(parent) - - def filterAcceptsRow(self, source_row, source_parent): - """ - Returns true if the item in the row indicated by the given source_row and source_parent should be included in - the model; otherwise returns false. - - Parameters - ---------- - source_row : int - item row index - source_parent : int - item parent index - - Returns - ------- - bool - true if the value held by the relevant item matches the filter string, wildcard string or regular expression - false otherwise. - - """ - index0 = self.sourceModel().index(source_row, 0, source_parent) - index1 = self.sourceModel().index(source_row, 1, source_parent) - - return ((self.filterRegExp().indexIn(self.sourceModel().data(index0)) >= 0 - or self.filterRegExp().indexIn(self.sourceModel().data(index1)) >= 0)) diff --git a/qats/app/threading.py b/qats/app/threading.py index 98222bc0..794e3757 100644 --- a/qats/app/threading.py +++ b/qats/app/threading.py @@ -1,6 +1,9 @@ import sys import traceback -from qtpy.QtCore import QRunnable, Signal as QSignal, QObject, Slot as QSlot + +from qtpy.QtCore import QObject, QRunnable +from qtpy.QtCore import Signal as QSignal +from qtpy.QtCore import Slot as QSlot class WorkerSignals(QObject): diff --git a/qats/app/widgets.py b/qats/app/widgets.py index 2e568faf..5d9d363f 100644 --- a/qats/app/widgets.py +++ b/qats/app/widgets.py @@ -6,7 +6,7 @@ @author: perl """ from qtpy.QtCore import Qt -from qtpy.QtWidgets import QTabWidget, QTableWidgetItem, QTableWidget +from qtpy.QtWidgets import QTableWidget, QTableWidgetItem, QTabWidget class CustomTabWidget (QTabWidget): diff --git a/qats/cli.py b/qats/cli.py index 73056ac1..cc0de1e7 100644 --- a/qats/cli.py +++ b/qats/cli.py @@ -2,52 +2,24 @@ """ Command line interface to app (GUI). """ +import argparse import os import sys -import argparse import sysconfig -from qtpy.QtWidgets import QApplication + from pkg_resources import resource_filename -from .app.exceptions import handle_exception -from .app.gui import Qats, LOGGING_LEVELS +from qtpy.QtWidgets import QApplication + from . import __version__ +from .app.exceptions import handle_exception +from .app.gui import LOGGING_LEVELS, Qats def link_app(): """ - Create start menu item and desktop shortcut to QATS desktop app. - """ - if not sys.platform == "win32": - print(f"Unable to create links to app on {sys.platform} OS.") - sys.exit() - - from win32com.client import Dispatch - - pkg_name = "qats" - scripts_dir = sysconfig.get_path("scripts") - ico_path = resource_filename("qats.app", "qats.ico") - target = os.path.join(scripts_dir, f"{pkg_name}-app.exe") - lnk_name = pkg_name.upper() + ".lnk" - - # open shell - shell = Dispatch("WScript.Shell") - - # create shortcuts to gui in desktop folder and start-menu programs - for loc in ("Desktop", "Programs"): - location = shell.SpecialFolders(loc) - path_link = os.path.join(location, lnk_name) - shortcut = shell.CreateShortCut(path_link) - shortcut.Description = f"{pkg_name.upper()} v{__version__}" - shortcut.TargetPath = target - shortcut.WorkingDirectory = os.getenv("USERPROFILE") - shortcut.IconLocation = ico_path - shortcut.Save() - - -def link_app_no_exe(): - """ - Create start menu item and desktop shortcut to QATS desktop app - (by invoking pythonw.exe with the necessary arguments, not qats.app.exe) + Create start menu item and desktop shortcut to `qats` desktop app + (by invoking pythonw.exe with the necessary arguments, not the + previous entry point qats-app.exe (or .cmd)). """ if not sys.platform == "win32": print(f"Unable to create links to app on {sys.platform} OS.") @@ -70,12 +42,9 @@ def link_app_no_exe(): target = python_exec_path # define arguments to target - # alternative 1: does not rely on entry point scripts + # (relies on invoking qats.__main__, not the entry point executable) arguments = f"-m {pkg_name} app" - # # alternative 2: relies on the pyw script generated for the entry point 'qats-app.exe' - # scripts_dir = sysconfig.get_path("scripts") - # arguments = os.path.join(scripts_dir, f"{pkg_name}-app-script.pyw") - + # open shell shell = Dispatch("WScript.Shell") @@ -94,7 +63,7 @@ def link_app_no_exe(): def unlink_app(): """ - Remove start menu item and desktop shortcut to QATS desktop application. + Remove start menu item and desktop shortcut to `qats` desktop application. """ if not sys.platform == "win32": print(f"Unable to remove links to app on {sys.platform} OS.") @@ -146,8 +115,8 @@ def main(): """ # top-level parser parser = argparse.ArgumentParser(prog="qats", - description="QATS is a library and desktop application for time series analysis") - parser.add_argument("--version", action="version", version=f"QATS {__version__}", help="Package version") + description="qats is a library and desktop application for time series analysis") + parser.add_argument("--version", action="version", version=f"qats {__version__}", help="Package version") subparsers = parser.add_subparsers(title="Commands", dest="command") # app parser @@ -162,10 +131,7 @@ def main(): config_parser = subparsers.add_parser("config", help="Configure the package") applink_group = config_parser.add_mutually_exclusive_group() applink_group.add_argument("--link-app", action="store_true", - help="Create start menu and destop links to the app.") - applink_group.add_argument("--link-app-no-exe", action="store_true", - help="Create start menu and destop links to the app. The desktop link generated by this " - "option does not rely on invoking any other executables but 'pythonw.exe'.") + help="Create start menu and destop links to the app (GUI).") applink_group.add_argument("--unlink-app", action="store_true", help="Remove start menu and destop links to the app.") @@ -179,8 +145,6 @@ def main(): elif args.command == "config": if args.link_app: link_app() - elif args.link_app_no_exe: - link_app_no_exe() elif args.unlink_app: unlink_app() else: diff --git a/qats/fatigue/__init__.py b/qats/fatigue/__init__.py index dda2a233..37e9b703 100644 --- a/qats/fatigue/__init__.py +++ b/qats/fatigue/__init__.py @@ -2,6 +2,4 @@ """ Sub-package for fatigue related calculations and operations. """ -from . import corrections -from . import rainflow -from . import sn +from . import corrections, rainflow, sn diff --git a/qats/fatigue/rainflow.py b/qats/fatigue/rainflow.py index 60db89f3..442eb2fa 100644 --- a/qats/fatigue/rainflow.py +++ b/qats/fatigue/rainflow.py @@ -5,6 +5,7 @@ """ from collections import deque from itertools import chain + import numpy as np # TODO: Evaluate from-to counting which stores the "orientation" of each cycle. Enables reconstruction of a time history diff --git a/qats/fatigue/sn.py b/qats/fatigue/sn.py index 8c5c1187..bd5b2068 100644 --- a/qats/fatigue/sn.py +++ b/qats/fatigue/sn.py @@ -6,7 +6,8 @@ - Fatigue damage calculation (functions) """ import numpy as np -from scipy.special import gamma as gammafunc, gammainc, gammaincc +from scipy.special import gamma as gammafunc +from scipy.special import gammainc, gammaincc # todo: Update SNCurve docstring to include description of class and attributes diff --git a/qats/io/__init__.py b/qats/io/__init__.py index e285f3c2..376450b7 100644 --- a/qats/io/__init__.py +++ b/qats/io/__init__.py @@ -2,10 +2,6 @@ """ Sub-package with io for various file formats. """ -from . import csv -from . import direct_access -from . import sintef_mat -from . import sintef_mat as matlab # for backwards compatibility -from . import other -from . import sima -from . import sima_h5 +from . import csv, direct_access, other, sima, sima_h5 +from . import sintef_mat # for backwards compatibility +from . import sintef_mat as matlab diff --git a/qats/io/direct_access.py b/qats/io/direct_access.py index 843477f5..0738d7f0 100644 --- a/qats/io/direct_access.py +++ b/qats/io/direct_access.py @@ -2,10 +2,10 @@ Readers for various direct access formatted time series files """ import os -import numpy as np -from struct import unpack -from struct import pack from array import array +from struct import pack, unpack + +import numpy as np def read_ts_names(path): diff --git a/qats/io/other.py b/qats/io/other.py index 03d9e424..bbdd6f20 100644 --- a/qats/io/other.py +++ b/qats/io/other.py @@ -2,6 +2,7 @@ Readers for various time series file formats """ import fnmatch + import numpy as np @@ -39,7 +40,7 @@ def read_dat_names(path): if len(timekeys) < 1: raise KeyError(f"The file '{path}' does not contain a time vector") elif len(timekeys) > 1: - raise KeyError(f"The file '{path}' contain duplicate time vectors") + raise KeyError(f"The file '{path}' contains duplicate time vectors") # skip the time array name assumed to be in the first column return names[1:] @@ -72,13 +73,13 @@ def read_dat_data(path, ind=None): """ with open(path, 'r') as f: - # skip commmented lines at start of file + # skip commented lines at beginning of file for line in f: + # break at first uncommented line (which is the header row and should not be read here) if not line.startswith("#"): - # skip all comment lines break - - # load data from the remaining rows as an array, skip the header row with keys (first row after comments) + + # load data, skipping commented lines and the header row with keys (first uncommented line) data = np.loadtxt(f, skiprows=0, usecols=ind, unpack=True) return data diff --git a/qats/io/sima.py b/qats/io/sima.py index ffac36f2..f0b6849d 100644 --- a/qats/io/sima.py +++ b/qats/io/sima.py @@ -4,8 +4,9 @@ import fnmatch import os import re -import numpy as np from struct import unpack + +import numpy as np from scipy.interpolate import interp1d diff --git a/qats/io/sima_h5.py b/qats/io/sima_h5.py index 9984f269..9dc3a70b 100644 --- a/qats/io/sima_h5.py +++ b/qats/io/sima_h5.py @@ -2,6 +2,7 @@ Readers for HDF5 formatted time series files exported from SIMA """ import os + import h5py import numpy as np @@ -205,11 +206,14 @@ def _timearray_info(dset): attrs = dset.attrs if "start" in attrs and "delta" in attrs: # SIMA-way of defining time array - timeinfo = { - "kind": "sima", - "start": float(attrs["start"]), - "dt": float(attrs["delta"]), - } + timeinfo = {"kind": "sima"} + # timeinfo["start"] = float(attrs["start"]) + # timeinfo["dt"} =: float(attrs["delta"]), + for keyout, keysima in {"start": "start", "dt": "delta"}.items(): + # we presume that attrs["start"] and attrs["delta"] are numpy arrays of size 1 + # (otherwise .item() will fail, but then it should because we expect start and delta + # to be single values not arrays) + timeinfo[keyout] = attrs[keysima].item() return timeinfo else: return None diff --git a/qats/io/sintef_mat.py b/qats/io/sintef_mat.py index b1cab485..acedfbcb 100644 --- a/qats/io/sintef_mat.py +++ b/qats/io/sintef_mat.py @@ -4,9 +4,10 @@ Works for matlab file format version <=7.2 and >=7.3. """ import fnmatch -import numpy as np -from typing import List, Tuple, Union from datetime import datetime, timedelta +from typing import List, Tuple, Union + +import numpy as np from pymatreader import read_mat diff --git a/qats/io/tdms.py b/qats/io/tdms.py index 5d2bcd0e..28d8f888 100644 --- a/qats/io/tdms.py +++ b/qats/io/tdms.py @@ -1,6 +1,7 @@ -from nptdms import TdmsFile -from typing import List, Tuple, Union import os +from typing import List, Tuple, Union + +from nptdms import TdmsFile def read_names(path): diff --git a/qats/signal.py b/qats/signal.py index 7fe3f84f..66c124e4 100644 --- a/qats/signal.py +++ b/qats/signal.py @@ -3,11 +3,15 @@ """ Module with functions for signal processing. """ -import numpy as np -from scipy.fftpack import fft, ifft, rfft, irfft -from scipy.signal import welch, butter, filtfilt, sosfiltfilt, csd as spcsd, coherence as spcoherence import warnings +import numpy as np +from scipy.fftpack import fft, ifft, irfft, rfft +from scipy.signal import butter +from scipy.signal import coherence as spcoherence +from scipy.signal import csd as spcsd +from scipy.signal import filtfilt, sosfiltfilt, welch + def extend_signal_ends(x: np.ndarray, n: int) -> np.ndarray: """Extend the signal ends with `n` values to mitigate the edge effect. diff --git a/qats/stats/__init__.py b/qats/stats/__init__.py index f81ef465..77c8c49b 100644 --- a/qats/stats/__init__.py +++ b/qats/stats/__init__.py @@ -2,7 +2,4 @@ """ Sub-package for statistics/distributions. """ -from . import empirical -from . import gumbel -from . import gumbelmin -from . import weibull +from . import empirical, gumbel, gumbelmin, weibull diff --git a/qats/stats/gumbel.py b/qats/stats/gumbel.py index 438b36e0..022ce16f 100644 --- a/qats/stats/gumbel.py +++ b/qats/stats/gumbel.py @@ -3,12 +3,12 @@ """ :class:`Gumbel` class and functions related to Gumbel distribution. """ -import numpy as np -from scipy.special import zetac, binom -from scipy.optimize import leastsq, fsolve import matplotlib.pyplot as plt -from .empirical import empirical_cdf +import numpy as np +from scipy.optimize import fsolve, leastsq +from scipy.special import binom, zetac +from .empirical import empirical_cdf # todo: build documentation and check that docstrings behave as intended # todo: create examples diff --git a/qats/stats/gumbelmin.py b/qats/stats/gumbelmin.py index 842b485f..b36c023a 100644 --- a/qats/stats/gumbelmin.py +++ b/qats/stats/gumbelmin.py @@ -4,13 +4,14 @@ :class:`GumbelMin` class and functions related to Gumbel (minima) distribution. """ import numpy as np +from matplotlib.pyplot import (figure, grid, legend, plot, savefig, show, + xlabel, ylabel, ylim, yticks) +from scipy.optimize import fsolve, leastsq from scipy.special import zetac -from scipy.optimize import leastsq, fsolve -from matplotlib.pyplot import figure, ylabel, yticks, plot, legend, grid, show, xlabel, ylim, savefig + from .empirical import empirical_cdf from .gumbel import _euler_masceroni as em - # todo: move fit methods e.g. _msm from class to standalone functions (importable) # todo: check fit procedures (read up once more and check implementation) # todo: create unit tests diff --git a/qats/stats/weibull.py b/qats/stats/weibull.py index 54967e84..9299479f 100644 --- a/qats/stats/weibull.py +++ b/qats/stats/weibull.py @@ -4,13 +4,13 @@ :class:`Weibull` class and functions related to Weibull distribution. """ -import numpy as np -from scipy.special import gamma, binom -from scipy.optimize import leastsq, fsolve, brentq import matplotlib.pyplot as plt -from .empirical import empirical_cdf -from ..signal import find_maxima +import numpy as np +from scipy.optimize import brentq, fsolve, leastsq +from scipy.special import binom, gamma +from ..signal import find_maxima +from .empirical import empirical_cdf # todo: build documentation and check that docstrings behave as intended # todo: create examples diff --git a/qats/ts.py b/qats/ts.py index c7d86ce1..41853350 100644 --- a/qats/ts.py +++ b/qats/ts.py @@ -7,17 +7,20 @@ import os from collections import OrderedDict from datetime import datetime, timedelta + +import matplotlib.pyplot as plt import numpy as np +from matplotlib import cm from scipy.interpolate import interp1d from scipy.stats import kurtosis, skew, tstd -import matplotlib.pyplot as plt -from matplotlib import cm -from .fatigue.rainflow import count_cycles, rebin as rebin_cycles, mesh -from .signal import lowpass, highpass, bandblock, bandpass, threshold as thresholdpass, smooth, taper, \ - average_frequency, find_maxima, psd -from .stats.weibull import Weibull, weibull2gumbel, pwm -from .stats.gumbel import Gumbel +from .fatigue.rainflow import count_cycles, mesh +from .fatigue.rainflow import rebin as rebin_cycles +from .signal import (average_frequency, bandblock, bandpass, find_maxima, + highpass, lowpass, psd, smooth, taper) +from .signal import threshold as thresholdpass +from .stats.gumbel import Gumbel +from .stats.weibull import Weibull, pwm, weibull2gumbel # todo: weibull and gumbel + plotting (self.pd = Weibull(), self.evd = Gumbel()) # todo: autocorrelation (see signal module) diff --git a/qats/tsdb.py b/qats/tsdb.py index cb8fa6ab..9926c7f4 100644 --- a/qats/tsdb.py +++ b/qats/tsdb.py @@ -3,52 +3,35 @@ """ Provides :class:`TsDB` class. """ -import os -import glob import copy import fnmatch +import glob +import os +from collections import OrderedDict, defaultdict from uuid import uuid4 + import matplotlib.pyplot as plt import numpy as np import pandas as pd -from collections import OrderedDict, defaultdict -from .ts import TimeSeries + from .fatigue.rainflow import rebin as rebin_cycles -from .io.sima import ( - read_names as read_sima_names, - read_ascii_data as read_sima_ascii_data, - read_bin_data as read_sima_bin_data, - read_sima_wind_names -) -from .io.sima_h5 import ( - read_names as read_sima_h5_names, - read_data as read_sima_h5_data, - write_data as write_sima_h5_data -) -from .io.csv import ( - read_names as read_csv_names, - read_data as read_csv_data -) -from .io.direct_access import ( - read_ts_names, - read_tda_names, - read_ts_data, - read_tda_data, - write_ts_data -) -from .io.sintef_mat import ( - read_names as read_mat_names, - read_data as read_mat_data -) -from .io.tdms import ( - read_names as read_tdms_names, - read_data as read_tdms_data -) -from .io.other import ( - read_dat_names, - read_dat_data, - write_dat_data -) +from .io.csv import read_data as read_csv_data +from .io.csv import read_names as read_csv_names +from .io.direct_access import (read_tda_data, read_tda_names, read_ts_data, + read_ts_names, write_ts_data) +from .io.other import read_dat_data, read_dat_names, write_dat_data +from .io.sima import read_ascii_data as read_sima_ascii_data +from .io.sima import read_bin_data as read_sima_bin_data +from .io.sima import read_names as read_sima_names +from .io.sima import read_sima_wind_names +from .io.sima_h5 import read_data as read_sima_h5_data +from .io.sima_h5 import read_names as read_sima_h5_names +from .io.sima_h5 import write_data as write_sima_h5_data +from .io.sintef_mat import read_data as read_mat_data +from .io.sintef_mat import read_names as read_mat_names +from .io.tdms import read_data as read_tdms_data +from .io.tdms import read_names as read_tdms_names +from .ts import TimeSeries # todo: cross spectrum(scipy.signal.csd) # todo: coherence (scipy.signal.coherence) @@ -116,9 +99,7 @@ def __iter__(self): time series in database """ for key in self.register_keys: - item = self.register[key] - if item is not None: - yield self.register[key] + yield self.get(name=key) def __len__(self): return self.n @@ -429,7 +410,7 @@ def _make_export_friendly_names(self, container, keep_basename=False): container : dict Container with time series keep_basename : bool, optional - Keep only time series names e.g. 'tension' in key 'C:\data\results.ts\tension'. Default False. + Keep only time series names e.g. 'tension' in key 'C:\\data\\results.ts\\tension'. Default False. Returns ------- diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 03a47877..00000000 --- a/requirements.txt +++ /dev/null @@ -1,18 +0,0 @@ -h5py>=3.5.0, <=3.7.0 -pymatreader==0.0.24 -matplotlib>=3.3.3, <=3.6 -npTDMS==1.1.0 -numpy>=1.21.6, <=1.24.* -openpyxl==3.0.5 -pandas>=1.1.4, <=1.5 -QtPy==1.9.0 -pyside2>=5.15.2, <5.16 -PyQt5>=5.15.2, <=5.15.7 -scipy>=1.7.2, <=1.9.2 # in accordance with https://docs.scipy.org/doc/scipy/dev/toolchain.html#numpy -setuptools-scm>=7.1.0, <=8 -wheel==0.38.4 -# documentation build requirements: -Sphinx>=5.1.1 -##sphinx-rtd-theme>=1.1.1 -furo>=2022.12.7 -myst-parser>=0.18.1 diff --git a/setup.py b/setup.py deleted file mode 100644 index bbe6d43f..00000000 --- a/setup.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Setup script for building and installing package and building html documentation -""" -from setuptools import setup, find_packages -import os - - -def read(fname): - """Utility function to read the README file.""" - return open(os.path.join(os.path.dirname(__file__), fname)).read() - - -setup( - # package data - name="qats", - use_scm_version=True, - packages=find_packages(exclude=("test",)), - package_data={ - "qats.app": ["qats.ico"], - }, - python_requires=">=3.7, <3.11", - setup_requires=["setuptools_scm"], - install_requires=[ - "numpy>=1,<2", - "openpyxl>=3,<4", - "scipy>=1,<2", - "matplotlib>=3,<4", - "npTDMS>=1,<2", - "h5py>=2.7,<4", - "QtPy>=1,<2", - "pandas>=1,<2", - "pymatreader>=0.0.20,<1", - "pywin32; platform_system == 'Windows'" - ], - entry_points={ - "console_scripts": ["qats = qats.cli:main"], - "gui_scripts": ["qats-app = qats.cli:launch_app"] - }, - zip_safe=True, - - # meta-data - author="Per Voie & Erling Lone", - description="Library for efficient processing and visualization of time series.", - long_description=read('README.md'), - long_description_content_type="text/markdown", - license="MIT", - url="https://github.com/dnvgl/qats", - download_url="https://pypi.org/project/qats/", - project_urls={ - "Issue Tracker": "https://github.com/dnvgl/qats/issues", - "Documentation": "https://qats.readthedocs.io", - "Changelog": "https://github.com/dnvgl/qats/blob/master/CHANGELOG.md", - }, - classifiers=[ - 'Topic :: Scientific/Engineering', - 'Development Status :: 5 - Production/Stable', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Operating System :: OS Independent', - 'License :: OSI Approved :: MIT License', - ] - -) diff --git a/test/test_fatigue_sn.py b/test/test_fatigue_sn.py index 875bb05c..f25409ab 100644 --- a/test/test_fatigue_sn.py +++ b/test/test_fatigue_sn.py @@ -4,10 +4,12 @@ """ import unittest -import numpy as np from collections import OrderedDict, defaultdict + +import numpy as np from scipy.optimize import brenth, brentq from scipy.special import gamma + from qats.fatigue.sn import SNCurve, minersum, minersum_weibull # todo: include tests for thickness correction of SNCurve class diff --git a/test/test_gumbel.py b/test/test_gumbel.py index 597bfcdf..bb7aff53 100644 --- a/test/test_gumbel.py +++ b/test/test_gumbel.py @@ -5,8 +5,7 @@ import unittest -from qats.stats.gumbel import Gumbel, pwm, mle, msm, lse -from qats.stats.gumbel import _euler_masceroni +from qats.stats.gumbel import Gumbel, _euler_masceroni, lse, mle, msm, pwm class EulerMascheroniTest(unittest.TestCase): diff --git a/test/test_io.py b/test/test_io.py index a5091a1a..2ab8da39 100644 --- a/test/test_io.py +++ b/test/test_io.py @@ -2,11 +2,12 @@ """ Module for testing io operations directly """ -from qats import TsDB -from qats.io import sima -import unittest import os import sys +import unittest + +from qats import TsDB +from qats.io import sima class TestTsDB(unittest.TestCase): diff --git a/test/test_motions.py b/test/test_motions.py index 105a6d1a..7fce6b9a 100644 --- a/test/test_motions.py +++ b/test/test_motions.py @@ -2,12 +2,14 @@ """ Module for testing functions in operations module """ -from qats import TsDB -from qats.motions import transform_motion, velocity, acceleration -import unittest import os +import unittest + import numpy as np +from qats import TsDB +from qats.motions import acceleration, transform_motion, velocity + class TestOperations(unittest.TestCase): def setUp(self): diff --git a/test/test_rainflow.py b/test/test_rainflow.py index 450c42e2..4059f0bf 100644 --- a/test/test_rainflow.py +++ b/test/test_rainflow.py @@ -4,11 +4,13 @@ """ import itertools +import os import unittest + import numpy as np -import os -from qats.fatigue import rainflow + from qats import TsDB +from qats.fatigue import rainflow class TestRainflowCounting(unittest.TestCase): diff --git a/test/test_readers.py b/test/test_readers.py index c89f835a..560dac28 100644 --- a/test/test_readers.py +++ b/test/test_readers.py @@ -4,19 +4,22 @@ The module utilizes TsDB.fromfile and .get() to read at least one time series from the file, to check that this does not generate any exceptions. """ -from qats import TsDB -import unittest import os import sys +import unittest +from pathlib import Path + +from qats import TsDB # todo: add test class for matlab +ROOT = Path(__file__).resolve().parent class TestAllReaders(unittest.TestCase): def setUp(self): # the data directory used in the test relative to this module # necessary to do it like this for the tests to work both locally and in virtual env - self.data_directory = os.path.join(os.path.dirname(__file__), '..', 'data') + self.data_directory = os.path.join(ROOT, '..', 'data') # file name, number of (time series) keys self.files = [ # sima h5 files @@ -50,6 +53,7 @@ def setUp(self): ] def test_correct_number_of_timeseries(self): + """ Read key file, check number of keys (data not loaded) """ failed = [] for filename, nts in self.files: db = TsDB.fromfile(os.path.join(self.data_directory, filename)) @@ -60,6 +64,7 @@ def test_correct_number_of_timeseries(self): f"\n *** ".join(failed)) def test_correct_timeseries_size(self): + """ Load time series: check that it loads and that t.size matches x.size """ failed = [] for filename, _ in self.files: try: diff --git a/test/test_signal.py b/test/test_signal.py index 1acdf1b6..15269eec 100644 --- a/test/test_signal.py +++ b/test/test_signal.py @@ -2,11 +2,14 @@ """ Module for testing signal processing functions """ +import os import unittest + import numpy as np -import os -from qats.signal import smooth, average_frequency, taper, lowpass, highpass, bandblock, bandpass, psd, find_maxima + from qats import TsDB +from qats.signal import (average_frequency, bandblock, bandpass, find_maxima, + highpass, lowpass, psd, smooth, taper) class TestSignal(unittest.TestCase): diff --git a/test/test_ts.py b/test/test_ts.py index 7dabe3f1..e177c563 100644 --- a/test/test_ts.py +++ b/test/test_ts.py @@ -3,12 +3,14 @@ Module for testing TimeSeries class """ +import copy import os import unittest from datetime import datetime + import numpy as np + from qats import TimeSeries, TsDB -import copy class TestTs(unittest.TestCase): diff --git a/test/test_tsdb.py b/test/test_tsdb.py index 52f90f58..a38dd944 100644 --- a/test/test_tsdb.py +++ b/test/test_tsdb.py @@ -2,12 +2,13 @@ """ Module for testing TsDB class """ -from qats import TimeSeries, TsDB -import unittest import os -import numpy as np import sys +import unittest +import numpy as np + +from qats import TimeSeries, TsDB # todo: add tests for listing subset(s) based on specifying parameter `names` (with and wo param. `keys`) # todo: add test for getm() with fullkey=False (similar to test_get_many_correct_key, but with shorter key) diff --git a/test/test_weibull.py b/test/test_weibull.py index 69e3f9a0..eba5c511 100644 --- a/test/test_weibull.py +++ b/test/test_weibull.py @@ -1,7 +1,6 @@ import unittest -from qats.stats.weibull import Weibull, pwm, mle, msm, lse, pwm2 - +from qats.stats.weibull import Weibull, lse, mle, msm, pwm, pwm2 # todo: more test cases for weibull class and functions diff --git a/version_bumper.py b/version_bumper.py deleted file mode 100644 index 91f3072f..00000000 --- a/version_bumper.py +++ /dev/null @@ -1,194 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Set new version tag, using git command line interface. - -In order to obtain the current version (before setting new), git command 'git describe --tag' is used. This will -yield version tags like '4.1.0' or '4.1.0-1-g82869e0'. - -Alternatively, pgk_resources.get_distribution (available through setuptools) could be used. This would yield version -tags like '4.1.0' or '4.0.1.dev0+g963a10f.d20190823', i.e. with slightly better description of development version. -However; information about development version is not relevant here, and also the use of pkg_resources.get_distribution -increases the risk of extracting version for another qats installation on the computer (e.g. if this script is invoked -within the wrong conda/virtual environment. - -Note: user is prompted for approval on command line before new tag is actually set. -""" -import argparse -import os -import sys -import textwrap -from pkg_resources import get_distribution - - -def get_version_setuptools(package="qats", return_dev=False): - # get version (will raise DistributionNotFound error if package is not found/installed) - version_string = get_distribution(package).version - - version = version_string.split(".", maxsplit=2) - assert len(version) == 3, f"Not able to interpret version string: {version_string}" - - # extract major, minor, micro - major, minor, micro = version - - # interpret/correct micro - if "-" in micro: - # dev info included in micro - micro, dev = micro.split(".", maxsplit=1) - else: - # pure major.minor.micro version, no dev part of tag - dev = "" - - if return_dev: - return major, minor, micro, dev - else: - return major, minor, micro - - -def get_version_git(return_dev=False): - # get version (will raise DistributionNotFound error if package is not found/installed) - version_string = os.popen("git describe --tag").read().strip() - - if version_string.startswith("fatal"): - # git failed, probably because not invoked at root of a git repo (.git not found) - raise Exception("Not able to extract version using git") - - version = version_string.split(".", maxsplit=2) - assert len(version) == 3, f"Not able to interpret version string: {version_string}" - - # extract major, minor, micro - major, minor, micro = version - - # interpret/correct micro - if "-" in micro: - # dev info included in micro - micro, dev = micro.split("-", maxsplit=1) - else: - # pure major.minor.micro version, no dev part of tag - dev = "" - - if return_dev: - return major, minor, micro, dev - else: - return major, minor, micro - - -def query_yes_no(question, default="yes"): - """Ask a yes/no question via raw_input() and return their answer. - - "question" is a string that is presented to the user. - "default" is the presumed answer if the user just hits . - It must be "yes" (the default), "no" or None (meaning - an answer is required of the user). - - The "answer" return value is True for "yes" or False for "no". - - This function is an adjusted copy of: https://stackoverflow.com/a/3041990 - """ - valid = {"yes": True, "y": True, "ye": True, - "no": False, "n": False} - - assert default is None or default in valid, f"Invalid default option: {default}" - - if default is None: - prompt = " (y/n) " - elif default == "yes": - prompt = " ([y]/n) " - elif default == "no": - prompt = " (y/[n]) " - else: - raise ValueError("invalid default answer: '%s'" % default) - - while True: - sys.stdout.write(question + prompt) - choice = input().lower() - if default is not None and choice == '': - return valid[default] - elif choice in valid: - return valid[choice] - else: - sys.stdout.write("Please respond with 'yes' or 'no' " - "(or 'y' or 'n').\n") - - -def construct_version_string(major, minor, micro, dev=None): - """ - Construct version tag: "major.minor.micro" (or if 'dev' is specified: "major.minor.micro-dev"). - """ - version_tag = f"{major}.{minor}.{micro}" - if dev is not None: - version_tag += f"-{dev}" - return version_tag - - -def main(): - parser = argparse.ArgumentParser( - description="Set new version tag using git command line interface. The tag is set by augmenting either " - "'major', 'minor' or 'micro' (specified by user) by 1.", - ) - parser.add_argument("type", choices=("major", "minor", "micro"), - help="Which part of version tag to augment by one.") - - parser.add_argument("-m", "--message", default="", help="Commit message to include. Default is empty string") - - # parser.add_argument("--test", action="store_true", - # help="Do not set tag, only print what tag would have been set.") - - args = parser.parse_args() - - # for debug and verification - # print(f"get_version_setuptools() : {get_version_setuptools()}") - # print(f"get_version_git() : {get_version_git(return_dev=True)}") - - # extract current version tag - major, minor, micro, dev = get_version_git(return_dev=True) - current_version = construct_version_string(major, minor, micro, dev=dev) - - # determine new version - if args.type == "major": - # augment major by 1, reset minor and micro - major = str(int(major) + 1) - minor = "0" - micro = "0" - elif args.type == "minor": - # augment minor by 1, reset minor - minor = str(int(minor) + 1) - micro = "0" - else: - # augment micro by 1 - micro = str(int(micro) + 1) - # finally, reset dev in any case - dev = None - - # construct new version tag - new_version = construct_version_string(major, minor, micro, dev=dev) - - # ask user whether to conduct version tag update - info_string = textwrap.dedent(f''' - Current version tag : {current_version} - New version tag : {new_version} - Commit message : {args.message} - - Set to new version tag? ''') - set_new_tag = query_yes_no(info_string, default="no") # bool - - # act according to answer from user - if set_new_tag: - # ref: https://git-scm.com/book/en/v2/Git-Basics-Tagging - _ = os.popen(f'git tag -a {new_version} -m "{args.message}"').read() - - sys.stdout.write(textwrap.dedent(f''' - New version tag ({new_version}) has been set. - - Verify the new tag by the following git command: - git describe --tag - - To reverse (delete) the new tag, use the following git command: - git tag -d {new_version} - ''')) - - else: - sys.stdout.write("\nNew tag has NOT been set.") - - -if __name__ == "__main__": - main()