Commit d49a15d4 authored by Michele's avatar Michele

moved to 2 sets observation table only, fixed bug: quiescence was not included in OutputPurpose

parent a84b8477
......@@ -10,6 +10,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Counterexample handling
- Algorithms for double sets in the table
### Changed
- Moved old algorithm for learning to `oraclelearning` and `oracleobservationtable`
## [v0.1.0] - 2015-09-29
### Added
- First release, tag v0.1.0.
......
This diff is collapsed.
This diff is collapsed.
from .observationtable import Table, DoubleSetTable
from .observationtable import Table
import random
from systems.implementations import SuspensionAutomaton
import os, inspect
import helpers.graphhelper as gh
import logging
import helpers.traces as th
from systems.iopurpose import InputPurpose, OutputPurpose
class LearningAlgorithm:
def __init__(self, teacher, oracle, tester, tablePreciseness = 1000,
modelPreciseness = 0.1, closeStrategy = None,
printPath = None, maxLoops=10, logger=None,):
modelPreciseness = 0.1, closeStrategy = None,
printPath = None, maxLoops=10, logger=None, outputPurpose=None,
inputPurpose=None):
self._logger = logger or logging.getLogger(__name__)
self._inputPurpose = inputPurpose
self._outputPurpose = outputPurpose
if self._inputPurpose == None:
self._inputPurpose = InputPurpose(teacher.getInputAlphabet().copy())
if self._outputPurpose == None:
self._outputPurpose = OutputPurpose(teacher.getOutputAlphabet().union(set((teacher.getQuiescence(),))))
self._teacher = teacher
self._oracle = oracle
self.tester = tester
self._tablePreciseness = tablePreciseness
self._modelPreciseness = modelPreciseness
self._table = Table(self._teacher.getInputAlphabet().copy(),
self._teacher.getOutputAlphabet().copy(),
self._teacher.getQuiescence(),
closeStrategy, logger=self._logger)
self._table = Table(teacher.getInputAlphabet().copy(),
teacher.getOutputAlphabet().copy(),
teacher.getQuiescence(),
closeStrategy, logger=logger,
outputPurpose=self._outputPurpose,
inputPurpose=self._inputPurpose)
# Maximum number of loops with no effect on hPlus model
self._noEffectLimit = maxLoops
......@@ -372,9 +384,15 @@ class LearningAlgorithm:
return hyp
# Get all labels enabled after row
# check input enabledness
def _getAllLabels(self, row):
allLabels = self._teacher.getInputAlphabet().union(
self._teacher.getOutputAlphabet(),
enabledInputs = self._teacher.getInputAlphabet()
enabledOutputs = self._teacher.getOutputAlphabet()
if self._inputPurpose != None:
enabledInputs = self._inputPurpose.getEnabled(row)
if self._outputPurpose != None:
enabledOutputs = self._outputPurpose.getEnabled(row)
allLabels = enabledInputs.union(enabledOutputs,
set((self._teacher.getQuiescence(),)))
return allLabels
......@@ -534,36 +552,3 @@ class LearningAlgorithm:
if self._logger.isEnabledFor(logging.DEBUG):
self._table.printTable(prefix="_final_")
return (self._hMinus, self._hPlus)
class LearningAlgorithm2Sets(LearningAlgorithm):
def __init__(self, teacher, oracle, tester, tablePreciseness = 1000,
modelPreciseness = 0.1, closeStrategy = None,
printPath = None, maxLoops=10, logger=None, outputPurpose=None,
inputPurpose=None):
LearningAlgorithm.__init__(self, teacher, oracle, tester,
tablePreciseness,
modelPreciseness, closeStrategy,
printPath, maxLoops, logger)
self._table = DoubleSetTable(teacher.getInputAlphabet().copy(),
teacher.getOutputAlphabet().copy(),
teacher.getQuiescence(),
closeStrategy, logger=logger,
outputPurpose=outputPurpose,
inputPurpose=inputPurpose)
self._inputPurpose = inputPurpose
self._outputPurpose = outputPurpose
# Get all labels enabled after row
# check input enabledness
def _getAllLabels(self, row):
enabledInputs = self._teacher.getInputAlphabet()
enabledOutputs = self._teacher.getOutputAlphabet()
if self._inputPurpose != None:
enabledInputs = self._inputPurpose.getEnabled(row)
if self._outputPurpose != None:
enabledOutputs = self._outputPurpose.getEnabled(row)
allLabels = enabledInputs.union(enabledOutputs,
set((self._teacher.getQuiescence(),)))
return allLabels
......@@ -6,10 +6,16 @@ import helpers.traces as th
class Table:
def __init__(self, inputs, outputs, quiescence, closeStrategy = None, logger=None):
def __init__(self, inputs, outputs, quiescence,
closeStrategy = None, logger=None, outputPurpose=None,
inputPurpose=None):
self._logger = logger or logging.getLogger(__name__)
# allows underspecification in inputs and outputs
self._outputPurpose = outputPurpose
self._inputPurpose = inputPurpose
self._inputs = inputs
self._outputs = outputs
self._quiescence = quiescence
......@@ -25,18 +31,18 @@ class Table:
# _rows also contains empty trace
self._rows = set()
self._rows.add(())
for label in self._inputs:
for label in self._inputPurpose.getEnabled(()):
self._rows.add(tuple(label))
# Define the top part of the table (for closedness)
self._rowsInS = set()
# Empty trace is always in S
self._rowsInS.add(())
# _entries is a dictionary (tuple of actions) -> (set(outputs), Bool)
# _entries is a dictionary (tuple of actions) -> (set(outputs), set(outputs))
# start with the empty sequence of actions and one letter extensions
self._entries = {():(set(),False)}
for label in self._inputs:
self._entries[tuple(label)] = (set(),False)
self._entries = {():(set(),self._possibleOutputs(()))}
for label in self._possibleInputs(()):
self._entries[tuple(label)] = (set(),self._possibleOutputs(tuple(label)))
# Calculate the preciseness of the table
def preciseness(self):
......@@ -144,7 +150,7 @@ class Table:
def addOneLetterExtensions(self, rows):
modified = False
for row in rows:
for input in self._inputs:
for input in self._possibleInputs(row):
if self.addOneLetterExtension(row, input):
modified = True
outputs, observation = self._entries[row]
......@@ -269,7 +275,9 @@ class Table:
# If we are in chaos_quiescence only quiescence is enabled
enabledRow2.add(self._quiescence)
else:
enabledRow2 = self._entries[current[1]][0].union(self._inputs)
# input enabledness
enabledInputs = self._inputPurpose.getEnabled(current[1])
enabledRow2 = self._entries[current[1]][0].union(enabledInputs)
for label in enabledRow2:
enabledRow1 = set()
if current[0] == "chaos_quiescence":
......@@ -277,7 +285,8 @@ class Table:
# enabled
enabledRow1.add(self._quiescence)
else:
enabledRow1 = self._entries[current[0]][0].union(self._inputs)
enabledInputs = self._inputPurpose.getEnabled(current[0])
enabledRow1 = self._entries[current[0]][0].union(enabledInputs)
if label not in enabledRow1:
suffixes = set()
suffixes.add(current[2])
......@@ -296,10 +305,10 @@ class Table:
except StopIteration as error:
# current[0] + (label,) might not have
# corresponding row in S. This is the case when
# the first entry is (set(), false).
# the first entry is emptyEntry.
# In this case (it can only be an input) we
# send the input to chaotic_delta
if self._entries[current[0] + (label,)] == self._emptyEntry:
if self._entries[current[0] + (label,)] == self._emptyEntry(current[0] + (label,)):
newRow1 = "chaos_quiescence"
else:
self._logger.error("Quiescence reducibility check: cannot obtain new pair of states. Row1: "
......@@ -424,19 +433,19 @@ class Table:
# get an empty nonfinal entry
def _emptyEntry(self, trace):
return (set(), False)
return (set(), self._possibleOutputs(trace))
# get an empty final entry
def _emptyFinalEntry(self, trace):
return (set(), True)
# get a final entry with given set of outputs
def _finalEntry(self, trace, outputs):
return (outputs.copy(), True)
return (set(), set())
# is the entry final? If False, we can still observe some outputs
def _isFinal(self, entry):
return self._entries[entry][1]
return self._entries[entry][0] == self._entries[entry][1]
# get a final entry with given set of outputs
def _finalEntry(self, trace, outputs):
return (outputs.copy(), outputs.copy())
# Private method for creating entries from rows and columns
def _createEntries(self):
......@@ -538,18 +547,38 @@ class Table:
trace = th.flatten(trace, self._quiescence)
if trace in self._entries:
(outputs, observed) = self._entries[trace]
if observation == None:
observation = observed
if output != None:
outputs.add(output)
self._entries[trace] = (outputs, observation)
if observation == None or observation == False:
self._entries[trace] = (outputs.copy(), observed)
else:
self._entries[trace] = (outputs.copy(), outputs.copy())
else:
if observation == None:
observation = False
outputs = set()
if output != None:
outputs.add(output)
self._entries[trace] = (outputs, observation)
if observation == None or observation == False:
self._entries[trace] = self._emptyEntry(trace)
observed = self._entries[trace][1]
else:
observed = outputs
self._entries[trace] = (outputs.copy(), observed.copy())
# given a trace, query outputPurpose for the outputs that are possibly
# enabled after it
def _possibleOutputs(self, trace):
if self._outputPurpose == None:
return self._outputs.union(set((self._quiescence,)))
else:
return self._outputPurpose.getEnabled(trace)
# given a trace, query inputPurpose for the inputs that are
# enabled after it
def _possibleInputs(self, trace):
if self._inputPurpose == None:
return self._inputs
else:
return self._inputPurpose.getEnabled(trace)
# Print table, for test
def printTable(self, path=None, prefix=""):
......@@ -597,197 +626,3 @@ class Table:
else:
rowRow.append(self._entries[trace])
tablewriter.writerow(rowRow)
class DoubleSetTable(Table):
def __init__(self, inputs, outputs, quiescence,
closeStrategy = None, logger=None, outputPurpose=None,
inputPurpose=None):
Table.__init__(self, inputs, outputs, quiescence,
closeStrategy, logger)
# allows underspecification in inputs and outputs
self._outputPurpose = outputPurpose
self._inputPurpose = inputPurpose
self._rows = set()
self._rows.add(())
for label in self._inputPurpose.getEnabled(()):
self._rows.add(tuple(label))
# _entries is a dictionary (tuple of actions) -> (set(outputs), set(outputs))
# start with the empty sequence of actions and one letter extensions
self._entries = {():(set(),self._possibleOutputs(()))}
for label in self._possibleInputs(()):
self._entries[tuple(label)] = (set(),self._possibleOutputs(tuple(label)))
# given a trace, query outputPurpose for the outputs that are possibly
# enabled after it
def _possibleOutputs(self, trace):
if self._outputPurpose == None:
return self._outputs.union(set((self._quiescence,)))
else:
return self._outputPurpose.getEnabled(trace)
# given a trace, query inputPurpose for the inputs that are
# enabled after it
def _possibleInputs(self, trace):
if self._inputPurpose == None:
return self._inputs
else:
return self._inputPurpose.getEnabled(trace)
def addOneLetterExtensions(self, rows):
modified = False
for row in rows:
for input in self._possibleInputs(row):
if self.addOneLetterExtension(row, input):
modified = True
outputs, observation = self._entries[row]
for output in outputs:
if self.addOneLetterExtension(row, output):
modified = True
return modified
# get an empty nonfinal entry
def _emptyEntry(self, trace):
return (set(), self._possibleOutputs(trace))
# get an empty final entry
def _emptyFinalEntry(self, trace):
return (set(), set())
# is the entry final? If False, we can still observe some outputs
def _isFinal(self, entry):
return self._entries[entry][0] == self._entries[entry][1]
# get a final entry with given set of outputs
def _finalEntry(self, trace, outputs):
return (outputs.copy(), outputs.copy())
# Update an entry in the table. If it does not exist, create it
def updateEntry(self, trace, output=None, observation=None):
trace = th.flatten(trace, self._quiescence)
if trace in self._entries:
(outputs, observed) = self._entries[trace]
if output != None:
outputs.add(output)
if observation == None or observation == False:
self._entries[trace] = (outputs.copy(), observed)
else:
self._entries[trace] = (outputs.copy(), outputs.copy())
else:
outputs = set()
if output != None:
outputs.add(output)
if observation == None or observation == False:
self._entries[trace] = self._emptyEntry(trace)
observed = self._entries[trace][1]
else:
observed = outputs
self._entries[trace] = (outputs.copy(), observed.copy())
def _isNotReducible(self, chaos=False):
# function used for filtering rows, we want only those which
# have quiescence in their first column
rowsWithQuiescence = lambda x: self._quiescence in self._entries[x][0]
# past contains the pair of rows already visited.
# If I visited already a pair for one loop of the following for loop,
# I do not need to check it again for other loops. Thus past is outside
# the for loop.
past = set()
# filter rows
for row1 in filter(rowsWithQuiescence, self._rowsInS):
row1Extended = row1 + (self._quiescence,)
# filter for rows in top part of the table that are
# equivalent to row1Extended
equivalentRows = lambda x: lambda y: self._rowEquality(x, y, chaos)
row2 = next(filter(equivalentRows(row1Extended), self._rowsInS))
if row1 == row2:
# same row, simulation is trivial
continue
if (row1, row2) in past:
# I already visited this pair. Go to next state enabling quiescence
continue
wait = set()
wait.add((row1, row2, ()))
while wait:
current = wait.pop()
past.add((current[0],current[1]))
enabledRow2 = set()
if current[1] == "chaos_quiescence":
# If we are in chaos_quiescence only quiescence is enabled
enabledRow2.add(self._quiescence)
else:
# input enabledness
enabledInputs = self._inputPurpose.getEnabled(current[1])
enabledRow2 = self._entries[current[1]][0].union(enabledInputs)
for label in enabledRow2:
enabledRow1 = set()
if current[0] == "chaos_quiescence":
# If we are in chaos_quiescence only quiescence is
# enabled
enabledRow1.add(self._quiescence)
else:
enabledInputs = self._inputPurpose.getEnabled(current[0])
enabledRow1 = self._entries[current[0]][0].union(enabledInputs)
if label not in enabledRow1:
suffixes = set()
suffixes.add(current[2])
# create set with current[2] and all its suffixes
for i in range(len(current[2])):
suffixes.add(current[2][i:])
return suffixes
else:
newRow1 = ""
if current[0] == "chaos_quiescence":
newRow1 = "chaos_quiescence"
else:
try:
newRow1 = next(filter(equivalentRows(current[0] + (label,)), self._rowsInS))
except StopIteration as error:
# current[0] + (label,) might not have
# corresponding row in S. This is the case when
# the first entry is emptyEntry.
# In this case (it can only be an input) we
# send the input to chaotic_delta
if self._entries[current[0] + (label,)] == self._emptyEntry(current[0] + (label,)):
newRow1 = "chaos_quiescence"
else:
self._logger.error("Quiescence reducibility check: cannot obtain new pair of states. Row1: "
+ str(current[0]) + " label: " + str(label))
self.printTable(prefix="_error")
self._logger.error("Table printed in ./tables/_error_table.csv")
self._logger.error(error)
raise
newRow2 = ""
if current[1] == "chaos_quiescence":
newRow2 = "chaos_quiescence"
else:
try:
newRow2 = next(filter(equivalentRows(current[1] + (label,)), self._rowsInS))
except StopIteration as error:
# current[1] + (label,) might not have
# corresponding row in S. This is the case when
# the first entry is (set(), false).
# In this case (it can only be an input) we
# send the input to chaotic_delta
if self._entries[current[1] + (label,)] == self._emptyEntry(current[1] + (label,)):
newRow2 = "chaos_quiescence"
else:
self._logger.error("Quiescence reducibility check: cannot obtain new pair of states."
+ " Row2: " + str(current[1]) + " label: " + str(label))
self.printTable(prefix="_error")
self._logger.error("Table printed in ./tables/_error_table.csv")
self._logger.error(error)
raise
if (newRow1,newRow2) not in past and newRow1 != newRow2:
wait.add((newRow1, newRow2, current[2] + (label,)))
return set()
# Test learning algorithm with double set observation table
import os, inspect
from nose.tools import *
from learning.learning import LearningAlgorithm, LearningAlgorithm2Sets
from teachers.ltsteachers import InputOutputTeacher
from systems.implementations import InputOutputLTS
from systems.iopurpose import InputPurpose, OutputPurpose
from teachers.ltsoracles import InputOutputPowerOracle
import helpers.graphhelper as gh
import helpers.bisimulation as bi
from testing.randomtesting import RandomTester
import logging
class TestLearningAlgorithm2:
def setUp(self):
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
inputs = set(['a','b', 'c', 'd'])
outputs = set(['x','y','z', 'k'])
quiescence = 'delta'
self.I1=InputOutputLTS(5, inputs, outputs, quiescence)
self.I1.addTransition(0,'a',1)
self.I1.addTransition(0,'a',2)
self.I1.addTransition(0,'b',0)
self.I1.addTransition(1,'a',0)
self.I1.addTransition(1,'b',3)
self.I1.addTransition(2,'a',0)
self.I1.addTransition(2,'b',3)
self.I1.addTransition(2,'x',4)
self.I1.addTransition(3,'a',0)
self.I1.addTransition(3,'x',4)
self.I1.addTransition(3,'b',3)
self.I1.addTransition(4,'y',2)
self.I1.addTransition(4,'b',4)
self.I1.addTransition(4,'a',0)
self.I1.addTransition(4,'x',0)
self.I1.addTransition(1,'a',0)
self.I1.addTransition(3,'b',1)
self.I1.addTransition(3,'a',0)
self.I1.addTransition(4,'b',2)
self.I1.addTransition(2,'a',3)
self.I1.addTransition(4,'y',0)
#self.I1.makeInputEnabled()
self.T1 = InputOutputTeacher(self.I1)
self.O1 = InputOutputPowerOracle(self.I1)
self.tester = RandomTester(self.T1, 10000, 60)
outputExpert = OutputPurpose(set(['x','y', 'k']))
inputExpert = InputPurpose(set(['a','b']))
currentdir = os.path.dirname(os.path.abspath(
inspect.getfile(inspect.currentframe())))
parentdir = os.path.dirname(currentdir)
path = os.path.join(parentdir, "tests", "test_algo_2_sets")
currentdir = os.path.dirname(os.path.abspath(
inspect.getfile(inspect.currentframe())))
parentdir = os.path.dirname(currentdir)
pathI1 = os.path.join(path, "I1")
gh.createDOTFile(self.I1, pathI1, "pdf")
self.L1 = LearningAlgorithm2Sets(self.T1, self.O1, self.tester, printPath=path,
logger=logger, tablePreciseness = 10000, outputPurpose=outputExpert,
inputPurpose=inputExpert)
def creation_test(self):
Hminus, Hplus = self.L1.run()
print(bi.bisimilar(self.I1,Hplus))
assert_equal(bi.bisimilar(self.I1,Hplus), True)
# Tests for the OLD Oracle Learning algorithm (set and boolean in the observation table)
import os, inspect
from nose.tools import *
from learning.Deprecated.oraclelearning import LearningAlgorithm
from teachers.ltsteachers import InputOutputTeacher
from systems.implementations import InputOutputLTS
from teachers.ltsoracles import InputOutputPowerOracle
import helpers.graphhelper as gh
import helpers.bisimulation as bi
from testing.randomtesting import RandomTester
class TestLearningAlgorithm:
def setUp(self):
inputs = set(['a','b'])
outputs = set(['x','y'])
quiescence = 'delta'
self.I1=InputOutputLTS(5, inputs, outputs, quiescence)
self.I1.addTransition(0,'a',1)
self.I1.addTransition(0,'a',2)
self.I1.addTransition(0,'b',0)
self.I1.addTransition(1,'a',0)
self.I1.addTransition(1,'b',3)
self.I1.addTransition(2,'a',0)
self.I1.addTransition(2,'b',3)
self.I1.addTransition(2,'x',4)
self.I1.addTransition(3,'a',0)
self.I1.addTransition(3,'x',4)
self.I1.addTransition(3,'b',3)
self.I1.addTransition(4,'y',2)
self.I1.addTransition(4,'b',4)
self.I1.addTransition(4,'a',0)
self.I1.makeInputEnabled()
self.T1 = InputOutputTeacher(self.I1)
self.O1 = InputOutputPowerOracle(self.I1)
self.tester = RandomTester(self.T1, 1000, 20)
self.L1 = LearningAlgorithm(self.T1, self.O1, self.tester)
def creation_test(self):
self._Hminus, self._Hplus = self.L1.run()