semanticdltl 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dltl/__init__.py ADDED
@@ -0,0 +1,39 @@
1
+ # -----------------------------------------------------------------------------
2
+ # SemanticDLTL: a DLTL model checker over finite traces
3
+ #
4
+ # Copyright (C) 2024-2026
5
+ # Joaquín Ezpeleta, Universidad de Zaragoza, Spain
6
+ # Javier Fabra, Universidad de Zaragoza, Spain
7
+ # María José Ibáñez, Universidad de La Rioja, Spain
8
+ # Contact: semanticdltl@unizar.es
9
+ #
10
+ # SPDX-License-Identifier: GPL-3.0-or-later
11
+ #
12
+ # This file is part of SemanticDLTL. It is free software: you can redistribute
13
+ # it and/or modify it under the terms of the GNU General Public License as
14
+ # published by the Free Software Foundation, either version 3 of the License,
15
+ # or (at your option) any later version. See the LICENSE file for details.
16
+ #
17
+ # Based on: J. M. Couvreur, J. Ezpeleta, "A Linear Temporal Logic Model
18
+ # Checking Method over Finite Words with Correlated Transition Attributes",
19
+ # SIMPDA 2017, LNBIP vol. 340, Springer, 2019.
20
+ # -----------------------------------------------------------------------------
21
+ """SemanticDLTL: a model checker for DLTL (data-aware LTL with freeze operators) over finite traces.
22
+
23
+ Typical use as a library::
24
+
25
+ from dltl import Log, Session
26
+
27
+ session = Session(Log.load("examples/sample"))
28
+ session.check_formula('F x.(b & "(x)x[V] == 10")')
29
+ session.execute("_WRITE")
30
+ """
31
+ __version__ = "1.1.0"
32
+
33
+ from dltl.evaluator import Evaluator, results_statistics # noqa: E402
34
+ from dltl.log import I_ATOM, I_POS, Log # noqa: E402
35
+ from dltl.parser import parse_formula # noqa: E402
36
+ from dltl.session import CheckSummary, Session # noqa: E402
37
+
38
+ __all__ = ["I_ATOM", "I_POS", "CheckSummary", "Evaluator", "Log", "Session", "parse_formula",
39
+ "results_statistics", "__version__"]
dltl/__main__.py ADDED
@@ -0,0 +1,25 @@
1
+ # -----------------------------------------------------------------------------
2
+ # SemanticDLTL: a DLTL model checker over finite traces
3
+ #
4
+ # Copyright (C) 2024-2026
5
+ # Joaquín Ezpeleta, Universidad de Zaragoza, Spain
6
+ # Javier Fabra, Universidad de Zaragoza, Spain
7
+ # María José Ibáñez, Universidad de La Rioja, Spain
8
+ # Contact: semanticdltl@unizar.es
9
+ #
10
+ # SPDX-License-Identifier: GPL-3.0-or-later
11
+ #
12
+ # This file is part of SemanticDLTL. It is free software: you can redistribute
13
+ # it and/or modify it under the terms of the GNU General Public License as
14
+ # published by the Free Software Foundation, either version 3 of the License,
15
+ # or (at your option) any later version. See the LICENSE file for details.
16
+ #
17
+ # Based on: J. M. Couvreur, J. Ezpeleta, "A Linear Temporal Logic Model
18
+ # Checking Method over Finite Words with Correlated Transition Attributes",
19
+ # SIMPDA 2017, LNBIP vol. 340, Springer, 2019.
20
+ # -----------------------------------------------------------------------------
21
+ import sys
22
+
23
+ from dltl.cli import main
24
+
25
+ sys.exit(main())
dltl/cli.py ADDED
@@ -0,0 +1,181 @@
1
+ # -----------------------------------------------------------------------------
2
+ # SemanticDLTL: a DLTL model checker over finite traces
3
+ #
4
+ # Copyright (C) 2024-2026
5
+ # Joaquín Ezpeleta, Universidad de Zaragoza, Spain
6
+ # Javier Fabra, Universidad de Zaragoza, Spain
7
+ # María José Ibáñez, Universidad de La Rioja, Spain
8
+ # Contact: semanticdltl@unizar.es
9
+ #
10
+ # SPDX-License-Identifier: GPL-3.0-or-later
11
+ #
12
+ # This file is part of SemanticDLTL. It is free software: you can redistribute
13
+ # it and/or modify it under the terms of the GNU General Public License as
14
+ # published by the Free Software Foundation, either version 3 of the License,
15
+ # or (at your option) any later version. See the LICENSE file for details.
16
+ #
17
+ # Based on: J. M. Couvreur, J. Ezpeleta, "A Linear Temporal Logic Model
18
+ # Checking Method over Finite Words with Correlated Transition Attributes",
19
+ # SIMPDA 2017, LNBIP vol. 340, Springer, 2019.
20
+ # -----------------------------------------------------------------------------
21
+ """
22
+ Command-line interface of the model checker.
23
+
24
+ dltl-mc --log-file <model> [--init-file F] [--formula-file F]
25
+ [--no-interactive] [--multi-line] [--propositions F.py]
26
+
27
+ The legacy ``key=value`` syntax is also accepted:
28
+
29
+ python MC.py log-file=<model> init-file=F formula-file=F interactive=false
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import argparse
34
+ import importlib.util
35
+ import re
36
+ import sys
37
+ import time
38
+ from collections.abc import Iterator
39
+ from types import ModuleType
40
+ from typing import TextIO
41
+
42
+ from dltl import __version__
43
+ from dltl.log import Log
44
+ from dltl.session import Session
45
+
46
+ PROMPT = "DLTL -> "
47
+ MULTI_LINE_END = '$'
48
+
49
+ _LEGACY_ARG = re.compile(
50
+ r'(log-file|init-file|formula-file|interactive|multi-line|propositions)=(.*)')
51
+
52
+
53
+ def normalize_legacy_argv(argv: list[str]) -> list[str]:
54
+ """Translate ``key=value`` arguments into their ``--key value`` form."""
55
+ out: list[str] = []
56
+ for arg in argv:
57
+ m = _LEGACY_ARG.fullmatch(arg)
58
+ if not m:
59
+ out.append(arg)
60
+ continue
61
+ key, value = m.groups()
62
+ if key == 'interactive':
63
+ out.append('--no-interactive' if value.strip().lower() == 'false' else '--interactive')
64
+ elif key == 'multi-line':
65
+ if value.strip().lower() == 'true':
66
+ out.append('--multi-line')
67
+ else:
68
+ out += [f'--{key}', value]
69
+ return out
70
+
71
+
72
+ def build_parser() -> argparse.ArgumentParser:
73
+ p = argparse.ArgumentParser(
74
+ prog="dltl-mc",
75
+ description="Model checker for DLTL formulas over the traces of a .mod file.",
76
+ epilog="The legacy syntax key=value (e.g. log-file=model interactive=false) "
77
+ "is also accepted.")
78
+ p.add_argument('--log-file', required=True, metavar='MODEL',
79
+ help="trace model, with or without the .mod suffix")
80
+ p.add_argument('--init-file', metavar='FILE',
81
+ help="file with commands executed before reading formulas")
82
+ p.add_argument('--formula-file', metavar='FILE',
83
+ help="file with the formulas to check (default: standard input)")
84
+ p.add_argument('--propositions', metavar='FILE.py',
85
+ help="Python file with the user propositions available as PROP "
86
+ "(default: dltl.propositions)")
87
+ p.add_argument('--interactive', action=argparse.BooleanOptionalAction, default=True,
88
+ help="show a prompt when reading formulas from standard input")
89
+ p.add_argument('--multi-line', action='store_true',
90
+ help=f"formulas may span several lines and end with '{MULTI_LINE_END}'")
91
+ p.add_argument('--version', action='version', version=f"%(prog)s {__version__}")
92
+ return p
93
+
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # input readers
97
+
98
+ def read_lines(stream: TextIO, prompt: str = "", multi_line: bool = False) -> Iterator[str]:
99
+ """Yield formulas/commands from ``stream``, one per line or, in multi-line
100
+ mode, joining lines until one containing ``$``."""
101
+ buffer: list[str] = []
102
+ while True:
103
+ if prompt:
104
+ print(prompt, end='', flush=True)
105
+ line = stream.readline()
106
+ if line == '': # EOF
107
+ if buffer:
108
+ yield ''.join(buffer)
109
+ if prompt:
110
+ print("EOF found when reading formula", file=sys.stderr)
111
+ return
112
+ line = line.rstrip('\n')
113
+ if not multi_line:
114
+ yield line
115
+ elif MULTI_LINE_END in line:
116
+ buffer.append(line.split(MULTI_LINE_END)[0])
117
+ yield ''.join(buffer)
118
+ buffer = []
119
+ else:
120
+ buffer.append(line)
121
+
122
+
123
+ def read_file(path: str, multi_line: bool = False) -> Iterator[str]:
124
+ with open(path) as f:
125
+ yield from read_lines(f, multi_line=multi_line)
126
+
127
+
128
+ def load_propositions(path: str | None) -> ModuleType:
129
+ if path is None:
130
+ from dltl import propositions
131
+ return propositions
132
+ spec = importlib.util.spec_from_file_location("user_propositions", path)
133
+ if spec is None or spec.loader is None:
134
+ raise ImportError(f"cannot load propositions from '{path}'")
135
+ module = importlib.util.module_from_spec(spec)
136
+ spec.loader.exec_module(module)
137
+ return module
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+
142
+ def main(argv: list[str] | None = None) -> int:
143
+ argv = sys.argv[1:] if argv is None else list(argv)
144
+ parser = build_parser()
145
+ if not argv:
146
+ parser.print_help(sys.stderr)
147
+ return 2
148
+ args = parser.parse_args(normalize_legacy_argv(argv))
149
+
150
+ for path in (args.init_file, args.formula_file, args.propositions):
151
+ if path is not None:
152
+ try:
153
+ open(path).close()
154
+ except OSError as e:
155
+ print(f"Error: cannot read '{path}': {e.strerror}", file=sys.stderr)
156
+ return 1
157
+
158
+ try:
159
+ props = load_propositions(args.propositions)
160
+ start = time.time()
161
+ log = Log.load(args.log_file)
162
+ loading_time = time.time() - start
163
+ except (OSError, ValueError, ImportError) as e:
164
+ print(f"Error: {e}", file=sys.stderr)
165
+ return 1
166
+
167
+ session = Session(log, props)
168
+ if args.interactive:
169
+ print(f"Loading time: {loading_time:.4f} seconds")
170
+ print(log.info())
171
+
172
+ if args.init_file and not session.run(read_file(args.init_file)):
173
+ return 0
174
+
175
+ if args.formula_file:
176
+ lines = read_file(args.formula_file, multi_line=args.multi_line)
177
+ else:
178
+ prompt = PROMPT if args.interactive else ""
179
+ lines = read_lines(sys.stdin, prompt=prompt, multi_line=args.multi_line)
180
+ session.run(lines)
181
+ return 0
dltl/evaluator.py ADDED
@@ -0,0 +1,393 @@
1
+ # -----------------------------------------------------------------------------
2
+ # SemanticDLTL: a DLTL model checker over finite traces
3
+ #
4
+ # Copyright (C) 2024-2026
5
+ # Joaquín Ezpeleta, Universidad de Zaragoza, Spain
6
+ # Javier Fabra, Universidad de Zaragoza, Spain
7
+ # María José Ibáñez, Universidad de La Rioja, Spain
8
+ # Contact: semanticdltl@unizar.es
9
+ #
10
+ # SPDX-License-Identifier: GPL-3.0-or-later
11
+ #
12
+ # This file is part of SemanticDLTL. It is free software: you can redistribute
13
+ # it and/or modify it under the terms of the GNU General Public License as
14
+ # published by the Free Software Foundation, either version 3 of the License,
15
+ # or (at your option) any later version. See the LICENSE file for details.
16
+ #
17
+ # Based on: J. M. Couvreur, J. Ezpeleta, "A Linear Temporal Logic Model
18
+ # Checking Method over Finite Words with Correlated Transition Attributes",
19
+ # SIMPDA 2017, LNBIP vol. 340, Springer, 2019.
20
+ # -----------------------------------------------------------------------------
21
+ """
22
+ Evaluation of DLTL formulas over a finite trace.
23
+
24
+ The evaluation is bottom-up and per trace: ``Evaluator.eval_formula`` returns a
25
+ list with one (partially evaluated) node per event, the node at position ``i``
26
+ being the value of the formula at event ``i``. When a formula contains no free
27
+ freeze variables that node is simply ``TRUE()`` or ``FALSE()``.
28
+
29
+ Model checker based on the DLTL logic and algorithm in:
30
+ J. M. Couvreur, J. Ezpeleta, "A Linear Temporal Logic Model Checking
31
+ Method over Finite Words with Correlated Transition Attributes",
32
+ SIMPDA 2017, LNBIP vol. 340, Springer, 2019.
33
+ """
34
+ from __future__ import annotations
35
+
36
+ import re
37
+ import sys
38
+ import traceback
39
+ from types import ModuleType
40
+ from typing import Any
41
+
42
+ from dltl.formula import AND, FALSE, NOT, OR, TRUE, e1, e2, is_false, is_true, t, v
43
+ from dltl.log import I_ATOM, I_POS
44
+
45
+ # Name under which the current trace is visible inside data expressions after
46
+ # the freeze variables have been substituted ("x[V]" -> "THE_TRACE[3][V]").
47
+ TRACE_NAME = 'THE_TRACE'
48
+
49
+ _UNARY_OPS = frozenset({'atom', '!', 'X', 'G', 'F', 'Y', 'H', 'O'})
50
+ _BINARY_OPS = frozenset({'&', '|', 'U', 'S'})
51
+
52
+
53
+ class Evaluator:
54
+ """Evaluates formulas over traces of a given model.
55
+
56
+ ``column_index`` maps attribute names to positions in the event tuple (see
57
+ :class:`dltl.log.Log`), and ``props`` is the module exposed as ``PROP``
58
+ inside data expressions. Both, together with the current trace, form the
59
+ namespace in which data expressions are ``eval``'d.
60
+ """
61
+
62
+ def __init__(self, column_index: dict[str, int], props: ModuleType | None = None):
63
+ if props is None:
64
+ from dltl import propositions as props
65
+ self._ns: dict[str, Any] = {**column_index,
66
+ 'COL': dict(column_index),
67
+ 'I_POS': I_POS,
68
+ 'I_ATOM': I_ATOM,
69
+ 'PROP': props,
70
+ TRACE_NAME: ()}
71
+ self._cases = {
72
+ 'X': self.eval_X, 'U': self.eval_U, 'F': self.eval_F, 'G': self.eval_G,
73
+ 'Y': self.eval_Y, 'S': self.eval_S, 'O': self.eval_O, 'H': self.eval_H,
74
+ 'True': self.eval_True_False, 'False': self.eval_True_False,
75
+ '!': self.eval_Not, '&': self.eval_AND, '|': self.eval_OR,
76
+ 'fvar': self.eval_fvar, 'exp': self.eval_exp, 'atom': self.eval_atom,
77
+ }
78
+
79
+ def add_module(self, name: str, module: ModuleType) -> None:
80
+ """Make ``module`` available as ``name`` inside data expressions (``_LOAD``)."""
81
+ self._ns[name] = module
82
+
83
+ # ------------------------------------------------------------------
84
+ def eval_formula(self, exp, trace) -> list:
85
+ """Evaluate ``exp`` at every event of ``trace``; one node per event."""
86
+ self._ns[TRACE_NAME] = trace
87
+ try:
88
+ return self._cases[exp[t]](exp, trace)
89
+ except Exception as e: # noqa: BLE001 - report and keep the session alive
90
+ print(f"An error occurred: {e}", file=sys.stderr)
91
+ print("\n--- Full Traceback ---", file=sys.stderr)
92
+ traceback.print_exc(file=sys.stderr)
93
+ print("returning FALSE value\n", file=sys.stderr)
94
+ print("----------------------\n", file=sys.stderr)
95
+ return [FALSE() for _ in trace]
96
+
97
+ def _eval_data(self, expression: str):
98
+ return eval(expression, self._ns) # noqa: S307 - data expressions are user code by design
99
+
100
+ # ------------------------------------------------------------------
101
+ def replace(self, traza, i, exp, var):
102
+ """Substitute freeze variable ``var`` by event ``i`` in ``exp``.
103
+
104
+ ``var[#]`` becomes the (1-based) position of the event and any other
105
+ occurrence of ``var`` becomes ``THE_TRACE[i]``. Data expressions left
106
+ without free variables are evaluated on the spot.
107
+ """
108
+ # everything that depends only on (i, var) is computed once per call
109
+ var_set = frozenset({var})
110
+ position = str(i + 1) # first event position is 1, not 0
111
+ trace_ref = f"{TRACE_NAME}[{i}]"
112
+ var_hash = f"{var}[#]"
113
+ pattern = re.compile(rf'\b{re.escape(var)}\b')
114
+
115
+ stack = [(exp, False)]
116
+ resultMap = {}
117
+
118
+ while stack:
119
+ theForm, visited = stack.pop()
120
+ form_id = id(theForm)
121
+ if form_id in resultMap:
122
+ continue
123
+
124
+ vars = theForm[v]
125
+ op = theForm[t]
126
+
127
+ if var not in vars or is_false(theForm) or is_true(theForm):
128
+ resultMap[form_id] = theForm
129
+ continue
130
+
131
+ if visited: # descendant results are already in resultMap
132
+ new_vars = vars - var_set
133
+ if op in _UNARY_OPS:
134
+ resultMap[form_id] = [new_vars, op, resultMap[id(theForm[e1])]]
135
+ elif op in _BINARY_OPS:
136
+ resultMap[form_id] = [new_vars, op,
137
+ resultMap[id(theForm[e1])],
138
+ resultMap[id(theForm[e2])]]
139
+ elif op == 'fvar':
140
+ resultMap[form_id] = [new_vars, op, theForm[e1], resultMap[id(theForm[e2])]]
141
+ elif op == 'exp':
142
+ newFormula = theForm[e1].replace(var_hash, position)
143
+ newFormula = pattern.sub(trace_ref, newFormula)
144
+ if not new_vars:
145
+ resultMap[form_id] = [new_vars, str(self._eval_data(newFormula))]
146
+ else:
147
+ resultMap[form_id] = [new_vars, op, newFormula]
148
+ else:
149
+ # postorder: reinsert with visited=True and process descendants
150
+ stack.append((theForm, True))
151
+ if op in _UNARY_OPS:
152
+ stack.append((theForm[e1], False))
153
+ elif op in _BINARY_OPS:
154
+ stack.append((theForm[e1], False))
155
+ stack.append((theForm[e2], False))
156
+ elif op == 'fvar':
157
+ stack.append((theForm[e2], False))
158
+
159
+ return resultMap[id(exp)]
160
+
161
+ # ------------------------------------------------------------------
162
+ def eval_formula_in_event(self, exp, i, traza):
163
+ """Simplify ``exp`` at event ``i`` as far as its free variables allow."""
164
+ # (exp, False): children not yet processed
165
+ # (exp, True): children already resolved and their value stored in resultMap
166
+ stack = [(exp, False)]
167
+ resultMap = {}
168
+
169
+ while stack:
170
+ theForm, visited = stack.pop()
171
+
172
+ if id(theForm) in resultMap:
173
+ continue
174
+
175
+ vars = theForm[v]
176
+ op = theForm[t]
177
+
178
+ # true or false, or non-evaluable because of the vars
179
+ if len(vars) != 0 or is_false(theForm) or is_true(theForm):
180
+ resultMap[id(theForm)] = theForm
181
+ continue
182
+
183
+ if visited:
184
+ if op == 'exp':
185
+ val = theForm[e1]
186
+ try:
187
+ val_str = str(self._eval_data(str(val)))
188
+ except Exception as ex: # noqa: BLE001
189
+ print(f"Eval error in 'exp': {ex}", file=sys.stderr)
190
+ val_str = "False"
191
+ resultMap[id(theForm)] = [set(), val_str]
192
+ elif op == 'atom':
193
+ resultMap[id(theForm)] = TRUE() if theForm[e1] in traza[i][I_ATOM] else FALSE()
194
+ elif op == '&':
195
+ ev1 = resultMap[id(theForm[e1])]
196
+ ev2 = resultMap[id(theForm[e2])]
197
+ if is_false(ev1) or is_false(ev2):
198
+ resultMap[id(theForm)] = FALSE()
199
+ elif is_true(ev1) and is_true(ev2):
200
+ resultMap[id(theForm)] = TRUE()
201
+ elif is_true(ev1):
202
+ resultMap[id(theForm)] = ev2
203
+ elif is_true(ev2):
204
+ resultMap[id(theForm)] = ev1
205
+ else:
206
+ resultMap[id(theForm)] = [ev1[0] | ev2[0], '&', ev1, ev2]
207
+ elif op == '|':
208
+ ev1 = resultMap[id(theForm[e1])]
209
+ ev2 = resultMap[id(theForm[e2])]
210
+ if is_true(ev1) or is_true(ev2):
211
+ resultMap[id(theForm)] = TRUE()
212
+ elif is_false(ev1):
213
+ resultMap[id(theForm)] = ev2
214
+ elif is_false(ev2):
215
+ resultMap[id(theForm)] = ev1
216
+ else:
217
+ resultMap[id(theForm)] = [ev1[0] | ev2[0], '|', ev1, ev2]
218
+ elif op == '!':
219
+ ev = resultMap[id(theForm[e1])]
220
+ if len(ev[0]) == 0:
221
+ resultMap[id(theForm)] = FALSE() if ev[1] == 'True' else TRUE()
222
+ else:
223
+ resultMap[id(theForm)] = [ev[0], '!', ev]
224
+ elif op == 'fvar':
225
+ new_exp = self.replace(traza, i, theForm[e2], theForm[e1])
226
+ stack.append((new_exp, False))
227
+ stack.append((theForm, True)) # cannot be resolved yet
228
+ else:
229
+ stack.append((theForm, True))
230
+ # preorder: descendants first
231
+ if op in {'&', '|'}:
232
+ stack.append((theForm[e2], False))
233
+ stack.append((theForm[e1], False))
234
+ elif op == '!':
235
+ stack.append((theForm[e1], False))
236
+ # 'fvar' is handled in the post-processing step; 'atom'/'exp' have no children
237
+
238
+ return resultMap[id(exp)]
239
+
240
+ # ------------------------------------------------------------------
241
+ # future operators
242
+ def eval_X(self, exp, traza):
243
+ n = len(traza)
244
+ res = [None] * n
245
+ # "X false" is the idiom for "last event": it holds only there
246
+ res[n - 1] = TRUE() if is_false(exp[e1]) else FALSE()
247
+ eval_exp = self.eval_formula(exp[e1], traza)
248
+ for i in reversed(range(n - 1)):
249
+ res[i] = eval_exp[i + 1]
250
+ return res
251
+
252
+ def eval_U(self, exp, traza):
253
+ n = len(traza)
254
+ res = [None] * n
255
+ res_f = self.eval_formula(exp[e1], traza)
256
+ res_g = self.eval_formula(exp[e2], traza)
257
+ res[n - 1] = res_g[n - 1]
258
+ for i in reversed(range(n - 1)):
259
+ res[i] = self.eval_formula_in_event(
260
+ OR(res_g[i], AND(res_f[i], res[i + 1])), i, traza)
261
+ return res
262
+
263
+ def eval_F(self, exp, traza):
264
+ n = len(traza)
265
+ res = [None] * n
266
+ res_parcial = self.eval_formula(exp[e1], traza)
267
+ res[n - 1] = self.eval_formula_in_event(res_parcial[n - 1], n - 1, traza)
268
+ already_true = is_true(res[n - 1])
269
+ for i in reversed(range(n - 1)):
270
+ if already_true: # F f holds at i+1, hence at every earlier event
271
+ res[i] = TRUE()
272
+ else:
273
+ res[i] = self.eval_formula_in_event(OR(res_parcial[i], res[i + 1]), i, traza)
274
+ already_true = is_true(res[i])
275
+ return res
276
+
277
+ def eval_G(self, exp, traza):
278
+ n = len(traza)
279
+ res = [None] * n
280
+ res_parcial = self.eval_formula(exp[e1], traza)
281
+ res[n - 1] = self.eval_formula_in_event(res_parcial[n - 1], n - 1, traza)
282
+ already_false = is_false(res[n - 1])
283
+ for i in reversed(range(n - 1)):
284
+ if already_false: # G f fails at i+1, hence at every earlier event
285
+ res[i] = FALSE()
286
+ else:
287
+ res[i] = self.eval_formula_in_event(AND(res_parcial[i], res[i + 1]), i, traza)
288
+ already_false = is_false(res[i])
289
+ return res
290
+
291
+ # past operators
292
+ def eval_Y(self, exp, traza):
293
+ n = len(traza)
294
+ res = [None] * n
295
+ # "Y false" is the idiom for "first event": it holds only there
296
+ res[0] = TRUE() if is_false(exp[e1]) else FALSE()
297
+ res_exp = self.eval_formula(exp[e1], traza)
298
+ for i in range(1, n):
299
+ res[i] = res_exp[i - 1]
300
+ return res
301
+
302
+ def eval_S(self, exp, traza):
303
+ n = len(traza)
304
+ res = [None] * n
305
+ res_f = self.eval_formula(exp[e1], traza)
306
+ res_g = self.eval_formula(exp[e2], traza)
307
+ res[0] = res_g[0]
308
+ for i in range(1, n):
309
+ res[i] = self.eval_formula_in_event(
310
+ OR(res_g[i], AND(res_f[i], res[i - 1])), i, traza)
311
+ return res
312
+
313
+ def eval_O(self, exp, traza):
314
+ n = len(traza)
315
+ res = [None] * n
316
+ res_f = self.eval_formula(exp[e1], traza)
317
+ res[0] = self.eval_formula_in_event(res_f[0], 0, traza)
318
+ already_true = is_true(res[0])
319
+ for i in range(1, n):
320
+ if already_true: # O f holds at i-1, hence at every later event
321
+ res[i] = TRUE()
322
+ else:
323
+ res[i] = self.eval_formula_in_event(OR(res_f[i], res[i - 1]), i, traza)
324
+ already_true = is_true(res[i])
325
+ return res
326
+
327
+ def eval_H(self, exp, traza):
328
+ n = len(traza)
329
+ res = [None] * n
330
+ res_f = self.eval_formula(exp[e1], traza)
331
+ res[0] = self.eval_formula_in_event(res_f[0], 0, traza)
332
+ already_false = is_false(res[0])
333
+ for i in range(1, n):
334
+ if already_false: # H f fails at i-1, hence at every later event
335
+ res[i] = FALSE()
336
+ else:
337
+ res[i] = self.eval_formula_in_event(AND(res_f[i], res[i - 1]), i, traza)
338
+ already_false = is_false(res[i])
339
+ return res
340
+
341
+ # propositional operators and leaves
342
+ def eval_True_False(self, exp, traza):
343
+ return [[set(), exp[t]] for _ in traza]
344
+
345
+ def eval_Not(self, exp, traza):
346
+ res_f = self.eval_formula(exp[e1], traza)
347
+ return [self.eval_formula_in_event(NOT(res_f[i]), i, traza) for i in range(len(traza))]
348
+
349
+ def eval_AND(self, exp, traza):
350
+ res_f1 = self.eval_formula(exp[e1], traza)
351
+ res_f2 = self.eval_formula(exp[e2], traza)
352
+ return [self.eval_formula_in_event(
353
+ [res_f1[i][0] | res_f2[i][0], '&', res_f1[i], res_f2[i]], i, traza)
354
+ for i in range(len(traza))]
355
+
356
+ def eval_OR(self, exp, traza):
357
+ res_f1 = self.eval_formula(exp[e1], traza)
358
+ res_f2 = self.eval_formula(exp[e2], traza)
359
+ return [self.eval_formula_in_event(
360
+ [res_f1[i][0] | res_f2[i][0], '|', res_f1[i], res_f2[i]], i, traza)
361
+ for i in range(len(traza))]
362
+
363
+ def eval_fvar(self, exp, traza):
364
+ n = len(traza)
365
+ freezeVar = exp[e1]
366
+ newRow = self.eval_formula(exp[e2], traza)
367
+ newRow = [self.replace(newRow, i, newRow[i], freezeVar) for i in range(n)]
368
+ return [self.eval_formula_in_event(newRow[i], i, traza) for i in range(n)]
369
+
370
+ def eval_exp(self, exp, traza):
371
+ return [self.eval_formula_in_event(exp, i, traza) for i in range(len(traza))]
372
+
373
+ def eval_atom(self, exp, traza):
374
+ return [TRUE() if exp[e1] in event[I_ATOM] else FALSE() for event in traza]
375
+
376
+
377
+ # ---------------------------------------------------------------------------
378
+
379
+ def results_statistics(res) -> tuple[int, int, int, float]:
380
+ """Summarise the per-event results of a formula over one trace.
381
+
382
+ Returns ``(holds, true_count, false_count, ratio)`` where ``holds`` is 1
383
+ when the formula is true at the first event (the classic model checking
384
+ answer) and ``ratio`` is the fraction of events where it is true.
385
+
386
+ Raises ``ValueError`` if some event was left with an unresolved node.
387
+ """
388
+ n = len(res)
389
+ trueCount = sum(1 for r in res if is_true(r))
390
+ falseCount = sum(1 for r in res if is_false(r))
391
+ if trueCount + falseCount != n:
392
+ raise ValueError(f"unresolved evaluation: {trueCount + falseCount} != {n}")
393
+ return (1 if is_true(res[0]) else 0), trueCount, falseCount, trueCount / n