pint icon indicating copy to clipboard operation
pint copied to clipboard

Context for volt/gauss -> volt/tesla

Open climbit opened this issue 4 years ago • 3 comments

Hi everyone,

I am trying to write a transformation for V/G -> V/T and I don't get it done.

Just to double, check I tried the following code and it is running: pint_conversion1.py:

import pint

ureg = pint.UnitRegistry(system='SI')
ureg.load_definitions('pint_definition1.py')

ureg.enable_contexts('Gaussian')
ureg.enable_contexts('hall')

print(ureg.parse_expression('1nm').to('K', 'sp', 'boltzmann'))
print(ureg.parse_expression('1nm').to('Hz'))
print(ureg.parse_expression('VA').to('W'))
print(ureg.parse_expression('VA s').to('J'))
print(ureg.parse_expression('1J/m').to('VA s/m'))
print(ureg.parse_expression('1G').to('T'))

pint_definition1.py:

@context hall
    [length] <-> [frequency]: speed_of_light / value
@end

So, the context seems to work in general.

Afterwards I used the following piece of code to get the transformation for my actual problem: pint_conversion2.py:

import pint

ureg = pint.UnitRegistry(system='SI')
ureg.load_definitions('pint_definition2.py')

ureg.enable_contexts('Gaussian')
ureg.enable_contexts('hall')

print(ureg.parse_expression('1G').to('T'))
print(ureg.parse_expression('V/G').to('V/T'))

pint_definition2.py:

@context hall
    [electric_potential]/[gaussian_magnetic_flux] -> [electric_potential]/[magnetic_flux]: value * (4 * pi / vacuum_permeability) ** 0.5
    [electric_potential]/[magnetic_flux] -> [electric_potential]/[gaussian_magnetic_flux]: value / (4 * pi / vacuum_permeability) ** 0.5
@end

But with this code I am getting the following error:

pint.errors.DimensionalityError: Cannot convert from 'gaussian_hall_sensitivity_absolute' ([length] ** 2.5 * [mass] ** 0.5 / [current] / [time] ** 2) to 'hall_sensitivity_absolute' ([length] ** 2 / [time])

I also tried the following code snippets: pint_conversion3.py:

import pint

ureg = pint.UnitRegistry(system='SI')
ureg.load_definitions('pint_definition3.py')

ureg.enable_contexts('Gaussian')
ureg.enable_contexts('hall')

print(ureg.parse_expression('1G').to('T'))
print(ureg.parse_expression('V/G').to('V/T'))

pint_definition3.py:

@context hall
    [electric_potential]/[gaussian_magnetic_flux] -> [electric_potential]/[magnetic_flux]: value * (1 / 10**-7 / (V*s/A/m)) ** 0.5
    [electric_potential]/[magnetic_flux] -> [electric_potential]/[gaussian_magnetic_flux]: value / (1 / 10**-7 / (V*s/A/m)) ** 0.5
@end

and: pint_conversion4.py:

import pint

ureg = pint.UnitRegistry(system='SI')
ureg.load_definitions('pint_definition4.py')

ureg.enable_contexts('Gaussian')
ureg.enable_contexts('hall')

print(ureg.parse_expression('1G').to('T'))
print(ureg.parse_expression('V/G').to('V/T'))

pint_definition4.py:

[gaussian_hall_sensitivity_absolute] = [electric_potential] / [gaussian_magnetic_flux]
[hall_sensitivity_absolute] = [electric_potential] / [magnetic_flux]
hall_sensitivity_absolute = V/T
gaussian_hall_sensitivity_absolute = V/G
@context hall
    [gaussian_hall_sensitivity_absolute] -> [hall_sensitivity_absolute]: value * (4 * pi / vacuum_permeability) ** 0.5
    [hall_sensitivity_absolute] -> [gaussian_hall_sensitivity_absolute]: value / (4 * pi / vacuum_permeability) ** 0.5
@end

but the error remains.

Additionally I was triing to create the context programmaticaly:

pint_conversion5.py:

import pint

