top_level#
Overview#
This example is for creating a top-level operator that calls all existing root operators.
The interface of the top-level operator is the union of the model’s sensors and the inputs/outputs of the called operators.
The semantic parts of the example are quite simple. Most of the statements are related to the computation of positions and sizes. However, this is not so complex, and you can reuse this example as a foundation for additional scripts.
The input model has the following call graph:
The resulting top-level operator is as follows:
Import directives and main#
The main
function allows the script to be used by the wrapper script.
It saves the project and the model before returning it.:
from pathlib import Path
import scade.model.project.stdproject as std
import scade.model.suite as suite
import ansys.scade.apitools.create as create
def main():
"""Create a top-level operator which calls the existing root operators."""
# load the SCADE project and model
# note: the script shall be launched with a single project
project = std.get_roots()[0]
model = suite.get_roots()[0].model
# create the top-level operator instance
root = add_operator(project, model, 'Root')
# fill the operator: interfaces and equations
fill_top_level(model, root)
# save the created/modified files
create.save_project(project)
create.save_all()
if __name__ == "__main__":
# launched from scade -script
main()
Helper for operators#
The add_operator
utility function adds an operator to the model in a separate storage file.
This file has the same name and is located in the project’s directory:
def add_operator(project: std.Project, model: suite.Model, name: str) -> suite.Operator:
"""
Add a new operator to the model, and add its separate storage file in the project.
Parameters
----------
project : std.Project
Input project.
model : suite.Model
Input model.
name : str
Name of the operator.
Returns
-------
suite.Operator
"""
# store the operator in the project's directory
path = Path(project.pathname).with_name(name + '.xscade')
# create the operator in the model, assuming it is a node
operator = create.create_graphical_operator(model, name, path, state=True)
# add the separate file to the project
create.add_element_to_project(project, operator)
return operator
Top-level operator#
The fill_top_level
function is the main one of this example. It
creates the interface and the graphical equations of the new operator.
The algorithm caches the interface of the root operator in two dictionaries,
map_inputs
and map_outputs
.
The map_inputs
dictionary is initialized with all existing sensors and
then updated with the inputs of the called operators.
When two operators have an input with the same name, only one input is created in the root operator. Having outputs with same names is not acceptable, although this is not checked.
def fill_top_level(model: suite.Model, root: suite.Operator):
"""
Create the interface of the operator and complete the diagram.
Parameters
----------
project : std.Project
Input project.
model : suite.Model
Input model.
name : str
Name of the operator.
Returns
-------
suite.Operator
"""
# local cache: top-level inputs by name
map_inputs = {}
# local cache: top-level outputs by name
map_outputs = {}
# cache the sensors as inputs
for sensor in model.all_sensors:
map_inputs[sensor.name] = sensor
# gather the root operators of the model, ignoring the libraries
operators = [_ for _ in model.sub_operators if not _.expr_calls and _ != root]
# 1. interface
for operator in operators:
# create the inputs
for input in operator.inputs + operator.hiddens:
name = input.name
if map_inputs.get(input.name):
# ignore duplicated inputs or existing sensors
continue
new_inputs = create.add_operator_inputs(root, [(name, input.type)], None)
map_inputs[name] = new_inputs[0]
# create the outputs
for output in operator.outputs:
name = output.name
new_outputs = create.add_operator_outputs(root, [(name, output.type)], None)
map_outputs[name] = new_outputs[0]
# 2. common positions and sizes based on SCADE Editor's defaults for all calls
# operator
x_op = 5000
y_op = 1000
w_op = 1800
# input
h_in = 500
w_in = 260
# output
h_out = 500
w_out = 300
# 3. calls
# add the equations in the first diagram: there must be one and only one
diagram = root.diagrams[0]
for operator in operators:
inputs = operator.inputs
outputs = operator.outputs
in_count = len(inputs)
out_count = len(outputs)
# vertical space between the pins of an equation
io_margin = 100
h_op = (h_in + io_margin) * max(in_count, out_count) + h_in
# 3.1. equations for the inputs/sensors (define an internal variable)
w, h = w_in, h_in
x = x_op - w - 2000
parameters = []
for index, input in enumerate(inputs):
y = y_op + h_op / (in_count + 1) * (index + 1) - h / 2
eq = create.add_data_def_equation(
root, diagram, [input.type], map_inputs[input.name], (x, y), (w, h)
)
# retrieve the defined internal variable
parameters.append(eq.lefts[0])
# 3.2. equation for the operator
# create the expression tree corresponding to a call to an operator
tree = create.create_call(operator, parameters)
# get the list of types for each output
out_types = [_.type for _ in outputs]
eq = create.add_data_def_equation(
root, diagram, out_types, tree, (x_op, y_op), (w_op, h_op)
)
# retrieve the defined internal variables
lefts = eq.lefts
# 3.3. equations for the outputs
w, h = w_out, h_out
x = x_op + w_op + 2000
for index, (output, input) in enumerate(zip(outputs, lefts)):
y = y_op + h_op / (out_count + 1) * (index + 1) - h / 2
create.add_data_def_equation(
root, diagram, [map_outputs[output.name]], input, (x, y), (w, h)
)
# 3.4. propagate the attribute state of the called operator if it is a node
if operator.state:
root.state = True
# 3.5. add an offset for next call
op_margin = 1000
y_op += h_op + op_margin
# 4. create automatically the graphical connections, with default positions
create.add_diagram_missing_edges(diagram)