Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Large area simulation rework #203

Merged
merged 31 commits into from
May 28, 2024

Conversation

KieranRatcliffeInvertedAI
Copy link
Contributor

Moved versions of initialize and drive designed for simulating on large maps to a different submodule called "large" in invertedai.

  • Developed a new basic datatype called Region which contains a center, fov, and agent information such as state, attributes, and recurrent state as well as helper functions. This data type was designed with some thought towards the future in case basic assumptions about what a "region" can be changed (e.g. bigger or smaller than 100x100m square, non-square regions).

  • Due to the lack of need for the pygame visualization and design constraint of being stateless, region_drive has become simpler and less reliant on dependencies.

  • Region initialize is now separated from HOW the regions are produced. Default helper functions have been provided for this but can be easily swapped out by the user.

  • The core functionality is largely the same as it was before, algorithmically speaking.

Questions for reviewers:

  • What should we do concerning support for old versions of these utilities?
  • Do the comments make sense? Should more be added?
  • Any other thoughts on the structure of the interface or future-proofing concerns?

Copy link
Collaborator

@Ruishenl Ruishenl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial comments

agent_attributes: Optional[List[AgentAttributes]] = None # The attributes of agents that exist within the region or that will be initialized within the region
recurrent_states: Optional[List[RecurrentState]] = None # Recurrent states of the agents contained within the region
region_type: str = 'square' # Geometric category of the region. As of now, only 'square' is supported.
vertexes: List[Point] # An ordered list of x-y coordinates of the region defined clockwise around the perimeter of the region
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are referring to vertex, i believe the plural form is vertices.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vertices is indeed used but Vertexes is also used and more grammatically correct.


BUFFER_FOV = 35

class Particle(BaseModel):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure we want to keep using the word Particle here ? TreeNode or Node make more sense to me here. Or we can simply just call it Agent in this local namespace.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a carry over from the old area drive, however particle is the most commonly used term for "things that are placed within a quad tree". However Agent would indeed be more effective and descriptive.


return vertexes

def check_point_in_region(self,point):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense to have a doc string for this method explaining how things work in general, but you should reformat it to have it enclosed instead of using the comments.
And i don't think you need those additional comments explain how things work line by line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function was removed since it is not needed at this time.

invertedai/large/common.py Outdated Show resolved Hide resolved
Copy link
Contributor

@adscib adscib left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's some feedback for the parts I managed to read tonight. I'll add more tomorrow. Generally, the structure is looking good and this is what we want, but there are a lot of things that can be simplified and renamed to something more obvious.

If we're confident that the old versions of those functions are not used, I think it's fine to delete them. If we thought they might be used, deprecating them for some time would be in order.

One name change to consider is to use large_initialize and large_drive instead of region_initialize and region_drive. Those are more in line with what those functions accomplish rather than how they do it.


class Region(BaseModel):
"""
A data structure to contain information about a region in a map to be used in large simulations.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
A data structure to contain information about a region in a map to be used in large simulations.
A region in a map used to divide a large simulation into smaller parts.

It is self-evidence that this is a data structure

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

AgentAttributes
"""

center: Optional[Point] = None # The center of the region if such a concept is relevant (e.g. center of a square, center of a rectangle)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
center: Optional[Point] = None # The center of the region if such a concept is relevant (e.g. center of a square, center of a rectangle)
center: Optional[Point] = None #: The center of the region if such a concept is relevant (e.g. center of a square, center of a rectangle)

Use #: for each field to include those comments in the documentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

"""

center: Optional[Point] = None # The center of the region if such a concept is relevant (e.g. center of a square, center of a rectangle)
fov: Optional[float] = None # Side length of the region for the default interpretation of a region as a square
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

size or length would be a better name, as the region definition doesn't necessarily have anything to do with viewing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

invertedai/large/common.py Show resolved Hide resolved
Comment on lines 17 to 19
agent_states: Optional[List[AgentState]] = None # A list of existing agents within the region
agent_attributes: Optional[List[AgentAttributes]] = None # The attributes of agents that exist within the region or that will be initialized within the region
recurrent_states: Optional[List[RecurrentState]] = None # Recurrent states of the agents contained within the region
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a semantic difference between None and [] here? I'm also wondering if we should make it a List[Tuple[AgentAttributes, AgentState, Optional[RecurrentState]]] instead, but I'm not convinced.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these Regions were only used for driving then I would agree with the Tuple but since it's used for initialize as well which can be quite wonky, I'd say lets keep state, attributes, and recurrent states separate.

AGENT_SCOPE_FOV_BUFFER = 20

@validate_call
def define_regions_grid(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a docstring explaining how the grid is produced and what is a typical use case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


queue, centers = [map_center], []

while queue:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BFS is way of an overkill here. You can compute the grid of a rectangular area in closed form.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the sake of time, I can update this later since this won't impact the user interface and we know this code works so we can quietly make that change later while users are testing the features?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it can be done later.

Comment on lines 58 to 59
max_agent_density: Optional[int] = 10,
scaling_factor: Optional[float] = 1.0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These argument names, as well as the name of the function itself, are not at all intuitive. Presumably what this function does is compute agent numbers for each region given some parameters, so the naming should reflect that. Neither density nor scaling have obvious meanings here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've renamed them but when could you provide some feedback on if the new names are insufficient, too wordy, and if so, could you elaborate on what you are looking for?

center_road_area_dict = {}
max_drivable_area_ratio = 0

iterable_regions = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
iterable_regions = None

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted.

Comment on lines 88 to 91
# Convert to grayscale using Luminosity Method: gray = 0.114*B + 0.587*G + 0.299*R
# Image should be in BGR color pixel format
birdview_grayscale = np.matmul(birdview.reshape(total_num_pixels,3),np.array([[0.114],[0.587],[0.299]]))
number_of_black_pix = np.sum(birdview_grayscale == 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Convert to grayscale using Luminosity Method: gray = 0.114*B + 0.587*G + 0.299*R
# Image should be in BGR color pixel format
birdview_grayscale = np.matmul(birdview.reshape(total_num_pixels,3),np.array([[0.114],[0.587],[0.299]]))
number_of_black_pix = np.sum(birdview_grayscale == 0)
number_of_black_pix = np.sum((birdview.sum(axis=-1) == 0).int())

This is a very elaborate conversion only to compare things to zero at the end.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm yeah I guess at this point we know we don't need to do anything more fancy with a grayscale image. I made this change.

Copy link
Collaborator

@rf-ivtdai rf-ivtdai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally I see 3 types of comments:

  1. definition to arguments and fields
  2. explanation of key steps within a long workflow
  3. explanation of why things are called/implemented as such

For this repo, I believe type 1 and 2 are reasonable, but we should remove/rephrase those that are type 3. I marked them below.

self.southWest.get_leaf_nodes() + self.southEast.get_leaf_nodes()

def _flatten_and_sort(nested_list,index_list):
# Cannot unpack iterables during list comprehension so use this helper function instead
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

agent_x[i] = agent.center.x
agent_y[i] = agent.center.y
max_x, min_x, max_y, min_y = max(agent_x), min(agent_x), max(agent_y), min(agent_y)
region_fov = ceil(max(max_x,max_y) - min(min_x,min_y)) #Round up the starting FOV to an integer to reduce floating point issues
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider removing the comment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 261 to 265
if len(region.agent_states) == 0:
# Region is empty, do not call DRIVE
continue

else:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if len(region.agent_states) == 0:
# Region is empty, do not call DRIVE
continue
else:
if len(region.agent_states) > 0:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor

@adscib adscib left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a more careful review of initialize. I'll review drive as well, but posting this already in the interest of expediency.

Comment on lines 261 to 263
response.agent_states = all_agent_states
response.agent_attributes = all_agent_attributes
response.recurrent_states = all_recurrent_states
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little tricky, but for the traffic lights to work correctly, you need to return the recurrent light states from the response that originally set them. When you provide light states as input, the recurrent light states returned are garbage. @rf-ivtdai is adding this explanation to the documentation, but you'll want to fix the returned value here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I've made a change that should account for this.

traffic_light_state_history=traffic_light_state_history,
location_of_interest=(region_center.x, region_center.y),
random_seed=random_seed,
get_birdview=get_birdview,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's wasteful to request a birdview on every call, if you're only using the last one. I would remove this option altogether, but if you keep it, maybe there's a way to only request one birdview?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with leaving this out for now because it's not-trivial how to interpret this in the "large" version of initialize and drive and there are indeed other visualization methods.

Comment on lines 264 to 266
except InvertedAIError as e:
iai.logger.warning(e)
continue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little odd to just ignore this. This breaks the usual contract of INITIALIZE, which is that you get exactly as many agents as you request, or it's a failure. Let's discuss in person.


queue, centers = [map_center], []

while queue:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it can be done later.

region_center = region.center
region_size = region.size

existing_agent_states, existing_agent_attributes, _ = _get_all_existing_agents_from_regions(regions)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can pull this out of the loop?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regions are populated with agents one at a time. Therefore, each loop all agents currently in the simulation (initialized, preddefined, whatever) need to be accessed so that ones that are relevant to the current region of interest can be included as conditional. An alternative would be to have 3 running lists for states, attributes, and recurrent states but I went with this method for the sake of saving memory space and code cleanliness at the cost of a small increase in computation time due to looping through every region.

response = iai.initialize(
location=location,
states_history=None if len(all_agent_states) == 0 else [all_agent_states],
agent_attributes=[] if len(all_agent_attributes) == 0 else all_agent_attributes,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
agent_attributes=[] if len(all_agent_attributes) == 0 else all_agent_attributes,
agent_attributes=all_agent_attributes,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

conditional_agent_attributes = [x[1] for x in conditional_agents]

region_predefined_agent_states = [] if region.agent_states is None else region.agent_states
all_agent_states = conditional_agent_states + region_predefined_agent_states
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't those already captured in conditional_agent_states? It looks like you're including them twice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference here is that conditional_agent_states contains states from regions that have already been initialized and predefined agents from other regions whereas region_predefined_agent_states contains predefined agents for the current region of interest. Any of these agents could be relevant as "conditional" for calling initialize on the current region.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've renamed conditional_agent_states to out_of_region_conditional_agent_states and used this naming scheme wherever else relevant.

Comment on lines 273 to 279
# Filter out agents that are not inside the ROI to avoid collision with other agents not passed as conditional
# SLACK is for removing the agents that are very close to the boundary and they may collide with agents not
# labelled as conditional
valid_agents = list(filter(
lambda x: inside_fov(center=region_center, agent_scope_fov=region_size - SLACK, point=x[0].center),
zip(response_agent_states_sampled, response_agent_attributes_sampled, response_recurrent_states_sampled)
))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another instance where we remove some agents that the user is expecting to see. Perhaps this is fine, but we need to be clear in the documentation about what we return.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually upon further inspection, this can be removed because of the filter earlier in the loop that only takes in agents as conditional if they are within a certain scope so no collisions should happen at the edge anyways.

Comment on lines 286 to 288
regions[i].agent_states = response.agent_states[predefined_agents_slice] + valid_agent_state
regions[i].agent_attributes = response.agent_attributes[predefined_agents_slice] + valid_agent_attrs
regions[i].recurrent_states = response.recurrent_states[predefined_agents_slice] + valid_agent_rs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modifying Region objects in place is another thing that we need to be careful about in the documentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed so that it uses the member function region[i].insert_all_agent_details instead of modifying the fields directly.

valid_agent_attrs = [x[1] for x in valid_agents]
valid_agent_rs = [x[2] for x in valid_agents]

predefined_agents_slice = slice(len(conditional_agent_states),len(conditional_agent_states)+len(region_predefined_agent_states))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure this is correct? I don't know what exactly was the intention here, but this seems suspicious to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My current understanding of initialize is that the order of agents matters, insomuch as if N is the number of conditional agents each with an AgentState in the the states_history argument, then initialize only samples M-N agents where M is the number of AgentAttributes in the agent_attributes argument. That is fine and dandy however the trickiness here is that there are two types of "conditional" agents here: conditional agents predefined within the region of interest and conditional agents that exist outside of the region of interest. The former need to be kept within the region whereas the former, at this point in the loop have served their purpose and can be ignored. So this slice is trying to collect just the conditional agents predefined within this region.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, now with the deletion of that "valid states" filter, this is moot and the slice can just be len(out_of_region_conditional_agent_states) onwards.

Copy link
Contributor

@adscib adscib left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a review for drive. I'll do one more read of the whole thing.

get_infractions: bool = False,
random_seed: Optional[int] = None,
api_model_version: Optional[str] = None,
capacity: Optional[int] = 100,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
capacity: Optional[int] = 100,
single_call_agent_limit: Optional[int] = None,

Setting the default to None will make for a seamless transition if we increase the agent limit in DRIVE in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense, change is implemented.

random_seed: Optional[int] = None,
api_model_version: Optional[str] = None,
capacity: Optional[int] = 100,
is_async: Optional[bool] = True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
is_async: Optional[bool] = True
async_api_calls: bool = True

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

:func:`drive`
"""

num_agents = len(agent_states)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe check that all the input lists are the same length? I believe that's required here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 249 to 250
agent_x = [None]*num_agents
agent_y = [None]*num_agents
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
agent_x = [None]*num_agents
agent_y = [None]*num_agents
agent_x = [None for _ in range(num_agents)]
agent_y = [None for _ in range(num_agents)]

What you did works, but I recommend this as a habit. Otherwise you end up getting surprising bugs when you change None to a mutable object, and you get N references instead of N separate objects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 251 to 253
for i, agent in enumerate(agent_states):
agent_x[i] = agent.center.x
agent_y[i] = agent.center.y
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be the idiomatic Python equivalent.

Suggested change
for i, agent in enumerate(agent_states):
agent_x[i] = agent.center.x
agent_y[i] = agent.center.y
agent_x = [agent.center.x for agent in agent_states]
agent_y = [agent.center.y for agent in agent_states]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines 308 to 311
api_model_version = '' if len(all_responses) == 0 else all_responses[0].api_model_version,
birdview = None if len(all_responses) == 0 or not get_birdview else all_responses[0].birdview,
traffic_lights_states = traffic_lights_states if len(all_responses) == 0 else all_responses[0].traffic_lights_states,
light_recurrent_states = light_recurrent_states if len(all_responses) == 0 else all_responses[0].light_recurrent_states
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under what circumstances would we ever get no responses? Only if there are no input agents? In that case, I think we'd want behavior consistent with that drive does with no agents - easiest to achieve by making a single call to drive and returning the response.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I've made that change as well as adding an if statement to check whether it should just call regular drive (i.e. if there is only 1 leaf node).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user happens to input no agent information, the regular call to drive() will handle that.

all_responses = asyncio.run(async_drive_all(async_input_params))

response = DriveResponse(
agent_states = _flatten_and_sort([response.agent_states[:len(region.agent_attributes)] for response, region in zip(all_responses,non_empty_regions)],agent_id_order),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor repeated parts, len(region.agent_attributes) and zip(all_responses,non_empty_regions) into named variables for better readability.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put the zipped lists into a single variable and added a "get number of agents" function to the quad tree leaf node to make it more readable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually scratch that, if you attempt to refactor the zip part into a single variable, I think there is some pass-by-reference shenanigans where that zipped variable is being affected by the _flatten_and_sort function and that's why I had to repeat it before.

Comment on lines 147 to 149
flat_list = []
for sublist in nested_list:
flat_list.extend(sublist)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
flat_list = []
for sublist in nested_list:
flat_list.extend(sublist)
[x for sublist in nested_list for x in sublist]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 151 to 154
sorted_list = []
for ind in range(len(index_list)):
agent_index = index_list.index(ind)
sorted_list.append(flat_list[agent_index])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sorted_list = []
for ind in range(len(index_list)):
agent_index = index_list.index(ind)
sorted_list.append(flat_list[agent_index])
sorted_list = [x[1] for x in sorted(zip(index_list, flat_list))]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if is_async:
all_responses = asyncio.run(async_drive_all(async_input_params))

response = DriveResponse(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole _flatten_and_sort business is quite convoluted and could use refactoring, but we can leave that for later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is an annoying circumstance for sure. There is no connection between the location of the agent and it's index in the input parameter. This is a situation where an explicit ID for each agent would be useful but I understand it wouldn't make sense to change one of our basic data classes just to solve this.

I think a middle ground is that this flatten and sort method can be a member function of the quadtree class instead to make it a bit cleaner remaining functionally the same.

@KieranRatcliffeInvertedAI
Copy link
Contributor Author

Outstanding issues:

  1. Despite some small buffer room from rounding up the region_size for the root layer of the quadtree, some gaps still exist between leaf nodes due to floating point issues when dividing the size of the leaf nodes over and over.
  2. I've only seen this once but I got a recursion limit error, perhaps because there was one region on the map that was too dense and and, because the neighbouring regions were also dense, could not subdivide to a size of leaf node that contained less than the capacity.

Copy link
Contributor

@adscib adscib left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, it looks like we have the major things covered. Other than for some final tidying up, we should be good to go. Switching to an iterative QuadTree implementation would be my recommendation for getting rid of the recursion depth errors, but that can wait for later. More importantly, we need some test to ensure those new functions won't get broken by subsequent commits, but that can also be done in a separate PR.

Comment on lines 15 to 16
center: Optional[Point] = None #: The center of the region if such a concept is relevant (e.g. center of a square, center of a rectangle)
size: Optional[float] = None #: Side length of the region for the default interpretation of a region as a square
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these optional? It would seem that both are needed to define a region.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes a oversight when this was changed from the more general Region, this has been updated.

size: Optional[float] = None #: Side length of the region for the default interpretation of a region as a square
agent_states: Optional[List[AgentState]] = [] #: A list of existing agents within the region
agent_attributes: Optional[List[AgentAttributes]] = [] #: The attributes of agents that exist within the region or that will be initialized within the region
recurrent_states: Optional[List[RecurrentState]] = [] #: Recurrent states of the agents contained within the region
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
recurrent_states: Optional[List[RecurrentState]] = [] #: Recurrent states of the agents contained within the region
recurrent_states: Optional[List[RecurrentState]] = [] #: Recurrent states of the existing agents within the region

Only if I understand correctly that this is supposed to have the same length as agent_states if given.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, if recurrent_states is not empty, it should be the same length as agent_states AND agent_attributes. The two uses cases that I can identify for this object are for passing as arguments to DRIVE and to INITIALIZE which have slightly different needs so it's hard to enforce a standard across the board, especially because INITIALIZE has many different operating modes (i.e. equal number states and attributes, more attributes than states, no states at all).

Do you think it'd be worthwhile to split these regions up? Or have some sort of data validation? E.g. if agent details are to be added, it should check that it is a valid configuration?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I would perform some validation.

recurrent_states: Optional[List[RecurrentState]] = [] #: Recurrent states of the agents contained within the region

@classmethod
def init_square_region(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def init_square_region(
def create_square_region(

init may be confused with initialize

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 42 to 45
"""
Helper function to clear all agent data within the Region but keep geometric information.

"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""
Helper function to clear all agent data within the Region but keep geometric information.
"""

I don't think this sentence add any information beyond what the name already says, so I would remove it to reduce the maintenance burden.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 52 to 55
"""
Helper function to insert the details relevant to a single agent.

"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""
Helper function to insert the details relevant to a single agent.
"""

I don't think this one is even correct. Case in point that it's best to avoid docstrings that don't provide any extra information.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

total_num_agents: Optional[int] = 10,
random_seed: Optional[int] = None,
display_progress_bar: Optional[bool] = True
) -> List[Region]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're returning a list of regions. Make sure to specify whether those are the input Region objects, and if not whether the input objects are modified.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

agent_attributes=agent_attributes,
recurrent_states=recurrent_states
)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A copy method would be a good idea for future proofing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

A utility function to drive more than the normal capacity of agents in a call to :func:`drive`.
The agents are inserted into a quadtree structure and :func:`drive` is then called on each
region represented by a leaf node of the quadtree. Agents near this region are included in the
:func:`drive` calls to ensure the agents behave properly locally. The quadtree is constructed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:func:`drive` calls to ensure the agents behave properly locally. The quadtree is constructed
:func:`drive` calls to ensure the agents see all their neighbours. The quadtree is constructed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 42 to 69
agent_states:
Current states of all agents.
The state must include x: [float], y: [float] coordinate in meters
orientation: [float] in radians with 0 pointing along x and pi/2 pointing along y and
speed: [float] in m/s.

agent_attributes:
Static attributes of all agents.
List of agent attributes. Each agent requires, length: [float]
width: [float] and rear_axis_offset: [float] all in meters. agent_type: [str],
currently supports 'car' and 'pedestrian'.
waypoint: optional [Point], the target waypoint of the agent.

recurrent_states:
Recurrent states for all agents, obtained from the previous call to
:func:`drive` or :func:`initialize` or the large equivalents.

traffic_lights_states:
If the location contains traffic lights within the supported area,
their current state should be provided here. Any traffic light for which no
state is provided will have a state generated by iai.

light_recurrent_states:
Light recurrent states for all agents, obtained from the previous call to
:func:`drive` or :func:`initialize` or the large equivalents.
Specifies the state and time remaining for each light group in the map.
If manual control of individual traffic lights is desired, modify the relevant state(s)
in traffic_lights_states, then pass in light_recurrent_states as usual.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider pointing the reader to the documentation for drive instead. That has its downsides, but makes it less likely that this description gets out of sync from the one in drive.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this idea, I will do the same with large_initialize wherever possible because yeah it might be annoying but it is much more convenient for maintainability and less prone to mistakes and confusion.

Comment on lines 83 to 85
The number of agents allowed in a region before it must subdivide. Currently this
value represents the capacity of a quadtree leaf node that will subdivide if the
number of vehicles in the region passes this threshold.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The number of agents allowed in a region before it must subdivide. Currently this
value represents the capacity of a quadtree leaf node that will subdivide if the
number of vehicles in the region passes this threshold.
The number of agents allowed in a region before it must subdivide. Currently this
value represents the capacity of a quadtree leaf node that will subdivide if the
number of vehicles in the region passes this threshold. In any case,
this will be limited to the maximum currently supported by :func:`drive`.

Assuming we clip this value at 100, which I think we should do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@KieranRatcliffeInvertedAI
Copy link
Contributor Author

Outstanding addition to include:

  • Tests for large initialize
  • Tests for large drive

@Ruishenl
Copy link
Collaborator

Before we merge, please make sure all the tests have passed. Right now we have been getting this error from the log:

tests/test_large_drive.py:38: in <module>
    initial_response = large_initialize(
../../../.cache/pypoetry/virtualenvs/invertedai-mfDNwQNM-py3.10/lib/python3.10/site-packages/pydantic/_internal/_validate_call.py:113: in __call__
    res = self.__pydantic_validator__.validate_python(pydantic_core.ArgsKwargs(args, kwargs))
invertedai/large/initialize.py:368: in large_initialize
    raise InvertedAIError(message=exception_string)
E   invertedai.error.InvertedAIError: Unable to initialize region at x=79.8 y=-4.3 with size 100.0 after 1 attempts. 

@KieranRatcliffeInvertedAI
Copy link
Contributor Author

Before we merge, please make sure all the tests have passed. Right now we have been getting this error from the log:

tests/test_large_drive.py:38: in <module>
    initial_response = large_initialize(
../../../.cache/pypoetry/virtualenvs/invertedai-mfDNwQNM-py3.10/lib/python3.10/site-packages/pydantic/_internal/_validate_call.py:113: in __call__
    res = self.__pydantic_validator__.validate_python(pydantic_core.ArgsKwargs(args, kwargs))
invertedai/large/initialize.py:368: in large_initialize
    raise InvertedAIError(message=exception_string)
E   invertedai.error.InvertedAIError: Unable to initialize region at x=79.8 y=-4.3 with size 100.0 after 1 attempts. 

Absolutely. I ran those tests locally and they passed without issue so I think therein lies the cause for the failure. I will come back and solve this issue before merging.

@KieranRatcliffeInvertedAI KieranRatcliffeInvertedAI merged commit 6d50dac into develop May 28, 2024
5 checks passed
@KieranRatcliffeInvertedAI KieranRatcliffeInvertedAI deleted the large_area_simulation_rework branch May 28, 2024 21:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants