Passed
Push — main ( 6e4731...702ebc )
by Douglas
02:44
created

pocketutils.tools.unit_tools   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 319
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 166
dl 0
loc 319
rs 8.96
c 0
b 0
f 0
wmc 43

10 Methods

Rating   Name   Duplication   Size   Complexity  
C UnitTools.approx_time_wrt() 0 38 9
A UnitTools.delta_time_to_str() 0 24 4
A UnitTools.extract_micromolar() 0 24 3
A UnitTools.round_to_sigfigs() 0 22 4
A UnitTools.split_species_micromolar() 0 28 3
A UnitTools.friendly_size() 0 7 1
A UnitTools.concentration_to_micromolar() 0 28 1
C UnitTools.format_micromolar() 0 57 11
A UnitTools.canonicalize_quantity() 0 21 2
A UnitTools.ms_to_minsec() 0 37 5

How to fix   Complexity   

Complexity

Complex classes like pocketutils.tools.unit_tools often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import logging
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
import math
3
from datetime import date, datetime, timedelta
4
from typing import Optional, SupportsFloat, Tuple, Union
5
6
import regex
0 ignored issues
show
introduced by
Unable to import 'regex'
Loading history...
7
from pint import Quantity, UnitRegistry
0 ignored issues
show
introduced by
Unable to import 'pint'
Loading history...
8
from pint.errors import PintTypeError
0 ignored issues
show
introduced by
Unable to import 'pint.errors'
Loading history...
9
10
from pocketutils.core._internal import nicesize
0 ignored issues
show
Bug introduced by
The name core does not seem to exist in module pocketutils.
Loading history...
11
from pocketutils.core.exceptions import OutOfRangeError, StringPatternError
0 ignored issues
show
Bug introduced by
The name core does not seem to exist in module pocketutils.
Loading history...
12
from pocketutils.tools.base_tools import BaseTools
13
from pocketutils.tools.string_tools import StringTools
14
15
logger = logging.getLogger("pocketutils")
16
_UNIT_REG = UnitRegistry()
17
18
19
class UnitTools(BaseTools):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
20
    @classmethod
21
    def approx_time_wrt(
0 ignored issues
show
best-practice introduced by
Too many return statements (7/6)
Loading history...
22
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
23
        now: Union[date, datetime],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
24
        then: Union[date, datetime],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
25
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
26
        skip_today: bool = False,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
27
        sig: int = 3,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
28
    ) -> str:
29
        """
30
        Describes ``then`` with higher resolution for smaller differences to ``now``.
31
32
        Examples:
33
            - ``approx_time_wrt(date(2021, 1, 12), date(1996, 10, 1))  # "1996"``
34
            - ``approx_time_wrt(date(2021, 1, 12), date(2021, 10, 1))  # "2021-01-12"``
35
            - ``approx_time_wrt(date(2021, 10, 1), datetime(2021, 10, 1, 11, 55))  # "2021-01-12 11:55"``
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (105/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
36
            - ``approx_time_wrt(date(2021, 10, 1), datetime(2021, 10, 1, 11, 0, 0, 30, 222222))  # "2021-01-12 00:00:30"``
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (122/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
37
            - ``approx_time_wrt(date(2021, 10, 1), datetime(2021, 10, 1, 11, 0, 0, 2, 222222))  # "2021-01-12 00:00:02.222"``
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (125/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
38
            - ``approx_time_wrt(date(2021, 10, 1), datetime(2021, 10, 1, 11, 0, 0, 2, 22))  # "2021-01-12 00:00:02.000022"``
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (124/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
39
        """
40
        delta = now - then if now > then else then - now
41
        tot_days = (delta.days) + (delta.seconds / 86400) + (delta.microseconds / 86400 / 10**6)
42
        tot_secs = tot_days * 86400
43
        _today = "" if skip_today and then.date() == now.date() else "%Y-%m-%d "
44
        if tot_days > sig * 365.24219:
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
45
            return str(then.year)
46
        elif tot_days > sig * 30.437:
47
            return then.strftime("%Y-%m")
48
        elif tot_days > sig:
