Colin THOMAS

add CTL and FairCTL model checker

1 +from ddd import ddd, sdd, shom
2 +from tl import parse, Phi
3 +from functools import reduce, lru_cache
4 +
5 +def fixpoint(fonction, start):
6 + current = start
7 + previous = None
8 + while previous != current:
9 + previous = current
10 + current = fonction(current)
11 + return current
12 +
13 +
14 +
15 +####################### CTL #######################
16 +
17 +class CTL_model_checker(object):
18 + def __init__ (self, universe, pred):
19 + """
20 + Input :
21 + - universe is a sdd representing the state space (for example v.g.reachable)
22 + - pred is a shom representing the inverse of succ (for exemple v.g.m.pred())
23 + """
24 + # TODO : mettre en cache les output de phi2sdd ?
25 + assert isinstance(universe, sdd), "universe must be of type sdd"
26 + assert isinstance(pred, shom), "pred must be of type shom"
27 +
28 + self.variables = next(iter(universe))[0].vars() # c'est tres sale mais je trouve pas d'autre solution pour l'instant
29 +
30 + self.CTL_True = universe
31 + self.CTL_False = sdd.empty()
32 + self.neg = lambda phi : self.CTL_True - phi
33 + self.gfp = lambda fonction : fixpoint(fonction, self.CTL_True)
34 + self.lfp = lambda fonction : fixpoint(fonction, self.CTL_False)
35 +
36 + self.EX = pred & universe
37 + self.EF = lambda phi : self.lfp(lambda Z : phi | self.EX(Z))
38 + self.EG = lambda phi : self.gfp(lambda Z : phi & self.EX(Z))
39 + self.EU = lambda phi1, phi2 : self.lfp(lambda Z : phi2 | (phi1 & self.EX(Z)))
40 + self.ER = lambda phi1, phi2 : self.gfp(lambda Z : phi2 & (phi1 | self.EX(Z)))
41 + self.EW = lambda phi1, phi2 : self.gfp(lambda Z : phi2 | (phi1 & self.EX(Z)))
42 +
43 + # definition of universal operators in term of fix points
44 + self.AX = lambda phi : self.EX(phi) - self.EX(neg(phi))
45 + self.AF = lambda phi : self.lfp(lambda Z : phi | self.AX(Z))
46 + self.AG = lambda phi : self.gfp(lambda Z : phi & self.AX(Z))
47 + self.AU = lambda phi1, phi2 : self.lfp(lambda Z : phi2 | (phi1 & self.AX(Z)))
48 + self.AR = lambda phi1, phi2 : self.gfp(lambda Z : phi2 & (phi1 | self.AX(Z)))
49 + self.AW = lambda phi1, phi2 : self.gfp(lambda Z : phi2 | (phi1 & self.AX(Z)))
50 +
51 + # redefinition of universal operators in term of negation of existential ones
52 + self.AX = lambda phi : self.neg(self.EX(self.neg(phi))) # /!\ if there is no path from s, then s |= AX(phi) because there is no path that falsifies phi
53 + self.AF = lambda phi : self.neg(self.EG(self.neg(phi)))
54 + self.AG = lambda phi : self.neg(self.EF(self.neg(phi)))
55 + self.AU = lambda phi1, phi2 : self.neg(self.EU(self.neg(phi2),self.neg(phi1) & self.neg(phi2)) | self.EG(self.neg(phi2)))
56 +
57 + self.unarymod = {"EX" : self.EX, "EF" : self.EF, "EG" : self.EG, "AX" : self.AX, "AF" : self.AF, "AG" : self.AG}
58 + self.binarymod = {"EU" : self.EU, "ER" : self.ER, "EW" : self.EW, "AU" : self.AU, "AR" : self.AR, "AW" : self.AW}
59 +
60 + @lru_cache(maxsize=None) # si la version de python etait 3.9 on pourrait utiliser functools.cache directement
61 + def atom(self, var):
62 + """
63 +
64 + """
65 + assert var in self.variables, var + " is not a variable"
66 + d = ddd.one()
67 + for v in reversed(self.variables):
68 + if v == var :
69 + d = ddd.from_range(var, 1, 1, d)
70 + else :
71 + d = ddd.from_range(v, 0, 1, d)
72 + return sdd.mkz(d) & self.CTL_True
73 +
74 +
75 + # recursive function that builds the sdd along the syntax tree (bottom-up, going-up from the tree leaves)
76 + def _phi2sdd(self, phi):
77 + if phi.kind == 'bool':
78 + assert isinstance(phi.value, bool), repr(phi) + " is of kind bool but its value is not a boolean"
79 + if phi.value:
80 + return self.CTL_True
81 + else:
82 + return self.CTL_False
83 + elif phi.kind == 'name':
84 + return self.atom(phi.value)
85 + elif phi.kind == 'not':
86 + return self.neg(self._phi2sdd(phi.children[0]))
87 + elif phi.kind =='and':
88 + return reduce(sdd.__and__, [self._phi2sdd(child) for child in phi.children], self.CTL_True)
89 + elif phi.kind =='or':
90 + return reduce(sdd.__or__, [self._phi2sdd(child) for child in phi.children], self.CTL_False)
91 + elif phi.kind in self.unarymod:
92 + return self.unarymod[phi.kind](self._phi2sdd(phi.children[0]))
93 + elif phi.kind in self.binarymod:
94 + return self.binarymod[phi.kind](self._phi2sdd(phi.children[0]), self._phi2sdd(phi.children[1]))
95 + else:
96 + raise ValueError(repr(phi) + "is not a CTL sub formula")
97 +
98 +
99 + def check(self, formula):
100 + assert isinstance(formula , str) or isinstance(formula , Phi), "The formula given in input must be a string or a parsed formula"
101 + if isinstance(formula , str):
102 + formula = parse(formula).ctl()
103 + return self._phi2sdd(formula)
104 +
105 +
106 +
107 +####################### Fair CTL #######################
108 +
109 +class FairCTL_model_checker(CTL_model_checker):
110 + def __init__ (self, universe, pred, fairness):
111 + """
112 + Input :
113 + - universe is a sdd representing the state space (for example v.g.reachable)
114 + - pred is a shom representing the inverse of succ (for exemple v.g.m.pred())
115 + - fairness is a list of CTL formulae (or sdd) to be used as fairness constraints (the fair trajectory must satisfy and[GF f for f in fairness])
116 + """
117 + super().__init__(universe, pred)
118 + if isinstance(fairness, list):
119 + pass
120 + elif isinstance(fairness, str) or isinstance(fairness, sdd) :
121 + fairness = [fairness]
122 + else:
123 + raise TypeError("fairness must be a list or a string expressing a CTL formula")
124 + if fairness == []:
125 + print("The list of fairness constraints is empty, you should used the CTL_model_checker instead")
126 +
127 + def fairness_preprocess(f):
128 + if isinstance(f, str):
129 + return CTL_model_checker(universe, pred).check(f)
130 + elif isinstance(f, sdd):
131 + return f
132 + else:
133 + raise TypeError("Fairness constraints must be CTL formulae or SDD")
134 +
135 + self.fairness = [fairness_preprocess(f) for f in fairness]
136 +
137 + # see Symbolic Model Checking, McMillan 1993, section 6.4 or Symbolic model checking: 1020 states and beyond, Burch et al 1992, section 6.2
138 + self.EG_fair = lambda phi : self.gfp(lambda Z : phi & self.EX(reduce(sdd.__and__ , [self.EU(Z, Z & f) for f in self.fairness], self.CTL_True)))
139 + self.EG_fair_True = self.EG_fair(self.CTL_True)
140 + self.EX_fair = lambda phi : self.EX(phi & self.EG_fair_True)
141 + self.EF_fair = lambda phi : self.EF(phi & self.EG_fair_True)
142 + self.EU_fair = lambda phi1, phi2 : self.EU(phi1, phi2 & self.EG_fair_True)
143 +
144 + # definition of universal operators as negation of existential ones (not sure of myself here)
145 + self.AX_fair = lambda phi : self.neg(self.EX_fair(self.neg(phi)))
146 + self.AF_fair = lambda phi : self.neg(self.EG_fair(self.neg(phi)))
147 + self.AG_fair = lambda phi : self.neg(self.EF_fair(self.neg(phi)))
148 + self.AU_fair = lambda phi1, phi2 : self.neg(self.EU_fair(self.neg(phi2), self.neg(phi1) & self.neg(phi2)) | self.EG_fair(self.neg(phi2)))
149 +
150 + self.unarymod = {"EX" : self.EX_fair, "EF" : self.EF_fair, "EG" : self.EG_fair, "AX" : self.AX_fair, "AF" : self.AF_fair, "AG" : self.AG_fair}
151 + self.binarymod = {"EU" : self.EU_fair, "AU" : self.AU_fair}#, "ER" : ER, "EW" : EW, "AR" : AR, "AW" : AW}
152 +
153 +
154 +
155 +####################### ACTL #######################
156 +
157 +class ACTL_model_checker(CTL_model_checker):
158 + pass
159 +
160 +
161 +
162 +####################### ARCTL #######################
163 +
164 +class ARCTL_model_checker(CTL_model_checker):
165 + pass