Advanced Meta-Modeling Tutorial

By Denis Dube

 

This tutorial assumes a basic knowledge of AToM3 meta-modeling and goes into the nitty gritty details of writing constraints/actions in Python code.

 

NOTE: If your viewing this in MSWord, select View (in the menu) then Web Layout. Otherwise the images are cropped and you won't see what's going on! There's also a Zoom in the View menu, which I set to 75%. No doubt Open Office has an equivalent functionality.

 

 

1) Initial setup, I assume you've saved this model, given it a visual representation (to both class and association, including giving each visual icon the attributes of the class/association), named it as QuickTut after pressing the EDIT button, and generated it. It consists of one class and one association. The class has an attribute "name" and "number" of types String and Integer respectively. The association has an attribute "name" of type string. For debugging purposes, giving all classes/associations an attribute name can come in very handy, as you can simply traverse the entire graph and print them out (see next step).

 

 

 

2) A common extension to an AToM3 formalism is a code generator, simulator, analyzer, etc. To do this requires traversing the internal graph structure and creating a button so that we can trigger it. Step 1 is to open your favourite code editor (*cough* Eclipse *cough*) and create a file called "tutorialPrettyPrinter.py" with the following inside:

 

def tutorialPrettyPrinter(atom3i):

 

  # Get the ASGroot for this formalism

  ASGroot = atom3i.ASGroot.getASGbyName('QuickTut_META') # Where QuickTut_META.py is the generated buttons model, it is in your formalism directory

 

  # NOTE: you can use atom3i.ASGroot instead of ASGroot for multi-formalism traversals

 

  # Traverse all nodes in the graph of this formalism only

  nodeTypeList = ASGroot.listNodes.keys()

  for nodeType in nodeTypeList:

    nodeList = ASGroot.listNodes[nodeType]

    for node in nodeList:

      print node.name.getValue() # Access the name attribute and get its value

      

     

  # Traverse only the instances of the class QuickTurialClass

  for node in ASGroot.listNodes['QuickTurialClass']:

    newValue = hash(node.name.getValue()) # Hash the string name

    node.number.setValue(newValue) # Example of setting an integer value

    #node.graphObject_.ModifyAttribute('number', newValue) # Update the visual icon

    print node.number.getValue() # Access the number attribute and get its value

 

 

3a) Create a button to trigger the graph traverser. Start a fresh copy of AToM3, file open (Ctrl-O), choose *_META.py as the filetype, and find the generate buttons model for your formalism.

 

 

3b) The buttons model looks something like this:

 

 

3c) Create a new button. Edit the button. For the "Select Attribute", choose either Text or Image.

 

Image means you have to give a path to a GIF. If your formalism lies in the directory QuickTutorialDir and you placed the icon in QuickTutorialDir/icons/myIcon.gif then the correct path to this icon is QuickTutorialDir/icons/myIcon.gif

 

Drawing_Mode (checked) means that pressing the button in the toolbar does nothing UNTIL the user presses Ctrl-MouseRight on the canvas. For a graph traverser, UN-CHECK it.

 

 

3d) Action is where the fun occurs. You can type arbitrary Python code here, however the dialog box is not the worlds foremost IDE and will not aid you in your debugging efforts! Type in the following code to import and run the "real" code:

 

from tutorialPrettyPrinter import tutorialPrettyPrinter

tutorialPrettyPrinter(self) # self = atom3i in this context

 

 

NOTE: Assuming QuickTutorialDir contains tutorialPrettyPrinter.py

Alternatively, QuickTutorialDir contains codeDir and codeDir contains tutorialPrettyPrinter.py and __init__.py

In this case the code becomes:

 

from codeDir.tutorialPrettyPrinter import tutorialPrettyPrinter

tutorialPrettyPrinter(self) # self = atom3i in this context

 

NOTE2: In the picture below you see strange things like "Constraint name:", "POST condition", "EDIT, SAVE, CREATE, …", and etc. All these are there merely to confuse you (they're there because in some contexts they do have a use, and we only have one dialog for this). They have absolutely no effect in this context however and someday they'll be removed…

 

 

 

 

3e) Save the buttons model. To make the changes take effect, you must now restart AToM3.

 

