diff --git a/README.md b/README.md index 99ec1f6..dcaff7b 100644 --- a/README.md +++ b/README.md @@ -139,8 +139,6 @@ slow to refresh - they are not necessarily the instrument's fault. 1. Make sure you have [Python][] installed. - On Windows, you can get it from the Microsoft Store, or just run `winget install python`. - - Avoid Python 3.12 (and later) as it [causes issues with - `si-prefix`][si-prefix-issue11] (a dependency of videojitter). 2. Make sure you have [FFmpeg][] installed. - On Windows, you can install it by running `winget install ffmpeg`. - You don't need FFmpeg if you don't need to generate a test video, e.g. you diff --git a/pyproject.toml b/pyproject.toml index b869481..10dc27a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,10 +51,11 @@ videojitter-generate-video = "videojitter.generate_video:main" strip-extras = true [tool.black] +force-exclude = "src/si_prefix/" preview = true # For long string line breaks [tool.pylint.main] -ignore-paths = ["src/videojitter/_version_generated.py"] +ignore-paths = ["src/si_prefix", "src/videojitter/_version_generated.py"] disable = [ "duplicate-code", "missing-module-docstring", diff --git a/requirements.in b/requirements.in index 81e6747..b12a0cf 100644 --- a/requirements.in +++ b/requirements.in @@ -7,5 +7,4 @@ ffmpeg-python numpy pandas scipy -si-prefix soundfile diff --git a/requirements.txt b/requirements.txt index 63dea4a..e83654c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -52,8 +52,6 @@ rpds-py==0.12.0 # referencing scipy==1.11.3 # via -r requirements.in -si-prefix==1.2.2 - # via -r requirements.in six==1.16.0 # via python-dateutil soundfile==0.12.1 diff --git a/src/si_prefix/LICENSE.md b/src/si_prefix/LICENSE.md new file mode 100644 index 0000000..2990e46 --- /dev/null +++ b/src/si_prefix/LICENSE.md @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2017, Christian Fobel +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/si_prefix/README.md b/src/si_prefix/README.md new file mode 100644 index 0000000..4c45930 --- /dev/null +++ b/src/si_prefix/README.md @@ -0,0 +1,10 @@ +# si-prefix + +This directory contains a copy of the main code of [si-prefix v1.2.2][] by +Christian Fobel (with a couple of minor compatibility changes). + +The reason why videojitter does not simply pull si-prefix as a pip dependency is +because the si-prefix build [fails on Python 3.12][]. + +[si-prefix v1.2.2]: https://github.com/cfobel/si-prefix/tree/v1.2.2 +[fails on Python 3.12]: https://github.com/cfobel/si-prefix/issues/11 diff --git a/src/si_prefix/__init__.py b/src/si_prefix/__init__.py new file mode 100644 index 0000000..daf3a14 --- /dev/null +++ b/src/si_prefix/__init__.py @@ -0,0 +1,291 @@ +# coding: utf-8 +from __future__ import division +import math +import re + +# Print a floating-point number in engineering notation. +# Ported from [C version][1] written by +# Jukka “Yucca” Korpela . +# +# [1]: http://www.cs.tut.fi/~jkorpela/c/eng.html + +#: .. versionchanged:: 1.0 +#: Define as unicode string and use µ (i.e., ``\N{MICRO SIGN}``, ``\x0b5``) +#: to denote micro (not u). +#: +#: .. seealso:: +#: +#: `Issue #4`_. +#: +#: `Forum post`_ discussing unicode using µ as an example. +#: +#: `The International System of Units (SI) report`_ from the Bureau +#: International des Poids et Mesures +#: +#: .. _`Issue #4`: https://github.com/cfobel/si-prefix/issues/4 +#: .. _`Forum post`: https://mail.python.org/pipermail/python-list/2009-February/525913.html +#: .. _`The International System of Units (SI) report`: https://www.bipm.org/utils/common/pdf/si_brochure_8_en.pdf +SI_PREFIX_UNITS = u"yzafpnµm kMGTPEZY" +#: .. versionchanged:: 1.0 +#: Use unicode string for SI unit to support micro (i.e., µ) character. +#: +#: .. seealso:: +#: +#: `Issue #4`_. +#: +#: .. _`Issue #4`: https://github.com/cfobel/si-prefix/issues/4 +CRE_SI_NUMBER = re.compile(r'\s*(?P[\+\-])?' + r'(?P\d+)' + r'(?P.\d+)?\s*' + r'(?P[%s])?\s*' % SI_PREFIX_UNITS) + + +def split(value, precision=1): + ''' + Split `value` into value and "exponent-of-10", where "exponent-of-10" is a + multiple of 3. This corresponds to SI prefixes. + + Returns tuple, where the second value is the "exponent-of-10" and the first + value is `value` divided by the "exponent-of-10". + + Args + ---- + value : int, float + Input value. + precision : int + Number of digits after decimal place to include. + + Returns + ------- + tuple + The second value is the "exponent-of-10" and the first value is `value` + divided by the "exponent-of-10". + + Examples + -------- + + .. code-block:: python + + si_prefix.split(0.04781) -> (47.8, -3) + si_prefix.split(4781.123) -> (4.8, 3) + + See :func:`si_format` for more examples. + ''' + negative = False + digits = precision + 1 + + if value < 0.: + value = -value + negative = True + elif value == 0.: + return 0., 0 + + expof10 = int(math.log10(value)) + if expof10 > 0: + expof10 = (expof10 // 3) * 3 + else: + expof10 = (-expof10 + 3) // 3 * (-3) + + value *= 10 ** (-expof10) + + if value >= 1000.: + value /= 1000.0 + expof10 += 3 + elif value >= 100.0: + digits -= 2 + elif value >= 10.0: + digits -= 1 + + if negative: + value *= -1 + + return value, int(expof10) + + +def prefix(expof10): + ''' + Args: + + expof10 : Exponent of a power of 10 associated with a SI unit + character. + + Returns: + + str : One of the characters in "yzafpnum kMGTPEZY". + ''' + prefix_levels = (len(SI_PREFIX_UNITS) - 1) // 2 + si_level = expof10 // 3 + + if abs(si_level) > prefix_levels: + raise ValueError("Exponent out range of available prefixes.") + return SI_PREFIX_UNITS[si_level + prefix_levels] + + +def si_format(value, precision=1, format_str=u'{value} {prefix}', + exp_format_str=u'{value}e{expof10}'): + ''' + Format value to string with SI prefix, using the specified precision. + + Parameters + ---------- + value : int, float + Input value. + precision : int + Number of digits after decimal place to include. + format_str : str or unicode + Format string where ``{prefix}`` and ``{value}`` represent the SI + prefix and the value (scaled according to the prefix), respectively. + The default format matches the `SI prefix style`_ format. + exp_str : str or unicode + Format string where ``{expof10}`` and ``{value}`` represent the + exponent of 10 and the value (scaled according to the exponent of 10), + respectively. This format is used if the absolute exponent of 10 value + is greater than 24. + + Returns + ------- + unicode + :data:`value` formatted according to the `SI prefix style`_. + + Examples + -------- + + For example, with `precision=2`: + + .. code-block:: python + + 1e-27 --> 1.00e-27 + 1.764e-24 --> 1.76 y + 7.4088e-23 --> 74.09 y + 3.1117e-21 --> 3.11 z + 1.30691e-19 --> 130.69 z + 5.48903e-18 --> 5.49 a + 2.30539e-16 --> 230.54 a + 9.68265e-15 --> 9.68 f + 4.06671e-13 --> 406.67 f + 1.70802e-11 --> 17.08 p + 7.17368e-10 --> 717.37 p + 3.01295e-08 --> 30.13 n + 1.26544e-06 --> 1.27 u + 5.31484e-05 --> 53.15 u + 0.00223223 --> 2.23 m + 0.0937537 --> 93.75 m + 3.93766 --> 3.94 + 165.382 --> 165.38 + 6946.03 --> 6.95 k + 291733 --> 291.73 k + 1.22528e+07 --> 12.25 M + 5.14617e+08 --> 514.62 M + 2.16139e+10 --> 21.61 G + 9.07785e+11 --> 907.78 G + 3.8127e+13 --> 38.13 T + 1.60133e+15 --> 1.60 P + 6.7256e+16 --> 67.26 P + 2.82475e+18 --> 2.82 E + 1.1864e+20 --> 118.64 E + 4.98286e+21 --> 4.98 Z + 2.0928e+23 --> 209.28 Z + 8.78977e+24 --> 8.79 Y + 3.6917e+26 --> 369.17 Y + 1.55051e+28 --> 15.51e+27 + 6.51216e+29 --> 651.22e+27 + + .. versionchanged:: 1.0 + Use unicode string for :data:`format_str` and SI value format string to + support micro (i.e., µ) characte, and change return type to unicode + string. + + .. seealso:: + + `Issue #4`_. + + .. _`Issue #4`: https://github.com/cfobel/si-prefix/issues/4 + .. _SI prefix style: + http://physics.nist.gov/cuu/Units/checklist.html + ''' + svalue, expof10 = split(value, precision) + value_format = u'%%.%df' % precision + value_str = value_format % svalue + try: + return format_str.format(value=value_str, + prefix=prefix(expof10).strip()) + except ValueError: + sign = '' + if expof10 > 0: + sign = "+" + return exp_format_str.format(value=value_str, + expof10=''.join([sign, str(expof10)])) + + +def si_parse(value): + ''' + Parse a value expressed using SI prefix units to a floating point number. + + Parameters + ---------- + value : str or unicode + Value expressed using SI prefix units (as returned by :func:`si_format` + function). + + + .. versionchanged:: 1.0 + Use unicode string for SI unit to support micro (i.e., µ) character. + + .. seealso:: + + `Issue #4`_. + + .. _`Issue #4`: https://github.com/cfobel/si-prefix/issues/4 + ''' + CRE_10E_NUMBER = re.compile(r'^\s*(?P[\+\-]?\d+)?' + r'(?P.\d+)?\s*([eE]\s*' + r'(?P[\+\-]?\d+))?$') + CRE_SI_NUMBER = re.compile(r'^\s*(?P(?P[\+\-]?\d+)?' + r'(?P.\d+)?)\s*' + r'(?P[%s])?\s*$' % SI_PREFIX_UNITS) + match = CRE_10E_NUMBER.match(value) + if match: + # Can be parse using `float`. + assert(match.group('integer') is not None or + match.group('fraction') is not None) + return float(value) + match = CRE_SI_NUMBER.match(value) + assert(match.group('integer') is not None or + match.group('fraction') is not None) + d = match.groupdict() + si_unit = d['si_unit'] if d['si_unit'] else ' ' + prefix_levels = (len(SI_PREFIX_UNITS) - 1) // 2 + scale = 10 ** (3 * (SI_PREFIX_UNITS.index(si_unit) - prefix_levels)) + return float(d['number']) * scale + + +def si_prefix_scale(si_unit): + ''' + Parameters + ---------- + si_unit : str + SI unit character, i.e., one of "yzafpnµm kMGTPEZY". + + Returns + ------- + int + Multiple associated with `si_unit`, e.g., 1000 for `si_unit=k`. + ''' + return 10 ** si_prefix_expof10(si_unit) + + +def si_prefix_expof10(si_unit): + ''' + Parameters + ---------- + si_unit : str + SI unit character, i.e., one of "yzafpnµm kMGTPEZY". + + Returns + ------- + int + Exponent of the power of ten associated with `si_unit`, e.g., 3 for + `si_unit=k` and -6 for `si_unit=µ`. + ''' + prefix_levels = (len(SI_PREFIX_UNITS) - 1) // 2 + return (3 * (SI_PREFIX_UNITS.index(si_unit) - prefix_levels)) diff --git a/src/videojitter/generate_report.py b/src/videojitter/generate_report.py index aaf6441..62571d1 100644 --- a/src/videojitter/generate_report.py +++ b/src/videojitter/generate_report.py @@ -4,8 +4,8 @@ import altair as alt import numpy as np import pandas as pd -from si_prefix import si_format from scipy import stats +from si_prefix import si_format from videojitter import _util, _version