Repository navigation
Draft: An equivalency for geometric conversions - #12847
deepchatterjeeligo wants to merge 9 commits into
Conversation
There was a problem hiding this comment.
Welcome to Astropy 👋 and congratulations on your first pull request! 🎉
A project member will respond to you as soon as possible; in the meantime, please have a look over the Checklist for Contributed Code and make sure you've addressed as many of the questions there as possible.
If you feel that this pull request has not been responded to in a timely manner, please leave a comment mentioning our software support engineer @embray, or send a message directly to the development mailing list. If the issue is urgent or sensitive in nature (e.g., a security vulnerability) please send an e-mail directly to the private e-mail [email protected].
|
Hi astropy maintainers, I created a small patch for some natural unit conversions. I am happy to add any additional testing, docs before this is merged into main. Do let me know. Thank you. |
There was a problem hiding this comment.
Thanks @deepchatterjeeligo for the PR!
I think something like this is a good idea, but I I think some of Astropy's unit architecture needs to be improved to best realize your suggestion. Allow me to work through some musings.
Geometric units are really different unit systems than SI and CGS. Re-imagining your example with this understanding:
(see See https://docs.astropy.org/en/stable/units/decomposing_and_composing.html#converting-between-systems)
>>> import astropy.units as u
>>> qSI = (3 * u.km)
>>> q = qSI.to_system(u.schwartzchild_system)[0] # note the [0] :(
>>> q.to(u.solMass)
<Quantity 2.03165998 solMass>Here I'm thinking the unit system carries the equivalency. That doesn't seem unreasonable given the nature and intent of geometrized unit systems.
Unfortunately, I don't think Astropy has all the necessary mechanics to realize this — see #11525 @adrn.
With a UnitSystem class and realizations thereof we could even condense the above example to
>>> import astropy.units as u
>>> (3 * u.km).to(u.solMass, equivalencies=u.schwartzchild_system)
<Quantity 2.03165998 solMass>Since the UnitSystem doesn't yet exist, implementing the equivalencies portion feels like a good intermediate step. I recommend renaming mass_length_time and adding in any remaining equivalencies. Then the equivalency will approximate a unit system.
| __all__ = ['parallax', 'spectral', 'spectral_density', 'doppler_radio', | ||
| 'doppler_optical', 'doppler_relativistic', 'doppler_redshift', 'mass_energy', | ||
| 'brightness_temperature', 'thermodynamic_temperature', | ||
| 'mass_length_time', 'brightness_temperature', 'thermodynamic_temperature', |
There was a problem hiding this comment.
In the above example I changed mass_length_time to schwartzchild_system. Looking at https://en.wikipedia.org/wiki/Geometrized_unit_system, there a many geometrized unit systems, e.g. Planck natural units, so
mass_length_time isn't specific enough to inform the user that the relationship is through Schwarzschild radius of a nonrotating uncharged black hole. I like the idea of adding new unit systems and the one in this PR would be a good addition amongst many: Planck, cosmology (8πG = 1), Galactic & extragalactic (rescaled SI), etc.
| """ | ||
| schwarzchild_factor = _si.GM_sun / _si.c**2 # 1 M_sun -> approx 1.5 km | ||
|
|
||
| sol_mass_to_meters = lambda _: _ * schwarzchild_factor.to('m').value |
There was a problem hiding this comment.
These functions don't rely on any arguments to the equivalency, so they can be defined outside the function. That way they can also be serialized by, e.g. pickle.
It's probably good to make them "private" functions
def _sol_mass_to_meters(mass):
return mass * schwarzchild_factor.to_value(u.m)| schwarzchild_factor = _si.GM_sun / _si.c**2 # 1 M_sun -> approx 1.5 km | ||
|
|
||
| sol_mass_to_meters = lambda _: _ * schwarzchild_factor.to('m').value | ||
| meters_to_sol_mass = lambda _: _ / schwarzchild_factor.to('m').value |
There was a problem hiding this comment.
Also, I think having variable names is easier to read, so
| meters_to_sol_mass = lambda _: _ / schwarzchild_factor.to('m').value | |
| meters_to_sol_mass = lambda dist: dist / schwarzchild_factor.to('m').value |
Also, using to_value
| meters_to_sol_mass = lambda _: _ / schwarzchild_factor.to('m').value | |
| meters_to_sol_mass = lambda _: _ / schwarzchild_factor.to_value(u.m) |
| [[2 * u.solMass, 3 * u.km, 10 * u.microsecond], | ||
| [1 * u.kg, 7.54e-31 * u.km, 2.47e-36 * u.s]] | ||
| ) | ||
| def test_mass_length_time(mass, length, time): |
| - Added a ``mass_length_time`` equivalency for converting mass, length, and time | ||
| based on geometric units. | ||
|
|
There was a problem hiding this comment.
For Astropy code contributions we actually don't directly commit to CHANGES.rst, but instead use a system call towncrier to assemble the changelog.
See https://docs.astropy.org/en/latest/development/workflow/development_workflow.html#add-a-changelog-entry
I see you added a changelog fragment already, so this can be removed.
| - Added a ``mass_length_time`` equivalency for converting mass, length, and time | |
| based on geometric units. |
|
|
||
| .. EXAMPLE END | ||
|
|
||
| Mass-Length-Time Equivalency |
There was a problem hiding this comment.
Given that there are many types of geometrized unit systems, I think this should be renamed.
mhvk
left a comment
There was a problem hiding this comment.
@deepchatterjeeligo - thanks from me too! Like @nstarman, I wondered about a unit system, since in principle the list should arguably include all physical types... That said, even though something like unit.geometrized might look like a nice equivalent to unit.si or unit.cgs, it has a real problem: after conversion many physical types share the same unit (length, area, etc.), and thus one cannot convert back to SI without knowing whether one wants a mass, etc. (i.e., unit.cgs == unit.geometric.cgs would not hold, unlike unit.si.cgs). So, overall, I'm not sure this would actually work.
So, I second going for the equivalency! It would seem good, however, to include more physical types like velocity, etc.
In fact, I think we could make it more general by defining this as a general "geometrized" equivalency, which takes as an argument a physical type that one would want to convert (defaulting perhaps to the more or less standard set on the wikipedia page).
So, if one wanted to be sure that the units were OK to start with (and thus if mass was in 'm/s', it would raise an error), one could do:
mass.to(u.km, equivalencies=u.geometrized('mass'))
Indeed, another argument to the class constructor could be what one wanted c and G to be, i.e., one might be able to do something cosmological like
density.to(u.Gpc**-2, equivalencies=u.geometrized('density', G=8*np.pi)
p.s. With a bit of extra coding (follow-up?), the above scheme could easily be extended to units not initially included, since the SI decomposition could be used to give the right factors.
| sol_mass_to_meters = lambda _: _ * schwarzchild_factor.to('m').value | ||
| meters_to_sol_mass = lambda _: _ / schwarzchild_factor.to('m').value | ||
|
|
||
| sol_mass_to_seconds = lambda _: ( |
There was a problem hiding this comment.
To add to @nstarman's comments, for equivalencies the functions should not use units internally, rather they should assume the input are values in one unit and produce output that are values in the other unit. Probably easiest to look more closely to the other examples in this file.
In fact, since all these conversions are just multiplications with numbers, there is no need to have any function: one should just give the right input and output units. E.g., for the mass one wants to use the mass that would correspond to 1 m:
In [7]: mass_length = [(u.Unit(u.kg*const.c.value**2/const.G.value), u.m)]
In [8]: (1*u.Msun).to(u.km, mass_length)
Out[8]: <Quantity 1.47662504 km>
|
Hi @mhvk @nstarman thanks for the suggestions. I'll address the individual comments above related to the code separately. My initial aim with this was to have something quick, analogous to the existing |
|
@deepchatterjeeligo - sounds like a plan! |
|
Hi reviewers. I just wanted to mention that I am tagging this PR as a draft for now. I have worked on some of the requests, but some parts are still pending (and needs some thought). I also see that the CI is breaking (although the unittests corresponding to the changes were working fine locally) which I will diagnose later in the week. But feel free to have a second look on the changes so far. |
99435fc to
a69992f
Compare
There was a problem hiding this comment.
Looks like great progress! Thanks for keeping on top of this.
I only took a quick glance, so definitely ping me again.
Something to think about: from Wikipedia
"The geometrized unit system is not a completely defined system. Some other systems are geometrized unit systems in the sense that they set these, in addition to other constants, to unity, for example Stoney units and Planck units."
Maybe the geometrized should be the "parent" object for flavors of geometrized units that complete the unit system, e.g. Planck.
| ], "mass_energy") | ||
|
|
||
|
|
||
| @dataclass |
There was a problem hiding this comment.
Just glancing at this quickly... Maybe you've thought about this and this is the better implementation, but this feels like a case for Enum.
There was a problem hiding this comment.
Just looked at the docs for Enum, seems like a similar approach. Both Enum and Dataclasses are part of the standard library.
| <Quantity 4.03697802e+29 kg> | ||
| """ | ||
| physical_type = physical.get_physical_type(physical_type) | ||
| mass_length_time_physical_types = [ |
| allowed_physical_types += temperature_physical_types | ||
| allowed_physical_types += velocity_physical_types | ||
|
|
||
| assert physical_type in allowed_physical_types, ( |
There was a problem hiding this comment.
Assert statements are stripped in certain python compilation modes. If you don't want this check run in "optimized mode", this is fine, otherwise use an if statement and manually raise the desired error
There was a problem hiding this comment.
I wanted to take this opportunity to check if this is done in the CI pipeline.
| f"{[v._physical_type for v in allowed_physical_types]}" | ||
| ) | ||
|
|
||
| conversions = GeometricUnitConversionFactors() |
There was a problem hiding this comment.
I think with Enum you don't need to define a new instance each time.
There was a problem hiding this comment.
I have not worked with Enum a lot. But let me know if this is a potential issue. I could, as well, move the instantiation outside the scope of the function at the module level.
| # area-time**2 | ||
| ( | ||
| si.m**2, si.s**2, | ||
| lambda meter_sq: conversions._homogeneous_conversion( |
There was a problem hiding this comment.
I think all these equivalencies can be defined external to this function.
And @mhvk mentioned that these might not even require lambda functions. Not sure if that's still the case, but worth checking out!
There was a problem hiding this comment.
It can be but in almost all other existing equivalencies, this is how it has been implemented.
For example:
- https://github.com/astropy/astropy/blob/main/astropy/units/equivalencies.py#L126-L137
- https://github.com/astropy/astropy/blob/main/astropy/units/equivalencies.py#L203-L237
- https://github.com/astropy/astropy/blob/main/astropy/units/equivalencies.py#L340-L364
So that might be a part of larger refactoring that needs to be set in a future milestone
There was a problem hiding this comment.
OK I see that there is a stage where the tests are run in parallel: https://github.com/astropy/astropy/blob/main/.github/workflows/ci_workflows.yml#L150-L181 which is passing, so I believe there is no issue with serialization etc
There was a problem hiding this comment.
My apologies actually. I see that this stage had passed in a previous pipeline, but in the most recent one it has failed. Please ignore my above comment
There was a problem hiding this comment.
OK, the failure was caused by a typo in a docstring unrelated to what I had mentioned before.
I am guessing your concern is regarding the serialization of functions for multiprocessing. I believe the parallel mode for tox in the 32-bit and parallel stage would have caught that. But, like I mentioned, my implementation follows that of most other existing equivalencies. Maybe refactoring the callables could be an item for a later release if its an issue.
There was a problem hiding this comment.
As @nstarman noted, there should be no need for the lambda functions.
|
BTW @pllim could you help me resolve the very first stage of the pipeline: Check PR change log / Check if towncrier change log entry is correct (pull_request_target) I see that stage was recently added. If you could let me know what I'm missing. |
|
Yes, definitely what's-new worthy! |
| assert converted_si_qty == si_qty | ||
|
|
||
|
|
||
| def test_mass_density_geometric_equivalency(): |
There was a problem hiding this comment.
Hi @mhvk based on the discussion we had earlier, I have changed the PR to include a few more physical types. I will mention in brief what all has been taken into account.
- Irreducible units mass, length, time can be converted into each other
- Composite units which are homogeneous like area i.e. length squared can be converted to mass squared etc
- The challenge is for heterogeneous composite units like density. Currently it works for kg/m^3 to 1/kg^2, 1/s^2 etc and the other which way. But not for kg/m^3 to g/s^3, or to 1/(m*s) etc.
Having support for all of these is a nice to have but will need more work. But let me know what you think of this until now. I have tried adding tests for each of the conversion, but if something is missing let me know. Adding support for other physical types like pressure/force is not difficult if we restrict the support to only target unit with a single base - like pressure in SI to 1/(length or time or distance)**2, similar to mass density.
Also since the code has changed a lot since my initial PR, I am happy to rebase to a single commit if that helps. I have not yet taken care of the definition of the G = 1 vs. 8 pi G = 1, but its on my radar. Thanks
There was a problem hiding this comment.
EDIT: after a bit more thought, what I had at first would not work, so an improved version:
I like where this is going but think a much more direct approach is possible, using a dict of equivalencies keyed by physical type. Something starting with the bases:
geometrized_bases = {
u.m: {
u.m: u.dimensionless_unscaled,
u.s: u.Unit(u.s/_si.c.value),
u.kg: u.Unit(u.kg * _si.c.value**2 / _si.G.value)),
...},
u.kg: {
...},
...}
You would then calculate the actual equivalencies, including more complicated ones, on-the-fly (caching the result), with a version of your heterogeneous one, like
geometrized_equivalencies = {}
def geometrized(physical_type):
physical_type = PhysicalType(physical_type)
if physical_type not in geometrized_equivalencies:
pt_unit = physical_type._unit
factor, bases, powers = pt_unit
equivalencies = []
for to_base in geometrized_bases.values():
to_unit = u.Unit(pt_unit.factor)
for base, power in zip(pt_unit.bases, pt_unit.powers):
to_unit *= geometrized_bases[base][to_base] ** power
equivalencies.append((pt_unit, to_unit))
geometrized_equivalencies[physical_type] = Equivalency(equivalencies, name='geometrized ' + str(physical_type))
return geometrized_equivalencies[physical_type]
Note that I did not test the above! But I think it would be simpler than what you have and should do the same.
| Schwarzschild radius for sun is 3 km, which is about 10 microsecond | ||
| of light crossing time. | ||
| """ | ||
| converted_mass = mass.to('kg', equivalencies=u.geometrized('mass')).value |
There was a problem hiding this comment.
Comparisons are normally done like
assert_quantity_allclose(mass.to('kg', equivalencies=u.geometrized('mass'), comparison, atol=...)
see elsewhere in this file.
There was a problem hiding this comment.
These particular tests do not actually seem to do anything interesting! You are converting something that is already a mass to a mass (and length to length, time to time) here, which will not even use the equivalency.
|
|
||
| # geometrized equivalencies | ||
| assert u.kg.is_equivalent( | ||
| (u.m, u.s), equivalencies=u.geometrized('mass')) |
There was a problem hiding this comment.
These tests are best split in 2, by pytest.mark.parametrizeing the function for u.m and u.s - as is, it will just check u.m, see that it is equivalent, and return True
| # area-time**2 | ||
| ( | ||
| si.m**2, si.s**2, | ||
| lambda meter_sq: conversions._homogeneous_conversion( |
There was a problem hiding this comment.
As @nstarman noted, there should be no need for the lambda functions.
| ) | ||
| return conversion_factor.get(target_base.__str__()) | ||
|
|
||
| def _irreducible_conversion(self, source_quantity, target_unit): |
There was a problem hiding this comment.
These functions duplicate what the unit machinery already does - you just need to define the equivalency with the units scaled the right way -- see my earlier comment.
There was a problem hiding this comment.
there should be no need for the lambda functions.
Can you help me understand this better. As I mentioned before, there are several places, where this is done: #12847 (comment)
the third and fourth arguments of the tuple need to be callables?
There was a problem hiding this comment.
The third and fourth arguments can be omitted, in which case effectively it is treated as a pass-through function. So, conceptually, for q.to(new_unit, equivalencies=[(unit1, unit2)]), the quantity will first be converted to unit1, then its unit is replaced by unit2, and finally it is converted to new_unit. Maybe easiest to look again at #12847 (comment) and play around with the example there in an interactive session.
|
|
||
| conversions = GeometricUnitConversionFactors() | ||
|
|
||
| if physical_type in velocity_physical_types: |
There was a problem hiding this comment.
This linked if-elseif statement could I think more logically be done with a dict that is keyed by physical_type (they are hashable, so that should work), i.e.,
return geometrized_equivalencies[physical_type]
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class GeometricUnitConversionFactors: |
There was a problem hiding this comment.
To me it is slightly illogical to define this in terms of string units - I think conceptually it makes more sense to have a dict of equivalencies (see below), and here a dict of dict keyed by the units rather than there strings. But I think it can be done more directly by just defining a dict of equivalencies -- see main comment.
|
I did not have time to look at it this week. Will take a stab at the comments next week. |
|
Hi humans 👋 - this pull request hasn't had any new commits for approximately 4 months. I plan to close this in 30 days if the pull request doesn't have any new commits by then. In lieu of a stalled pull request, please consider closing this and open an issue instead if a reminder is needed to revisit in the future. Maintainers may also choose to add keep-open label to keep this PR open but it is discouraged unless absolutely necessary. If this PR still needs to be reviewed, as an author, you can rebase it to reset the clock. If you believe I commented on this pull request incorrectly, please report this here. |
|
I'm going to close this pull request as per my previous message. If you think what is being added/fixed here is still important, please remember to open an issue to keep track of it. Thanks! If this is the first time I am commenting on this issue, or if you believe I closed this issue incorrectly, please report this here. |
Description
This pull request adds a new unit equivalency called
mass_length_timefor converting mass, length, time in geometric units. As an example, by using this equivalency, one can express the Schwarzchild radius for sun in km or in solar masses.Checklist for package maintainer(s)
This checklist is meant to remind the package maintainer(s) who will review this pull request of some common things to look for. This list is not exhaustive.
Extra CIlabel.no-changelog-entry-neededlabel. If this is a manual backport, use theskip-changelog-checkslabel unless special changelog handling is necessary.astropy-botcheck might be missing; do not let the green checkmark fool you.backport-X.Y.xlabel(s) before merge.