'''***************************************************************************** AToMPM - A Tool for Multi-Paradigm Modelling Copyright (c) 2011 Raphael Mannadiar (raphael.mannadiar@mail.mcgill.ca) This file is part of AToMPM. AToMPM is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. AToMPM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with AToMPM. If not, see . *****************************************************************************''' import re, json, uuid, cPickle as pickle, threading, itertools, traceback, logging from random import Random from utils import Utilities as utils from tcontext import ModelTransformationContext, ExhaustContext from tconstants import TConstants as TC from compiler import ModelAndRuleCompiler from pytcore.core.himesis import HConstants as HC from pytcore.rules.ndarule import NDARule from pytcore.tcore.messages import Packet from accurate_time import time as clock from accurate_time import set_start_time set_start_time() import cProfile, pstats, StringIO ''' hergin :: motif-integration start ''' from motifcontext import MotifContext from tcorecontext import TCoreContext from pytcore.tcore.messages import Pivots ''' hergin :: motif-integration end ''' import igraph as ig #import pydot import datetime from random import * from threading import * from Queue import * from barrier import * from synchgraph import * from itertools import * #from petrinet import * from _abcoll import Iterable from pprint import isreadable from math import * import os ''' py-t-core abstraction layer _lock used to synchronize access to self._changelogs _changelogs used to hold asworker changelogs _M the himesis graph we're transforming _mtContexts the current transformation contexts _transfData a mapping between transformation filenames and their data _userTransfs stores the transformations loaded by the user (needed to return to the pre-run state after running) _execmode the current execution mode _debugOn indicates whether or not debugging is enabled _debugProgrammedBreak a blocking flag used to freeze the execution of a transformation until the user explicitly resumes, at which point we continue precisely where we left off _mtContexts2debugClients used to map ModelTransformationContexts to their associated atompm debugging window, if any _aswCommTools bundle of properties and functions that enable and facilitate sending requests to our parent mtworker's asworker _aswNextSequenceNumber used to determine if a changelog is received out of order, and if a pending changelog is now ready to be handled username the user's username defaultDCL the user's preferred designer code language ''' class PyTCoreAbstractionLayer : def __init__(self,aswCommTools,mtwid) : self._lock = threading.Condition() self._changelogs = [] self._M = None ''' hergin :: motif-integration start ''' self.packet = None self.globalDeltas = [] self.incUpdates = True self.sendDeltas = True ''' hergin :: motif-integration end ''' self._mtContexts = [] self._transfData = {} self._userTransfs = [] self._execmode = 'STOPPED' self._debugOn = False self._debugProgrammedBreak = threading.Event() self._debugProgrammedBreak.set() self._mtContexts2debugClients = {} self._aswCommTools = aswCommTools self._aswNextSequenceNumber = None self.username = None self.defaultDCL = TC.PYTHON self._mtwid = mtwid ''' Used only in COMP 522 and comp 621 Petri Net Modules, docomposition of disconnected _M graph ''' self.modules = {} ''' Synchronization graph for modular state spaces ''' self.SG = None self.modStart = None self.modEnd = None self.flatStart = None self.flatEnd = None ''' State spaces for individual modules ''' self.modIDtoPOS= {} self.SSs = {} #Enactment vars self.bdapiQueue = Queue() #Enactment, If this is true the OpenModel will append model to the canvas, else reload. self.loadedModel=False #Enactment, set of formalisms we load automatically to do the cleanups. self._loadedMMs = set() def processQuery(self,query): result = "" query = query['query'].replace('"',"'") qValue = query.split('(')[0].strip() if qValue == "getCount": typ = query.split("'")[1].strip() def f(e): return e['$ft__'].endswith(typ) result = "Number of '" + typ +"' in the resulting graph: " + str(len(filter(f,self.packet.graph.vs))) # I tried to use self.packet.graph.vs.select(tokens_eq=3) but the attribute names starting with $ didnt let me elif qValue == "toggleSendDelta": self.sendDeltas = not self.sendDeltas if self.sendDeltas: result = "Deltas will be sent at the end!" else: result = "Deltas won't be sent at the end!" elif qValue == "toggleIncUpdate": self.incUpdates = not self.incUpdates if self.incUpdates: result = "Incremental updates are on!" else: result = "Incremental updates are off!" self._aswPrintReq(result) ''' Use this to put the BDAPI response from client''' def _queueBDAPI(self,resp): self.bdapiQueue.put(resp) ''' send a PUT /GET/console request to the asworker ''' def _aswPrintReq(self,msg) : return self._aswCommTools['httpReq']('PUT','/GET/console',{'text':msg}) ''' do 'callback()' once we've received the feedback for the last step that ran, if any, checking for the said feedback and handling any newly received feedback every TC.WAIT_ON_CHLOG_DELAY seconds ''' def _doWhenLastStepFeedbackReceived(self,callback) : if self.incUpdates: def condition() : self._handleChangelogs() return self._mtContexts[-1].isLastStepFeedbackReceived() utils.doWhen( condition, TC.WAIT_ON_CHLOG_DELAY, callback) else: callback() ''' synchronously verify if any changelogs have been received, and if we are ready to handle them, remove them from self._changelogs and handle them 1. sort changelogs by ascending sequence# 2. do nothing if no self._changelogs is empty, if no model has been loaded yet (i.e., self._aswNextSequenceNumber == None), or if the oldest changelog is still too new to be handled 3. crash if we encouter an invalid sequence# 4. otherwise (we're ready to handle the oldest changelog), handle the oldest changelog, increment self._aswNextSequenceNumber and recurse back to step 1 TBI: performance would benefit greatly from caches that map atompm ids to GUIDs ''' def _handleChangelogs(self) : ''' handle a single changelog ''' def _handleChangelog(changelog) : def eq(a,b) : return str(a) == str(b) for c in changelog : if c['op'] == 'MKEDGE' : node1 = \ self._M.vs.select(lambda v : eq(v['$atompmId'],c['id1']))[0] node2 = \ self._M.vs.select(lambda v : eq(v['$atompmId'],c['id2']))[0] self._M.add_edges([(node1.index, node2.index)]) elif c['op'] == 'RMEDGE' : pass elif c['op'] == 'MKNODE' : self._compiler.addNode(self._M, json.loads(c['node']), c['id']) elif c['op'] == 'RMNODE' : node = \ self._M.vs.select(lambda v : eq(v['$atompmId'],c['id']))[0] self._M.delete_nodes([node.index]) elif c['op'] == 'CHATTR' : node = \ self._M.vs.select(lambda v : eq(v['$atompmId'],c['id']))[0] self._M.vs[node.index][c['attr']] = c['new_val'] elif c['op'] == 'LOADMM' : self._compiler.parseMetamodel( c['name'], utils.fread( '/users/%s/%s.metamodel'%(self.username,c['name'])), loadMM=True) elif c['op'] == 'DUMPMM' : self._compiler.unloadMetamodel(c['name']) elif c['op'] == 'RESETM' : self._M = self._compiler.compileModel(c['new_model']) self._M.mmTypeData = self._compiler.getMMTypeData() elif c['op'] == 'SYSOUT' : ''' hergin :: motif-integration :: modify :: added startsWith functions ''' if c['text'].startswith(TC.RULE_SUCCESS_MSG) or \ c['text'].startswith(TC.RULE_NOT_APPLICABLE_MSG) or \ c['text'].startswith(TC.RULE_FAILURE_MSG) or \ c['text'].startswith(TC.TRANSFORMATION_DONE) or \ c['text'].startswith(TC.REMOTE_APPLICATION_FAILURE) : self._mtContexts[-1].setLastStepFeedbackReceived() self._lock.acquire() self._changelogs.sort(key=lambda c : utils.sn2int(c['sequence#'])) if len(self._changelogs) == 0 or \ self._aswNextSequenceNumber == None or \ utils.sn2int(self._changelogs[0]['sequence#']) > \ utils.sn2int(self._aswNextSequenceNumber) : self._lock.release() else : sn = self._changelogs[0]['sequence#'] if utils.sn2int(sn) < utils.sn2int(self._aswNextSequenceNumber) : raise ValueError('invalid changelog sequence# :: '+sn) else : logging.debug('++ ('+sn+') '+str(self._changelogs[0]['changelog'])) _handleChangelog(self._changelogs.pop(0)['changelog']) self._aswNextSequenceNumber = \ utils.incrementSequenceNumber(self._aswNextSequenceNumber) self._lock.release() self._handleChangelogs() ''' load a model (and its metamodels) 1. compile the provided model into a himesis graph and save it to self._M 2. synchronize self._M's mmTypeData with that of self._compiler's so that it gets updated as new metamodels are loaded 3. initialize self._aswNextSequenceNumber based on the sequence# 'sn' of the provided model, and forget any already received out-of-date changelogs NOTE: this function should only get called once (when the asworker initially sets up this mtworker) ''' def loadModel(self,m,mms,sn) : assert self._M == None, 'ptcal.loadModel() should only be called once' self._compiler = ModelAndRuleCompiler( self.username, self._aswCommTools['wid'], self.defaultDCL, self._mtwid) self._M = self._compiler.compileModel(m,mmsData=mms) self._M.mmTypeData = self._compiler.getMMTypeData() ''' hergin :: motif-integration start ''' self.packet = Packet(self._M) ''' hergin :: motif-integration end ''' self._aswNextSequenceNumber = utils.incrementSequenceNumber(sn) self._lock.acquire() self._changelogs = \ filter(lambda c : utils.sn2int(sn) < utils.sn2int(c['sequence#']), self._changelogs) self._lock.release() ''' load a PN model (and its metamodels) 1. compile the provided model into a himesis graph and save it to self._M 2. synchronize self._M's mmTypeData with that of self._compiler's so that it gets updated as new metamodels are loaded 3. initialize self._aswNextSequenceNumber based on the sequence# 'sn' of the provided model, and forget any already received out-of-date changelogs NOTE: this function should only get called once (when the asworker initially sets up this mtworker) ''' def loadModelPN(self,m,mms,sn) : assert self._M == None, 'ptcal.loadModel() should only be called once' self._compiler = ModelAndRuleCompiler(self.username) self._M = self._compiler.compileModelPN(m,mmsData=mms) disjoint = self._M.decompose(mode=ig.WEAK) #create dictionary of modules for mod in disjoint: self.modules [ uuid.uuid4()] = mod self._M.mmTypeData = self._compiler.getMMTypeData() self._aswNextSequenceNumber = utils.incrementSequenceNumber(sn) self._lock.acquire() self._changelogs = \ filter(lambda c : utils.sn2int(sn) < utils.sn2int(c['sequence#']), self._changelogs) self._lock.release() ''' setup internal state to reflect given runtime configuration ''' def _loadRuntimeConfiguration(self,rc) : if 'looseSubtypingMM' in rc : self._compiler.RC__looseSubtypingMM = rc['looseSubtypingMM'] ''' read in some json that describes a model transformation from file 'fname', store in self._transfData, and push a new ModelTransformationContext onto self._mtContexts ''' def _loadTransform(self,fname) : if fname not in self._transfData : self._transfData[fname] = \ utils.fread('/users/%s/%s'%(self.username,fname)) ''' hergin :: motif-integration start ''' if TC.MOTIFMM in self._transfData[fname]['metamodels']: self._mtContexts.append(MotifContext(fname,self)) elif TC.TCOREMM in self._transfData[fname]['metamodels']: self._mtContexts.append(TCoreContext(fname,self)) elif TC.TRANSFMM in self._transfData[fname]['metamodels']: self._mtContexts.append(ModelTransformationContext(self._transfData[fname],fname)) ''' hergin :: motif-integration end ''' ''' load a set of user-specified transformations and forget anything previously loaded 1. forget previously loaded transformations and compiled rules 2. reset 'session' area 3. load transformations 4. remember loaded transformations ''' def loadTransforms(self,fnames) : self._transfData = {} self._mtContexts = [] self._compiler.forgetCompiledRules() self._M.session = {} for fname in fnames : self._loadTransform(fname) self._userTransfs = fnames ''' returns the filename and compiled form of the next rule to run 1. fetch the current transformation context 2. retrieve and load runtime configuration options, if any 3. if transformatio debugging is enabled and we're entering a ModelTransformationContext for the first time, we a) remember self._execmode and set self._execmode to PAUSE b) notify the user of the impending "programmed debugging pause" c) request a new atompm instance loaded with the transformation model corresponding to the current transformation context d) unset the _debugProgrammedBreak flag and make a blocking call that waits for another thread to reset it (via self._startDebugProgrammedBreak())... this may occur in a number of cases if when this occurs i. user presses stop --> self._execmode == 'STOPPING' : _nextRule() returns an error which in turn triggers the stopping of the current transformation ii. user disables debugging --> self._execmode == 'PAUSE' : self._execmode is restored to its previous value and execution continues as planned (i.e., _nextRule() returns with the next rule to run) iii. user presses play/step --> self._execmode == 'PLAY/STEP' : same behaviour as ii. 4. call its nextStep() function to get the next transformation step a) if the step is not an object (i.e., is an application code) i. if there is more than 1 loaded transformation context, j. pop the newly completed transformation context jj. if the next context (which may be a "parent" transformation or the next of multiple loaded transformations) is a parent, update its last step application info jjj. make a recursive call to get the next rule to run from within the "next" context ii. otherwise, simply return the said application code b) if the step is an error, return it c) otherwise, determine the type of the step (via the node's $type attribute or by inspecting the 'metamodels' array in the provided .model file) i. Rules get stored in 'nr' to be later returned ii. Transformations and Exhausts[Randoms] cause the pushing of a new transformation context onto self._mtContexts, and of a recursive call to get the next rule to run from within that new context 5. return the rule name and compiled form NOTE:: in step 4c), while in debug mode, we highlight transformations, exhausts and rules before recursing on them or returning them, respectively ''' def _nextRule(self) : mtc = self._mtContexts[-1] self._loadRuntimeConfiguration(mtc.getRuntimeConfiguration()) if self._debugOn and not mtc.isTransformationUnderWay() and \ (type(mtc) == MotifContext or type(mtc) == TCoreContext) : # hergin :: motif-integration modify _execmode = self._execmode self._execmode = 'PAUSE' self._aswPrintReq(TC.DEBUGGING_HALT) self._requestClientDebugWindow(mtc.fname) self._startDebugProgrammedBreak() if self._execmode == 'STOPPING' : return {'$err':'transformation stopped during debugging pause'} elif self._execmode == 'PAUSE' or \ self._execmode == 'PLAY' or \ self._execmode == 'STEP' : self._execmode = _execmode self.bdapiQueue = Queue() ns = mtc.nextStep() if ns.__class__ != {}.__class__ : if len(self._mtContexts) > 1 : self._mtContexts = self._mtContexts[:-1] if self._mtContexts[-1].isTransformationUnderWay() : self._mtContexts[-1].setLastStepApplicationInfo(ns) return self._nextRule() else : return ns elif '$err' in ns : return ns['$err'] else : def highlightUpcomingStep() : for _mtc in reversed(self._mtContexts) : if id(_mtc) in self._mtContexts2debugClients : debugClient = self._mtContexts2debugClients[id(_mtc)] self._requestNodeHighlight( debugClient['host'], debugClient['aswid'], _mtc.getCurrentStepId()) break if 'id' in ns : fulltype = mtc.t['nodes'][ns['id']]['$type'] ''' hergin :: motif-integration start ''' if fulltype == mtc.metamodel+"/CRule": if self._debugOn : highlightUpcomingStep() self._loadTransform(ns['rule']) return self._nextRule() elif fulltype.startswith(TC.TCOREMM) or\ fulltype.startswith(TC.MOTIFMM): if self._debugOn : highlightUpcomingStep() return ns ''' hergin :: motif-integration end ''' elif fulltype == TC.TRANSFMM+'/Rule' : if self._debugOn : highlightUpcomingStep() return {'fname':ns['fname'], 'cr':self._compiler.compileRule(None,ns['fname'])} #Enactment OpenModel blob, pathToFormalism is present is MM, but not used here, #for functionality of opening window with formalisms is in WriteModel. #pathToFormalism should be removed from MM for OpenModel (was not removed due to #language evolution). elif fulltype == TC.TRANSFMM+'/OpenModel' : if self._debugOn : highlightUpcomingStep() fname = mtc.t['nodes'][ns['id']]['pathToModel']['value'] #formalism = mtc.t['nodes'][ns['id']]['pathToFormalism']['value'] formalism = "" return {'fname':fname,'formalism':formalism,'rtype':'OpenModel'} #Enactment WriteModel blob elif fulltype == TC.TRANSFMM+'/WriteModel' : if self._debugOn : highlightUpcomingStep() fname = mtc.t['nodes'][ns['id']]['pathToModel']['value'] formalism = mtc.t['nodes'][ns['id']]['pathToFormalism']['value'] return {'fname':fname,'formalism':formalism,'rtype':'WriteModel'} elif fulltype == TC.TRANSFMM+'/Transformation' : if self._debugOn : highlightUpcomingStep() self._loadTransform(ns['fname']) return self._nextRule() elif fulltype == TC.TRANSFMM+'/Exhaust' : self._mtContexts.append( ExhaustContext(mtc.t,ns['id']) ) return self._nextRule() elif fulltype == TC.TRANSFMM+'/ExhaustRandom' : self._mtContexts.append( ExhaustContext(mtc.t,ns['id'],self._randomGen) ) return self._nextRule() else : ''' hergin :: motif-integration start ''' if 'trafoResult' in ns: return ns; ''' hergin :: motif-integration end ''' contents = utils.fread('/users/%s/%s'%(self.username,ns['fname'])) if self._debugOn : highlightUpcomingStep() if TC.RULEMM in contents['metamodels'] : return {'fname':ns['fname'], 'cr':self._compiler.compileRule(contents,ns['fname'])} elif TC.TRANSFMM in contents['metamodels'] : self._transfData[ns['fname']] = contents self._loadTransform(ns['fname']) return self._nextRule() raise ValueError(\ 'file does not contain valid rule or transformation '+\ 'model :: '+ns['fname']) ''' Enactment do OpenModel magic ''' def runOpenModelRule(self, fname="",formalism=""): unload = "" if not fname: return (None,TC.FAILED) else: if not formalism: self._aswPrintReq('auto loading model :: '+fname) try: with open(os.getcwd()+'/users/'+self.username+fname) as f: pass except IOError as e: self._aswPrintReq('failed opening a file :: '+fname) return (None,TC.FAILED) if not self.loadedModel: method = '_loadModelForTransform' if len(self._loadedMMs) == 0: self._loadedMMs = self._compiler._loadedMMs.copy() diff = self._compiler._loadedMMs.difference(self._loadedMMs) for u in diff: unload += u+'.defaultIcons.metamodel,' else: method = '_appendModelForTransform' resp = self._aswCommTools['httpReq']( 'PUT', '/GET/console', {'text':'CLIENT_BDAPI :: '+ '{"func":"'+method+'",'+ ' "args":'+ '{"fname":"'+fname+'",'+ '"unload":"'+unload+'",'+ ' "callback-url":"/__mt/bdapiresp?wid='+ self._aswCommTools['wid']+'"}}'}) resp = self.bdapiQueue.get(block=True,timeout=5000) if not resp['resp'] == 'ok': return (None,TC.FAILED) else: if not self.loadedModel: self.loadedModel = True return (None,TC.SUCCEEDED) #Was used to open new window with loaded formalisms and pause the transform, now #this functionality is in WriteMOdel. else: pass # Keep for now.... # self._aswPrintReq('pausing transform') # self._execmode = 'PAUSE' # self._aswPrintReq('opening new window for manual step:: '+fname) # try: # with open(os.getcwd()+'/users/'+self.username+fname) as f: # exists = 'true' # except IOError as e: # exists = 'false' # resp = self._aswCommTools['httpReq']( # 'PUT', # '/GET/console', # {'text':'CLIENT_BDAPI :: '+ # '{"func":"_createEmptyModelInNewWindow",'+ # ' "args":'+ # '{"fname":"'+fname+'","exists":"'+exists+'",'+'"formalism":"'+formalism+'",' # ' "callback-url":"/__mt/bdapiresp?wid='+ # self._aswCommTools['wid']+'"}}'}) # self.loadedModel = False # return (None,TC.SUCCEEDED) ''' Enactment do WriteModel magic ''' def runWriteModelRule(self,fname="",formalism=""): if not fname: #this makes next openmodel call a load model instead of append. #basically use this trick to clean up canvas and have new model: #first place WriteModel blob without fname, #followed by OpenModel. self.loadedModel = False return (None,TC.SUCCEEDED) else: #No formalism specified, save model if not formalism: self._aswPrintReq('auto saving model :: '+fname) resp = self._aswCommTools['httpReq']( 'PUT', '/GET/console', {'text':'CLIENT_BDAPI :: '+ '{"func":"_writeModelAfterTransform",'+ ' "args":'+ '{"fname":"'+fname+'",'+ ' "callback-url":"/__mt/bdapiresp?wid='+ self._aswCommTools['wid']+'"}}'}) #Need to wait for the model to load. resp = self.bdapiQueue.get(block=True,timeout=5000) if resp['resp'] == 'ok': self.loadedModel = False return (None,TC.SUCCEEDED) else: (None,TC.FAILED) #Formalism specified, open new window, with loaded formalism and/or model. else: self._aswPrintReq('pausing transform') self._execmode = 'PAUSE' self._aswPrintReq('opening new window for manual step:: '+fname) try: with open(os.getcwd()+'/users/'+self.username+fname) as f: #open existing model exists = 'true' except IOError as e: #or save model with the fname provided exists = 'false' resp = self._aswCommTools['httpReq']( 'PUT', '/GET/console', {'text':'CLIENT_BDAPI :: '+ '{"func":"_createEmptyModelInNewWindow",'+ ' "args":'+ '{"fname":"'+fname+'","exists":"'+exists+'",'+'"formalism":"'+formalism+'",' ' "callback-url":"/__mt/bdapiresp?wid='+ self._aswCommTools['wid']+'"}}'}) self.loadedModel = False return (None,TC.SUCCEEDED) ''' synchronously save 1 changelog into self._changelogs ''' def onchangelog(self,c) : self._lock.acquire() self._changelogs.append(c) self._lock.release() ''' causes the execution of the current transformation(s) to pause (by preventing _play()'s next call to _step(), if any) ''' def pause(self) : self._execmode = 'PAUSE' if not self.incUpdates: req = self.buildEditHttpReq(self.globalDeltas) self.globalDeltas = [] resp = self._aswCommTools['httpReq']('POST','/batchEdit',req) if not utils.isHttpSuccessCode(resp['statusCode']) : self.stop() self._aswPrintReq(TC.REMOTE_APPLICATION_FAILURE + resp['reason']) return self._handleChangelogs() ''' play() calls _play() if it isn't already running (i.e., if we're already in PLAY mode, a timed call to _play() has already been placed) and if there isn't already another thread currently paused on _debugProgrammedBreak... if there is such a thread, it is unpaused (via _stopDebugProgrammedBreak()) and the current thread terminates immediately _play() schedules an action for when feedback from the last step is received... the action is 1. return if we're no longer in PLAY mode 2. take one _step() 3. schedule a recursive call to _play() in TC.INTER_RULE_DELAY seconds ''' def play(self) : self.start_time = clock() if self._execmode == 'STOPPED': self._randomGen = Random(0) if self._execmode != 'PLAY' : self._execmode = 'PLAY' if not self._stopDebugProgrammedBreak() : self._play() def _play(self) : if self.incUpdates: self._doWhenLastStepFeedbackReceived( lambda : self._execmode == 'PLAY' and \ self._step() and \ utils.setTimeout(TC.INTER_RULE_DELAY,self._play)) else: self._doWhenLastStepFeedbackReceived( lambda : self._execmode == 'PLAY' and \ self._step() and \ self._play()) ''' associate the newly created debugging window described by 'clientInfo' to the ModelTransformationContext that requested its creation ''' def registerDebugClient(self,clientInfo) : clientInfo = json.loads(clientInfo) for mtc in reversed(self._mtContexts) : if hasattr(mtc,'fname') and mtc.fname == clientInfo['fname'] : self._mtContexts2debugClients[id(mtc)] = clientInfo ''' request a new atompm client via the client backdoor API... the new client will be loaded with the specified model and sufficient information to identify and communicate with the client will be POSTed to the callback url ''' def _requestClientDebugWindow(self,fname) : return self._aswCommTools['httpReq']( 'PUT', '/GET/console', {'text':'CLIENT_BDAPI :: '+ '{"func":"_loadModelInNewWindow",'+ ' "args":'+ '{"fname":"'+fname+'",'+ ' "callback-url":"/__mt/debugClient?wid='+ self._aswCommTools['wid']+'"}}'}) ''' request that the specified node from the specified atompm instance be highlighted ''' def _requestNodeHighlight(self,host,aswid,asid,timeout=5000) : return utils.httpReq( 'PUT', host, '/GET/console?wid='+aswid, {'text':'CLIENT_BDAPI :: '+ '{"func":"_highlight",'+ ' "args":'+ '{"asid":"'+asid+'",'+ ' "timeout":'+str(timeout)+'}}'}) ''' hergin :: motif-integration :: START :: put this to outside of step function ''' ''' also added self ''' ''' go through a rule's deltas and (1) produce a batchEdit request, and (2) undo them NOTE: we undo the deltas for 2 reasons 1. so that changes become driven by asworker changelogs (like in csworkers) 2. so that we don't need to figure out which entries in received changelogs correspond to user-operations and which ones correspond to the effects of the constructed batchEdit NOTE: since we sometimes need to use the result from one request as the parameter of another (i.e., create a node, update *it*), we use the 'mknodes' map to remember which requests created which new nodes... this also allows to know which nodes already exist and which ones were created by the last rule NOTE: because creation of connector nodes and their linking to their ends is described in non-contiguous deltas, we use the 'mknodes' map to remember incomplete connector creation requests until the appropriate MKEDGE deltas are encountered ''' def buildEditHttpReq(self,deltas) : reqs = [] mknodes = {} neighborhood = None ''' construct an atompmId given a node... the result will be a) a 'macro' to be replaced by the result of an earlier request within the batchEdit, if the node was created by the last rule b) the atompmId stored within the node, if the node already has a counter-part in atompm ''' def atompmInstanceId(node) : if node[HC.GUID] in mknodes : return '$'+str(mknodes[node[HC.GUID]])+'$' else : return node['$atompmId'] for d in deltas : if d['op'] == 'RMNODE' : reqs.append({\ 'method':'DELETE', 'uri':d['attrs'][HC.FULLTYPE]+'/'+\ d['attrs']['$atompmId']+'.instance'}) elif d['op'] == 'MKNODE' : mknodes[d['guid']] = len(reqs) node = self._M.vs[self._M.get_node(d['guid'])] if neighborhood == None : neighborhood = map( lambda n: n[HC.FULLTYPE]+'/'+n['$atompmId']+'.instance', d['neighborhood']) if node[HC.CONNECTOR_TYPE] : reqs.append({\ 'method':'POST', 'uri':node[HC.FULLTYPE]+'.type', 'reqData': {'src':None, 'dest':None, 'hitchhiker': {'segments':None, 'asSrc':None, 'asDest':None, 'neighborhood':neighborhood}}}) else : reqs.append({\ 'method':'POST', 'uri':node[HC.FULLTYPE]+'.type', 'reqData':{'hitchhiker':{'neighborhood':neighborhood}}}) elif d['op'] == 'RMEDGE' : pass elif d['op'] == 'MKEDGE' : def isConnectorMKNODE(req): return 'dest' in req['reqData'] if d['guid1'] in mknodes : req = reqs[ mknodes[d['guid1']] ] if isConnectorMKNODE(req) : node2 = self._M.vs[self._M.get_node(d['guid2'])] id = atompmInstanceId(node2) req['reqData']['dest'] = \ req['reqData']['hitchhiker']['asDest'] = \ node2[HC.FULLTYPE]+'/'+id+'.instance' if d['guid2'] in mknodes : req = reqs[ mknodes[d['guid2']] ] if isConnectorMKNODE(req) : node1 = self._M.vs[self._M.get_node(d['guid1'])] id = atompmInstanceId(node1) req['reqData']['src'] = \ req['reqData']['hitchhiker']['asSrc'] = \ node1[HC.FULLTYPE]+'/'+id+'.instance' elif d['op'] == 'CHATTR' : node = self._M.vs[self._M.get_node(d['guid'])] id = atompmInstanceId(node) reqs.append({\ 'method':'PUT', 'uri':node[HC.FULLTYPE]+'/'+id+'.instance', 'reqData':{'changes':{d['attr']:d['new_val']}}}) elif d['op'] == 'LOADMM' : reqs.append({\ 'method':'PUT', 'uri':'/current.metamodels', 'reqData': {'mm':'/%s%s.metamodel'%(self.username,d['name'])}}) if self.incUpdates: for d in reversed(deltas) : if d['op'] == 'RMNODE' : newNodeIndex = self._M.add_node(newNodeGuid=d['attrs'][HC.GUID]) for attr,val in d['attrs'].iteritems() : self._M.vs[newNodeIndex][attr] = val elif d['op'] == 'MKNODE' : node = self._M.vs[self._M.get_node(d['guid'])] self._M.delete_nodes([node.index]) elif d['op'] == 'RMEDGE' : node1 = self._M.vs[self._M.get_node(d['guid1'])] node2 = self._M.vs[self._M.get_node(d['guid2'])] self._M.add_edges([(node1.index, node2.index)]) elif d['op'] == 'MKEDGE' : pass elif d['op'] == 'CHATTR' : node = self._M.vs[self._M.get_node(d['guid'])] node[d['attr']] = d['old_val'] elif d['op'] == 'LOADMM' : pass ''' hergin :: motif-integration modify: succeeded rule name + time ''' #reqs.append({\ # 'method':'PUT', # 'uri':'/GET/console', # 'reqData':{'text':TC.RULE_SUCCESS_MSG+" ("+self._mtContexts[-1]._lastStep['alias']+":"+self._mtContexts[-1]._lastStep['name']+") in "+str(self._mtContexts[-1]._lastStep['time'])}}) # 'reqData':{'text':TC.RULE_SUCCESS_MSG}}) return reqs ''' hergin :: motif-integration :: END :: put this to outside of step function ''' ''' author: hergin sendAndApplyDelta() If debug mode: Sends and applies the inputted deltas to the model and UI instance. else: Collect deltas in a globalDeltas variable to handle later ''' def sendAndApplyDelta(self,deltas): if self.incUpdates: req = self.buildEditHttpReq(deltas) resp = self._aswCommTools['httpReq']('POST','/batchEdit',req) if not utils.isHttpSuccessCode(resp['statusCode']) : self.stop() self._aswPrintReq(TC.REMOTE_APPLICATION_FAILURE + resp['reason']) return self._handleChangelogs() else: self.globalDeltas.extend(deltas) self.packet.deltas = [] ''' step() wrapper around _step() that ensures that step requests from user are ignored when in PLAY mode, and that valid requests only go through (i.e., actually call _step()) when feedback for the last step is received... moreover, as is the case for play(), if there is already another thread currently paused on _debugProgrammedBreak, it is unpaused (via _stopDebugProgrammedBreak()) and the current thread terminates immediately _step() fetch and run next rule 1. fetch next rule a) if next rule is not a {} (i.e., all available transformations have terminated and _nextRule() returned the resulting application code), report application code and stop() b) if an error is returned, report it and stop() c) otherwise, i. run rule (returns (deltas|error,applicationInfo)) ii. set ran rule's application info iii. if rule was n/a, report this iii. if rule failed, report this and error iii. otherwise, j. construct a batchEdit operation based on the rule's effects, and unfo the said effects jj. send off the batchEdit jjj. stop() if the batchEdit fails NOTE: this function assumes that feedback for the last step has already been received ''' def step(self) : if not hasattr(self, 'start_time'): self.start_time = clock() if self._execmode == 'PLAY' : pass else : if self._execmode == 'STOPPED': self._randomGen = Random(0) self._execmode = 'STEP' if not self._stopDebugProgrammedBreak() : self._doWhenLastStepFeedbackReceived(self._step) def _step(self) : ''' run the specified rule and return a tuple describing its execution ''' def runRule(r) : ''' hergin :: motif-integration start ''' #self._aswPrintReq('launching rule :: '+r['fname']) #ar = NDARule(r['cr']['lhs'],r['cr']['rhs'],rng=self._randomGen) mtc = self._mtContexts[-1] if mtc.metamodel == TC.MOTIFMM or mtc.metamodel == TC.TCOREMM: ar = r['rule'] else: ar = NDARule(r['cr']['lhs'],r['cr']['rhs'],rng=self._randomGen,sendAndApplyDeltaFunc=self.sendAndApplyDelta) if mtc.nextInput == "packetIn": startTime=clock() self.packet = ar.packet_in(self.packet) mtc.setLastStepExecTime(clock()-startTime) elif mtc.nextInput == "nextIn": startTime=clock() self.packet = ar.next_in(self.packet) mtc.setLastStepExecTime(clock()-startTime) elif mtc.nextInput == "cancelIn": startTime=clock() self.packet = ar.cancelIn(self.packet) mtc.setLastStepExecTime(clock()-startTime) elif mtc.nextInput == "successIn": startTime=clock() self.packet = ar.success_in(self.packet) mtc.setLastStepExecTime(clock()-startTime) ''' hergin :: motif-integration end ''' if ar.is_success : return (self.packet.deltas,TC.SUCCEEDED) elif not ar.is_success : ''' hergin :: motif-integration start (Some terminology fixed) ''' if ar.exception : return (str(ar.exception),TC.EXCEPTION) else : return (None,TC.FAILED) ''' hergin :: motif-integration end ''' try : nr = self._nextRule() except Exception : nr = {'$err':traceback.format_exc()} ''' hergin :: motif-integration start TRAFO RESULT: in case of a CRule_end, pop it from context and continue the rest ''' while 'trafoResult' in nr: if len(self._mtContexts)==1: if not self.incUpdates and self.sendDeltas: ''' hergin TO BE MODIFIED - release mode will change ''' req = self.buildEditHttpReq(self.globalDeltas) self.globalDeltas = [] resp = self._aswCommTools['httpReq']('POST','/batchEdit',req) if not utils.isHttpSuccessCode(resp['statusCode']) : self.stop() self._aswPrintReq(TC.REMOTE_APPLICATION_FAILURE + resp['reason']) return self._handleChangelogs() self._aswPrintReq(TC.TRANSFORMATION_DONE+nr['trafoResult']+" in "+str(self._mtContexts[-1].totalExecutionTime/1000.0)+" seconds, in total "+str((clock()-self.start_time)/1000.0)) self.stop() return else: prevTrafo=self._mtContexts.pop() self._mtContexts[-1].setLastStepExecTime(prevTrafo.totalExecutionTime) self._mtContexts[-1].setLastStepApplicationInfo(nr['trafoResult']) try : nr = self._nextRule() except Exception : nr = {'$err':traceback.format_exc()} if nr.__class__ != {}.__class__ : self._aswPrintReq(TC.TRANSFORMATION_DONE + nr) self.stop() return elif '$err' in nr : self._aswPrintReq(TC.NO_NEXT_RULE+nr['$err']) self._stop() return else : if 'rtype' in nr: type = nr['rtype'] if type == 'OpenModel': (res,ai) = self.runOpenModelRule(nr['fname'],nr['formalism']) elif type == 'WriteModel': (res,ai) = self.runWriteModelRule(nr['fname'],nr['formalism']) self._mtContexts[-1].setLastStepApplicationInfo(ai) self._mtContexts[-1].setLastStepFeedbackReceived() return True else: (res,ai) = runRule(nr) self._mtContexts[-1].setLastStepApplicationInfo(ai) if ai == TC.FAILED and self.incUpdates: ''' hergin :: motif-integration modify (which rule is not succeeded) ''' self._aswPrintReq(TC.RULE_FAILURE_MSG+" ("+self._mtContexts[-1]._lastStep['alias']+":"+self._mtContexts[-1]._lastStep['name']+")") elif ai == TC.EXCEPTION and self.incUpdates: self._aswPrintReq(TC.RULE_EXCEPTION_MSG + res) else : ''' hergin :: motif-integration :: start ''' mtc = self._mtContexts[-1] if self.incUpdates: if mtc.metamodel == TC.MOTIFMM or mtc.metamodel == TC.TCOREMM: self._aswPrintReq(TC.RULE_SUCCESS_MSG+" ("+self._mtContexts[-1]._lastStep['alias']+":"+self._mtContexts[-1]._lastStep['name']+")") else: self._aswPrintReq(TC.RULE_SUCCESS_MSG) self._mtContexts[-1].setLastStepFeedbackReceived() ''' hergin :: motif-integration :: end ''' return True ''' stop() sets self._execmode to STOPPING and schedules a call to _stop() for when feedback from the last step is received... being in STOPPING mode implies that the _play() loop, if any, will be broken, and that incoming user requests will be rejected... in the case where there is already another thread currently paused on _debugProgrammedBreak, it is unpaused (via _stopDebugProgrammedBreak()) which leads to the transformation being stopped from within that thread; the current thread terminates immediately _stop() 1. restores self._mtContexts to right after the user loaded his transformation(s) (i.e., to before we actually [partially] ran it) 2. resets self._mtContexts2debugClients 3. sends a console message to notify the user that the transformation has stopped 4. sets self._execmode to STOPPED (i.e., we're done STOPPING and can handle new requests) ''' def isStopped(self) : return self._execmode == 'STOPPED' def isStopping(self) : return self._execmode == 'STOPPING' def stop(self) : if not self.incUpdates: req = self.buildEditHttpReq(self.globalDeltas) self.globalDeltas = [] resp = self._aswCommTools['httpReq']('POST','/batchEdit',req) if not utils.isHttpSuccessCode(resp['statusCode']) : self.stop() self._aswPrintReq(TC.REMOTE_APPLICATION_FAILURE + resp['reason']) return self._handleChangelogs() self._execmode = 'STOPPING' #Used for enactment, prevents open being append. self.loadedModel = False if not self._stopDebugProgrammedBreak() : self._doWhenLastStepFeedbackReceived(self._stop) def _stop(self) : self._mtContexts = [] for fname in self._userTransfs : self._loadTransform(fname) self._mtContexts2debugClients = {} self._aswPrintReq(TC.TRANSFORMATION_STOPPED) self._execmode = 'STOPPED' ''' enter a "programmed debugging pause" (i.e. unset the _debugProgrammedBreak flag and block until another thread resets it ''' def _startDebugProgrammedBreak(self) : self._debugProgrammedBreak.clear() self._debugProgrammedBreak.wait() ''' if the _debugProgrammedBreak flag is not set (this can only happen when another thread unset it to enter a programmed debugging pause), set it and return true, otherwise do nothing and return false NOTE:: this function returns true iff there is a thread waiting on _debugProgrammedBreak ''' def _stopDebugProgrammedBreak(self) : if not self._debugProgrammedBreak.isSet() : self._debugProgrammedBreak.set() return True return False ''' toggle the _debugOn flag and report debugger status to atompm... when disabling debugging, any current programmed debugging pauses are stopped (and execution resumes normally) while in DEBUG MODE, . entering a ModelTransformationContext for the first time triggers a) setting self._execmode to PAUSE b) notifying the user of the "programmed debugging pause" c) spawning of a new atompm instance loaded with the relevant transformation model d) entering programmed debugging pause that can be broken by _stopDebugProgrammedBreak() which is triggered by i. disabling transformation debugging; OR ii. pressing "play", "step" or "stop" . before running a rule, it or its enclosing ExhaustContext's associated atompm node is highlighted ''' def toggleDebugMode(self) : self._debugOn = not self._debugOn if self._debugOn : self._aswPrintReq(TC.DEBUGGING_ON) else : self._stopDebugProgrammedBreak() self._aswPrintReq(TC.DEBUGGING_OFF) ''' #Perform flat reachability analysis #This is what creates the simple reachability graph. #If the Petri nets are disjoint there will be several PNs "modules" that will be calculated in parallel. def PNFull(self,fname='',dot=False): #if not self.modules: self._handleChangelogs() self.modules = {} self.modStart = time() disjoint = self._M.decompose(mode=ig.WEAK) #Let's compile PN graph out of generic himesis graph and create dictionary with unique IDs barier = barrier(len(disjoint)+1) #barier, modules + synch thread for mod in disjoint: queue = Queue() uid = uuid.uuid4() key = str(uid)[:str(uid).index('-')] module = PnModule(self.toPN(mod),queue, barier,True) self.modules [ key] = module module.start() barier.wait() print 'Time elapsed flat analysis %f'%(time() - self.modStart) self.modules [ key].summary() self.modStart = time() #do the test of reachability here. #self.reachabilityTestFlat() print 'Find place elapsed flat analysis %f'%(time() - self.modStart) for key,mod in self.modules.items(): if dot: #can take too long to plot for large state space and/or hand dot binary #here we output the reacability graph in svg mod.graph(key=key,fname=fname) else: #here we output reachability graph in xml mod.reachtoxml(fname,key) #peroform modular analysis, use at your own risk. Toolbar does not enable this, experimental def analyzePN(self): #First lets break model into submodels self.modStart = time() #if not self.modules: self.modules = {} disjoint = self._M.decompose(mode=ig.WEAK) barier = barrier(len(disjoint)+1) #barier, modules + synch thread for mod in disjoint: queue = Queue() module = PnModule(self.toPN(mod),queue, barier) self.modules [ module.getKey()] = module module.start() barier.wait() #wait till all threads stop doing first phase. M0 = [] TFenabled = [] TF = {} work = [] tofire = [] for key,mod in self.modules.items(): TF[key] = mod.TFS M0.append('%s-%d'%(key,0)) ind=0 self.sg = synchgraph(len(self.modules),M0) work.append(M0) res={} while work: M = work.pop() for key,mod in self.modules.items(): TFenabled.append( mod.getEnabledTFs()) tofire = reduce(set.intersection,map(set,TFenabled)) for key,mod in self.modules.items(): mod.que.put(tofire) barier.wait() #lets wait for threads, they may building local graphs still end = False for key,mod in self.modules.items(): if not mod.result: end = True res[key] = mod.result #got results now produce new states for syngraph and archs. if not end: work.append(M) else: #self.sg.graph() for key,mod in self.modules.items(): mod.SC() mod.graph() mod.que.put(['@exit']) self.sg.markSCC(self.modules) self.sg.graph() print '---------------------------' print 'Time elapsed modular analysis %f'%(time() - self.modStart) for key,mod in self.modules.items(): mod.summary() print '---------------------------' print 'Synch graph:' self.sg.summary() print '---------------------------' self.modStart = time() self.reachabilityTestModular() print 'Find place elapsed modular analysis %f'%(time() - self.modStart) return #main result fr ={} to = {} for key,value in res.items(): for k,v in value.items(): if not k in fr: fr[k] = [] fr[k].append([]) fr[k].append([]) fr[k][0].append(v[0]) fr[k][1].append(v[1]) from_prod=[] to_prod = [] T=None for key,value in fr.items(): T = key #res = list(list(itertools.product(*value[0]))[0]) from_prod.append(list( list(itertools.product(*value[0])))) to_prod.append(list (list(itertools.product (*value[1])))) self.sg.addMarkingBatch(T,from_prod,to_prod) #ENABLE self.sg.graph(ind) ind+=1; # # self.sg.addMarking(from_prod[i],to_prod[i],T) res.clear() TFenabled = [] #ENABLE #self.sg.graph() TM = {} #dict by tf transition inside slists of lists of start and end states #compile into our PN representation def toPN(self,mod): oldtonew = {} g = ig.Graph(0,directed=True) for node in mod.vs: if not node['$type'].find('Place') == -1 or not node['$type'].find('Transition') == -1: g.add_vertices(1) index = g.vcount()-1 oldtonew[node.index]=index g.vs[index]['name'] = node['name'] if not node['$type'].find('Place') == -1: g.vs[index]['type'] = 'P' g.vs[index]['nbTokens'] = node['nbTokens'] elif not node['$type'].find('Transition') == -1: g.vs[index]['type'] = 'T' g.vs[index]['fusion'] = node['fusion'] elif not node['$type'].find('P2T') == -1: node['type'] = 'P2T' elif not node['$type'].find('T2P') == -1: node['type'] = 'T2P' #Let's connect P2T = mod.vs.select(type_eq = 'P2T') T2P = mod.vs.select(type_eq = 'T2P') for p2t in P2T: to = mod.successors(p2t.index) fr = mod.predecessors(p2t.index) try: p2tid = g.get_eid(oldtonew[fr[0]],oldtonew[to[0]]) except: g.add_edges([(oldtonew[fr[0]],oldtonew[to[0]])]) p2tid = g.get_eid(oldtonew[fr[0]],oldtonew[to[0]]) g.es[p2tid]['weight'] = p2t['weight'] else: old = int(g.es[p2tid]['weight']) g.es[p2tid]['weight'] = old + int(p2t['weight']) for t2p in T2P: to = mod.successors(t2p.index) fr = mod.predecessors(t2p.index) try: t2pid = g.get_eid(oldtonew[fr[0]],oldtonew[to[0]]) except: g.add_edges([(oldtonew[fr[0]],oldtonew[to[0]])]) t2pid = g.get_eid(oldtonew[fr[0]],oldtonew[to[0]]) g.es[t2pid]['weight'] = t2p['weight'] else: old = int(g.es[t2pid]['weight']) g.es[t2pid]['weight'] = old + int(t2p['weight']) #dot graph of our petri net, not quite himesis. #self.graphPN('pn', g) return g def isReachableFlat(self,state,key=None): if not key: if self.modules.values()[0].reachable(state): return True else: return False else: if self.modules[key].reachable(state): return True else: return False def reachabilityTestModular(self): aa = 'a' bb = 'b' moda = {} modb = {} moda['a2'] = 1 #moda['a4'] = 4 #moda['a3'] = 1 modb['b1'] = 1 modb['b3'] = 1 statea = [] stateb = [] statea.append(moda) stateb.append(modb) if not self.isReachableFlat(statea,aa): print 'Modular state %s%s not reachable'%(statea,stateb) return False if not self.isReachableFlat(stateb,bb): print 'Modular state %s%s not reachable'%(statea,stateb) return False scca = self.modules[aa].reachableMod(statea) print 'A SCC of ancestors %s'%scca sccb = self.modules[bb].reachableMod(stateb) print 'B SCC of ancestors %s'%sccb result = list( list(itertools.product(scca,sccb))) for node in result: v = [] a = 'a-%d'%node[0] b = 'b-%d'%node[1] v.append(a) v.append(b) id = self.sg.statePresentReach(v) if not id == -1: print 'Modular state %s%s reachable'%(statea,stateb) return True print 'Modular state %s%s not reachable'%(statea,stateb) return False def reachabilityTestFlat(self): moda = {} modb = {} moda['a1'] = 1 moda['a4'] = 2 #moda['a3'] = 1 #modb['b3'] = 2 modb['b5'] = 3 state = [] state.append(moda) state.append(modb) if self.isReachableFlat(state): print 'Flat state %s reachable'%state else: print 'Flat state %s not reachable'%state def graph(self,key,g): vattr='' eattr = '' nodes = {} graph = pydot.Dot(key, graph_type='digraph') dateTag = datetime.datetime.now().strftime("%Y-%b-%d_%H-%M-%S") for v in g.vs: vattr +='(' i = len(v['M']) for key,value in v['M'].items(): vattr += '%s-%s'%(key,value) if not i-1 == 0: vattr+=',' i -=1 vattr +=')' nodes[v.index] = pydot.Node(vattr) graph.add_node(nodes[v.index]) vattr = '' for e in g.es: graph.add_edge(pydot.Edge(nodes[e.source],nodes[e.target],label=e['T'])) graph.write_svg('graphs/STATE%s%s.svg'%(key,dateTag)) #graph.write_png('graphs/STATE%s%s.png'%(key,dateTag)) #use this one to output your PN net in a svg graph to analyze structure #and verify that compilation from Himesis to PN went fine, since we collaps #repeated edges. def graphPN(self,key,g): vattr='' eattr = '' nodes = {} graph = pydot.Dot(key, graph_type='digraph') dateTag = datetime.datetime.now().strftime("%Y-%b-%d_%H-%M-%S") for v in g.vs: for at in v.attributes(): if not v[at] == None: vattr += '%s->%s\n'%(at,v[at]) nodes[v.index] = pydot.Node(vattr) graph.add_node(nodes[v.index]) vattr = '' for e in g.es: graph.add_edge(pydot.Edge(nodes[e.source],nodes[e.target],label=e['weight'])) graph.write_svg('graphs/PN%s%s.svg'%(key,dateTag)) '''