ureg = pint.UnitRegistry(system='SI')
ureg.enable_contexts('Gaussian')

hall_def = pint.Context('hall')
hall_def.add_transformation('[electric_potential]/[gaussian_magnetic_flux]', '[electric_potential]/[magnetic_flux]', lambda ureg, x: x * (4 * pi / vacuum_permeability) ** 0.5)
ureg.add_context(hall_def)
print(ureg.parse_expression('V/G').to('V/T','hall'))

Still the same error.

Further I tried to convert another (meaningless) unit which is SI only, which worked just fine: pint_conversion6.py:

import pint
ureg = pint.UnitRegistry(system='SI')
ureg.load_definitions('pint_definition6.py')
print(ureg.parse_expression('V/nm').to('V Hz', 'hall'))

pint_definition6.py:

@context hall
    [electric_potential]/[length] <-> [electric_potential]*[frequency]: value * speed_of_light
@end

V/G means Si and CGS systems are mixed. Might it be, that it is related to that? I think I have any error in reasoning and looked to long onto the same piece of code and therefore being blind for it. Any help is highly appreciated!

The example code is uploaded: code.zip

Thank you very much!

climbit avatar Apr 24 '21 19:04 climbit

There is somethign I do not get from your snippet. In the second example you say you are getting: "gaussian_hall_sensitivity_absolute" but such dimensionality is no defined. Is that possible that there is something missing.

but in any case, mixing SI and CGS-Gaussian units is not a good idea as they are incompatible.

PS.- Do not name your definitions files .py, as they are not really python files and might lead to problems in the future. Use .txt

hgrecco avatar Apr 24 '21 20:04 hgrecco

Thanks for your fast reply!

You are right, there was a copy paste error in the second example. I updated it. In the uploaded code.zip it should be right.

Ah, I wanted to mention that: I just use the py extension as I get some syntaxhighlighting, which is a little easier to read. I had txt before, this does not solve the problem.

For sure they are incompatible, therefore I would like to convert them. Is it possible to solve that in any way? I mean this is the same equation as in default_en.txt in context gaussian [gaussian_magnetic_flux] -> [magnetic_flux]: value / (4 * π / µ_0) ** 0.5, why does it not work in my examples? As I got that error I made a dimensional analysis by hand to check back if everything works out and it should result in the target dimension.

climbit avatar Apr 24 '21 22:04 climbit

I've got the same issue trying to convert Oe to A/m:

[si_magnetic_field_strength] = [current] / [length]
[cgs_magnetic_field_strength] = [mass] ** 0.5 / [length] ** 0.5 / [time]

@context test
    [cgs_magnetic_field_strength] <-> [si_magnetic_field_strength]: value * 1000 / (4 * π)
@end
from os import path

import pint

ureg = pint.UnitRegistry()

directory = path.dirname(path.abspath(__file__))
ureg.load_definitions(path.join(directory, 'unit_registry.txt'))
ureg.enable_contexts('test')

Unit = pint.Unit
Quantity = pint.Quantity

print(Quantity(1.0, ureg.oersted).to(ureg.ampere / ureg.meter))
# pint.errors.DimensionalityError: Cannot convert from 'oersted' ([mass] ** 0.5 / [length] ** 0.5 / [time]) to 'ampere / meter' ([current] / [length])

egormkn avatar May 22 '22 12:05 egormkn

It seems that contexts aren't taken into account for mixed units. Here's an even simpler example using the built-in definitions and contexts:

import pint
ureg = pint.UnitRegistry()
(1 * ureg.eV).to(ureg.K, "boltzmann")  # Works as expected
(1 * ureg.eV / ureg.m).to(ureg.K / ureg.m, "boltzmann")  # Raises exception

# DimensionalityError: Cannot convert from 'electron_volt / meter' ([length] * [mass] / [time] ** 2) to 'kelvin / meter' ([temperature] / [length])

I guess this is because contexts look for the exact dimensional transformation, and combining with another dimension ruins it.

ZedThree avatar Sep 26 '22 10:09 ZedThree

I have a bit of a brute-force method for this, that I was able to implement using the new facets system:

    def _try_transform(self, src_value, src_unit, src_dim, dst_dim):
        path = pint.util.find_shortest_path(self._active_ctx.graph, src_dim, dst_dim)
        if not path:
            return None

        src = self.Quantity(src_value, src_unit)
        for a, b in zip(path[:-1], path[1:]):
            src = self._active_ctx.transform(a, b, self, src)

        return src._magnitude, src._units

    def _convert(self, value, src, dst, inplace=False):
        """Convert value from some source to destination units.

        In addition to what is done by the PlainRegistry,
        converts between units with different dimensions by following
        transformation rules defined in the context.

        Parameters
        ----------
        value :
            value
        src : UnitsContainer
            source units.
        dst : UnitsContainer
            destination units.
        inplace :
             (Default value = False)

        Returns
        -------
        callable
            converted value
        """

        if not self._active_ctx:
            return super()._convert(value, src, dst, inplace)

        src_dim = self._get_dimensionality(src)
        dst_dim = self._get_dimensionality(dst)

        # Try converting the quantity with units as given
        if converted := self._try_transform(value, src, src_dim, dst_dim):
            value, src = converted
            return super()._convert(value, src, dst, inplace)

        # That wasn't possible, so now we break up the units and see
        # if we can convert them individually.

        # These are the new units resulting from any transformations
        new_units = src

        for unit, power in src.items():
            # Here, we're assuming that the transformation is based on [dim]**1,
            # while the unit in our quantity might be e.g. its inverse
            unit_uc = pint.util.UnitsContainer({unit: 1})
            unit_dim = self._get_dimensionality(unit_uc)

            # Now we try to convert between this unit and one of the
            # destination units
            for dst_part, dst_power in dst.items():
                dst_part_uc = pint.util.UnitsContainer({dst_part: 1})
                dst_part_dim = self._get_dimensionality(dst_part_uc)
                # If we're dealing with an inverse unit, we need to
                # invert the value to get the transformation right.
                # This is a bit hacky. Assuming we don't have any
                # non-multiplicative units, we should always be able
                # to convert zero though
                try:
                    value_power = value**power
                except ZeroDivisionError:
                    value_power = value

                if converted := self._try_transform(
                    value_power, unit_uc, unit_dim, dst_part_dim
                ):
                    value, new_unit = converted
                    # Undo any inversions
                    try:
                        value = value**dst_power
                    except ZeroDivisionError:
                        value = value
                    # It worked, so we can replace the original unit
                    # with the transformed one
                    new_units = (
                        new_units
                        / pint.util.UnitsContainer({unit: power})
                        * (new_unit**dst_power)
                    )

        return super()._convert(value, new_units, dst, inplace)

It basically splits up the units in the source and destination, and tries to convert between them pair-wise.

This has meant we no longer need to define both the base transformation, and any transformations with additional units that we want to use.

This is obviously explodes combinatorially with the number units in source and destination, so maybe there's something cleverer that can be done.

ZedThree avatar Nov 16 '22 18:11 ZedThree

How about trying to find the dimensions which are present in src but not in dst and viceversa and see if that conversion is present in the context?

hgrecco avatar Nov 16 '22 20:11 hgrecco

Maybe? Looking at an example:

>>> src = 1 * ureg.kelvin / ureg.metre
>>> dst = ureg.eV / ureg.m
>>> src.dimensionality
# <UnitsContainer({'[length]': -1, '[temperature]': 1})>
>>> dst.dimensionality
# <UnitsContainer({'[length]': 1, '[mass]': 1, '[time]': -2})>

We'd be able to see [temperature] isn't in dst and then subtract the remaining units from dst, which should leave us with a valid conversion.

That wouldn't work if there were two separate conversions needed though.

ZedThree avatar Nov 23 '22 14:11 ZedThree

I am closing this for the time being. feel free to reopen.

hgrecco avatar Apr 27 '23 04:04 hgrecco