Writing your own action

Finesse3 provides multiple Actions to interact with a model. But maybe you want to do something no one else has thought of. In this case you can add your own Action to finesse. That way it can be easily used in any model, shared with other users and maybe even added to finesse itself.

All actions start with finesse.analysis.actions.Action. It is a metaclass all Actions should inherit from. And it tells us exactly what we need to implement for our Action to work: We need to implement the functions left as abstract in Action: _requests and _do.

The first step is to create a new class

from finesse.analysis.action import Action

class MyAction(Action):
        def __init__(self, some_argument, name="myaction"):
                super().__init__(name)
                # do some other initialsation
                self.arg = some_argument

_requests informs finesse about what parameters your action needs to be included in the simulation. The docstring tells us the details:

def _requests(self, model, memo, first=True):
        """
        Updates the memo dictionary with details about what this action needs from a
        simulation to run. Parent actions will get requests from all its child actions
        so that it can build a model that suits all of them, to minimise the amount of
        building.

        This method can do initial checks to make sure the model has the
        required features to perform the action too.

        memo['changing_parameters'] - append to this list the full name string
                                      of parameters that this action needs
        memo['keep_nodes'] - append to this list the full name string
                                    of nodes that this action needs to keep.
                                    This should be used where actions are
                                    accessing node outputs without using a
                                    detector element (which registers that
                                    nodes should be kept already).

        Parameters
        ----------
        model : Model
            The Model that the action will be operating on
        memo : defaultdict(list)
            A dictionary that should be filled with requests
        first : boolean
            True if this is the first request being made
        """

So we get the model object, a list to write our parameters to and a flag whether we’re the first action. If you want to add a parameter p to memo the syntax is

memo["changing_parameter"].extend(p)

If your action calls other actions you need to call their _requests as well. This is also a good point to check whether your action can actually be run on the model. If it is in some bad state its better to raise an Exception here than wait for things to go wrong during the simulation. This can save a lot of time while debugging your simulation.

The second function you need to include is _do. This is where your action actually does something. It gets called with one argument: AnalysisState. It includes a lot of information about the simulation for you to use, look it up in the documentation.

_do needs to return some kind of Solution, that is an object that inherits from BaseSolution. You can create your own solution class tailored to your action, but you can also use finesse.solutions.SimpleSolution:

from finesse.solutions import SimpleSolution

def _do(self, state):
        sol = SimpleSolution(self.name)
        # calculate something
        result = self._some_func(state.model)
        sol.children.append(result)
        return sol

A class defined like this can just be used in the python API with finesse.model.run or inside other actions like Series. They behave like any standard action.

But maybe you also want to call your action from KatScript. This is easily doable:

from finesse.script.spec import make_analysis, KATSPEC
adapter = make_analysis(MyAction, "myaction")
KATSPEC.register_analysis(adapter)

In the first step a KatScript adapter is created from the Action, this is what can create the action from a KatScript string. The second line then adds this adapter to the default KatScript specification. Note that by default no command can be overwritten, if the name already exists trying to register it will throw an exception. If you do want to overwrite a command, use overwrite=True

There are limitations to this: for an action to work in KatScript, it must be possible to specify its arguments in KatScript. This is possible for model parameters, integers, lists and several other datatypes, but of course not for every possible one: Everyone can create their own type and even some built-in types cannot be written in KatScript. This is either because they just are not implemented or because their usual syntax would collide with KatScript syntax. This is one of the reasons some actions only exist in the python API but not in the KatScript.

Example

Lets create a simple example action. It will just print a message when run.

from finesse.analysis.actions import Action
from finesse.solutions import SimpleSolution

class MessageAction(Action):
    def __init__(self, message=None, name="messageaction"):
        super().__init__(name)
        self._message = message
    
    def _requests(self, model, memo, first=True):
        pass
    
    def _do(self, state):
        print(f"{state.model} says {self._message}")
        return SimpleSolution(self.name)

Now we can run

import finesse

model = finesse.Model()
model.parse(
    """
    laser l1 P=0
    """
)
sol = model.run(MessageAction("Hello World!))

which prints the following line to stdout. Note that sol contains no useful data.

<finesse.model.Model object at 0x7f9c78877460> says Hello World!

To add it to KatScript:

from finesse.script.spec import make_analysis, KATSPEC

KATSPEC.register_analysis(make_analysis(MessageAction, "messageaction"))

which allows

model = finesse.Model()
model.parse(
    """
    laser l1 P=0
    messageaction("Hello World!")
    """
)
sol = m.run()

producing the same result as before.

To learn more about how actions work it can help to look at the source of the existing actions. Good starting points are Sweep and random. The first is the basic action for scanning a parameter and is a good example for writing _requests. The second contains several minimalistic actions that are easier to understand than Sweep, which does most of its actual action in a cpython function for performance reasons.

Leave a Reply