49
            return then.strftime("%Y-%m-%d")
50
        elif tot_secs > sig * 60:
51
            return then.strftime(_today + "%H:%M")
52
        elif tot_secs > sig:
53
            return then.strftime(_today + "%H:%M:%S")
54
        elif tot_secs > sig / 1000:
55
            return then.strftime(_today + "%H:%M:%S") + "." + str(round(then.microsecond / 1000))
56
        else:
57
            return then.strftime(_today + "%H:%M:%S.%f")
58
59
    @classmethod
60
    def delta_time_to_str(cls, delta_sec: Union[float, timedelta], *, space: str = "") -> str:
61
        """
62
        Returns a pretty string from a difference in time in seconds.
63
        Rounds hours and minutes to 2 decimal places, and seconds to 1.
64
        Ex: delta_time_to_str(313) == 5.22min
65
            delta_sec: The time in seconds
66
            space: Space char between digits and units;
67
                   good choices are empty, ASCII space, Chars.narrownbsp, Chars.thinspace,
68
                   and Chars.nbsp.
69
70
        Returns:
71
            A string with units 'hr', 'min', or 's'
72
        """
73
        if isinstance(delta_sec, timedelta):
74
            delta_sec = delta_sec.total_seconds()
75
        if abs(delta_sec) > 60 * 60 * 3:
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
76
            return (
77
                StringTools.strip_empty_decimal(str(round(delta_sec / 60 / 60, 2))) + space + "hr"
78
            )
79
        elif abs(delta_sec) > 180:
80
            return StringTools.strip_empty_decimal(str(round(delta_sec / 60, 2))) + space + "min"
81
        else:
82
            return StringTools.strip_empty_decimal(str(round(delta_sec, 1))) + space + "s"
83
84
    @classmethod
85
    def ms_to_minsec(cls, ms: int, space: str = "") -> str:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "ms" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
86
        """
87
        Converts a number of milliseconds to one of the following formats.
88
        Will be one of these:
89
            - 10ms         if < 1 sec
90
            - 10:15        if < 1 hour
91
            - 10:15:33     if < 1 day
92
            - 5d:10:15:33  if > 1 day
93
        Prepends a minus sign (−) if negative.
94
95
        Args:
96
            ms: The milliseconds
97
            space: Space char between digits and 'ms' (if used);
98
                   good choices are empty, ASCII space, Chars.narrownbsp,
99
                   Chars.thinspace, and Chars.nbsp.
100
101
        Returns:
102
            A string of one of the formats above
103
        """
104
        ms = abs(int(ms))
105
        seconds = int((ms / 1000) % 60)
106
        minutes = int((ms / (1000 * 60)) % 60)
107
        hours = int((ms / (1000 * 60 * 60)) % 24)
108
        days = int(ms / (1000 * 60 * 60 * 24))
109
        z_hr = str(hours).zfill(2)
110
        z_min = str(minutes).zfill(2)
111
        z_sec = str(seconds).zfill(2)
112
        sgn = "−" if ms < 0 else ""
113
        if ms < 1000:
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
114
            return f"{sgn}{ms}{space}ms"
115
        elif days > 1:
116
            return f"{days}d:{z_hr}:{z_min}:{z_sec}"
117
        elif hours > 1:
118
            return f"{sgn}{z_hr}:{z_min}:{z_sec}"
119
        else:
120
            return f"{sgn}{z_min}:{z_sec}"
121
122
    @classmethod
123
    def friendly_size(cls, n_bytes: int, *, space: str = " ") -> str:
124
        """
125
        Returns a text representation of a number of bytes.
126
        Uses base 2 with IEC 1998, rounded to 0 decimal places, and without a space.
127
        """
128
        return nicesize(n_bytes, space=space)
129
130
    @classmethod
131
    def round_to_sigfigs(cls, num: SupportsFloat, sig_figs: Optional[int]) -> float:
132
        """
133
        Round to specified number of sigfigs.
134
135
        Args:
136
            num: A Python or Numpy float or something that supports __float__
137
            sig_figs: The number of significant figures, non-negative
138
139
        Returns:
140
            A Python integer
141
        """
