diff --git a/example_two_car_lane_switch.py b/example_two_car_lane_switch.py index bd8f4798a1e57b00365033daff6d1abcf15958cd..69b9b9250725ea088b49dd9df42f3763f6e2f498 100644 --- a/example_two_car_lane_switch.py +++ b/example_two_car_lane_switch.py @@ -1,30 +1,39 @@ -from enum import Enum,auto +import matplotlib.pyplot as plt +from ourtool.agents.car_agent import CarAgent +import numpy as np +from user.simple_map import SimpleMap, SimpleMap2 +from ourtool.scenario.scenario import Scenario +from enum import Enum, auto from ourtool.map.lane_map import LaneMap + class VehicleMode(Enum): Normal = auto() SwitchLeft = auto() SwitchRight = auto() Brake = auto() + class LaneMode(Enum): Lane0 = auto() Lane1 = auto() Lane2 = auto() + class State: x = 0.0 y = 0.0 theta = 0.0 v = 0.0 - vehicle_mode:VehicleMode = VehicleMode.Normal - lane_mode:LaneMode = LaneMode.Lane0 + vehicle_mode: VehicleMode = VehicleMode.Normal + lane_mode: LaneMode = LaneMode.Lane0 - def __init__(self,x,y,theta,v,vehicle_mode:VehicleMode, lane_mode:LaneMode): + def __init__(self, x, y, theta, v, vehicle_mode: VehicleMode, lane_mode: LaneMode): self.data = [] -def controller(ego:State, other:State, lane_map): + +def controller(ego: State, other: State, lane_map): output = ego if ego.vehicle_mode == VehicleMode.Normal: if other.x - ego.x > 3 and other.x - ego.x < 5 and ego.lane_mode == other.lane_mode: @@ -34,58 +43,53 @@ def controller(ego:State, other:State, lane_map): if lane_map.has_right(ego.lane_mode): output.vehicle_mode = VehicleMode.SwitchRight if ego.vehicle_mode == VehicleMode.SwitchLeft: - if ego.y >= lane_map.lane_geometry(ego.lane_mode)-0.5: + if ego.y >= lane_map.lane_geometry(ego.lane_mode)-2.5: output.vehicle_mode = VehicleMode.Normal output.lane_mode = lane_map.left_lane(ego.lane_mode) if ego.vehicle_mode == VehicleMode.SwitchRight: - if ego.y <= lane_map.lane_geometry(ego.lane_mode)+0.5: + if ego.y <= lane_map.lane_geometry(ego.lane_mode)+2.5: output.vehicle_mode = VehicleMode.Normal output.lane_mode = lane_map.right_lane(ego.lane_mode) - + return output - -from ourtool.agents.car_agent import CarAgent -from ourtool.scenario.scenario import Scenario -from user.simple_map import SimpleMap, SimpleMap2 -import matplotlib.pyplot as plt -import numpy as np + if __name__ == "__main__": input_code_name = 'example_two_car_lane_switch.py' scenario = Scenario() - + car = CarAgent('car1', file_name=input_code_name) scenario.add_agent(car) car = CarAgent('car2', file_name=input_code_name) scenario.add_agent(car) scenario.add_map(SimpleMap2()) scenario.set_init( - [[0,-3,0,1.0], [10,-3,0,0.5]], + [[10, 3, 0, 0.5], [0, 3, 0, 1.0]], [ - (VehicleMode.Normal, LaneMode.Lane2), - (VehicleMode.Normal, LaneMode.Lane2) + (VehicleMode.Normal, LaneMode.Lane0), + (VehicleMode.Normal, LaneMode.Lane0) ] ) # simulator = Simulator() - traces = scenario.simulate(40) + traces = scenario.verify(40) - plt.plot([0,40],[3,3],'g') - plt.plot([0,40],[0,0],'g') - plt.plot([0,40],[-3,-3],'g') + plt.plot([0, 40], [3, 3], 'g') + plt.plot([0, 40], [0, 0], 'g') + plt.plot([0, 40], [-3, -3], 'g') queue = [traces] - while queue!=[]: + while queue != []: node = queue.pop(0) traces = node.trace # for agent_id in traces: agent_id = 'car2' trace = np.array(traces[agent_id]) - plt.plot(trace[:,1], trace[:,2], 'r') + plt.plot(trace[:, 1], trace[:, 2], 'r') agent_id = 'car1' trace = np.array(traces[agent_id]) - plt.plot(trace[:,1], trace[:,2], 'b') + plt.plot(trace[:, 1], trace[:, 2], 'b') # if node.child != []: - queue += node.child + queue += node.child plt.show() diff --git a/ourtool/simulator/__init__.py b/ourtool/analysis/__init__.py similarity index 100% rename from ourtool/simulator/__init__.py rename to ourtool/analysis/__init__.py diff --git a/ourtool/analysis/analysis_tree_node.py b/ourtool/analysis/analysis_tree_node.py new file mode 100644 index 0000000000000000000000000000000000000000..55606657c6979470576165eb2d5701203249042b --- /dev/null +++ b/ourtool/analysis/analysis_tree_node.py @@ -0,0 +1,25 @@ +from typing import List, Dict + +class AnalysisTreeNode: + """AnalysisTreeNode class + A AnalysisTreeNode stores the continous execution of the system without transition happening""" + trace: Dict + """The trace for each agent. + The key of the dict is the agent id and the value of the dict is simulated traces for each agent""" + init: Dict + + def __init__( + self, + trace={}, + init={}, + mode={}, + agent={}, + child=[], + start_time = 0 + ): + self.trace:Dict = trace + self.init:Dict = init + self.mode:Dict = mode + self.agent:Dict = agent + self.child:List[AnalysisTreeNode] = child + self.start_time:float = start_time diff --git a/ourtool/simulator/simulator.py b/ourtool/analysis/simulator.py similarity index 82% rename from ourtool/simulator/simulator.py rename to ourtool/analysis/simulator.py index ea80c9afa95483c2422d276ae279042b058f0312..10a1b23f77a4d51b4b84c0581310773df007bdda 100644 --- a/ourtool/simulator/simulator.py +++ b/ourtool/analysis/simulator.py @@ -1,31 +1,10 @@ from typing import List, Dict -from ourtool.agents.base_agent import BaseAgent -import numpy as np import copy -class SimulationTreeNode: - """SimulationTreeNode class - A SimulationTreeNode stores the continous execution of the system without transition happening""" - trace: Dict - """The trace for each agent. - The key of the dict is the agent id and the value of the dict is simulated traces for each agent""" - - init: Dict - def __init__( - self, - trace={}, - init={}, - mode={}, - agent={}, - child=[], - start_time = 0 - ): - self.trace:Dict = trace - self.init:Dict = init - self.mode:Dict = mode - self.agent:Dict = agent - self.child:List[SimulationTreeNode] = child - self.start_time:float = start_time +import numpy as np + +from ourtool.agents.base_agent import BaseAgent +from ourtool.analysis.analysis_tree_node import AnalysisTreeNode class Simulator: def __init__(self): @@ -33,7 +12,7 @@ class Simulator: def simulate(self, init_list, init_mode_list, agent_list:List[BaseAgent], transition_graph, time_horizon, lane_map): # Setup the root of the simulation tree - root = SimulationTreeNode() + root = AnalysisTreeNode() for i, agent in enumerate(agent_list): root.init[agent.id] = init_list[i] init_mode = init_mode_list[i][0].name @@ -46,7 +25,7 @@ class Simulator: simulation_queue.append(root) # Perform BFS through the simulation tree to loop through all possible transitions while simulation_queue != []: - node:SimulationTreeNode = simulation_queue.pop(0) + node:AnalysisTreeNode = simulation_queue.pop(0) print(node.mode) remain_time = time_horizon - node.start_time if remain_time <= 0: @@ -88,7 +67,7 @@ class Simulator: # copy the traces that are not under transition for transition in transitions: transit_agent_idx, src_mode, dest_mode, next_init, idx = transition - # next_node = SimulationTreeNode(trace = {},init={},mode={},agent={}, child = [], start_time = 0) + # next_node = AnalysisTreeNode(trace = {},init={},mode={},agent={}, child = [], start_time = 0) next_node_mode = copy.deepcopy(node.mode) next_node_mode[transit_agent_idx] = dest_mode next_node_agent = node.agent @@ -101,7 +80,7 @@ class Simulator: else: next_node_trace[agent_idx] = truncated_trace[agent_idx] - tmp = SimulationTreeNode( + tmp = AnalysisTreeNode( trace = next_node_trace, init = next_node_init, mode = next_node_mode, @@ -112,7 +91,7 @@ class Simulator: node.child.append(tmp) simulation_queue.append(tmp) # Put the node in the child of current node. Put the new node in the queue - # node.child.append(SimulationTreeNode( + # node.child.append(AnalysisTreeNode( # trace = next_node_trace, # init = next_node_init, # mode = next_node_mode, diff --git a/ourtool/analysis/verifier.py b/ourtool/analysis/verifier.py new file mode 100644 index 0000000000000000000000000000000000000000..528a83e4eea4c4077b1f72d8cf117a6c45c49242 --- /dev/null +++ b/ourtool/analysis/verifier.py @@ -0,0 +1,85 @@ +from typing import List, Dict +import copy + +import numpy as np + +from ourtool.agents.base_agent import BaseAgent +from ourtool.analysis.analysis_tree_node import AnalysisTreeNode +from ourtool.dryvr.core.dryvrcore import calc_bloated_tube +import ourtool.dryvr.common.config as userConfig + +class Verifier: + def __init__(self): + self.reachtube_tree_root = None + self.unsafe_set = None + self.verification_result = None + + def compute_full_reachtube( + self, + init_list, + init_mode_list, + agent_list:List[BaseAgent], + transition_graph, + time_horizon, + lane_map + ): + root = AnalysisTreeNode() + for i, agent in enumerate(agent_list): + root.init[agent.id] = init_list[i] + init_mode = [elem.name for elem in init_mode_list[i]] + init_mode =','.join(init_mode) + root.mode[agent.id] = init_mode + root.agent[agent.id] = agent + self.reachtube_tree_root = root + verification_queue = [] + verification_queue.append(root) + while verification_queue != []: + node:AnalysisTreeNode = verification_queue.pop(0) + print(node.mode) + remain_time = time_horizon - node.start_time + if remain_time <= 0: + continue + # For reachtubes not already computed + for agent_id in node.agent: + if agent_id not in node.trace: + # Compute the trace starting from initial condition + mode = node.mode[agent_id] + init = node.init[agent_id] + # trace = node.agent[agent_id].TC_simulate(mode, init, remain_time,lane_map) + # trace[:,0] += node.start_time + # node.trace[agent_id] = trace.tolist() + + cur_bloated_tube = calc_bloated_tube(mode, + init, + remain_time, + node.agent[agent_id].TC_simulate, + 'GLOBAL', + None, + userConfig.SIMTRACENUM, + lane_map = lane_map + ) + trace = np.array(cur_bloated_tube) + trace[:,0] += node.start_time + node.trace[agent_id] = trace.tolist() + print("here") + + trace_length = len(list(node.trace.values())[0])/2 + transitions = [] + for idx in range(0,trace_length,2): + # For each trace, check with the guard to see if there's any possible transition + # Store all possible transition in a list + # A transition is defined by (agent, src_mode, dest_mode, corresponding reset, transit idx) + # Here we enforce that only one agent transit at a time + all_agent_state = {} + for agent_id in node.agent: + all_agent_state[agent_id] = (node.trace[agent_id][idx:idx+2], node.mode[agent_id]) + possible_transitions = transition_graph.get_all_transitions_set(all_agent_state) + if possible_transitions != []: + any_contained = False + for agent_idx, src_mode, dest_mode, next_init, contained in possible_transitions: + transitions.append((agent_idx, src_mode, dest_mode, next_init, idx)) + any_contained = any_contained or contained + if any_contained: + break + pass + diff --git a/ourtool/dryvr/__init__.py b/ourtool/dryvr/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ourtool/dryvr/common/__init__.py b/ourtool/dryvr/common/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7dbea944864d1e4d5337014013f9b49ae5ce1bb1 --- /dev/null +++ b/ourtool/dryvr/common/__init__.py @@ -0,0 +1,22 @@ +# _oo0oo_ +# o8888888o +# 88" . "88 +# (| -_- |) +# 0\ = /0 +# ___/`---'\___ +# .' \\| |// '. +# / \\||| : |||// \ +# / _||||| -:- |||||- \ +# | | \\\ - /// | | +# | \_| ''\---/'' |_/ | +# \ .-\__ '-' ___/-. / +# ___'. .' /--.--\ `. .'___ +# ."" '< `.___\_<|>_/___.' >' "". +# | | : `- \`.;`\ _ /`;.`/ - ` : | | +# \ \ `_. \_ __\ /__ _/ .-` / / +# =====`-.____`.___ \_____/___.-`___.-'===== +# `=---=' +# +# +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Codes are far away from bugs with the protection diff --git a/ourtool/dryvr/common/config.py b/ourtool/dryvr/common/config.py new file mode 100644 index 0000000000000000000000000000000000000000..6b23f518d74bd9d178fe2f56ad613ad65c0ea1a5 --- /dev/null +++ b/ourtool/dryvr/common/config.py @@ -0,0 +1,11 @@ +""" Global Configurations """ + +# Verification config +SIMUTESTNUM = 10 +SIMTRACENUM = 10 +REFINETHRES = 10 +CHILDREFINETHRES = 2 + +# Synthesis config +RANDMODENUM = 3 +RANDSECTIONNUM = 3 diff --git a/ourtool/dryvr/common/constant.py b/ourtool/dryvr/common/constant.py new file mode 100644 index 0000000000000000000000000000000000000000..0eaaa69353ee78fe8b7023ec6d73a12550213335 --- /dev/null +++ b/ourtool/dryvr/common/constant.py @@ -0,0 +1,28 @@ +""" +This file contains constant for DryVR +""" + +# Logic constant +DEBUG = False +PLOTGRAPH = True + +# Flag value +SAFE = 1 +UNSAFE = -1 +UNKNOWN = 0 +NOSTATUS = 99 +BLOATDEBUG = False +PLOTDIM = 2 + +# File Paths +GRAPHOUTPUT = 'output/curGraph.png' +RRTGRAPHPOUTPUT = 'output/rrtGraph.png' +SIMRESULTOUTPUT = 'output/Traj.txt' +REACHTUBEOUTPUT = 'output/reachtube.txt' +RRTOUTPUT = 'output/rrtTube.txt' +UNSAFEFILENAME = 'output/unsafeTube.txt' + +# Useful constant string +NEWLINE = '----------------------------------------------' +PW = "PW" +GLOBAL = "GLOBAL" diff --git a/ourtool/dryvr/common/io.py b/ourtool/dryvr/common/io.py new file mode 100644 index 0000000000000000000000000000000000000000..c5b55e206966a7b8a5adb4fc016f5da359ec2b3d --- /dev/null +++ b/ourtool/dryvr/common/io.py @@ -0,0 +1,156 @@ +""" +This file contains IO functions for DryVR +""" + +import six + +from ourtool.dryvr.common.utils import DryVRInput, RrtInput, checkVerificationInput, checkSynthesisInput + + +def writeReachTubeFile(result, path): + """ + Write reach tube to a file + + reach tube example: + mode1 + [0.0, 1, 2] + [0.1, 2, 3] + [0.1, 2, 3] + .... + mode1->mode2 + [1.0, 3, 4] + .... + + Args: + result (list): list of reachable state. + path (str): file name. + + Returns: + None + + """ + with open(path, 'w') as f: + for line in result: + if isinstance(line, six.string_types): + f.write(line + '\n') + elif isinstance(line, list): + f.write(' '.join(map(str, line)) + '\n') + + +def writeRrtResultFile(modes, traces, path): + """ + Write control synthesis result to a file + + Args: + modes (list): list of mode. + traces (list): list of traces corresponding to modes + path (str): file name. + + Returns: + None + + """ + with open(path, 'w') as f: + for mode, trace in zip(modes, traces): + f.write(mode + '\n') + for line in trace: + f.write(" ".join(map(str, line)) + '\n') + + +def parseVerificationInputFile(data): + """ + Parse the json input for DryVR verification + + Args: + data (dict): dictionary contains parameters + + Returns: + DryVR verification input object + + """ + + # If resets is missing, fill with empty resets + if not 'resets' in data: + data['resets'] = ["" for _ in range(len(data["edge"]))] + + # If initialMode is missing, fill with empty initial mode + if not 'initialVertex' in data: + data['initialVertex'] = -1 + + # If deterministic is missing, default to non-deterministic + if not 'deterministic' in data: + data['deterministic'] = False + + # If bloating method is missing, default global descrepancy + if not 'bloatingMethod' in data: + data['bloatingMethod'] = 'GLOBAL' + + # Set a fake kvalue since kvalue is not used in this case + + if data['bloatingMethod'] == "GLOBAL": + data['kvalue'] = [1.0 for i in range(len(data['variables']))] + + # Set a fake directory if the directory is not provided, this means the user provides + # simulation function to DryVR directly + if not 'directory' in data: + data['directory'] = "" + + checkVerificationInput(data) + return DryVRInput( + vertex=data["vertex"], + edge=data["edge"], + guards=data["guards"], + variables=data["variables"], + initialSet=data["initialSet"], + unsafeSet=data["unsafeSet"], + timeHorizon=data["timeHorizon"], + path=data["directory"], + resets=data["resets"], + initialVertex=data["initialVertex"], + deterministic=data["deterministic"], + bloatingMethod=data['bloatingMethod'], + kvalue=data['kvalue'], + ) + + +def parseRrtInputFile(data): + """ + Parse the json input for DryVR controller synthesis + + Args: + data (dict): dictionary contains parameters + + Returns: + DryVR controller synthesis input object + + """ + + # If bloating method is missing, default global descrepancy + if not 'bloatingMethod' in data: + data['bloatingMethod'] = 'GLOBAL' + + # set a fake kvalue since kvalue is not used in this case + + if data['bloatingMethod'] == "GLOBAL": + data['kvalue'] = [1.0 for i in range(len(data['variables']))] + + # Set a fake directory if the directory is not provided, this means the user provides + # simulation function to DryVR directly + if not 'directory' in data: + data['directory'] = "" + + checkSynthesisInput(data) + + return RrtInput( + modes=data["modes"], + variables=data["variables"], + initialSet=data["initialSet"], + unsafeSet=data["unsafeSet"], + goalSet=data["goalSet"], + timeHorizon=data["timeHorizon"], + minTimeThres=data["minTimeThres"], + path=data["directory"], + goal=data["goal"], + bloatingMethod=data['bloatingMethod'], + kvalue=data['kvalue'], + ) diff --git a/ourtool/dryvr/common/utils.py b/ourtool/dryvr/common/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..21d64ff954b9f63fff37f7595b33a37808234b39 --- /dev/null +++ b/ourtool/dryvr/common/utils.py @@ -0,0 +1,304 @@ +""" +This file contains common utils for DryVR +""" + +import importlib +import random + +from collections import namedtuple + +# This is the tuple for input file parsed by DryVR +DryVRInput = namedtuple( + 'DryVRInput', + 'vertex edge guards variables initialSet unsafeSet timeHorizon path resets initialVertex deterministic ' + 'bloatingMethod kvalue ' +) + +# This is the tuple for rtt input file parsed by DryVR +RrtInput = namedtuple( + 'RttInput', + 'modes variables initialSet unsafeSet goalSet timeHorizon minTimeThres path goal bloatingMethod kvalue' +) + + +def importSimFunction(path): + """ + Load simulation function from given file path + Note the folder in the examples directory must have __init__.py + And the simulation function must be named TC_Simulate + The function should looks like following: + TC_Simulate(Mode, initialCondition, time_bound) + + Args: + path (str): Simulator directory. + + Returns: + simulation function + + """ + try: + # FIXME TC_Simulate should just be a simple call back function + import os + import sys + sys.path.append(os.path.abspath(path)) + mod_name = path.replace('/', '.') + module = importlib.import_module(mod_name) + sys.path.pop() + + return module.TC_Simulate + except ImportError as e: + print("Import simulation function failed!") + print(e) + exit() # TODO Proper return + + +def randomPoint(lower, upper): + """ + Pick a random point between lower and upper bound + This function supports both int or list + + Args: + lower (list or int or float): lower bound. + upper (list or int or float): upper bound. + + Returns: + random point (either float or list of float) + + """ + # random.seed(4) + + if isinstance(lower, int) or isinstance(lower, float): + return random.uniform(lower, upper) + + if isinstance(lower, list): + assert len(lower) == len(upper), "Random Point List Range Error" + + return [random.uniform(lower[i], upper[i]) for i in range(len(lower))] + + +def calcDelta(lower, upper): + """ + Calculate the delta value between the lower and upper bound + The function only supports list since we assue initial set is always list + + Args: + lower (list): lowerbound. + upper (list): upperbound. + + Returns: + delta (list of float) + + """ + # Convert list into float in case they are int + lower = [float(val) for val in lower] + upper = [float(val) for val in upper] + + assert len(lower) == len(upper), "Delta calc List Range Error" + return [(upper[i] - lower[i]) / 2 for i in range(len(upper))] + + +def calcCenterPoint(lower, upper): + """ + Calculate the center point between the lower and upper bound + The function only supports list since we assue initial set is always list + + Args: + lower (list): lowerbound. + upper (list): upperbound. + + Returns: + delta (list of float) + + """ + + # Convert list into float in case they are int + lower = [float(val) for val in lower] + upper = [float(val) for val in upper] + assert len(lower) == len(upper), "Center Point List Range Error" + return [(upper[i] + lower[i]) / 2 for i in range(len(upper))] + + +def buildModeStr(g, vertex): + """ + Build a unique string to represent a mode + This should be something like "modeName,modeNum" + + Args: + g (igraph.Graph): Graph object. + vertex (int or str): vertex number. + + Returns: + str: a string to represent a mode + + """ + return g.vs[vertex]['label'] + ',' + str(vertex) + + +def handleReplace(input_str, keys): + """ + Replace variable in inputStr to self.varDic["variable"] + For example: + input + And(y<=0,t>=0.2,v>=-0.1) + output: + And(self.varDic["y"]<=0,self.varDic["t"]>=0.2,self.varDic["v"]>=-0.1) + + Args: + input_str (str): original string need to be replaced + keys (list): list of variable strings + + Returns: + str: a string that all variables have been replaced into a desire form + + """ + idxes = [] + i = 0 + original = input_str + + keys.sort(key=lambda s: len(s)) + for key in keys[::-1]: + for i in range(len(input_str)): + if input_str[i:].startswith(key): + idxes.append((i, i + len(key))) + input_str = input_str[:i] + "@" * len(key) + input_str[i + len(key):] + + idxes = sorted(idxes) + + input_str = original + for idx in idxes[::-1]: + key = input_str[idx[0]:idx[1]] + target = 'self.varDic["' + key + '"]' + input_str = input_str[:idx[0]] + target + input_str[idx[1]:] + return input_str + + +def neg(orig): + """ + Neg the original condition + For example: + input + And(y<=0,t>=0.2,v>=-0.1) + output: + Not(And(y<=0,t>=0.2,v>=-0.1)) + + Args: + orig (str): original string need to be neg + + Returns: + a neg condition string + + """ + return 'Not(' + orig + ')' + + +def trimTraces(traces): + """ + trim all traces to the same length + + Args: + traces (list): list of traces generated by simulator + Returns: + traces (list) after trim to the same length + + """ + + ret_traces = [] + trace_lengths = [] + for trace in traces: + trace_lengths.append(len(trace)) + trace_len = min(trace_lengths) + + for trace in traces: + ret_traces.append(trace[:trace_len]) + + return ret_traces + + +def checkVerificationInput(data): + """ + Check verification input to make sure it is valid + + Args: + data (obj): json data object + Returns: + None + + """ + assert len(data['variables']) == len(data['initialSet'][0]), "Initial set dimension mismatch" + + assert len(data['variables']) == len(data['initialSet'][1]), "Initial set dimension mismatch" + + assert len(data['edge']) == len(data["guards"]), "guard number mismatch" + + assert len(data['edge']) == len(data["resets"]), "reset number mismatch" + + if data["bloatingMethod"] == "PW": + assert 'kvalue' in data, "kvalue need to be provided when bloating method set to PW" + + for i in range(len(data['variables'])): + assert data['initialSet'][0][i] <= data['initialSet'][1][i], "initial set lowerbound is larger than upperbound" + + +def checkSynthesisInput(data): + """ + Check Synthesis input to make sure it is valid + + Args: + data (obj): json data object + Returns: + None + + """ + assert len(data['variables']) == len(data['initialSet'][0]), "Initial set dimension mismatch" + + assert len(data['variables']) == len(data['initialSet'][1]), "Initial set dimension mismatch" + + for i in range(len(data['variables'])): + assert data['initialSet'][0][i] <= data['initialSet'][1][i], "initial set lowerbound is larger than upperbound" + + assert data["minTimeThres"] < data["timeHorizon"], "min time threshold is too large!" + + if data["bloatingMethod"] == "PW": + assert 'kvalue' in data, "kvalue need to be provided when bloating method set to PW" + + +def isIpynb(): + """ + Check if the code is running on Ipython notebook + """ + try: + cfg = get_ipython().config + if "IPKernelApp" in cfg: + return True + else: + return False + except NameError: + return False + + +def overloadConfig(configObj, userConfig): + """ + Overload user config to config module + + Args: + configObj (module): config module + userConfig (dict): user specified config + """ + + if "SIMUTESTNUM" in userConfig: + configObj.SIMUTESTNUM = userConfig["SIMUTESTNUM"] + + if "SIMTRACENUM" in userConfig: + configObj.SIMTRACENUM = userConfig["SIMTRACENUM"] + + if "REFINETHRES" in userConfig: + configObj.REFINETHRES = userConfig["REFINETHRES"] + + if "CHILDREFINETHRES" in userConfig: + configObj.CHILDREFINETHRES = userConfig["CHILDREFINETHRES"] + + if "RANDMODENUM" in userConfig: + configObj.RANDMODENUM = userConfig["RANDMODENUM"] + + if "RANDSECTIONNUM" in userConfig: + configObj.RANDSECTIONNUM = userConfig["RANDSECTIONNUM"] diff --git a/ourtool/dryvr/core/__init__.py b/ourtool/dryvr/core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ourtool/dryvr/core/distance.py b/ourtool/dryvr/core/distance.py new file mode 100644 index 0000000000000000000000000000000000000000..8da15a12091e3501880ca1411bb5fc4e4156099d --- /dev/null +++ b/ourtool/dryvr/core/distance.py @@ -0,0 +1,52 @@ +""" +This file contains a distance checker class for controller synthesis +""" +from math import sqrt + + +class DistChecker: + """ + This is the class to calculate the distance between + current set and the goal set for DryVR controller synthesis. + The distance it calculate is the euclidean distance. + """ + + def __init__(self, goal_set, variables): + """ + Distance checker class initialization function. + + Args: + goal_set (list): a list describing the goal set. + [["x_1","x_2"],[19.5,-1],[20.5,1]] + which defines a goal set + 19.5<=x_1<=20.5 && -1<=x_2<=1 + variables (list): list of variable name + """ + var, self.lower, self.upper = goal_set + self.idx = [] + for v in var: + self.idx.append(variables.index(v) + 1) + + def calc_distance(self, lower_bound, upper_bound): + """ + Calculate the euclidean distance between the + current set and goal set. + + Args: + lower_bound (list): the lower bound of the current set. + upper_bound (list): the upper bound of the current set. + + Returns: + the euclidean distance between current set and goal set + + """ + dist = 0.0 + for i in range(len(self.idx)): + max_val = max( + (self.lower[i] - lower_bound[self.idx[i]]) ** 2, + (self.upper[i] - lower_bound[self.idx[i]]) ** 2, + (self.lower[i] - upper_bound[self.idx[i]]) ** 2, + (self.upper[i] - upper_bound[self.idx[i]]) ** 2 + ) + dist += max_val + return sqrt(dist) diff --git a/ourtool/dryvr/core/dryvrcore.py b/ourtool/dryvr/core/dryvrcore.py new file mode 100644 index 0000000000000000000000000000000000000000..29ff8cd3152d260cd3d85eadc64aaf6dcd57e595 --- /dev/null +++ b/ourtool/dryvr/core/dryvrcore.py @@ -0,0 +1,309 @@ +""" +This file contains core functions used by DryVR +""" +from __future__ import print_function +import random + +import networkx as nx +import numpy as np +import igraph + + +from ourtool.dryvr.common.constant import * +from ourtool.dryvr.common.io import writeReachTubeFile +from ourtool.dryvr.common.utils import randomPoint, calcDelta, calcCenterPoint, trimTraces +from ourtool.dryvr.discrepancy.Global_Disc import get_reachtube_segment +# from ourtool.dryvr.tube_computer.backend.reachabilityengine import ReachabilityEngine +# from ourtool.dryvr.tube_computer.backend.initialset import InitialSet + +def build_graph(vertex, edge, guards, resets): + """ + Build graph object using given parameters + + Args: + vertex (list): list of vertex with mode name + edge (list): list of edge that connects vertex + guards (list): list of guard corresponding to each edge + resets (list): list of reset corresponding to each edge + + Returns: + graph object + + """ + g = igraph.Graph(directed=True) + g.add_vertices(len(vertex)) + g.add_edges(edge) + + g.vs['label'] = vertex + g.vs['name'] = vertex + labels = [] + for i in range(len(guards)): + cur_guard = guards[i] + cur_reset = resets[i] + if not cur_reset: + labels.append(cur_guard) + else: + labels.append(cur_guard + '|' + cur_reset) + + g.es['label'] = labels + g.es['guards'] = guards + g.es['resets'] = resets + + # if PLOTGRAPH: + # graph = igraph.plot(g, GRAPHOUTPUT, margin=40) + # graph.save() + return g + + +def build_rrt_graph(modes, traces, is_ipynb): + """ + Build controller synthesis graph object using given modes and traces. + Note this function is very different from buildGraph function. + This is white-box transition graph learned from controller synthesis algorithm + The reason to build it is to output the transition graph to file + + Args: + modes (list): list of mode name + traces (list): list of trace corresponding to each mode + is_ipynb (bool): check if it's in Ipython notebook environment + + Returns: + None + + """ + if is_ipynb: + vertex = [] + # Build unique identifier for a vertex and mode name + for idx, v in enumerate(modes): + vertex.append(v + "," + str(idx)) + + edge_list = [] + edge_label = {} + for i in range(1, len(modes)): + edge_list.append((vertex[i - 1], vertex[i])) + lower = traces[i - 1][-2][0] + upper = traces[i - 1][-1][0] + edge_label[(vertex[i - 1], vertex[i])] = "[" + str(lower) + "," + str(upper) + "]" + + fig = plt.figure() + ax = fig.add_subplot(111) + nx_graph = nx.DiGraph() + nx_graph.add_edges_from(edge_list) + pos = nx.spring_layout(nx_graph) + colors = ['green'] * len(nx_graph.nodes()) + fig.suptitle('transition graph', fontsize=10) + nx.draw_networkx_labels(nx_graph, pos) + options = { + 'node_color': colors, + 'node_size': 1000, + 'cmap': plt.get_cmap('jet'), + 'arrowstyle': '-|>', + 'arrowsize': 50, + } + nx.draw_networkx(nx_graph, pos, arrows=True, **options) + nx.draw_networkx_edge_labels(nx_graph, pos, edge_labels=edge_label) + fig.canvas.draw() + + else: + g = igraph.Graph(directed=True) + g.add_vertices(len(modes)) + edges = [] + for i in range(1, len(modes)): + edges.append([i - 1, i]) + g.add_edges(edges) + + g.vs['label'] = modes + g.vs['name'] = modes + + # Build guard + guard = [] + for i in range(len(traces) - 1): + lower = traces[i][-2][0] + upper = traces[i][-1][0] + guard.append("And(t>" + str(lower) + ", t<=" + str(upper) + ")") + g.es['label'] = guard + graph = igraph.plot(g, RRTGRAPHPOUTPUT, margin=40) + graph.save() + + +def simulate(g, init_condition, time_horizon, guard, sim_func, reset, init_vertex, deterministic): + """ + This function does a full hybrid simulation + + Args: + g (obj): graph object + init_condition (list): initial point + time_horizon (float): time horizon to simulate + guard (src.core.guard.Guard): list of guard string corresponding to each transition + sim_func (function): simulation function + reset (src.core.reset.Reset): list of reset corresponding to each transition + init_vertex (int): initial vertex that simulation starts + deterministic (bool) : enable or disable must transition + + Returns: + A dictionary obj contains simulation result. + Key is mode name and value is the simulation trace. + + """ + + ret_val = igraph.defaultdict(list) + # If you do not declare initialMode, then we will just use topological sort to find starting point + if init_vertex == -1: + computer_order = g.topological_sorting(mode=igraph.OUT) + cur_vertex = computer_order[0] + else: + cur_vertex = init_vertex + remain_time = time_horizon + cur_time = 0 + + # Plus 1 because we need to consider about time + dimensions = len(init_condition) + 1 + + sim_result = [] + # Avoid numeric error + while remain_time > 0.01: + + if DEBUG: + print(NEWLINE) + print((cur_vertex, remain_time)) + print('Current State', g.vs[cur_vertex]['label'], remain_time) + + if init_condition is None: + # Ideally this should not happen + break + + cur_successors = g.successors(cur_vertex) + transit_time = remain_time + cur_label = g.vs[cur_vertex]['label'] + + cur_sim_result = sim_func(cur_label, init_condition, transit_time) + if isinstance(cur_sim_result, np.ndarray): + cur_sim_result = cur_sim_result.tolist() + + if len(cur_successors) == 0: + # Some model return numpy array, convert to list + init_condition, truncated_result = guard.guard_sim_trace( + cur_sim_result, + "" + ) + cur_successor = None + + else: + # First find all possible transition + # Second randomly pick a path and time to transit + next_modes = [] + for cur_successor in cur_successors: + edge_id = g.get_eid(cur_vertex, cur_successor) + cur_guard_str = g.es[edge_id]['guards'] + cur_reset_str = g.es[edge_id]['resets'] + + next_init, truncated_result = guard.guard_sim_trace( + cur_sim_result, + cur_guard_str + ) + + next_init = reset.reset_point(cur_reset_str, next_init) + # If there is a transition + if next_init: + next_modes.append((cur_successor, next_init, truncated_result)) + if next_modes: + # It is a non-deterministic system, randomly choose next state to transit + if not deterministic: + cur_successor, init_condition, truncated_result = random.choice(next_modes) + # This is deterministic system, choose earliest transition + else: + shortest_time = float('inf') + for s, i, t in next_modes: + cur_tube_time = t[-1][0] + if cur_tube_time < shortest_time: + cur_successor = s + init_condition = i + truncated_result = t + shortest_time = cur_tube_time + else: + cur_successor = None + init_condition = None + + # Get real transit time from truncated result + transit_time = truncated_result[-1][0] + ret_val[cur_label] += truncated_result + sim_result.append(cur_label) + for simRow in truncated_result: + simRow[0] += cur_time + sim_result.append(simRow) + + remain_time -= transit_time + print("transit time", transit_time, "remain time", remain_time) + cur_time += transit_time + cur_vertex = cur_successor + + writeReachTubeFile(sim_result, SIMRESULTOUTPUT) + return ret_val + + +def calc_bloated_tube( + mode_label, + initial_set, + time_horizon, + sim_func, + bloating_method, + kvalue, + sim_trace_num, + guard_checker=None, + guard_str="", + lane_map = None + ): + """ + This function calculate the reach tube for single given mode + + Args: + mode_label (str): mode name + initial_set (list): a list contains upper and lower bound of the initial set + time_horizon (float): time horizon to simulate + sim_func (function): simulation function + bloating_method (str): determine the bloating method for reach tube, either GLOBAL or PW + sim_trace_num (int): number of simulations used to calculate the discrepancy + kvalue (list): list of float used when bloating method set to PW + guard_checker (src.core.guard.Guard or None): guard check object + guard_str (str): guard string + + Returns: + Bloated reach tube + + """ + random.seed(4) + cur_center = calcCenterPoint(initial_set[0], initial_set[1]) + cur_delta = calcDelta(initial_set[0], initial_set[1]) + traces = [sim_func(mode_label, cur_center, time_horizon, lane_map)] + # Simulate SIMTRACENUM times to learn the sensitivity + for _ in range(sim_trace_num): + new_init_point = randomPoint(initial_set[0], initial_set[1]) + traces.append(sim_func(mode_label, new_init_point, time_horizon, lane_map)) + + # Trim the trace to the same length + traces = trimTraces(traces) + if guard_checker is not None: + # pre truncated traces to get better bloat result + max_idx = -1 + for trace in traces: + ret_idx = guard_checker.guard_sim_trace_time(trace, guard_str) + max_idx = max(max_idx, ret_idx + 1) + for i in range(len(traces)): + traces[i] = traces[i][:max_idx] + + # The major + if bloating_method == GLOBAL: + # TODO: Replace this with ReachabilityEngine.get_reachtube_segment + cur_reach_tube: np.ndarray = get_reachtube_segment(np.array(traces), np.array(cur_delta), "PWGlobal") + # cur_reach_tube: np.ndarray = ReachabilityEngine.get_reachtube_segment_wrapper(np.array(traces), np.array(cur_delta)) + elif bloating_method == PW: + # TODO: Replace this with ReachabilityEngine.get_reachtube_segment + cur_reach_tube: np.ndarray = get_reachtube_segment(np.array(traces), np.array(cur_delta), "PW") + # cur_reach_tube: np.ndarray = ReachabilityEngine.get_reachtube_segment_wrapper(np.array(traces), np.array(cur_delta)) + else: + raise ValueError("Unsupported bloating method '" + bloating_method + "'") + final_tube = np.zeros((cur_reach_tube.shape[0]*2, cur_reach_tube.shape[2])) + final_tube[0::2, :] = cur_reach_tube[:, 0, :] + final_tube[1::2, :] = cur_reach_tube[:, 1, :] + print(final_tube.tolist()[-2], final_tube.tolist()[-1]) + return final_tube.tolist() diff --git a/ourtool/dryvr/core/dryvrmain.py b/ourtool/dryvr/core/dryvrmain.py new file mode 100644 index 0000000000000000000000000000000000000000..af6ddd87302b6811d056b6ccffb4930da690bfea --- /dev/null +++ b/ourtool/dryvr/core/dryvrmain.py @@ -0,0 +1,544 @@ +""" +This file contains a single function that verifies model +""" +from __future__ import print_function +import time + +import src.common.config as userConfig +from src.common.io import parseVerificationInputFile, parseRrtInputFile, writeRrtResultFile +from src.common.utils import buildModeStr, isIpynb, overloadConfig +from src.core.distance import DistChecker +from src.core.dryvrcore import * +from src.core.goalchecker import GoalChecker +from src.core.graph import Graph +from src.core.guard import Guard +from src.core.initialset import InitialSet +from src.core.initialsetstack import InitialSetStack, GraphSearchNode +from src.core.reachtube import ReachTube +from src.core.reset import Reset +from src.core.uniformchecker import UniformChecker +# from src.tube_computer.backend.reachabilityengine import ReachabilityEngine +# from src.tube_computer.backend.initialset import InitialSet + +def verify(data, sim_function, param_config=None): + """ + DryVR verification algorithm. + It does the verification and print out the verify result. + + Args: + data (dict): dictionary that contains params for the input file + sim_function (function): black-box simulation function + param_config (dict or None): user-specified configuration + + Returns: + Safety (str): safety of the system + Reach (obj): reach tube object + + """ + if param_config is None: + param_config = {} + # There are some fields can be config by user, + # If user specified these fields in paramConfig, + # overload these parameters to userConfig + overloadConfig(userConfig, param_config) + + refine_counter = 0 + + params = parseVerificationInputFile(data) + # Build the graph object + graph = build_graph( + params.vertex, + params.edge, + params.guards, + params.resets + ) + + # Build the progress graph for jupyter notebook + # isIpynb is used to detect if the code is running + # on notebook or terminal, the graph will only be shown + # in notebook mode + progress_graph = Graph(params, isIpynb()) + + # Make sure the initial mode is specified if the graph is dag + # FIXME should move this part to input check + # Bolun 02/12/2018 + assert graph.is_dag() or params.initialVertex != -1, "Graph is not DAG and you do not have initial mode!" + + checker = UniformChecker(params.unsafeSet, params.variables) + guard = Guard(params.variables) + reset = Reset(params.variables) + t_start = time.time() + + # Step 1) Simulation Test + # Random generate points, then simulate and check the result + for _ in range(userConfig.SIMUTESTNUM): + rand_init = randomPoint(params.initialSet[0], params.initialSet[1]) + + if DEBUG: + print('Random checking round ', _, 'at point ', rand_init) + + # Do a full hybrid simulation + sim_result = simulate( + graph, + rand_init, + params.timeHorizon, + guard, + sim_function, + reset, + params.initialVertex, + params.deterministic + ) + + # Check the traces for each mode + for mode in sim_result: + safety = checker.check_sim_trace(sim_result[mode], mode) + if safety == -1: + print('Current simulation is not safe. Program halt') + print('simulation time', time.time() - t_start) + return "UNSAFE", None + sim_end_time = time.time() + + # Step 2) Check Reach Tube + # Calculate the over approximation of the reach tube and check the result + print("Verification Begin") + + # Get the initial mode + if params.initialVertex == -1: + compute_order = graph.topological_sorting(mode=igraph.OUT) + initial_vertex = compute_order[0] + else: + initial_vertex = params.initialVertex + + # Build the initial set stack + cur_mode_stack = InitialSetStack(initial_vertex, userConfig.REFINETHRES, params.timeHorizon,0) + cur_mode_stack.stack.append(InitialSet(params.initialSet[0], params.initialSet[1])) + cur_mode_stack.bloated_tube.append(buildModeStr(graph, initial_vertex)) + while True: + # backward_flag can be SAFE, UNSAFE or UNKNOWN + # If the backward_flag is SAFE/UNSAFE, means that the children nodes + # of current nodes are all SAFE/UNSAFE. If one of the child node is + # UNKNOWN, then the backward_flag is UNKNOWN. + backward_flag = SAFE + + while cur_mode_stack.stack: + print(str(cur_mode_stack)) + print(cur_mode_stack.stack[-1]) + + if not cur_mode_stack.is_valid(): + # A stack will be invalid if number of initial sets + # is more than refine threshold we set for each stack. + # Thus we declare this stack is UNKNOWN + print(cur_mode_stack.mode, "is not valid anymore") + backward_flag = UNKNOWN + break + + # This is condition check to make sure the reach tube output file + # will be readable. Let me try to explain this. + # A reachtube output will be something like following + # MODEA->MODEB + # [0.0, 1.0, 1.1] + # [0.1, 1.1, 1.2] + # ..... + # Once we have refinement, we will add multiple reach tube to + # this cur_mode_stack.bloatedTube + # However, we want to copy MODEA->MODEB so we know that two different + # reach tube from two different refined initial set + # The result will be look like following + # MODEA->MODEB + # [0.0, 1.0, 1.1] + # [0.1, 1.1, 1.2] + # ..... + # MODEA->MODEB (this one gets copied!) + # [0.0, 1.5, 1.6] + # [0.1, 1.6, 1.7] + # ..... + if isinstance(cur_mode_stack.bloated_tube[-1], list): + cur_mode_stack.bloated_tube.append(cur_mode_stack.bloated_tube[0]) + + cur_stack = cur_mode_stack.stack + cur_vertex = cur_mode_stack.mode + cur_remain_time = cur_mode_stack.remain_time + cur_label = graph.vs[cur_vertex]['label'] + cur_successors = graph.successors(cur_vertex) + cur_initial = [cur_stack[-1].lower_bound, cur_stack[-1].upper_bound] + # Update the progress graph + progress_graph.update(buildModeStr(graph, cur_vertex), cur_mode_stack.bloated_tube[0], + cur_mode_stack.remain_time) + + if len(cur_successors) == 0: + # If there is not successor + # Calculate the current bloated tube without considering the guard + cur_bloated_tube = calc_bloated_tube(cur_label, + cur_initial, + cur_remain_time, + sim_function, + params.bloatingMethod, + params.kvalue, + userConfig.SIMTRACENUM, + ) + + candidate_tube = [] + shortest_time = float("inf") + shortest_tube = None + + for cur_successor in cur_successors: + edge_id = graph.get_eid(cur_vertex, cur_successor) + cur_guard_str = graph.es[edge_id]['guards'] + cur_reset_str = graph.es[edge_id]['resets'] + # Calculate the current bloated tube with guard involved + # Pre-check the simulation trace so we can get better bloated result + cur_bloated_tube = calc_bloated_tube(cur_label, + cur_initial, + cur_remain_time, + sim_function, + params.bloatingMethod, + params.kvalue, + userConfig.SIMTRACENUM, + guard_checker=guard, + guard_str=cur_guard_str, + ) + + # Use the guard to calculate the next initial set + # TODO: Made this function return multiple next_init + next_init, truncated_result, transit_time = guard.guard_reachtube( + cur_bloated_tube, + cur_guard_str, + ) + + if next_init is None: + continue + + # Reset the next initial set + # TODO: Made this function handle multiple next_init + next_init = reset.reset_set(cur_reset_str, next_init[0], next_init[1]) + + # Build next mode stack + next_mode_stack = InitialSetStack( + cur_successor, + userConfig.CHILDREFINETHRES, + cur_remain_time - transit_time, + start_time=transit_time+cur_mode_stack.start_time + ) + next_mode_stack.parent = cur_mode_stack + # TODO: Append all next_init into the next_mode_stack + next_mode_stack.stack.append(InitialSet(next_init[0], next_init[1])) + next_mode_stack.bloated_tube.append( + cur_mode_stack.bloated_tube[0] + '->' + buildModeStr(graph, cur_successor)) + cur_stack[-1].child[cur_successor] = next_mode_stack + if len(truncated_result) > len(candidate_tube): + candidate_tube = truncated_result + + # In case of must transition + # We need to record shortest tube + # As shortest tube is the tube invoke transition + if truncated_result[-1][0] < shortest_time: + shortest_time = truncated_result[-1][0] + shortest_tube = truncated_result + + # Handle must transition + if params.deterministic and len(cur_stack[-1].child) > 0: + next_modes_info = [] + for next_mode in cur_stack[-1].child: + next_modes_info.append((cur_stack[-1].child[next_mode].remain_time, next_mode)) + # This mode gets transit first, only keep this mode + max_remain_time, max_time_mode = max(next_modes_info) + # Pop other modes because of deterministic system + for _, next_mode in next_modes_info: + if next_mode == max_time_mode: + continue + cur_stack[-1].child.pop(next_mode) + candidate_tube = shortest_tube + print("Handle deterministic system, next mode", graph.vs[list(cur_stack[-1].child.keys())[0]]['label']) + + if not candidate_tube: + candidate_tube = cur_bloated_tube + + for i in range(len(candidate_tube)): + candidate_tube[i][0] += cur_mode_stack.start_time + + # Check the safety for current bloated tube + safety = checker.check_reachtube(candidate_tube, cur_label) + if safety == UNSAFE: + print("System is not safe in Mode ", cur_label) + # Start back Tracking from this point and print tube to a file + # push current unsafe_tube to unsafe tube holder + unsafe_tube = [cur_mode_stack.bloated_tube[0]] + candidate_tube + while cur_mode_stack.parent is not None: + prev_mode_stack = cur_mode_stack.parent + unsafe_tube = [prev_mode_stack.bloated_tube[0]] + prev_mode_stack.stack[-1].bloated_tube \ + + unsafe_tube + cur_mode_stack = prev_mode_stack + print('simulation time', sim_end_time - t_start) + print('verification time', time.time() - sim_end_time) + print('refine time', refine_counter) + writeReachTubeFile(unsafe_tube, UNSAFEFILENAME) + ret_reach = ReachTube(cur_mode_stack.bloated_tube, params.variables, params.vertex) + return "UNSAFE", ret_reach + + elif safety == UNKNOWN: + # Refine the current initial set + print(cur_mode_stack.mode, "check bloated tube unknown") + discard_initial = cur_mode_stack.stack.pop() + init_one, init_two = discard_initial.refine() + cur_mode_stack.stack.append(init_one) + cur_mode_stack.stack.append(init_two) + refine_counter += 1 + + elif safety == SAFE: + print("Mode", cur_mode_stack.mode, "check bloated tube safe") + if cur_mode_stack.stack[-1].child: + cur_mode_stack.stack[-1].bloated_tube += candidate_tube + next_mode, next_mode_stack = cur_mode_stack.stack[-1].child.popitem() + cur_mode_stack = next_mode_stack + print("Child exist in cur mode inital", cur_mode_stack.mode, "is cur_mode_stack Now") + else: + cur_mode_stack.bloated_tube += candidate_tube + cur_mode_stack.stack.pop() + print("No child exist in current initial, pop") + + if cur_mode_stack.parent is None: + # We are at head now + if backward_flag == SAFE: + # All the nodes are safe + print("System is Safe!") + print("refine time", refine_counter) + writeReachTubeFile(cur_mode_stack.bloated_tube, REACHTUBEOUTPUT) + ret_reach = ReachTube(cur_mode_stack.bloated_tube, params.variables, params.vertex) + print('simulation time', sim_end_time - t_start) + print('verification time', time.time() - sim_end_time) + return "SAFE", ret_reach + elif backward_flag == UNKNOWN: + print("Hit refine threshold, system halt, result unknown") + print('simulation time', sim_end_time - t_start) + print('verification time', time.time() - sim_end_time) + return "UNKNOWN", None + else: + if backward_flag == SAFE: + prev_mode_stack = cur_mode_stack.parent + prev_mode_stack.stack[-1].bloated_tube += cur_mode_stack.bloated_tube + print('back flag safe from', cur_mode_stack.mode, 'to', prev_mode_stack.mode) + if len(prev_mode_stack.stack[-1].child) == 0: + # There is no next mode from this initial set + prev_mode_stack.bloated_tube += prev_mode_stack.stack[-1].bloated_tube + prev_mode_stack.stack.pop() + cur_mode_stack = prev_mode_stack + print("No child in prev mode initial, pop,", prev_mode_stack.mode, "is cur_mode_stack Now") + else: + # There is another mode transition from this initial set + next_mode, next_mode_stack = prev_mode_stack.stack[-1].child.popitem() + cur_mode_stack = next_mode_stack + print("Child exist in prev mode inital", next_mode_stack.mode, "is cur_mode_stack Now") + elif backward_flag == UNKNOWN: + prev_mode_stack = cur_mode_stack.parent + print('back flag unknown from', cur_mode_stack.mode, 'to', prev_mode_stack.mode) + discard_initial = prev_mode_stack.stack.pop() + init_one, init_two = discard_initial.refine() + prev_mode_stack.stack.append(init_one) + prev_mode_stack.stack.append(init_two) + cur_mode_stack = prev_mode_stack + refine_counter += 1 + + +def graph_search(data, sim_function, param_config=None): + """ + DryVR controller synthesis algorithm. + It does the controller synthesis and print out the search result. + tube and transition graph will be stored in output folder if algorithm finds one + + Args: + data (dict): dictionary that contains params for the input file + sim_function (function): black-box simulation function + param_config (dict or None): user-specified configuration + + Returns: + None + + """ + if param_config is None: + param_config = {} + # There are some fields can be config by user, + # If user specified these fields in paramConfig, + # overload these parameters to userConfig + overloadConfig(userConfig, param_config) + # Parse the input json file and read out the parameters + params = parseRrtInputFile(data) + # Construct objects + checker = UniformChecker(params.unsafeSet, params.variables) + goal_set_checker = GoalChecker(params.goalSet, params.variables) + distance_checker = DistChecker(params.goal, params.variables) + # Read the important param + available_modes = params.modes + start_modes = params.modes + remain_time = params.timeHorizon + min_time_thres = params.minTimeThres + + # Set goal reach flag to False + # Once the flag is set to True, It means we find a transition Graph + goal_reached = False + + # Build the initial mode stack + # Current Method is ugly, we need to get rid of the initial Mode for GraphSearch + # It helps us to achieve the full automate search + # TODO Get rid of the initial Mode thing + random.shuffle(start_modes) + dummy_node = GraphSearchNode("start", remain_time, min_time_thres, 0) + for mode in start_modes: + dummy_node.children[mode] = GraphSearchNode(mode, remain_time, min_time_thres, dummy_node.level + 1) + dummy_node.children[mode].parent = dummy_node + dummy_node.children[mode].initial = (params.initialSet[0], params.initialSet[1]) + + cur_mode_stack = dummy_node.children[start_modes[0]] + dummy_node.visited.add(start_modes[0]) + + t_start = time.time() + while True: + + if not cur_mode_stack: + break + + if cur_mode_stack == dummy_node: + start_modes.pop(0) + if len(start_modes) == 0: + break + + cur_mode_stack = dummy_node.children[start_modes[0]] + dummy_node.visited.add(start_modes[0]) + continue + + print(str(cur_mode_stack)) + + # Keep check the remain time, if the remain time is less than minTime + # It means it is impossible to stay in one mode more than minTime + # Therefore, we have to go back to parents + if cur_mode_stack.remain_time < min_time_thres: + print("Back to previous mode because we cannot stay longer than the min time thres") + cur_mode_stack = cur_mode_stack.parent + continue + + # If we have visited all available modes + # We should select a new candidate point to proceed + # If there is no candidates available, + # Then we can say current node is not valid and go back to parent + if len(cur_mode_stack.visited) == len(available_modes): + if len(cur_mode_stack.candidates) < 2: + print("Back to previous mode because we do not have any other modes to pick") + cur_mode_stack = cur_mode_stack.parent + # If the tried all possible cases with no luck to find path + if not cur_mode_stack: + break + continue + else: + print("Pick a new point from candidates") + cur_mode_stack.candidates.pop(0) + cur_mode_stack.visited = set() + cur_mode_stack.children = {} + continue + + # Generate bloated tube if we haven't done so + if not cur_mode_stack.bloated_tube: + print("no bloated tube find in this mode, generate one") + cur_bloated_tube = calc_bloated_tube( + cur_mode_stack.mode, + cur_mode_stack.initial, + cur_mode_stack.remain_time, + sim_function, + params.bloatingMethod, + params.kvalue, + userConfig.SIMTRACENUM + ) + + # Cut the bloated tube once it intersect with the unsafe set + cur_bloated_tube = checker.cut_tube_till_unsafe(cur_bloated_tube) + + # If the tube time horizon is less than minTime, it means + # we cannot stay in this mode for min thres time, back to the parent node + if not cur_bloated_tube or cur_bloated_tube[-1][0] < min_time_thres: + print("bloated tube is not long enough, discard the mode") + cur_mode_stack = cur_mode_stack.parent + continue + cur_mode_stack.bloated_tube = cur_bloated_tube + + # Generate candidates points for next node + random_sections = cur_mode_stack.random_picker(userConfig.RANDSECTIONNUM) + + if not random_sections: + print("bloated tube is not long enough, discard the mode") + cur_mode_stack = cur_mode_stack.parent + continue + + # Sort random points based on the distance to the goal set + random_sections.sort(key=lambda x: distance_checker.calc_distance(x[0], x[1])) + cur_mode_stack.candidates = random_sections + print("Generate new bloated tube and candidate, with candidates length", len(cur_mode_stack.candidates)) + + # Check if the current tube reaches goal + result, tube = goal_set_checker.goal_reachtube(cur_bloated_tube) + if result: + cur_mode_stack.bloated_tube = tube + goal_reached = True + break + + # We have visited all next mode we have, generate some thing new + # This is actually not necessary, just shuffle all modes would be enough + # There should not be RANDMODENUM things since it does not make any difference + # Anyway, for each candidate point, we will try to visit all modes eventually + # Therefore, using RANDMODENUM to get some random modes visit first is useless + # TODO, fix this part + if len(cur_mode_stack.visited) == len(cur_mode_stack.children): + # leftMode = set(available_modes) - set(cur_mode_stack.children.keys()) + # random_modes = random.sample(leftMode, min(len(leftMode), RANDMODENUM)) + random_modes = available_modes + random.shuffle(random_modes) + + random_sections = cur_mode_stack.random_picker(userConfig.RANDSECTIONNUM) + for mode in random_modes: + candidate = cur_mode_stack.candidates[0] + cur_mode_stack.children[mode] = GraphSearchNode(mode, cur_mode_stack.remain_time - candidate[1][0], + min_time_thres, cur_mode_stack.level + 1) + cur_mode_stack.children[mode].initial = (candidate[0][1:], candidate[1][1:]) + cur_mode_stack.children[mode].parent = cur_mode_stack + + # Random visit a candidate that is not visited before + for key in cur_mode_stack.children: + if key not in cur_mode_stack.visited: + break + + print("transit point is", cur_mode_stack.candidates[0]) + cur_mode_stack.visited.add(key) + cur_mode_stack = cur_mode_stack.children[key] + + # Back track to print out trace + print("RRT run time", time.time() - t_start) + if goal_reached: + print("goal reached") + traces = [] + modes = [] + while cur_mode_stack: + modes.append(cur_mode_stack.mode) + if not cur_mode_stack.candidates: + traces.append([t for t in cur_mode_stack.bloated_tube]) + else: + # Cut the trace till candidate + temp = [] + for t in cur_mode_stack.bloated_tube: + if t == cur_mode_stack.candidates[0][0]: + temp.append(cur_mode_stack.candidates[0][0]) + temp.append(cur_mode_stack.candidates[0][1]) + break + else: + temp.append(t) + traces.append(temp) + if cur_mode_stack.parent != dummy_node: + cur_mode_stack = cur_mode_stack.parent + else: + break + # Reorganize the content in modes list for plotter use + modes = modes[::-1] + traces = traces[::-1] + build_rrt_graph(modes, traces, isIpynb()) + for i in range(1, len(modes)): + modes[i] = modes[i - 1] + '->' + modes[i] + + writeRrtResultFile(modes, traces, RRTOUTPUT) + else: + print("could not find graph") diff --git a/ourtool/dryvr/core/goalchecker.py b/ourtool/dryvr/core/goalchecker.py new file mode 100644 index 0000000000000000000000000000000000000000..ba84296c159c2d102123179eb75a5cb640e71dcc --- /dev/null +++ b/ourtool/dryvr/core/goalchecker.py @@ -0,0 +1,107 @@ +""" +This file contains uniform checker class for DryVR +""" + +from src.common.utils import handleReplace, neg +from z3 import * + + +class GoalChecker: + """ + This is class to check if the goal set is reached + by reach tube + """ + + def __init__(self, goal, variables): + """ + Goal checker class initialization function. + + Args: + goal (str): a str describing the goal set. + For example: "And(x_1>=19.5, x_1<=20.5, x_2>=-1.0, x_2<=1.0)" + variables (list): list of variable name + """ + self.varDic = {'t': Real('t')} + self.variables = variables + for var in variables: + self.varDic[var] = Real(var) + + goal = handleReplace(goal, list(self.varDic.keys())) + + self.intersectChecker = Solver() + self.containChecker = Solver() + + self.intersectChecker.add(eval(goal)) + self.containChecker.add(eval(neg(goal))) + + def goal_reachtube(self, tube): + """ + Check if the reach tube satisfied the goal + + Args: + tube (list): the reach tube. + + Returns: + A bool indicates if the goal is reached + The truncated tube if the goal is reached, otherwise the whole tube + """ + for i in range(0, len(tube), 2): + lower = tube[i] + upper = tube[i + 1] + if self._check_intersection(lower, upper): + if self._check_containment(lower, upper): + return True, tube[:i + 2] + return False, tube + + def _check_intersection(self, lower, upper): + """ + Check if the goal set intersect with the current set + #FIXME Maybe this is not necessary since we only want to check + the fully contained case + Bolun 02/13/2018 + + Args: + lower (list): the list represent the set's lowerbound. + upper (list): the list represent the set's upperbound. + + Returns: + A bool indicates if the set intersect with the goal set + """ + cur_solver = self.intersectChecker + cur_solver.push() + cur_solver.add(self.varDic["t"] >= lower[0]) + cur_solver.add(self.varDic["t"] <= upper[0]) + for i in range(1, len(lower)): + cur_solver.add(self.varDic[self.variables[i - 1]] >= lower[i]) + cur_solver.add(self.varDic[self.variables[i - 1]] <= upper[i]) + if cur_solver.check() == sat: + cur_solver.pop() + return True + else: + cur_solver.pop() + return False + + def _check_containment(self, lower, upper): + """ + Check if the current set contained in goal set. + + Args: + lower (list): the list represent the set's lowerbound. + upper (list): the list represent the set's upperbound. + + Returns: + A bool indicates if the set if contained in the goal set + """ + cur_solver = self.containChecker + cur_solver.push() + cur_solver.add(self.varDic["t"] >= lower[0]) + cur_solver.add(self.varDic["t"] <= upper[0]) + for i in range(1, len(lower)): + cur_solver.add(self.varDic[self.variables[i - 1]] >= lower[i]) + cur_solver.add(self.varDic[self.variables[i - 1]] <= upper[i]) + if cur_solver.check() == sat: + cur_solver.pop() + return False + else: + cur_solver.pop() + return True diff --git a/ourtool/dryvr/core/graph.py b/ourtool/dryvr/core/graph.py new file mode 100644 index 0000000000000000000000000000000000000000..d4770ff2001d436b7c4e0c9408812cea809c2fa1 --- /dev/null +++ b/ourtool/dryvr/core/graph.py @@ -0,0 +1,80 @@ +""" +This file contains graph class for DryVR +""" +# import matplotlib.pyplot as plt +import networkx as nx + + +class Graph: + """ + This is class to plot the progress graph for dryvr's + function, it is supposed to display the graph in jupyter + notebook and update the graph as dryvr is running + """ + + def __init__(self, params, is_ipynb): + """ + Guard checker class initialization function. + + Args: + params (obj): An object contains the parameter + is_ipynb (bool): check if the code is running on ipython or not + """ + self._is_ipynb = is_ipynb + if not is_ipynb: + return + vertex = [] + # Build unique identifier for a vertex and mode name + for idx, v in enumerate(params.vertex): + vertex.append(v + "," + str(idx)) + + edges = params.edge + self.edgeList = [] + for e in edges: + self.edgeList.append((vertex[e[0]], vertex[e[1]])) + # Initialize the plot + # self.fig = plt.figure() + # self.ax = self.fig.add_subplot(111) + + # Initialize the graph + self.G = nx.DiGraph() + self.G.add_edges_from(self.edgeList) + self.pos = nx.spring_layout(self.G) + self.colors = ['green'] * len(self.G.nodes()) + # self.fig.suptitle('', fontsize=10) + # Draw the graph when initialize + # self.draw() + # plt.show() + + def draw(self): + """ + Draw the white-box transition graph. + """ + + nx.draw_networkx_labels(self.G, self.pos) + options = { + 'node_color': self.colors, + 'node_size': 1000, + 'cmap': plt.get_cmap('jet'), + 'arrowstyle': '-|>', + 'arrowsize': 50, + } + nx.draw_networkx(self.G, self.pos, arrows=True, **options) + self.fig.canvas.draw() + + def update(self, cur_node, title, remain_time): + """ + update the graph + Args: + cur_node (str): current vertex dryvr is verifying + title (str): current transition path as the title + remain_time (float): remaining time + """ + if not self._is_ipynb: + return + self.ax.clear() + self.colors = ['green'] * len(self.G.nodes()) + self.colors[list(self.G.nodes()).index(cur_node)] = 'red' + self.fig.suptitle(title, fontsize=10) + self.ax.set_title("remain:" + str(remain_time)) + self.draw() diff --git a/ourtool/dryvr/core/guard.py b/ourtool/dryvr/core/guard.py new file mode 100644 index 0000000000000000000000000000000000000000..76e7e8238defec4aaa34af58a41ce1609455306f --- /dev/null +++ b/ourtool/dryvr/core/guard.py @@ -0,0 +1,213 @@ +""" +This file contains guard class for DryVR +""" + +import random + +import sympy +from z3 import * + +from src.common.utils import handleReplace + + +class Guard: + """ + This is class to calculate the set in the + reach tube that intersect with the guard + """ + + def __init__(self, variables): + """ + Guard checker class initialization function. + + Args: + variables (list): list of variable name + """ + self.varDic = {'t': Real('t')} + self.variables = variables + for var in variables: + self.varDic[var] = Real(var) + + def _build_guard(self, guard_str): + """ + Build solver for current guard based on guard string + + Args: + guard_str (str): the guard string. + For example:"And(v>=40-0.1*u, v-40+0.1*u<=0)" + + Returns: + A Z3 Solver obj that check for guard. + A symbol index dic obj that indicates the index + of variables that involved in the guard. + """ + cur_solver = Solver() + # This magic line here is because SymPy will evaluate == to be False + # Therefore we are not be able to get free symbols from it + # Thus we need to replace "==" to something else + sympy_guard_str = guard_str.replace("==", ">=") + + symbols = list(sympy.sympify(sympy_guard_str, evaluate=False).free_symbols) + symbols = [str(s) for s in symbols] + symbols_idx = {s: self.variables.index(s) + 1 for s in symbols if s in self.variables} + if 't' in symbols: + symbols_idx['t'] = 0 + + guard_str = handleReplace(guard_str, list(self.varDic.keys())) + cur_solver.add(eval(guard_str)) # TODO use an object instead of `eval` a string + return cur_solver, symbols_idx + + def guard_sim_trace(self, trace, guard_str): + """ + Check the guard for simulation trace. + Note we treat the simulation trace as the set as well. + Consider we have a simulation trace as following + [0.0, 1.0, 1.1] + [0.1, 1.02, 1.13] + [0.2, 1.05, 1.14] + ... + We can build set like + lower_bound: [0.0, 1.0, 1.1] + upper_bound: [0.1, 1.02, 1.13] + + lower_bound: [0.1, 1.02, 1.13] + upper_bound: [0.2, 1.05, 1.14] + And check guard for these set. This is because if the guard + is too small, it is likely for simulation point ignored the guard. + For example: + . . . . |guard| . . . + In this case, the guard gets ignored + + Args: + trace (list): the simulation trace + guard_str (str): the guard string. + For example:"And(v>=40-0.1*u, v-40+0.1*u<=0)" + + Returns: + A initial point for next mode, + The truncated simulation trace + """ + if not guard_str: + return None, trace + cur_solver, symbols = self._build_guard(guard_str) + guard_set = {} + + for idx in range(len(trace) - 1): + lower = trace[idx] + upper = trace[idx + 1] + cur_solver.push() + for symbol in symbols: + cur_solver.add(self.varDic[symbol] >= min(lower[symbols[symbol]], upper[symbols[symbol]])) + cur_solver.add(self.varDic[symbol] <= max(lower[symbols[symbol]], upper[symbols[symbol]])) + if cur_solver.check() == sat: + cur_solver.pop() + guard_set[idx] = upper + else: + cur_solver.pop() + if guard_set: + # Guard set is not empty, randomly pick one and return + # idx, point = random.choice(list(guard_set.items())) + idx, point = list(guard_set.items())[0] + # Return the initial point for next mode, and truncated trace + return point[1:], trace[:idx + 1] + + if guard_set: + # Guard set is not empty, randomly pick one and return + # idx, point = random.choice(list(guard_set.items())) + idx, point = list(guard_set.items())[0] + # Return the initial point for next mode, and truncated trace + return point[1:], trace[:idx + 1] + + # No guard hits for current tube + return None, trace + + def guard_sim_trace_time(self, trace, guard_str): + """ + Return the length of the truncated traces + + Args: + trace (list): the simulation trace + guard_str (str): the guard string. + For example:"And(v>=40-0.1*u, v-40+0.1*u<=0)" + + Returns: + the length of the truncated traces. + """ + next_init, trace = self.guard_sim_trace(trace, guard_str) + return len(trace) + + def guard_reachtube(self, tube, guard_str): + """ + Check the guard intersection of the reach tube + + + Args: + tube (list): the reach tube + guard_str (str): the guard string. + For example:"And(v>=40-0.1*u, v-40+0.1*u<=0)" + + Returns: + Next mode initial set represent as [upper_bound, lower_bound], + Truncated tube before the guard, + The time when elapsed in current mode. + + """ + if not guard_str: + return None, tube + + cur_solver, symbols = self._build_guard(guard_str) + guard_set_lower = [] + guard_set_upper = [] + for i in range(0, len(tube), 2): + cur_solver.push() + lower_bound = tube[i] + upper_bound = tube[i + 1] + for symbol in symbols: + cur_solver.add(self.varDic[symbol] >= lower_bound[symbols[symbol]]) + cur_solver.add(self.varDic[symbol] <= upper_bound[symbols[symbol]]) + if cur_solver.check() == sat: + # The reachtube hits the guard + cur_solver.pop() + guard_set_lower.append(lower_bound) + guard_set_upper.append(upper_bound) + + # TODO: If the reachtube completely fall inside guard, break + tmp_solver = Solver() + tmp_solver.add(Not(cur_solver.assertions()[0])) + for symbol in symbols: + tmp_solver.add(self.varDic[symbol] >= lower_bound[symbols[symbol]]) + tmp_solver.add(self.varDic[symbol] <= upper_bound[symbols[symbol]]) + if tmp_solver.check() == unsat: + print("Full intersect, break") + break + else: + cur_solver.pop() + if guard_set_lower: + # Guard set is not empty, build the next initial set and return + # At some point we might further reduce the initial set for next mode + init_lower = guard_set_lower[0][1:] + init_upper = guard_set_upper[0][1:] + for j in range(1, len(guard_set_lower)): + for k in range(1, len(guard_set_lower[0])): + init_lower[k - 1] = min(init_lower[k - 1], guard_set_lower[j][k]) + init_upper[k - 1] = max(init_upper[k - 1], guard_set_upper[j][k]) + # Return next initial Set, the result tube, and the true transit time + return [init_lower, init_upper], tube[:i], guard_set_lower[0][0] + + # Construct the guard if all later trace sat the guard condition + if guard_set_lower: + # Guard set is not empty, build the next initial set and return + # At some point we might further reduce the initial set for next mode + init_lower = guard_set_lower[0][1:] + init_upper = guard_set_upper[0][1:] + for j in range(1, len(guard_set_lower)): + for k in range(1, len(guard_set_lower[0])): + init_lower[k - 1] = min(init_lower[k - 1], guard_set_lower[j][k]) + init_upper[k - 1] = max(init_upper[k - 1], guard_set_upper[j][k]) + # TODO: Treat tau as a special clock variable that don't have variation + # init_upper[0] = init_lower[0] + + # Return next initial Set, the result tube, and the true transit time + return [init_lower, init_upper], tube[:i], guard_set_lower[0][0] + + return None, tube, tube[-1][0] diff --git a/ourtool/dryvr/core/initialset.py b/ourtool/dryvr/core/initialset.py new file mode 100644 index 0000000000000000000000000000000000000000..0bc2a0a4f00648f93e859d83b6d8c0e172b5d69b --- /dev/null +++ b/ourtool/dryvr/core/initialset.py @@ -0,0 +1,69 @@ +""" +This file contains initial set class for DryVR +""" + + +class InitialSet: + """ + This is class to represent the initial set + """ + + def __init__(self, lower, upper): + """ + Initial set class initialization function. + + Args: + lower (list): lower bound of the initial set + upper (list): upper bound of the initial set + """ + + self.lower_bound = lower + self.upper_bound = upper + self.delta = [(upper[i] - lower[i]) / 2.0 for i in range(len(upper))] + # Child point points to children InitialSetStack obj + # This it how it works + # Since a initial set can generate a reach tube that intersect + # with different guards + # So there can be multiple children pointers + # Therefore this is a dictionary obj + # self.child["MODEA"] = InitialSetStack for MODEA + self.child = {} + self.bloated_tube = [] + + def refine(self): + """ + This function refine the current initial set into two smaller set + + Returns: + two refined initial set + + """ + # Refine the initial set into two smaller set + # based on index with largest delta + idx = self.delta.index(max(self.delta)) + # Construct first smaller initial set + init_set_one_ub = list(self.upper_bound) + init_set_one_lb = list(self.lower_bound) + init_set_one_lb[idx] += self.delta[idx] + # Construct second smaller initial set + init_set_two_ub = list(self.upper_bound) + init_set_two_lb = list(self.lower_bound) + init_set_two_ub[idx] -= self.delta[idx] + + return ( + InitialSet(init_set_one_lb, init_set_one_ub), + InitialSet(init_set_two_lb, init_set_two_ub), + ) + + def __str__(self): + """ + Build string representation for the initial set + + Returns: + A string describes the initial set + """ + ret = "" + ret += "Lower Bound: " + str(self.lower_bound) + "\n" + ret += "Upper Bound: " + str(self.upper_bound) + "\n" + ret += "Delta: " + str(self.delta) + "\n" + return ret diff --git a/ourtool/dryvr/core/initialsetstack.py b/ourtool/dryvr/core/initialsetstack.py new file mode 100644 index 0000000000000000000000000000000000000000..e3474a07eb00a97e1fe734eaccc323ea410d2500 --- /dev/null +++ b/ourtool/dryvr/core/initialsetstack.py @@ -0,0 +1,132 @@ +""" +This file contains initial set class stack for DryVR +""" +import random + + +class InitialSetStack: + """ + This is class is for list of initial sets for a single node + """ + + def __init__(self, mode, threshold, remain_time, start_time): + """ + Initial set class initialization function. + + Args: + mode (str): the mode name for current node + threshold (int): threshold for refinement + remain_time (float): remaining time of the current node + """ + + # Mode name for the current mode + self.mode = mode + # Threshold (int) for the refinement + self.threshold = threshold + # Pointer point to the parent node + # If you wonder where child pointers are + # It's actually in the InitialSet class......... + self.parent = None + # A stack for InitialSet object + self.stack = [] + self.remain_time = remain_time + # Stores bloated tube result for all tubes start from initial sets in stack + self.bloated_tube = [] + self.start_time = start_time + + def is_valid(self): + """ + Check if current node is still valid or not. + (if number of initial sets in stack are more than threshold) + + Returns: + A bool to check if the stack is valid or not valid + """ + return len(self.stack) <= self.threshold + + def __str__(self): + """ + Build string representation for the initial set stack + + Args: + None + Returns: + A string describes the stack + """ + ret = "===============================================\n" + ret += "Mode: " + str(self.mode) + "\n" + ret += "stack size: " + str(len(self.stack)) + "\n" + ret += "remainTime: " + str(self.remain_time) + "\n" + return ret + + +class GraphSearchNode: + """ + This is class for graph search node + Contains some helpful stuff in graph search tree + """ + + def __init__(self, mode, remain_time, min_time_thres, level): + """ + GraphSearchNode class initialization function. + + Args: + mode (str): the mode name for current node + remain_time (float): remaining time of the current node + min_time_thres (float): minimal time should stay in this mode + level (int): tree depth + """ + self.mode = mode + # Pointer point to parent node + self.parent = None + # Initial set + self.initial = None + self.bloated_tube = [] + self.remain_time = remain_time + self._min_time_thres = min_time_thres + # Pointer point to children + self.children = {} + # keep track which child is visited + self.visited = set() + # select number of candidates initial set for children + self.candidates = [] + self.level = level + + def random_picker(self, k): + """ + Randomly pick k candidates initial set + + Args: + k (int): number of candidates + Returns: + A list of candidate initial sets + """ + i = 0 + ret = [] + while i < len(self.bloated_tube): + if self.bloated_tube[i][0] >= self._min_time_thres: + ret.append((self.bloated_tube[i], self.bloated_tube[i + 1])) + i += 2 + + random.shuffle(ret) + return ret[:k] + + def __str__(self): + """ + Build string representation for the Graph Search Node + + Returns: + A string describes the stack + """ + ret = "" + ret += "Level: " + str(self.level) + "\n" + ret += "Mode: " + str(self.mode) + "\n" + ret += "Initial: " + str(self.initial) + "\n" + ret += "visited: " + str(self.visited) + "\n" + ret += "num candidates: " + str(len(self.candidates)) + "\n" + ret += "remaining time: " + str(self.remain_time) + "\n" + if self.bloated_tube: + ret += "bloatedTube: True" + "\n" + else: + ret += "bloatedTube: False" + "\n" + return ret diff --git a/ourtool/dryvr/core/reachtube.py b/ourtool/dryvr/core/reachtube.py new file mode 100644 index 0000000000000000000000000000000000000000..f985f43778cc4fe2b8ef7f165025a6394e32a17b --- /dev/null +++ b/ourtool/dryvr/core/reachtube.py @@ -0,0 +1,88 @@ +""" +This file contains reach tube class for DryVR +""" + +import six + + +class ReachTube: + """ + This is class is an object for reach tube + Ideally it should support to fetch reachtube by mode and variable name + And it should allow users to plot the reach tube in different ways + """ + + def __init__(self, tube, variables, modes): + """ + ReachTube class initialization function. + + Args: + tube (list): raw reach tube (that used to print to file) + variables (list): list of variables in the reach tube + modes (list): list of modes in the reach ReachTube + """ + self._tube = tube + self._variables = variables + self._modes = modes + + # Build the raw string representation so user can print it + + self.raw = "" + for line in tube: + if isinstance(line, str): + self.raw += line + "\n" + else: + self.raw += " ".join(map(str, line)) + '\n' + + # Build dictionary object so you can easily pull out part of the list + self._tube_dict = {} + for mode in modes: + self._tube_dict[mode] = {} + for var in variables + ["t"]: + self._tube_dict[mode][var] = [] + + cur_mode = "" + for line in tube: + if isinstance(line, six.string_types): + cur_mode = line.split('->')[-1].split(',')[0] # Get current mode name + for var in ['t'] + self._variables: + self._tube_dict[cur_mode][var].append(line) + else: + for var, val in zip(['t'] + self._variables, line): + self._tube_dict[cur_mode][var].append(val) + + def __str__(self): + """ + print the raw tube + """ + return str(self.raw) + + def filter(self, mode=None, variable=None, contain_label=True): + """ + This is a filter function that allows you to select + Args: + mode (str, list): single mode name or list of mode name + variable (str, list): single variable or list of variables + """ + if mode is None: + mode = self._modes + if variable is None: + variable = ["t"] + self._variables + + if isinstance(mode, str): + mode = [mode] + if isinstance(variable, str): + variable = [variable] + + res = [] + for m in mode: + temp = [] + for i in range(len(self._tube_dict[m]["t"])): + if isinstance(self._tube_dict[m]["t"][i], str): + if contain_label: + temp.append([self._tube_dict[m]["t"][i]] + variable) + continue + + temp.append([self._tube_dict[m][v][i] for v in variable]) + res.append(temp) + return res diff --git a/ourtool/dryvr/core/reset.py b/ourtool/dryvr/core/reset.py new file mode 100644 index 0000000000000000000000000000000000000000..684b9c5e2b3af8de2ba8231d51e8d3dafdd32875 --- /dev/null +++ b/ourtool/dryvr/core/reset.py @@ -0,0 +1,209 @@ +""" +This file contains reset class for DryVR +""" + +import sympy + +from src.common.utils import randomPoint + + +class Reset: + """ + This is class for resetting the initial set + """ + + def __init__(self, variables): + """ + Reset class initialization function. + + Args: + variables (list): list of varibale name + """ + self.variables = variables + + def reset_set(self, raw_eqs_str, lower_bound, upper_bound): + """ + Reset the initial set based on reset expressions + + Args: + raw_eqs_str (str): reset expressions separated by ';' + lower_bound (list): lower bound of the initial set + upper_bound (list): upper bound of the initial set + + Returns: + lower bound and upper bound of the initial set after reset + """ + if not raw_eqs_str: + return lower_bound, upper_bound + + raw_eqs = raw_eqs_str.split(';') + lb_list = [] + ub_list = [] + for rawEqu in raw_eqs: + lb, ub = self._handle_reset(rawEqu, lower_bound, upper_bound) + lb_list.append(lb) + ub_list.append(ub) + + return self._merge_result(lb_list, ub_list, lower_bound, upper_bound) + + def reset_point(self, raw_eqs_str, point): + """ + Reset the initial point based on reset expressions + + Args: + raw_eqs_str (str): list of reset expression + point (list): the initial point need to be reset + + Returns: + a point after reset + """ + if point == [] or not point: + return point + lower, upper = self.reset_set(raw_eqs_str, point, point) + return randomPoint(lower, upper) + + @staticmethod + def _merge_result(lb_list, ub_list, lower_bound, upper_bound): + """ + Merge the a list of reset result + Since we allow multiple reset per transition, + we get list of reset result, each result corresponding to one reset expression + We need to merge all reset result together + + Args: + lb_list (list): list of reset lower bound results + ub_list (list): list of reset upper bound results + lower_bound(list): original lower bound + upper_bound(list): original upper bound + + Returns: + Upper bound and lower bound after merge the reset result + """ + ret_lb = list(lower_bound) + ret_ub = list(upper_bound) + + for i in range(len(lb_list)): + cur_lb = lb_list[i] + cur_ub = ub_list[i] + for j in range(len(cur_lb)): + if cur_lb[j] != lower_bound[j]: + ret_lb[j] = cur_lb[j] + if cur_ub[j] != upper_bound[j]: + ret_ub[j] = cur_ub[j] + return ret_lb, ret_ub + + def _build_all_combo(self, symbols, lower_bound, upper_bound): + """ + This function allows us to build all combination given symbols + For example, if we have a 2-dimension set for dim A and B. + symbols = [A,B] + lowerBound = [1.0, 2.0] + upperBound = [3.0, 4.0] + Then the result should be all possible combination of the value of A and B + result: + [[1.0, 2.0], [3.0, 4.0], [3.0, 2.0], [1.0, 4.0]] + + Args: + symbols (list): symbols we use to create combo + lower_bound (list): lower bound of the set + upper_bound (list): upper bound of the set + + Returns: + List of combination value + """ + if not symbols: + return [] + + cur_symbol = str(symbols[0]) + idx = self.variables.index(cur_symbol) + lo = lower_bound[idx] + up = upper_bound[idx] + ret = [] + next_level = self._build_all_combo(symbols[1:], lower_bound, upper_bound) + if next_level: + for n in next_level: + ret.append(n + [(cur_symbol, lo)]) + ret.append(n + [(cur_symbol, up)]) + else: + ret.append([cur_symbol, lo]) + ret.append([cur_symbol, up]) + return ret + + def _handle_wrapped_reset(self, raw_eq, lower_bound, upper_bound): + """ + This is a function to handle reset such as V = [0, V+1] + + Args: + raw_eq (str): reset equation + lower_bound (list): lower bound of the set + upper_bound (list): upper bound of the set + + Returns: + Upper bound and lower bound after the reset + """ + final_equ = sympy.sympify(raw_eq) + rhs_symbols = list(final_equ.free_symbols) + combos = self._build_all_combo(rhs_symbols, lower_bound, upper_bound) + min_reset = float('inf') + max_reset = float('-inf') + if combos: + for combo in combos: + if len(combo) == 2: + result = float(final_equ.subs(combo[0], combo[1])) + else: + result = float(final_equ.subs(combo)) + min_reset = min(min_reset, float(result)) + max_reset = max(max_reset, float(result)) + else: + min_reset = float(final_equ) + max_reset = float(final_equ) + return (min_reset, max_reset) + + def _handle_reset(self, raw_equ, lower_bound, upper_bound): + """ + Handle the reset with single reset expression + + Args: + raw_equ (str): reset equation + lower_bound (list): lower bound of the set + upper_bound (list): upper bound of the set + + Returns: + Upper bound and lower bound after the reset + """ + equ_split = raw_equ.split('=') + lhs, rhs = equ_split[0], equ_split[1] + target = sympy.sympify(lhs) + # Construct the equation + final_equ = sympy.sympify(rhs) + if not isinstance(final_equ, list): + rhs_symbols = list(sympy.sympify(rhs).free_symbols) + else: + rhs_symbols = None + # print target, rhs_symbols + combos = self._build_all_combo(rhs_symbols, lower_bound, upper_bound) + # final_equ = solve(equ, target)[0] + + min_reset = float('inf') + max_reset = float('-inf') + if combos: + for combo in combos: + if len(combo) == 2: + result = float(final_equ.subs(combo[0], combo[1])) + else: + result = float(final_equ.subs(combo)) + min_reset = min(min_reset, float(result)) + max_reset = max(max_reset, float(result)) + elif isinstance(final_equ, list): + min_reset = min(self._handle_wrapped_reset(final_equ[0], lower_bound, upper_bound)) + max_reset = max(self._handle_wrapped_reset(final_equ[1], lower_bound, upper_bound)) + else: + min_reset = float(final_equ) + max_reset = float(final_equ) + + ret_lb = list(lower_bound) + ret_ub = list(upper_bound) + target_idx = self.variables.index(str(target)) + ret_lb[target_idx] = min_reset + ret_ub[target_idx] = max_reset + return ret_lb, ret_ub diff --git a/ourtool/dryvr/core/uniformchecker.py b/ourtool/dryvr/core/uniformchecker.py new file mode 100644 index 0000000000000000000000000000000000000000..57641ad630bed35fed0199d015a90b7af6ba9210 --- /dev/null +++ b/ourtool/dryvr/core/uniformchecker.py @@ -0,0 +1,213 @@ +""" +This file contains uniform checker class for DryVR +""" +import sympy +from z3 import * + +from src.common.constant import * +from src.common.utils import handleReplace, neg + + +class UniformChecker: + """ + This is class for check unsafe checking + """ + + def __init__(self, unsafe, variables): + """ + Reset class initialization function. + + Args: + unsafe (str): unsafe constraint + variables (list): list of variable name + """ + self.varDic = {'t': Real('t')} + self._variables = variables + self._solver_dict = {} + for var in variables: + self.varDic[var] = Real(var) + + if not unsafe: + return + + original = unsafe + + unsafe = handleReplace(unsafe, list(self.varDic.keys())) + unsafe_list = unsafe[1:].split('@') + for unsafe in unsafe_list: + mode, cond = unsafe.split(':') + self._solver_dict[mode] = [Solver(), Solver()] + self._solver_dict[mode][0].add(eval(cond)) + self._solver_dict[mode][1].add(eval(neg(cond))) + + unsafe_list = original[1:].split('@') + for unsafe in unsafe_list: + mode, cond = unsafe.split(':') + # This magic line here is because SymPy will evaluate == to be False + # Therefore we are not be able to get free symbols from it + # Thus we need to replace "==" to something else, which is >= + cond = cond.replace("==", ">=") + symbols = list(sympy.sympify(cond).free_symbols) + symbols = [str(s) for s in symbols] + symbols_idx = {s: self._variables.index(s) + 1 for s in symbols if s in self._variables} + if 't' in symbols: + symbols_idx['t'] = 0 + self._solver_dict[mode].append(symbols_idx) # TODO Fix typing + + def check_sim_trace(self, traces, mode): + """ + Check the simulation trace + + Args: + traces (list): simulation traces + mode (str): mode need to be checked + + Returns: + An int for checking result SAFE = 1, UNSAFE = -1 + """ + if mode in self._solver_dict: + cur_solver = self._solver_dict[mode][0] + symbols = self._solver_dict[mode][2] + elif 'Allmode' in self._solver_dict: + cur_solver = self._solver_dict['Allmode'][0] + symbols = self._solver_dict['Allmode'][2] + else: + # Return True if we do not check this mode + return SAFE + + for t in traces: + cur_solver.push() + for symbol in symbols: + cur_solver.add(self.varDic[symbol] == t[symbols[symbol]]) + + if cur_solver.check() == sat: + cur_solver.pop() + return UNSAFE + else: + cur_solver.pop() + return SAFE + + def check_reachtube(self, tube, mode): + """ + Check the bloated reach tube + + Args: + tube (list): reach tube + mode (str): mode need to be checked + + Returns: + An int for checking result SAFE = 1, UNSAFE = -1, UNKNOWN = 0 + """ + if mode not in self._solver_dict and 'Allmode' not in self._solver_dict: + # Return True if we do not check this mode + return SAFE + + safe = SAFE + for i in range(0, len(tube), 2): + lower = tube[i] + upper = tube[i + 1] + if self._check_intersection(lower, upper, mode): + if self._check_containment(lower, upper, mode): + # The unsafe region is fully contained + return UNSAFE + else: + # We do not know if it is truly unsafe or not + safe = UNKNOWN + return safe + + def cut_tube_till_unsafe(self, tube): + """ + Truncated the tube before it intersect with unsafe set + + Args: + tube (list): reach tube + + Returns: + truncated tube + """ + if not self._solver_dict: + return tube + # Cut the reach tube till it intersect with unsafe + for i in range(0, len(tube), 2): + lower = tube[i] + upper = tube[i + 1] + if self._check_intersection(lower, upper, 'Allmode'): + # we need to cut here + return tube[:i] + + return tube + + def _check_intersection(self, lower, upper, mode): + """ + Check if current set intersect with the unsafe set + + Args: + lower (list): lower bound of the current set + upper (list): upper bound of the current set + mode (str): the mode need to be checked + + Returns: + Return a bool to indicate if the set intersect with the unsafe set + """ + if mode in self._solver_dict: + cur_solver = self._solver_dict[mode][0] + symbols = self._solver_dict[mode][2] + elif 'Allmode' in self._solver_dict: + cur_solver = self._solver_dict['Allmode'][0] + symbols = self._solver_dict['Allmode'][2] + else: + raise ValueError("Unknown mode '" + mode + "'") + + cur_solver.push() + for symbol in symbols: + cur_solver.add(self.varDic[symbol] >= lower[symbols[symbol]]) + cur_solver.add(self.varDic[symbol] <= upper[symbols[symbol]]) + + check_result = cur_solver.check() + + if check_result == sat: + cur_solver.pop() + return True + if check_result == unknown: + print("Z3 return unknown result") + exit() # TODO Proper return instead of exit + else: + cur_solver.pop() + return False + + def _check_containment(self, lower, upper, mode): + """ + Check if the current set is fully contained in unsafe region + + Args: + lower (list): lower bound of the current set + upper (list): upper bound of the current set + mode (str): the mode need to be checked + + Returns: + Return a bool to indicate if the set is fully contained in unsafe region + """ + if mode in self._solver_dict: + cur_solver = self._solver_dict[mode][1] + symbols = self._solver_dict[mode][2] + elif 'Allmode' in self._solver_dict: + cur_solver = self._solver_dict['Allmode'][1] + symbols = self._solver_dict['Allmode'][2] + else: + raise ValueError("Unknown mode '" + mode + "'") + + cur_solver.push() + for symbol in symbols: + cur_solver.add(self.varDic[symbol] >= lower[symbols[symbol]]) + cur_solver.add(self.varDic[symbol] <= upper[symbols[symbol]]) + check_result = cur_solver.check() + + if check_result == sat: + cur_solver.pop() + return False + if check_result == unknown: + print("Z3 return unknown result") + exit() # TODO Proper return instead of exit + else: + cur_solver.pop() + return True diff --git a/ourtool/dryvr/discrepancy/Global_Disc.py b/ourtool/dryvr/discrepancy/Global_Disc.py new file mode 100644 index 0000000000000000000000000000000000000000..d84b5176e7c3ae8aca917af0ea533e5f6aab137d --- /dev/null +++ b/ourtool/dryvr/discrepancy/Global_Disc.py @@ -0,0 +1,125 @@ +""" +This file contains core bloating algorithm for dryvr +""" + +from typing import List, Tuple +import numpy as np +import scipy as sp +import scipy.spatial + +_TRUE_MIN_CONST = -10 +_EPSILON = 1.0e-6 +_SMALL_EPSILON = 1e-10 + + +def get_reachtube_segment(training_traces: np.ndarray, initial_radii: np.ndarray, method='PWGlobal') -> np.array: + num_traces: int = training_traces.shape[0] + ndims: int = training_traces.shape[2] # This includes time + trace_len: int = training_traces.shape[1] + center_trace: np.ndarray = training_traces[0, :, :] + trace_initial_time = center_trace[0, 0] + x_points: np.ndarray = center_trace[:, 0] - trace_initial_time + assert np.all(training_traces[0, :, 0] == training_traces[1:, :, 0]) + y_points: np.ndarray = all_sensitivities_calc(training_traces, initial_radii) + points: np.ndarray = np.zeros((ndims - 1, trace_len, 2)) + points[np.where(initial_radii != 0), 0, 1] = 1.0 + points[:, :, 0] = np.reshape(x_points, (1, x_points.shape[0])) + points[:, 1:, 1] = y_points + normalizing_initial_set_radii: np.ndarray = initial_radii.copy() + normalizing_initial_set_radii[np.where(normalizing_initial_set_radii == 0)] = 1.0 + df: np.ndarray = np.zeros((trace_len, ndims)) + if method == 'PW': + df[:, 1:] = np.transpose( + points[:, :, 1] * np.reshape(normalizing_initial_set_radii, (normalizing_initial_set_radii.size, 1))) + elif method == 'PWGlobal': + # replace zeros with epsilons + # points[np.where(points[:, 0, 1] == 0), 0, 1] = 1.0e-100 + # to fit exponentials make y axis log of sensitivity + points[:, :, 1] = np.maximum(points[:, :, 1], _EPSILON) + points[:, :, 1] = np.log(points[:, :, 1]) + for dim_ind in range(1, ndims): + new_min = min(np.min(points[dim_ind - 1, 1:, 1]) + _TRUE_MIN_CONST, -10) + if initial_radii[dim_ind - 1] == 0: + # exclude initial set, then add true minimum points + new_points: np.ndarray = np.row_stack( + (np.array((points[dim_ind - 1, 1, 0], new_min)), np.array((points[dim_ind - 1, -1, 0], new_min)))) + else: + # start from zero, then add true minimum points + new_points: np.ndarray = np.row_stack((points[dim_ind - 1, 0, :], + np.array((points[dim_ind - 1, 0, 0], new_min)), + np.array((points[dim_ind - 1, -1, 0], new_min)))) + df[0, dim_ind] = initial_radii[dim_ind - 1] + # Tuple order is start_time, end_time, slope, y-intercept + cur_dim_points = np.concatenate((points[dim_ind - 1, 1:, :], new_points), axis=0) + cur_hull: sp.spatial.ConvexHull = sp.spatial.ConvexHull(cur_dim_points) + linear_separators: List[Tuple[float, float, float, float, int, int]] = [] + vert_inds = list(zip(cur_hull.vertices[:-1], cur_hull.vertices[1:])) + vert_inds.append((cur_hull.vertices[-1], cur_hull.vertices[0])) + for end_ind, start_ind in vert_inds: + if cur_dim_points[start_ind, 1] != new_min and cur_dim_points[end_ind, 1] != new_min: + slope = (cur_dim_points[end_ind, 1] - cur_dim_points[start_ind, 1]) / ( + cur_dim_points[end_ind, 0] - cur_dim_points[start_ind, 0]) + y_intercept = cur_dim_points[start_ind, 1] - cur_dim_points[start_ind, 0] * slope + start_time = cur_dim_points[start_ind, 0] + end_time = cur_dim_points[end_ind, 0] + assert start_time < end_time + if start_time == 0: + linear_separators.append((start_time, end_time, slope, y_intercept, 0, end_ind + 1)) + else: + linear_separators.append((start_time, end_time, slope, y_intercept, start_ind + 1, end_ind + 1)) + linear_separators.sort() + prev_val = 0 + prev_ind = 1 if initial_radii[dim_ind - 1] == 0 else 0 + for linear_separator in linear_separators: + _, _, slope, y_intercept, start_ind, end_ind = linear_separator + assert prev_ind == start_ind + assert start_ind < end_ind + segment_t = center_trace[start_ind:end_ind + 1, 0] + segment_df = normalizing_initial_set_radii[dim_ind - 1] * np.exp(y_intercept) * np.exp( + slope * segment_t) + segment_df[0] = max(segment_df[0], prev_val) + df[start_ind:end_ind + 1, dim_ind] = segment_df + prev_val = segment_df[-1] + prev_ind = end_ind + else: + print('Discrepancy computation method,', method, ', is not supported!') + raise ValueError + assert (np.all(df >= 0)) + reachtube_segment: np.ndarray = np.zeros((trace_len - 1, 2, ndims)) + reachtube_segment[:, 0, :] = np.minimum(center_trace[1:, :] - df[1:, :], center_trace[:-1, :] - df[:-1, :]) + reachtube_segment[:, 1, :] = np.maximum(center_trace[1:, :] + df[1:, :], center_trace[:-1, :] + df[:-1, :]) + # assert 100% training accuracy (all trajectories are contained) + for trace_ind in range(training_traces.shape[0]): + if not (np.all(reachtube_segment[:, 0, :] <= training_traces[trace_ind, 1:, :]) and np.all(reachtube_segment[:, 1, :] >= training_traces[trace_ind, 1:, :])): + assert np.any(np.abs(training_traces[trace_ind, 0, 1:]-center_trace[0, 1:]) > initial_radii) + print(f"Warning: Trace #{trace_ind}", "of this initial set is sampled outside of the initial set because of floating point error and is not contained in the initial set") + return reachtube_segment + + +def all_sensitivities_calc(training_traces: np.ndarray, initial_radii: np.ndarray): + num_traces: int + trace_len: int + ndims: int + num_traces, trace_len, ndims = training_traces.shape + normalizing_initial_set_radii: np.array = initial_radii.copy() + y_points: np.array = np.zeros((normalizing_initial_set_radii.shape[0], trace_len - 1)) + normalizing_initial_set_radii[np.where(normalizing_initial_set_radii == 0)] = 1.0 + for cur_dim_ind in range(1, ndims): + normalized_initial_points: np.array = training_traces[:, 0, 1:] / normalizing_initial_set_radii + initial_distances = sp.spatial.distance.pdist(normalized_initial_points, 'chebyshev') + _SMALL_EPSILON + for cur_time_ind in range(1, trace_len): + y_points[cur_dim_ind - 1, cur_time_ind - 1] = np.max((sp.spatial.distance.pdist( + np.reshape(training_traces[:, cur_time_ind, cur_dim_ind], + (training_traces.shape[0], 1)), 'chebychev' + )/ normalizing_initial_set_radii[cur_dim_ind - 1]) / initial_distances) + return y_points + +if __name__=="__main__": + with open("test.npy", "rb") as f: + training_traces = np.load(f) + initial_radii = np.array([1.96620653e-06, 2.99999995e+00, 3.07000514e-07, 8.84958773e-13, 1.05625786e-16, 3.72500000e+00, 0.00000000e+00, 0.00000000e+00]) + result = get_reachtube_segment(training_traces, initial_radii, method='PWGlobal') + print(training_traces.dtype) + # plot_rtsegment_and_traces(result, training_traces[np.array((0, 6))]) + + diff --git a/ourtool/dryvr/discrepancy/PW_Discrepancy.py b/ourtool/dryvr/discrepancy/PW_Discrepancy.py new file mode 100644 index 0000000000000000000000000000000000000000..53cac39418bfb66277fc7a37b25387648e5c076e --- /dev/null +++ b/ourtool/dryvr/discrepancy/PW_Discrepancy.py @@ -0,0 +1,341 @@ +from __future__ import division, print_function + +import math + + +def read_data(traces): + """ Read in all the traces """ + error_thred_time = 1e-3 + + trace = traces[0] + delta_time = trace[1][0] - trace[0][0] + + # Calculate variables + dimensions = len(trace[0]) + dimensions_nt = dimensions - 1 + end_time = trace[-1][0] + + # Align all the traces + for i in range(len(traces)): + initial_time = traces[i][0][0] + for j in range(len(traces[i])): + traces[i][j][0] = traces[i][j][0] - initial_time + + # reassign the start time and end time + start_time = 0 + for i in range(len(traces)): + end_time = min(end_time, traces[i][-1][0]) + + # trim all the points after the end time + traces_trim = traces + + # reassign trace_len + trace_len = len(traces_trim[0]) + + return (traces_trim, dimensions, dimensions_nt, trace_len, end_time, delta_time, start_time) + + +def compute_diff(traces): + """ Compute difference between traces """ + # Iterate over all combinations + traces_diff = [] + for i in range(0, len(traces)): + for j in range(i + 1, len(traces)): + + trace_diff = [] + + # Iterate over the data of the trace + for t in range(0, len(traces[i])): + diff = [abs(x_i - y_i) for x_i, y_i in zip(traces[i][t], + traces[j][t])] + trace_diff.append(diff[1:]) + + # Append to traces diff minus time difference + traces_diff.append(trace_diff) + + # TODO Eliminate hardcoded file name + with open('output/tracediff2.txt', 'w') as write_file: + for i in range(len(traces_diff)): + for j in range(len(traces_diff[0])): + write_file.write(str(traces_diff[i][j]) + '\n') + write_file.write('\n') + return traces_diff + + +def find_time_intervals(traces_diff, dimensions_nt, end_time, trace_len, delta_time, K_value): + """ Compute the time intervals """ + # FIXME just do 1 dimension for now + # Iterate through all dimensions + num_ti = [] + time_intervals = [] + + for i in range(0, dimensions_nt): + + time_dim = [] + + # Obtain difference at start of interval + diff_0 = [] + t_0 = 0.0 + time_dim.append(t_0) + for k in range(0, len(traces_diff)): + diff_0.append(traces_diff[k][0][i]) + # Iterate through all points in trace + + for j in range(1, trace_len): + # Obtain difference at ith time of interval + diff_i = [] + try: + for k in range(0, len(traces_diff)): + diff_i.append(traces_diff[k][j][i]) + except IndexError: + print(trace_len) + print(k, j, i) + print(len(traces_diff[k])) + print(len(traces_diff[k][j])) + + # Check time + t_i = j * delta_time + t = t_i - t_0 + if t <= 0: + continue + + # Compute ratios + ratio = [] + for d_0, d_i in zip(diff_0, diff_i): + if d_i < 1E-3: + continue + elif d_0 < 1E-3: + continue + + # NOTE not sure if this is right? + # ratio.append((1 / t) * math.log(d_i / d_0)) + ratio.append(d_i / d_0) + + # Check ratios if less than constant + # new_int = all(r <= 2.0*K_value[i] for r in ratio) + # new_int = all(r <= 2**(2*t)*K_value[i] for r in ratio) + new_int = all(r <= 1 for r in ratio) + if new_int == False: + if t_i != end_time: + time_dim.append(t_i) + diff_0 = diff_i + t_0 = t_i + + # Append the time intervals + time_dim.append(end_time) + time_intervals.append(time_dim) + # record the number of time intervals + num_ti.append(len(time_intervals[i]) - 1) + + return (time_intervals, num_ti) + + +# Compute discrepancies +def calculate_discrepancies(time_intervals, traces_diff, dimensions_nt, delta_time, K_value): + # FIXME + # Iterate over all dimensions + discrepancies = [] + for nd in range(0, dimensions_nt): + # for nd in xrange(0, P_DIM): + disc = [] + + # Iterate over all time intervals + for ni in range(0, len(time_intervals[nd]) - 1): + t_0 = time_intervals[nd][ni] + t_e = time_intervals[nd][ni + 1] + # t_i = t_0 + delta_time + + # FIXME (???) + # print "note",delta_time + points = int((t_e - t_0) / delta_time + 0.5) + 1 + idx = int(t_0 / delta_time) + + # try to find the best K and gamma + tmp_K_value = K_value[nd] + # Iterate over all trace difference + glpk_rows = [] + close_flag = 0 + for k in range(0, len(traces_diff)): + + # Compute initial + diff_0 = traces_diff[k][0][nd] + if diff_0 <= 1E-3: + # print('Find two traces to be too closed!') + # print('use the default value!') + close_flag = 1 + break + ln_0 = math.log(diff_0) + + # FIXME need to reset the delta_time here + t_i = t_0 + delta_time + # print(disc) + # Obtain rows for GLPK + for r in range(1, points): + t_d = t_i - t_0 + t_i += delta_time + diff_i = traces_diff[k][idx + r][nd] + + if diff_i < 1E-3: + continue + + ln_i = math.log(diff_i) + + # compute the existing previous time interval discrepancy + discrepancy_now = 0 + if len(disc) != 0: + for time_prev in range(0, len(disc)): + discrepancy_now = discrepancy_now + disc[time_prev] * ( + time_intervals[nd][time_prev + 1] - time_intervals[nd][time_prev]) + + ln_d = ln_i - ln_0 - math.log(tmp_K_value) - discrepancy_now + glpk_rows.append([t_d, ln_d]) + + # Debugging algebraic solution + if close_flag == 0: + alg = [d / t for t, d in glpk_rows] + if len(alg) != 0: + alg_max = max(alg) + else: + alg_max = 0 + else: + alg_max = 0 + + disc.append(alg_max) + + # Append discrepancies + discrepancies.append(disc) + + return discrepancies + + +# Obtain bloated tube +def generate_bloat_tube(traces, time_intervals, discrepancies, Initial_Delta, end_time, trace_len, dimensions_nt, + delta_time, K_value): + + # Iterate over all dimensions + # FIXME + bloat_tube = [] + for i in range(trace_len): + bloat_tube.append([]) + bloat_tube.append([]) + + for nd in range(0, dimensions_nt): + # for nd in xrange(P_DIM - 1, P_DIM): + + time_bloat = [] + low_bloat = [] + up_bloat = [] + + # To construct the reach tube + time_tube = [] + tube = [] + + prev_delta = Initial_Delta[nd] + + # Iterate over all intervals + previous_idx = -1 + + for ni in range(0, len(time_intervals[nd]) - 1): + t_0 = time_intervals[nd][ni] + t_e = time_intervals[nd][ni + 1] + + if t_e == end_time: + points = int((t_e - t_0) / delta_time + 0.5) + 1 + else: + points = int((t_e - t_0) / delta_time + 0.5) + idx = int(t_0 / delta_time) + + gamma = discrepancies[nd][ni] + + # Iterate over all points in center trace + for r in range(0, points): + + current_idx = idx + r + + if current_idx != previous_idx + 1: + # print('Index mismatch found!') + if current_idx == previous_idx: + idx += 1 + elif current_idx == previous_idx + 2: + idx -= 1 + + pnt = traces[0][idx + r] + pnt_time = pnt[0] + pnt_data = pnt[nd + 1] + + cur_delta = prev_delta * math.exp(gamma * delta_time) + max_delta = max(prev_delta, cur_delta) + + time_bloat.append(pnt_time) + low_bloat.append(pnt_data - max_delta * K_value[nd]) + up_bloat.append(pnt_data + max_delta * K_value[nd]) + + if nd == 0: + bloat_tube[2 * (idx + r)].append(pnt_time) + bloat_tube[2 * (idx + r)].append(pnt_data - max_delta * K_value[nd]) + bloat_tube[2 * (idx + r) + 1].append(pnt_time + delta_time) + bloat_tube[2 * (idx + r) + 1].append(pnt_data + max_delta * K_value[nd]) + else: + bloat_tube[2 * (idx + r)].append(pnt_data - max_delta * K_value[nd]) + bloat_tube[2 * (idx + r) + 1].append(pnt_data + max_delta * K_value[nd]) + + prev_delta = cur_delta + + previous_idx = idx + r + + return bloat_tube + +# Print out the intervals and discrepancies +def print_int_disc(discrepancies, time_intervals): + for nd in range(0, len(discrepancies)): + for p in range(0, len(discrepancies[nd])): + print('idx: ' + str(p) + ' int: ' + str(time_intervals[nd][p]) + + ' to ' + str(time_intervals[nd][p + 1]) + ', disc: ' + + str(discrepancies[nd][p])) + print('') + +def PW_Bloat_to_tube(Initial_Delta, plot_flag, plot_dim, traces, K_value): + # Read data in + # if Mode == 'Const': + # K_value = [1.0,1.0,2.0] + # elif Mode == 'Brake': + # K_value = [1.0,1.0,7.0] + + # if Mode == 'Const;Const': + # K_value = [1.0,1.0,2.0,1.0,1.0,2.0] + # elif Mode == 'Brake;Const': + # K_value = [1.0,1.0,2.0,1.0,1.0,2.0] + + # elif Mode == 'Brake;Brake': + # K_value = [1.0,1.0,5.0,1.0,1.0,2.0] + + traces, dimensions, dimensions_nt, trace_len, end_time, delta_time, start_time = read_data(traces) + # Compute difference between traces + traces_diff = compute_diff(traces) + # print traces_diff + + # Find time intervals for discrepancy calculations + time_intervals, num_ti = find_time_intervals(traces_diff, dimensions_nt, end_time, trace_len, delta_time, K_value) + # print('number of time intervals:') + # print num_ti + # Discrepancy calculation + discrepancies = calculate_discrepancies(time_intervals, traces_diff, dimensions_nt, delta_time, K_value) + # print('The K values') + # print K_value, + # system.exit('test') + + # Write discrepancies to file + # write_to_file(time_intervals,discrepancies,write_path +' disp.txt', 'disc') + + # Nicely print the intervals and discrepancies + # print_int_disc(discrepancies,time_intervals) + + # Bloat the tube using time intervals + reach_tube = generate_bloat_tube(traces, time_intervals, discrepancies, Initial_Delta, end_time, trace_len, + dimensions_nt, delta_time, K_value) + + # if plot_flag: + # plot_traces(traces, plot_dim, reach_tube) + # plt.show() + + return reach_tube diff --git a/ourtool/dryvr/discrepancy/__init__.py b/ourtool/dryvr/discrepancy/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ourtool/dryvr/plotter/.ipynb_checkpoints/plotterInterface-checkpoint.ipynb b/ourtool/dryvr/plotter/.ipynb_checkpoints/plotterInterface-checkpoint.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..2fd64429bf421126b7000c94ce0f6fd186fbd01f --- /dev/null +++ b/ourtool/dryvr/plotter/.ipynb_checkpoints/plotterInterface-checkpoint.ipynb @@ -0,0 +1,6 @@ +{ + "cells": [], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/ourtool/dryvr/plotter/README.md b/ourtool/dryvr/plotter/README.md new file mode 100644 index 0000000000000000000000000000000000000000..2ea9534697e8de9a5fa0ab9831ce4102499684ab --- /dev/null +++ b/ourtool/dryvr/plotter/README.md @@ -0,0 +1 @@ +This folder consist plotter code for DryVR reachtube output diff --git a/ourtool/dryvr/plotter/__init__.py b/ourtool/dryvr/plotter/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5dbc287d927065e02e00ed2cccd0389194764818 --- /dev/null +++ b/ourtool/dryvr/plotter/__init__.py @@ -0,0 +1,22 @@ +# _oo0oo_ +# o8888888o +# 88" . "88 +# (| -_- |) +# 0\ = /0 +# ___/`---'\___ +# .' \\| |// '. +# / \\||| : |||// \ +# / _||||| -:- |||||- \ +# | | \\\ - /// | | +# | \_| ''\---/'' |_/ | +# \ .-\__ '-' ___/-. / +# ___'. .' /--.--\ `. .'___ +# ."" '< `.___\_<|>_/___.' >' "". +# | | : `- \`.;`\ _ /`;.`/ - ` : | | +# \ \ `_. \_ __\ /__ _/ .-` / / +# =====`-.____`.___ \_____/___.-`___.-'===== +# `=---=' +# +# +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Codes are far away from bugs with the protection \ No newline at end of file diff --git a/ourtool/dryvr/plotter/linkednode.py b/ourtool/dryvr/plotter/linkednode.py new file mode 100644 index 0000000000000000000000000000000000000000..21ee9f462f066b926a39cea050e034db2ab3c078 --- /dev/null +++ b/ourtool/dryvr/plotter/linkednode.py @@ -0,0 +1,18 @@ +""" +This is a data structure to hold reachtube data per node +""" + + +class LinkedNode: + def __init__(self, node_id, file_name): + self.file_name = file_name.strip() + self.nodeId = node_id + self.lower_bound = {} + self.upper_bound = {} + self.child = {} + + def print_tube(self): + for key in sorted(self.lower_bound): + if key in self.upper_bound: + print(self.upper_bound[key]) + print(self.lower_bound[key]) diff --git a/ourtool/dryvr/plotter/parser.py b/ourtool/dryvr/plotter/parser.py new file mode 100644 index 0000000000000000000000000000000000000000..0d84ecd8a55fac0e7ab966cbfb3774d1fd30b4bc --- /dev/null +++ b/ourtool/dryvr/plotter/parser.py @@ -0,0 +1,84 @@ +""" +This file consist parser code for DryVR reachtube output +""" + +from src.plotter.linkednode import LinkedNode + + +def parse(data): + init_node = None + prev_node = None + cur_node = None + lower_bound = {} + upper_bound = {} + y_min = [float("inf") for _ in range(len(data[2].strip().split()))] + y_max = [float("-inf") for _ in range(len(data[2].strip().split()))] + + for line in data: + # This is a mode indicator + if ',' in line or '->' in line or line.strip().isalpha() or len(line.strip()) == 1: + insert_data(cur_node, lower_bound, upper_bound) + # There is new a transition + if '->' in line: + mode_list = line.strip().split('->') + prev_node = init_node + for i in range(1, len(mode_list) - 1): + prev_node = prev_node.child[mode_list[i]] + cur_node = prev_node.child.setdefault(mode_list[-1], LinkedNode(mode_list[-1], line)) + else: + cur_node = LinkedNode(line.strip(), line) + if not init_node: + init_node = cur_node + else: + cur_node = init_node + # Using dictionary because we want to concat data + lower_bound = {} + upper_bound = {} + LOWER = True + + else: + line = list(map(float, line.strip().split())) + if len(line) <= 1: + continue + if LOWER: + LOWER = False + # This data appeared in lower_bound before, concat the data + if line[0] in lower_bound: + for i in range(1, len(line)): + lower_bound[line[0]][i] = min(lower_bound[line[0]][i], line[i]) + else: + lower_bound[line[0]] = line + + for i in range(len(line)): + y_min[i] = min(y_min[i], line[i]) + else: + LOWER = True + if line[0] in upper_bound: + for i in range(1, len(line)): + upper_bound[line[0]][i] = max(upper_bound[line[0]][i], line[i]) + else: + upper_bound[line[0]] = line + + for i in range(len(line)): + y_max[i] = max(y_max[i], line[i]) + insert_data(cur_node, lower_bound, upper_bound) + return init_node, y_min, y_max + + +def insert_data(node, lower_bound, upper_bound): + if not node or len(lower_bound) == 0: + return + + for key in lower_bound: + if key in node.lower_bound: + for i in range(1, len(lower_bound[key])): + node.lower_bound[key][i] = min(node.lower_bound[key][i], lower_bound[key][i]) + else: + node.lower_bound[key] = lower_bound[key] + + for key in sorted(upper_bound): + if key in node.upper_bound: + for i in range(1, len(upper_bound[key])): + node.upper_bound[key][i] = max(node.upper_bound[key][i], upper_bound[key][i]) + else: + node.upper_bound[key] = upper_bound[key] diff --git a/ourtool/dryvr/plotter/plot.py b/ourtool/dryvr/plotter/plot.py new file mode 100644 index 0000000000000000000000000000000000000000..b07e575197d200745f9a83a5fbaa866bc6229fb1 --- /dev/null +++ b/ourtool/dryvr/plotter/plot.py @@ -0,0 +1,78 @@ +""" +This file consist main plotter code for DryVR reachtube output +""" + +import matplotlib.patches as patches +import matplotlib.pyplot as plt + +colors = ['red', 'green', 'blue', 'yellow', 'black'] + + +def plot(node, dim, y_min, y_max, x_dim): + fig1 = plt.figure() + ax1 = fig1.add_subplot('111') + lower_bound = [] + upper_bound = [] + for key in sorted(node.lower_bound): + lower_bound.append(node.lower_bound[key]) + for key in sorted(node.upper_bound): + upper_bound.append(node.upper_bound[key]) + + for i in range(min(len(lower_bound), len(upper_bound))): + lb = list(map(float, lower_bound[i])) + ub = list(map(float, upper_bound[i])) + + for ci, d in enumerate(dim): + rect = patches.Rectangle((lb[x_dim], lb[d]), ub[x_dim] - lb[x_dim], ub[d] - lb[d], + color=colors[ci % len(colors)], alpha=0.7) + ax1.add_patch(rect) + + y_axis_min = min([y_min[i] for i in dim]) + y_axis_max = max([y_max[i] for i in dim]) + ax1.set_title(node.nodeId, fontsize=12) + ax1.set_ylim(bottom=y_axis_min, top=y_axis_max) + ax1.plot() + # TODO Eliminate hardcoded folders + fig1.savefig('output/' + node.file_name + '.png', format='png', dpi=200) + + +def rrt_plot(lower_bound, upper_bound, x_dim, y_dim, goal, unsafe_list, region, initial): + fig1 = plt.figure() + ax1 = fig1.add_subplot('111') + x_min = region[0][0] + y_min = region[0][1] + x_max = region[1][0] + y_max = region[1][1] + + # Draw the path + for i in range(min(len(lower_bound), len(upper_bound))): + lb = list(map(float, lower_bound[i])) + ub = list(map(float, upper_bound[i])) + + rect = patches.Rectangle((lb[x_dim], lb[y_dim]), ub[x_dim] - lb[x_dim], ub[y_dim] - lb[y_dim], color='blue', + alpha=0.7) + ax1.add_patch(rect) + + # Draw the goal + if goal: + lb, ub = goal + rect = patches.Rectangle((lb[0], lb[1]), ub[0] - lb[0], ub[1] - lb[1], color='green', alpha=0.7) + ax1.add_patch(rect) + + if initial: + lb, ub = initial + rect = patches.Rectangle((lb[0], lb[1]), ub[0] - lb[0], ub[1] - lb[1], color='yellow', alpha=0.7) + ax1.add_patch(rect) + + # Draw the unsafe + if unsafe_list: + for unsafe in unsafe_list: + lb, ub = unsafe + rect = patches.Rectangle((lb[0], lb[1]), ub[0] - lb[0], ub[1] - lb[1], color='red', alpha=0.7) + ax1.add_patch(rect) + + ax1.set_title("RRT", fontsize=12) + ax1.set_ylim(bottom=y_min, top=y_max) + ax1.set_xlim(left=x_min, right=x_max) + ax1.plot() + fig1.savefig('output/rrt.png', format='png', dpi=200) diff --git a/ourtool/dryvr/plotter/plot_util.py b/ourtool/dryvr/plotter/plot_util.py new file mode 100644 index 0000000000000000000000000000000000000000..6718c923b4dd9148d8f6b55464a662daeab2a30f --- /dev/null +++ b/ourtool/dryvr/plotter/plot_util.py @@ -0,0 +1,46 @@ +import numpy as np +from matplotlib import pyplot as plt +from matplotlib.patches import Rectangle + + +def plot_rtsegment_and_traces(rtsegment: np.ndarray, traces: np.ndarray): + for dim_ind in range(1, traces.shape[2]): + fig, ax = plt.subplots(1) + facecolor = 'r' + for trace_ind in range(traces.shape[0]): + ax.plot(traces[trace_ind, :, 0], traces[trace_ind, :, dim_ind]) + for hrect_ind in range(rtsegment.shape[0]): + ax.add_patch(Rectangle((rtsegment[hrect_ind, 0, 0], rtsegment[hrect_ind, 0, dim_ind]), rtsegment[hrect_ind, 1, 0]-rtsegment[hrect_ind, 0, 0], + rtsegment[hrect_ind, 1, dim_ind] - rtsegment[hrect_ind, 0, dim_ind], alpha=0.1, facecolor='r')) + ax.set_title(f'dim #{dim_ind}') + fig.canvas.draw() + plt.show() + + +def plot_traces(traces, dim, bloat_tube): + """ Plot the traces """ + # Iterate over all individual traces + for i in range(0, len(traces)): + trace = traces[i] + + # Obtain desired dimension + time = [] + data = [] + for j in range(0, len(trace)): + # for j in xrange(0,2): + time.append(trace[j][0]) + data.append(trace[j][dim]) + + # Plot data + if i == 0: + plt.plot(time, data, 'b') + else: + plt.plot(time, data, 'r') + + time = [row[0] for row in bloat_tube] + value = [row[dim] for row in bloat_tube] + time_bloat = [time[i] for i in range(0, len(value), 2)] + lower_bound = [value[i] for i in range(0, len(value), 2)] + upper_bound = [value[i + 1] for i in range(0, len(value), 2)] + plt.plot(time_bloat, lower_bound, 'g') + plt.plot(time_bloat, upper_bound, 'g') \ No newline at end of file diff --git a/ourtool/dryvr/plotter/plotter_interface.ipynb b/ourtool/dryvr/plotter/plotter_interface.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..de6c0d1e2aa8693652246f3ea36fa58d135ed351 --- /dev/null +++ b/ourtool/dryvr/plotter/plotter_interface.ipynb @@ -0,0 +1,132 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "<script>requirejs.config({paths: { 'plotly': ['https://cdn.plot.ly/plotly-latest.min']},});if(!window.Plotly) {{require(['plotly'],function(plotly) {window.Plotly=plotly;});}}</script>" + ], + "text/vnd.plotly.v1+html": [ + "<script>requirejs.config({paths: { 'plotly': ['https://cdn.plot.ly/plotly-latest.min']},});if(!window.Plotly) {{require(['plotly'],function(plotly) {window.Plotly=plotly;});}}</script>" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.plotly.v1+json": { + "data": [ + { + "type": "scatter", + "x": [ + 1, + 2, + 3, + 4 + ], + "y": [ + 10, + 15, + 13, + 17 + ] + }, + { + "type": "scatter", + "x": [ + 1, + 2, + 3, + 4 + ], + "y": [ + 16, + 5, + 11, + 9 + ] + } + ], + "layout": {} + }, + "text/html": [ + "<div id=\"2839cfe2-5e9b-4166-bdb3-f660cb538d3e\" style=\"height: 525px; width: 100%;\" class=\"plotly-graph-div\"></div><script type=\"text/javascript\">require([\"plotly\"], function(Plotly) { window.PLOTLYENV=window.PLOTLYENV || {};window.PLOTLYENV.BASE_URL=\"https://plot.ly\";Plotly.newPlot(\"2839cfe2-5e9b-4166-bdb3-f660cb538d3e\", [{\"y\": [10, 15, 13, 17], \"x\": [1, 2, 3, 4], \"type\": \"scatter\"}, {\"y\": [16, 5, 11, 9], \"x\": [1, 2, 3, 4], \"type\": \"scatter\"}], {}, {\"linkText\": \"Export to plot.ly\", \"showLink\": true})});</script>" + ], + "text/vnd.plotly.v1+html": [ + "<div id=\"2839cfe2-5e9b-4166-bdb3-f660cb538d3e\" style=\"height: 525px; width: 100%;\" class=\"plotly-graph-div\"></div><script type=\"text/javascript\">require([\"plotly\"], function(Plotly) { window.PLOTLYENV=window.PLOTLYENV || {};window.PLOTLYENV.BASE_URL=\"https://plot.ly\";Plotly.newPlot(\"2839cfe2-5e9b-4166-bdb3-f660cb538d3e\", [{\"y\": [10, 15, 13, 17], \"x\": [1, 2, 3, 4], \"type\": \"scatter\"}, {\"y\": [16, 5, 11, 9], \"x\": [1, 2, 3, 4], \"type\": \"scatter\"}], {}, {\"linkText\": \"Export to plot.ly\", \"showLink\": true})});</script>" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from plotly.offline import init_notebook_mode, iplot\n", + "from plotly.graph_objects import Data, Scatter\n", + "\n", + "init_notebook_mode(connected=True) # initiate notebook for offline plot\n", + "\n", + "trace0 = Scatter(\n", + " x=[1, 2, 3, 4],\n", + " y=[10, 15, 13, 17]\n", + ")\n", + "trace1 = Scatter(\n", + " x=[1, 2, 3, 4],\n", + " y=[16, 5, 11, 9]\n", + ")\n", + "data = Data([trace0, trace1])\n", + "\n", + "iplot(data) # use plotly.offline.iplot for offline plot" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import networkx as nx\n", + "\n", + "G=nx.random_geometric_graph(200,0.125)\n", + "pos=nx.get_node_attributes(G,'pos')\n", + "\n", + "dmin=1\n", + "ncenter=0\n", + "for n in pos:\n", + " x,y=pos[n]\n", + " d=(x-0.5)**2+(y-0.5)**2\n", + " if d<dmin:\n", + " ncenter=n\n", + " dmin=d\n", + "\n", + "p=nx.single_source_shortest_path_length(G,ncenter)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 2", + "language": "python", + "name": "python2" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/ourtool/map/lane_map.py b/ourtool/map/lane_map.py index c62e2f48993e6e478dd6cb888b1879e15ecf8c8f..31cd4b7ab4b0d3e924d7115412eb71b0e681c53d 100644 --- a/ourtool/map/lane_map.py +++ b/ourtool/map/lane_map.py @@ -1,5 +1,6 @@ from typing import Dict, List import copy +from enum import Enum,auto from ourtool.map.lane_segment import LaneSegment @@ -20,6 +21,8 @@ class LaneMap: self.right_lane_dict[lane_seg.id] = [] def has_left(self, lane_idx): + if isinstance(lane_idx, Enum): + lane_idx = lane_idx.name if lane_idx not in self.lane_segment_dict: Warning(f'lane {lane_idx} not available') return False @@ -28,12 +31,16 @@ class LaneMap: def left_lane(self, lane_idx): assert all((elem in self.left_lane_dict) for elem in self.lane_segment_dict) + if isinstance(lane_idx, Enum): + lane_idx = lane_idx.name if lane_idx not in self.left_lane_dict: raise ValueError(f"lane_idx {lane_idx} not in lane_segment_dict") left_lane_list = self.left_lane_dict[lane_idx] return copy.deepcopy(left_lane_list) def has_right(self, lane_idx): + if isinstance(lane_idx, Enum): + lane_idx = lane_idx.name if lane_idx not in self.lane_segment_dict: Warning(f'lane {lane_idx} not available') return False @@ -42,10 +49,23 @@ class LaneMap: def right_lane(self, lane_idx): assert all((elem in self.right_lane_dict) for elem in self.lane_segment_dict) + if isinstance(lane_idx, Enum): + lane_idx = lane_idx.name if lane_idx not in self.right_lane_dict: raise ValueError(f"lane_idx {lane_idx} not in lane_segment_dict") right_lane_list = self.right_lane_dict[lane_idx] return copy.deepcopy(right_lane_list) def lane_geometry(self, lane_idx): - return self.lane_segment_dict[lane_idx].get_geometry() \ No newline at end of file + if isinstance(lane_idx, Enum): + lane_idx = lane_idx.name + return self.lane_segment_dict[lane_idx].get_geometry() + + def get_longitudinal_error(self, lane_idx, agent_state): + raise NotImplementedError + + def get_lateral_error(self, lane_idx, agent_state): + raise NotImplementedError + + def get_altitude_error(self, lane_idx, agent_state): + raise NotImplementedError \ No newline at end of file diff --git a/ourtool/scenario/scenario.py b/ourtool/scenario/scenario.py index e5cbe70efc5372021b55d08b4f22733fa5e16e0a..fac09725138356405c73021ecdda8587050849ac 100644 --- a/ourtool/scenario/scenario.py +++ b/ourtool/scenario/scenario.py @@ -3,11 +3,15 @@ import copy import itertools import ast +import numpy as np +from rsa import verify + from ourtool.agents.base_agent import BaseAgent from ourtool.automaton.guard import GuardExpressionAst from pythonparser import Guard from pythonparser import Reset -from ourtool.simulator.simulator import Simulator +from ourtool.analysis.simulator import Simulator +from ourtool.analysis.verifier import Verifier from ourtool.map.lane_map import LaneMap class FakeSensor: @@ -56,6 +60,7 @@ class Scenario: def __init__(self): self.agent_dict = {} self.simulator = Simulator() + self.verifier = Verifier() self.init_dict = {} self.init_mode_dict = {} self.map = None @@ -84,6 +89,20 @@ class Scenario: agent_list.append(self.agent_dict[agent_id]) return self.simulator.simulate(init_list, init_mode_list, agent_list, self, time_horizon, self.map) + def verify(self, time_horizon): + init_list = [] + init_mode_list = [] + agent_list = [] + for agent_id in self.agent_dict: + init = self.init_dict[agent_id] + tmp = np.array(init) + if tmp.ndim < 2: + init = [init, init] + init_list.append(init) + init_mode_list.append(self.init_mode_dict[agent_id]) + agent_list.append(self.agent_dict[agent_id]) + return self.verifier.compute_full_reachtube(init_list, init_mode_list, agent_list, self, time_horizon, self.map) + def get_all_transition(self, state_dict): lane_map = self.map satisfied_guard = [] diff --git a/user/simple_map.py b/user/simple_map.py index 3c777ad471c8ce7e326d3c1f7f6f2cc6d2ea60fd..848171f5e7748577bfefcd39e5cd9a0d294719c7 100644 --- a/user/simple_map.py +++ b/user/simple_map.py @@ -22,6 +22,27 @@ class SimpleMap2(LaneMap): self.right_lane_dict[segment1.id].append(segment2.id) self.right_lane_dict[segment2.id].append(segment3.id) +class SimpleMap3(LaneMap): + def __init__(self): + super().__init__() + segment1 = LaneSegment('Lane0', ((0,-30),33)) + segment2 = LaneSegment('Lane1', ((0,-30),30)) + segment3 = LaneSegment('Lane3', ((0,-30),27)) + self.add_lanes([segment1,segment2,segment3]) + self.left_lane_dict[segment2.id].append(segment1.id) + self.left_lane_dict[segment3.id].append(segment2.id) + self.right_lane_dict[segment1.id].append(segment2.id) + self.right_lane_dict[segment2.id].append(segment3.id) + + def get_longitudinal_error(self, lane_idx, agent_state): + return super().get_longitudinal_error(lane_idx, agent_state) + + def get_lateral_error(self, lane_idx, agent_state): + return super().get_lateral_error(lane_idx, agent_state) + + def get_altitude_error(self, lane_idx, agent_state): + return super().get_altitude_error(lane_idx, agent_state) + if __name__ == "__main__": test_map = SimpleMap() print(test_map.left_lane_dict)