Add dynamic rulebook code and example#59
Add dynamic rulebook code and example#59kevinchang73 wants to merge 9 commits intoBerkeleyLearnVerify:mainfrom
Conversation
There was a problem hiding this comment.
Is the example really specific to Town05? If not, can we change it to use Town01 so that it's not necessary to include another map in the repo? (Town01 is already under the tests folder)
| bypass_dist_max = [] | ||
|
|
||
| for file in all_files: | ||
| infile = open(directory+'/'+file, 'r') |
There was a problem hiding this comment.
Use a with statement so that the file is properly closed.
Also, hard-coding / will not work on Windows: use pathlib or os.path to manipulate paths. (Actually in this particular case glob.glob would probably be best, since then you don't need to manually use endswith and startswith above.)
| import sys | ||
| import matplotlib.pyplot as plt | ||
| import numpy as np | ||
| import os |
There was a problem hiding this comment.
In all files, please group imports into 3 blocks separated by 1 blank line: Python stdlib, external packages, local imports from VerifAI itself.
|
|
||
| print('Error weights') | ||
| print('segment 0:') | ||
| for i in range(1): |
There was a problem hiding this comment.
Why these unnecessary loops? If this code is copied from a template, why not expose the general template as a helper function which you can import here?
| ################################# | ||
|
|
||
| behavior EgoBehavior(path): | ||
| current_lane = network.laneAt(self) |
There was a problem hiding this comment.
| current_lane = network.laneAt(self) | |
| current_lane = self.lane |
It doesn't look like you're handling the case when the car is not in a lane, so I've used the lane property here, which will reject the simulation if the car isn't in a lane. If you actually want current_lane to be None in that case, use the _lane property instead (documentation here).
| do FollowLaneBehavior(globalParameters.EGO_SPEED, is_oppositeTraffic=True) until (distance to blockingCar) > globalParameters.BYPASS_DIST | ||
| laneChangeCompleted = True | ||
| interrupt when (blockingCar can see ego) and (distance to blockingCar) > globalParameters.BYPASS_DIST and not bypassed: | ||
| current_laneSection = network.laneSectionAt(self) |
There was a problem hiding this comment.
| current_laneSection = network.laneSectionAt(self) | |
| current_laneSection = self.laneSection |
| require (distance from blockingCar to intersection) > DIST_TO_INTERSECTION | ||
| terminate when (distance to spawnPt) > TERM_DIST |
There was a problem hiding this comment.
| require (distance from blockingCar to intersection) > DIST_TO_INTERSECTION | |
| terminate when (distance to spawnPt) > TERM_DIST | |
| require distance from blockingCar to intersection > DIST_TO_INTERSECTION | |
| terminate when distance to spawnPt > TERM_DIST |
| # RECORDING # | ||
| ################################# | ||
|
|
||
| record initial (initLaneSec.polygon.exterior.coords) as initLaneCoords |
There was a problem hiding this comment.
Likewise, the parser should be able to handle having no parentheses here.
| @@ -0,0 +1,19 @@ | |||
| import numpy as np | |||
|
|
|||
| def rule0(simulation, indices): # safe distance to obstacle | |||
There was a problem hiding this comment.
Did you decide not to implement my suggestion for a decorator to reduce the boilerplate here?
| # AGENT BEHAVIORS # | ||
| ################################# | ||
|
|
||
| behavior DecelerateBehavior(brake): |
There was a problem hiding this comment.
What's the purpose of having a behavior that only takes 1 action? Why not just inline the body?
| try: | ||
| do FollowTrajectoryBehavior(target_speed=globalParameters.EGO_SPEED, trajectory=trajectory) | ||
| do FollowLaneBehavior(target_speed=globalParameters.ADV_SPEED) | ||
| interrupt when withinDistanceToAnyObjs(self, SAFETY_DIST) and (ped in network.drivableRegion) and flag: |
There was a problem hiding this comment.
Is the intended behavior here that once this interrupt fires the first time, it can never fire again because the flag is set to false? That's a little surprising, so it might be good to explain what the ego is trying to do either here or in the docstring for the whole scenario (which is just a stub).
|
|
||
| ped = new Pedestrian at pedSpawnPt, | ||
| facing toward pedEndPt, | ||
| with regionContainedIn None, |
There was a problem hiding this comment.
This is the default (meaning the pedestrian can be anywhere in the workspace). Did you mean everywhere?
| @@ -0,0 +1,137 @@ | |||
| """ | |||
| TITLE: Verifai 2.0 Left Turn | |||
There was a problem hiding this comment.
| TITLE: Verifai 2.0 Left Turn | |
| TITLE: VerifAI 3.0 Left Turn |
| record (distance from ego to adv1) as egoDistToAdv1 | ||
| record (distance to egoSpawnPt) as egoDistToEgoSpawnPt | ||
|
|
||
| record ego._boundingPolygon as egoPoly |
There was a problem hiding this comment.
I don't think we should be using an internal, undocumented property in our examples. You can use ego.boundingPolygon to get a PolygonalRegion, which should be fine for whatever you need to do with it.
Actually having said that, it doesn't seem that these polygons are used anywhere. So I would remove them.
| for root, _, files in os.walk(path): | ||
| for name in files: | ||
| fname = os.path.join(root, name) | ||
| if os.path.splitext(fname)[1] == '.scenic': | ||
| paths.append(fname) |
There was a problem hiding this comment.
I think something like glob.glob(f"{path}/**/*.scenic") would work just as well here and be a lot easier to read.
| for p in paths: | ||
| falsifier = run_experiment(p, rulebook=rulebook, | ||
| parallel=parallel, model=model, sampler_type=sampler_type, headless=headless, | ||
| num_workers=num_workers, max_time=max_time, n_iters=n_iters, max_steps=max_steps) |
| model: Which simulator model to use (e.g. scenic.simulators.newtonian.driving_model) | ||
| sampler_type: Which VerifAI sampelr to use (e.g. halton, scenic, ce, mab, etc.) | ||
| headless: Whether or not to display each simulation. | ||
| num_workers: Number of parallel workers. Only used if parallel is true. |
There was a problem hiding this comment.
Missing some arguments here
| print(f'(multi.py) Sampler type: {falsifier.sampler_type}') | ||
|
|
||
| # Run falsification | ||
| t0 = time.time() |
There was a problem hiding this comment.
All timings should use time.monotonic() so as not to be affected by changes to the system clock.
| if __name__ == '__main__': | ||
| parser = argparse.ArgumentParser() | ||
| parser.add_argument('--scenic-path', '-sp', type=str, default='uberCrashNewton.scenic', | ||
| help='Path to Scenic script') |
There was a problem hiding this comment.
Confusing indentation (please go through all files for this kind of thing, or run a tool like Black)
| git clone https://github.com/BerkeleyLearnVerify/VerifAI | ||
| cd VerifAI | ||
| python -m pip install --upgrade pip | ||
| python -m pip install -e . |
There was a problem hiding this comment.
| python -m pip install -e . | |
| python -m pip install -e ".[examples]" |
| python -m pip install -e . | ||
| ``` | ||
|
|
||
| Then, for this example, we adopt the [Metadrive simulator](https://metadriverse.github.io/metadrive/) as the backend simulator. To install Metadrive, run the following command: |
There was a problem hiding this comment.
You should just add MetaDrive and sumolib to the examples extra instead of requiring a separate pip invocation (which can break dependency constraints).
|
|
||
| ```bash | ||
| python -m pip uninstall progressbar | ||
| python -m pip install --force-reinstall progressbar2==3.55.0 |
There was a problem hiding this comment.
This is not a good idea. Is it hard to fix our code to use progressbar2? I would hope it would be a simple change.
|
|
||
| We provide six different scenarios in the `examples/dynamic_rulebook` folder, and you can run any of them by modifying and executing the `run_multi_dynamic.sh` script. | ||
|
|
||
| ```bash linenums="1" |
There was a problem hiding this comment.
I think quoting the entire script here is confusing. I'm not sure you need to quote any of it, but if you think that would be helpful, just quote the specific part people should modify.
|
|
||
| *.cproject | ||
|
|
||
| examples/dynamic_rulebook/*/outputs/ |
There was a problem hiding this comment.
Let's move this to a new .gitignore file in the examples/dynamic_rulebook directory, so that it will keep working even if we move that folder later.
| rm $scenario/outputs/$csv_file\_scatter.png | ||
| if [ "$use_dynamic_rulebook" = true ]; then | ||
|
|
||
| for seed in $(seq 0 1); |
There was a problem hiding this comment.
Why are we running this twice with different fixed seeds?
| priority_graphs = {} | ||
| using_sampler = -1 | ||
| verbosity = 1 | ||
| exploration_ratio = 2.0 | ||
| using_continuous = False |
There was a problem hiding this comment.
Surely all of these should be attributes of the rulebook object, not global state attached to the class? What if somebody wants to instantiate multiple rulebooks with different parameters?
| def visit_FunctionDef(self, node): | ||
| self.functions.append(node) | ||
|
|
||
| class rulebook(ABC): |
There was a problem hiding this comment.
| class rulebook(ABC): | |
| class Rulebook(ABC): |
| else: | ||
| assert isinstance(subsampler, RandomSampler) | ||
| if not sorted(list(self.split_samplers.keys())) == list(range(len(rulebook.priority_graphs))): | ||
| raise ValueError('Priority graph IDs should be in order and start from 0') |
There was a problem hiding this comment.
Shouldn't this be an assertion? We're the ones deciding the structure of the rulebook object and how we're putting together the samplers here, not the user.
| RandomSampler) | ||
| for subsampler in self.split_samplers[id].samplers: | ||
| if isinstance(subsampler, ContinuousDynamicCESampler): | ||
| subsampler.set_graph(priority_graph) |
There was a problem hiding this comment.
Setting the graph in a second step seems unnecessary. Can't you just pass it in when creating the sampler originally? (in the lambdas above, which you'd have to move inside the loop)
| # Update each sampler based on the corresponding segment | ||
| try: | ||
| iter(rhos) | ||
| except: |
There was a problem hiding this comment.
Don't use bare except:, which will catch KeyboardInterrupt. If you really want to catch any ordinary exception you can use except Exception:, but in this case you seem to just be checking whether rho is iterable or not. Doesn't it have to be a tuple since we're doing multi-objective falsification?
| break | ||
| if self.verbosity >= 2: | ||
| print("Sample no: ", i, "\nSample: ", sample, "\nRho: ", rho) | ||
| if self.dynamic and self.verbosity >= 1: |
There was a problem hiding this comment.
This should only happen at verbosity 2, in which case we just printed rho above, so why do we need this?
| for r in rho: | ||
| self.populate_error_table(sample, r) |
There was a problem hiding this comment.
Why are we calling this multiple times? There should only be one row in the error table for each sample/simulation. The different elements of rho should become different columns, as in the old multi-objective falsification code, right?
There was a problem hiding this comment.
OK, if I understand correctly, in the dynamic case you've changed rho to be a list of lists, one per segment. But it still doesn't make sense to have multiple rows for a single simulation, and the rho_0 column for example will mean different things in different rows, since the first rule varies depending on which segment was active. We'll need to have a discussion on Tuesday about what the error table should look like.
| if self.dynamic: | ||
| while result is None: | ||
| sample = self.get_sample(1) | ||
| scene = self.sampler.lastScene | ||
| assert scene | ||
| result = self._simulate(scene) |
There was a problem hiding this comment.
What is the purpose of this code? We should not be running more than one simulation per sample. If the simulation is rejected, the sampler should receive the rejection feedback as in the following lines.
| function_name = function_node.name | ||
| function_code = compile(ast.Module(body=[function_node], type_ignores=[]), '<string>', 'exec') | ||
| exec(function_code) | ||
| self.functions[function_name] = locals()[function_name] |
There was a problem hiding this comment.
Rather than all this rigamarole of compiling each function separately (which incidentally would break if the function refers to a constant defined at the top level of the module, for example), why not just exec the entire file into a new namespace and pull out all the functions? runpy.run_path might work for this too.
| def getVector(self): | ||
| return self.generateSample() | ||
|
|
||
| def generateSample(self): |
There was a problem hiding this comment.
What's the point of separating this method from getVector, which is the actual API required for a BoxSampler?
| except: | ||
| return | ||
| if len(rho) != self.num_properties: | ||
| return |
There was a problem hiding this comment.
Returning silently doesn't seem like the right thing to do. Shouldn't this be an assertion?
| is_ce = True | ||
| for node in self.priority_graph.nodes: | ||
| if rho[node] >= self.thres[node]: | ||
| is_ce = False | ||
| break |
There was a problem hiding this comment.
Is this the intended logic? It seems to say you only consider the sample to be a counterexample if it violates all of the specifications in the graph.
| row *= self.alpha | ||
| row[b] += 1 - self.alpha | ||
|
|
||
| class DiscreteDynamicCESampler(DiscreteCrossEntropySampler): |
There was a problem hiding this comment.
Did you test this? I don't think it will work, since DiscreteCrossEntropySampler does not expect rho to be a list.
| self.ignore_locs = self.ignore_locs + list(locs[0]) | ||
| sample_dict[k] = float(v) if self.column_type[k] and v is not None else v | ||
| if isinstance(rho, (list, tuple)): | ||
| if is_multi or isinstance(rho, (list, tuple)): |
There was a problem hiding this comment.
Why is this change needed? If rho is not a list/tuple, won't iterating over it fail? Or are you passing some new kind of iterable in for rho?
dfremont
left a comment
There was a problem hiding this comment.
Hi Kai-Chun, thanks for making the PR. I haven't finished reviewing the new samplers and the rulebook code itself, but won't get to it till later so I wanted to send what I have now. Besides my detailed comments above, some big-picture points:
- There are important changes to the server and error table that don't seem necessary and which don't make sense to me. Please see my comments, and we can discuss in detail on Tuesday.
- We need test cases for the new functionality. I'm pretty sure some of the code won't work (e.g. discrete domains with the new DCE sampler), which would come out if there were tests.
- There is a lot of repeated code across the examples which I think could be substantially reduced, e.g. using decorators for rules that operate uniformly on trajectories (see our previous discussion), a helper function to segment trajectories based on when specified records become true, and a common base for the
collect_resultscripts. This isn't just to save lines: the code will be easier to understand if it has a simpler structure and the common parts are broken out into functions with informative names. - The description of the system in the README is great and very comprehensive, but I think it's long enough to be hard to navigate (it combines basic instructions on running the example with technical details about the APIs). Since we need to add a new page to the online documentation about this feature anyway, I would suggest moving the current README into the documentation (either as one big page with different sections [probably replacing the current Multi-Objective Falsification page], or possibly multiple pages) and having only the essentials in the README itself. You could include installation instructions, how to run the example, and a brief summary of what the different files are for, pointing to the documentation for details.
No description provided.