Plasmapy: Create a @plasma_input decorator?

Created on 14 May 2020  路  6Comments  路  Source: PlasmaPy/PlasmaPy

Many of the functions in the formulary depend on fairly standard quantities like magnetic field strength, number density, and temperature. I was wondering about the possibility of having a @plasma_input decorator that would transform the inputs into a kind of Plasma object. The operations in the formulary could then make use of plasma.magnetic_field, plasma.number_density, and plasma.temperature.

Here's a use case for why I'm thinking this would be worthwhile. Suppose we define number density and magnetic field as functions of space and/or time, each of which return a quantity in the appropriate units. It would be useful to be able to pass these two functions to Alfven_velocity, and then get a function back that returns the Alfv茅n velocity as a function of space and/or time. To do this, it may be helpful for there to be a class within astropy.units that would be like Quantity, except that the value could be a function instead of a constant or array. (I don't think it's FunctionQuantity, which appears to have the unit be a function rather than the value.)

It would also be useful to be able to do things like Alfven_velocity(plasma) after we create a plasma object, which would reduce the need for inputting all of the different plasma parameters. (Something similar could also be done by having Alfven_velocity be a method or attribute on a plasma object.)

I don't know yet how or if we would want to implement this, so we'd need further discussions about this in order to reach consensus. We would also want to figure out how this decorator would relate to other decorators like @particle_input, @validate_quantities, and @check_relativistic.

Formulary Particles

Most helpful comment

It's a neat idea! My biggest concern with this approach, it forces the user to do it our way and only our way. That is, they have to create a plasma object and then pass it to the function as Alfven_speed(plasma). While convenient to some, I think it would turn-off a lot of other users. This method of calling a formulary function (Alfven_speed(plasma)) should be up to the user's discretion and the current method should still be possible and documented as such. With that said, these are the options I see

Option 1

1.A

Have the formulary functions be callable on the plasma object (i.e. plasma.Alfven_speed). This makes a lot off sense to me and it's kind of how I'd expect the plasma object to work. I agree this could cause a lot off overhead for the class. Some overhead could be avoided by importing the functions within the method, thus, the function is only imported when the user calls the method. However, that means we still have manage all the formulary functions on the Plasma class. Yikes!!!!!

1.B

We could have a PlasmaFormulary class that would look something like...

class PlasmaFormualry:
    def __init__(self, *args, **kwargs):
        # *args and **kwargs would be the arguments eventually passed to the 
        # formulary functions

class Plasma:
    @property
    def formulary(self):
        # see option 2 below for explanation of self.pform_inputs
        return PlasmaFormulary(**self.pform_inputs)

This does does not necessary prevent us from having to manage all the formulary functions, it just offloads that to PlasmaFormulary. What would be really cool, if would could figure out how to dynamically call and import functions based on runtime method calls. That is, if plasma.formulary.Alfven_speed is called then PlasmaFormulary searches plasmapy.formulary for Alfven_speed. If found, then the function is imported and called. If not found, then the typical AttributeError is raised. This would reduce a lot of overhead on the PlasmaFormulary class and ease our formulary managing burdens, but hinders discoverability for users. We would also need to be really good about maintaining the `plasmapy.formulary`` namespace.

Option 2

Create a property on Plasma that gathers all the class attributes and builds a dictionary suitable for passing to a formulary function. For example,

class Plasma:
    @property
    def pform_inputs(self):
        # gather all the plasma attributes and build a formulary
        # friendly argument dictionary
        return pform_inputs

>>> plasma = Plasma()
>>> VA = Alfven_speed(**plasma.pform_inputs)

Option 3

This is kinda like option 2 but offloading the argument collection to a @plasma_input. For example, (a rough sketch of @plasma_input)

def plasma_input(f):
    @preserve_signature
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        # examine *args and **kwargs to see if and only if a plasma object is passed in

        # if not, then continue as normal
        return wrapper(*args, **kwags)

        # if yes, then build an input dictionary
        #    1. inspect function signature for expected arguments
        #    2. search the plasma object attributes for those arguments
        #    3. construct pform_inputs dictionary
        return wrapper(**pform_inputs)


    return wrapper

I see several hurdles with this approach: (1) making sure Plasma has attributes with same name as the formulary inputs, (2) maintaining consistency with argument names for the formulary, (3) updating when new formulary arguments are introduced, and (4) working with any nested decorators (especially @validate_quantities.


Opinion

I think option 2 would be the easiest to implement and manage. Options 1 and 2 are intriguing, but will require a lot of work and continued management.

All 6 comments

Sounds neat, but it's gonna be tough writing docstrings for this in a consistent way.

It should make more sense to just have methods. Syntactically, alfven_speed(plasma) is much the same as plasma.alfven_speed() - the latter can call the former with old arguments.

The pass-functions-of-time problem sounds interesting and I'll have to think about it some more. I think instead of using functions, as in callables, we could instead go for sympy integration... Do all the work on symbols, then lambdify the ultimate result. It would be less magical, plugging into an existing ecosystem that's well understood instead of introducing a custom system with unknown corner cases in there.

Those are all good points. We're going to have the balance the advantages and disadvantages of the different approaches.

A hesitation that I have about putting methods directly in Plasma objects is that it could lead to Plasma objects having a bloated namespace. We could also end up with the Plasma class being rather monolithic. I'm also trying to think in terms of separation of responsibilities. It would be helpful to have a class whose single responsibility is to store and process the fundamental parameters describing a plasma. My current thinking is that a @plasma_input decorator that constructs a Plasma object from the arguments would make the implementation cleaner, analogous to @particle_input letting us pass in a Particle object. In any case, I'd like to keep brainstorming how this might work. :cloud_with_lightning:

I think I should have asked the question about an equivalent to astropy.units.Quantity for functions as an Astropy issue or perhaps even in our Discourse group. My original post may have violated the Single Responsibility Principle by bringing up too many topics for discussion!

It's a neat idea! My biggest concern with this approach, it forces the user to do it our way and only our way. That is, they have to create a plasma object and then pass it to the function as Alfven_speed(plasma). While convenient to some, I think it would turn-off a lot of other users. This method of calling a formulary function (Alfven_speed(plasma)) should be up to the user's discretion and the current method should still be possible and documented as such. With that said, these are the options I see

Option 1

1.A

Have the formulary functions be callable on the plasma object (i.e. plasma.Alfven_speed). This makes a lot off sense to me and it's kind of how I'd expect the plasma object to work. I agree this could cause a lot off overhead for the class. Some overhead could be avoided by importing the functions within the method, thus, the function is only imported when the user calls the method. However, that means we still have manage all the formulary functions on the Plasma class. Yikes!!!!!

1.B

We could have a PlasmaFormulary class that would look something like...

class PlasmaFormualry:
    def __init__(self, *args, **kwargs):
        # *args and **kwargs would be the arguments eventually passed to the 
        # formulary functions

class Plasma:
    @property
    def formulary(self):
        # see option 2 below for explanation of self.pform_inputs
        return PlasmaFormulary(**self.pform_inputs)

This does does not necessary prevent us from having to manage all the formulary functions, it just offloads that to PlasmaFormulary. What would be really cool, if would could figure out how to dynamically call and import functions based on runtime method calls. That is, if plasma.formulary.Alfven_speed is called then PlasmaFormulary searches plasmapy.formulary for Alfven_speed. If found, then the function is imported and called. If not found, then the typical AttributeError is raised. This would reduce a lot of overhead on the PlasmaFormulary class and ease our formulary managing burdens, but hinders discoverability for users. We would also need to be really good about maintaining the `plasmapy.formulary`` namespace.

Option 2

Create a property on Plasma that gathers all the class attributes and builds a dictionary suitable for passing to a formulary function. For example,

class Plasma:
    @property
    def pform_inputs(self):
        # gather all the plasma attributes and build a formulary
        # friendly argument dictionary
        return pform_inputs

>>> plasma = Plasma()
>>> VA = Alfven_speed(**plasma.pform_inputs)

Option 3

This is kinda like option 2 but offloading the argument collection to a @plasma_input. For example, (a rough sketch of @plasma_input)

def plasma_input(f):
    @preserve_signature
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        # examine *args and **kwargs to see if and only if a plasma object is passed in

        # if not, then continue as normal
        return wrapper(*args, **kwags)

        # if yes, then build an input dictionary
        #    1. inspect function signature for expected arguments
        #    2. search the plasma object attributes for those arguments
        #    3. construct pform_inputs dictionary
        return wrapper(**pform_inputs)


    return wrapper

I see several hurdles with this approach: (1) making sure Plasma has attributes with same name as the formulary inputs, (2) maintaining consistency with argument names for the formulary, (3) updating when new formulary arguments are introduced, and (4) working with any nested decorators (especially @validate_quantities.


Opinion

I think option 2 would be the easiest to implement and manage. Options 1 and 2 are intriguing, but will require a lot of work and continued management.

Thank you for the extremely well-thought out response! I found myself learning as I was reading it. I have one clarification to the following statement while I'm still digesting the different options you provided.

My biggest concern with this approach, it forces the user to do it our way and only our way. That is, they have to create a plasma object and then pass it to the function as Alfven_speed(plasma).

My intention with @plasma_input is to allow users to choose between either of the following two approaches:

>>> plasma = AppropriatePlasmaClass(B=5*u.T, n=1e19*u.m**-3, ion="p+")
>>> Alfven_speed(plasma)

or

>>> Alfven_speed(5 * u.T, 1e19 * u .m ** -3, "p+")

This is similar to @particle_input, in that a user could use a Particle object, but doesn't have to. They can do electric_charge("p+"), or proton = Particle("p+") and then electric_charge(proton) . For @plasma_input, I'm thinking that the decorator could take the multiple inputs that correspond to a plasma, and transform them into the appropriate Plasma object.

Of course, I didn't mention implementation at all, since thinking about how to implement decorators makes my brain hurt. :exploding_head:

Then I think we're on the same page with the interface. The only difference I see, you're suggesting @particle_input will transform arguments into a plasma object, whereas, I see it transforming a plasma object into function arguments. I prefer the later since I think (1) it keeps formulary functions more simplified (less overhead), (2) will be easier for new contributors to write new formulary functions, and (3) I can't remember my 3rd point, will edit if I recall.

The "rough" decorator example I suggested, does do the interface in the way I prefer here.

We could also do a combination of option 1.B and option 3, where the PlasmaFormulary class gives quick access to the most common formulary functions, but you could still use the plasma object directly with any of the formulary functions.

Was this page helpful?
0 / 5 - 0 ratings