Skip to content

Dynamic contexts, currency conversion, and failing to find the shortest path #2146

@deanmalmgren

Description

@deanmalmgren

First time poster, long-time fan of pint. Thank you.

I'm currently working on a project where I use pint extensively for all kinds of unit conversions. We're working on incorporating a feature to allow for multiple types of currencies and I nearly have a working implementation that plays nicely with pint, but I've run into a strange bug. For reference, the relevant tidbit of the issue is captured in this gist.

When I'm only switching between a few currencies, I notice that it is able to successfully do the calculations without too much trouble. You can see this by running poetry run python working.py, the code is reproduced here for convenience:

import itertools

import pint
import currency_converter

# create a currency converter instance to load all of the data
cc = currency_converter.CurrencyConverter()

# load custom units and instantiate Quantity base class that is used everywhere
ureg = pint.UnitRegistry()
for c in cc.currencies:
    ureg.define(f"{c} = [currency_{c}]")  # i.e. USD = [currency_USD]


# add programmatic context for currency conversion
currency_context = pint.Context("currency", defaults={"date": None})
currencies = list(cc.currencies)  # <------- THIS WORKS WHEN IT IS A SUBSET OF THESE CURRENCIES
for a, b in itertools.combinations(currencies, 2):
    def a2b(_ureg, x, date=None):
        return cc.convert(x.magnitude, a, b, date=date) * _ureg(b)

    def b2a(_ureg, x, date=None):
        return cc.convert(x.magnitude, b, a, date=date) * _ureg(a)

    currency_context.add_transformation(f"[currency_{a}]", f"[currency_{b}]", a2b)
    currency_context.add_transformation(f"[currency_{b}]", f"[currency_{a}]", b2a)
ureg.add_context(currency_context)

Quantity = ureg.Quantity

q = Quantity(1, currencies[0])
with ureg.context("currency"):
    for c in currencies:
        print(q)
        q = q.to(c)
    print(q)
    q = q.to(currencies[0])
    print(q)

But when using all 42 currencies that are supported by currencyconverter as above, I notice that the conversion does not happen. When I KeyboardInterrupt, it appears to be stuck in a pint internal function that is related to the shortest path. Here is the output after running poetry run python hanging.py and then issuing a KeyboardInterrupt after ~5s:

1 LTL
1 LTL
^CTraceback (most recent call last):
  File "/Users/dean/Downloads/pint+currency_converter/hanging.py", line 35, in <module>
    q = q.to(c)
        ^^^^^^^
  File "/Users/dean/Library/Caches/pypoetry/virtualenvs/pint+currency-converter-xbgMmBYE-py3.11/lib/python3.11/site-packages/pint/facets/plain/quantity.py", line 536, in to
    magnitude = self._convert_magnitude_not_inplace(other, *contexts, **ctx_kwargs)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dean/Library/Caches/pypoetry/virtualenvs/pint+currency-converter-xbgMmBYE-py3.11/lib/python3.11/site-packages/pint/facets/plain/quantity.py", line 480, in _convert_magnitude_not_inplace
    return self._REGISTRY.convert(self._magnitude, self._units, other)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dean/Library/Caches/pypoetry/virtualenvs/pint+currency-converter-xbgMmBYE-py3.11/lib/python3.11/site-packages/pint/facets/plain/registry.py", line 1050, in convert
    return self._convert(value, src, dst, inplace)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dean/Library/Caches/pypoetry/virtualenvs/pint+currency-converter-xbgMmBYE-py3.11/lib/python3.11/site-packages/pint/facets/context/registry.py", line 397, in _convert
    path = find_shortest_path(self._active_ctx.graph, src_dim, dst_dim)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dean/Library/Caches/pypoetry/virtualenvs/pint+currency-converter-xbgMmBYE-py3.11/lib/python3.11/site-packages/pint/util.py", line 378, in find_shortest_path
    newpath = find_shortest_path(graph, node, end, path)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dean/Library/Caches/pypoetry/virtualenvs/pint+currency-converter-xbgMmBYE-py3.11/lib/python3.11/site-packages/pint/util.py", line 378, in find_shortest_path
    newpath = find_shortest_path(graph, node, end, path)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dean/Library/Caches/pypoetry/virtualenvs/pint+currency-converter-xbgMmBYE-py3.11/lib/python3.11/site-packages/pint/util.py", line 378, in find_shortest_path
    newpath = find_shortest_path(graph, node, end, path)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  [Previous line repeated 35 more times]
  File "/Users/dean/Library/Caches/pypoetry/virtualenvs/pint+currency-converter-xbgMmBYE-py3.11/lib/python3.11/site-packages/pint/util.py", line 377, in find_shortest_path
    if node not in path:
       ^^^^^^^^^^^^^^^^
  File "/Users/dean/Library/Caches/pypoetry/virtualenvs/pint+currency-converter-xbgMmBYE-py3.11/lib/python3.11/site-packages/pint/util.py", line 822, in __eq__
    return self.scale == other.scale and super().__eq__(other)
                                         ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dean/Library/Caches/pypoetry/virtualenvs/pint+currency-converter-xbgMmBYE-py3.11/lib/python3.11/site-packages/pint/util.py", line 581, in __eq__
    if UnitsContainer.__hash__(self) != UnitsContainer.__hash__(other):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dean/Library/Caches/pypoetry/virtualenvs/pint+currency-converter-xbgMmBYE-py3.11/lib/python3.11/site-packages/pint/util.py", line 561, in __hash__
    def __hash__(self) -> int:

KeyboardInterrupt

My best guess is that there is a gap in the shortest path algorithm that is being used to infer the unit conversion. I am specifying all pairwise conversions so there really shouldn't be any need for anything terribly fancy to infer the conversion path. Looking at the implementation of util.find_shortest_path, it made me wonder if a breadth first search approach would be more efficient.

Thanks in advance for your help!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions