Feature/sector agnostic scenario managers#71
Conversation
… out some common code
… out some common code
…c-scenario-managers
georgedeath
left a comment
There was a problem hiding this comment.
Thanks for the substantial cleanup here. I found two functional issues that look worth fixing before merge, both around preserving explicit user intent in the new shared paths.
|
Some constructor parameters are probably intentionally lower-level rather than public There is also a smaller naming consistency issue in the gym env wrappers: |
…e is None rather than general truthiness in custom.py
|
Thanks George - all those comments very valid+useful, and have been addressed now. |
AlvaroSierra
left a comment
There was a problem hiding this comment.
Not quite a full review, but here are some initial comments on the PR.
There was a problem hiding this comment.
Do all these notebooks always need to be run by hand if something changes? If yes should we add CI (similarly to the HMI) which checks this has been done. Otherwise, we will eventually forget (I deffinetly will 🤣).
There was a problem hiding this comment.
The notebooks normally won't need to be re-run and saved after changes to the code - the changes to notebooks in this PR are just because the scenario name and categories have changed (and an automatic CI step wouldn't be able to do that for us).
We can have CI running the notebooks to test that they aren't broken though - I have added that, but it is quite slow, so for now perhaps it should only run for PRs into main...
| if category in ["Two Aircraft", "Regular", "Custom", "Infinite"]: | ||
| return ["I-Sector", "X-Sector", "Xplus-Sector", "Y-Sector", "Two Sector", "Springfield"] |
There was a problem hiding this comment.
Should this list live in the AirspaceLoader type as a static method, such that it lives in the same place?
| # If we start or end outside the sector, use the `airspace.route_boundary_fixes` method | ||
| boundary_fixes = list(airspace.route_boundary_fixes(route).places.keys()) |
There was a problem hiding this comment.
This function returns the boundary of the airspace, not a specific sector, which although valid in default scenarios, it is not generalisable.
Also, a simple performance improvements:
| # If we start or end outside the sector, use the `airspace.route_boundary_fixes` method | |
| boundary_fixes = list(airspace.route_boundary_fixes(route).places.keys()) | |
| # If we start or end outside the sector, use the `airspace.route_boundary_fixes` method | |
| boundary_fixes = set(airspace.route_boundary_fixes(route).places.keys()) |
There was a problem hiding this comment.
There are a couple of things we could do here, with different levels of upstream change.
- We could move the
route_boundary_fixesmethod fromAirspacetoSector, which would seem to make sense to me. However,Sectordoesn't currently have any dependency on Fixes, so I'm a bit reluctant to introduce it now. - We could use the logic in
Airspace.route_boundary_fixesdirectly here.
My current thinking is to do the simple part of this (i.e. see if fixes are actually on the boundary) here in this code, as it is only a couple of lines. The other part of Airspace.route_boundary_fixes, that looks for fixes near a boundary, has a comment in the code implying it needs a bit of work anyway.
| if not entry_fix: | ||
| # start from the beginning of the route | ||
| for fix in route.filed: | ||
| if fix in boundary_fixes: | ||
| entry_fix = fix | ||
| break | ||
| # if no match, revert to first fix in route | ||
| entry_fix = entry_fix if entry_fix is not None else route.filed[0] | ||
| if not exit_fix: | ||
| # start from the end of the route | ||
| for fix in reversed(route.filed): | ||
| if fix in boundary_fixes: | ||
| exit_fix = fix | ||
| break |
There was a problem hiding this comment.
Both of these could resolve to having the same entry and exit fix, should we fall back if this is the case?
There was a problem hiding this comment.
Right, good point, I now check in the reverse loop that the first boundary fix encountered is not the entry fix, before assigning it as the exit fix.
| if fix in boundary_fixes: | ||
| entry_fix = fix | ||
| break | ||
| # if no match, revert to first fix in route |
There was a problem hiding this comment.
If no match, should we fail rather than create an unintended scenario?
There was a problem hiding this comment.
I don't think so - taking first/last fix in the route is what we currently have in main/dev, so as a fallback I think it's not so bad. It is an upgrade (useful, according to Ese) to use the actual boundary fixes if we find them, but if we end up in the fallback situation we are no worse off than now.
| exit_fix = fix | ||
| break | ||
| # if no match, use last fix in route | ||
| exit_fix = exit_fix if exit_fix is not None else route.filed[-1] |
There was a problem hiding this comment.
If no match, should we fail rather than create an unintended scenario?
| """ | ||
| # If airspace is not provided, fall back on first and last fixes on route | ||
| # for the entry and exit fixes | ||
| if not airspace: |
There was a problem hiding this comment.
In what cases would the airspace not be provided?
There was a problem hiding this comment.
Hmm... good question... The airspace is only needed in this function for finding the entry and exit fixes, so I originally added it as an optional argument, without which we'd fall back to the first/last fixes on route (the current situation). But it's true that this function is unlikely to be called directly by a user, rather than by a scenario manager, so they should always have an airspace. I will make it a mandatory argument, and remove this "if" block.
| def create_aircraft_with_coordinations( | ||
| callsign: str, | ||
| pos: Pos3D, | ||
| heading: float, | ||
| speed: float, | ||
| route: Route, | ||
| sector_name: str, | ||
| entry_fl: float, | ||
| exit_fl: float, | ||
| on_route: bool = False, | ||
| airspace: Airspace | None = None, | ||
| prev_sector: str = "background", | ||
| next_sector: str = "background", | ||
| typeof_aircraft: type[TAircraft] = Aircraft, |
There was a problem hiding this comment.
I'm not a huge fan of having these helper functions dotted around the code, as they become something extra to maintain, is there a specific reason why this:
- shouldn't be a separate constructor in the types which it is building
- needs to be abstracted away from the callers
There was a problem hiding this comment.
I agree having this type of utility function is a bit messy, but not sure what the alternative is. I don't how this could be a separate constructor, given that it's returning different types (an Aircraft and two Coordinations). And the reason to abstract it away from the callers is that it's called by several scenario managers and we shouldn't duplicate the same code in all of them.
The alternative I can think of is to have it as a method in a base ScenarioManager class, so that the concrete scenario managers get it via inheritance, but then not all scenario managers (e.g. real data) would use it, so then we would maybe add another layer of inheritance (an abstract ArtificialScenarioManager or something), which would add complication.
I'm open to suggestions, but this is the least worst option I've come up with.
| to_sector=sector_name, | ||
| fl=entry_fl, | ||
| fix=entry_fix, | ||
| direction="Horizontal", |
There was a problem hiding this comment.
Should all ordinations be horizontal?
There was a problem hiding this comment.
yep, fair point, I added this as an optional argument with default "Horizontal".
| save_log_to_file=save_log_to_file, | ||
| simulated_sectors=simulated_sectors, | ||
| ) | ||
| case "Custom": |
There was a problem hiding this comment.
Should we change the naming, as from the scenario name and category, it is not customisable.
There was a problem hiding this comment.
Hmm... true.... but I would be in favour of still calling it "Custom" so that people can still know to look at the Custom scenario manager if they want to figure out what's going on.
Changes to decouple airspace loading from scenario managers, meaning that any of the artificial airspaces can be used with any of the "Two Aircraft", "Regular", "Infinite" etc scenario managers.
Philosophy is that the scenario_category would be something like "Two Aircraft", "Infinite" etc. and the scenario_name would be the sector ("Xplus-Sector", "I-Sector", "Springfield" etc.). (Note that "Springfield" remains as a scenario_category, with the list of springfield scenarios available, unchanged.)
airspace_generator/airspace_loader.pyallowing the scenario managers to load airspace, routes, and default sector_name (to be used in Coordinations) for any of I, Y, X, Xplus, TwoSector, and Springfield.AirspaceLoader.load(scenario_name)rather than having match/case blocks within their code.Intro-Part3.ipynbnotebook.utility/scenario_manager_utils.py.Two Aircraft,Regular,CustomandInfinitescenario managers, such that they are all fully configurable via thesetupmethod, all of them usenp.random.default_rngfor any random number generation, and the random seed can be set and used consistently.bluebird-gymnasiumto reflect theTactical->Customchange.envs/base.py.