Scope of this guide
This is intended for people who are interested in developing new, functional PVs for simulacrum. Reading this article will help you implement the causation and logic that drives PV interactions within a simulacrum service.
Background
In the last article, Simulating PVs 1: caget and caput, we outlined how we can use caget and caput commands to read and change the values of writeable PVs. We also introduced the concept of read-only PVs. Now that we have clarified the interactions between PVs and the user, we can discuss PVs' interactions with each other, as well as PVs that can interact with the accelerator model. These interactions are accomplished (in large part) by putter functions. We introduced putter functions in the last article as well.
We can break this down into the following diagram. Blue and red arrows represent caget and caput commands. Pink and orange arrows handle data flow within a service, as well as data to/from the accelerator model. Putter functions make up a large part of the pink and orange arrows.
Fig. 1: A diagram of a simulation involving a single service. (The magnet service, for the sake of example).
Instructions
Just like last chapter, we are going to edit the B-value of a quadrupole. However, we don't just want to update the "desired" value, Bdes. We want to prompt an update to the physics of the quadrupole that reflects our new B value. For the user, the process will be practically the same. We will send a caput to a PV, then read back the result to verify the change. However, we will be reading and writing with much more robust PV putter functions. By looking at these putter functions, you can start to design your own.
Open up the magnet_service.py on your preferred text editor and look for the instances where bact, bctrl, and ctrl are defined as PVs.
Fig. 2: Lines 21-34 of magnet_service.py.
As usual, two of these PVs are defined in the very first class. Bact, or "actual" B is analogous to the accelerator's model's value for B. Ctrl exists for the operator to manipulate the B-value. The only unusual aspect is that bctrl is not defined upfront, due to "some hacky stuff" that seems to be a consequence of the way magnet data are handled. It is defined asynchronously, which is also how we handle our putter functions. We will ignore this fact to focus on the content of the putter functions.
Fig. 3: Lines 130-137 of magnet_service.py. You don't need to worry about implementing PVs this way, this is an exception to the standard.
Lets look at our putter functions side-by-side and see how they are different.
Fig. 4: A line-by-line explanation of what's going on in these putter functions!
The logic behind these putter commands constitutes the backbone of a lot of simulacrum functionality. By understanding how to interpret these putters, we can glean how the user is supposed to operate changes to the B-value. Without any outside knowledge of the operating system or quadrupoles, we can retrace the logic of these putter functions by drawing arrows that show the life cycle of a single caput from the user.
Fig. 5: The flow of data from an input command to the final change_callback function. The numbered arrows show the chronology of updates to PVs.
You can glean a lot from reading existing code closely. It may seem odd to have such a complicated process for updating a single value, but this sort of infrastructure is useful in any situation where PVs are codependent. The big logistical decisions come down to when to ".publish" (step 4) or when to ".write" (step 1, 2, 3) when updating the value of another pv. Put simply, ".publish" allows you to update a pv in isolation of any other functions, whereas ".write" will update the desired pv, and run the putter function for that pv. Lets build our own example to understand how certain pvs can cause a chain-reaction and some pvs are dead ends or placeholders.
Let's say we have the following PVs:
Fig. 6: Some example PVs that have a codependent relationship.
I know right away that I need to implement some putters to account for their mathematical relationship. If my simulation changes pv1 or pv2, then they should update each other automatically. The putter functions might look something like this:
Fig. 7: Putter functions based off of the PVs in fig. 6.
I have used the publish command to update these codependent values because publish enables me to update the other pv without triggering its putter function. I don't want an infinite loop where the putter functions call each other back and forth for eternity. However, if one of these pvs had another functionality attached to it, I might change one of these publish commands to something else.
Let's say that pv1 triggers an update to the accelerator model, by way of the change_callback function. We wrote change_callback into pv1's putter function so that changes to pv1 can trigger an update to the model.
With our previous arrangement, both putter functions were publishing to each other. If I update pv2, only pv2's putter will run. That's a problem if one putter function has more functionality than the other.
Fig. 8: pv1 and pv2. Despite pv1 being fully functional, pv2 is not connected to the accelerator model.
If I caput to pv1 in figure 8, everything will function correctly. However, if I caput to pv2, then the model service will never receive an update for pv1. Pv1 may change as a result of pv2, but pv2 has not triggered an update to our model. Instead of writing change_callback into every single putter function, we can make putter functions call each other. As we saw earlier, ".publish" isn't the only option for updating another pv. Which ".publish" do we need to change to a ".write" for pv1 and pv2 to be functional?
Fig. 9: Which putter uses .publish? Which putter uses .write?
The correct answer is...
Option 1!
We want pv2 to write to pv1 because pv1's putter will allow us to update the accelerator model. Here's the flow of data for pv1 and pv2! See how both pvs are fully functional since updating either pv will result in a change to the accelerator model.
Fig. 10: The data flow for two asymmetrical putter functions.
I did not describe using ".write" for both putter functions because we would get an infinite loop if both putter functions called each other.
This example is pretty much all you need to know when writing your own putter functions. Any other differences you see across the simulacrum library arise from other data formats (PVs can commonly be booleans or enumerated lists, not just float numbers) or the fact that the relationships between PVs can include more complicated conditional statements (see fig. 4!).
If you're struggling with making a pv functional, I highly recommend writing a putter function to help you keep track of how the data flow breaks down. Figs. 4, 5, and 10 are all ways to visualize the flow of data between pvs. While no longer a very accurate reference, Fig. 11 shows the working draft I used to develop PVs for the sc_rf service (aka the cavity service) from scratch. Once I had a diagram of the logic, I could implement the putter commands to execute it. Click and zoom in for higher quality.
Fig. 11: The author's outline for pvs of an actual service, the sc_rf service, in the style of fig. 10.
This diagram may not explain the sc_rf service on its own (the actual sc_rf service has some differences in its pvs), but it may help you visualize the infrastructure for writing your own simulacrum service. Alternatively, you can use what you know about putter commands to read and interpret existing simulacrum code, such as the magnet service!
To check your understanding, try opening the sc_rf service on github (https://github.com/slaclab/simulacrum/tree/master/sc_rf_service) and see if you can follow along with those putter functions! Make a diagram of the data flow, and check your work against the one I drew in the article Dissecting a Simulacrum Service