4) Open the generated formalism. F3 then QuickTut_META.py , draw some instances of the classes and associations, then press the graph traversal button. The screen grab below shows a few things:

a) The traverser did do the whole graph

b) The associations were traversed first!

c) The set attribute worked (i.e.: hash('Quick') = -809163688 in Python). However notice how the graphics were NOT updated. How do we fix that?  Turns out the line commented out in the traverser code given in step 2 was important, so uncomment:

 

#node.graphObject_.ModifyAttribute('number', newValue) # Update the visual icon

 

In general, when changing node.myAttribute, you must also do node.graphObject_.ModifyAttribute('myAttribute', newValue) to update the visual representation (only applicable if you actually display the attribute in the icon of course).

 

 

 

 

4) Creating Actions/Constraints in the meta-model. AToM3 constraints and actions are almost identical, except that if you return a value other than "None" in a constraint, the current operation will be cancelled. For example, below I create a constraint called "noDubes". It is a POST condition, so it is checked AFTER the trigger occurs. The trigger chosen is "EDIT". The constraint itself has the code:

 

if(self.name.getValue() == 'Dube'):

    return ("No Dube's allowed", self.graphObject_) # Tuple with an error message and the visual icon that will be highlighted to indicate where the error occured

 


 

4b) Re-generate the meta-model. Open it up. Create an instance of a class. Type in the name "Dube" and press enter.

 

 

4c) The constraint violation dialog opens up and the name is rejected.

 

 

 

 

5) Simple example of an action. In an action, there's no point in returning a value. Whenever your action is more than a few lines, it is recommended you use the file import methodology shown in the graph traverser example (step 2).  The trigger "CREATE" means that the action will be executed just after this class is instantiated. 

 

print self

print self.graphObject_ # Visual icon

print self.graphObject_.semanticObject # self

print self.__class__.__name__ # The name of the class as a string: 'QuickTurialClass'

 

In this case, "self" is a reference to an instance of QuickTurialClass. In the previous example of a graph traverser, this is a "node". If you want to get really deep, it is an subclass of ASGnode.py and is often called a "semanticObject". Interestingly, self.graphObject_.semanticObject = self (self.graphObject_ is the visual representation, subclass of graphEntity.py or graphLink.py).

 

NOTE: self.parent = atom3i in this context

 

NOTE2: This action would also work if put inside an association instead of a class

 

 

 

The result of this action when a new instance is created is the following:

 

<QuickTurialClass.QuickTurialClass instance at 0x01B349B8>

<graph_QuickTurialClass.graph_QuickTurialClass instance at 0x01B34B20>

<QuickTurialClass.QuickTurialClass instance at 0x01B349B8>

 

6a) Complex action that traverses the local graph structure. Suppose we have drawn the following model in this formalism:

 

 

Whenever we SELECT an entity, we would like the number attribute to equal its parents minus its children (example: clicking on A then A = C – B = 1, or clicking on C then C = B – A = 1, or clicking on B then B = A – C = -2).

 

 

The code is as follows:

 

# Incomming edges (from parents)

inValue = 0

for link in self.in_connections_:

    for semObj in link.in_connections_:

        inValue += semObj.number.getValue()

 

 

# Outgoing edges (to children)

outValue = 0

for link in self.out_connections_:

    for semObj in link.out_connections_:

        outValue += semObj.number.getValue()

 

newValue = inValue - outValue

self.number.setValue(newValue)

self.graphObject_.ModifyAttribute('number', newValue) # Important :)

 

6b) Actions can actually trigger other actions! For example, modify the previous select action code as follows:

 

# Incomming edges (from parents)

inValue = 0

for link in self.in_connections_:

    for semObj in link.in_connections_:

        inValue += semObj.number.getValue()

        semObj.createAction(None) # ß Triggers previously created action for QuickTurialClass

 

 

# Outgoing edges (to children)

outValue = 0

for link in self.out_connections_:

    for semObj in link.out_connections_:

        outValue += semObj.number.getValue()

 