142
        if sig_figs is None:
143
            return float(num)
144
        if sig_figs < 0:
145
            raise OutOfRangeError(f"sig_figs {sig_figs} is negative", minimum=0)
146
        num = float(num)
147
        if num != 0:
0 ignored issues
show
unused-code introduced by
Unnecessary "else" after "return"
Loading history...
148
            digits = -int(math.floor(math.log10(abs(num))) - (sig_figs - 1))
149
            return round(num, digits)
150
        else:
151
            return 0  # can't take the log of 0
152
153
    @classmethod
154
    def format_micromolar(
155
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
156
        micromolar: float,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
157
        n_sigfigs: Optional[int] = 5,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
158
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
159
        adjust_units: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
160
        use_sigfigs: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
161
        space: str = "",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
162
    ) -> str:
163
        """
164
        Returns a concentration with units, with the units scaled as needed.
165
        Can handle millimolar, micromolar, nanomolar, and picomolar.
166
167
        Args:
168
            micromolar: Value
169
            n_sigfigs: For rounding; no rounding if None
170
            adjust_units: If False, will always use micromolar
171
            use_sigfigs: If True, rounds to a number of significant figures; otherwise round to decimal places
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (110/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
172
            space: Space char between digits and units;
173
                   good choices are empty, ASCII space,
174
                   :attr:`pocketutils.core.chars.Chars.narrownbsp`,
175
                   :attr:`pocketutils.core.chars.Chars.thinspace`,
176
                   and :attr:`pocketutils.core.chars.Chars.nbsp`.
177
178
        Returns:
179
            The concentration with a suffix of µM, mM, nM, or mM
180
        """
181
        d = micromolar
0 ignored issues
show
Coding Style Naming introduced by
Variable name "d" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
182
        m = abs(d)
0 ignored issues
show
Coding Style Naming introduced by
Variable name "m" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
183
        unit = "µM"
184
        if adjust_units:
185
            if m < 1e-6:
186
                d *= 1e9
0 ignored issues
show
Coding Style Naming introduced by
Variable name "d" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
187
                unit = "fM"
188
            elif m < 1e-3:
189
                d *= 1e6
0 ignored issues
show
Coding Style Naming introduced by
Variable name "d" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
190
                unit = "pM"
191
            elif m < 1:
192
                d *= 1e3
0 ignored issues
show
Coding Style Naming introduced by
Variable name "d" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
193
                unit = "nM"
194
            elif m >= 1e6:
195
                d /= 1e6
0 ignored issues
show
Coding Style Naming introduced by
Variable name "d" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
196
                unit = "M"
197
            elif m >= 1e3:
198
                d /= 1e3
0 ignored issues
show
Coding Style Naming introduced by
Variable name "d" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
199
                unit = "mM"
200
        if n_sigfigs is None:
201
            pass
202
        elif use_sigfigs:
203
            d = cls.round_to_sigfigs(d, n_sigfigs)
0 ignored issues
show
Coding Style Naming introduced by
Variable name "d" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
204
        else:
205
            d = round(d, n_sigfigs)
0 ignored issues
show
Coding Style Naming introduced by
Variable name "d" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
206
        if round(d) == d and str(d).endswith(".0"):
0 ignored issues
show
unused-code introduced by
Unnecessary "else" after "return"
Loading history...
207
            return str(d)[:-2] + space + unit
208
        else:
209
            return str(d) + space + unit
210
211
    @classmethod
212
    def split_species_micromolar(cls, text: str) -> Tuple[str, Optional[float]]:
213
        """
214
        Splits a name into a chemical/concentration pair, falling back with the full name.
215
        Ex: "abc 3.5uM" → (abc, 3.5)
216
        Ex: "abc 3.5 µM" → (abc, 3.5)
217
        Ex: "abc (3.5mM)" → (abc, 3500.0)
218
        Ex: "abc 3.5mM" → (abc, None)
219
        Ex: "3.5mM" → (3.5mM, None)  # an edge case: don't pass in only units
220
        Uses a moderately strict pattern for the drug and dose:
221
            - The dose must terminate the string, except for end parenthesis or whitespace.
222
            - The drug and dose must be separated by at least one non-alphanumeric, non-dot, non-hyphen character.
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (114/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
223
            - Units must follow the digits, separated by at most whitespace, and are case-sensitive.
224
        """
225
        # lazy ops in the first group and in the non-(alphanumeric/dot/dash) separator between the drug and dose
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (112/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
226
        pat = regex.compile(
227
            r"^\s*(.*?)(?:[^A-Za-z0-9.\-]+?[\s(\[{]*(\d+(?:.\d*)?)\s*([mµunpf]M)\s*[)\]}]*)?\s*$",
228
            flags=regex.V1,
229
        )
230
        match = pat.fullmatch(text)
231
        if match is None:
232
            raise StringPatternError(f"Text {text} couldn't be parsed", value=text, pattern=pat)
233
        if match.group(2) is None:
0 ignored issues
show
unused-code introduced by
Unnecessary "else" after "return"
Loading history...
234
            return text.strip(), None
235
        else:
236
            drug = match.group(1).strip("([{)]}")
237
            dose = UnitTools.concentration_to_micromolar(float(match.group(2)), match.group(3))
238
            return drug, dose
239
240
    @classmethod
241
    def extract_micromolar(cls, text: str) -> Optional[float]:
242
        """
243
        Returns what looks like a concentration with units. Accepts one of: mM, µM, uM, nM, pM.
244
        Searches pretty flexibly.
245
        If no matches are found, returns None.
246
        If multiple matches are found, warns and returns None.
247
        """
248
        # we need to make sure mM ex isn't part of a larger name
249
        pat1 = regex.compile(r"(\d+(?:.\d*)?)\s*([mµunpf]M)\s*[)\]}]*", flags=regex.V1)
250
251
        def find(pat):
252
            return {
253
                UnitTools.concentration_to_micromolar(float(match.group(1)), match.group(2))
254
                for match in pat.finditer(text)
255
                if match is not None
256
            }
257
258
        matches = find(pat1)
259
        if len(matches) == 1:
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
260
            return next(iter(matches))
261
        elif len(matches) > 1:
262
            logger.warning(f"Found {len(matches)} potential doses: {matches} . Returning None.")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
263
        return None
264
265
    @classmethod
266
    def concentration_to_micromolar(cls, digits: SupportsFloat, units: str) -> float:
267
        """
268
        Converts a concentration with units to micromolar.
269
270
        Args:
271
            digits: Float or float-compatible value
272
            units: Units that ``digits`` are in
273
274
        Example:
275
            .. code-block::
276
277
                concentration_to_micromolar(53, 'nM')  # returns 0.053
278
279
        See Also:
280
            :meth:`extract_micromolar`
281
        """
282
        return (
283
            float(digits)
284
            * {
285
                "M": 1e6,
286
                "mM": 1e3,
287
                "µM": 1,
288
                "uM": 1,
289
                "nM": 1e-3,
290
                "pM": 1e-6,
291
                "fM": 1e-9,
292
            }[units]
293
        )
294
295
    @classmethod
296
    def canonicalize_quantity(cls, s: str, dimensionality: str) -> Quantity:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "s" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
297
        """
298
        Returns a quantity in reduced units from a magnitude with units.
299
300
        Args:
301
            s: The string to parse; e.g. ``"1 m/s^2"``.
302
               Unit names and symbols permitted, and spaces may be omitted.
303
            dimensionality: The resulting Quantity is checked against this;
304
                            e.g. ``"[length]/[meter]^2"``
305
306
        Returns:
307
            a pint ``Quantity``
308
309
        Raise:
310
            PintTypeError: If the dimensionality is inconsistent
311
        """
312
        q = _UNIT_REG.Quantity(s).to_reduced_units()
0 ignored issues
show
Coding Style Naming introduced by
Variable name "q" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
313
        if not q.is_compatible_with(dimensionality):
314
            raise PintTypeError(f"{s} not of dimensionality {dimensionality}")
315
        return q
316
317
318
__all__ = ["UnitTools"]
319