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: .. figure:: /examples/create/create_top_level_cg.png The resulting top-level operator is as follows: .. figure:: /examples/create/create_top_level_root.png 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)