newValue = inValue - outValue

self.number.setValue(newValue)

self.graphObject_.ModifyAttribute('number', newValue) # Important :)

 

 

 

7) Re-generate the meta-model, open the formalism, and click on the instances of class QuickTurialClass. You'll see the numbers automatically change just by selecting the instances.

 

WARNING

 

Never assume in an action that a in_connections_ or out_connections_ are non-empty lists. In particular, a "CREATE" trigger often precedes the actual existence of in_connections_ and out_connections_ entries. For these situations, you'll want a "CONNECT" trigger inside an association instead that does the work (i.e.: the POST condition of a "CONNECT" trigger can be counted upon to actually have populated the in_connections_ and out_connections_).

 

Furthermore, if you wanted the above code in an association's action, you would modify it as so:

 

# Incomming edges (from parents)

inValue = 0

for semObj in self.in_connections_:

        inValue += semObj.number.getValue()

 

 

# Outgoing edges (to children)

outValue = 0

for semObj in self.out_connections_:

        outValue += semObj.number.getValue()

 

newValue = inValue - outValue

self.number.setValue(newValue)

self.graphObject_.ModifyAttribute('number', newValue) # Important :)

 

8) QOCA. This is a declarative way of specifying layout constraints. The ease of high level constraints is counter-balanced by the limited layouts that can be achieved however. If your formalism lends itself well to linear constraints, by all means use the integrated QOCA solver. Doing this is easy in the meta-model. When editing an association find the "QOCA" edit button. A template for a QOCA constraint will be shown. Remove the "return" statement to activate it, then scroll to the bottom and change the constraint to what you need. It looks like as follows:

 

"""

QOCA Constraint Template

NOTE: DO NOT select a POST/PRE action trigger

Constraints will be added/removed in a logical manner by other mechanisms.

"""

return # <--- Remove this if you want to use QOCA

 

# Get the high level constraint helper and solver

from Qoca.atom3constraints.OffsetConstraints import OffsetConstraints

oc = OffsetConstraints(self.parent.qocaSolver) 

 

# Constraint only makes sense if there exists 2 objects connected to this link

if(not (self.in_connections_ and self.out_connections_)): return

 

# Get the graphical objects (subclass of graphEntity/graphLink)

graphicalObjectLink = self.graphObject_

graphicalObjectSource = self.in_connections_[0].graphObject_

graphicalObjectTarget = self.out_connections_[0].graphObject_

objTuple = (graphicalObjectSource, graphicalObjectTarget, graphicalObjectLink)

 

"""

Example constraint, see Kernel/QOCA/atom3constraints/OffsetConstraints.py

For more types of constraints

"""

oc.LeftExactDistance(objTuple, 20)

oc.resolve() # Resolve immediately after creating entity & constraint

 

9) The actual constraint in the template is "oc.LeftExactDistance(objTuple, 20)".  Notice that this is a high level constraint (i.e.: you don't need to actually write a linear constraint). To find all the available high-level constraints, see this file: atom3\Kernel\Qoca\atom3constraints\OffsetConstraints.py

 

10) You can add constraints to classes too, but generally the only worthwhile constraint is a static size constraint. This is what the template for classes does (when activated). For an example of the usage of these constraints, you should have a look at the PacMan formalism (hopefully included with your AToM3 distribution!).  Also, the constraint solver may be disabled, check in OPTIONS (click the big AToM3 logo in the top left) that this is not the case.

 

11) Other types of layout, such as Hierarchical, Spring, Tree-like, Circle, FTA, random. They can be triggered at will, such as in a button or action. Here's an example taken from the class diagram formalism. It imports the graph abstraction layer and a specific layout algorithm, then it runs it:

 

from AToM3LayoutInterfaceModule.AbstractGraph import AbstractGraph

from HierarchicalLayoutModule.HierarchicalLayout import doHierarchicalLayout \

  as hierarchicalLayoutMethod

 

# Hierarchical Layout Options

optionsDict = dict()

optionsDict['Origin'] = True

optionsDict['EdgePromotion'] = 'Always' # ['Never', 'Smart', 'Always']

