12.1. Action Types and Action Steps
An action type (definition) is a kind of KerML Behavior that classifies actions (or action performances). An action step ("usage") is a kind of KerML Step (a behavioral feature). Action definitions and action steps can be composed of subaction steps. Every action type inherits the features start and done from Actions::Action, which represent the start and end events (or snapshots) of an action.
In SysML2, action definitions can represent both high-level behaviors, such as business activities, and low-level behaviors, such as computational actions like value assignments or conditional branchings. Actions may be atomic or composite, and they may have some duration or may be instantaneous.
In the following example, an action type TakePicture is defined with the two action steps focus and shoot:
action def TakePicture { action focus[1] : Focus; action shoot[1] : Shoot; }
Notice that both action steps have the multiplicity 1, which means that a TakePicture action consists of exactly one focus and one shoot action.
The following example defines a part type Camera with an attribute and the two action steps focus and shoot, both of which have the multiplicity * indicating that a camera may perform actions of these types zero-or-many times during its lifetime:
part def Camera { attribute model : String; action focus[*] : Focus; action shoot[*] : Shoot; }
An action definition may declare properties with directions in, out or inout that act as the parameters of corresponding action expressions and are called its owned parameters. If an action step has parameters, then it may also transform the values of its input parameters into values of its output parameters.
action def Focus { in scene: Scene; out image: Image; }
An action can generate effects on involved objects (items or parts), including their existence and relationships to other things.
Subaction steps, such as focus and shoot, can be sequenced with the help of successions, which express temporal precedence relations:
action def TakePicture { action focus[1] : Focus; succession [1] focus then [1] shoot; action shoot[1] : Shoot; }
The succession declaration states that for any TakePicture action, each focus subaction is succeeded by exactly one shoot subaction, and each shoot subaction is preceded by exactly one focus subaction.
The sequencing of subaction steps can be coordinated with the help of fork/join and decision/merge control nodes, which are viewed as special types of action steps in SysML2.
An output parameter of an action step may be bound to an input parameter of a follow-up action step with the help of a binding. An input parameter of an action definition or step can also be bound to the input parameter of a nested action step, passing the values of the input parameter into the nested action, and an output parameter of a nested action step can be bound to an output parameter of a containing action definition or step, passing the values of the output parameter out. This is illustrated in the following example:
action def Focus { in scene; out image; } action def Shoot { in image; out picture; } action def TakePicture { in scene : Scene; out picture : Picture; binding focus.scene = scene; action focus[1] : Focus; succession [1] focus then [1] shoot; binding shoot.image = focus.image; action shoot[1] : Shoot; binding picture = shoot.picture; }
The binding of action parameters, however, does not model the case when there is an actual transfer of items between actions that may itself take time or have other properties. Such a transfer can be more properly modeled using a flow between the two action steps, in which the transfer source output is an output parameter of the source action step and the transfer target input is the input parameter of the target action step. When we assume, in the example above, that the image produced by the focus action cannot be simply passed to the succeeding shoot action, but rather needs to be streamed, or transmitted, to it, then the binding of the focus.image to the shoot.image, as in
binding shoot.image = focus.image;
has to be replaced with a corresponding flow:
flow focus.image to shoot.image;
Transfers can also be performed using send and accept action steps. In this case, the source and target of the transfer do not have to be explicitly connected with a flow. Instead, the source of the transfer is specified using a send action step contained in some some source part or action type, while the target is given by an accept action step in some destination part or action type (which may be the same as, or different than, the source). A send action step includes an expression that is evaluated to provide the values to be transferred, and it specifies the destination to which those values are to be sent (possibly delegated through a port and across an interface).
Both part types and action types can have perform action steps. In the case of a perform action step of a part type, the referenced action is performed by the containing part (as its performer) during its lifetime. In the case of a perform action step of an action type, the perform action step represents a "call" from the containing action to the performed action.
Timing Constraints
Timing constraints can be expressed within an action type definition like so:
private import Time::TimeOf; action def A { first start; then action a1; then action a2; then done; assert constraint { TimeOf( that.done) - TimeOf( that.start) < 10[s] } }
This constraint requires that any action of type A must be completed within 10 s.
In the following example, the part type MyPart has an action property onStart, and the asserted constraint enforces that an onStart action starts as soon as a new MyPart is created:
private import Time::TimeOf; part def MyPart { action onStart[1] { ... } assert constraint { TimeOf( MyPart::start) == TimeOf( onStart) } }
Notice that the expression MyPart::start
designates the implicit start property of MyPart, evaluating to the start shot of the newly created MyPart, and the expression TimeOf( onStart)
measures the time of the onStart action's start shot.
The same timing can be achieved with the help of a special succession declaration:
part def MyPart { succession : Occurrences::HappensJustBefore first start then onStart; action onStart[1] { ... } }
Notice that the succession is not typed by the predefined association HappensBefore, but rather by the predefined association HappensJustBefore, which specializes HappensBefore.
12.1.1. Initiating an Action with Perform
A perform action property or step can be declared using just the keyword perform followed by a qualified name or feature chain identifying an action step and thus the type of actions (to be) performed. Such a property or step is always referential, representing a set of referenced actions that are initiated by the owner of the perform action property, but are actually carried out in the context of the owner of the referenced action property.
A perform action property or step allows actions that are performed in the same way like synchronous operation calls, as shown in the following example model of a client server system where the client "calls" the server's provideService operation with the perform action step perform server.provideService
. Notice that while the values of the server's provideService
action property are provideService actions, the values of the client's perform server.provideService
action step are references to these provideService actions.
A server receives requests (from a client) and produces a reply:
item def Request; item def Reply; part def Server1 { action provideService { in request : Request; out reply : Reply; ... assign reply := new Reply(); } }
The client that has a reference to the server and performs requestService actions on that server (in the style of operation calls), providing a request as an input and receiving the reply as an output:
part def Client1 { ref part server : Server1; ref item lastReply : Reply [0..1] := null; action makeRequest { perform server.provideService { in request = new Request(); out reply; } then assign lastReply := provideService.reply; } }
A system with a single client and a single server can be put together like this:
part def System1 { part server : Server1; part client : Client1; action system_behavior[1] { assign client.server := server; perform client.makeRequest; } }
12.1.2. Sequencing Action Steps with Successions
Two action steps can be sequenced with the help of a succession property, the concept of which is essentially copied from KerML as a special kind of KerML connector, the values of which are links representing temporal precedence relationships. Notice that succession links represent abstract relationships that do not exist in time and space, as opposed to connections, which are occurrences in time and space.
An output parameter of an action step may be bound to an input parameter of a succeeding action step for passing its value.
A succession value is a binary link that implies that the action of the first
step happens before the action of the then
step.
In the following example, an action type TakePicture is defined with the two action steps focus and shoot such that the succession with name controlFlow requires that for any TakePicture action, its focus action happens before its shoot action:
action def TakePicture { action focus[1] : Focus; action shoot[1] : Shoot; succession controlFlow first [1] focus then [1] shoot; }
Notice that in this example, both action steps, focus and shoot, as well as the source and target of the succession, also focus and shoot, have the multiplicity of 1. Consequently,
- a TakePicture action consists of exactly one focus action and exactly one shoot action, and
- any focus action is succeeded by exactly one shoot action and, vice versa, any shoot action is preceded by exactly one focus action.
This means that the only admissible sequence of actions is ( focus, shoot ).
A succession declaration does not imply that the then action immediately succeeds the first action, nor that the then action is the next action after the first action in any sequence of actions. Rather, since a succession does not represent the triggering of the follow-up action, but the much weaker relation of temporal precedence, the time in-between the two actions may be short or long, and there may be any number of other actions in-between the two.
The following example modifies the example above by allowing multiple focus actions preceding the shoot action:
action def TakePictureWithMultiFocusing { action focus[1..*] : Focus; action shoot[1] : Shoot; // a shoot action must be preceded by at least one focus action, and // a focus action must be succeeded by exactly one shoot action succession multiFocusing first [1..*] focus then [1] shoot; // a shoot action must not be succeeded by a focus action first [0] shoot then [0] focus; }
Due to the new multiplicity [1..*] of the action step focus, any TakePicture action now consists of one or many focus actions and exactly one shoot action. Examples of admissible sequences, as defined by the given succession multiplicities, are:
- ( focus, shoot )
- ( focus, focus, shoot )
- ( focus, focus, focus, shoot )
Examples of non-admissible sequences (consisting of at least one focus action and exactly one shoot action) are:
- ( shoot, focus )
- ( focus, shoot, focus )
Succession Shorthand Notations
Instead of writing
action action1; action action2; first action1 then action2;
we can also write
action action1; action action2; first action1; then action2;
The then shorthand can be used following any action step declaration, not just following a first declaration, with the preceding action step becoming the source of the succession, as in the following example:
action initialize; then action monitor; then action finalize;
The source of a succession must be an occurrence property (typically, an action step, or, in certain cases, an item property). Therefore, the source of a succession represented with the then shorthand notation is actually determined as the nearest occurrence property lexically preceding the then clause, skipping over any other intervening clauses. This allows several then successions to be placed in a sequence after a common source action step, which is particularly useful for specifying multiple successions outgoing from fork and decide nodes, as illustrated by the following fragment:
// Both successions following fork1 have fork1 as their source. fork fork1; then action1; then action2; action action1; then join1; action action2; then join1; join join1; // Both successions following decision1 have decision1 as their source. then decide decision1; then action3; then action4; action action3; then merge1; action action4; then merge1; merge merge1;
Notice that while the then shorthand notation allows specifying a target cross multiplicity (following the keyword then), it does not allow specifying a source cross multiplicity.
Conditional Succession
A conditional succession asserts the existence of the succession only if its condition (also called guard) is true.
succession conditionalOnActive first initialize if isActive then monitor;
A conditional succession actually declares a special transition property, which is a kind of action property typed by DecisionTransitionAction from the Actions model library (see 9.2.10).
Like for successions, the shorthand notation without a name can be used:
first initialize if isActive then monitor;
Also the then shorthand notation can be used:
action initialize; if isActive then monitor;
The keyword else may be used in place of a guard to indicate a succession to be taken if the guards evaluate to false on all of an immediately preceding set of conditional successions, as in the following example:
action initialize; if isActive then monitor; else beep;
12.1.3. Coordinating Action Steps with Control Nodes
Control nodes are used for expressing the conditional and parallel branching of the control flow of a composite action (or activity), for instance, in flow-charts such as BPMN process diagrams. In SysML2, they represent special types of action steps ("usages") within an action type definition.
- fork nodes
- have one incoming succession and one or more outgoing successions, such that the actions connected to the outgoing successions cannot start until the action connected to the incoming succession has completed. The concept is also called "AND-split" in the process modeling literature.
- join nodes
- have one or more incoming successions and one outgoing succession, such that the action connected to the outgoing succession cannot start until all the actions connected to the incoming successions have completed. The concept is also called "AND-join".
- decision nodes
- have one incoming succession and one or more outgoing successions, such that exactly one of the actions connected to an outgoing succession can start (controlled by placing guards on the outgoing successions) after the action connected to the incoming succession has completed. The concept is also called "OR-split".
- merge nodes
- have one or more incoming successions and one outgoing succession, such that the action connected to the outgoing succession cannot start until any one of the actions connected to an incoming succession has completed. The concept is also called "OR-join".
The following example describes the loading of a battery with the help of an implicit loop that is constructed with the control nodes Decide and Merge:
action def MonitorBattery {out batteryCharge : Real;} action def ChargeBattery { first start; then merge continueCharging; then action monitor : MonitorBattery; then decide; if monitor.batteryCharge < 100 then addCharge; else endCharging; action addCharge : AddCharge; then continueCharging; action endCharging : EndCharging; then done; }
This example is based on a corresponding example presented by Ed Seidewitz in his Introduction to the SysML v2 Language Textual Notation.
In a simulated "execution" (or instantiation) of the above action type ChargeBattery,
- all (temporally unconstrained) successions would imply a minimal delay for their succeeding occurrences,
- all computational actions (like start, merge and decide actions) would have a minimal duration,
- while all domain actions, such as MonitorBattery and AddCharge actions, would have some duration provided by, e.g., a fixed default value or a value sampled from a probability distribution function defined in an annotation of the corresponding action types.
The meaning of minimal delay and minimal duration depend on the time model chosen for the simulation. There are at least the following two options:
- Using continuous time with a time step granularity that defines the concept of the "next moment".
- Using a superdense time model where the timestamps of instantaneous occurrences (or snapshots) have the form (t,i) with t being the (real) time and i being a sequential index for linearizing simultaneous snapshots. This approach implies that the natural partial order of snapshots in (continuous) time, where simultaneous snapshots happen at the same time point t, is replaced by an artificial linear order of superdense timestamps, where simultaneous snapshots happen at subsequent timestamps (t,0), (t,1), (t,2), etc.
For instance, assuming a superdense time model and default durations of 1 s, resp. 2 s, for MonitorBattery and AddCharge actions, we may get traces for ChargeBattery actions like
start@(0,0), merge@(0,1), monitor-startshot@(0,2),
monitor-endshot@(1,0), decide@(1,1), addCharge-startshot@(1,2),
addCharge-endshot@(3,0), merge@(3,1), monitor-startshot@(3,2),
monitor-endshot@(4,0), decide@(4,1), endCharging@(4,2)
12.1.4. Computational Actions
The computational action steps of SysML2 correspond to program statements of structured (imperative) programming languages supporting variable value assignments, conditional branching and various types of looping.
Assignment Actions
An assignment action step is used to change the value of a referent property of a target occurrence. The target may be specified as the result of an expression and the referent is specified as a property chain relative to that target. The new value for the property is determined as the result of the assigned expression. When an assignment action completes, the referent property has the new assigned value for the target occurrence.
In the following example, the target of the assignment is the part referenced by "sim" and the referent feature chain is "vehicle.position":
action def UpdateVehiclePosition { in part sim : Simulation; in attribute deltaT : TimeDurationValue; assign sim.vehicle.position := sim.vehicle.position + sim.vehicle.velocity * deltaT; }
Conditional Branching
T.B.D.
Looping
T.B.D.
12.1.5. Sending and Receiving Signals
Parts, or actions, can send signals to receivers, which are typically other parts or actions, but may also be other types of occurrences. A signal, also called "payload", can be any kind of value.
Sending a signal is like a send message action, while receiving a signal is treated as an "accept action", which includes not only the case of receiving (and reacting to) a message, but also the case of reacting to change events ("change signals") and to time events ("time signals").
Send Actions
A SendAction transfers a payload from the sender to a receiver:
send payload via sender to receiver;
A send action step ("usage") is implicitly typed by SendAction from the Actions library model and is always owned by an action type (or step), unless it is the entry, do or exit action of a state type definition (or state property), or the effect action of a transition property. In the following example, the send action step is owned by the action step sendSensorReading:
part def monitor { action sendSensorReading { in part destination; perform getReading { out reading : SensorReading; } send getReading.reading via monitor to destination; } }
When a send action step is a (possibly indirect) feature of a part type, then the default for the sender is the containing part, allowing to simplify the above example in the following way:
part def monitor { action sendSensorReading { in part destination; perform getReading { out reading : SensorReading; } send getReading.reading to destination; } }
When sending a payload through a port, the port will usually be the sender, with the actual receiver determined by interface connections having the port property as their source. When sending via a port, the send action is allowed to also include an explicit receiver, but this must still be another port connected to the sending port by an interface.
part def MonitorDevice { port readingPort; action monitoring { perform getReading { out reading : SensorReading; } send getReading.reading via readingPort; } }
Accept Actions
An AcceptAction accepts the transfer of a payload received by the given receiver or it reacts to change signals or time signals. It has the following forms:
// receiving a signal/message accept payload via receiver; // reacting to a change signal accept when condition; // reacting to an absolute time signal accept at TimeInstantValue; // reacting to a relative time signal accept payload after DurationValue;
An accept action step ("usage") is implicitly typed by AcceptAction from the Actions library model and is always owned by an action type (or step), unless it is the entry, do or exit action of a state type definition (or state property), or the effect action or accept action of a transition property. In the following example, the accept action step is owned by the action step sendSensorReading:
part def controller { action receiveSensorReading { ... action acceptReading accept reading : SensorReading via controller; } }
The name of the action step ("acceptReading") and the name of the payload parameter ("reading") can be omitted:
part def controller { action receiveSensorReading { ... accept SensorReading via controller; } }
When an accept action step is a (possibly indirect) feature of a part type (or property), then the default for the receiver is the containing part, allowing to simplify the above example in the following way:
part def controller { action receiveSensorReading { ... accept SensorReading; } }
When an accept action step is a feature of a perform action step, then the default for the receiver is the perform action step.
Signals can also be received via ports:
part def ControllerDevice { port sensorPort; action control { accept reading : SensorReading via sensorPort; } }