Skip to content

Create a class to represent physical types#11204

Merged
mhvk merged 17 commits into
astropy:mainfrom
namurphy:physical-type-refactoring
Apr 16, 2021
Merged

Create a class to represent physical types#11204
mhvk merged 17 commits into
astropy:mainfrom
namurphy:physical-type-refactoring

Conversation

@namurphy

@namurphy namurphy commented Jan 4, 2021

Copy link
Copy Markdown
Contributor

This pull request creates a class, units.physical.PhysicalType, that represents the physical type(s) associated with a set of units. The overall purpose is to allow there to be multiple physical types associated with a particular set of units. This functionality could allow for pressure having equivalent units to energy density. Instances of this class will become the physical_type attribute of Unit instances. The dunder methods were written to mostly preserve backward compatibility, and also allow for some dimensional analysis.

The motivation and general API for this is described in #11202. Closes #11202. The idea for this came about while working on #11200 and PlasmaPy/PlasmaPy#859.

I did notice and fix one minor bug: the physical type for moles per cubic meter was corrected to "molar concentration". It was previously "molar volume", which was its inverse.

Things I still need to do:

  • Expand tests (situations with unknown or multiple physical types)
  • Expand the docstring of PhysicalType (parameters, examples including of dunder methods, etc.)
  • Write a changelog entry in iambic pentameter
  • Define multiple physical types (perhaps mostly in a follow-up pull request that will supersede Add new physical types and parametrize physical type tests #11200)

In #11202 the idea came up to have this work with equivalencies. I'm not quite sure if or how we should deal with that since it could get kind of messy and/or ambiguous. Oh well, that's for another day.

@github-actions github-actions Bot added the units label Jan 4, 2021
@namurphy namurphy marked this pull request as ready for review January 5, 2021 02:44
namurphy added a commit to namurphy/astropy that referenced this pull request Jan 6, 2021
Thermal conductivity, heat capacity, specific heat capacity, energy flux,
molar volume (had been previously misdefined), electrical resistivity,
electrical conductivity, magnetic dipole moment, volumetric rate,
jerk, snap, crackle, pop, temperature gradient, specific energy,
reaction rate, moment of inertia, catalytic activity, molar heat capacity,
molality, absement, volumetric flow rate, frequency drift, compressibility.
These changes were already proposed in astropy#11200, which was supplanted by the
current pull request (astropy#11204).

This commit was brought to you by needing an excuse to procrastinate for an
hour on Wikipedia.
@pep8speaks

pep8speaks commented Jan 6, 2021

Copy link
Copy Markdown

Hello @namurphy 👋! It looks like you've made some changes in your pull request, so I've checked the code again for style.

There are no PEP8 style issues with this pull request - thanks! 🎉

Comment last updated at 2021-04-16 18:54:54 UTC

@mhvk

mhvk commented Jan 6, 2021

Copy link
Copy Markdown
Contributor

@namurphy - this sounds interesting. But before I dive in, is it ready for review, or are you still changing things around?

@namurphy

namurphy commented Jan 6, 2021

Copy link
Copy Markdown
Contributor Author

@mhvk — thanks! It's ready for review. At this point the only likely changes are expanding the physical types...in particular for cases where the same set of units can correspond to multiple physical types.

Some things to look out for in code review:

  • Physical types for temperature units: Though K, °C, °F, K, and °Ra are not directly convertible, they all correspond to the same physical type.
  • Definitions of new physical types: The last ~20 physical types defined in _units_and_physical_types in physical.py are new. I put in absement and absity in there which are integrals of the position vector, but for the moment I decided against including even higher order integrals for which names have been proposed but aren't in wide use. I'm guessing the higher order integrals will probably only be used by integral kinematics hobbyists.
  • How equality is handled: I tried to keep the existing API for physical types as much as possible, while also expanding it.
  • Should the objects in physical.py be made private?: This module states that it is not intended for use by the user, in which case it might make sense to deprecate them. At the same time, the documentation for PhysicalType especially is probably worth having public.

All in all, this was a pretty fun pull request to write to give me an excuse to procrastinate doing other things. Thanks again!

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@namurphy - This looks very nice indeed! Quite a number of comments in-line. In terms of implementation, the main ones are to let str(physical_type) and set(physical_type) be the simple ways for the current as_set and as_string properties (which I do not like so much).

More general:

  • Would it be possible not to use typing yet? I've no strong opinion myself but feel it is something the project should decide on how to do it; for instance, it may be that we decide to go the numpy route of keeping this out of the code proper and work with stubs instead. (Also, there is a more specific problem that the units are not in fact instances of u.Unit...)
  • I prefer EAFP over LBYL myself, so would not worry so much about all the isinstance checks. This especially since it is code used internally. But it is just a preference!

On your specific points:

  1. I think your work-around for temperature is quite good. For now, it is nicely an implementation detail - perhaps we will eventually think of something better!
  2. I like the units you added, though am a bit less sure about 'count', 'pixel', etc., as "number of ..." does not really feel like a physical type.
  3. I like how you handle equality!
  4. I don't think the objects should be made private. The two functions were already public and I think it is fine to add the new class. It also means we can refer to PhysicalType elsewhere if needed. Regarding that, do you think we should add some of your examples in its docstring to the actual docs? Looking at the docs, we basically have next to nothing right now... Overall, my sense would be to do this as a follow-up PR, leaving this one as a supernice one about implementation.

Comment thread astropy/units/tests/test_physical.py Outdated
(u.m * u.s ** 8, "unknown"),
(u.m / u.m, "dimensionless"),
(hbar.unit, "angular momentum"),
(hbar.unit, "angular momentum"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplication

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙀 Thanks!

Comment thread astropy/units/tests/test_physical.py Outdated

def test_physical_type_as_set(length, pressure):
"""Test the `as_set` attribute of `physical.PhysicalType`."""
assert length.as_set == {"length"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, maybe just allow set(length)? (just looking at tests for now...)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! I suppose the best way to do this is to set __iter__ and __next__ and make it iterable, unless there's another way I'm unaware of.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha, yes, as you'll see below! (I tried, you need __iter__ only.)

Comment thread astropy/units/tests/test_physical.py Outdated
def test_speed():
assert (u.km / u.h).physical_type == 'speed'

@pytest.fixture

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big deal, but to me these fixtures obscure the tests a bit; not even sure all are used!

Comment thread astropy/units/physical.py Outdated
`physical_type` property.
"""

__all__ = ["def_physical_type", "get_physical_type", "PhysicalType"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you move __all__ back down? It seems the standard place within astropy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, force of habit! In PlasmaPy we ended up having the __all__ before as a PEP 8 thing. I'll move it back.

Comment thread astropy/units/physical.py
Comment thread astropy/units/physical.py Outdated
_physical_unit_mapping[physical_type_id] = physical_type

def get_physical_type(unit):
for ptype in physical_type.as_set:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With __iter__ defined as above, you can just iterate directly in physical_type.

Comment thread astropy/units/physical.py Outdated
def get_physical_type(unit):
for ptype in physical_type.as_set:
if ptype in _unit_physical_mapping.keys():
raise ValueError(f"{ptype} has already been defined")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to cover this?

Comment thread astropy/units/physical.py Outdated

physical_type_id = unit._get_physical_type_id()

known_unit = physical_type_id in _physical_unit_mapping

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd either do

ptype = _physical_unit_mapping.get(physical_type_id)
if ptype is not None:
   ...

or put the expression directly in the if statement.

Comment thread astropy/units/physical.py Outdated
other = self._standardize_physical_type_names(other)

if isinstance(other, set):
return other.issubset(self.as_set)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't other.intersection(self.as_set) good enough?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah...that makes a bit more sense, come to think of it. Like, if someone is looking for physical types of "mass" and "force" they might want to check if the physical type is either of those things. I should also document this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha, I did not actually think of that, somehow! Just that they might have a set of names for the same unit too. I now wonder if we're not being too generous here. Perhaps we should not allow comparison with a set of strings at all?

Comment thread astropy/units/physical.py Outdated

if isinstance(other, set):
return other.issubset(self.as_set)
elif isinstance(other, PhysicalType):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of those totally trivial things, but I'd put this first as it is the comparison that should be easiest to deal with.

@mhvk

mhvk commented Jan 7, 2021

Copy link
Copy Markdown
Contributor

Ah, and of course this needs a changelog entry in CHANGES.rst, under 4.3, units

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few more comments, though when I started going I realized it was not quite ready for the next round...

Comment thread CHANGES.rst Outdated
^^^^^^^^^^^^^

- Create the ``astropy.units.physical.PhysicalType`` class to represent the
physical types of units and all there to be more than one physical type

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all -> allow

Comment thread CHANGES.rst Outdated

- The ``physical_type`` attribute of units now returns an instance of
``astropy.units.physical.PhysicalType`` instead of a string. This
class was written to largely maintain the existing API. [#11204]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add "PhysicalType instances can be compared to strings, so no code changes should be necessary."

Comment thread astropy/units/tests/test_physical.py Outdated

Many of these tests are used to test backwards compatibility.
"""
if unit.physical_type != physical_type:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just assert unit.physical_type == physical_type? Pytest will then list essentially the failure message added here, no?

Comment thread astropy/units/tests/test_physical.py Outdated
Comment thread astropy/units/physical.py
Comment thread astropy/units/physical.py Outdated
@mhvk

mhvk commented Jan 11, 2021

Copy link
Copy Markdown
Contributor

@namurphy - the test failure on 32-bit looks related. I have not looked in detail, but maybe something got changed and not put back or so.

Otherwise, let me know when ready for the next round of review!

@namurphy

Copy link
Copy Markdown
Contributor Author

@mhvk — thanks! I addressed most or all of your code review suggestions, so it's ready for another round of review.

One change is that I decided to let def_physical_type expand the names of physical types for a PhysicalType instance that has already been defined for a unit. I did this thinking about backward compatibility, since there are a few uses of def_physical_type that I found on GitHub that define physical types that are being added in this PR, though with different names. This change will make it less likely to break code in those packages.

I also made PhysicalType instances hashable, since I figured I might end up using them as dict keys here and there.

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@namurphy - I like the idea to allow adding new physical types to existing units, as well as making them hashable.

Still quite a lot of comments. For the operators, mostly to ensure you return NotImplemented instead of raising (should have caught that earlier); for tests, nitpicks, but perhaps good to organize those a bit more.

The other things are probably more philosophical: I'm very much a "Easier to Ask Forgiveness than Permission" coder, and avoid "Look before you leap" isinstance checks - I like duck-typing, making the most minimal assumptions possible! And in non-user facing code it seems just not very necessary.

Comment thread astropy/units/core.py Outdated

>>> u.m.physical_type
'length'
>>> u.m.physical_type ** 3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd maybe remove this example here, leaving it for the (nice) more detailed examples later.

Comment thread astropy/units/tests/test_physical.py Outdated


def test_expanding_names_for_physical_type():
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring is wrong here. Also, you need to be sure to remove the definition from the physical types dict, i.e., some try/finally construct or setup/teardown (it may be an idea to move some functions in a test class with setup_class and teardown_class methods).

Comment thread astropy/units/tests/test_physical.py Outdated


def test_multiple_same_physical_type_names():
"""Test that def_physical_type does not define a """

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete docstring.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops! I have a bad habit of starting sentences and then forgetting to

Comment thread astropy/units/physical.py Outdated
(si.A / si.m ** 2, "electrical current density"),
(si.V / si.m, "electrical field strength"),
(
si.C / si.m ** 2,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why so weirdly offset? I'd just indent the set by 4 spaces, and keep the closing parenthesis behind it (or break after the set items)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh...I got in the habit of using black for formatting and have gotten accustomed to that style. I'll change this to match the Astropy style.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I noticed it is taking python programmers by storm... I'm still a bit on the fence, feeling the urge to minimize line changes is coming at the cost of readability. Though I can see the case for consistency. Anyway, some of the others are quite allergic to it!

Comment thread astropy/units/physical.py
Comment thread astropy/units/physical.py Outdated
elif isinstance(obj, PhysicalType):
return obj._unit
else:
raise TypeError("Expecting a unit or a physical type")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, since this is used in methods, you should not raise but return NotImplemented (after all, it is possible other knows how to deal with a PhysicalType even though we do not know how to deal with other). Probably another reason to do it on the class...

Comment thread astropy/units/physical.py Outdated
other = _standardize_physical_type_names(other)
return other.issubset(self._physical_type)
else:
return False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly, you should return NotImplemented here (and adjust __ne__ to take care of that as needed)

Comment thread astropy/units/physical.py Outdated
else:
return "{" + str(sorted(self._physical_type))[1:-1] + "}"

def __mul__(self, other):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be able to return NotImplemented as needed, I would write a new method, like

def _dimensional_analysis(self, other, op):
    # Maybe easiest to just include function in here.
    other_unit = _identify_unit_from_unit_or_physical_type(other)
    if other_unit is NotImplemented:
        return NotImplemented
    other_unit = _replace_temperatures_with_kelvin(other_unit)
    new_unit = getattr(self._unit, op)(other_unit)
    # Don't think we can get NotImplemented here any more
    return new_unit.physical_type

and then

def __mul__(self, other):
    return self._dimensional_analysis(other, '__mul__')
def __rmul__....
def __truediv__....
def __rtruediv__....

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! I like this!

Comment thread astropy/units/physical.py Outdated
Comment thread astropy/units/physical.py Outdated
(cgs.abcoulomb, 'electrical charge (EMU)')
]:
def_physical_type(unit, name)
if not isinstance(unit, core.UnitBase):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not sold on these checks for non-user code - just let the replacement fail?

Comment thread astropy/units/core.py Outdated
'length'

>>> u.Pa.physical_type
{'energy density', 'pressure', 'stress'}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I strongly disagree. A Pascal is not a unit of energy density (J / m³). It just decomposes to the same SI base units.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, of course you are right and I guess the more appropriate physical example would be (u.kg/(u.m*u.s**2)).physical_type. Somehow, I've always "decomposed in my head...".

In terms of implementation, this is tricky, though - I'm not sure how we can distinguish, since everything right now goes through _physical_type_id. Possibly, individual given units could be associated with a given physical type.

@maxnoe

maxnoe commented Jan 19, 2021

Copy link
Copy Markdown
Member

I have to issues with this:

  • By going through decomposition, special units loose their semantic meaning, which should be avoided
  • physical_type is now sometimes a string, sometimes a set. This should be consistent (give a set of length one if there is only one possibility). This avoids the need for checking what you got and won't break code if we realize we forgot a meaning.

@mhvk

mhvk commented Jan 19, 2021

Copy link
Copy Markdown
Contributor

@maxnoe - just to be clear, there is the reverse problem that this is trying to solve:

u.Unit("kg / (m s2)").physical_type
'pressure'

Internally, physical_type now already always is a set - is your concrete suggestion to let str and repr reflect that? The reason that for length 1 they look like strings is for backwards compatibility. Similarly, for that reason __eq__ is really defined as whether there is an intersection.

@maxnoe

maxnoe commented Jan 19, 2021

Copy link
Copy Markdown
Member

@mhvk ok, good, then I was only confusing the repr with the actual thing in the examples.

Which brings about another point then: the common python way is that repr(thing) identifies really what an object is, and only str should simplify this for nice printing.

So repr should probably be something like PhysicalType({length, }), not 'length' and str can then simplify for things that only have one type.

@mhvk

mhvk commented Jan 19, 2021

Copy link
Copy Markdown
Contributor

True, main problem of course is that changing __repr__ that way will break doctests, etc. But perhaps worth it for the enhanced clarity...

@namurphy

Copy link
Copy Markdown
Contributor Author

the common python way is that repr(thing) identifies really what an object is, and only str should simplify this for nice printing.

My intention was to maintain backwards compatibility with the previous API of having physical_type be a string, though I personally would prefer it to have repr be something more descriptive like you suggested. As @mhvk mentioned, this could end up breaking doctests, in particular in downstream packages. This change would probably be straightforward enough for other packages to fix, so I'd be okay with a minor break in backwards compatibility to match Python conventions. If we have agreement, I'll go ahead and change __repr__ when next I have a chance to work on this.

@mhvk

mhvk commented Jan 19, 2021

Copy link
Copy Markdown
Contributor

On balance, I think a PhysicalType(...) repr is better. I think most bigger packages that have documentation would test against astropy-dev, so we'd hopefully find out about problems before an actual release.

@namurphy

Copy link
Copy Markdown
Contributor Author

I changed PhysicalType.__repr__ to return strings like PhysicalType('length') for things with one physical type, and strings like PhysicalType({'absement', 'sustained displacement'}) when there is more than one physical type. I'd be okay with having both cases come back with set notation, or both without the set notation. I put a note in the changelog that stringifying physical types is not backwards compatible. Updating the doctests was pretty straightforward. This still could break doctests in downstream packages, but the fix for the broken doctests is pretty straightforward. I like the improved clarity.

I changed the docstrings to mention that PhysicalType instances represent the physical types of "dimensionally compatible" units, which provides context for units like pascals.

@mhvk — when you have a chance, could you partake in another round of code review? Thanks in advance!

Comment thread astropy/units/physical.py
@namurphy

Copy link
Copy Markdown
Contributor Author

Update: I fell down a rabbit hole and am fixing a few more things, so it'd be best to review this January 21 or later.

Comment thread astropy/units/physical.py Outdated
Comment on lines +367 to +371
def __array_ufunc__(self):
# This prevents operations like Unit(...) * PhysicalType(...)
# from returning a Quantity with a PhysicalType instance as the
# Quantity's value.
pass

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My motivation for adding this method was to try ways to prevent things like this from happening:

>>> u.m * u.m.physical_type
<Quantity PhysicalType('length') m>

I wanted Unit.__mul__ to return NotImplemented here so that this operation would fall back on PhysicalType.__rmul__, ideally without making any changes to Unit or Quantity. I speculated that having PhysicalType.__array_ufunc__ raise a TypeError would prevent this from happening, but it appears to only be necessary for PhysicalType.__array_ufunc__ to exist without actually doing anything. It's a bit confusing why this works, so I'm curious if there is a more understandable way to ensure this behavior.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll think about this - I think the underlying problem is np.array(physical_type) creating an object array. __array__ = None may be better.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I'll go with that for the time being.

@namurphy namurphy requested a review from mhvk January 27, 2021 17:49

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, went for another round. It is looking good. The only more major comment is that I'm not sure why in the dimensional analysis numbers are allowed.

Also, maybe time to squash the commits?

Comment thread astropy/units/core.py Outdated
Comment thread astropy/units/tests/test_physical.py Outdated
assert obscure_unit.physical_type == "unknown"


def _undef_physical_type(unit):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the way to do this is with teardown_function - that way it is guaranteed to run also if the test fails.

(I think all this is easier with a test class as suggested in https://github.com/astropy/astropy/pull/11204/files#r558759530 below)

Comment thread astropy/units/physical.py Outdated
Comment thread astropy/units/physical.py
Comment thread astropy/units/physical.py
Comment thread astropy/units/physical.py
Comment thread astropy/units/physical.py
Comment thread astropy/units/physical.py
Comment thread astropy/units/physical.py Outdated
Comment thread astropy/units/physical.py Outdated
@namurphy

Copy link
Copy Markdown
Contributor Author

@mhvk — Thank you! Your comments have been really helpful in improving this pull request on both the small scale code changes as well as the larger overall structure. I'll hopefully have a chance to implement the suggestions by early this week.

Also, maybe time to squash the commits?

That does seem like an increasingly good idea. We could either squash and merge when the PR is ready, or I could do a rebase and squash commits individually.

@nstarman

Copy link
Copy Markdown
Member

@pllim. Alternatively, for backward compatibility we could mod __contains__ in PhysicalType..

@pllim

pllim commented Apr 20, 2021

Copy link
Copy Markdown
Member

If I am the only one affected, there is no need, as I already patched it downstream. Not sure about other users. @karllark , are you affected by this change?

@nstarman

Copy link
Copy Markdown
Member

something like:

def __contains__(self, other):
    contains = super().__contains__(other)
    if not contains:
        warnings.warn("deprecated", AstropyDeprecationWarning)  # ?
        contains |= any([other in x for x in self])
    return contains

@karllark

Copy link
Copy Markdown
Contributor

If I am the only one affected, there is no need, as I already patched it downstream. Not sure about other users. @karllark , are you affected by this change?

I don't think this affects me. I haven't used this capability to my knowledge.

@rosteen

rosteen commented Apr 20, 2021

Copy link
Copy Markdown
Contributor

For what it's worth, this also breaks Jdaviz in a few places where we were using unit.physical_type.title to populate axis labels. We can fix it, but I wouldn't be surprised if there are other places throughout the ecosystem explicitly treating physical_type as a string.

@mhvk

mhvk commented Apr 20, 2021

Copy link
Copy Markdown
Contributor

Hmm, two reports when not even released makes me think that perhaps we should add something like

def __getattr__(self, attr):
    self_str_attr = getattr(str(self), attr, None)
    if self_str_attr is not None:
        warnings(... DeprecationWarning)
        return self_str_attr
    return super().__getattribute__()  # Raise AttributeError

With the deprecation error mentioning that if one relied on string attributes, one could just do str(physical_type).attr (which would be backwards compatible).

Or perhaps this is still rare enough that we can just put it in the docs (and the what's new entry)?

@astrofrog

Copy link
Copy Markdown
Member

Yes I agree that we should transparently make string methods work on the new physical type class, with a deprecation warning. I'm pretty sure I've also relied on physical_type being a string in some of my code.

@namurphy

Copy link
Copy Markdown
Contributor Author

I just created #11617 on addressing this. Thank you for raising these issues, and my apologies for breaking things!

namurphy added a commit to namurphy/astropy that referenced this pull request Apr 23, 2021
Because the physical_type attribute of a unit was a str before astropy#11204,
there may be code that depends on those str methods for the physical_type
attribute. This approach was adapted from code suggested in astropy#11204 after
it was merged.

Co-authored-by: Marten van Kerkwijk <mhvk@astro.utoronto.ca>
namurphy added a commit to namurphy/astropy that referenced this pull request Apr 25, 2021
Because the physical_type attribute of a unit was a str before astropy#11204,
there may be code that depends on those str methods for the physical_type
attribute. This approach was adapted from code suggested in astropy#11204 after
it was merged.

Co-authored-by: Marten van Kerkwijk <mhvk@astro.utoronto.ca>
nstarman pushed a commit to nstarman/astropy that referenced this pull request Apr 26, 2021
Parametrize and expand unit physical type tests

Now pretty much all of the existing unit/physical type pairs are
being tested in advance of the impending refactoring.  These tests
will check backward compatibility.

Define __eq__, __ne__, and __contains__ in PhysicalType

__eq__ and __ne__ are being written so as to preserve the current
API to the extent that is possible.  For example, if we do
`PhysicalType(u.m, "length") == "length"`, it will return True.
Similarly, we are now additionally able to do things like
`"length" in PhysicalType(u.m, "length")` and have it return
True also.

Define __repr__ and __str__ for PhysicalType

This too is to preserve backward compatibility.  If there is only one physical
type, then these methods will return the string of the physical type.  If there
is more than one physical type, then these methods will stringify the set
containing the multiple physical types.

Define __mul__, __truediv__, & __pow__ for PhysicalType

Multiplication, division, and exponentiation can be done for physical
types analogously to units, like volume = length * area or density of
wombats times velocity of wombats equals flux of wombats.  This would
enable a form of dimensional analysis.

Add new physical types

Thermal conductivity, heat capacity, specific heat capacity, energy flux,
molar volume (had been previously misdefined), electrical resistivity,
electrical conductivity, magnetic dipole moment, volumetric rate,
jerk, snap, crackle, pop, temperature gradient, specific energy,
reaction rate, moment of inertia, catalytic activity, molar heat capacity,
molality, absement, volumetric flow rate, frequency drift, compressibility.
These changes were already proposed in astropy#11200, which was supplanted by the
current pull request (astropy#11204).

Update class that represents physical types

 - Use kelvin to represent all temperatures internally.  Celsius, Fahrenheit,
   and Rankine temperatures are not directly convertible to kelvin, but these
   all represent the same physical type.
 - Updated docstrings of PhysicalType.  Add a warning banner that PhysicalType
   is not intended to be instantiated by the user.
 - Allow string representations of physical types to have underscores instead
   of spaces.  If someone is working with an object that has an attribute that
   is named after a physical type, then the words in that physical type will
   probably be separated by underscores.  Using underscores will simplify the
   code for this a bit.  (I'm planning on using this feature in PlasmaPy).
 - Allow string representations of physical types to contain multiple physical
   types that are separated by a backslash or a comma.  This is to maintain
   backward compatibility (for example with "momentum/impulse" which was a
   physical type before this pull request).

   and to contain multiple physical types that are separated by a
   backslash or a comma.

Expand and refine physical type tests

 - Include more tests of temperature
 - Test that string representations of different physical types
   work (e.g., that underscores are treated as spaces and that
   strings can have multiple physical types separated by commas
   or backslashes).

Refinements following code review

 - Change Jansky physical type back
 - Clean up docstrings
 - Remove type annotations
 - Make PhysicalType iterable over different physical type names (sorted)
 - Test iteration over PhysicalType
 - Remove `as_string` attribute in favor of using str(physical_type)
 - Remove `as_set` attribute in favor of using set(physical_type)
   - When necessary, use `_as_set` attribute internally
 - Remove duplication in parametrized test
 - Remove fixtures from tests
 - Test that a physical type name cannot be used for multiple units,
   except for unknown

Clean up PhysicalType class & tests

This includes changing an exception being raised with a warning being
issued when a unit's physical type is being redefined.

Update physical types and tests

 - Expanded definitions of physical types
 - Edited PhysicalType docstring to include discussion of comparisons
   and how physical type names are processed.  Also revised examples.
 - No longer allow physical type names in a single string to be separated
   by a comma, while stating that the slash separator is kept for backwards
   compatibility.
 - Removed `_get_physical_type_id` method. Now using a private
   attribute instead.
 - Remove feature of allowing PhysicalType instances to be compared with sets
   that may contain strings representing physical types.

Clean up PhysicalType class

 - Simplify `__iter__` by using `yield`
 - Rename some private attributes

Raise ValueError when trying to redefine unit

Also added a note to docstrings of `get_physical_type` and `def_physical_type`
that these are not intended for use by users.
nstarman pushed a commit to nstarman/astropy that referenced this pull request Apr 26, 2021
@pllim pllim mentioned this pull request Apr 26, 2021
4 tasks
@astrofrog

astrofrog commented May 2, 2021

Copy link
Copy Markdown
Member

Another downstream package whose tests are broken with the current astropy latest: radio-astro-tools/spectral-cube#709 - is there any way to fix this? Could we perhaps make sure the hash is the same as the string hash?

Ping @mhvk @namurphy

duncanmmacleod added a commit to duncanmmacleod/gwpy that referenced this pull request Jun 21, 2021
astropy/astropy#11204 removed `imperial` from being imported into `astropy.units` namespace
duncanmmacleod added a commit to duncanmmacleod/gwpy that referenced this pull request Jun 21, 2021
astropy/astropy#11204 removed `imperial` from being imported into `astropy.units` namespace
Comment thread astropy/units/physical.py
from . import si
from . import astrophys
from . import cgs
from . import imperial

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at #11975 , it is unclear to me now why this line was removed...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh my. This was a huge mistake on my part. I apologize for that. I have no idea how or why I let that slip in there....

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a tool warned "unused import"?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow units to have multiple physical types

10 participants