Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add separate constructor for CustomParticle and improve |ParticleList| compatibility with |Quantity| objects #1874

Closed
wants to merge 24 commits into from

Conversation

namurphy
Copy link
Member

@namurphy namurphy commented Jan 6, 2023

This PR makes the following changes:

The changes to CustomParticle made the changes to ParticleList pretty straightforward to implement, so I addressed them here.

This PR should make it easier to address #1869 and #1871.

@namurphy namurphy added Breaking plasmapy.particles Related to the plasmapy.particles subpackage labels Jan 6, 2023
@github-actions
Copy link

github-actions bot commented Jan 6, 2023

Thank you for contributing to PlasmaPy! The project's future depends deeply on contributors like you, so we deeply appreciate it! 🌱 The following checklist will be used by the code reviewer to help guide the code review process.

  • Overall
    • Does the PR do what it intends to do?
    • Except for very minor changes, is a changelog entry included and consistent with the changelog guide?
    • Are the continuous integration checks passing? (Most linter problems can be automagically fixed by commenting on this PR with pre-commit.ci autofix.)
  • Code
    • Is new/updated code understandable and consistent with the coding guide?
    • Are there ways to greatly simplify the implementation?
    • Are there any large functions that should be split up into shorter functions?
  • Tests
    • Are tests added/updated as required, and consistent with the testing guide?
    • Are the tests understandable?
    • Do the tests cover all important cases?
  • Docs
    • Are docs added/updated as required, and consistent with the doc guide?
    • Are the docs understandable?
    • Do the docs show up correctly in the preview, including Jupyter notebooks?

@namurphy
Copy link
Member Author

namurphy commented Jan 6, 2023

This PR is still in its early stages, and I'm still thinking about the implementation details.

I'm going to try to reduce breaking changes, but it's likely this would make symbol keyword-only which would be a breaking changes. I'm also wondering if the mass and charge parameters should be deprecated.

@codecov
Copy link

codecov bot commented Jan 8, 2023

Codecov Report

Base: 98.31% // Head: 98.31% // Decreases project coverage by -0.00% ⚠️

Coverage data is based on head (307ee75) compared to base (83375f8).
Patch coverage: 100.00% of modified lines in pull request are covered.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1874      +/-   ##
==========================================
- Coverage   98.31%   98.31%   -0.01%     
==========================================
  Files          95       95              
  Lines        8383     8424      +41     
==========================================
+ Hits         8242     8282      +40     
- Misses        141      142       +1     
Impacted Files Coverage Δ
plasmapy/particles/_factory.py 100.00% <100.00%> (ø)
plasmapy/particles/particle_class.py 98.56% <100.00%> (+0.04%) ⬆️
plasmapy/particles/particle_collections.py 100.00% <100.00%> (ø)
plasmapy/utils/_units_helpers.py 96.00% <100.00%> (-4.00%) ⬇️

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report at Codecov.
📢 Do you have feedback about the report comment? Let us know in this issue.

@namurphy namurphy removed the Breaking label Jan 8, 2023
@namurphy namurphy changed the title Change signature of CustomParticle Change signature of CustomParticle and improve |ParticleList| compatibility with |Quantity| objects Jan 9, 2023
@namurphy namurphy marked this pull request as ready for review January 9, 2023 03:12
@namurphy namurphy requested a review from a team as a code owner January 9, 2023 03:12
@namurphy namurphy removed the request for review from a team January 9, 2023 03:12
Copy link
Member

@StanczakDominik StanczakDominik left a comment

Choose a reason for hiding this comment

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

Could use some cleaning up imo!

Comment on lines +47 to +48
5 * u.kg,
5 * u.C,
Copy link
Member

Choose a reason for hiding this comment

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

So these two will create, respectively, a charge-less 5kg chonkyboi of a particle and a massless one with a massive 5 C charge, both custom ones, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep!

Comment on lines 101 to 110
for particle_type in (Particle, CustomParticle, ParticleList):
with contextlib.suppress(TypeError, InvalidParticleError):
with contextlib.suppress(TypeError, InvalidParticleError, ValueError):

# Do not interpret a string as the first positional argument
# as the symbol for a CustomParticle or something to be
# iterated over for a ParticleList.
if particle_type is not Particle and args and isinstance(args[0], str):
continue

return particle_type(*args, **kwargs)
Copy link
Member

Choose a reason for hiding this comment

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

Might be myself being tired but this whole block seems difficult to reason about! This needs a refactor:

  • let's enter suppress context once instead of within the for loop
  • the case for Particle should probably be separate

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, this block is a bit hard to wrap one's mind around.

The reason why contextlib.suppress is used within the for block is that it tries to create a Particle, CustomParticle, and ParticleList sequentially. There's a chance for one of these exceptions to be raised each time. If the suppress context were outside the for loop, then it would exit the context environment upon the first exception...I think. If it failed to create a Particle, then it wouldn't try to create a CustomParticle or ParticleList afterwards.

I'll keep thinking about how to clean this up and reduce the cognitive complexity.