optionsDict['LayoutDirection'] = 'South' # ['North', 'East', 'South', 'West']

optionsDict['yOffset'] = 20

optionsDict['xOffset'] = 20

optionsDict['layerAlg'] = 'BFS' # ['BFS', 'Longest-path', 'Minimum-width']

optionsDict['maxTotalRounds'] = 50

optionsDict['maxNoProgressRounds'] = 10

optionsDict['crossAlgChoice'] = 'Both' # ['None', 'Barycenter', 'Transpose', 'Both']

optionsDict['randomRestartsWith'] = 'Both' # ['None', 'Barycenter', 'Transpose', 'Both']

optionsDict['baryPlaceMax'] = 100

optionsDict['Arrow curvature'] = 3

optionsDict['Spline optimization'] = True

 

abstractGraph = AbstractGraph(self, [])

hierarchicalLayoutMethod(abstractGraph, optionsDict)   

abstractGraph.updateAToM3(quickUpdate=False) # Use quickUpdate=True if doing multiple layout methods at once (does not redraw screen needlessly)

 

12) To get the right imports for the other layouts, have a look at: atom3\Kernel\LayoutModule\AToM3LayoutInterface.py

 

13) To get the exact options needed by each formalism you can press the HOME key in AToM3 and then choose "Dump Options To Console" or you can do (in any action statement):

 

from HierarchicalLayoutModule import AToM3HierarchicalOptions

AToM3HierarchicalOptions.dumpOptions2Console(atom3i)

print '\n'

from CircleLayoutModule import AToM3CircleOptions

AToM3CircleOptions.dumpOptions2Console(atom3i)

print '\n'

from SpringLayoutModule import AToM3SpringOptions

AToM3SpringOptions.dumpOptions2Console(atom3i)

print '\n'

from TreeLikeLayoutModule import AToM3TreeLikeOptions

AToM3TreeLikeOptions.dumpOptions2Console(atom3i)

print '\n'

from ForceTransferModule import AToM3FTAOptions

AToM3FTAOptions.dumpOptions2Console(atom3i)

 

14) If you would like to run AToM3 without a GUI (example so you can get work done while your washing the dishes), have a look at: atom3\scriptedAToM3.py

 

15) Sometimes the attributes built-into AToM3 are not enough! String, Integer, and Float are nice, but what if you want a tuple (String, Boolean, Boolean) type? The following picture crams in the entire process!

 

  1. In the class diagram formalism, you suddenly realize your type is missing. You press Control-M (Model Menu) and choose "Edit Types".
  2. Press New
  3. Press edit
  4. A new AToM3 in the type formalism pops up.
    1. Create a new "TypeName" (in this example I call it BooleanTuple) and create new "LeafeType" to make up the string, Boolean, Boolean tuple.
    2. Draw an arrow from the TypeName to the LeafType. Afterwards, draw arrows starting from the edge to the other LeafType's.
    3. When done, save the type model (just in case), then press the "Gen Python" button.
    4. Code will appear in "User Arer/User Formalisms/". Two files are created, "BooleanTuple.py" and "BooleanTupleImpl.py" in this example.
    5. You must manually cut and paste them into your formalism directory.
    6. Now press the OK button.
    7. The Name & className (where the 3 is in the picture below) should be automatically set, but if not, just give it the name of "BooleanTuple" (in this example) for both fields.
    8. Press OK (hopefully you did the cut & paste, otherwise this won't work so well).
  5. Edit your class, and add the new attribute which is now there in the list.
  6. The new attribute is a composite of 3 basic attributes, and you can set initial values for each part.

 

NOTE: Be wary when saving models when you have just added a custom type. Make sure that you keep your AToM3 on that class diagram only, and restart before doing anything else. For example, you might add a new type, close the class diagram, open up the PacMan formalism, and then save a PacMan model… with the result that the PacMan model then depends on the BooleanTuple type. A very unfortunate thing when you then try to open that PacMan model! If this happens to you, it is simple to modify the model file and remove the type info (check at the top and bottom of the file, just comment any lines containing references to the type).

 

 

 

16) It is possible to set actions directly on the visual icon in the IconEditor. I recommend not doing that and instead putting the action directly on the class/association. You just do something like self.graphObject_.gf41.setColor('green') in the class/association action. I recommend this because it's obvious by looking at a class what the actions are, but because the visual icons are attributes of the class, it's not at all obvious what the visual icon actions are. NOTE: gf41 is the name of a particular graphical component of a visual icon. You can find the correct name for your icon either in the IconEditor (it shows the name of what your name is hovering over) or by viewing the contents of the graph_*.py file.

 

