Skip to content

Allow vector-like string input in Quantity parsing - #19214

Merged
nstarman merged 2 commits into
astropy:mainfrom
kartavayv:units-quantity-parsing
Feb 26, 2026
Merged

nstarman merged 2 commits into
astropy:mainfrom
kartavayv:units-quantity-parsing

Conversation

@kartavayv

Copy link
Copy Markdown
Contributor

Description

Before this PR, we couldn't do u.Quantity("[ 7 8 9]") and get a nice Quantity, however u.Quantity(np.array([ 7 8 9])) and u.Quantity([ 7 8 9]) were allowed. The changes made in this PR specifically help in parsing back the LaTeX strings into Quantity objects; in short, it supports the LaTeX reader. (More context: #19110)

Also includes, some new tests covering the new accepted inputs

@github-actions

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.

  • Do the proposed changes actually accomplish desired goals?
  • Do the proposed changes follow the Astropy coding guidelines?
  • Are tests added/updated as required? If so, do they follow the Astropy testing guidelines?
  • Are docs added/updated as required? If so, do they follow the Astropy documentation guidelines?
  • Is rebase and/or squash necessary? If so, please provide the author with appropriate instructions. Also see instructions for rebase and squash.
  • Did the CI pass? If no, are the failures related? If you need to run daily and weekly cron jobs as part of the PR, please apply the "Extra CI" label. Codestyle issues can be fixed by the bot.
  • Is a change log needed? If yes, did the change log check pass? If no, add the "no-changelog-entry-needed" label. If this is a manual backport, use the "skip-changelog-checks" label unless special changelog handling is necessary.
  • Is this a big PR that makes a "What's new?" entry worthwhile and if so, is (1) a "what's new" entry included in this PR and (2) the "whatsnew-needed" label applied?
  • At the time of adding the milestone, if the milestone set requires a backport to release branch(es), apply the appropriate "backport-X.Y.x" label(s) before merge.

@github-actions github-actions Bot added the units label Jan 24, 2026
@nstarman nstarman added this to the v8.0.0 milestone Jan 24, 2026

@nstarman nstarman left a comment

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.

Thanks @kartavayv. I like this and think it's useful for users.
Can you post the timing stats in this thread to show the speed comparison?
Thanks!

Comment thread astropy/units/quantity.py Resolved
Comment thread astropy/units/quantity.py Resolved
@kartavayv

kartavayv commented Jan 26, 2026 •

Copy link
Copy Markdown
Contributor Author

Hey @nstarman , I made some changes just like you suggested. Thank you very much for guiding me! 😉

Some timing stats:

Scalar String Parsing:

  • main: 5.33 μs ± 311 ns
  • this branch: 5.64 μs ± 82.4 ns

List-like inputs (u.Quantity([4, 8 , 9])) (no regression):

  • main: 3.28 μs ± 235 ns
  • this branch: 3.04 μs ± 55.3 ns

New Vector-like strings:

  • this branch: 6.44 μs ± 151 ns

All timings are mean ± std over 7 runs, 100k loops each.

Apart from that, I have also tried to make regex a little bit different (using NUMS and VECTOR), I wonder if that's good....

@nstarman
nstarman requested a review from mhvk January 26, 2026 14:58
@nstarman

Copy link
Copy Markdown
Member

Thanks @kartavayv! IMO, since scalar string input to a Quantity isn't meant to be in a hot loop, the minor performance regression is fine. Moreover, since this now enables a string vector input to Quantity, people can actually get a performance increase by moving the Quantity(str) out of the loop!

Comment thread astropy/units/quantity.py Resolved
Comment thread astropy/units/quantity.py Resolved

@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.

I like the idea! However, should it not work recursively, so that it can deal with multi-dimensional quantities too? I.e., u.Quantity(str(q)) == q for any quantity for which the array is not summarized? Also, I think one might as well be generous on input and also accept commas (like [1., 2., 3.], as one gets in the __repr__).

Note that this needs a changelog fragment (19214.feature.rst in docs/changes/units). I think it might also make sense to have the tests be a bit more exhaustive. Could be of the form

@pytest.mark.parametrize(q, [VAROUS QUANTITIES])
def test_string_roundtrip(q):
    s = str(q)
    assert_array_allclose(u.Quantity(s), q)
    r = repr(q)
    assert_array_allclose(u.Quantity(r[9:-1]), q)

Comment thread astropy/units/quantity.py Resolved
Comment thread astropy/units/quantity.py Outdated

def _parse_floats_from_whitespace(data:str)->list[float]:
data = data.strip("[]")
return [float(a) for a in data.split()]

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.

If we were to use data.replace(",", " ").split() one could have "," inside as well.

Comment thread astropy/units/quantity.py Resolved
@mhvk

mhvk commented Jan 26, 2026

Copy link
Copy Markdown
Contributor

If we go with replacing white space with ",", we could use ast.literal_eval to do some of the parsing.

@kartavayv

kartavayv commented Jan 27, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks! @nstarman and @mhvk

Also, I think one might as well be generous on input and also accept commas (like [1., 2., 3.], as one gets in the repr).

I feel like we should do that, people might use commas (,) by habit, a lot, and it's a bit odd to allow something like "[4 7 8]" without commas....

However, should it not work recursively, so that it can deal with multi-dimensional quantities too?

This also looks like a good idea. I am working on it (interesting challenge!)

I will update shortly after making the suggested improvements!

@kartavayv

Copy link
Copy Markdown
Contributor Author

Also, I was thinking, since the current code allows one dot, plus or minus [.+-] after a scalar and if i put NUM in VECTOR, then in VECTOR too, then v = re.match(pattern, value) selects, these characters after the allowed patterns too, which upon using, float() gives us : TypeError

Example

u.Quantity("56..")

OUTPUT: TypeError: Cannot parse "56.." as a Quantity. It does not start with a number.

Because ```v.group()`` is 56.. (dot-dot)

I think it's a good idea to make some changes in the pipeline to remove these extra characters, should I go in that direction?...or should I remove, [.+-]? entirely, which makes it a little bit strict (don't feel like it's a good idea) and creates a problem in unit_string creation

@nstarman

Copy link
Copy Markdown
Member

Start simple and build from there :). I recommend implementing changes as discrete commits, which makes building / reviewing changes much simpler.

@mhvk

mhvk commented Jan 28, 2026

Copy link
Copy Markdown
Contributor

@kartavayv - the inclusion of some of the extra stuff was on purpose, so that float could fail on a case where it is pretty obvious someone is trying to put in a number, and then give the hopefully more informative error message... But if it is easier as part of that to change it, go ahead (ideally ensuring that the tests keen on passing, i.e., that the error does not change).

Comment thread astropy/units/tests/test_quantity.py Resolved
@kartavayv

kartavayv commented Jan 31, 2026 •

Copy link
Copy Markdown
Contributor Author

IMPROVEMENTS

  • Added new and better tests for string vectors with and without dimensions

  • Added an extra strip() in data = data.replace(",", " ").strip().strip("[]").split() because it was trying to convert "9]" (for generic examples) into a float, which is weird, and I have no idea why that was happening! (Now it works fine!)

  • Included spaces again as separators, which I had excluded (but I think "maintainers" like it)

NEW FEATURES...?

  • Since @mhvk suggested the idea of multi-dimensional quantities, should I proceed to do that....?

SOMETHING UNUSUAL

Since now the parser accepts both spaces and commas as separators, so things like u.Quantity([7, 8 9]) also gives us nice Quantities, is that a good idea...?

Comment thread astropy/units/tests/test_quantity.py Resolved
@kartavayv

Copy link
Copy Markdown
Contributor Author

@nstarman - while trying to remove mixed separators, I found out that whitespace should either work for grammar (parsing) or as a formatter.

CONTEXT:

Code snippet:

VECTOR: Final = rf"""
        \[\s*
        {NUM}
        (?:
        **((\s*,\s*)|
        (\s*))**  #subject to change
        {NUM}
        )*
        \s*\]
"""

We want a same separator for the whole input to prevent mixed separators. If we allow multiple white spaces as separators ( ((\s*,\s*)| (\s*)) --> (?P<sep>,|\s+) and we get an input with inconsistent number of white spaces between the numbers, the regex backtracks and uses \s*+ from \s*\] to parse the inconsistent whitespaces (i.e. (?P<sep>,|\s+) fails as soon as it encounters an inconsistent whitespace between the numbers). Same is the case with using a single space ((?P<sep>,| )) as a separator, in case of multiple whitespaces (?P<sep>,|\s+) fails, and if we use \s*(?P<sep>,|\s+)\s*, then first \s* consumes all the whitespaces, and (?P<sep>,|\s+) still fails to capture a space as a separator.

But regex still parses the string in both the cases mentioned above, because of \s*\], which means each digit is parsing independently because of regex backtracking.

So, either we need to use whitespaces as only separators or to do just formatting (i.e. allow any number of spaces between the [] and numbers.

I guess using only commas as separator and allowing any number of white spaces before and after it is a good idea.

I might be missing something, any design suggestions will help a lot 😉

@nstarman

nstarman commented Feb 3, 2026 •

Copy link
Copy Markdown
Member

@kartavayv the regex can distinguish between space between [ x / x ] where it's for formatting versus between numbers [x y] where it's the delimiter. So we should be able to support [ x y ]/[x y ]/etc. versus [ x, y ].

@kartavayv

Copy link
Copy Markdown
Contributor Author

I was trying to add an indefinite number of white spaces as a delimiter 😢🫤, now it's fine!

Should I also try to add multi-dim vector like strings or tackle that in some other PR...?

Thanks a lot for helping me ❤️

@mhvk

mhvk commented Feb 4, 2026

Copy link
Copy Markdown
Contributor

@kartavayv - sorry to comment without actually having had time to look at the PR again, but my tendency would be to try to do the multi-D as well. I think you could do it by iteratively interpreting strings inside [...]. But it is probably best to first experiment outside of astropy on how this would work, and then push it up (I recommend generally to test locally, as waiting for CI to run is slow and a bit wasteful of resources). That said, I'm not stuck to having multi-D work in the first round.

@nstarman

nstarman commented Feb 4, 2026

Copy link
Copy Markdown
Member

@mhvk this PR came about from needing to read 1D Quantity strings in #19110. For the sake of that PR, I'd prefer to keep this PR on the nice expansion from scalars to 1D arrays and leave the 1D -> ND generalization for a followup.

@mhvk

mhvk commented Feb 4, 2026

Copy link
Copy Markdown
Contributor

Sure, fine to do it in steps!

@nstarman nstarman left a comment •

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.

There's some edge cases that will fail

  • "[7 8 9] eV"
  • "[1. 2. 3.] eV"

I think the following will help catch this

NUM = r"""
[+-]?
(?:
    (?:\d+\.?\d*) | (?:\.\d+) |
    (?:[nN][aA][nN]) |
    (?:[iI][nN][fF](?:[iI][nN][iI][tT][yY])?)
)
(?:[eE][+-]?\d+)?
[.+-]?
"""

SEP = r"(?:\s*,\s*|\s+)"  # comma OR (one+ whitespace)

VECTOR_1D = rf"""
\[
\s*
{NUM}
(?: {SEP} {NUM} )*
(?: {SEP} )?            # allow a trailing separator (covers "[1.  2.  3.]" and "[1,2,3,]")
\s*
\]
"""

I'm not so good at regex, so other maintainer eyes on this will be useful!

@kartavayv

Copy link
Copy Markdown
Contributor Author

@nstarman - I tried the edge cases that you suggested, and they are getting parsed 😊. Probably you are pointing to something else that I am unable to see yet!
image

  1. Initially, I thought to allow only a single space and single comma as a separator (which is strict), but since you are now suggesting an indefinite number of spaces in between the numbers, I am going to do that! Also, if there are n spaces between two numbers, we won't make it strict to have the same number of spaces between any other two adjacent numbers, which allows vectors like:
    [ 7 8 9] and with units [ 7 8 9] eV

    So, zero expectations of consistent separator behavior

  2. I tried using SEP = r"(?:\s*,\s*|\s+)" but it is accepting mixed separator, then I tried using capturing groups but that's getting way to complex and since you suggested this, I have separated VECTOR_1D into 2 sort of subclasses VECTOR_COMMA and VECTOR_SPACE (although the names are bad!). This prevents mixed separators, like [7, 8 9]

  3. Acc. to your suggestion, I have made changes to allow trailing separators, so things like "[1,2,3,]" will get parsed now!

@nstarman nstarman left a comment

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.

Thanks @kartavayv!

@nstarman
nstarman requested a review from mhvk February 14, 2026 23:35
@mhvk
mhvk force-pushed the units-quantity-parsing branch from 7080cef to d055e1b Compare February 25, 2026 18:06

@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.

@kartavayv - sorry for the very delayed review. There were some nits that bugged me, and I also thought your changes gave the opportunity to clean the lot up a bit. Anyway, I ended up just pushing a commit to your branch with my clean-up, hope that's OK (and squashed your commits together).

Since I now made changes, perhaps @nstarman can have a last look, approve and merge...

Comment thread astropy/units/tests/test_quantity.py Outdated
"[7 8 9]",
),
)
def test_quantity_initialisation_string_array_dimensionless(array):

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.

How about combing the two test functions by rewriting as

@pytest.mark.parametrize("array_str", 
   # what you have above
)
@pytest.mark.parametrize("unit_str", ["", "eV", "  cm"])
def test_quantity_initialisation_string_array(array_str, unit_str):
    input_str = array_str + unit_str
    q = u.Quantity(array)
    assert q.unit == unit_str
    assert_array_equal(q.value, np.array([7.0, 8.0, 9.0]))

Comment thread astropy/units/quantity.py Outdated
VECTOR_COMMA: Final = rf"""
\[\s*
{NUM}
(?:

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 put this part on one line -- makes it more readable for me at least.

Comment thread astropy/units/quantity.py Outdated
VECTOR_WSPACE: Final = rf"""
\[\s*
{NUM}
(?:

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.

Same here.

Comment thread astropy/units/quantity.py Outdated
unit_string = None
try:
value = float(v.group())
value_str = v.group()

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, still feels like it could just call the one function, including doing the string...

@mhvk

mhvk commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

Just to be clear, my last commit implements all my comments...

Comment thread astropy/units/quantity.py
Comment on lines +503 to +504
if "[" not in value:
msg += " It does not start with a number."

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.

Suggested change
if "[" not in value:
msg += " It does not start with a number."
if "[" not in value:
msg += " It does not start with an array."

?

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.

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 wanted to keep the original message for the original case, of just a number, but didn't see much point in elaborating for the case where an array was passed in... I could have an else clause along the lines of "Failed to parse the list of values."

But if I'd go back in time, I would probably not have had the "It does not start with a number" either...

@nstarman nstarman left a comment

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.

Thanks @kartavayv and @mhvk for this nice addition to Quantity's string parsing!

@nstarman
nstarman merged commit 3255f4b into astropy:main Feb 26, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants