Initial version of ODD monitoring code and examples#60
Initial version of ODD monitoring code and examples#60a-luque wants to merge 9 commits intoBerkeleyLearnVerify:DynamicSamplingfrom
Conversation
…Fork into DynamicSampling
…d corrected typo in README
There was a problem hiding this comment.
It's not ideal to add a 50 MB data file to the repository -- is there any way we can make this smaller? If not (e.g. it's not feasible to use a smaller model, or having users train their own would take a long time / a fancy GPU), I guess it's OK.
| import torch | ||
| import torchvision |
There was a problem hiding this comment.
Dependencies like these which are not already listed in pyproject.toml should be added there under the examples extra (so that someone running pip install verifai[examples] gets everything necessary to run the examples -- please test this in a fresh virtual environment).
| from torch.utils.data import Dataset | ||
| from torchvision.io import read_image |
There was a problem hiding this comment.
Please move to the top of the file with the rest of the imports so that they are more easily found (likewise for the other imports later on).
|
|
||
| import os | ||
|
|
||
| sys.setrecursionlimit(10000) |
There was a problem hiding this comment.
Is this really necessary? It's not a good sign to have to change the recursion limit, and nothing here looks recursive on a quick skim.
| @@ -0,0 +1,225 @@ | |||
| import pandas as pd | |||
There was a problem hiding this comment.
In all files, please separate imports into 3 sections separated by 1 blank line: Python stdlib imports, external packages (like pandas), and local imports from within VerifAI.
| from dotmap import DotMap | ||
| from PIL import Image | ||
| import cv2 | ||
|
|
||
| import pandas as pd |
There was a problem hiding this comment.
These and some of the other imports don't appear to be used.
| if not os.path.exists(sim_dir): | ||
| os.mkdir(sim_dir) | ||
|
|
||
| flag_good = False |
| flag_good = False | ||
| print(f"Executing sim {i}") | ||
| try: | ||
| os.system(f"scenic -S {scenic_path} --count 1 --time {num_of_steps} --2d --param recordFolder {sim_dir} --param timeout 30") |
There was a problem hiding this comment.
Using Scenic's Python API would probably be better here, but if you want to stick with the CLI, you should use subprocess.run with a list of arguments to make sure they are properly quoted/escaped on all platforms.
| try: | ||
| os.system(f"scenic -S {scenic_path} --count 1 --time {num_of_steps} --2d --param recordFolder {sim_dir} --param timeout 30") | ||
| flag_good = True | ||
| except: |
There was a problem hiding this comment.
Never use a bare except:, which will catch KeyboardInterrupt and prevent people from stopping your script with Ctrl-C. You should use except Exception: if you have no more specific exception types in mind.
| flag_good = True | ||
| except: | ||
| print(f"Simulation {j} failed.") | ||
| wait(5) |
There was a problem hiding this comment.
I don't think wait is defined.
| except: | ||
| print(f"Simulation {j} failed.") | ||
| wait(5) | ||
| os.system(f"scenic -S {scenic_path} --count 1 --time {num_of_steps} --2d --param recordFolder {sim_dir} --param timeout 30") |
There was a problem hiding this comment.
If the first attempt failed, why would you expect it to work a second time after a 5-second delay? Scenic already retries the simulation if there was a SimulationCreationError, and any other ways Scenic might fail (e.g. if there is a syntax error in the program) are unlikely to fix themselves given a little time. Given that wait isn't defined, I suspect this code path was never run: in which case you might as well remove it.
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
There was a problem hiding this comment.
Please remove unnecessary blank lines (1 is OK).
| import pickle | ||
| import sklearn | ||
| import numpy as np | ||
| import random |
There was a problem hiding this comment.
Scenic programs should not use the random module directly: use Scenic's own distribution facilities for generating random numbers (otherwise you will break serialization, for example).
| EGO_BRAKING_THRESHOLD = 10 | ||
|
|
||
|
|
||
| behavior FollowLaneBehaviorModified(target_speed = 10, laneToFollow=None, is_oppositeTraffic=False, leaderCar=None): |
There was a problem hiding this comment.
It's unfortunate to have to duplicate most of the code of FollowLaneBehavior here. It's probably unavoidable, since it looks like you're adding noise to the actions the ordinary behavior would take, but in that case I would just put a short docstring explaining how this version differs from the ordinary one rather than copying the original docstring).
| with behavior FollowLaneBehaviorModified(target_speed=EGO_SPEED), | ||
| with color Color(0,0,0) | ||
|
|
||
| ego = new Car following roadDirection from leader.position for EGO_TO_LEADER, |
There was a problem hiding this comment.
| ego = new Car following roadDirection from leader.position for EGO_TO_LEADER, | |
| ego = new Car following roadDirection from leader for EGO_TO_LEADER, |
| with sensors {"front_rgb": RGBSensor(offset=(0, 2, 1), attributes=attrs) | ||
| } |
There was a problem hiding this comment.
| with sensors {"front_rgb": RGBSensor(offset=(0, 2, 1), attributes=attrs) | |
| } | |
| with sensors {"front_rgb": RGBSensor(offset=(0, 2, 1), attributes=attrs)} |
|
|
||
|
|
||
|
|
||
| require distance to intersection > 10 and distance from leader to intersection > 10 and ego can see leader |
There was a problem hiding this comment.
Better to write this as 3 separate require statements: it will help Scenic's pruning, and it's more obvious that you have 3 independent constraints.
Actually, for ego can see leader it would be better to just add with requireVisible True when defining leader (which will also help Scenic do pruning).
| @@ -0,0 +1,491 @@ | |||
| """ Scenario Description | |||
| The ego car follows the leader car maintaining a normal distance while lane keeping. | |||
There was a problem hiding this comment.
This is the same description as the previous file.
| import warnings | ||
| warnings.filterwarnings("ignore") |
There was a problem hiding this comment.
Why? If actually necessary, can you use a more specific filter?
| if globalParameters.seed != "": | ||
| random.seed(globalParameters.seed) | ||
| np.random.seed(globalParameters.seed) |
There was a problem hiding this comment.
Should not need this: use Scenic's -s / --seed option.
|
|
||
| torch.set_default_device('cuda') | ||
|
|
||
| class resNet(torch.nn.Module): |
There was a problem hiding this comment.
Aren't this and the following class the same as in controller_training.py? Please consolidate if possible.
| ]) | ||
|
|
||
| while True: | ||
| front_img = self.sensors["front_rgb"]._lastObservation |
There was a problem hiding this comment.
Any reason to use this internal, undocumented API? Why not self.observations["front_rgb"]?
|
|
||
|
|
||
| # Monitor trigger | ||
| if monitor_model: |
There was a problem hiding this comment.
The body of this conditional can probably be moved into a helper function used in 3 places as suggested above.
| obstacle = new Car at 0 @ 25 relative to leader, | ||
| with heading leader | ||
|
|
||
| ego = new Car following roadDirection from leader.position for EGO_TO_LEADER, |
There was a problem hiding this comment.
| ego = new Car following roadDirection from leader.position for EGO_TO_LEADER, | |
| ego = new Car following roadDirection from leader for EGO_TO_LEADER, |
| with distIntersec EGO_TO_LEADER, | ||
| with isSafe 1, | ||
| with sensors {"front_rgb": RGBSensor(offset=(0, 2, 1), width=640, height=320), | ||
| "aerial_rgb": RGBSensor(offset=(0, -10, 4), width=1280, height=640) |
There was a problem hiding this comment.
This sensor doesn't appear to be used.
|
|
||
|
|
||
|
|
||
| require ego can see leader |
There was a problem hiding this comment.
See note about requireVisible in previous file.
| @@ -0,0 +1,516 @@ | |||
| """ Scenario Description | |||
| The ego car follows the leader car maintaining a normal distance while lane keeping. | |||
There was a problem hiding this comment.
Again, please distinguish from other scenarios.
| import warnings | ||
| warnings.filterwarnings("ignore") |
There was a problem hiding this comment.
I won't keep repeating the comments -- please check all from the previous file in this one too.
|
|
||
|
|
||
|
|
||
| behavior FollowLaneBehaviorModified(target_speed = 10, laneToFollow=None, is_oppositeTraffic=False, leaderCar=None): |
There was a problem hiding this comment.
This version looks almost identical to the one in the previous file -- please consolidate to make these scenarios easier to follow.
|
|
||
| from scenic.core.sensors import ActiveSensor | ||
|
|
||
| class CarlaOtherFeaturesSensor(ActiveSensor): |
There was a problem hiding this comment.
I don't think this is actually used anywhere. Maybe this file is left over from some other experiments?
| #################### Sampling parameters ############################ | ||
| ##################################################################### | ||
|
|
||
| class SpecMonitor(specification_monitor): |
There was a problem hiding this comment.
Just as a heads-up, some minor changes will be needed to this code when the simplification of monitors is merged -- but I just realized while reviewing this that I forgot to make a PR for that last December. Once I've made that PR I can try fixing this up myself.
| try: | ||
| while True: | ||
| try: | ||
| start = time.time() |
There was a problem hiding this comment.
Should use time.monotonic() for all timings so that they aren't messed up by changes to the system clock.
| rho = self.sampling_params.server.lastValue | ||
| self.total_sample_time += timings.sample_time | ||
| self.total_simulate_time += timings.simulate_time | ||
| except: |
There was a problem hiding this comment.
Don't use bare except, and please distinguish the error and non-error paths (for the latter I assume you want TerminationException?).
| sample = self.sampling_params.server.get_sample() | ||
| after_sampling = time.time() | ||
| scene = self.sampling_params.server.sampler.lastScene | ||
| assert scene | ||
| result = self.sampling_params.server._simulate(scene) | ||
| if result is None: | ||
| print(self.sampling_params.server.rejectionFeedback) | ||
| return self.sampling_params.server.rejectionFeedback | ||
| print(f"Time steps run for this simulation: {len(result.trajectory)}") | ||
| value = (0 if self.sampling_params.server.monitor is None | ||
| else self.sampling_params.server.monitor.evaluate(result)) | ||
| self.sampling_params.server.lastValue = value |
There was a problem hiding this comment.
This block seems to replicate the logic of Server.run_server (and ScenicServer.evaluate_sample), including manipulating internal state of the server, which is fragile. Can't you just call run_server on the Server you have?
| return 0 | ||
|
|
||
| def load_simulations(self, load_path): | ||
| import os |
There was a problem hiding this comment.
Unnecessary, and if it was needed it should be at the top of the file.
| def generate(self): | ||
| pass | ||
|
|
||
| class GenericDataGenerator(DataGenerator): |
There was a problem hiding this comment.
There don't seem to be any other subclasses of DataGenerator, so why not just move all of this code into DataGenerator and drop this class?
|
|
||
|
|
||
|
|
||
| class GenericEvaluator(Evaluator): |
There was a problem hiding this comment.
What's the need for this class vs. just moving everything into Evaluator?
| super().__init__(eval_params, sampling_params, global_params) | ||
|
|
||
|
|
||
| def sample_simulations(self, num_simulations, num_steps, save_datagen_path, save_scenes_path): |
There was a problem hiding this comment.
This seems very similar to DataGenerator.sample_simulations. Please consolidate the common code.
There was a problem hiding this comment.
Actually, the entire class is almost the same as the data generator, so I would make a common superclass of those (which does not need to be public).
| if i == 1: | ||
| t0 = time.time() | ||
| if i == num_simulations: | ||
| filehandler = open(f"{save_path}training_{i}.pkl", 'wb') |
There was a problem hiding this comment.
It doesn't look like this file will ever be closed, nor is it used anywhere else. I would use a with statement. (Ditto for the similar code a few lines down.)
| if save_path[-1] != "/": | ||
| save_path += "/" |
There was a problem hiding this comment.
This won't work on Windows. Use pathlib or os.path to construct paths instead.
| pass | ||
|
|
||
|
|
||
| class MODDLearner(ODDLearner): |
There was a problem hiding this comment.
What is the difference supposed to be between these two classes? Unless you anticipate further subclasses, I would just have a single class.
| # collect losses | ||
| train_loss_all.append(train_loss) | ||
|
|
||
| # Print message |
There was a problem hiding this comment.
Can we drop all these obvious, uninformative comments? (presumably AI generated?)
| pass | ||
|
|
||
|
|
||
| class GenericTrainer(Trainer): |
There was a problem hiding this comment.
I don't see the need for both of these classes.
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
There was a problem hiding this comment.
Please remove extraneous whitespace.
| from verifai.modd.odd_monitor import Monitor | ||
| from dotmap import DotMap | ||
|
|
||
| class MODD(): |
There was a problem hiding this comment.
Should this actually be a class? You only run it once, I think, so if there's no reason to subclass it, it might as well just be a function. A more informative name like generate_monitor might be better too.
There was a problem hiding this comment.
Actually I now realize that MODDLearner is the top-level API. But it doesn't seem to do anything itself, just call this class. Why do you need both?
| def __init__(self, sampling_params): | ||
| self.sampling_params = sampling_params | ||
|
|
||
| if self.sampling_params.has_key("server_type"): |
There was a problem hiding this comment.
Can this condition ever be false?
| self.sampling_params = sampling_params | ||
|
|
||
| if self.sampling_params.has_key("server_type"): | ||
| if self.sampling_params.server_type == "scenic": |
There was a problem hiding this comment.
If I understand the code correctly, it only works with the Scenic server anyway, so why do this check? Raising an exception if the user tries to instantiate MODD with a different kind of server would make more sense. In any case, silently doing nothing when this condition is false seems like the wrong thing to do.
| sampling_data.sampler = ScenicSampler.fromScenario(self.sampling_params.path, mode2D=True, maxSteps=300, params={"monitor": "", "seed":"", "render" : 1, "verbosity": 3, "timeBound": 300, "controller": self.sampling_params.controller_path}) | ||
|
|
||
| elif self.sampling_params.mode == "eval": | ||
| sampling_data.sampler = ScenicSampler.fromScenario(self.sampling_params.path, mode2D=True, maxSteps=300, params={"monitor": os.path.abspath(self.sampling_params.monitor), "seed": 42, "render" : 1, "verbosity": 3, "timeBound": 300, "controller": self.sampling_params.controller_path}) |
There was a problem hiding this comment.
Do you really want to hard-code the seed and the number of steps/timeBound here?
Also, why force mode2D? I don't think anything you're doing requires that, and it's possible to run CARLA scenarios in 3D mode for example.
| sampling_data.sampler = ScenicSampler.fromScenario(self.sampling_params.path, mode2D=True, maxSteps=300, params={"monitor": os.path.abspath(self.sampling_params.monitor), "seed": 42, "render" : 1, "verbosity": 3, "timeBound": 300, "controller": self.sampling_params.controller_path}) | ||
|
|
||
| elif self.sampling_params.mode == "eval_nomonitor": | ||
| sampling_data.sampler = ScenicSampler.fromScenario(self.sampling_params.path, mode2D=True, maxSteps=300, params={"monitor": "", "seed": 42, "render" : 1, "verbosity": 3, "timeBound": 300, "controller": self.sampling_params.controller_path}) |
There was a problem hiding this comment.
The differences between these three cases would be more evident if you had them modify a common set of parameters, then call ScenicSampler.fromScenario once after the if statement.
Also you should have either an assertion or an exception to handle the case of an invalid mode (which one depends on whether the user can specify the mode or whether it's a purely internal thing).
| from sklearn.pipeline import Pipeline | ||
| from sklearn.linear_model import LogisticRegression | ||
| from sklearn.preprocessing import StandardScaler | ||
| from verifai.modd.odd_sampler import ODDSampler |
dfremont
left a comment
There was a problem hiding this comment.
Hi Alejandro, thanks for putting this together! The code looks OK overall, but see the detailed comments above. A few bigger things to change/investigate:
- I could not run the example on my laptop because it requires CARLA. I can try later on another machine, but it would be nice to have a more portable example. I don't think anything you're doing is CARLA-specific, so could you try making it work with MetaDrive too? I spent a few minutes on that myself but ran into this error:
File "/Users/dfremont/Documents/Berkeley/repositories/BerkeleyLearnVerify/VerifAI/src/verifai/modd/odd_data_generator.py", line 48, in sample_simulations
bar = progressbar.ProgressBar(max_value=num_simulations)
TypeError: __init__() got an unexpected keyword argument 'max_value'
- Some unit tests for the new functionality should be added. Even a basic test (e.g. using the Newtonian simulator) would catch issues like forgetting to add a dependency to
pyproject.toml. (For example, the code will currently not run becausetqdmis missing.) - I think the
MODDLearnerclass is the only one that's intended to be public (?), but it's not clear how to actually use it. The README in the example explains what it does mathematically, but there's no documentation of the actual API (e.g. how does one actually run the resulting monitor?). A new page should be added to the documentation explaining the monitor generation feature. - Even if all the other classes are internal, it would be good to at least have short docstrings so that people maintaining this code can understand what each of the components is supposed to do, what its arguments are, etc. Right now the internal architecture is not clear.
No description provided.