17) Named Ports. Suppose you want to create a connection to a specific location on an class instance (visually) and be able to access that connection easily (such as to propagate values). This is where Named Ports come in handy. To create a named port, you must first create an attribute "Port".

 

 

Now head over to the Icon Editor (edit the Graphical_Appearence). Click on the bottom right of the toolbar where it shows a N. A dialog appears to allow you to select WHICH named port you want. In this example, I create an input in the bottom right, and an output in the top left.

 

 

 

Now re-generate the formalism, when you drawn an arrow from a QuickTurial class, you'll see this dialog (if you click anywhere near the "MagicOutput" port. You can still do a regular connection by clever clicking… such as on the outside of the circle in this case). If you select the "MagicOutput" button in the dialog, it will then lock the arrow so that it starts at the named port (and never moves from there).

 

 

The same thing happens when you finish your arrow…

 

 

Note: normally you would identify (with a box, or something) exactly where your port is (visually).

 

Once the arrow is created, it will never leave those Named Ports… so you may end up with something like this:

 

 

Now suppose we are the circle on the left. We can do this:

 

for link in self.out_connections_:

            if(link in self.MagicOutput):

                        print 'Observe how the link is duplicated in the out_connections_ and the specific port'

 

# NOTE: self.out_connections_ != self.MagicOutput if an outgoing arrow starts at a different named port or at a regular port.

# Also: type(self.out_connections_) == type(self.MagicOutput) == type(list())

 

If your formalism explicitly restricts a port to having a single connection (like the above picture), then you can do the following:

link = self.MagicOutput[0]

otherObj = link.out_connections_[0] # Assuming that the link has only a source and a target (not a hyperedge)

 

 

18) Cloning a formalism. Suppose you want to make a similar formalism to an existing one but with some important changes. Rather than start working from scratch you'd like to take advantage of the existing meta-model and graphical icons. However, if you simply copy them to a new directory and both formalisms are opened at the same time, you'll have a big problem (two files with the same name but different implementations cannot be handled by AToM3 at this time). In fact, even if two formalisms are NOT simultaneously open, the wrong file *might* be loaded. BEWARE!

 

18a) If you simply want to recycle graphical icons, then graphRenamer.py (located in the central AToM3 directory) is your friend. Copy this file to a folder containing just the icons you want to re-name, then execute it. For each graph icon, such as graph_atomicNinja.py it will prompt you for a new name, such as "reactiveNinja", and create a new file graph_reactiveNinja.py in the same directory.

 

18b) If you want to recycle an entire formalism however, use uniqueFormalismNamer.py (located in the central AToM3 directory). After starting the script you'll see a dialog…

 

 

Choose the directory of the formalism to be re-named…

 

 

Give an extra name prefix to your re-named formalism. This is somewhat "intelligent", so if you use the re-name twice, only the last prefix will be kept.

 

 

The formalism is re-named… and a dialog is shown to tell you what you must do next!

 

NOTE: You *could* ignore this step and it will probably work… but this is MUCH safer.

 

 

 

 

The final step is to open the generative model and re-generate the re-named formalism. First, check to make sure everything is in order… (including pressing the "EDIT" button to make sure that the new name of the formalism is what you wanted!).

 

 

The re-named formalism in action.

 

NOTE: the "traverser" button got lost when the buttons model were re-generated. You must either re-make the button or you could save the *_META.py file (i.e.: not delete it in the step where I recommend deleting all those files. However this is  not *safe* because when you re-generate the formalism names may differ. Good luck though!).