Comment on lines +2020 to +2040
*quantities : tuple of ~astropy.units.Quantity or `str`
The mass, electrical charge, and/or symbol of the custom
particle, provided as positional arguments. The mass and charge
may be provided in either order, but the symbol may only be the
final positional argument.

charge : ~astropy.units.Quantity or ~numbers.Real, optional
The electric charge of the custom particle. If provided as a
`~astropy.units.Quantity`, then it must be in units of electric
charge. Defaults to |nan| C.
charge : ~astropy.units.Quantity or ~numbers.Real, |keyword-only|, optional
The electrical charge of the custom particle, if not provided in
``quantities`` as a positional argument.

Z : ~numbers.Real, optional, |keyword-only|
The :term:`charge number`, which is equal to the ratio of the
charge to the elementary charge.
mass : ~astropy.units.Quantity, |keyword-only|, |keyword-only|, optional
The mass of the custom particle, if not provided in
``quantities`` as a positional argument.

symbol : str, |keyword-only|, |keyword-only|, optional
The symbol to be assigned to the custom particle, if not
provided as the final positional argument.

symbol : str, optional
The symbol to be assigned to the custom particle.
Z : ~numbers.Real, optional, |keyword-only|
The |charge number|, which is equal to the ratio of the charge
to the elementary charge.
Copy link
Member

Choose a reason for hiding this comment

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

I kind of dislike how this whole change actually made the user-facing complexity, as measured by the length of docstring necessary, higher! Nothing to change here, probably, just voicing my concern.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed — the downside of this change is the increased user-facing complexity. My original plan was to deprecate the mass and charge keyword arguments, which I haven't done but am still considering. The extra complexity comes from attempting to preserve backward compatibility where mass and charge can be used as keyword arguments too.

Comment on lines 2088 to 2124
if isinstance(quantities[-1], str) and symbol is None:
symbol = quantities[-1]
quantities = list(quantities[:-1])

try:
physical_type_dict = _get_physical_type_dict(
quantities,
only_quantities=True,
strict=True,
allowed_physical_types={
u.physical.mass,
u.physical.electrical_charge,
},
)
except (TypeError, ValueError) as exc:
raise InvalidParticleError(
"Unable to create a CustomParticle based off of the "
"following positional arguments, which should be a"
f"Quantity representing electrical charge and/or a"
f"Quantity representing mass: {quantities}."
) from exc

if u.physical.electrical_charge in physical_type_dict:
if charge is not None:
raise TypeError(
"Cannot provide charge as both a positional and keyword "
"argument."
)
charge = physical_type_dict[u.physical.electrical_charge]

if u.physical.mass in physical_type_dict:
if mass is not None:
raise TypeError(
"Cannot provide mass as both a positional and keyword argument."
)
mass = physical_type_dict[u.physical.mass]

Copy link
Member

Choose a reason for hiding this comment

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

Since this whole block is pretty separate from other functionality, how about moving it to a separate semi-private constructor like this...

@classmethod
def _from_positional(cls, args):
    ... # this whole block

and then calling that from here if quantities is non-empty?

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed. This would be cleaner by extracting a bunch of this into its own method.

@namurphy namurphy added this to the 2023.1.0 milestone Jan 9, 2023
@namurphy
Copy link
Member Author

namurphy commented Jan 9, 2023

My main question at this point is: would it be better to deprecate the mass and charge keyword arguments, and make the mass and charge positional-only? Right now, this is looking like the optimal approach to reduce complexity not only in CustomParticle.__init__ and its docstring, but also in ParticleList and its attributes, particle_input, and the particle factory function.

Comment on lines +103 to 113
try:
return Particle(*args, **kwargs)
except _particle_creation_exceptions:
# If the first argument is a string but is not a valid particle,
# then don't try to create a CustomParticle or ParticleList.
if args and isinstance(args[0], str):
raise

for particle_type in (CustomParticle, ParticleList):
with contextlib.suppress(*_particle_creation_exceptions):
return particle_type(*args, **kwargs)
Copy link
Member Author

Choose a reason for hiding this comment

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

Hm...it's looking like I'll need to restructure this part again in #1867 to enable @particle_input to take values for Z that are real but not integers.

@StanczakDominik
Copy link
Member

Can we instead hold off on this until we've discussed #1873 in more detail? I'm writing up some thoughts on it right now, and we do have a telecon today, right?

@namurphy namurphy changed the title Change signature of CustomParticle and improve |ParticleList| compatibility with |Quantity| objects Add separate constructor for CustomParticle and improve |ParticleList| compatibility with |Quantity| objects Jan 10, 2023
@namurphy
Copy link
Member Author

The approach suggested by @StanczakDominik at today's telecon is to implement a pattern like:

class CustomParticle:
    def __init__(self, mass: u.kg, charge: u.C):
        # current broad-strokes signature of CustomParticle creator
        pass

    @classmethod
    def _from_positionals(cls, *args):
        # use this from @particleinput
        ... # figure out physical type of argument -> create a dict -> initialize particle
        d = dict(mass = mass_value_kg, charge = charge_value_C)
        return cls(**d)

I think I might try this in a separate PR...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
plasmapy.particles Related to the plasmapy.particles subpackage
Projects
None yet
2 participants