diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..9e3740a3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +quantities/_version.py export-subst diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..573dfa41 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,130 @@ +name: Test + +on: [push, pull_request] + +env: + FORCE_COLOR: 1 + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest ] + python-version: [ "3.9", "3.10", "3.11", "3.12" ] + numpy-version: [ "1.22", "1.23", "1.24", "1.25", "1.26", "2.0" ] + exclude: + - python-version: "3.12" + numpy-version: "1.22" + os: ubuntu-latest + - python-version: "3.12" + numpy-version: "1.23" + os: ubuntu-latest + - python-version: "3.12" + numpy-version: "1.24" + os: ubuntu-latest + - python-version: "3.12" + numpy-version: "1.25" + os: ubuntu-latest + - python-version: "3.12" + numpy-version: "2.0" + os: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: Cache + uses: actions/cache@v4 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: + ${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.numpy-version }}-v1-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.numpy-version }}-v1- + + - name: Install dependencies + run: | + python -m pip install -U pip + python -m pip install -U setuptools + python -m pip install -U wheel + python -m pip install -U pytest + python -m pip install "numpy==${{ matrix.numpy-version }}" + + - name: Install + run: | + pip install . + + - name: Test + run: | + PY_IGNORE_IMPORTMISMATCH=1 pytest + python -m doctest README.rst + + type-check: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest ] + python-version: [ "3.9", "3.10", "3.11", "3.12" ] + numpy-version: [ "1.22", "1.23", "1.24", "1.25", "1.26", "2.0" ] + exclude: + - python-version: "3.12" + numpy-version: "1.22" + os: ubuntu-latest + - python-version: "3.12" + numpy-version: "1.23" + os: ubuntu-latest + - python-version: "3.12" + numpy-version: "1.24" + os: ubuntu-latest + - python-version: "3.12" + numpy-version: "1.25" + os: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: Cache + uses: actions/cache@v4 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: + ${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.numpy-version }}-v1-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.numpy-version }}-v1- + + - name: Install dependencies + run: | + python -m pip install -U pip + python -m pip install -U setuptools + python -m pip install -U wheel + python -m pip install "numpy==${{ matrix.numpy-version }}" + python -m pip install -U pytest + python -m pip install -U mypy + + - name: Install + run: | + pip install . + + - name: Check type information + run: | + mypy quantities diff --git a/.gitignore b/.gitignore index f05b080b..a5c99dcd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,13 @@ *.pyc quantities.egg-info build +_build dist +.project +.pydevproject +.settings +.idea +.*cache/ +_version.py +MANIFEST +.venv diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..5b39a43f --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,27 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.10" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: doc/conf.py + +# If using Sphinx, optionally build your docs in additional formats such as PDF +formats: + - pdf + +# Optionally declare the Python requirements required to build your docs +python: + install: + - requirements: doc/rtd-requirements.txt + - method: pip + path: . diff --git a/CHANGES.txt b/CHANGES.txt index 8d70bb49..3e4f48de 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,6 +2,162 @@ CHANGES ======= +------ +0.16.2 +------ + +- Added a property `dimensionless_magnitude` to the Quantity class ([`PR#248 `_]) +- Implemented an alternative approach to avoiding arbitrary evaluation of code when parsing strings as units, which fixes some bugs introduced in v0.16.1 ([`PR#251 `_]) +- Added the kilonewton + +------ +0.16.1 +------ + +- Fixed a couple of small bugs ([`PR#238 `_] and [`PR#242 `_]) +- Added umath funcs: `maximum` & `minimum` + +------ +0.16.0 +------ + +- Added support for NumPy 2.0, while maintaining support for older versions back to 1.22 [`PR#235 `_]. Many thanks to Björn Dahlgren and Zach McKenzie for this. +- Fixed a potential security hole [`PR#236 `_] +- Dropped support for Python 3.8 + +------ +0.15.0 +------ + +- Quantities now has type stubs for all classes and functions. Many thanks to Peter Konradi and Takumasa Nakamura for this major effort. +- Fixed a number of deprecations coming from NumPy (thanks to Zach McKenzie) +- Dropped support for NumPy 1.19, added testing for Numpy 1.25 and 1.26, and for Python 3.12 + +------ +0.14.1 +------ + +- Fixed a bug when scaling quantities with integer dtype [`PR#216 `_] + +------ +0.14.0 +------ + +- Added decimeter to list of supported units [`PR#202 `_] +- Removed deprecated mb as symbol for millibar unit [`PR#203 `_] +- Fixed failure to preserve dtype in rescale [`PR#204 `_] +- Added exp2 as a supported ufunc +- Fixed failure to handle units with floordiv [`PR#207 `_] +- Added femtofarad (fF) to list of supported units +- Dropped support for Python 3.7 +- Dropped support for NumPy versions older than 1.19 +- Converted the project packaging from setup.py-based to pyproject.toml-based + + +------ +0.13.0 +------ + +- Dropped support for Python versions older than 3.7, in particular, for Python 2.7. +- Dropped support for NumPy versions older than 1.16 +- Switched test runner to pytest, and CI to Github Actions + + +------ +0.12.5 +------ + +- Added preferred units support for .rescale +- Added dimensionless unit 'lsb' (least significant bit) +- Added SI multiples for Kelvin +- Fixed invalid escape sequence + +All changes +*********** + +https://github.com/python-quantities/python-quantities/issues?utf8=✓&q=is%3Aclosed+closed%3A2020-01-08..2021-08-16 + +------ +0.12.4 +------ + +- Fix broken support for `pq.Quanitty('mbar')` +- Add a `__format__` implementation for Quantity +- Fix `np.arctan2` regression due to newer numpy version +- Fix " not supported" error +- Test against Python 3.8 and NumPy 1.17 + +All changes +*********** + +https://github.com/python-quantities/python-quantities/issues?utf8=✓&q=is%3Aclosed+closed%3A2019-02-23..2020-01-08+ + +------ +0.12.3 +------ + +Updates to support NumPy up to version 1.16.1, and Python 3.7. +Added microcoulomb and millicoulomb units. + +All changes +*********** + +https://github.com/python-quantities/python-quantities/issues?utf8=✓&q=is%3Aclosed%20closed%3A2018-07-03..2019-02-22 + + +------ +0.12.2 +------ + +Added SI multiples for the byte unit (kB, MB, ...) and the IEC units (KiB, MiB...). + +All changes +*********** + +https://github.com/python-quantities/python-quantities/issues?utf8=✓&q=is%3Aclosed%20closed%3A2017-09-01..2018-07-02 + + +------ +0.12.1 +------ + +Bugs fixed +********** + +https://github.com/python-quantities/python-quantities/issues?utf8=✓&q=is%3Aclosed%20closed%3A2017-08-02..2017-08-30 + +----- +0.12.0 +----- + +Removed support for Python 2.6, since NumPy removed support for it as of +version 1.12. Numpy-1.8.2 or later is now required. + +Added more ufuncs: equal, not_equal, less, less_equal, greater, greater_equal + + +Bugs fixed +********** + +https://github.com/python-quantities/python-quantities/issues?utf8=✓&q=is%3Aissue%20is%3Aclosed%20closed%3A2015-12-06..2017-08-01 + + +----- +0.11.0 +----- + +Added many new unit definitions, including aliases for American/British spellings of liter/litre +The Quantity class can now be subclassed. +Supports `np.fabs` +The test suite is now run with Travis CI + + +Bugs fixed +********** + +https://github.com/python-quantities/python-quantities/issues?utf8=✓&q=is%3Aissue%20is%3Aclosed%20closed%3A2011-09-27..2015-12-06 + + ----- 0.10.0 ----- @@ -54,7 +210,7 @@ more robust. Bugs fixed ********** -* #1 use revolution/min instead of 1/min as definition of rpm +* #1 use revolution/min instead of 1/min as definition of rpm * #2 silently fail to import test runner if nose is not installed * #4 remove the "jiffy", as there are conflicting definitions depending on (or even within a) discipline. @@ -76,7 +232,7 @@ The log and exp functions have been removed. Quantities will work with numpy's version of these functions. Quantities development has migrated from bzr/launchpad to -git/github. Please report problems to +git/github. Please report problems to http://github.com/python-quantities/python-quantities or visit the mailing list at http://groups.google.com/group/python-quantities @@ -111,7 +267,7 @@ as well. Numpydoc, an external package developed to extend Sphinx for the numpy documentation project, is now required to build quantities' -documentation. +documentation. Bugs fixed ********** diff --git a/MANIFEST.in b/MANIFEST.in index ff7a2615..aa5ef330 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,3 @@ -include distribute_setup.py include CHANGES.txt -include py3tool.py include quantities/constants/NIST_codata.txt +exclude conda.recipe \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..12c56fd1 --- /dev/null +++ b/README.rst @@ -0,0 +1,96 @@ +========== +quantities +========== + +Quantities is designed to handle arithmetic and +conversions of physical quantities, which have a magnitude, dimensionality +specified by various units, and possibly an uncertainty. See the tutorial_ +for examples. Quantities builds on the popular numpy library and is +designed to work with numpy ufuncs, many of which are already +supported. Quantities is actively developed, and while the current features +and API are stable, test coverage is incomplete so the package is not +suggested for mission-critical applications. + +|pypi version|_ |Build status|_ + +.. |pypi version| image:: https://img.shields.io/pypi/v/quantities.png +.. _`pypi version`: https://pypi.python.org/pypi/quantities +.. |Build status| image:: https://github.com/python-quantities/python-quantities/actions/workflows/test.yml/badge.svg?branch=master +.. _`Build status`: https://github.com/python-quantities/python-quantities/actions/workflows/test.yml +.. _tutorial: http://python-quantities.readthedocs.io/en/latest/user/tutorial.html + + +A Python package for handling physical quantities. The source code and issue +tracker are hosted on GitHub: + +https://www.github.com/python-quantities/python-quantities + +Download +-------- +Get the latest version of quantities from +https://pypi.python.org/pypi/quantities/ + +To get the Git version do:: + + $ git clone git://github.com/python-quantities/python-quantities.git + + +Documentation and usage +----------------------- +You can find the official documentation at: + +http://python-quantities.readthedocs.io/ + +Here is a simple example: + +.. code:: python + + >>> import quantities as pq + >>> distance = 42*pq.metre + >>> time = 17*pq.second + >>> velocity = distance / time + >>> "%.3f %s" % (velocity.magnitude, velocity.dimensionality) + '2.471 m/s' + >>> velocity + 3 + Traceback (most recent call last): + ... + ValueError: Unable to convert between units of "dimensionless" and "m/s" + +Installation +------------ +quantities has a hard dependency on the `NumPy `_ library. +You should install it first, please refer to the NumPy installation guide: + +http://docs.scipy.org/doc/numpy/user/install.html + +To install quantities itself, then simply run:: + + $ pip install quantities + + +Tests +----- +To execute all tests, install pytest:: + + $ python -m pip install pytest + +And run:: + + $ pytest + +in the current directory. The master branch is automatically tested by +GitHub Actions. + +Author +------ +quantities was originally written by Darren Dale, and has received contributions from `many people`_. + +.. _`many people`: https://github.com/python-quantities/python-quantities/graphs/contributors + +License +------- +Quantities only uses BSD compatible code. See the Open Source +Initiative `licenses page `_ +for details on individual licenses. + +See `doc/user/license.rst `_ for further details on the license of quantities diff --git a/README.txt b/README.txt deleted file mode 100644 index 15060264..00000000 --- a/README.txt +++ /dev/null @@ -1,3 +0,0 @@ -to install quantities, run the following in the source directory: - -python setup.py install diff --git a/conda.recipe/bld.bat b/conda.recipe/bld.bat new file mode 100644 index 00000000..cf9541bd --- /dev/null +++ b/conda.recipe/bld.bat @@ -0,0 +1,5 @@ +git describe --tags --dirty > %SRC_DIR%/__conda_version__.txt +%PYTHON% %RECIPE_DIR%/format_version.py %SRC_DIR%/__conda_version__.txt + +%PYTHON% setup.py install +if errorlevel 1 exit 1 diff --git a/conda.recipe/build.sh b/conda.recipe/build.sh new file mode 100755 index 00000000..7a25c281 --- /dev/null +++ b/conda.recipe/build.sh @@ -0,0 +1,4 @@ +git describe --tags --dirty > $SRC_DIR/__conda_version__.txt +$PYTHON $RECIPE_DIR/format_version.py $SRC_DIR/__conda_version__.txt + +$PYTHON setup.py install diff --git a/conda.recipe/format_version.py b/conda.recipe/format_version.py new file mode 100644 index 00000000..b96d641c --- /dev/null +++ b/conda.recipe/format_version.py @@ -0,0 +1,8 @@ +import os +import sys + +fn = sys.argv[1] +with open(fn) as f: + s = f.read().lstrip('v').replace('-', '+', 1).replace('-', '.') +with open(fn, 'w') as f: + f.write(s) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml new file mode 100644 index 00000000..8ef6016c --- /dev/null +++ b/conda.recipe/meta.yaml @@ -0,0 +1,26 @@ +package: + name: quantities + version: master + +source: + git_url: ../ + git_tag: master + +build: + number: 0 + +requirements: + build: + - python + run: + - python + - numpy + +test: + imports: + - quantities + +about: + license: BSD + home: https://github.com/python-quantities/python-quantities/ + summary: Physical quantities with units, based upon Numpy diff --git a/conda.recipe/run_test.py b/conda.recipe/run_test.py new file mode 100644 index 00000000..3939ce71 --- /dev/null +++ b/conda.recipe/run_test.py @@ -0,0 +1,7 @@ +import os +import sys + +import unittest + +suite = unittest.TestLoader().discover('quantities') +unittest.TextTestRunner(verbosity=1).run(suite) diff --git a/doc/conf.py b/doc/conf.py index 7d8c44a5..0149cd51 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # quantities documentation build configuration file, created by # sphinx-quickstart on Sun Oct 25 09:49:05 2009. @@ -24,7 +23,7 @@ # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', - 'sphinx.ext.coverage', 'sphinx.ext.pngmath', 'sphinx.ext.ifconfig', + 'sphinx.ext.coverage', 'sphinx.ext.imgmath', 'sphinx.ext.ifconfig', 'sphinx.ext.autosummary', 'ipython_console_highlighting', 'numpydoc.numpydoc' ] @@ -41,21 +40,18 @@ master_doc = 'index' # General information about the project. -project = u'quantities' -copyright = u'2009, Darren Dale' +project = 'quantities' +copyright = '2009, Darren Dale' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # -with open('../quantities/version.py') as f: - for line in f: - if line.startswith('__version__'): - exec(line) +from quantities import __version__ # The short X.Y version. version = __version__ # The full version, including alpha/beta/rc tags. -release = version +release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -100,7 +96,7 @@ # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'sphinxdoc' +#html_theme = 'sphinxdoc' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -144,7 +140,7 @@ # Additional templates that should be rendered to pages, maps page names to # template names. -html_sidebars = {'index': 'indexsidebar.html'} +html_sidebars = {'index': ['localtoc.html', 'relations.html', 'sourcelink.html', 'indexsidebar.html', 'searchbox.html']} # If false, no module index is generated. #html_use_modindex = True @@ -181,8 +177,8 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'quantities.tex', u'quantities Documentation', - u'Darren Dale', 'manual'), + ('index', 'quantities.tex', 'quantities Documentation', + 'Darren Dale', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -204,4 +200,4 @@ # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} +intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} \ No newline at end of file diff --git a/doc/devel/devnotes.rst b/doc/devel/devnotes.rst index 6d2daec4..e48ce96b 100644 --- a/doc/devel/devnotes.rst +++ b/doc/devel/devnotes.rst @@ -5,8 +5,4 @@ Quantities development uses the principles of test-driven development. New features or bug fixes need to be accompanied by unit tests based on Python's unittest package. Unit tests can be run with the following:: - python setup.py test - -This works with the version of unittest provided by the python-2.7 and -python-3.2 standard library. If you are running python-2.6 or python-3.1, you -need to install unittest2 to run the test suite. + pytest diff --git a/doc/devel/documenting.rst b/doc/devel/documenting.rst index d7992a11..f43eb019 100644 --- a/doc/devel/documenting.rst +++ b/doc/devel/documenting.rst @@ -13,7 +13,7 @@ Sphinx extension. Sphinx-0.6.3 or later is required. You can obtain Sphinx and numpydoc from the `Python Package Index`_ or by doing:: - easy_install sphinx + pip install sphinx .. _Sphinx: http://sphinx.pocoo.org/ .. _numpydoc: http://pypi.python.org/pypi/numpydoc @@ -37,7 +37,7 @@ Organization of Quantities' documentation ========================================== The actual ReStructured Text files are kept in :file:`doc`. The main -entry point is :file:`doc/index.rst`, which pulls in the +entry point is :file:`doc/index.rst`, which pulls in the :file:`index.rst` file for the user guide and the developers guide. The documentation suite is built as a single document in order to make the most effective use of cross referencing, we want to make @@ -257,7 +257,7 @@ and refer to it using the standard reference syntax:: Keep in mind that we may want to reorganize the contents later, so let's avoid top level names in references like ``user`` or ``devel`` -or ``faq`` unless necesssary, because for example the FAQ "what is a +or ``faq`` unless necessary, because for example the FAQ "what is a backend?" could later become part of the users guide, so the label:: .. _what-is-a-backend @@ -286,7 +286,7 @@ Emacs helpers There is an emacs mode `rst.el `_ which -automates many important ReST tasks like building and updateing +automates many important ReST tasks like building and updating table-of-contents, and promoting or demoting section headings. Here is the basic ``.emacs`` configuration:: @@ -314,4 +314,3 @@ Some helpful functions:: C-c C-r rst-shift-region-right Shift region to the right - diff --git a/doc/devel/release.rst b/doc/devel/release.rst index bf9a6520..f4340430 100644 --- a/doc/devel/release.rst +++ b/doc/devel/release.rst @@ -5,12 +5,14 @@ Releases Creating Source Releases ======================== -Quantities is distributed as a source release for Linux and OS-X. To create a +Quantities is distributed as a source release for Linux and Mac OS. To create a source release, just do:: - python setup.py register - python setup.py sdist --formats=zip,gztar upload --sign + pip install build + python -m build + twine upload dist/quantities-.* +(replacing `x`, `y` and `z` appropriately). This will create the tgz source file and upload it to the Python Package Index. Uploading to PyPi requires a .pypirc file in your home directory, something like:: @@ -21,30 +23,19 @@ like:: You can create a source distribution without uploading by doing:: - python setup.py sdist + python -m build --sdist This creates a source distribution in the `dist/` directory. -Creating Windows Installers -=========================== - -We distribute binary installers for the windows platform. In order to build the -windows installer, open a DOS window, cd into the quantities source directory -and run:: - - python setup.py build - python setup.py bdist_msi - -This creates the executable windows installer in the `dist/` directory. - - Building Quantities documentation ================================= -When publishing a new release, the Quantities doumentation needs to be generated -and published as well. Sphinx_, LaTeX_ (preferably `TeX-Live`_), and dvipng_ are -required to build the documentation. Once these are installed, do:: +The Quantities documentation is automatically built on readthedocs.io. + +Should you need to build the documentation locally, +Sphinx_, LaTeX_ (preferably `TeX-Live`_), and dvipng_ are +required. Once these are installed, do:: cd doc make html @@ -56,15 +47,7 @@ which will produce the html output and save it in build/sphinx/html. Then run:: make all-pdf cp Quantities.pdf ../html -which will generate a pdf file in the latex directory. Finally, upload the html -content to the http://packages.python.org/quantities/ webserver. To do so:: - - cd build/html - zip -r quantities * - -and then visit `the Quantities project page -`_ at the Python Package Index to -upload the zip archive. +which will generate a pdf file in the latex directory. .. _Sphinx: http://sphinx.pocoo.org/ .. _LaTeX: http://www.latex-project.org/ diff --git a/doc/rtd-requirements.txt b/doc/rtd-requirements.txt new file mode 100644 index 00000000..a2954e31 --- /dev/null +++ b/doc/rtd-requirements.txt @@ -0,0 +1 @@ +numpydoc diff --git a/doc/sphinxext/ipython_console_highlighting.py b/doc/sphinxext/ipython_console_highlighting.py index 217b779d..e6e34f52 100644 --- a/doc/sphinxext/ipython_console_highlighting.py +++ b/doc/sphinxext/ipython_console_highlighting.py @@ -52,10 +52,10 @@ class IPythonConsoleLexer(Lexer): name = 'IPython console session' aliases = ['ipython'] mimetypes = ['text/x-ipython-console'] - input_prompt = re.compile("(In \[[0-9]+\]: )|( \.\.\.+:)") - output_prompt = re.compile("(Out\[[0-9]+\]: )|( \.\.\.+:)") - continue_prompt = re.compile(" \.\.\.+:") - tb_start = re.compile("\-+") + input_prompt = re.compile(r"(In \[[0-9]+\]: )|( \.\.\.+:)") + output_prompt = re.compile(r"(Out\[[0-9]+\]: )|( \.\.\.+:)") + continue_prompt = re.compile(r" \.\.\.+:") + tb_start = re.compile(r"\-+") def get_tokens_unprocessed(self, text): pylexer = PythonLexer(**self.options) diff --git a/doc/user/installation.rst b/doc/user/installation.rst index 9896442d..f3c40c96 100644 --- a/doc/user/installation.rst +++ b/doc/user/installation.rst @@ -8,18 +8,17 @@ Prerequisites Quantities has a few dependencies: -* Python_ (>=2.6) -* NumPy_ (>=1.4) +* Python_ (>=3.8) +* NumPy_ (>=1.19) -If you want to run the test suite on python-2.6 or python-3.1, install the -unittest2_ or unittest2py3k_ package, respectively. +(bearing in mind that not all combinations of Python and NumPy versions necessarily work). Source Code Installation ======================== -To install Quantities, download the Quantites sourcecode from PyPi_ -and run "python setup.py install" in the quantities source directory. +To install Quantities, run "pip install quantities". + Development =========== @@ -28,13 +27,11 @@ You can follow and contribute to Quantities' development using git:: git clone git@github.com:python-quantities/python-quantities.git -Bugs, feature requests, and questions can be directed to the github_ +Bugs, feature requests, and questions can be directed to the GitHub_ website. .. _Python: http://www.python.org/ .. _NumPy: http://www.scipy.org -.. _PyPi: http://pypi.python.org/pypi/quantities -.. _unittest2: http://pypi.python.org/pypi/unittest2 -.. _unittest2py3k: http://pypi.python.org/pypi/unittest2py3k -.. _github: http://github.com/python-quantities/python-quantities +.. _PyPI: http://pypi.python.org/pypi/quantities +.. _GitHub: http://github.com/python-quantities/python-quantities diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..ca1193ec --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,52 @@ +[project] +name = "quantities" +description = "Support for physical quantities with units, based on numpy" +readme = "README.rst" +requires-python = ">=3.9" +license = {file = "doc/user/license.rst"} +authors = [ + {name = "Darren Dale", email = "dsdale24@gmail.com"} +] +maintainers = [ + {name = "Andrew Davison", email = "andrew.davison@cnrs.fr"} +] +keywords = ["quantities", "units", "physical", "constants"] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Topic :: Education", + "Topic :: Scientific/Engineering" +] +dependencies = [ + "numpy>=1.20" +] +dynamic = ["version"] + +[project.optional-dependencies] +test = [ + "pytest", + "wheel" +] +doc = [ + "sphinx" +] + +[project.urls] +documentation = "http://python-quantities.readthedocs.io/" +repository = "https://github.com/python-quantities/python-quantities" +changelog = "https://github.com/python-quantities/python-quantities/blob/master/CHANGES.txt" +download = "http://pypi.python.org/pypi/quantities" + +[build-system] +requires = ["setuptools", "setuptools_scm[toml]"] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +write_to = "quantities/_version.py" diff --git a/quantities/__init__.py b/quantities/__init__.py index a8ea5e38..d534d589 100644 --- a/quantities/__init__.py +++ b/quantities/__init__.py @@ -213,24 +213,24 @@ >>> print pq.constants.proton_mass.simplified 1.672621637e-27 kg +/-8.3e-35 kg (1 sigma) - + A Latex representation of the dimensionality may be obtained in the following fashion:: >>> g = pq.Quantity(9.80665,'m/s**2') >>> mass = 50 * pq.kg >>> weight = mass*g >>> print weight.dimensionality.latex - $\mathrm{\\frac{kg{\\cdot}m}{s^{2}}}$ + $\\mathrm{\\frac{kg{\\cdot}m}{s^{2}}}$ >>> weight.units = pq.N >>> print weight.dimensionality.latex $\\mathrm{N}$ -The Latex output is compliant with the MathText subset used by Matplotlib. To add +The Latex output is compliant with the MathText subset used by Matplotlib. To add formatted units to the axis label of a Matplotlib figure, one could use:: >>> ax.set_ylabel('Weight ' + weight.dimensionality.latex) - -Greater customization is available via the markup.format_units_latex function. It allows + +Greater customization is available via the markup.format_units_latex function. It allows the user to modify the font, the multiplication symbol, or to encapsulate the latex string in parentheses. Due to the complexity of CompoundUnits, the latex rendering of CompoundUnits will utilize the latex \\frac{num}{den} construct. @@ -265,9 +265,10 @@ """ -from __future__ import absolute_import +class QuantitiesDeprecationWarning(DeprecationWarning): + pass -from .version import __version__ +from ._version import __version__ from .registry import unit_registry @@ -285,3 +286,8 @@ from . import constants from .umath import * + +def test(verbosity=1): + import unittest + suite = unittest.TestLoader().discover('quantities') + unittest.TextTestRunner(verbosity=verbosity).run(suite) diff --git a/quantities/constants/__init__.py b/quantities/constants/__init__.py index b79ac170..9aaa9b36 100644 --- a/quantities/constants/__init__.py +++ b/quantities/constants/__init__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from .alpha import * from .astronomy import * from .atomicunits import * diff --git a/quantities/constants/_utils.py b/quantities/constants/_utils.py index 7fe4da02..e14f4cb9 100644 --- a/quantities/constants/_utils.py +++ b/quantities/constants/_utils.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from ._codata import physical_constants from quantities.quantity import Quantity from quantities.uncertainquantity import UncertainQuantity diff --git a/quantities/constants/alpha.py b/quantities/constants/alpha.py index 6840f0f8..e3a1991b 100644 --- a/quantities/constants/alpha.py +++ b/quantities/constants/alpha.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ._utils import _cd from ..unitquantity import UnitConstant diff --git a/quantities/constants/astronomy.py b/quantities/constants/astronomy.py index de350e26..ba64a975 100644 --- a/quantities/constants/astronomy.py +++ b/quantities/constants/astronomy.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ._utils import _cd from ..uncertainquantity import UncertainQuantity diff --git a/quantities/constants/atomicunits.py b/quantities/constants/atomicunits.py index 857ecac1..8e53e93c 100644 --- a/quantities/constants/atomicunits.py +++ b/quantities/constants/atomicunits.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ._utils import _cd from ..unitquantity import UnitConstant diff --git a/quantities/constants/deuteron.py b/quantities/constants/deuteron.py index 925f8e22..a6846a06 100644 --- a/quantities/constants/deuteron.py +++ b/quantities/constants/deuteron.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ._utils import _cd from ..unitquantity import UnitConstant diff --git a/quantities/constants/electromagnetism.py b/quantities/constants/electromagnetism.py index f2b3a09c..9aca2fdc 100644 --- a/quantities/constants/electromagnetism.py +++ b/quantities/constants/electromagnetism.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ._utils import _cd from ..unitquantity import UnitConstant diff --git a/quantities/constants/electron.py b/quantities/constants/electron.py index 4fbc8885..865862d2 100644 --- a/quantities/constants/electron.py +++ b/quantities/constants/electron.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ._utils import _cd from ..unitquantity import UnitConstant diff --git a/quantities/constants/helion.py b/quantities/constants/helion.py index bb28c8b9..dcc23a00 100644 --- a/quantities/constants/helion.py +++ b/quantities/constants/helion.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ._utils import _cd from ..unitquantity import UnitConstant diff --git a/quantities/constants/mathematical.py b/quantities/constants/mathematical.py index 5aae3fa7..2c97069c 100644 --- a/quantities/constants/mathematical.py +++ b/quantities/constants/mathematical.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import math as _math from ..unitquantity import UnitConstant @@ -14,5 +12,5 @@ 'golden_ratio', (1 + _math.sqrt(5)) / 2, u_symbol='ϕ', - aliases='golden' + aliases=['golden'] ) diff --git a/quantities/constants/muon.py b/quantities/constants/muon.py index 2dc97c8f..cf81fae3 100644 --- a/quantities/constants/muon.py +++ b/quantities/constants/muon.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ._utils import _cd from ..unitquantity import UnitConstant diff --git a/quantities/constants/naturalunits.py b/quantities/constants/naturalunits.py index 2b480127..41473912 100644 --- a/quantities/constants/naturalunits.py +++ b/quantities/constants/naturalunits.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ._utils import _cd from ..unitquantity import UnitConstant diff --git a/quantities/constants/neutron.py b/quantities/constants/neutron.py index dee1f545..84d5565d 100644 --- a/quantities/constants/neutron.py +++ b/quantities/constants/neutron.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ._utils import _cd from ..unitquantity import UnitConstant diff --git a/quantities/constants/proton.py b/quantities/constants/proton.py index 4b9bed7f..1700c303 100644 --- a/quantities/constants/proton.py +++ b/quantities/constants/proton.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ._utils import _cd from ..unitquantity import UnitConstant diff --git a/quantities/constants/quantum.py b/quantities/constants/quantum.py index 8f9650ca..b98c1f7e 100644 --- a/quantities/constants/quantum.py +++ b/quantities/constants/quantum.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ._utils import _cd from ..unitquantity import UnitConstant diff --git a/quantities/constants/relationships.py b/quantities/constants/relationships.py index 9a7916ed..7d90227a 100644 --- a/quantities/constants/relationships.py +++ b/quantities/constants/relationships.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ._utils import _cd from ..unitquantity import UnitConstant diff --git a/quantities/constants/statisticalmechanics.py b/quantities/constants/statisticalmechanics.py index 2d545cda..b3cbf050 100644 --- a/quantities/constants/statisticalmechanics.py +++ b/quantities/constants/statisticalmechanics.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ._utils import _cd from ..unitquantity import UnitConstant diff --git a/quantities/constants/tau.py b/quantities/constants/tau.py index 35b8479b..fdbe59af 100644 --- a/quantities/constants/tau.py +++ b/quantities/constants/tau.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ._utils import _cd from ..unitquantity import UnitConstant diff --git a/quantities/constants/triton.py b/quantities/constants/triton.py index e2a4f0d7..ba50eb5c 100644 --- a/quantities/constants/triton.py +++ b/quantities/constants/triton.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ._utils import _cd from ..unitquantity import UnitConstant diff --git a/quantities/constants/weak.py b/quantities/constants/weak.py index ac9424c3..225ab9b9 100644 --- a/quantities/constants/weak.py +++ b/quantities/constants/weak.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ._utils import _cd from ..unitquantity import UnitConstant diff --git a/quantities/constants/xray.py b/quantities/constants/xray.py index 5a5a866c..e2f3bbe2 100644 --- a/quantities/constants/xray.py +++ b/quantities/constants/xray.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ._utils import _cd from ..unitquantity import UnitConstant diff --git a/quantities/decorators.py b/quantities/decorators.py index 3cd2ec1f..bd4d0e2b 100644 --- a/quantities/decorators.py +++ b/quantities/decorators.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - import inspect import os import re @@ -46,10 +43,10 @@ def __call__(self, new_method): if original_doc and new_doc: new_method.__doc__ = """ - %s - %s - %s - """ % (original_doc, header, new_doc) + {} + {} + {} + """.format(original_doc, header, new_doc) elif original_doc: new_method.__doc__ = original_doc @@ -99,7 +96,7 @@ def wrapped_function(*args , **kwargs): #test if the argument is a quantity if isinstance(args[i], Quantity): #convert the units to the base units - args[i] = args[i].simplified() + args[i] = args[i].simplified #view the array as an ndarray args[i] = args[i].magnitude @@ -107,7 +104,7 @@ def wrapped_function(*args , **kwargs): #convert the list back to a tuple so it can be used as an output args = tuple (args) - #repalce all the quantities in the keyword argument + #replace all the quantities in the keyword argument #dictionary with ndarrays for i in kwargs: #test if the argument is a quantity @@ -142,7 +139,7 @@ def wrapped_function(*args , **kwargs): result[i] = Quantity( result[i], handler_quantities[i] - .dimensionality.simplified() + .dimensionality.simplified ) #now convert the quantity to the appropriate units result[i] = result[i].rescale( diff --git a/quantities/dimensionality.py b/quantities/dimensionality.py index abec291a..c00190bb 100644 --- a/quantities/dimensionality.py +++ b/quantities/dimensionality.py @@ -1,9 +1,6 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import -import sys import operator import numpy as np @@ -12,12 +9,15 @@ from .registry import unit_registry from .decorators import memoize +_np_version = tuple(map(int, np.__version__.split(".dev")[0].split("."))) + + def assert_isinstance(obj, types): try: assert isinstance(obj, types) except AssertionError: raise TypeError( - "arg %r must be of type %r, got %r" % (obj, types, type(obj)) + f"arg {obj!r} must be of type {types!r}, got {type(obj)!r}" ) @@ -47,11 +47,15 @@ def string(self): @property def unicode(self): return markup.format_units_unicode(self) - + @property def latex(self): return markup.format_units_latex(self) + @property + def html(self): + return markup.format_units_html(self) + def __hash__(self): res = hash(unit_registry['dimensionless']) for key in sorted(self.keys(), key=operator.attrgetter('format_order')): @@ -145,11 +149,6 @@ def __truediv__(self, other): new[unit] = -power return new - if sys.version_info[0] < 3: - def __div__(self, other): - assert_isinstance(other, Dimensionality) - return self.__truediv__(other) - def __itruediv__(self, other): assert_isinstance(other, Dimensionality) for unit, power in other.items(): @@ -161,11 +160,6 @@ def __itruediv__(self, other): self[unit] = -power return self - if sys.version_info[0] < 3: - def __idiv__(self, other): - assert_isinstance(other, Dimensionality) - return self.__itruediv__(other) - def __pow__(self, other): try: assert np.isscalar(other) @@ -247,6 +241,7 @@ def _d_divide(q1, q2, out=None): return q2.dimensionality**-1 p_dict[np.divide] = _d_divide p_dict[np.true_divide] = _d_divide +p_dict[np.floor_divide] = _d_divide def _d_check_uniform(q1, q2, out=None): try: @@ -282,9 +277,27 @@ def _d_check_uniform(q1, q2, out=None): p_dict[np.mod] = _d_check_uniform p_dict[np.fmod] = _d_check_uniform p_dict[np.remainder] = _d_check_uniform -p_dict[np.floor_divide] = _d_check_uniform -p_dict[np.arctan2] = _d_check_uniform p_dict[np.hypot] = _d_check_uniform +p_dict[np.equal] = _d_check_uniform +p_dict[np.not_equal] = _d_check_uniform +p_dict[np.less] = _d_check_uniform +p_dict[np.less_equal] = _d_check_uniform +p_dict[np.greater] = _d_check_uniform +p_dict[np.greater_equal] = _d_check_uniform +p_dict[np.maximum] = _d_check_uniform +p_dict[np.minimum] = _d_check_uniform + +def _d_arctan2(q1, q2, out=None): + try: + assert q1._dimensionality == q2._dimensionality + return Dimensionality() + except AssertionError: + raise ValueError( + 'quantities must have identical units, got "%s" and "%s"' % + (q1.units, q2.units) + ) + +p_dict[np.arctan2] = _d_arctan2 def _d_power(q1, q2, out=None): if getattr(q2, 'dimensionality', None): @@ -309,6 +322,7 @@ def _d_reciprocal(q1, out=None): def _d_copy(q1, out=None): return q1.dimensionality +p_dict[np.fabs] = _d_copy p_dict[np.absolute] = _d_copy p_dict[np.conjugate] = _d_copy p_dict[np.negative] = _d_copy @@ -318,6 +332,14 @@ def _d_copy(q1, out=None): p_dict[np.fix] = _d_copy p_dict[np.ceil] = _d_copy +def _d_clip(a1, a2, a3, q): + return q.dimensionality + +if _np_version < (2, 0, 0): + p_dict[np.core.umath.clip] = _d_clip +else: + p_dict[np.clip] = _d_clip + def _d_sqrt(q1, out=None): return q1._dimensionality**0.5 p_dict[np.sqrt] = _d_sqrt @@ -351,6 +373,7 @@ def _d_dimensionless(q1, out=None): p_dict[np.log2] = _d_dimensionless p_dict[np.log1p] = _d_dimensionless p_dict[np.exp] = _d_dimensionless +p_dict[np.exp2] = _d_dimensionless p_dict[np.expm1] = _d_dimensionless p_dict[np.logaddexp] = _d_dimensionless p_dict[np.logaddexp2] = _d_dimensionless diff --git a/quantities/dimensionality.pyi b/quantities/dimensionality.pyi new file mode 100644 index 00000000..f9443061 --- /dev/null +++ b/quantities/dimensionality.pyi @@ -0,0 +1,85 @@ +class Dimensionality(dict): + @property + def ndims(self) -> int: + ... + + @property + def simplified(self) -> Dimensionality: + ... + + @property + def string(self) -> str: + ... + + @property + def unicode(self) -> str: + ... + + @property + def latex(self) -> str: + ... + + @property + def html(self) -> str: + ... + + + def __hash__(self) -> int: # type: ignore[override] + ... + + def __add__(self, other: Dimensionality) -> Dimensionality: + ... + + def __iadd__(self, other: Dimensionality) -> Dimensionality: + ... + + def __sub__(self, other: Dimensionality) -> Dimensionality: + ... + + def __isub__(self, other: Dimensionality) -> Dimensionality: + ... + + def __mul__(self, other: Dimensionality) -> Dimensionality: + ... + + def __imul__(self, other: Dimensionality) -> Dimensionality: + ... + + def __truediv__(self, other: Dimensionality) -> Dimensionality: + ... + + def __itruediv__(self, other: Dimensionality) -> Dimensionality: + ... + + def __pow__(self, other : Dimensionality) -> Dimensionality: + ... + + def __ipow__(self, other: Dimensionality) -> Dimensionality: + ... + + def __repr__(self) -> str: + ... + + def __str__(self) -> str: + ... + + def __eq__(self, other: object) -> bool: + ... + + def __ne__(self, other: object) -> bool: + ... + + def __gt__(self, other: Dimensionality) -> bool: + ... + + def __ge__(self, other: Dimensionality) -> bool: + ... + + def __lt__(self, other: Dimensionality) -> bool: + ... + + def __le__(self, other: Dimensionality)-> bool: + ... + + def copy(self) -> Dimensionality: + ... diff --git a/quantities/markup.py b/quantities/markup.py index 88872b8d..d8d42842 100644 --- a/quantities/markup.py +++ b/quantities/markup.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import, with_statement import copy import operator @@ -9,7 +7,7 @@ import threading -class _Config(object): +class _Config: @property def lock(self): @@ -32,7 +30,6 @@ def __init__(self): superscripts = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'] def superscript(val): - # TODO: use a regexp: items = re.split(r'\*{2}([\d]+)(?!\.)', val) ret = [] while items: @@ -52,7 +49,7 @@ def format_units(udict): den = [] keys = [k for k, o in sorted( - [(k, k.format_order) for k in udict], + ((k, k.format_order) for k in udict), key=operator.itemgetter(1) ) ] @@ -87,30 +84,30 @@ def format_units_unicode(udict): return res -def format_units_latex(udict,font='mathrm',mult=r'\cdot',paren=False): +def format_units_latex(udict,font='mathrm',mult=r'\\cdot',paren=False): ''' Replace the units string provided with an equivalent latex string. - + Division (a/b) will be replaced by \frac{a}{b}. - + Exponentiation (m**2) will be replaced with superscripts (m^{2}) - - The latex is set with the font argument, and the default is the normal, - non-italicized font mathrm. Other useful options include 'mathnormal', + + The latex is set with the font argument, and the default is the normal, + non-italicized font mathrm. Other useful options include 'mathnormal', 'mathit', 'mathsf', and 'mathtt'. - + Multiplication (*) are replaced with the symbol specified by the mult argument. - By default this is the latex \cdot symbol. Other useful + By default this is the latex \\cdot symbol. Other useful options may be '' or '*'. - - If paren=True, encapsulate the string in '\left(' and '\right)' - + + If paren=True, encapsulate the string in '\\left(' and '\\right)' + The result of format_units_latex is encapsulated in $. This allows the result to be used directly in Latex in normal text mode, or in Matplotlib text via the MathText feature. - + Restrictions: - This routine will not put CompoundUnits into a fractional form. + This routine will not put CompoundUnits into a fractional form. ''' res = format_units(udict) if res.startswith('(') and res.endswith(')'): @@ -127,5 +124,38 @@ def format_units_latex(udict,font='mathrm',mult=r'\cdot',paren=False): res = re.sub(r'\*','{'+mult+'}',res) if paren and not compound: res = r'\left(%s\right)' % res - res = r'$\%s{%s}$' % (font,res) + res = fr'$\{font}{{{res}}}$' + return res + + +def format_units_html(udict,font='%s',mult=r'⋅',paren=False): + ''' + Replace the units string provided with an equivalent html string. + + Exponentiation (m**2) will be replaced with superscripts (m2}) + + No formating is done, change `font` argument to e.g.: + '%s' to have text be colored blue. + + Multiplication (*) are replaced with the symbol specified by the mult + argument. By default this is the latex ⋅ symbol. Other useful options + may be '' or '*'. + + If paren=True, encapsulate the string in '(' and ')' + + ''' + res = format_units(udict) + if res.startswith('(') and res.endswith(')'): + # Compound Unit + compound = True + else: + # Not a compound unit + compound = False + # Replace exponentiation (**exp) with ^{exp} + res = re.sub(r'\*{2,2}(?P\d+)',r'\g',res) + # Remove multiplication signs + res = re.sub(r'\*',mult,res) + if paren and not compound: + res = '(%s)' % res + res = font % res return res diff --git a/quantities/quantity.py b/quantities/quantity.py index d0b3cfce..74a425f5 100644 --- a/quantities/quantity.py +++ b/quantities/quantity.py @@ -1,20 +1,20 @@ """ """ -from __future__ import absolute_import import copy from functools import wraps -import sys +import warnings import numpy as np -from . import markup +from . import markup, QuantitiesDeprecationWarning from .dimensionality import Dimensionality, p_dict from .registry import unit_registry from .decorators import with_doc -if sys.version.startswith('3'): - unicode = str +PREFERRED = [] # List of preferred quantities for each symbol, + # e.g. PREFERRED = [pq.mV, pq.pA, pq.UnitQuantity('femtocoulomb', 1e-15*pq.C, 'fC')] + # Intended to be overwritten in down-stream packages def validate_unit_quantity(value): try: @@ -29,7 +29,7 @@ def validate_unit_quantity(value): return value def validate_dimensionality(value): - if isinstance(value, (str, unicode)): + if isinstance(value, str): try: return unit_registry[value].dimensionality except (KeyError, UnicodeDecodeError): @@ -60,7 +60,7 @@ def g(self, other, *args): if not isinstance(other, Quantity): other = other.view(type=Quantity) if other._dimensionality != self._dimensionality: - other = other.rescale(self.units) + other = other.rescale(self.units, dtype=np.result_type(self.dtype, other.dtype)) return f(self, other, *args) return g @@ -115,15 +115,19 @@ class Quantity(np.ndarray): # TODO: what is an appropriate value? __array_priority__ = 21 - def __new__(cls, data, units='', dtype=None, copy=True): - if isinstance(data, cls): + def __new__(cls, data, units='', dtype=None, copy=None): + if copy is not None: + warnings.warn(("The 'copy' argument in Quantity is deprecated and will be removed in the future. " + "The argument has no effect since quantities-0.16.0 (to aid numpy-2.0 support)."), + QuantitiesDeprecationWarning, stacklevel=2) + if isinstance(data, Quantity): if units: data = data.rescale(units) if isinstance(data, unit_registry['UnitQuantity']): return 1*data - return np.array(data, dtype=dtype, copy=copy, subok=True) + return np.asanyarray(data, dtype=dtype).view(cls) - ret = np.array(data, dtype=dtype, copy=copy).view(cls) + ret = np.asarray(data, dtype=dtype).view(cls) ret._dimensionality.update(validate_dimensionality(units)) return ret @@ -141,8 +145,34 @@ def _reference(self): @property def magnitude(self): + """ + Returns a view onto the numerical value of the quantity, stripping + away the associated units. For example: + ``` + import quantities as pq + t = 2 * pq.millisecond + n = t.magnitude # n will be 2 (not 0.002) + ``` + See also: dimensionless_magnitude. + """ return self.view(type=np.ndarray) + @property + def real(self): + return Quantity(self.magnitude.real, self.dimensionality) + + @real.setter + def real(self, r): + self.magnitude.real = Quantity(r, self.dimensionality).magnitude + + @property + def imag(self): + return Quantity(self.magnitude.imag, self.dimensionality) + + @imag.setter + def imag(self, i): + self.magnitude.imag = Quantity(i, self.dimensionality).magnitude + @property def simplified(self): rq = 1*unit_registry['dimensionless'] @@ -179,15 +209,24 @@ def units(self, units): mag *= cf self._dimensionality = to_u.dimensionality - def rescale(self, units): + def rescale(self, units=None, dtype=None): """ - Return a copy of the quantity converted to the specified units + Return a copy of the quantity converted to the specified units. + If `units` is `None`, an attempt will be made to rescale the quantity + to preferred units (see `rescale_preferred`). """ + if units is None: + try: + return self.rescale_preferred() + except Exception as e: + raise Exception('No argument passed to `.rescale` and %s' % e) to_dims = validate_dimensionality(units) + if dtype is None: + dtype = self.dtype if self.dimensionality == to_dims: - return self.astype(self.dtype) - to_u = Quantity(1.0, to_dims) - from_u = Quantity(1.0, self.dimensionality) + return self.astype(dtype) + to_u = Quantity(1.0, to_dims, dtype=dtype) + from_u = Quantity(1.0, self.dimensionality, dtype=dtype) try: cf = get_conversion_factor(from_u, to_u) except AssertionError: @@ -195,19 +234,60 @@ def rescale(self, units): 'Unable to convert between units of "%s" and "%s"' %(from_u._dimensionality, to_u._dimensionality) ) - return Quantity(cf*self.magnitude, to_u) + if np.dtype(dtype).kind in 'fc': + cf = np.array(cf, dtype=dtype) + new_magnitude = cf*self.magnitude + dtype = np.result_type(dtype, new_magnitude) + return Quantity(new_magnitude, to_u, dtype=dtype) + + def rescale_preferred(self): + """ + Return a copy of the quantity converted to the preferred units and scale. + These will be identified from among the compatible units specified in the + list PREFERRED in this module. For example, a voltage quantity might be + converted to `mV`: + ``` + import quantities as pq + pq.quantity.PREFERRED = [pq.mV, pq.pA] + old = 3.1415 * pq.V + new = old.rescale_preferred() # `new` will be 3141.5 mV. + ``` + """ + units_str = str(self.simplified.dimensionality) + for preferred in PREFERRED: + if units_str == str(preferred.simplified.dimensionality): + return self.rescale(preferred) + raise Exception("Preferred units for '%s' (or equivalent) not specified in " + "quantites.quantity.PREFERRED." % self.dimensionality) + + @property + def dimensionless_magnitude(self): + """ + Returns the numerical value of a dimensionless quantity in the form of + a numpy array. Any decimal prefixes are normalized away first. + For example: + ``` + import quantities as pq + t = 2 * pq.ms + f = 3 * pq.MHz + n = (t*f).dimensionless_magnitude # n will be 6000 (not 6) + ``` + If the quantity is not dimensionless, a conversion error is raised. + See also: magnitude. + """ + return self.rescale(unit_registry['dimensionless']).magnitude @with_doc(np.ndarray.astype) - def astype(self, dtype=None): + def astype(self, dtype=None, **kwargs): '''Scalars are returned as scalar Quantity arrays.''' - ret = super(Quantity, self.view(Quantity)).astype(dtype) + ret = super(Quantity, self.view(Quantity)).astype(dtype, **kwargs) # scalar quantities get converted to plain numbers, so we fix it # might be related to numpy ticket # 826 if not isinstance(ret, type(self)): if self.__array_priority__ >= Quantity.__array_priority__: - ret = type(self)(ret, self._dimensionality) + ret = type(self)(ret, self._dimensionality, dtype=self.dtype) else: - ret = Quantity(ret, self._dimensionality) + ret = Quantity(ret, self._dimensionality, dtype=self.dtype) return ret @@ -226,105 +306,105 @@ def __array_prepare__(self, obj, context=None): uf, objs, huh = context if uf.__name__.startswith('is'): return obj - #print self, obj, res, uf, objs + try: res._dimensionality = p_dict[uf](*objs) except KeyError: raise ValueError( """ufunc %r not supported by quantities please file a bug report at https://github.com/python-quantities - """ + """ % uf ) return res - def __array_wrap__(self, obj, context=None): - if not isinstance(obj, Quantity): - # backwards compatibility with numpy-1.3 - obj = self.__array_prepare__(obj, context) - return obj + def __array_wrap__(self, obj, context=None, return_scalar=False): + _np_version = tuple(map(int, np.__version__.split(".dev")[0].split("."))) + # For NumPy < 2.0 we do old behavior + if _np_version < (2, 0, 0): + if not isinstance(obj, Quantity): + return self.__array_prepare__(obj, context) + else: + return obj + # For NumPy > 2.0 we either do the prepare or the wrap + else: + if not isinstance(obj, Quantity): + return self.__array_prepare__(obj, context) + else: + return super().__array_wrap__(obj, context, return_scalar) + @with_doc(np.ndarray.__add__) @scale_other_units def __add__(self, other): - return super(Quantity, self).__add__(other) + return super().__add__(other) @with_doc(np.ndarray.__radd__) @scale_other_units def __radd__(self, other): return np.add(other, self) - return super(Quantity, self).__radd__(other) + return super().__radd__(other) @with_doc(np.ndarray.__iadd__) @scale_other_units def __iadd__(self, other): - return super(Quantity, self).__iadd__(other) + return super().__iadd__(other) @with_doc(np.ndarray.__sub__) @scale_other_units def __sub__(self, other): - return super(Quantity, self).__sub__(other) + return super().__sub__(other) @with_doc(np.ndarray.__rsub__) @scale_other_units def __rsub__(self, other): return np.subtract(other, self) - return super(Quantity, self).__rsub__(other) + return super().__rsub__(other) @with_doc(np.ndarray.__isub__) @scale_other_units def __isub__(self, other): - return super(Quantity, self).__isub__(other) + return super().__isub__(other) @with_doc(np.ndarray.__mod__) @scale_other_units def __mod__(self, other): - return super(Quantity, self).__mod__(other) + return super().__mod__(other) @with_doc(np.ndarray.__imod__) @scale_other_units def __imod__(self, other): - return super(Quantity, self).__imod__(other) + return super().__imod__(other) @with_doc(np.ndarray.__imul__) @protected_multiplication def __imul__(self, other): - return super(Quantity, self).__imul__(other) + return super().__imul__(other) @with_doc(np.ndarray.__rmul__) def __rmul__(self, other): return np.multiply(other, self) - return super(Quantity, self).__rmul__(other) + return super().__rmul__(other) @with_doc(np.ndarray.__itruediv__) @protected_multiplication def __itruediv__(self, other): - return super(Quantity, self).__itruediv__(other) + return super().__itruediv__(other) @with_doc(np.ndarray.__rtruediv__) def __rtruediv__(self, other): return np.true_divide(other, self) - return super(Quantity, self).__rtruediv__(other) - - if sys.version_info[0] < 3: - @with_doc(np.ndarray.__idiv__) - @protected_multiplication - def __idiv__(self, other): - return super(Quantity, self).__itruediv__(other) - - @with_doc(np.ndarray.__rdiv__) - def __rdiv__(self, other): - return np.divide(other, self) + return super().__rtruediv__(other) @with_doc(np.ndarray.__pow__) @check_uniform def __pow__(self, other): - return super(Quantity, self).__pow__(other) + return np.power(self, other) @with_doc(np.ndarray.__ipow__) @check_uniform @protected_power def __ipow__(self, other): - return super(Quantity, self).__ipow__(other) + return super().__ipow__(other) def __round__(self, decimals=0): return np.around(self, decimals) @@ -343,9 +423,19 @@ def __str__(self): dims = self.dimensionality.string return '%s %s'%(str(self.magnitude), dims) + if tuple(map(int, np.__version__.split('.')[:2])) >= (1, 14): + # in numpy 1.14 the formatting of scalar values was changed + # see https://github.com/numpy/numpy/pull/9883 + + def __format__(self, format_spec): + ret = super().__format__(format_spec) + if self.ndim: + return ret + return ret + f' {self.dimensionality}' + @with_doc(np.ndarray.__getitem__) def __getitem__(self, key): - ret = super(Quantity, self).__getitem__(key) + ret = super().__getitem__(key) if isinstance(ret, Quantity): return ret else: @@ -356,7 +446,9 @@ def __setitem__(self, key, value): if not isinstance(value, Quantity): value = Quantity(value) if self._dimensionality != value._dimensionality: - value = value.rescale(self._dimensionality) + # Setting `dtype` to 'd' is done to ensure backwards + # compatibility, arguably it's questionable design. + value = value.rescale(self._dimensionality, dtype='d') self.magnitude[key] = value @with_doc(np.ndarray.__lt__) @@ -404,7 +496,10 @@ def tolist(self): #first get a dummy array from the ndarray method work_list = self.magnitude.tolist() #now go through and replace all numbers with the appropriate Quantity - self._tolist(work_list) + if isinstance(work_list, list): + self._tolist(work_list) + else: + work_list = Quantity(work_list, self.dimensionality) return work_list def _tolist(self, work_list): @@ -422,10 +517,21 @@ def _tolist(self, work_list): @with_doc(np.ndarray.sum) def sum(self, axis=None, dtype=None, out=None): + ret = self.magnitude.sum(axis, dtype, None if out is None else out.magnitude) + dim = self.dimensionality + if out is None: + return Quantity(ret, dim) + if not isinstance(out, Quantity): + raise TypeError("out parameter must be a Quantity") + out._dimensionality = dim + return out + + @with_doc(np.nansum) + def nansum(self, axis=None, dtype=None, out=None): + import numpy as np return Quantity( - self.magnitude.sum(axis, dtype, out), - self.dimensionality, - copy=False + np.nansum(self.magnitude, axis, dtype, out), + self.dimensionality ) @with_doc(np.ndarray.fill) @@ -437,15 +543,17 @@ def fill(self, value): pass @with_doc(np.ndarray.put) - def put(self, indicies, values, mode='raise'): + def put(self, indicies, values, mode='raise', dtype='d'): """ performs the equivalent of ndarray.put() but enforces units values - must be an Quantity with the same units as self """ + # The default of `dtype` is set to 'd' to ensure backwards + # compatibility, arguably it's questionable design. if not isinstance(values, Quantity): values = Quantity(values) if values._dimensionality != self._dimensionality: - values = values.rescale(self.units) + values = values.rescale(self.units, dtype=dtype) self.magnitude.put(indicies, values, mode) # choose does not function correctly, and it is not clear @@ -458,7 +566,7 @@ def argsort(self, axis=-1, kind='quick', order=None): @with_doc(np.ndarray.searchsorted) def searchsorted(self,values, side='left'): if not isinstance (values, Quantity): - values = Quantity(values, copy=False) + values = Quantity(values) if values._dimensionality != self._dimensionality: raise ValueError("values does not have the same units as self") @@ -471,39 +579,74 @@ def nonzero(self): @with_doc(np.ndarray.max) def max(self, axis=None, out=None): + ret = self.magnitude.max(axis, None if out is None else out.magnitude) + dim = self.dimensionality + if out is None: + return Quantity(ret, dim) + if not isinstance(out, Quantity): + raise TypeError("out parameter must be a Quantity") + out._dimensionality = dim + return out + + @with_doc(np.ndarray.argmax) + def argmax(self, axis=None, out=None): + return self.magnitude.argmax(axis, out) + + @with_doc(np.nanmax) + def nanmax(self, axis=None, out=None): return Quantity( - self.magnitude.max(), - self.dimensionality, - copy=False + np.nanmax(self.magnitude), + self.dimensionality ) @with_doc(np.ndarray.min) def min(self, axis=None, out=None): + ret = self.magnitude.min(axis, None if out is None else out.magnitude) + dim = self.dimensionality + if out is None: + return Quantity(ret, dim) + if not isinstance(out, Quantity): + raise TypeError("out parameter must be a Quantity") + out._dimensionality = dim + return out + + @with_doc(np.nanmin) + def nanmin(self, axis=None, out=None): return Quantity( - self.magnitude.min(), - self.dimensionality, - copy=False + np.nanmin(self.magnitude), + self.dimensionality ) @with_doc(np.ndarray.argmin) - def argmin(self,axis=None, out=None): - return self.magnitude.argmin() + def argmin(self, axis=None, out=None): + return self.magnitude.argmin(axis, out) + + @with_doc(np.nanargmin) + def nanargmin(self,axis=None, out=None): + return np.nanargmin(self.magnitude) + + @with_doc(np.nanargmax) + def nanargmax(self,axis=None, out=None): + return np.nanargmax(self.magnitude) @with_doc(np.ndarray.ptp) def ptp(self, axis=None, out=None): - return Quantity( - self.magnitude.ptp(), - self.dimensionality, - copy=False - ) + ret = np.ptp(self.magnitude, axis, None if out is None else out.magnitude) + dim = self.dimensionality + if out is None: + return Quantity(ret, dim) + if not isinstance(out, Quantity): + raise TypeError("out parameter must be a Quantity") + out._dimensionality = dim + return out @with_doc(np.ndarray.clip) def clip(self, min=None, max=None, out=None): if min is None and max is None: raise ValueError("at least one of min or max must be set") else: - if min is None: min = Quantity(-np.Inf, self._dimensionality) - if max is None: max = Quantity(np.Inf, self._dimensionality) + if min is None: min = Quantity(-np.inf, self._dimensionality) + if max is None: max = Quantity(np.inf, self._dimensionality) if self.dimensionality and not \ (isinstance(min, Quantity) and isinstance(max, Quantity)): @@ -516,45 +659,88 @@ def clip(self, min=None, max=None, out=None): max.rescale(self._dimensionality).magnitude, out ) - return Quantity(clipped, self.dimensionality, copy=False) + dim = self.dimensionality + if out is None: + return Quantity(clipped, dim) + if not isinstance(out, Quantity): + raise TypeError("out parameter must be a Quantity") + out._dimensionality = dim + return out @with_doc(np.ndarray.round) def round(self, decimals=0, out=None): - return Quantity( - self.magnitude.round(decimals, out), - self.dimensionality, - copy=False - ) + ret = self.magnitude.round(decimals, None if out is None else out.magnitude) + dim = self.dimensionality + if out is None: + return Quantity(ret, dim) + if not isinstance(out, Quantity): + raise TypeError("out parameter must be a Quantity") + out._dimensionality = dim + return out @with_doc(np.ndarray.trace) def trace(self, offset=0, axis1=0, axis2=1, dtype=None, out=None): + ret = self.magnitude.trace(offset, axis1, axis2, dtype, None if out is None else out.magnitude) + dim = self.dimensionality + if out is None: + return Quantity(ret, dim) + if not isinstance(out, Quantity): + raise TypeError("out parameter must be a Quantity") + out._dimensionality = dim + return out + + @with_doc(np.ndarray.squeeze) + def squeeze(self, axis=None): return Quantity( - self.magnitude.trace(offset, axis1, axis2, dtype, out), - self.dimensionality, - copy=False + self.magnitude.squeeze(axis), + self.dimensionality ) @with_doc(np.ndarray.mean) def mean(self, axis=None, dtype=None, out=None): + ret = self.magnitude.mean(axis, dtype, None if out is None else out.magnitude) + dim = self.dimensionality + if out is None: + return Quantity(ret, dim) + if not isinstance(out, Quantity): + raise TypeError("out parameter must be a Quantity") + out._dimensionality = dim + return out + + @with_doc(np.nanmean) + def nanmean(self, axis=None, dtype=None, out=None): + import numpy as np return Quantity( - self.magnitude.mean(axis, dtype, out), - self.dimensionality, - copy=False) + np.nanmean(self.magnitude, axis, dtype, out), + self.dimensionality) @with_doc(np.ndarray.var) def var(self, axis=None, dtype=None, out=None, ddof=0): - return Quantity( - self.magnitude.var(axis, dtype, out, ddof), - self._dimensionality**2, - copy=False - ) + ret = self.magnitude.var(axis, dtype, out, ddof) + dim = self._dimensionality**2 + if out is None: + return Quantity(ret, dim) + if not isinstance(out, Quantity): + raise TypeError("out parameter must be a Quantity") + out._dimensionality = dim + return out @with_doc(np.ndarray.std) def std(self, axis=None, dtype=None, out=None, ddof=0): + ret = self.magnitude.std(axis, dtype, out, ddof) + dim = self.dimensionality + if out is None: + return Quantity(ret, dim) + if not isinstance(out, Quantity): + raise TypeError("out parameter must be a Quantity") + out._dimensionality = dim + return out + + @with_doc(np.nanstd) + def nanstd(self, axis=None, dtype=None, out=None, ddof=0): return Quantity( - self.magnitude.std(axis, dtype, out, ddof), - self._dimensionality, - copy=False + np.nanstd(self.magnitude, axis, dtype, out, ddof), + self._dimensionality ) @with_doc(np.ndarray.prod) @@ -564,15 +750,25 @@ def prod(self, axis=None, dtype=None, out=None): else: power = self.shape[axis] - return Quantity( - self.magnitude.prod(axis, dtype, out), - self._dimensionality**power, - copy=False - ) + ret = self.magnitude.prod(axis, dtype, None if out is None else out.magnitude) + dim = self._dimensionality**power + if out is None: + return Quantity(ret, dim) + if not isinstance(out, Quantity): + raise TypeError("out parameter must be a Quantity") + out._dimensionality = dim + return out @with_doc(np.ndarray.cumsum) def cumsum(self, axis=None, dtype=None, out=None): - return super(Quantity, self).cumsum(axis, dtype, out)*self.units + ret = self.magnitude.cumsum(axis, dtype, None if out is None else out.magnitude) + dim = self.dimensionality + if out is None: + return Quantity(ret, dim) + if not isinstance(out, Quantity): + raise TypeError("out parameter must be a Quantity") + out._dimensionality = dim + return out @with_doc(np.ndarray.cumprod) def cumprod(self, axis=None, dtype=None, out=None): @@ -581,39 +777,36 @@ def cumprod(self, axis=None, dtype=None, out=None): raise ValueError( "Quantity must be dimensionless, try using simplified" ) - else: - return super(Quantity, self).cumprod(axis, dtype, out) - # list of unsupported functions: [choose] + ret = self.magnitude.cumprod(axis, dtype, out) + dim = self.dimensionality + if out is None: + return Quantity(ret, dim) + if isinstance(out, Quantity): + out._dimensionality = dim + return out - def __getstate__(self): - """ - Return the internal state of the quantity, for pickling - purposes. - - """ - cf = 'CF'[self.flags.fnc] - state = (1, - self.shape, - self.dtype, - self.flags.fnc, - self.tostring(cf), - self._dimensionality, - ) - return state + # list of unsupported functions: [choose] def __setstate__(self, state): - (ver, shp, typ, isf, raw, units) = state - np.ndarray.__setstate__(self, (shp, typ, isf, raw)) + ndarray_state = state[:-1] + units = state[-1] + np.ndarray.__setstate__(self, ndarray_state) self._dimensionality = units def __reduce__(self): """ Return a tuple for pickling a Quantity. """ + reconstruct,reconstruct_args,state = super().__reduce__() + state = state + (self._dimensionality,) return (_reconstruct_quantity, (self.__class__, np.ndarray, (0, ), 'b', ), - self.__getstate__()) + state) + + def __deepcopy__(self, memo_dict): + # constructor copies by default + return Quantity(self.magnitude, self.dimensionality) def _reconstruct_quantity(subtype, baseclass, baseshape, basetype,): diff --git a/quantities/quantity.pyi b/quantities/quantity.pyi new file mode 100644 index 00000000..2b0bd553 --- /dev/null +++ b/quantities/quantity.pyi @@ -0,0 +1,114 @@ +from typing import Any, Optional + +import numpy.typing as npt + +from quantities.dimensionality import Dimensionality +from quantities.typing.quantities import DimensionalityDescriptor, QuantityData + +def validate_unit_quantity(value: Quantity) -> Quantity: + ... + + +def validate_dimensionality(value: DimensionalityDescriptor) -> Dimensionality: + ... + + +def get_conversion_factor(from_u: Quantity, to_u: Quantity) -> float: + ... + + +def scale_other_units(f: Any) -> None: + ... + + +class Quantity(npt.NDArray): + + def __new__(cls, data: QuantityData, units: DimensionalityDescriptor = ..., + dtype: Optional[object] = ..., copy: bool = ...) -> Quantity: + ... + + @property + def dimensionality(self) -> Dimensionality: + ... + + @property + def _reference(self): + ... + + @property + def magnitude(self) -> npt.NDArray: + ... + + @property # type: ignore[misc] + def real(self) -> Quantity: # type: ignore[override] + ... + + @property # type: ignore[misc] + def imag(self) -> Quantity: # type: ignore[override] + ... + + @property + def units(self) -> Quantity: + ... + + def rescale(self, units: Optional[DimensionalityDescriptor] = ...) -> Quantity: + ... + + def rescale_preferred(self) -> Quantity: + ... + + # numeric methods + def __add__(self, other: Quantity) -> Quantity: ... # type: ignore[override] + def __radd__(self, other: Quantity) -> Quantity: ... # type: ignore[override] + def __iadd__(self, other: Quantity) -> Quantity: ... # type: ignore[override] + + def __sub__(self, other: Quantity) -> Quantity: ... # type: ignore[override] + def __rsub__(self, other: Quantity) -> Quantity: ... # type: ignore[override] + def __isub__(self, other: Quantity) -> Quantity: ... # type: ignore[override] + + def __mul__(self, other) -> Quantity: ... + def __rmul__(self, other) -> Quantity: ... + def __imul__(self, other) -> Quantity: ... + + # NOTE matmul is not supported + + def __truediv__(self, other) -> Quantity: ... # type: ignore[override] + def __rtruediv__(self, other) -> Quantity: ... # type: ignore[override] + def __itruediv__(self, other) -> Quantity: ... # type: ignore[override] + + def __floordiv__(self, other) -> Quantity: ... # type: ignore[override] + def __rfloordiv__(self, other) -> Quantity: ... # type: ignore[override] + def __ifloordiv__(self, other) -> Quantity: ... # type: ignore[override] + + def __mod__(self, other: Quantity) -> Quantity: ... # type: ignore[override] + def __rmod__(self, other: Quantity) -> Quantity: ... # type: ignore[override] + def __imod__(self, other: Quantity) -> Quantity: ... # type: ignore[override] + + # NOTE divmod is not supported + + def __pow__(self, power) -> Quantity: ... + def __rpow__(self, power) -> Quantity: ... + def __ipow__(self, power) -> Quantity: ... + + # shift and bitwise are not supported + + # unary methods + def __neg__(self) -> Quantity: ... + # def __pos__(self) -> Quantity: ... # GH#94 + def __abs__(self) -> Quantity: ... + # NOTE invert is not supported + + def __round__(self, decimals: int = ...) -> Quantity: + ... + + def __repr__(self) -> str: + ... + + def __str__(self) -> str: + ... + + def __getitem__(self, item: Any) -> Quantity: + ... + + def __setitem__(self, key: int, value: QuantityData) -> None: + ... diff --git a/quantities/registry.py b/quantities/registry.py index 0cd57bc7..76039654 100644 --- a/quantities/registry.py +++ b/quantities/registry.py @@ -1,29 +1,50 @@ -# -*- coding: utf-8 -*- """ """ -import copy +import ast import re class UnitRegistry: + # Note that this structure ensures that UnitRegistry behaves as a singleton class __Registry: __shared_state = {} + whitelist = ( + ast.Expression, + ast.Constant, + ast.Name, + ast.Load, + ast.BinOp, + ast.UnaryOp, + ast.operator, + ast.unaryop, + ) def __init__(self): self.__dict__ = self.__shared_state self.__context = {} def __getitem__(self, string): - try: - return eval(string, self.__context) - except NameError: + # This approach to avoiding arbitrary evaluation of code is based on https://stackoverflow.com/a/11952618 + # by https://stackoverflow.com/users/567292/ecatmur + tree = ast.parse(string, mode="eval") + valid = all(isinstance(node, self.whitelist) for node in ast.walk(tree)) + if valid: + try: + item = eval( + compile(tree, filename="", mode="eval"), + {"__builtins__": {}}, + self.__context, + ) + except NameError: + raise LookupError('Unable to parse units: "%s"' % string) + else: + return item + else: # could return self['UnitQuantity'](string) - raise LookupError( - 'Unable to parse units: "%s"'%string - ) + raise LookupError('Unable to parse units: "%s"' % string) def __setitem__(self, string, val): assert isinstance(string, str) @@ -33,7 +54,7 @@ def __setitem__(self, string, val): if val == self.__context[string]: return raise KeyError( - '%s has already been registered for %s' + '%s has already been registered for %s' % (string, self.__context[string]) ) self.__context[string] = val @@ -55,7 +76,7 @@ def __getitem__(self, label): # make sure we can parse the label .... if label == '': label = 'dimensionless' - if label == "%": label = "percent" + if "%" in label: label = label.replace("%", "percent") if label.lower() == "in": label = "inch" return self.__registry[label] diff --git a/quantities/registry.pyi b/quantities/registry.pyi new file mode 100644 index 00000000..344585dd --- /dev/null +++ b/quantities/registry.pyi @@ -0,0 +1,9 @@ +from quantities import UnitQuantity + + +class UnitRegistry: + + def __getitem__(self, item: str) -> UnitQuantity: + ... + +unit_registry: UnitRegistry \ No newline at end of file diff --git a/quantities/tests/common.py b/quantities/tests/common.py index 0cdf8c68..ed28c83c 100644 --- a/quantities/tests/common.py +++ b/quantities/tests/common.py @@ -1,8 +1,5 @@ import sys -if sys.version.startswith('2.6') or sys.version.startswith('3.1'): - import unittest2 as unittest -else: - import unittest +import unittest import numpy as np @@ -22,15 +19,20 @@ def assertQuantityEqual(self, q1, q2, msg=None, delta=None): Make sure q1 and q2 are the same quantities to within the given precision. """ - delta = 1e-5 if delta is None else delta + if delta is None: + # NumPy 2 introduced float16, so we base tolerance on machine epsilon + delta1 = np.finfo(q1.dtype).eps if isinstance(q1, np.ndarray) and q1.dtype.kind in 'fc' else 1e-15 + delta2 = np.finfo(q2.dtype).eps if isinstance(q2, np.ndarray) and q2.dtype.kind in 'fc' else 1e-15 + delta = max(delta1, delta2)**0.3 msg = '' if msg is None else ' (%s)' % msg q1 = Quantity(q1) q2 = Quantity(q2) if q1.shape != q2.shape: raise self.failureException( - "Shape mismatch (%s vs %s)%s" % (q1.shape, q2.shape, msg) + f"Shape mismatch ({q1.shape} vs {q2.shape}){msg}" ) + if not np.all(np.abs(q1.magnitude - q2.magnitude) < delta): raise self.failureException( "Magnitudes differ by more than %g (%s vs %s)%s" @@ -41,5 +43,5 @@ def assertQuantityEqual(self, q1, q2, msg=None, delta=None): d2 = getattr(q2, '_dimensionality', None) if (d1 or d2) and not (d1 == d2): raise self.failureException( - "Dimensionalities are not equal (%s vs %s)%s" % (d1, d2, msg) + f"Dimensionalities are not equal ({d1} vs {d2}){msg}" ) diff --git a/quantities/tests/test_arithmetic.py b/quantities/tests/test_arithmetic.py index 813b89bc..1b6aeeb9 100644 --- a/quantities/tests/test_arithmetic.py +++ b/quantities/tests/test_arithmetic.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import operator as op from functools import partial import sys @@ -50,35 +48,12 @@ def check(f, *args, **kwargs): return (new, ) -class iter_dtypes(object): - - def __init__(self): - self._i = 1 - self._typeDict = np.typeDict.copy() - self._typeDict[17] = int - self._typeDict[18] = long - self._typeDict[19] = float - self._typeDict[20] = complex - - def __iter__(self): - return self - - def __next__(self): - if self._i > 20: - raise StopIteration - - i = self._i - self._i += 1 - return self._typeDict[i] - - def next(self): - return self.__next__() - def get_dtypes(): - return list(iter_dtypes()) + numeric_dtypes = 'iufc' # https://numpy.org/doc/stable/reference/generated/numpy.dtype.kind.html + return [v for v in np.sctypeDict.values() if np.dtype(v).kind in numeric_dtypes] + [int, long, float, complex] -class iter_types(object): +class iter_types: def __init__(self, dtype): self._index = -1 @@ -134,6 +109,32 @@ def test_mul(self): self.check_rmul(x, y) dtypes.pop(0) + def test_truediv(self): + q = Quantity([44, 40, 36, 32], units=pq.ms) + self.assertQuantityEqual( + q/(4 * pq.ms), + Quantity([11, 10, 9, 8], units=pq.dimensionless) + ) + + q = Quantity([46, 42, 38, 34], units=pq.ms) + self.assertQuantityEqual( + q/(4 * pq.ms), + Quantity([11.5, 10.5, 9.5, 8.5], units=pq.dimensionless) + ) + + def test_floordiv(self): + q = Quantity([45, 43, 39, 32], units=pq.ms) + self.assertQuantityEqual( + q//(4 * pq.ms), + Quantity([11, 10, 9, 8], units=pq.dimensionless) + ) + + q = Quantity([46, 42, 38, 34], units=pq.ms) + self.assertQuantityEqual( + q//(4 * pq.ms), + Quantity([11, 10, 9, 8], units=pq.dimensionless) + ) + def test_mixed_addition(self): self.assertQuantityEqual(1*pq.ft + 1*pq.m, 4.280839895 * pq.ft) self.assertQuantityEqual(1*pq.ft + pq.m, 4.280839895 * pq.ft) @@ -225,6 +226,12 @@ def test_addition(self): [1, 2, 3]*pq.hp + [1, 2, 3]*pq.hp, [2, 4, 6]*pq.hp ) + # add in test with 'min' since this caused issues + # see https://github.com/python-quantities/python-quantities/issues/243 + self.assertQuantityEqual( + Quantity(1, 'min') + Quantity(1, 'min'), + 2*pq.min + ) self.assertRaises(ValueError, op.add, pq.kPa, pq.lb) self.assertRaises(ValueError, op.add, pq.kPa, 10) @@ -360,9 +367,16 @@ def test_in_place_subtraction(self): self.assertRaises(ValueError, op.isub, [1, 2, 3]*pq.m, pq.J) self.assertRaises(ValueError, op.isub, [1, 2, 3]*pq.m, 5*pq.J) + def test_division(self): + molar = pq.UnitQuantity('M', 1000 * pq.mole/pq.m**3, u_symbol='M') + for subtr in [1, 1.0]: + q = 1*molar/(1000*pq.mole/pq.m**3) + self.assertQuantityEqual((q - subtr).simplified, 0) + def test_powering(self): # test raising a quantity to a power self.assertQuantityEqual((5.5 * pq.cm)**5, (5.5**5) * (pq.cm**5)) + self.assertQuantityEqual((5.5 * pq.cm)**0, (5.5**0) * pq.dimensionless) # must also work with compound units self.assertQuantityEqual((5.5 * pq.J)**5, (5.5**5) * (pq.J**5)) diff --git a/quantities/tests/test_comparison.py b/quantities/tests/test_comparison.py index 8485aac7..523dc3ba 100644 --- a/quantities/tests/test_comparison.py +++ b/quantities/tests/test_comparison.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import operator as op from .. import units as pq diff --git a/quantities/tests/test_constants.py b/quantities/tests/test_constants.py index 1fa6ef7e..363e5fb5 100644 --- a/quantities/tests/test_constants.py +++ b/quantities/tests/test_constants.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from .. import units as pq from .. import constants as pc from .common import TestCase diff --git a/quantities/tests/test_conversion.py b/quantities/tests/test_conversion.py index 3646e1be..7ae0384a 100644 --- a/quantities/tests/test_conversion.py +++ b/quantities/tests/test_conversion.py @@ -1,6 +1,7 @@ -# -*- coding: utf-8 -*- - +import unittest +import numpy as np from .. import units as pq +from .. import quantity from .common import TestCase @@ -15,6 +16,46 @@ def test_inplace_conversion(self): def test_rescale(self): for u in ('ft', 'feet', pq.ft): self.assertQuantityEqual((10*pq.m).rescale(u), 32.80839895 * pq.ft) + self.assertQuantityEqual((10 * pq.deg).rescale(pq.rad), 0.17453293 * pq.rad) + self.assertQuantityEqual(quantity.Quantity(10, pq.deg).rescale(pq.rad), 0.17453293 * pq.rad) + + def test_rescale_preferred(self): + quantity.PREFERRED = [pq.mV, pq.pA] + q = 10*pq.V + self.assertQuantityEqual(q.rescale_preferred(), q.rescale(pq.mV)) + q = 5*pq.A + self.assertQuantityEqual(q.rescale_preferred(), q.rescale(pq.pA)) + quantity.PREFERRED = [] + + def test_rescale_preferred_failure(self): + quantity.PREFERRED = [pq.pA] + q = 10*pq.V + try: + self.assertQuantityEqual(q.rescale_preferred(), q.rescale(pq.mV)) + except: + self.assertTrue(True) + else: + self.assertTrue(False) + quantity.PREFERRED = [] + + def test_rescale_noargs(self): + quantity.PREFERRED = [pq.mV, pq.pA] + q = 10*pq.V + self.assertQuantityEqual(q.rescale(), q.rescale(pq.mV)) + q = 5*pq.A + self.assertQuantityEqual(q.rescale(), q.rescale(pq.pA)) + quantity.PREFERRED = [] + + def test_rescale_noargs_failure(self): + quantity.PREFERRED = [pq.pA] + q = 10*pq.V + try: + self.assertQuantityEqual(q.rescale_preferred(), q.rescale(pq.mV)) + except: + self.assertTrue(True) + else: + self.assertTrue(False) + quantity.PREFERRED = [] def test_compound_reduction(self): pc_per_cc = pq.CompoundUnit("pc/cm**3") @@ -55,3 +96,45 @@ def test_default_system(self): pq.set_default_units('cgs', length='mm') self.assertQuantityEqual(pq.kg.simplified, 1000*pq.g) self.assertQuantityEqual(pq.m.simplified, 1000*pq.mm) + + # test a time default as well as mass and weight + pq.set_default_units('SI') + self.assertQuantityEqual(pq.min.simplified, 60*pq.sec) + +class TestUnitInformation(TestCase): + + def test_si(self): + pq.set_default_units(information='B') + self.assertQuantityEqual(pq.kB.simplified, pq.B*pq.kilo) + self.assertQuantityEqual(pq.MB.simplified, pq.B*pq.mega) + self.assertQuantityEqual(pq.GB.simplified, pq.B*pq.giga) + self.assertQuantityEqual(pq.TB.simplified, pq.B*pq.tera) + self.assertQuantityEqual(pq.PB.simplified, pq.B*pq.peta) + self.assertQuantityEqual(pq.EB.simplified, pq.B*pq.exa) + self.assertQuantityEqual(pq.ZB.simplified, pq.B*pq.zetta) + self.assertQuantityEqual(pq.YB.simplified, pq.B*pq.yotta) + + def test_si_aliases(self): + prefixes = ['kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zetta', 'yotta'] + for prefix in prefixes: + self.assertQuantityEqual(pq.B.rescale(prefix + 'byte'), pq.B.rescale(prefix + 'bytes')) + self.assertQuantityEqual(pq.B.rescale(prefix + 'byte'), pq.B.rescale(prefix + 'octet')) + self.assertQuantityEqual(pq.B.rescale(prefix + 'byte'), pq.B.rescale(prefix + 'octets')) + + def test_iec(self): + pq.set_default_units(information='B') + self.assertQuantityEqual(pq.KiB.simplified, pq.B*pq.kibi) + self.assertQuantityEqual(pq.MiB.simplified, pq.B*pq.mebi) + self.assertQuantityEqual(pq.GiB.simplified, pq.B*pq.gibi) + self.assertQuantityEqual(pq.TiB.simplified, pq.B*pq.tebi) + self.assertQuantityEqual(pq.PiB.simplified, pq.B*pq.pebi) + self.assertQuantityEqual(pq.EiB.simplified, pq.B*pq.exbi) + self.assertQuantityEqual(pq.ZiB.simplified, pq.B*pq.zebi) + self.assertQuantityEqual(pq.YiB.simplified, pq.B*pq.yobi) + + def test_iec_aliases(self): + prefixes = ['kibi', 'mebi', 'gibi', 'tebi', 'pebi', 'exbi', 'zebi', 'yobi'] + for prefix in prefixes: + self.assertQuantityEqual(pq.B.rescale(prefix + 'byte'), pq.B.rescale(prefix + 'bytes')) + self.assertQuantityEqual(pq.B.rescale(prefix + 'byte'), pq.B.rescale(prefix + 'octet')) + self.assertQuantityEqual(pq.B.rescale(prefix + 'byte'), pq.B.rescale(prefix + 'octets')) diff --git a/quantities/tests/test_dimensionality.py b/quantities/tests/test_dimensionality.py index 9fcc6d91..6d20ca2d 100644 --- a/quantities/tests/test_dimensionality.py +++ b/quantities/tests/test_dimensionality.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import operator as op from .. import units as pq @@ -14,6 +12,7 @@ joule_str = 'kg*m**2/s**2' joule_uni = 'kg·m²/s²' joule_tex = r'$\mathrm{\frac{kg{\cdot}m^{2}}{s^{2}}}$' +joule_htm = 'kg⋅m2/s2' Joule = Dimensionality({pq.J: 1}) Joule_str = 'J' @@ -24,6 +23,7 @@ def test_dimensionality_str(self): self.assertEqual(joule.string, joule_str) self.assertEqual(joule.unicode, joule_uni) self.assertEqual(joule.latex, joule_tex) + self.assertEqual(joule.html, joule_htm) self.assertEqual(Joule.string, 'J') def test_equality(self): diff --git a/quantities/tests/test_formatting.py b/quantities/tests/test_formatting.py new file mode 100644 index 00000000..7f0e465c --- /dev/null +++ b/quantities/tests/test_formatting.py @@ -0,0 +1,17 @@ +from .. import units as pq +from .common import TestCase + + +class TestFormatting(TestCase): + + @staticmethod + def _check(quantity, formatted): + assert str(quantity) == formatted + assert f'{quantity}' == formatted + assert f'{quantity!s}' == formatted + + def test_str_format_scalar(self): + self._check(1*pq.J, '1.0 J') + + def test_str_format_non_scalar(self): + self._check([1, 2]*pq.J, '[1. 2.] J') diff --git a/quantities/tests/test_methods.py b/quantities/tests/test_methods.py index abbf487d..ff842107 100644 --- a/quantities/tests/test_methods.py +++ b/quantities/tests/test_methods.py @@ -1,8 +1,7 @@ -# -*- coding: utf-8 -*- - -from .. import units as pq +import warnings +from .. import QuantitiesDeprecationWarning, units as pq from .common import TestCase - +import numpy as np class TestQuantityMethods(TestCase): @@ -11,12 +10,20 @@ def setUp(self): def test_tolist(self): self.assertEqual(self.q.tolist(), [[1*pq.m, 2*pq.m], [3*pq.m, 4*pq.m]]) + q_singleton = 1 * pq.m + self.assertEqual(q_singleton.tolist(), q_singleton) def test_sum(self): self.assertQuantityEqual(self.q.sum(), 10*pq.m) self.assertQuantityEqual(self.q.sum(0), [4, 6]*pq.m) self.assertQuantityEqual(self.q.sum(1), [3, 7]*pq.m) + def test_nansum(self): + import numpy as np + qnan = [[1,2], [3,4], [np.nan,np.nan]] * pq.m + self.assertQuantityEqual(qnan.nansum(), 10*pq.m ) + self.assertQuantityEqual(qnan.nansum(0), [4,6]*pq.m ) + def test_fill(self): self.q.fill(6 * pq.ft) self.assertQuantityEqual(self.q, [[6, 6], [6, 6]] * pq.ft) @@ -100,69 +107,212 @@ def test_nonzero(self): q = [1, 0, 5, 6, 0, 9] * pq.m self.assertQuantityEqual(q.nonzero()[0], [0, 2, 3, 5]) + def methodWithOut(self, name, result, q=None, *args, **kw): + import numpy as np + from .. import Quantity + + if q is None: + q = self.q + + self.assertQuantityEqual( + getattr(q.copy(), name)(*args,**kw), + result + ) + if isinstance(result, Quantity): + # deliberately using an incompatible unit + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=QuantitiesDeprecationWarning) + out = Quantity(np.empty_like(result.magnitude), pq.s, copy=False) + # we can drop 'copy=False' above once the deprecation of the arg has expired. + else: + out = np.empty_like(result) + ret = getattr(q.copy(), name)(*args, out=out, **kw) + self.assertQuantityEqual( + ret, + result + ) + # returned array should be the same as out + self.assertEqual(id(ret),id(out)) + # but the units had to be adjusted + if isinstance(result, Quantity): + self.assertEqual(ret.units,result.units) + else: + self.assertEqual( + getattr(ret, 'units', pq.dimensionless), + pq.dimensionless + ) + + def test_max(self): - self.assertQuantityEqual(self.q.max(), 4*pq.m) + self.methodWithOut('max', 4 * pq.m) + self.methodWithOut('max', [3, 4] * pq.m, axis=0) + self.methodWithOut('max', [2, 4] * pq.m, axis=1) + + def test_nanmax(self): + q = np.append(self.q, np.nan) * self.q.units + self.assertQuantityEqual(q.nanmax(), 4*pq.m) def test_argmax(self): - self.assertEqual(self.q.argmax(), 3) + import numpy as np + self.assertQuantityEqual(self.q.argmax(), 3) + self.assertQuantityEqual(self.q.argmax(axis=0), [1, 1]) + self.assertQuantityEqual(self.q.argmax(axis=1), [1, 1]) + # apparently, numpy's argmax does not return the same object when out is specified. + # instead, we test here for shared data + out = np.r_[0, 0] + ret = self.q.argmax(axis=0,out=out) + self.assertQuantityEqual(ret, [1, 1]) + self.assertEqual(ret.ctypes.data, out.ctypes.data) + + def test_nanargmax(self): + q = np.append(self.q, np.nan) * self.q.units + self.assertEqual(self.q.nanargmax(), 3) def test_min(self): - self.assertEqual(self.q.min(), 1 * pq.m) + self.methodWithOut('min', 1 * pq.m) + self.methodWithOut('min', [1, 2] * pq.m, axis=0) + self.methodWithOut('min', [1, 3] * pq.m, axis=1) + + def test_nanmin(self): + q = np.append(self.q, np.nan) * self.q.units + self.assertQuantityEqual(q.nanmin(), 1*pq.m) def test_argmin(self): - self.assertEqual(self.q.argmin(), 0) + import numpy as np + self.assertQuantityEqual(self.q.argmin(), 0) + self.assertQuantityEqual(self.q.argmin(axis=0), [0, 0]) + self.assertQuantityEqual(self.q.argmin(axis=1), [0, 0]) + # apparently, numpy's argmax does not return the same object when out is specified. + # instead, we test here for shared data + out = np.r_[2, 2] + ret = self.q.argmin(axis=0,out=out) + self.assertQuantityEqual(ret, [0, 0]) + self.assertEqual(ret.ctypes.data, out.ctypes.data) + + def test_nanargmin(self): + q = np.append(self.q, np.nan) * self.q.units + self.assertEqual(self.q.nanargmin(), 0) def test_ptp(self): - self.assertQuantityEqual(self.q.ptp(), 3 * pq.m) + self.methodWithOut('ptp', 3 * pq.m) + self.methodWithOut('ptp', [2, 2] * pq.m, axis=0) + self.methodWithOut('ptp', [1, 1] * pq.m, axis=1) def test_clip(self): - self.assertQuantityEqual( - self.q.copy().clip(max=2*pq.m), - [[1, 2], [2, 2]] * pq.m + self.methodWithOut( + 'clip', + [[1, 2], [2, 2]] * pq.m, + max=2*pq.m, ) - self.assertQuantityEqual( - self.q.copy().clip(min=3*pq.m), - [[3, 3], [3, 4]] * pq.m + self.methodWithOut( + 'clip', + [[3, 3], [3, 4]] * pq.m, + min=3*pq.m, ) - self.assertQuantityEqual( - self.q.copy().clip(min=2*pq.m, max=3*pq.m), - [[2, 2], [3, 3]] * pq.m + self.methodWithOut( + 'clip', + [[2, 2], [3, 3]] * pq.m, + min=2*pq.m, max=3*pq.m ) self.assertRaises(ValueError, self.q.clip, pq.J) self.assertRaises(ValueError, self.q.clip, 1) def test_round(self): q = [1, 1.33, 5.67, 22] * pq.m - self.assertQuantityEqual(q.round(0), [1, 1, 6, 22] * pq.m) - self.assertQuantityEqual(q.round(-1), [0, 0, 10, 20] * pq.m) - self.assertQuantityEqual(q.round(1), [1, 1.3, 5.7, 22] * pq.m) + self.methodWithOut( + 'round', + [1, 1, 6, 22] * pq.m, + q=q, + decimals=0, + ) + self.methodWithOut( + 'round', + [0, 0, 10, 20] * pq.m, + q=q, + decimals=-1, + ) + self.methodWithOut( + 'round', + [1, 1.3, 5.7, 22] * pq.m, + q=q, + decimals=1, + ) def test_trace(self): - self.assertQuantityEqual(self.q.trace(), (1+4) * pq.m) + self.methodWithOut('trace', (1+4) * pq.m) def test_cumsum(self): - self.assertQuantityEqual(self.q.cumsum(), [1, 3, 6, 10] * pq.m) + self.methodWithOut('cumsum', [1, 3, 6, 10] * pq.m) + self.methodWithOut('cumsum', [[1, 2], [4, 6]] * pq.m, axis=0) + self.methodWithOut('cumsum', [[1, 3], [3, 7]] * pq.m, axis=1) def test_mean(self): - self.assertQuantityEqual(self.q.mean(), 2.5 * pq.m) + self.methodWithOut('mean', 2.5 * pq.m) + self.methodWithOut('mean', [2, 3] * pq.m, axis=0) + self.methodWithOut('mean', [1.5, 3.5] * pq.m, axis=1) + + def test_nanmean(self): + import numpy as np + q = [[1,2], [3,4], [np.nan,np.nan]] * pq.m + self.assertQuantityEqual(q.nanmean(), self.q.mean()) def test_var(self): - self.assertQuantityEqual(self.q.var(), 1.25*pq.m**2) + self.methodWithOut('var', 1.25 * pq.m**2) + self.methodWithOut('var', [1, 1] * pq.m**2, axis=0) + self.methodWithOut('var', [0.25, 0.25] * pq.m**2, axis=1) def test_std(self): - self.assertQuantityEqual(self.q.std(), 1.11803*pq.m, delta=1e-5) + self.methodWithOut('std', 1.1180339887498949 * pq.m) + self.methodWithOut('std', [1, 1] * pq.m, axis=0) + self.methodWithOut('std', [0.5, 0.5] * pq.m, axis=1) + + def test_nanstd(self): + import numpy as np + q0 = [[1,2], [3,4]] * pq.m + q1 = [[1,2], [3,4], [np.nan,np.nan]] * pq.m + self.assertQuantityEqual(q0.std(), q1.nanstd()) def test_prod(self): - self.assertQuantityEqual(self.q.prod(), 24 * pq.m**4) + self.methodWithOut('prod', 24 * pq.m**4) + self.methodWithOut('prod', [3, 8] * pq.m**2, axis=0) + self.methodWithOut('prod', [2, 12] * pq.m**2, axis=1) def test_cumprod(self): self.assertRaises(ValueError, self.q.cumprod) self.assertQuantityEqual((self.q/pq.m).cumprod(), [1, 2, 6, 24]) + q = self.q/pq.m + self.methodWithOut( + 'cumprod', + [1, 2, 6, 24], + q=q, + ) + self.methodWithOut( + 'cumprod', + [[1, 2], [3, 8]], + q=q, + axis=0, + ) + self.methodWithOut( + 'cumprod', + [[1, 2], [3, 12]], + q=q, + axis=1, + ) def test_conj(self): self.assertQuantityEqual((self.q*(1+1j)).conj(), self.q*(1-1j)) self.assertQuantityEqual((self.q*(1+1j)).conjugate(), self.q*(1-1j)) + def test_real(self): + test_q = self.q * (1 + 1j) + test_q.real = [[39.3701, 39.3701], [39.3701, 39.3701]] * pq.inch + self.assertQuantityEqual(test_q.real, [[1., 1.], [1., 1.]] * pq.m) + + def test_imag(self): + test_q = self.q * (1 + 1j) + test_q.imag = [[39.3701, 39.3701], [39.3701, 39.3701]] * pq.inch + self.assertQuantityEqual(test_q.imag, [[1., 1.], [1., 1.]] * pq.m) + def test_getitem(self): self.assertRaises(IndexError, self.q.__getitem__, (0,10)) self.assertQuantityEqual(self.q[0], [1,2]*pq.m) @@ -199,3 +349,17 @@ def test_setitem (self): def test_iterator(self): for q in self.q.flatten(): self.assertQuantityEqual(q.units, pq.m) + + def test_rescale_integer_argument(self): + from .. import Quantity + self.assertQuantityEqual( + Quantity(10, pq.deg).rescale(pq.rad), + np.pi/18*pq.rad + ) + + def test_dimensionless_magnitude(self): + self.assertEqual((pq.kg/pq.g).dimensionless_magnitude, 1000) + self.assertQuantityEqual((self.q / pq.cm).dimensionless_magnitude, + 100 * self.q.magnitude) + self.assertRaises(ValueError, lambda x: x.dimensionless_magnitude, + self.q) diff --git a/quantities/tests/test_persistence.py b/quantities/tests/test_persistence.py index b173689a..da26924f 100644 --- a/quantities/tests/test_persistence.py +++ b/quantities/tests/test_persistence.py @@ -1,8 +1,8 @@ -# -*- coding: utf-8 -*- - import pickle +import copy from .. import units as pq +from ..quantity import Quantity from ..uncertainquantity import UncertainQuantity from .. import constants from .common import TestCase @@ -10,7 +10,7 @@ class TestPersistence(TestCase): - def test_unitquantity_persistance(self): + def test_unitquantity_persistence(self): x = pq.m y = pickle.loads(pickle.dumps(x)) self.assertQuantityEqual(x, y) @@ -19,17 +19,60 @@ def test_unitquantity_persistance(self): y = pickle.loads(pickle.dumps(x)) self.assertQuantityEqual(x, y) - def test_quantity_persistance(self): + def test_quantity_persistence(self): x = 20*pq.m y = pickle.loads(pickle.dumps(x)) self.assertQuantityEqual(x, y) - def test_uncertainquantity_persistance(self): + def test_uncertainquantity_persistence(self): x = UncertainQuantity(20, 'm', 0.2) y = pickle.loads(pickle.dumps(x)) self.assertQuantityEqual(x, y) - def test_unitconstant_persistance(self): + def test_unitconstant_persistence(self): x = constants.m_e y = pickle.loads(pickle.dumps(x)) self.assertQuantityEqual(x, y) + + def test_quantity_object_dtype(self): + # Regression test for github issue #113 + x = Quantity(1,dtype=object) + y = pickle.loads(pickle.dumps(x)) + self.assertQuantityEqual(x, y) + + def test_uncertainquantity_object_dtype(self): + # Regression test for github issue #113 + x = UncertainQuantity(20, 'm', 0.2, dtype=object) + y = pickle.loads(pickle.dumps(x)) + self.assertQuantityEqual(x, y) + + def test_backward_compat(self): + """ A few pickles collected before fixing #113 just to make sure we remain backwards compatible. """ + orig = [ + pq.m, + 20*pq.m, + UncertainQuantity(20, 'm', 0.2), + constants.m_e, + ] + data = [ + # generated with protocol=2 + b'\x80\x02cquantities.unitquantity\nUnitLength\nq\x00(X\x05\x00\x00\x00meterq\x01NX\x01\x00\x00\x00mq\x02N]q\x03(X\x06\x00\x00\x00metersq\x04X\x05\x00\x00\x00metreq\x05X\x06\x00\x00\x00metresq\x06eNtq\x07Rq\x08K\x01K\x02K\x02\x86q\t\x86q\nb.', + b'\x80\x02cquantities.quantity\n_reconstruct_quantity\nq\x00(cquantities.quantity\nQuantity\nq\x01cnumpy\nndarray\nq\x02K\x00\x85q\x03X\x01\x00\x00\x00bq\x04tq\x05Rq\x06(K\x01)cnumpy\ndtype\nq\x07X\x02\x00\x00\x00f8q\x08K\x00K\x01\x87q\tRq\n(K\x03X\x01\x00\x00\x00= (2, 0, 0): + self.assertQuantityEqual(np.trapezoid(self.q, dx = 1*pq.m), 7.5 * pq.J*pq.m) def test_sinh(self): q = [1, 2, 3, 4, 6] * pq.radian @@ -113,19 +122,19 @@ def test_around(self): [0, 0, 0, 10] * pq.J ) - def test_round_(self): + def test_round(self): self.assertQuantityEqual( - np.round_([.5, 1.5, 2.5, 3.5, 4.5] * pq.J), + np.round([.5, 1.5, 2.5, 3.5, 4.5] * pq.J), [0., 2., 2., 4., 4.] * pq.J ) self.assertQuantityEqual( - np.round_([1,2,3,11] * pq.J, decimals=1), + np.round([1,2,3,11] * pq.J, decimals=1), [1, 2, 3, 11] * pq.J ) self.assertQuantityEqual( - np.round_([1,2,3,11] * pq.J, decimals=-1), + np.round([1,2,3,11] * pq.J, decimals=-1), [0, 0, 0, 10] * pq.J ) @@ -150,22 +159,22 @@ def test_ceil(self): [-1., -1., -0., 1., 2., 2., 2.] * pq.m ) - @unittest.expectedFailure def test_fix(self): - try: - self.assertQuantityEqual(np.fix(3.14 * pq.degF), 3.0 * pq.degF) - self.assertQuantityEqual(np.fix(3.0 * pq.degF), 3.0 * pq.degF) - self.assertQuantityEqual( - np.fix([2.1, 2.9, -2.1, -2.9] * pq.degF), - [2., 2., -2., -2.] * pq.degF - ) - except ValueError as e: - raise self.failureException(e) + self.assertQuantityEqual(np.fix(3.14 * pq.degF), 3.0 * pq.degF) + self.assertQuantityEqual(np.fix(3.0 * pq.degF), 3.0 * pq.degF) + self.assertQuantityEqual( + np.fix([2.1, 2.9, -2.1, -2.9] * pq.degF), + [2., 2., -2., -2.] * pq.degF + ) def test_exp(self): self.assertQuantityEqual(np.exp(1*pq.dimensionless), np.e) self.assertRaises(ValueError, np.exp, 1*pq.m) + def test_exp2(self): + self.assertQuantityEqual(np.exp2(1*pq.dimensionless), 2.0) + self.assertRaises(ValueError, np.exp2, 1*pq.m) + def test_log(self): self.assertQuantityEqual(np.log(1*pq.dimensionless), 0) self.assertRaises(ValueError, np.log, 1*pq.m) @@ -218,7 +227,13 @@ def test_arctan2(self): np.arctan2(0*pq.dimensionless, 0*pq.dimensionless), 0 ) - self.assertRaises(ValueError, np.arctan2, (1*pq.m, 1*pq.m)) + self.assertQuantityEqual( + np.arctan2(3*pq.V, 3*pq.V), + np.radians(45)*pq.dimensionless + ) + # NumPy <1.21 raises ValueError + # NumPy >=1.21 raises TypeError + self.assertRaises((TypeError, ValueError), np.arctan2, (1*pq.m, 1*pq.m)) def test_hypot(self): self.assertQuantityEqual(np.hypot(3 * pq.m, 4 * pq.m), 5 * pq.m) @@ -242,3 +257,49 @@ def test_radians(self): def test_unwrap(self): self.assertQuantityEqual(np.unwrap([0,3*np.pi]*pq.radians), [0,np.pi]) self.assertQuantityEqual(np.unwrap([0,540]*pq.deg), [0,180]*pq.deg) + + def test_equal(self): + arr1 = (1, 1) * pq.m + arr2 = (1.0, 1.0) * pq.m + self.assertTrue(np.all(np.equal(arr1, arr2))) + self.assertFalse(np.all(np.equal(arr1, arr2 * 2))) + + def test_not_equal(self): + arr1 = (1, 1) * pq.m + arr2 = (1.0, 1.0) * pq.m + self.assertTrue(np.all(np.not_equal(arr1, arr2*2))) + self.assertFalse(np.all(np.not_equal(arr1, arr2))) + + def test_less(self): + arr1 = (1, 1) * pq.m + arr2 = (1.0, 1.0) * pq.m + self.assertTrue(np.all(np.less(arr1, arr2*2))) + self.assertFalse(np.all(np.less(arr1*2, arr2))) + + def test_less_equal(self): + arr1 = (1, 1) * pq.m + arr2 = (1.0, 2.0) * pq.m + self.assertTrue(np.all(np.less_equal(arr1, arr2))) + self.assertFalse(np.all(np.less_equal(arr2, arr1))) + + def test_greater(self): + arr1 = (1, 1) * pq.m + arr2 = (1.0, 2.0) * pq.m + self.assertTrue(np.all(np.greater(arr2*1.01, arr1))) + self.assertFalse(np.all(np.greater(arr2, arr1))) + + def test_greater_equal(self): + arr1 = (1, 1) * pq.m + arr2 = (1.0, 2.0) * pq.m + self.assertTrue(np.all(np.greater_equal(arr2, arr1))) + self.assertFalse(np.all(np.greater_equal(arr2*0.99, arr1))) + + def test_maximum(self): + arr1 = (998, 999) * pq.m + arr2 = (1e3, 5e2) * pq.m + self.assertQuantityEqual(np.maximum(arr1, arr2) - [1000, 999]*pq.m, [0, 0]*pq.m) + + def test_minimum(self): + arr1 = (998, 999) * pq.m + arr2 = (1e3, 5e2) * pq.m + self.assertQuantityEqual(np.minimum(arr1, arr2) - [998, 500]*pq.m, [0, 0]*pq.m) diff --git a/quantities/tests/test_uncertainty.py b/quantities/tests/test_uncertainty.py index 94cec2a9..85296d2f 100644 --- a/quantities/tests/test_uncertainty.py +++ b/quantities/tests/test_uncertainty.py @@ -1,8 +1,8 @@ -# -*- coding: utf-8 -*- - from .. import units as pq +from ..quantity import Quantity from ..uncertainquantity import UncertainQuantity from .common import TestCase +import numpy as np class TestUncertainty(TestCase): @@ -32,6 +32,17 @@ def test_rescale(self): [0.32808399, 0.32808399, 0.32808399]*pq.ft ) + seventy_km = Quantity(70, pq.km, dtype=np.float32) + seven_km = Quantity(7, pq.km, dtype=np.float32) + seventyish_km = UncertainQuantity(seventy_km, pq.km, seven_km, dtype=np.float32) + self.assertTrue(seventyish_km.dtype == np.float32) + in_meters = seventyish_km.rescale(pq.m) + self.assertTrue(in_meters.dtype == seventyish_km.dtype) + seventyish_km_rescaled_idempotent = seventyish_km.rescale(pq.km) + self.assertTrue(seventyish_km_rescaled_idempotent.dtype == np.float32) + self.assertQuantityEqual(seventyish_km + in_meters, 2*seventy_km) + + def test_set_uncertainty(self): a = UncertainQuantity([1, 2], 'm', [.1, .2]) a.uncertainty = [1., 2.]*pq.m @@ -50,6 +61,13 @@ def test_uncertainquantity_multiply(self): self.assertQuantityEqual(a*2, [2, 4]*pq.m) self.assertQuantityEqual((a*2).uncertainty, [0.2,0.4]*pq.m) + def test_uncertainquantity_negative(self): + a = UncertainQuantity([1, 2], 'm', [.1, .2]) + self.assertQuantityEqual(-a, [-1., -2.]*pq.m) + self.assertQuantityEqual((-a).uncertainty, [0.1, 0.2]*pq.m) + self.assertQuantityEqual(-a, a*-1) + self.assertQuantityEqual((-a).uncertainty, (a*-1).uncertainty) + def test_uncertainquantity_divide(self): a = UncertainQuantity([1, 2], 'm', [.1, .2]) self.assertQuantityEqual(a/a, [1., 1.]) @@ -60,3 +78,50 @@ def test_uncertainquantity_divide(self): self.assertQuantityEqual((a/2).uncertainty, [0.05, 0.1 ]*pq.m) self.assertQuantityEqual(1/a, [1., 0.5]/pq.m) self.assertQuantityEqual((1/a).uncertainty, [0.1, 0.05]/pq.m) + + def test_uncertaintity_mean(self): + a = UncertainQuantity([1,2], 'm', [.1,.2]) + mean0 = np.sum(a)/np.size(a) # calculated traditionally + mean1 = a.mean() # calculated using this code + self.assertQuantityEqual(mean0, mean1) + + def test_uncertaintity_nanmean(self): + a = UncertainQuantity([1,2], 'm', [.1,.2]) + b = UncertainQuantity([1,2,np.nan], 'm', [.1,.2,np.nan]) + self.assertQuantityEqual(a.mean(),b.nanmean()) + + def test_uncertainty_sqrt(self): + a = UncertainQuantity([1,2], 'm', [.1,.2]) + self.assertQuantityEqual(a**0.5, a.sqrt()) + + def test_uncertainty_nansum(self): + uq = UncertainQuantity([1,2], 'm', [1,1]) + uq_nan = UncertainQuantity([1,2,np.nan], 'm', [1,1,np.nan]) + self.assertQuantityEqual(np.sum(uq), np.nansum(uq)) + self.assertQuantityEqual(np.sum(uq), uq_nan.nansum()) + + def test_uncertainty_minmax_nan_arg(self): + q = [[1, 2], [3, 4]] * pq.m # quantity + self.assertQuantityEqual(q.min(), 1*pq.m) # min + self.assertQuantityEqual(q.max(), 4*pq.m) # max + self.assertQuantityEqual(q.argmin(), 0) # argmin + self.assertQuantityEqual(q.argmax(), 3) # argmax + # uncertain quantity + uq = UncertainQuantity([[1,2],[3,4]], pq.m, [[1,1],[1,1]]) + self.assertQuantityEqual(uq.min(), 1*pq.m) # min + self.assertQuantityEqual(uq.max(), 4*pq.m) # max + self.assertQuantityEqual(uq.argmin(), 0) # argmin + self.assertQuantityEqual(uq.argmax(), 3) # argmax + # now repeat the above with NaNs + nanq = [[1, 2], [3, 4], [np.nan,np.nan]] * pq.m # quantity + nanuq = UncertainQuantity([[1,2],[3,4],[np.nan,np.nan]], + pq.m, + [[1,1],[1,1],[np.nan,np.nan]]) + self.assertQuantityEqual(nanq.nanmin(), 1*pq.m) # min + self.assertQuantityEqual(nanq.nanmax(), 4*pq.m) # max + self.assertQuantityEqual(nanq.nanargmin(), 0) # argmin + self.assertQuantityEqual(nanq.nanargmax(), 3) # argmax + self.assertQuantityEqual(nanuq.nanmin(), 1*pq.m) # min + self.assertQuantityEqual(nanuq.nanmax(), 4*pq.m) # max + self.assertQuantityEqual(nanuq.nanargmin(), 0) # argmin + self.assertQuantityEqual(nanuq.nanargmax(), 3) # argmax diff --git a/quantities/tests/test_units.py b/quantities/tests/test_units.py index 67ebdecb..1912a3ae 100644 --- a/quantities/tests/test_units.py +++ b/quantities/tests/test_units.py @@ -1,8 +1,9 @@ -# -*- coding: utf-8 -*- +import pytest from .. import units as pq from .common import TestCase + class TestUnits(TestCase): def test_compound_units(self): @@ -32,3 +33,8 @@ def test_units_copy(self): self.assertQuantityEqual(pq.m.copy(), pq.m) pc_per_cc = pq.CompoundUnit("pc/cm**3") self.assertQuantityEqual(pc_per_cc.copy(), pc_per_cc) + + def test_code_injection(self): + with pytest.raises(LookupError) as exc_info: + pq.CompoundUnit("exec(\"print('Hello there.')\\nprint('General Wasabi!')\")") + assert "Wasabi" in str(exc_info.value) diff --git a/quantities/typing/__init__.py b/quantities/typing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/quantities/typing/quantities.pyi b/quantities/typing/quantities.pyi new file mode 100644 index 00000000..5d595a37 --- /dev/null +++ b/quantities/typing/quantities.pyi @@ -0,0 +1,9 @@ +from typing import Union, Iterable + +from quantities import Quantity +from quantities.dimensionality import Dimensionality +import numpy as np +import numpy.typing as npt + +DimensionalityDescriptor = Union[str, Quantity, Dimensionality] +QuantityData = Union[Quantity, npt.NDArray[Union[np.floating, np.integer]], Iterable[Union[float, int]], float, int] diff --git a/quantities/umath.py b/quantities/umath.py index 5c8019e4..16bc412f 100644 --- a/quantities/umath.py +++ b/quantities/umath.py @@ -1,15 +1,38 @@ -from __future__ import absolute_import - import numpy as np from .quantity import Quantity -from .units import dimensionless, radian, degree +from .units import dimensionless, radian, degree # type: ignore[no-redef] from .decorators import with_doc -#__all__ = [ -# 'exp', 'expm1', 'log', 'log10', 'log1p', 'log2' -#] +__all__ = [ + "arccos", + "arccosh", + "arcsin", + "arcsinh", + "arctan", + "arctan2", + "arctanh", + "cos", + "cosh", + "cross", + "cumprod", + "cumsum", + "diff", + "ediff1d", + "gradient", + "hypot", + "nansum", + "np", + "prod", + "sin", + "sinh", + "sum", + "tan", + "tanh", + "trapz", + "unwrap", +] @with_doc(np.prod) @@ -59,7 +82,7 @@ def ediff1d(ary, to_end=None, to_begin=None): @with_doc(np.gradient) def gradient(f, *varargs): # if no sample distances are specified, use dimensionless 1 - # this mimicks the behavior of np.gradient, but perhaps we should + # this mimics the behavior of np.gradient, but perhaps we should # remove this default behavior # removed for now:: # @@ -67,18 +90,18 @@ def gradient(f, *varargs): # varargs = (Quantity(1),) varargsQuantities = [Quantity(i, copy=False) for i in varargs] - varargsMag = tuple([i.magnitude for i in varargsQuantities]) + varargsMag = tuple(i.magnitude for i in varargsQuantities) ret = np.gradient(f.magnitude, *varargsMag) if len(varargs) == 1: # if there was only one sample distance provided, # apply the units in all directions - return tuple([ Quantity(i, f.units/varargs[0].units) for i in ret]) + return tuple( Quantity(i, f.units/varargs[0].units) for i in ret) else: #give each output array the units of the input array #divided by the units of the spacing quantity given - return tuple([ Quantity(i, f.units/j.units) - for i,j in zip( ret, varargsQuantities)]) + return tuple( Quantity(i, f.units/j.units) + for i,j in zip( ret, varargsQuantities)) @with_doc(np.cross) def cross (a, b , axisa=-1, axisb=-1, axisc=-1, axis=None): @@ -96,8 +119,65 @@ def cross (a, b , axisa=-1, axisb=-1, axisc=-1, axis=None): copy=False ) -@with_doc(np.trapz) + def trapz(y, x=None, dx=1.0, axis=-1): + r""" + Integrate along the given axis using the composite trapezoidal rule. + + If `x` is provided, the integration happens in sequence along its + elements - they are not sorted. + + Integrate `y` (`x`) along each 1d slice on the given axis, compute + :math:`\int y(x) dx`. + When `x` is specified, this integrates along the parametric curve, + computing :math:`\int_t y(t) dt = + \int_t y(t) \left.\frac{dx}{dt}\right|_{x=x(t)} dt`. + + Parameters + ---------- + y : array_like + Input array to integrate. + x : array_like, optional + The sample points corresponding to the `y` values. If `x` is None, + the sample points are assumed to be evenly spaced `dx` apart. The + default is None. + dx : scalar, optional + The spacing between sample points when `x` is None. The default is 1. + axis : int, optional + The axis along which to integrate. + + Returns + ------- + trapz : float or ndarray + Definite integral of `y` = n-dimensional array as approximated along + a single axis by the trapezoidal rule. If `y` is a 1-dimensional array, + then the result is a float. If `n` is greater than 1, then the result + is an `n`-1 dimensional array. + + See Also + -------- + sum, cumsum + + Notes + ----- + Image [2]_ illustrates trapezoidal rule -- y-axis locations of points + will be taken from `y` array, by default x-axis distances between + points will be 1.0, alternatively they can be provided with `x` array + or with `dx` scalar. Return value will be equal to combined area under + the red lines. + + Docstring is from the numpy 1.26 code base + https://github.com/numpy/numpy/blob/v1.26.0/numpy/lib/function_base.py#L4857-L4984 + + + References + ---------- + .. [1] Wikipedia page: https://en.wikipedia.org/wiki/Trapezoidal_rule + + .. [2] Illustration image: + https://en.wikipedia.org/wiki/File:Composite_trapezoidal_rule_illustration.png + + """ # this function has a weird input structure, so it is tricky to wrap it # perhaps there is a simpler way to do this if ( @@ -105,7 +185,7 @@ def trapz(y, x=None, dx=1.0, axis=-1): and not isinstance(x, Quantity) and not isinstance(dx, Quantity) ): - return np.trapz(y, x, dx, axis) + return _trapz(y, x, dx, axis) if not isinstance(y, Quantity): y = Quantity(y, copy = False) @@ -115,12 +195,50 @@ def trapz(y, x=None, dx=1.0, axis=-1): dx = Quantity(dx, copy = False) if x is None: - ret = np.trapz(y.magnitude , x, dx.magnitude, axis) + ret = _trapz(y.magnitude , x, dx.magnitude, axis) return Quantity ( ret, y.units * dx.units) else: - ret = np.trapz(y.magnitude , x.magnitude, dx.magnitude, axis) + ret = _trapz(y.magnitude , x.magnitude, dx.magnitude, axis) return Quantity ( ret, y.units * x.units) +def _trapz(y, x, dx, axis): + """ported from numpy 1.26 since it will be deprecated and removed""" + try: + # if scipy is available, we use it + from scipy.integrate import trapezoid # type: ignore + except ImportError: + # otherwise we use the implementation ported from numpy 1.26 + from numpy.core.numeric import asanyarray + from numpy.core.umath import add + y = asanyarray(y) + if x is None: + d = dx + else: + x = asanyarray(x) + if x.ndim == 1: + d = diff(x) + # reshape to correct shape + shape = [1]*y.ndim + shape[axis] = d.shape[0] + d = d.reshape(shape) + else: + d = diff(x, axis=axis) + nd = y.ndim + slice1 = [slice(None)]*nd + slice2 = [slice(None)]*nd + slice1[axis] = slice(1, None) + slice2[axis] = slice(None, -1) + try: + ret = (d * (y[tuple(slice1)] + y[tuple(slice2)]) / 2.0).sum(axis) + except ValueError: + # Operations didn't work, cast to ndarray + d = np.asarray(d) + y = np.asarray(y) + ret = add.reduce(d * (y[tuple(slice1)]+y[tuple(slice2)])/2.0, axis) + return ret + else: + return trapezoid(y, x=x, dx=dx, axis=axis) + @with_doc(np.sin) def sin(x, out=None): """ diff --git a/quantities/uncertainquantity.py b/quantities/uncertainquantity.py index 92946774..319a6cc1 100644 --- a/quantities/uncertainquantity.py +++ b/quantities/uncertainquantity.py @@ -1,13 +1,10 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import - -import sys import numpy as np +import warnings -from . import markup +from . import markup, QuantitiesDeprecationWarning from .quantity import Quantity, scale_other_units from .registry import unit_registry from .decorators import with_doc @@ -17,34 +14,39 @@ class UncertainQuantity(Quantity): # TODO: what is an appropriate value? __array_priority__ = 22 - def __new__(cls, data, units='', uncertainty=None, dtype='d', copy=True): - ret = Quantity.__new__(cls, data, units, dtype, copy) + def __new__(cls, data, units='', uncertainty=None, dtype='d', copy=None): + if copy is not None: + warnings.warn(("The 'copy' argument in UncertainQuantity is deprecated and will be removed in the future. " + "The argument has no effect since quantities-0.16.0 (to aid numpy-2.0 support)."), + QuantitiesDeprecationWarning, stacklevel=2) + ret = Quantity.__new__(cls, data, units, dtype) # _uncertainty initialized to be dimensionless by __array_finalize__: ret._uncertainty._dimensionality = ret._dimensionality if uncertainty is not None: ret.uncertainty = Quantity(uncertainty, ret._dimensionality) elif isinstance(data, UncertainQuantity): - if copy or ret._dimensionality != uncertainty._dimensionality: + is_copy = id(data) == id(ret) + if is_copy or ret._dimensionality != uncertainty._dimensionality: uncertainty = data.uncertainty.rescale(ret.units) ret.uncertainty = uncertainty return ret - @Quantity.units.setter + @Quantity.units.setter # type: ignore[attr-defined] def units(self, units): - super(UncertainQuantity, self)._set_units(units) + super()._set_units(units) self.uncertainty.units = self._dimensionality @property def _reference(self): - ret = super(UncertainQuantity, self)._reference.view(UncertainQuantity) + ret = super()._reference.view(UncertainQuantity) ret.uncertainty = self.uncertainty._reference return ret @property def simplified(self): - ret = super(UncertainQuantity, self).simplified.view(UncertainQuantity) + ret = super().simplified.view(UncertainQuantity) ret.uncertainty = self.uncertainty.simplified return ret @@ -54,7 +56,7 @@ def uncertainty(self): @uncertainty.setter def uncertainty(self, uncertainty): if not isinstance(uncertainty, Quantity): - uncertainty = Quantity(uncertainty, copy=False) + uncertainty = Quantity(uncertainty) try: assert self.shape == uncertainty.shape except AssertionError: @@ -68,10 +70,10 @@ def relative_uncertainty(self): return self.uncertainty.magnitude/self.magnitude @with_doc(Quantity.rescale, use_header=False) - def rescale(self, units): + def rescale(self, units, dtype=None): cls = UncertainQuantity - ret = super(cls, self).rescale(units).view(cls) - ret.uncertainty = self.uncertainty.rescale(units) + ret = super(cls, self).rescale(units, dtype=dtype).view(cls) + ret.uncertainty = self.uncertainty.rescale(units, dtype=dtype) return ret def __array_finalize__(self, obj): @@ -82,16 +84,15 @@ def __array_finalize__(self, obj): Quantity( np.zeros(self.shape, self.dtype), self._dimensionality, - copy=False ) ) @with_doc(Quantity.__add__, use_header=False) @scale_other_units def __add__(self, other): - res = super(UncertainQuantity, self).__add__(other) + res = super().__add__(other) u = (self.uncertainty**2+other.uncertainty**2)**0.5 - return UncertainQuantity(res, uncertainty=u, copy=False) + return UncertainQuantity(res, uncertainty=u) @with_doc(Quantity.__radd__, use_header=False) @scale_other_units @@ -101,28 +102,28 @@ def __radd__(self, other): @with_doc(Quantity.__sub__, use_header=False) @scale_other_units def __sub__(self, other): - res = super(UncertainQuantity, self).__sub__(other) + res = super().__sub__(other) u = (self.uncertainty**2+other.uncertainty**2)**0.5 - return UncertainQuantity(res, uncertainty=u, copy=False) + return UncertainQuantity(res, uncertainty=u) @with_doc(Quantity.__rsub__, use_header=False) @scale_other_units def __rsub__(self, other): if not isinstance(other, UncertainQuantity): - other = UncertainQuantity(other, copy=False) + other = UncertainQuantity(other) return UncertainQuantity.__sub__(other, self) @with_doc(Quantity.__mul__, use_header=False) def __mul__(self, other): - res = super(UncertainQuantity, self).__mul__(other) + res = super().__mul__(other) try: sru = self.relative_uncertainty oru = other.relative_uncertainty ru = (sru**2+oru**2)**0.5 u = res.view(Quantity) * ru except AttributeError: - other = np.array(other, copy=False, subok=True) + other = np.asanyarray(other) u = (self.uncertainty**2*other**2)**0.5 res._uncertainty = u @@ -132,16 +133,19 @@ def __mul__(self, other): def __rmul__(self, other): return self.__mul__(other) + def __neg__(self): + return self*-1 + @with_doc(Quantity.__truediv__, use_header=False) def __truediv__(self, other): - res = super(UncertainQuantity, self).__truediv__(other) + res = super().__truediv__(other) try: sru = self.relative_uncertainty oru = other.relative_uncertainty ru = (sru**2+oru**2)**0.5 u = res.view(Quantity) * ru except AttributeError: - other = np.array(other, copy=False, subok=True) + other = np.asanyarray(other) u = (self.uncertainty**2/other**2)**0.5 res._uncertainty = u @@ -151,17 +155,13 @@ def __truediv__(self, other): def __rtruediv__(self, other): temp = UncertainQuantity( 1/self.magnitude, self.dimensionality**-1, - self.relative_uncertainty/self.magnitude, copy=False + self.relative_uncertainty/self.magnitude ) return other * temp - if sys.version_info[0] < 3: - __div__ = __truediv__ - __rdiv__ = __rtruediv__ - @with_doc(Quantity.__pow__, use_header=False) def __pow__(self, other): - res = super(UncertainQuantity, self).__pow__(other) + res = super().__pow__(other) res.uncertainty = res.view(Quantity) * other * self.relative_uncertainty return res @@ -170,8 +170,7 @@ def __getitem__(self, key): return UncertainQuantity( self.magnitude[key], self._dimensionality, - self.uncertainty[key], - copy=False + self.uncertainty[key] ) @with_doc(Quantity.__repr__, use_header=False) @@ -203,22 +202,85 @@ def sum(self, axis=None, dtype=None, out=None): return UncertainQuantity( self.magnitude.sum(axis, dtype, out), self.dimensionality, - (np.sum(self.uncertainty.magnitude**2, axis))**0.5, - copy=False + (np.sum(self.uncertainty.magnitude**2, axis))**0.5 ) - def __getstate__(self): - """ - Return the internal state of the quantity, for pickling - purposes. + @with_doc(np.nansum) + def nansum(self, axis=None, dtype=None, out=None): + return UncertainQuantity( + np.nansum(self.magnitude, axis, dtype, out), + self.dimensionality, + (np.nansum(self.uncertainty.magnitude**2, axis))**0.5 + ) - """ - state = list(super(UncertainQuantity, self).__getstate__()) - state.append(self._uncertainty) - return tuple(state) + @with_doc(np.ndarray.mean) + def mean(self, axis=None, dtype=None, out=None): + return UncertainQuantity( + self.magnitude.mean(axis, dtype, out), + self.dimensionality, + ((1.0/np.size(self,axis))**2 * np.sum(self.uncertainty.magnitude**2, axis))**0.5 + ) + + @with_doc(np.nanmean) + def nanmean(self, axis=None, dtype=None, out=None): + size = np.sum(~np.isnan(self),axis) + return UncertainQuantity( + np.nanmean(self.magnitude, axis, dtype, out), + self.dimensionality, + ((1.0/size)**2 * np.nansum(np.nan_to_num(self.uncertainty.magnitude)**2, axis))**0.5 + ) + + @with_doc(np.sqrt) + def sqrt(self, out=None): + return self**0.5 + + @with_doc(np.ndarray.max) + def max(self, axis=None, out=None): + idx = np.unravel_index(np.argmax(self.magnitude), self.shape) + return self[idx] + + @with_doc(np.nanmax) + def nanmax(self, axis=None, out=None): + idx = np.unravel_index(np.nanargmax(self.magnitude), self.shape) + return self[idx] + + @with_doc(np.ndarray.min) + def min(self, axis=None, out=None): + idx = np.unravel_index(np.argmin(self.magnitude), self.shape) + return self[idx] + + @with_doc(np.nanmin) + def nanmin(self, axis=None, out=None): + idx = np.unravel_index(np.nanargmin(self.magnitude), self.shape) + return self[idx] + + @with_doc(np.ndarray.argmin) + def argmin(self,axis=None, out=None): + return self.magnitude.argmin() + + @with_doc(np.ndarray.argmax) + def argmax(self,axis=None, out=None): + return self.magnitude.argmax() + + @with_doc(np.nanargmin) + def nanargmin(self,axis=None, out=None): + return np.nanargmin(self.magnitude) + + @with_doc(np.nanargmax) + def nanargmax(self,axis=None, out=None): + return np.nanargmax(self.magnitude) def __setstate__(self, state): - (ver, shp, typ, isf, raw, units, sigma) = state - np.ndarray.__setstate__(self, (shp, typ, isf, raw)) + ndarray_state = state[:-2] + units, sigma = state[-2:] + np.ndarray.__setstate__(self, ndarray_state) self._dimensionality = units self._uncertainty = sigma + + def __reduce__(self): + """ + Return a tuple for pickling a Quantity. + """ + reconstruct, reconstruct_args, state = super().__reduce__() + state = state + (self._uncertainty,) + return reconstruct, reconstruct_args, state diff --git a/quantities/unitquantity.py b/quantities/unitquantity.py index 9ecc0ebe..d5487fc5 100644 --- a/quantities/unitquantity.py +++ b/quantities/unitquantity.py @@ -1,8 +1,6 @@ """ """ -from __future__ import absolute_import -import sys import weakref import numpy @@ -216,15 +214,6 @@ def __truediv__(self, other): def __rtruediv__(self, other): return self.view(Quantity).__rtruediv__(other) - if sys.version_info[0] < 3: - @with_doc(Quantity.__div__, use_header=False) - def __div__(self, other): - return self.view(Quantity).__div__(other) - - @with_doc(Quantity.__rdiv__, use_header=False) - def __rdiv__(self, other): - return self.view(Quantity).__rdiv__(other) - @with_doc(Quantity.__pow__, use_header=False) def __pow__(self, other): return self.view(Quantity).__pow__(other) @@ -249,11 +238,6 @@ def __imul__(self, other): def __itruediv__(self, other): raise TypeError('can not modify protected units') - if sys.version_info[0] < 3: - @with_doc(Quantity.__idiv__, use_header=False) - def __idiv__(self, other): - raise TypeError('can not modify protected units') - @with_doc(Quantity.__ipow__, use_header=False) def __ipow__(self, other): raise TypeError('can not modify protected units') @@ -311,7 +295,7 @@ def __init__( self, name, definition=None, symbol=None, u_symbol=None, aliases=[], doc=None ): - super(IrreducibleUnit, self).__init__( + super().__init__( name, definition, symbol, u_symbol, aliases, doc ) cls = type(self) @@ -515,3 +499,4 @@ def set_default_units( UnitTemperature.set_default_unit(temperature) UnitTime.set_default_unit(time) + diff --git a/quantities/unitquantity.pyi b/quantities/unitquantity.pyi new file mode 100644 index 00000000..772799f6 --- /dev/null +++ b/quantities/unitquantity.pyi @@ -0,0 +1,173 @@ +from typing import Any, List, Optional, Union, overload + +from quantities import Quantity +from quantities.dimensionality import Dimensionality + +class UnitQuantity(Quantity): + _primary_order: int + _secondary_order: int + _reference_quantity: Optional[Quantity] + + def __new__( + cls, name: str, definition: Optional[Union[Quantity, float, int]] = ..., symbol: Optional[str] = ..., + u_symbol: Optional[str] = ..., + aliases: List[str] = ..., doc=... + ) -> UnitQuantity: + ... + + def __init__( + self, name: str, definition: Optional[Union[Quantity, float, int]] = ..., symbol: Optional[str] = ..., + u_symbol: Optional[str] = ..., + aliases: List[str] = ..., doc=... + ) -> None: + ... + + def __hash__(self) -> int: # type: ignore[override] + ... + + @property + def _reference(self) -> UnitQuantity: + ... + + @property + def _dimensionality(self) -> Dimensionality: + ... + + @property + def name(self) -> str: + ... + + @property + def symbol(self) -> str: + ... + + @property + def u_symbol(self) -> str: + ... + + @property + def units(self) -> UnitQuantity: + ... + + def __repr__(self) -> str: + ... + + def __str__(self) -> str: + ... + + def __add__(self, other) -> Quantity: + ... + + def __radd__(self, other) -> Quantity: + ... + + def __sub__(self, other) -> Any: + ... + + + def __rsub__(self, other) -> Any: + ... + + def __mod__(self, other) -> Quantity: + ... + + def __rmod__(self, other) -> Quantity: + ... + + def __mul__(self, other) -> Quantity: + ... + + def __rmul__(self, other) -> Quantity: + ... + + def __truediv__(self, other) -> Any: + ... + + def __rtruediv__(self, other) -> Any: + ... + + def __pow__(self, other) -> Quantity: + ... + + def __rpow__(self, other) -> Quantity: + ... + + +class IrreducibleUnit(UnitQuantity): + _default_unit: Optional[Quantity] + + @property + def simplified(self) -> Quantity: + ... + + @classmethod + def get_default_unit(cls) -> Optional[Quantity]: + ... + + @classmethod + def set_default_unit(cls, unit: Union[str, Quantity]): + ... + + +class UnitMass(IrreducibleUnit): + ... + + +class UnitLength(IrreducibleUnit): + ... + + +class UnitTime(IrreducibleUnit): + ... + + +class UnitCurrent(IrreducibleUnit): + ... + +class UnitLuminousIntensity(IrreducibleUnit): + ... + + +class UnitSubstance(IrreducibleUnit): + ... + + +class UnitTemperature(IrreducibleUnit): + ... + + +class UnitInformation(IrreducibleUnit): + ... + + +class UnitCurrency(IrreducibleUnit): + ... + + +class CompoundUnit(UnitQuantity): + ... + + +class Dimensionless(UnitQuantity): + + @property + def _dimensionality(self) -> Dimensionality: + ... + +dimensionless: Dimensionless + +class UnitConstant(UnitQuantity): + ... + + +def set_default_units(system: Optional[str] = ..., + currency: Optional[Union[str, UnitCurrency]] = ..., + current: Optional[Union[str, UnitCurrent]] = ..., + information: Optional[Union[str, UnitInformation]] = ..., + length: Optional[Union[str, UnitLength]] = ..., + luminous_intensity: Optional[Union[str, UnitLuminousIntensity]] = ..., + mass: Optional[Union[str, UnitMass]] = ..., + substance: Optional[Union[str, UnitSubstance]] = ..., + temperature: Optional[Union[str, UnitTemperature]] = ..., + time: Optional[Union[str, UnitTime]] = ...): + ... diff --git a/quantities/units/__init__.py b/quantities/units/__init__.py index 48a34fe0..dbd63f41 100644 --- a/quantities/units/__init__.py +++ b/quantities/units/__init__.py @@ -1,13 +1,24 @@ """ """ -from __future__ import absolute_import from . import prefixes from .prefixes import * from . import acceleration -from .acceleration import * +from .acceleration import ( + g_0, + g_n, + gravity, + standard_gravity, + gee, + # force, + free_fall, + standard_free_fall, + gp, + dynamic, + geopotential, +) from . import angle from .angle import * @@ -18,8 +29,15 @@ from . import compound from .compound import * -from . import dimensionless -from .dimensionless import * +from . import concentration +from .concentration import * + +from . import dimensionless as _dimensionless +from .dimensionless import ( + percent, + count, counts, + lsb, +) from . import electromagnetism from .electromagnetism import * @@ -40,7 +58,44 @@ from .information import * from . import length -from .length import * +from .length import ( + m, meter, metre, + km, kilometer, kilometre, + dm, decimeter, decimetre, + cm, centimeter, centimetre, + mm, millimeter, millimetre, + um, micrometer, micrometre, micron, + nm, nanometer, nanometre, + pm, picometer, picometre, + angstrom, + fm, femtometer, femtometre, fermi, + + inch, international_inch, + ft, foot, international_foot, + mi, mile, international_mile, + yd, yard, international_yard, + mil, thou, + pc, parsec, + ly, light_year, + au, astronomical_unit, + + nmi, nautical_mile, + # pt, + printers_point, point, + pica, + + US_survey_foot, + US_survey_yard, + US_survey_mile, US_statute_mile, + rod, pole, perch, + furlong, + fathom, + chain, + barleycorn, + arpentlin, + + kayser, wavenumber +) from . import mass from .mass import * @@ -70,6 +125,41 @@ from .viscosity import * from . import volume -from .volume import * +from .volume import ( + l, L, liter, litre, + mL, milliliter, millilitre, + kL, kiloliter, kilolitre, + ML, megaliter, megalitre, + GL, gigaliter, gigalitre, + cc, cubic_centimeter, milliliter, + stere, + gross_register_ton, register_ton, + acre_foot, + board_foot, + bu, bushel, US_bushel, + US_dry_gallon, + gallon, liquid_gallon, US_liquid_gallon, + dry_quart, US_dry_quart, + dry_pint, US_dry_pint, + quart, liquid_quart, US_liquid_quart, + pt, pint, liquid_pint, US_liquid_pint, + cup, US_liquid_cup, + gill, US_liquid_gill, + floz, fluid_ounce, US_fluid_ounce, US_liquid_ounce, + Imperial_bushel, + UK_liquid_gallon, Canadian_liquid_gallon, + UK_liquid_quart, + UK_liquid_pint, + UK_liquid_cup, + UK_liquid_gill, + UK_fluid_ounce, UK_liquid_ounce, + bbl, barrel, + tbsp, Tbsp, Tblsp, tblsp, tbs, Tbl, tablespoon, + tsp, teaspoon, + pk, peck, + fldr, fluid_dram, fluidram, + firkin, +) from ..unitquantity import set_default_units +from ..unitquantity import dimensionless diff --git a/quantities/units/acceleration.py b/quantities/units/acceleration.py index d476857c..2641dc29 100644 --- a/quantities/units/acceleration.py +++ b/quantities/units/acceleration.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ..unitquantity import UnitQuantity from .time import s diff --git a/quantities/units/acceleration.pyi b/quantities/units/acceleration.pyi new file mode 100644 index 00000000..20bdbb46 --- /dev/null +++ b/quantities/units/acceleration.pyi @@ -0,0 +1,13 @@ +from ..unitquantity import UnitQuantity + +standard_free_fall: UnitQuantity +gp: UnitQuantity +dynamic: UnitQuantity +geopotential: UnitQuantity +g_0: UnitQuantity +g_n: UnitQuantity +gravity: UnitQuantity +standard_gravity: UnitQuantity +gee: UnitQuantity +force: UnitQuantity +free_fall: UnitQuantity \ No newline at end of file diff --git a/quantities/units/angle.py b/quantities/units/angle.py index ec6feb81..fbe812bf 100644 --- a/quantities/units/angle.py +++ b/quantities/units/angle.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from math import pi diff --git a/quantities/units/angle.pyi b/quantities/units/angle.pyi new file mode 100644 index 00000000..de3f8c09 --- /dev/null +++ b/quantities/units/angle.pyi @@ -0,0 +1,41 @@ +from ..unitquantity import UnitQuantity + +rad: UnitQuantity +radian: UnitQuantity +radians: UnitQuantity +mrad: UnitQuantity +milliradian: UnitQuantity +urad: UnitQuantity +microradian: UnitQuantity +turn: UnitQuantity +revolution: UnitQuantity +cycle: UnitQuantity +turns: UnitQuantity +circle: UnitQuantity +circles: UnitQuantity +deg: UnitQuantity +degree: UnitQuantity +degrees: UnitQuantity +arcdeg: UnitQuantity +arcdegree: UnitQuantity +angular_degree: UnitQuantity +arcminute: UnitQuantity +arcmin: UnitQuantity +arc_minute: UnitQuantity +angular_minute: UnitQuantity +arcsecond: UnitQuantity +arcsec: UnitQuantity +arc_second: UnitQuantity +angular_second: UnitQuantity +grad: UnitQuantity +grade: UnitQuantity +degrees_north: UnitQuantity +degrees_N: UnitQuantity +degrees_east: UnitQuantity +degrees_E: UnitQuantity +degrees_west: UnitQuantity +degrees_W: UnitQuantity +degrees_true: UnitQuantity +degrees_T: UnitQuantity +sr: UnitQuantity +steradian: UnitQuantity diff --git a/quantities/units/area.py b/quantities/units/area.py index 92376d0d..17c66fe5 100644 --- a/quantities/units/area.py +++ b/quantities/units/area.py @@ -1,6 +1,5 @@ """ """ -from __future__ import absolute_import from ..unitquantity import UnitQuantity from .length import m, rod diff --git a/quantities/units/area.pyi b/quantities/units/area.pyi new file mode 100644 index 00000000..34da5b7e --- /dev/null +++ b/quantities/units/area.pyi @@ -0,0 +1,18 @@ +from ..unitquantity import UnitQuantity + + +are: UnitQuantity +ares: UnitQuantity +b: UnitQuantity +barn: UnitQuantity +cmil: UnitQuantity +circular_mil: UnitQuantity +D: UnitQuantity +darcy: UnitQuantity +mD: UnitQuantity +millidarcy: UnitQuantity +ha: UnitQuantity +hectare: UnitQuantity +acre: UnitQuantity +international_acre: UnitQuantity +US_survey_acre: UnitQuantity diff --git a/quantities/units/compound.py b/quantities/units/compound.py index 5b5406af..dcc4d0b7 100644 --- a/quantities/units/compound.py +++ b/quantities/units/compound.py @@ -1,6 +1,5 @@ """ """ -from __future__ import absolute_import from ..unitquantity import CompoundUnit diff --git a/quantities/units/compound.pyi b/quantities/units/compound.pyi new file mode 100644 index 00000000..4a8e0e67 --- /dev/null +++ b/quantities/units/compound.pyi @@ -0,0 +1,3 @@ +from ..unitquantity import CompoundUnit + +pc_per_cc: CompoundUnit diff --git a/quantities/units/concentration.py b/quantities/units/concentration.py new file mode 100644 index 00000000..013ebb38 --- /dev/null +++ b/quantities/units/concentration.py @@ -0,0 +1,26 @@ +""" +""" + +from ..unitquantity import UnitQuantity +from .substance import mol +from .volume import L + +M = molar = UnitQuantity( + 'molar', + mol / L, + symbol='M', + aliases=['Molar'] +) + +mM = millimolar = UnitQuantity( + 'millimolar', + molar / 1000, + symbol='mM' +) + +uM = micromolar = UnitQuantity( + 'micromolar', + mM / 1000, + symbol='uM', + u_symbol='µM' +) diff --git a/quantities/units/concentration.pyi b/quantities/units/concentration.pyi new file mode 100644 index 00000000..f82f37fb --- /dev/null +++ b/quantities/units/concentration.pyi @@ -0,0 +1,9 @@ +from ..unitquantity import UnitQuantity + + +M: UnitQuantity +molar: UnitQuantity +mM: UnitQuantity +millimolar: UnitQuantity +uM: UnitQuantity +micromolar: UnitQuantity diff --git a/quantities/units/dimensionless.py b/quantities/units/dimensionless.py index bf6a73b0..5efdd9a9 100644 --- a/quantities/units/dimensionless.py +++ b/quantities/units/dimensionless.py @@ -1,6 +1,5 @@ """ """ -from __future__ import absolute_import from ..unitquantity import dimensionless, UnitQuantity @@ -17,4 +16,11 @@ aliases=['cts', 'counts'] ) +lsb = UnitQuantity( + 'least_significant_bit', + 1*dimensionless, + symbol='lsb', + aliases=['lsbs'] +) + del UnitQuantity diff --git a/quantities/units/dimensionless.pyi b/quantities/units/dimensionless.pyi new file mode 100644 index 00000000..a18340fa --- /dev/null +++ b/quantities/units/dimensionless.pyi @@ -0,0 +1,7 @@ +from ..unitquantity import UnitQuantity + + +percent: UnitQuantity +count: UnitQuantity +counts: UnitQuantity +lsb: UnitQuantity diff --git a/quantities/units/electromagnetism.py b/quantities/units/electromagnetism.py index b2f6da52..c22ac87b 100644 --- a/quantities/units/electromagnetism.py +++ b/quantities/units/electromagnetism.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ..unitquantity import UnitCurrent, UnitLuminousIntensity, UnitQuantity from .time import s @@ -76,6 +74,17 @@ A*s, symbol='C' ) +mC = millicoulomb = UnitQuantity( + 'millicoulomb', + 1e-3*C, + symbol='mC' +) +uC = microcoulomb = UnitQuantity( + 'microcoulomb', + 1e-6*C, + symbol='uC', + u_symbol='μC' +) V = volt = UnitQuantity( 'volt', J/C, @@ -107,17 +116,76 @@ symbol='F', aliases=['farads'] ) -ohm = UnitQuantity( +mF = UnitQuantity( + 'millifarad', + F/1000, + symbol='mF' +) +uF = UnitQuantity( + 'microfarad', + mF/1000, + symbol='uF', + u_symbol='μF' +) +nF = UnitQuantity( + 'nanofarad', + uF/1000, + symbol='nF' +) +pF = UnitQuantity( + 'picofarad', + nF/1000, + symbol='pF' +) +fF = UnitQuantity( + 'femtofarad', + pF/1000, + symbol='fF' +) +ohm = Ohm = UnitQuantity( 'ohm', V/A, u_symbol='Ω', - aliases=['ohms'] + aliases=['ohms', 'Ohm'] +) +kOhm = UnitQuantity( + 'kiloohm', + ohm*1000, + u_symbol='kΩ', + aliases=['kOhm', 'kohm', 'kiloohms'] +) +MOhm = UnitQuantity( + 'megaohm', + kOhm*1000, + u_symbol='MΩ', + aliases=['MOhm', 'Mohm', 'megaohms'] ) S = siemens = UnitQuantity( 'siemens', A/V, symbol='S' ) +mS = millisiemens = UnitQuantity( + 'millisiemens', + S/1000, + symbol='mS' +) +uS = microsiemens = UnitQuantity( + 'microsiemens', + mS/1000, + symbol='uS', + u_symbol='μS' +) +nS = nanosiemens = UnitQuantity( + 'nanosiemens', + uS/1000, + symbol='nS' +) +pS = picosiemens = UnitQuantity( + 'picosiemens', + nS/1000, + symbol='pS' +) Wb = weber = UnitQuantity( 'weber', V*s, diff --git a/quantities/units/electromagnetism.pyi b/quantities/units/electromagnetism.pyi new file mode 100644 index 00000000..bdc6ddb1 --- /dev/null +++ b/quantities/units/electromagnetism.pyi @@ -0,0 +1,110 @@ +from ..unitquantity import UnitCurrent, UnitLuminousIntensity, UnitQuantity + +A: UnitCurrent +amp: UnitCurrent +amps: UnitCurrent +ampere: UnitCurrent +amperes: UnitCurrent +mA: UnitCurrent +milliamp: UnitCurrent +milliampere: UnitCurrent +uA: UnitCurrent +microampere: UnitCurrent +nA: UnitCurrent +nanoamp: UnitCurrent +nanoampere: UnitCurrent +pA: UnitCurrent +picoamp: UnitCurrent +picoampere: UnitCurrent +aA: UnitCurrent +abampere: UnitCurrent +biot: UnitCurrent +esu: UnitQuantity +statcoulomb: UnitQuantity +statC: UnitQuantity +franklin: UnitQuantity +Fr: UnitQuantity +esu_per_second: UnitCurrent +statampere: UnitCurrent +ampere_turn: UnitQuantity +Gi: UnitQuantity +gilbert: UnitQuantity +C: UnitQuantity +coulomb: UnitQuantity +mC: UnitQuantity +millicoulomb: UnitQuantity +uC: UnitQuantity +microcoulomb: UnitQuantity +V: UnitQuantity +volt: UnitQuantity +kV: UnitQuantity +kilovolt: UnitQuantity +mV: UnitQuantity +millivolt: UnitQuantity +uV: UnitQuantity +microvolt: UnitQuantity +F: UnitQuantity +farad: UnitQuantity +mF: UnitQuantity +uF: UnitQuantity +nF: UnitQuantity +pF: UnitQuantity +ohm: UnitQuantity +Ohm: UnitQuantity +kOhm: UnitQuantity +MOhm: UnitQuantity +S: UnitQuantity +siemens: UnitQuantity +mS: UnitQuantity +millisiemens: UnitQuantity +uS: UnitQuantity +microsiemens: UnitQuantity +nS: UnitQuantity +nanosiemens: UnitQuantity +pS: UnitQuantity +picosiemens: UnitQuantity +Wb: UnitQuantity +weber: UnitQuantity +T: UnitQuantity +tesla: UnitQuantity +H: UnitQuantity +henry: UnitQuantity +abfarad: UnitQuantity +abhenry: UnitQuantity +abmho: UnitQuantity +abohm: UnitQuantity +abvolt: UnitQuantity +e: UnitQuantity +elementary_charge: UnitQuantity +chemical_faraday: UnitQuantity +physical_faraday: UnitQuantity +faraday: UnitQuantity +C12_faraday: UnitQuantity +gamma: UnitQuantity +gauss: UnitQuantity +maxwell: UnitQuantity +Oe: UnitQuantity +oersted: UnitQuantity +statfarad: UnitQuantity +statF: UnitQuantity +stF: UnitQuantity +stathenry: UnitQuantity +statH: UnitQuantity +stH: UnitQuantity +statmho: UnitQuantity +statS: UnitQuantity +stS: UnitQuantity +statohm: UnitQuantity +statvolt: UnitQuantity +statV: UnitQuantity +stV: UnitQuantity +unit_pole: UnitQuantity +vacuum_permeability: UnitQuantity +mu_0: UnitQuantity +magnetic_constant: UnitQuantity +vacuum_permittivity: UnitQuantity +epsilon_0: UnitQuantity +electric_constant: UnitQuantity +cd: UnitLuminousIntensity +candle: UnitLuminousIntensity +candela: UnitLuminousIntensity diff --git a/quantities/units/energy.py b/quantities/units/energy.py index 19a4f7f0..a40f495b 100644 --- a/quantities/units/energy.py +++ b/quantities/units/energy.py @@ -1,6 +1,5 @@ """ """ -from __future__ import absolute_import from ..unitquantity import UnitQuantity from .force import dyne, N diff --git a/quantities/units/energy.pyi b/quantities/units/energy.pyi new file mode 100644 index 00000000..e811e976 --- /dev/null +++ b/quantities/units/energy.pyi @@ -0,0 +1,40 @@ +from ..unitquantity import UnitQuantity + +J: UnitQuantity +joule: UnitQuantity +erg: UnitQuantity +btu: UnitQuantity +Btu: UnitQuantity +BTU: UnitQuantity +british_thermal_unit: UnitQuantity +eV: UnitQuantity +electron_volt: UnitQuantity +meV: UnitQuantity +keV: UnitQuantity +MeV: UnitQuantity +bev: UnitQuantity +GeV: UnitQuantity +thm: UnitQuantity +therm: UnitQuantity +EC_therm: UnitQuantity +cal: UnitQuantity +calorie: UnitQuantity +thermochemical_calorie: UnitQuantity +international_steam_table_calorie: UnitQuantity +ton_TNT: UnitQuantity +US_therm: UnitQuantity +Wh: UnitQuantity +watthour: UnitQuantity +watt_hour: UnitQuantity +kWh: UnitQuantity +kilowatthour: UnitQuantity +kilowatt_hour: UnitQuantity +MWh: UnitQuantity +megawatthour: UnitQuantity +megawatt_hour: UnitQuantity +GWh: UnitQuantity +gigawatthour: UnitQuantity +gigawatt_hour: UnitQuantity +E_h: UnitQuantity +hartree: UnitQuantity +hartree_energy: UnitQuantity diff --git a/quantities/units/force.py b/quantities/units/force.py index 8a50164c..c903969a 100644 --- a/quantities/units/force.py +++ b/quantities/units/force.py @@ -1,6 +1,5 @@ """ """ -from __future__ import absolute_import from ..unitquantity import UnitQuantity from .mass import gram, kg, ounce, lb @@ -15,6 +14,12 @@ symbol='N', aliases=['newtons'] ) +kN = kilonewton = UnitQuantity( + 'kilonewton', + 1000*N, + symbol='kN', + aliases=['kilonewtons'] +) dyne = UnitQuantity( 'dyne', gram*cm/s**2, diff --git a/quantities/units/force.pyi b/quantities/units/force.pyi new file mode 100644 index 00000000..43f52c63 --- /dev/null +++ b/quantities/units/force.pyi @@ -0,0 +1,23 @@ +from ..unitquantity import UnitQuantity + +N: UnitQuantity +newton: UnitQuantity +kilonewton: UnitQuantity +dyne: UnitQuantity +pond: UnitQuantity +kgf: UnitQuantity +force_kilogram: UnitQuantity +kilogram_force: UnitQuantity +ozf: UnitQuantity +force_ounce: UnitQuantity +ounce_force: UnitQuantity +lbf: UnitQuantity +force_pound: UnitQuantity +pound_force: UnitQuantity +poundal: UnitQuantity +gf: UnitQuantity +gram_force: UnitQuantity +force_gram: UnitQuantity +force_ton: UnitQuantity +ton_force: UnitQuantity +kip: UnitQuantity diff --git a/quantities/units/frequency.py b/quantities/units/frequency.py index 0c28b1a4..24c20c73 100644 --- a/quantities/units/frequency.py +++ b/quantities/units/frequency.py @@ -1,6 +1,5 @@ """ """ -from __future__ import absolute_import from ..unitquantity import UnitQuantity from .angle import revolution diff --git a/quantities/units/frequency.pyi b/quantities/units/frequency.pyi new file mode 100644 index 00000000..3f9bb67a --- /dev/null +++ b/quantities/units/frequency.pyi @@ -0,0 +1,14 @@ +from ..unitquantity import UnitQuantity + +Hz: UnitQuantity +hertz: UnitQuantity +rps: UnitQuantity +kHz: UnitQuantity +kilohertz: UnitQuantity +MHz: UnitQuantity +megahertz: UnitQuantity +GHz: UnitQuantity +gigahertz: UnitQuantity +rpm: UnitQuantity +revolutions_per_minute: UnitQuantity +cps: UnitQuantity diff --git a/quantities/units/heat.py b/quantities/units/heat.py index 50694116..4ca1509a 100644 --- a/quantities/units/heat.py +++ b/quantities/units/heat.py @@ -1,6 +1,5 @@ """ """ -from __future__ import absolute_import from ..unitquantity import UnitQuantity from .temperature import K, degF diff --git a/quantities/units/heat.pyi b/quantities/units/heat.pyi new file mode 100644 index 00000000..9ed86a7c --- /dev/null +++ b/quantities/units/heat.pyi @@ -0,0 +1,6 @@ +from ..unitquantity import UnitQuantity + +RSI: UnitQuantity +clo: UnitQuantity +clos: UnitQuantity +R_value: UnitQuantity diff --git a/quantities/units/information.py b/quantities/units/information.py index b9c78dff..d1385779 100644 --- a/quantities/units/information.py +++ b/quantities/units/information.py @@ -1,6 +1,5 @@ """ """ -from __future__ import absolute_import from ..unitquantity import UnitQuantity, UnitInformation, dimensionless from .time import s @@ -15,10 +14,108 @@ symbol='B', aliases=['bytes', 'o', 'octet', 'octets'] ) +kB = kilobyte = ko = UnitInformation( + 'kilobyte', + 1000 * byte, + symbol='kB', + aliases=['kilobytes', 'kilooctet', 'kilooctets'] +) +MB = megabyte = Mo = UnitInformation( + 'megabyte', + 1000 * kilobyte, + symbol='MB', + aliases=['megabytes', 'megaoctet', 'megaoctets'] +) +GB = gigabyte = Go = UnitInformation( + 'gigabyte', + 1000 * megabyte, + symbol='GB', + aliases=['gigabytes', 'gigaoctet', 'gigaoctets'] +) +TB = terabyte = To = UnitInformation( + 'terabyte', + 1000 * gigabyte, + symbol='TB', + aliases=['terabytes', 'teraoctet', 'teraoctets'] +) +PB = petabyte = Po = UnitInformation( + 'petabyte', + 1000 * terabyte, + symbol='PB', + aliases=['petabytes', 'petaoctet', 'petaoctets'] +) +EB = exabyte = Eo = UnitInformation( + 'exabyte', + 1000 * petabyte, + symbol='EB', + aliases=['exabytes', 'exaoctet', 'exaoctets'] +) +ZB = zettabyte = Zo = UnitInformation( + 'zettabyte', + 1000 * exabyte, + symbol='ZB', + aliases=['zettabytes', 'zettaoctet', 'zettaoctets'] +) +YB = yottabyte = Yo = UnitInformation( + 'yottabyte', + 1000 * zettabyte, + symbol='YB', + aliases=['yottabytes', 'yottaoctet', 'yottaoctets'] +) Bd = baud = bps = UnitQuantity( 'baud', bit/s, symbol='Bd', ) +# IEC +KiB = kibibyte = Kio = UnitInformation( + 'kibibyte', + 1024 * byte, + symbol='KiB', + aliases=['kibibytes', 'kibioctet', 'kibioctets'] +) +MiB = mebibyte = Mio = UnitInformation( + 'mebibyte', + 1024 * kibibyte, + symbol='MiB', + aliases=['mebibytes', 'mebioctet', 'mebioctets'] +) +GiB = gibibyte = Gio = UnitInformation( + 'gibibyte', + 1024 * mebibyte, + symbol='GiB', + aliases=['gibibytes', 'gibioctet', 'gibioctets'] +) +TiB = tebibyte = Tio = UnitInformation( + 'tebibyte', + 1024 * gibibyte, + symbol='TiB', + aliases=['tebibytes', 'tebioctet', 'tebioctets'] +) +PiB = pebibyte = Pio = UnitInformation( + 'pebibyte', + 1024 * tebibyte, + symbol='PiB', + aliases=['pebibytes', 'pebioctet', 'pebioctets'] +) +EiB = exbibyte = Eio = UnitInformation( + 'exbibyte', + 1024 * pebibyte, + symbol='EiB', + aliases=['exbibytes', 'exbioctet', 'exbioctets'] +) +ZiB = zebibyte = Zio = UnitInformation( + 'zebibyte', + 1024 * exbibyte, + symbol='ZiB', + aliases=['zebibytes', 'zebioctet', 'zebioctets'] +) +YiB = yobibyte = Yio = UnitInformation( + 'yobibyte', + 1024 * zebibyte, + symbol='YiB', + aliases=['yobibytes', 'yobioctet', 'yobioctets'] +) + del UnitQuantity, s, dimensionless diff --git a/quantities/units/information.pyi b/quantities/units/information.pyi new file mode 100644 index 00000000..423243f5 --- /dev/null +++ b/quantities/units/information.pyi @@ -0,0 +1,58 @@ +from ..unitquantity import UnitQuantity, UnitInformation + +bit: UnitInformation +B: UnitInformation +byte: UnitInformation +o: UnitInformation +octet: UnitInformation +kB: UnitInformation +kilobyte: UnitInformation +ko: UnitInformation +MB: UnitInformation +megabyte: UnitInformation +Mo: UnitInformation +GB: UnitInformation +gigabyte: UnitInformation +Go: UnitInformation +TB: UnitInformation +terabyte: UnitInformation +To: UnitInformation +PB: UnitInformation +petabyte: UnitInformation +Po: UnitInformation +EB: UnitInformation +exabyte: UnitInformation +Eo: UnitInformation +ZB: UnitInformation +zettabyte: UnitInformation +Zo: UnitInformation +YB: UnitInformation +yottabyte: UnitInformation +Yo: UnitInformation +Bd: UnitQuantity +baud: UnitQuantity +bps: UnitQuantity +KiB: UnitInformation +kibibyte: UnitInformation +Kio: UnitInformation +MiB: UnitInformation +mebibyte: UnitInformation +Mio: UnitInformation +GiB: UnitInformation +gibibyte: UnitInformation +Gio: UnitInformation +TiB: UnitInformation +tebibyte: UnitInformation +Tio: UnitInformation +PiB: UnitInformation +pebibyte: UnitInformation +Pio: UnitInformation +EiB: UnitInformation +exbibyte: UnitInformation +Eio: UnitInformation +ZiB: UnitInformation +zebibyte: UnitInformation +Zio: UnitInformation +YiB: UnitInformation +yobibyte: UnitInformation +Yio: UnitInformation diff --git a/quantities/units/length.py b/quantities/units/length.py index 0a7e822e..5634f751 100644 --- a/quantities/units/length.py +++ b/quantities/units/length.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ..unitquantity import UnitLength, UnitQuantity @@ -16,6 +14,12 @@ symbol='km', aliases=['kilometers', 'kilometre', 'kilometres'] ) +dm = decimeter = decimetre = UnitLength( + 'decimeter', + m/10, + 'dm', + aliases=['decimeters', 'decimetre', 'decimetres'] +) cm = centimeter = centimetre = UnitLength( 'centimeter', m/100, diff --git a/quantities/units/length.pyi b/quantities/units/length.pyi new file mode 100644 index 00000000..94a4f001 --- /dev/null +++ b/quantities/units/length.pyi @@ -0,0 +1,71 @@ +from ..unitquantity import UnitLength, UnitQuantity + +m: UnitLength +meter: UnitLength +metre: UnitLength +km: UnitLength +kilometer: UnitLength +kilometre: UnitLength +dm: UnitLength +decimeter: UnitLength +decimetre: UnitLength +cm: UnitLength +centimeter: UnitLength +centimetre: UnitLength +mm: UnitLength +millimeter: UnitLength +millimetre: UnitLength +um: UnitLength +micrometer: UnitLength +micrometre: UnitLength +micron: UnitLength +nm: UnitLength +nanometer: UnitLength +nanometre: UnitLength +pm: UnitLength +picometer: UnitLength +picometre: UnitLength +angstrom: UnitLength +fm: UnitLength +femtometer: UnitLength +femtometre: UnitLength +fermi: UnitLength +inch: UnitLength +international_inch: UnitLength +ft: UnitLength +foot: UnitLength +international_foot: UnitLength +mi: UnitLength +mile: UnitLength +international_mile: UnitLength +yd: UnitLength +yard: UnitLength +international_yard: UnitLength +mil: UnitLength +thou: UnitLength +pc: UnitLength +parsec: UnitLength +ly: UnitLength +light_year: UnitLength +au: UnitLength +astronomical_unit: UnitLength +nmi: UnitLength +nautical_mile: UnitLength +pt: UnitLength +printers_point: UnitLength +point: UnitLength +pica: UnitLength +US_survey_foot: UnitLength +US_survey_yard: UnitLength +US_survey_mile: UnitLength +US_statute_mile: UnitLength +rod: UnitLength +pole: UnitLength +perch: UnitLength +furlong: UnitLength +fathom: UnitLength +chain: UnitLength +barleycorn: UnitLength +arpentlin: UnitLength +kayser: UnitQuantity +wavenumber: UnitQuantity diff --git a/quantities/units/mass.py b/quantities/units/mass.py index c4f27c8a..02c67d84 100644 --- a/quantities/units/mass.py +++ b/quantities/units/mass.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ..unitquantity import UnitQuantity, UnitMass from .length import m diff --git a/quantities/units/mass.pyi b/quantities/units/mass.pyi new file mode 100644 index 00000000..6917562d --- /dev/null +++ b/quantities/units/mass.pyi @@ -0,0 +1,52 @@ +from ..unitquantity import UnitQuantity, UnitMass + +kg: UnitMass +kilogram: UnitMass +g: UnitMass +gram: UnitMass +mg: UnitMass +milligram: UnitMass +oz: UnitMass +ounce: UnitMass +avoirdupois_ounce: UnitMass +lb: UnitMass +pound: UnitMass +avoirdupois_pound: UnitMass +st: UnitMass +stone: UnitMass +carat: UnitMass +gr: UnitMass +grain: UnitMass +long_hundredweight: UnitMass +short_hundredweight: UnitMass +t: UnitMass +metric_ton: UnitMass +tonne: UnitMass +dwt: UnitMass +pennyweight: UnitMass +slug: UnitMass +slugs: UnitMass +toz: UnitMass +troy_ounce: UnitMass +apounce: UnitMass +apothecary_ounce: UnitMass +troy_pound: UnitMass +appound: UnitMass +apothecary_pound: UnitMass +u: UnitMass +amu: UnitMass +atomic_mass_unit: UnitMass +dalton: UnitMass +Da: UnitMass +scruple: UnitMass +dr: UnitMass +dram: UnitMass +drachm: UnitMass +apdram: UnitMass +bag: UnitMass +ton: UnitMass +short_ton: UnitMass +long_ton: UnitMass +denier: UnitQuantity +tex: UnitQuantity +dtex: UnitQuantity diff --git a/quantities/units/power.py b/quantities/units/power.py index 06774eb5..4bf99ca6 100644 --- a/quantities/units/power.py +++ b/quantities/units/power.py @@ -1,6 +1,5 @@ """ """ -from __future__ import absolute_import from ..unitquantity import UnitQuantity from .energy import Btu, J diff --git a/quantities/units/power.pyi b/quantities/units/power.pyi new file mode 100644 index 00000000..b4d6b310 --- /dev/null +++ b/quantities/units/power.pyi @@ -0,0 +1,21 @@ +from ..unitquantity import UnitQuantity + +W: UnitQuantity +watt: UnitQuantity +volt_ampere: UnitQuantity +mW: UnitQuantity +milliwatt: UnitQuantity +kW: UnitQuantity +kilowatt: UnitQuantity +MW: UnitQuantity +megawatt: UnitQuantity +hp: UnitQuantity +horsepower: UnitQuantity +UK_horsepower: UnitQuantity +British_horsepower: UnitQuantity +boiler_horsepower: UnitQuantity +metric_horsepower: UnitQuantity +electric_horsepower: UnitQuantity +water_horsepower: UnitQuantity +refrigeration_ton: UnitQuantity +ton_of_refrigeration: UnitQuantity diff --git a/quantities/units/prefixes.py b/quantities/units/prefixes.py index 5b4a2803..fbef4307 100644 --- a/quantities/units/prefixes.py +++ b/quantities/units/prefixes.py @@ -1,4 +1,3 @@ - #SI prefixes yotta = 1e24 zetta = 1e21 diff --git a/quantities/units/prefixes.pyi b/quantities/units/prefixes.pyi new file mode 100644 index 00000000..e69de29b diff --git a/quantities/units/pressure.py b/quantities/units/pressure.py index bc343da0..12f5434b 100644 --- a/quantities/units/pressure.py +++ b/quantities/units/pressure.py @@ -1,6 +1,5 @@ """ """ -from __future__ import absolute_import from ..unitquantity import UnitQuantity from .acceleration import gravity @@ -24,6 +23,11 @@ symbol='Pa', aliases=['pascals'] ) +hPa = hectopascal = UnitQuantity( + 'hectopascal', + 100*Pa, + symbol='hPa', +) kPa = kilopascal = UnitQuantity( 'kilopascal', 1000*Pa, @@ -47,13 +51,19 @@ 100000*pascal, aliases=['bars'] ) +mbar = millibar = UnitQuantity( + 'millibar', + 0.001*bar, + symbol='mbar', + aliases=['millibars'] +) kbar = kilobar = UnitQuantity( 'kilobar', 1000*bar, symbol='kbar', aliases=['kilobars'] ) -Mbar = kilobar = UnitQuantity( +Mbar = megabar = UnitQuantity( 'megabar', 1000*kbar, symbol='Mbar', @@ -91,7 +101,7 @@ kip/inch**2, symbol='ksi' ) -barye = barie = barad = barad = barrie = baryd = UnitQuantity( +barye = barie = barad = barrie = baryd = UnitQuantity( 'barye', 0.1*N/m**2, symbol='Ba', diff --git a/quantities/units/pressure.pyi b/quantities/units/pressure.pyi new file mode 100644 index 00000000..67b5cd74 --- /dev/null +++ b/quantities/units/pressure.pyi @@ -0,0 +1,57 @@ +from ..unitquantity import UnitQuantity + +Hg: UnitQuantity +mercury: UnitQuantity +conventional_mercury: UnitQuantity +Pa: UnitQuantity +pascal: UnitQuantity +hPa: UnitQuantity +hectopascal: UnitQuantity +kPa: UnitQuantity +kilopascal: UnitQuantity +MPa: UnitQuantity +megapascal: UnitQuantity +GPa: UnitQuantity +gigapascal: UnitQuantity +bar: UnitQuantity +mbar: UnitQuantity +millibar: UnitQuantity +kbar: UnitQuantity +kilobar: UnitQuantity +Mbar: UnitQuantity +megabar: UnitQuantity +Gbar: UnitQuantity +gigabar: UnitQuantity +atm: UnitQuantity +atmosphere: UnitQuantity +standard_atmosphere: UnitQuantity +at: UnitQuantity +technical_atmosphere: UnitQuantity +torr: UnitQuantity +psi: UnitQuantity +pound_force_per_square_inch: UnitQuantity +ksi: UnitQuantity +kip_per_square_inch: UnitQuantity +barye: UnitQuantity +barie: UnitQuantity +barad: UnitQuantity +barrie: UnitQuantity +baryd: UnitQuantity +mmHg: UnitQuantity +mm_Hg: UnitQuantity +millimeter_Hg: UnitQuantity +millimeter_Hg_0C: UnitQuantity +cmHg: UnitQuantity +cm_Hg: UnitQuantity +centimeter_Hg: UnitQuantity +inHg: UnitQuantity +in_Hg: UnitQuantity +inch_Hg: UnitQuantity +inch_Hg_32F: UnitQuantity +inch_Hg_60F: UnitQuantity +inch_H2O_39F: UnitQuantity +inch_H2O_60F: UnitQuantity +footH2O: UnitQuantity +cmH2O: UnitQuantity +foot_H2O: UnitQuantity +ftH2O: UnitQuantity diff --git a/quantities/units/radiation.py b/quantities/units/radiation.py index 99771c1c..0830820e 100644 --- a/quantities/units/radiation.py +++ b/quantities/units/radiation.py @@ -1,6 +1,5 @@ """ """ -from __future__ import absolute_import from ..unitquantity import UnitQuantity from .time import s diff --git a/quantities/units/radiation.pyi b/quantities/units/radiation.pyi new file mode 100644 index 00000000..21b271fa --- /dev/null +++ b/quantities/units/radiation.pyi @@ -0,0 +1,16 @@ +from ..unitquantity import UnitQuantity + +Bq: UnitQuantity +becquerel: UnitQuantity +Ci: UnitQuantity +curie: UnitQuantity +rd: UnitQuantity +rutherford: UnitQuantity +Gy: UnitQuantity +gray: UnitQuantity +Sv: UnitQuantity +sievert: UnitQuantity +rem: UnitQuantity +rads: UnitQuantity +R: UnitQuantity +roentgen: UnitQuantity diff --git a/quantities/units/substance.py b/quantities/units/substance.py index cc6206e4..2754aa30 100644 --- a/quantities/units/substance.py +++ b/quantities/units/substance.py @@ -1,6 +1,5 @@ """ """ -from __future__ import absolute_import from ..unitquantity import UnitSubstance @@ -8,3 +7,14 @@ 'mole', symbol='mol' ) +mmol = UnitSubstance( + 'millimole', + mol/1000, + symbol='mmol' +) +umol = UnitSubstance( + 'micromole', + mmol/1000, + symbol='umol', + u_symbol='µmol' +) diff --git a/quantities/units/substance.pyi b/quantities/units/substance.pyi new file mode 100644 index 00000000..5c06c12e --- /dev/null +++ b/quantities/units/substance.pyi @@ -0,0 +1,6 @@ +from ..unitquantity import UnitSubstance + +mol: UnitSubstance +mole: UnitSubstance +mmol: UnitSubstance +umol: UnitSubstance diff --git a/quantities/units/temperature.py b/quantities/units/temperature.py index 7dd38289..db7c70e3 100644 --- a/quantities/units/temperature.py +++ b/quantities/units/temperature.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ..unitquantity import UnitTemperature @@ -11,6 +9,35 @@ symbol='K', aliases=['degK', 'kelvin'] ) +for prefix, symbolprefix, magnitude in ( + ('yotta', 'Y', 1e24), + ('zetta', 'Z', 1e21), + ('exa', 'E', 1e18), + ('peta', 'P', 1e15), + ('tera', 'T', 1e12), + ('giga', 'G', 1e9), + ('mega', 'M', 1e6), + ('kilo', 'k', 1e3), + ('hecto', 'h', 1e2), + ('deka', 'da', 1e1), + ('deci', 'd', 1e-1), + ('centi', 'c', 1e-2), + ('milli', 'm', 1e-3), + ('micro', 'u', 1e-6), + ('nano', 'n', 1e-9), + ('pico', 'p', 1e-12), + ('femto', 'f', 1e-15), + ('atto', 'a', 1e-18), + ('zepto', 'z', 1e-21), + ('yocto', 'y', 1e-24), +): + symbol = symbolprefix +'K' + globals()[symbol] = UnitTemperature( + prefix + 'kelvin', + K*magnitude, + symbol=symbol + ) + degR = rankine = Rankine = UnitTemperature( 'Rankine', K/1.8, diff --git a/quantities/units/temperature.pyi b/quantities/units/temperature.pyi new file mode 100644 index 00000000..2a794149 --- /dev/null +++ b/quantities/units/temperature.pyi @@ -0,0 +1,15 @@ +from ..unitquantity import UnitTemperature + +K: UnitTemperature +degK: UnitTemperature +kelvin: UnitTemperature +Kelvin: UnitTemperature +degR: UnitTemperature +rankine: UnitTemperature +Rankine: UnitTemperature +degC: UnitTemperature +celsius: UnitTemperature +Celsius: UnitTemperature +degF: UnitTemperature +fahrenheit: UnitTemperature +Fahrenheit: UnitTemperature diff --git a/quantities/units/time.py b/quantities/units/time.py index 1295615e..9fa3505d 100644 --- a/quantities/units/time.py +++ b/quantities/units/time.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """ """ -from __future__ import absolute_import from ..unitquantity import UnitQuantity, UnitTime @@ -11,6 +9,18 @@ symbol='s', aliases=['sec', 'seconds'] ) +ks = kilosecond = UnitTime( + 'kilosecond', + s*1000, + 'ks', + aliases=['kiloseconds'] +) +Ms = megasecond = UnitTime( + 'megasecond', + ks*1000, + 'Ms', + aliases=['megaseconds'] +) ms = millisecond = UnitTime( 'millisecond', s/1000, @@ -54,7 +64,7 @@ 60*s, symbol='min', aliases=['minutes'] -) +) # min is function in python h = hr = hour = UnitTime( 'hour', 60*min, diff --git a/quantities/units/time.pyi b/quantities/units/time.pyi new file mode 100644 index 00000000..d581a7a7 --- /dev/null +++ b/quantities/units/time.pyi @@ -0,0 +1,52 @@ +from ..unitquantity import UnitQuantity, UnitTime + +s: UnitTime +sec: UnitTime +second: UnitTime +ks: UnitTime +kilosecond: UnitTime +Ms: UnitTime +megasecond: UnitTime +ms: UnitTime +millisecond: UnitTime +us: UnitTime +microsecond: UnitTime +ns: UnitTime +nanosecond: UnitTime +ps: UnitTime +picosecond: UnitTime +fs: UnitTime +femtosecond: UnitTime +attosecond: UnitTime +min: UnitTime +minute: UnitTime +h: UnitTime +hr: UnitTime +hour: UnitTime +d: UnitTime +day: UnitTime +week: UnitTime +fortnight: UnitTime +yr: UnitTime +year: UnitTime +tropical_year: UnitTime +a: UnitTime +month: UnitTime +shake: UnitTime +sidereal_day: UnitTime +sidereal_hour: UnitTime +sidereal_minute: UnitTime +sidereal_second: UnitTime +sidereal_year: UnitTime +sidereal_month: UnitTime +tropical_month: UnitTime +synodic_month: UnitTime +lunar_month: UnitTime +common_year: UnitTime +leap_year: UnitTime +Julian_year: UnitTime +Gregorian_year: UnitTime +millenium: UnitTime +eon: UnitTime +work_year: UnitQuantity +work_month: UnitQuantity diff --git a/quantities/units/velocity.py b/quantities/units/velocity.py index cf0d529c..e23cb88e 100644 --- a/quantities/units/velocity.py +++ b/quantities/units/velocity.py @@ -1,6 +1,5 @@ """ """ -from __future__ import absolute_import from ..unitquantity import UnitQuantity from .length import m, nmi diff --git a/quantities/units/velocity.pyi b/quantities/units/velocity.pyi new file mode 100644 index 00000000..3d65bcc0 --- /dev/null +++ b/quantities/units/velocity.pyi @@ -0,0 +1,8 @@ +from ..unitquantity import UnitQuantity + +c: UnitQuantity +speed_of_light: UnitQuantity +kt: UnitQuantity +knot: UnitQuantity +knot_international: UnitQuantity +international_knot: UnitQuantity diff --git a/quantities/units/viscosity.py b/quantities/units/viscosity.py index b23195cd..4b7c1283 100644 --- a/quantities/units/viscosity.py +++ b/quantities/units/viscosity.py @@ -1,6 +1,5 @@ """ """ -from __future__ import absolute_import from ..unitquantity import UnitQuantity from .time import s diff --git a/quantities/units/viscosity.pyi b/quantities/units/viscosity.pyi new file mode 100644 index 00000000..9290be44 --- /dev/null +++ b/quantities/units/viscosity.pyi @@ -0,0 +1,9 @@ +from ..unitquantity import UnitQuantity + +P: UnitQuantity +poise: UnitQuantity +cP: UnitQuantity +centipoise: UnitQuantity +St: UnitQuantity +stokes: UnitQuantity +rhe: UnitQuantity diff --git a/quantities/units/volume.py b/quantities/units/volume.py index 7737bb64..a8ee7415 100644 --- a/quantities/units/volume.py +++ b/quantities/units/volume.py @@ -1,6 +1,5 @@ """ """ -from __future__ import absolute_import from ..unitquantity import UnitQuantity from .length import cm, m, foot, inch @@ -12,13 +11,31 @@ symbol='L', aliases=['l', 'liters', 'litre', 'litres'] ) -mL = milliliter = UnitQuantity( +mL = milliliter = millilitre = UnitQuantity( 'milliliter', liter/1000, symbol='mL', - aliases=['milliliters'] -) -cc = cubic_centimeter = milliliter = UnitQuantity( + aliases=['ml', 'milliliters', 'millilitre', 'millilitres'] +) +kL = kiloliter = kilolitre = UnitQuantity( + 'kiloliter', + liter*1000, + symbol='kL', + aliases=['kl', 'kiloliters', 'kilolitre', 'kilolitres'] +) +ML = megaliter = megalitre = UnitQuantity( + 'megaliter', + kiloliter*1000, + symbol='ML', + aliases=['Ml', 'megaliters', 'megalitre', 'megalitres'] +) +GL = gigaliter = gigalitre = UnitQuantity( + 'gigaliter', + megaliter*1000, + symbol='GL', + aliases=['Gl', 'gigaliters', 'gigalitre', 'gigalitres'] +) +cc = cubic_centimeter = UnitQuantity( 'cubic_centimeter', cm**3, symbol='cc', @@ -186,4 +203,4 @@ barrel/4 ) -del UnitQuantity, cm, m +del UnitQuantity, cm, m, foot, inch, acre diff --git a/quantities/units/volume.pyi b/quantities/units/volume.pyi new file mode 100644 index 00000000..8e1fc058 --- /dev/null +++ b/quantities/units/volume.pyi @@ -0,0 +1,77 @@ +from ..unitquantity import UnitQuantity + +l: UnitQuantity +L: UnitQuantity +liter: UnitQuantity +litre: UnitQuantity +mL: UnitQuantity +milliliter: UnitQuantity +millilitre: UnitQuantity +kL: UnitQuantity +kiloliter: UnitQuantity +kilolitre: UnitQuantity +ML: UnitQuantity +megaliter: UnitQuantity +megalitre: UnitQuantity +GL: UnitQuantity +gigaliter: UnitQuantity +gigalitre: UnitQuantity +cc: UnitQuantity +cubic_centimeter: UnitQuantity +stere: UnitQuantity +gross_register_ton: UnitQuantity +register_ton: UnitQuantity +acre_foot: UnitQuantity +board_foot: UnitQuantity +bu: UnitQuantity +bushel: UnitQuantity +US_bushel: UnitQuantity +US_dry_gallon: UnitQuantity +gallon: UnitQuantity +liquid_gallon: UnitQuantity +US_liquid_gallon: UnitQuantity +dry_quart: UnitQuantity +US_dry_quart: UnitQuantity +dry_pint: UnitQuantity +US_dry_pint: UnitQuantity +quart: UnitQuantity +liquid_quart: UnitQuantity +US_liquid_quart: UnitQuantity +pt: UnitQuantity +pint: UnitQuantity +liquid_pint: UnitQuantity +US_liquid_pint: UnitQuantity +cup: UnitQuantity +US_liquid_cup: UnitQuantity +gill: UnitQuantity +US_liquid_gill: UnitQuantity +floz: UnitQuantity +fluid_ounce: UnitQuantity +US_fluid_ounce: UnitQuantity +US_liquid_ounce: UnitQuantity +Imperial_bushel: UnitQuantity +UK_liquid_gallon: UnitQuantity +Canadian_liquid_gallon: UnitQuantity +UK_liquid_quart: UnitQuantity +UK_liquid_pint: UnitQuantity +UK_liquid_cup: UnitQuantity +UK_liquid_gill: UnitQuantity +UK_fluid_ounce: UnitQuantity +UK_liquid_ounce: UnitQuantity +bbl: UnitQuantity +barrel: UnitQuantity +tbsp: UnitQuantity +Tbsp: UnitQuantity +Tblsp: UnitQuantity +tblsp: UnitQuantity +tbs: UnitQuantity +Tbl: UnitQuantity +tablespoon: UnitQuantity +tsp: UnitQuantity +teaspoon: UnitQuantity +pk: UnitQuantity +peck: UnitQuantity +fldr: UnitQuantity +fluid_dram: UnitQuantity +fluidram: UnitQuantity +firkin: UnitQuantity diff --git a/quantities/version.py b/quantities/version.py deleted file mode 100644 index d6c6ab53..00000000 --- a/quantities/version.py +++ /dev/null @@ -1,2 +0,0 @@ - -__version__ = '0.10.1' diff --git a/setup.py b/setup.py index 03d81e82..1d139deb 100755 --- a/setup.py +++ b/setup.py @@ -1,16 +1,13 @@ -from distutils.cmd import Command -from distutils.core import setup -from distutils.command.sdist import sdist as _sdist -from distutils.command.build import build as _build -import os +from setuptools import Command, setup +from setuptools.command.build_py import build_py as _build +from setuptools.command.sdist import sdist as _sdist +from datetime import datetime class data(Command): - description = "Convert the NIST databas of constants" - + description = "Convert the NIST database of constants" user_options = [] - boolean_options = [] def initialize_options(self): @@ -26,7 +23,8 @@ def run(self): with open('quantities/constants/_codata.py', 'w') as f: f.write('# THIS FILE IS AUTOMATICALLY GENERATED\n') - f.write('# ANY CHANGES MADE HERE WILL BE LOST\n\n') + f.write('# ANY CHANGES MADE HERE WILL BE LOST\n') + f.write(f'# LAST GENERATED: {datetime.now()}\n\n') f.write('physical_constants = {}\n\n') for line in data: name = line[:55].rstrip().replace('mag.','magnetic') @@ -53,88 +51,4 @@ def run(self): _build.run(self) -class test(Command): - - """Run the test suite.""" - - description = "Run the test suite" - - user_options = [('verbosity=', 'V', 'set test report verbosity')] - - def initialize_options(self): - self.verbosity = 0 - - def finalize_options(self): - try: - self.verbosity = int(self.verbosity) - except ValueError: - raise ValueError('verbosity must be an integer.') - - def run(self): - import sys - if sys.version.startswith('2.6') or sys.version.startswith('3.1'): - import unittest2 as unittest - else: - import unittest - suite = unittest.TestLoader().discover('.') - unittest.TextTestRunner(verbosity=self.verbosity+1).run(suite) - - -packages = [] -for dirpath, dirnames, filenames in os.walk('quantities'): - if '__init__.py' in filenames: - packages.append('.'.join(dirpath.split(os.sep))) - else: - del(dirnames[:]) - -with open('quantities/version.py') as f: - for line in f: - if line.startswith('__version__'): - exec(line) - -setup( - author = 'Darren Dale', - author_email = 'dsdale24@gmail.com', -# classifiers = """Development Status :: 4 - Beta -# Environment :: Console -# Intended Audience :: Developers -# Intended Audience :: Education -# Intended Audience :: End Users/Desktop -# Intended Audience :: Science/Research -# License :: OSI Approved :: BSD License -# Operating System :: OS Independent -# Programming Language :: Python -# Topic :: Education -# Topic :: Scientific/Engineering -# """, - cmdclass = { - 'build': build, - 'data': data, - 'sdist': sdist, - 'test': test, - }, - description = "Support for physical quantities with units, based on numpy", - download_url = "http://pypi.python.org/pypi/quantities", - keywords = ['quantities', 'units', 'physical', 'constants'], - license = 'BSD', - long_description = """Quantities is designed to handle arithmetic and - conversions of physical quantities, which have a magnitude, dimensionality - specified by various units, and possibly an uncertainty. See the tutorial_ - for examples. Quantities builds on the popular numpy library and is - designed to work with numpy ufuncs, many of which are already - supported. Quantities is actively developed, and while the current features - and API are stable, test coverage is incomplete so the package is not - suggested for mission-critical applications. - - .. _tutorial: http://packages.python.org/quantities/user/tutorial.html - """, - name = 'quantities', - packages = packages, - platforms = 'Any', - requires = [ - 'python (>=2.6.0)', - 'numpy (>=1.4.0)', - ], - url = 'http://packages.python.org/quantities', - version = __version__, -) +setup(cmdclass={"build_py": build, "sdist": sdist, "data": data})