diff --git a/v0.10.0/.buildinfo b/v0.10.0/.buildinfo new file mode 100644 index 0000000000..5211e701cc --- /dev/null +++ b/v0.10.0/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: f508726f7ae0f2c5e3ddb7f9042e91f8 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/v0.10.0/.doctrees/api/index.doctree b/v0.10.0/.doctrees/api/index.doctree new file mode 100644 index 0000000000..bd7856a96c Binary files /dev/null and b/v0.10.0/.doctrees/api/index.doctree differ diff --git a/v0.10.0/.doctrees/api/jupedsim/aabb/index.doctree b/v0.10.0/.doctrees/api/jupedsim/aabb/index.doctree new file mode 100644 index 0000000000..06fa993313 Binary files /dev/null and b/v0.10.0/.doctrees/api/jupedsim/aabb/index.doctree differ diff --git a/v0.10.0/.doctrees/api/jupedsim/distributions/index.doctree b/v0.10.0/.doctrees/api/jupedsim/distributions/index.doctree new file mode 100644 index 0000000000..c08ff10626 Binary files /dev/null and b/v0.10.0/.doctrees/api/jupedsim/distributions/index.doctree differ diff --git a/v0.10.0/.doctrees/api/jupedsim/distributions_app/index.doctree b/v0.10.0/.doctrees/api/jupedsim/distributions_app/index.doctree new file mode 100644 index 0000000000..4818f746dd Binary files /dev/null and b/v0.10.0/.doctrees/api/jupedsim/distributions_app/index.doctree differ diff --git a/v0.10.0/.doctrees/api/jupedsim/grid/index.doctree b/v0.10.0/.doctrees/api/jupedsim/grid/index.doctree new file mode 100644 index 0000000000..f074fe9d0d Binary files /dev/null and b/v0.10.0/.doctrees/api/jupedsim/grid/index.doctree differ diff --git a/v0.10.0/.doctrees/api/jupedsim/index.doctree b/v0.10.0/.doctrees/api/jupedsim/index.doctree new file mode 100644 index 0000000000..11cc7caab7 Binary files /dev/null and b/v0.10.0/.doctrees/api/jupedsim/index.doctree differ diff --git a/v0.10.0/.doctrees/api/jupedsim/recording/index.doctree b/v0.10.0/.doctrees/api/jupedsim/recording/index.doctree new file mode 100644 index 0000000000..715bf94d31 Binary files /dev/null and b/v0.10.0/.doctrees/api/jupedsim/recording/index.doctree differ diff --git a/v0.10.0/.doctrees/api/jupedsim/serialization/index.doctree b/v0.10.0/.doctrees/api/jupedsim/serialization/index.doctree new file mode 100644 index 0000000000..47a8643a2b Binary files /dev/null and b/v0.10.0/.doctrees/api/jupedsim/serialization/index.doctree differ diff --git a/v0.10.0/.doctrees/api/jupedsim/trajectory_writer_sqlite/index.doctree b/v0.10.0/.doctrees/api/jupedsim/trajectory_writer_sqlite/index.doctree new file mode 100644 index 0000000000..b18ab89dab Binary files /dev/null and b/v0.10.0/.doctrees/api/jupedsim/trajectory_writer_sqlite/index.doctree differ diff --git a/v0.10.0/.doctrees/api/jupedsim/util/index.doctree b/v0.10.0/.doctrees/api/jupedsim/util/index.doctree new file mode 100644 index 0000000000..4733e163a7 Binary files /dev/null and b/v0.10.0/.doctrees/api/jupedsim/util/index.doctree differ diff --git a/v0.10.0/.doctrees/disclaimer.doctree b/v0.10.0/.doctrees/disclaimer.doctree new file mode 100644 index 0000000000..12fd4b96ee Binary files /dev/null and b/v0.10.0/.doctrees/disclaimer.doctree differ diff --git a/v0.10.0/.doctrees/environment.pickle b/v0.10.0/.doctrees/environment.pickle new file mode 100644 index 0000000000..b956a7a29a Binary files /dev/null and b/v0.10.0/.doctrees/environment.pickle differ diff --git a/v0.10.0/.doctrees/index.doctree b/v0.10.0/.doctrees/index.doctree new file mode 100644 index 0000000000..839eeaf524 Binary files /dev/null and b/v0.10.0/.doctrees/index.doctree differ diff --git a/v0.10.0/404.html b/v0.10.0/404.html new file mode 100644 index 0000000000..feb24250a7 --- /dev/null +++ b/v0.10.0/404.html @@ -0,0 +1,343 @@ + + + + + + + +
+ + +
+[docs]class AABB:
+ def __init__(
+ self, *, xmin: float, xmax: float, ymin: float, ymax: float
+ ) -> None:
+ if xmax < xmin or ymax < ymin:
+ raise Exception(
+ f"Invalid arguments to create AABB: max values have to be larger than min values."
+ )
+ self.xmin = xmin
+ self.xmax = xmax
+ self.ymin = ymin
+ self.ymax = ymax
+
+ @property
+
+
+ @property
+
+
+ @property
+[docs] def center(self) -> tuple[float, float]:
+ return (self.xmin + self.width / 2, self.ymin + self.height / 2)
+
+ @staticmethod
+[docs] def combine(aabb, *other):
+ xmin = aabb.xmin
+ ymin = aabb.ymin
+ xmax = aabb.xmax
+ ymax = aabb.ymax
+
+ for aabb in other:
+ xmin = min(xmin, aabb.xmin)
+ ymin = min(ymin, aabb.ymin)
+ xmax = max(xmax, aabb.xmax)
+ ymax = max(ymax, aabb.ymax)
+
+ return AABB(xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax)
+
+import numpy as np
+import shapely.geometry as shply
+
+from jupedsim.grid import Grid
+
+
+
+
+
+[docs]class IncorrectParameterError(Exception):
+ def __init__(self, message):
+ self.message = message
+
+
+[docs]class OverlappingCirclesError(Exception):
+ def __init__(self, message):
+ self.message = message
+
+
+
+
+
+def __intersecting_area_polygon_circle(mid_point, radius, polygon):
+ """returns the intersecting area of circle and polygon"""
+ # creates a point
+ point = shply.Point(mid_point)
+ # creates a layer with the size of the radius all around this point
+ circle = point.buffer(radius)
+ # returns the size of the intersecting area
+ return polygon.intersection(circle).area
+
+
+def __is_inside_circle(point, mid, min_r, max_r):
+ """checks if a point is inside a Circle segment reaching from minimum radius to maximum radius"""
+ dif_x = point[0] - mid[0]
+ dif_y = point[1] - mid[1]
+ circle_equation = dif_x**2 + dif_y**2
+ return min_r**2 <= circle_equation <= max_r**2
+
+
+def __get_bounding_box(polygon):
+ """returns an Axis Aligned Bounding Box containing the minimal/maximal x and y values
+ formatted like : [(min(x_values), min(y_values)), (max(x_values), max(y_values))]
+ polygon needs to be a shapely polygon
+ """
+ corner_points = list(polygon.exterior.coords)
+ x_values, y_values = [], []
+ for point in corner_points:
+ x_values.append(point[0])
+ y_values.append(point[1])
+
+ return [(min(x_values), min(y_values)), ((max(x_values)), max(y_values))]
+
+
+def __min_distance_to_polygon(pt, polygon):
+ """returns the minimal distance between a point and every line segment of a polygon"""
+ pt = shply.Point(pt)
+ min_dist = polygon.exterior.distance(pt)
+ for hole in polygon.interiors:
+ candidate_dist = hole.distance(pt)
+ min_dist = min(min_dist, candidate_dist)
+ return min_dist
+
+
+[docs]def distribute_by_number(
+ *,
+ polygon,
+ number_of_agents,
+ distance_to_agents,
+ distance_to_polygon,
+ seed=None,
+ max_iterations=10000,
+):
+ """ "returns number_of_agents points randomly placed inside the polygon
+
+ :param polygon: shapely polygon in which the agents will be placed
+ :param number_of_agents: number of agents distributed
+ :param distance_to_agents: minimal distance between the centers of agents
+ :param distance_to_polygon: minimal distance between the center of agents and the polygon edges
+ :param seed: define a seed for random generation, Default value is None which corresponds to a random value
+ :param max_iterations: no more than max_iterations must find a point inside the polygon, default is 10_000
+ :return: list of created points"""
+
+ if not isinstance(polygon, shply.polygon.Polygon):
+ raise IncorrectParameterError(
+ f"Polygon is expected to be a shapely Polygon"
+ )
+ box = __get_bounding_box(polygon)
+
+ np.random.seed(seed)
+
+ grid = Grid(box, distance_to_agents)
+ created_points = 0
+ iterations = 0
+ while created_points < number_of_agents:
+ if iterations > max_iterations:
+ msg = (
+ f"Only {created_points} of {number_of_agents} could be placed."
+ f" density: {round(created_points / polygon.area, 2)} p/m²"
+ )
+ raise AgentNumberError(msg)
+ temp_point = (
+ np.random.uniform(box[0][0], box[1][0]),
+ np.random.uniform(box[0][1], box[1][1]),
+ )
+ if __check_distance_constraints(
+ temp_point, distance_to_polygon, grid, polygon
+ ):
+ grid.append_point(temp_point)
+
+ iterations = 0
+ created_points += 1
+ else:
+ iterations += 1
+
+ return grid.get_samples()
+
+
+[docs]def distribute_by_density(
+ *,
+ polygon,
+ density,
+ distance_to_agents,
+ distance_to_polygon,
+ seed=None,
+ max_iterations=10000,
+):
+ """returns points randomly placed inside the polygon with the given density
+
+ :param polygon: shapely polygon in which the agents will be placed
+ :param density: Density of agents inside the polygon
+ :param distance_to_agents: minimal distance between the centers of agents
+ :param distance_to_polygon: minimal distance between the center of agents and the polygon edges
+ :param seed: define a seed for random generation, Default value is None which corresponds to a random value
+ :param max_iterations: no more than max_iterations must find a point inside the polygon, Default is 10_000
+ :return: list of created points"""
+
+ if not isinstance(polygon, shply.polygon.Polygon):
+ raise IncorrectParameterError(
+ f"Polygon is expected to be a shapely Polygon"
+ )
+ area = polygon.area
+ number = round(density * area)
+ return distribute_by_number(
+ polygon=polygon,
+ number_of_agents=number,
+ distance_to_agents=distance_to_agents,
+ distance_to_polygon=distance_to_polygon,
+ seed=seed,
+ max_iterations=max_iterations,
+ )
+
+
+def __catch_wrong_inputs(
+ polygon, center_point, circle_segment_radii, fill_parameters
+):
+ """checks if an input parameter is incorrect and raises an Exception"""
+ if not isinstance(polygon, shply.polygon.Polygon):
+ raise IncorrectParameterError(
+ f"Polygon is expected to be a shapely Polygon"
+ )
+ try:
+ if len(center_point) != 2:
+ raise IncorrectParameterError(
+ f"Center_point expected a tuple of 2 numbers, {len(center_point)} were given"
+ )
+ except TypeError:
+ # center point is no tuple or list
+ raise IncorrectParameterError(
+ f"Center_point expected a tuple of 2 numbers, given Type: {type(center_point)}"
+ )
+ if len(circle_segment_radii) != len(fill_parameters):
+ raise IncorrectParameterError(
+ f"the number of circle segments does not match the number of fill parameters.\n"
+ f"radii given for {len(circle_segment_radii)} circle segments,"
+ f"fill parameter given for {len(fill_parameters)} circle segments"
+ )
+ for i, c_s_radius in enumerate(circle_segment_radii):
+ if c_s_radius[0] < 0 or c_s_radius[1] < 0:
+ raise NegativeValueError(
+ f"Circle segment {c_s_radius[0]} : {c_s_radius[1]} is expected to be positiv"
+ )
+ if c_s_radius[0] >= c_s_radius[1]:
+ raise OverlappingCirclesError(
+ f"inner radius bigger than/equal to outer radius\n"
+ f"a Circle segment from {c_s_radius[0]} to {c_s_radius[1]} is not possible"
+ )
+ j = 0
+ while j < i:
+ if (
+ c_s_radius[0] < c_s_radius[1] <= circle_segment_radii[j][0]
+ or circle_segment_radii[j][1] <= c_s_radius[0] < c_s_radius[1]
+ ):
+ j = j + 1
+ continue
+ else:
+ raise OverlappingCirclesError(
+ f"the Circle from {c_s_radius[0]} to {c_s_radius[1]} overlaps with others"
+ )
+
+
+[docs]def distribute_in_circles_by_number(
+ *,
+ polygon,
+ distance_to_agents,
+ distance_to_polygon,
+ center_point,
+ circle_segment_radii,
+ numbers_of_agents,
+ seed=None,
+ max_iterations=10_000,
+):
+ """returns points randomly placed inside the polygon inside each the circle segments
+
+ :param polygon: shapely polygon in which the agents will be placed
+ :param distance_to_agents: minimal distance between the centers of agents
+ :param distance_to_polygon: minimal distance between the center of agents and the polygon edges
+ :param center_point: the Center point of the circle segments
+ :param circle_segment_radii: a list of minimal and maximal radius for each circle segment
+ Circle segments must not overlap
+ formatted like [(minimum_radius, maximum_radius)]
+ :param numbers_of_agents: a list of number of agents for each Circle segment
+ the position of the number corresponds to the order in which the Circle segments are given
+ :param seed: define a seed for random generation, Default value is None which corresponds to a random value
+ :param max_iterations: no more than max_iterations must find a point inside the polygon, Default is 10_000
+ :return: list of created points"""
+
+ # catch wrong inputs
+ __catch_wrong_inputs(
+ polygon=polygon,
+ center_point=center_point,
+ circle_segment_radii=circle_segment_radii,
+ fill_parameters=numbers_of_agents,
+ )
+ np.random.seed(seed)
+ box = __get_bounding_box(polygon)
+ grid = Grid(box, distance_to_agents)
+
+ for circle_segment, number in zip(circle_segment_radii, numbers_of_agents):
+ outer_radius = circle_segment[1]
+ inner_radius = circle_segment[0]
+ big_circle_area = __intersecting_area_polygon_circle(
+ center_point, outer_radius, polygon
+ )
+ small_circle_area = __intersecting_area_polygon_circle(
+ center_point, inner_radius, polygon
+ )
+ placeable_area = big_circle_area - small_circle_area
+
+ # checking whether to place points
+ # inside the circle segment or
+ # inside the bounding box of the intersection of polygon and Circle Segment
+
+ # determine the entire area of the circle segment
+ entire_circle_area = np.pi * (outer_radius**2 - inner_radius**2)
+ # determine the area where a point might be placed around the polygon
+ sec_box = __box_of_intersection(polygon, center_point, outer_radius)
+ dif_x, dif_y = (
+ sec_box[1][0] - sec_box[0][0],
+ sec_box[1][1] - sec_box[0][1],
+ )
+ bounding_box_area = dif_x * dif_y
+
+ if entire_circle_area < bounding_box_area:
+ # inside the circle it is more likely to find a random point that is inside the polygon
+ for placed_count in range(number):
+ i = 0
+ while i < max_iterations:
+ i += 1
+ # determines a random radius within the circle segment
+ rho = np.sqrt(
+ np.random.uniform(inner_radius**2, outer_radius**2)
+ )
+ # determines a random degree
+ theta = np.random.uniform(0, 2 * np.pi)
+ pt = center_point[0] + rho * np.cos(theta), center_point[
+ 1
+ ] + rho * np.sin(theta)
+ if __check_distance_constraints(
+ pt, distance_to_polygon, grid, polygon
+ ):
+ grid.append_point(pt)
+ break
+
+ if i >= max_iterations and placed_count != number:
+ message = (
+ f"the desired amount of agents in the Circle segment from"
+ f" {inner_radius} to {outer_radius} could not be achieved."
+ f"\nOnly {placed_count} of {number} could be placed."
+ f"\nactual density: {round(placed_count / placeable_area, 2)} p/m²"
+ )
+ raise AgentNumberError(message)
+ else:
+ # placing point inside the bounding box is more likely to find a random point that is inside the circle
+ placed_count = 0
+ iterations = 0
+ while placed_count < number:
+ if iterations > max_iterations:
+ message = (
+ f"the desired amount of agents in the Circle segment from"
+ f" {inner_radius} to {outer_radius} could not be achieved."
+ f"\nOnly {placed_count} of {number} could be placed."
+ f"\nactual density: {round(placed_count / placeable_area, 2)} p/m²"
+ )
+ raise AgentNumberError(message)
+ temp_point = (
+ np.random.uniform(sec_box[0][0], sec_box[1][0]),
+ np.random.uniform(sec_box[0][1], sec_box[1][1]),
+ )
+ if __is_inside_circle(
+ temp_point, center_point, inner_radius, outer_radius
+ ) and __check_distance_constraints(
+ temp_point, distance_to_polygon, grid, polygon
+ ):
+ grid.append_point(temp_point)
+ iterations = 0
+ placed_count += 1
+ else:
+ iterations += 1
+ return grid.get_samples()
+
+
+[docs]def distribute_in_circles_by_density(
+ *,
+ polygon,
+ distance_to_agents,
+ distance_to_polygon,
+ center_point,
+ circle_segment_radii,
+ densities,
+ seed=None,
+ max_iterations=10_000,
+):
+ """returns points randomly placed inside the polygon inside each the circle segments
+
+ :param polygon: shapely polygon in which the agents will be placed
+ :param distance_to_agents: minimal distance between the centers of agents
+ :param distance_to_polygon: minimal distance between the center of agents and the polygon edges
+ :param center_point: the Center point of the circle segments
+ :param circle_segment_radii: a list of minimal and maximal radius for each circle segment
+ Circle segments must not overlap
+ formatted like [(minimum_radius, maximum_radius)]
+ :param densities: a list of densities for each Circle segment
+ the position of the number corresponds to the order in which the Circle segments are given
+ :param seed: define a seed for random generation, Default value is None which corresponds to a random value
+ :param max_iterations: no more than max_iterations must find a point inside the polygon, Default is 10_000
+ :return: list of created points"""
+
+ __catch_wrong_inputs(
+ polygon=polygon,
+ center_point=center_point,
+ circle_segment_radii=circle_segment_radii,
+ fill_parameters=densities,
+ )
+ number_of_agents = []
+ for circle_segment, density in zip(circle_segment_radii, densities):
+ big_circle_area = __intersecting_area_polygon_circle(
+ center_point, circle_segment[1], polygon
+ )
+ small_circle_area = __intersecting_area_polygon_circle(
+ center_point, circle_segment[0], polygon
+ )
+ placeable_area = big_circle_area - small_circle_area
+ number_of_agents.append(int(density * placeable_area))
+
+ return distribute_in_circles_by_number(
+ polygon=polygon,
+ distance_to_agents=distance_to_agents,
+ distance_to_polygon=distance_to_polygon,
+ center_point=center_point,
+ circle_segment_radii=circle_segment_radii,
+ numbers_of_agents=number_of_agents,
+ seed=seed,
+ max_iterations=max_iterations,
+ )
+
+
+[docs]def distribute_till_full(
+ *,
+ polygon,
+ distance_to_agents,
+ distance_to_polygon,
+ seed=None,
+ max_iterations=10_000,
+ k=30,
+):
+ """returns as many randomly placed points as fit into the polygon.
+ Points are distributed using Bridson’s algorithm for Poisson-disc sampling
+ The algorithm is explained in Robert Bridson´s Paper "Fast Poisson Disk Sampling in Arbitrary Dimensions"
+ :param polygon: shapely polygon in which the agents will be placed
+ :param distance_to_agents: minimal distance between the centers of agents
+ :param distance_to_polygon: minimal distance between the center of agents and the polygon edges
+ :param seed: define a seed for random generation, Default value is None which corresponds to a random value
+ :param max_iterations: no more than max_iterations must find a point inside the polygon, default is 10_000
+ :param k: around each point k point will be created before the point is considered inactive
+ :return: list of created points"""
+ if not isinstance(polygon, shply.polygon.Polygon):
+ raise IncorrectParameterError(
+ f"Polygon is expected to be a shapely Polygon"
+ )
+ box = __get_bounding_box(polygon)
+
+ np.random.seed(seed)
+ # initialises a list for active Points and a Grid administering all created points
+ active = []
+ grid = Grid(box, distance_to_agents)
+ # initialisation of the first point
+ iteration = 0
+ while iteration < max_iterations:
+ first_point = (
+ np.random.uniform(box[0][0], box[1][0]),
+ np.random.uniform(box[0][1], box[1][1]),
+ )
+ if __check_distance_constraints(
+ first_point, distance_to_polygon, grid, polygon
+ ):
+ grid.append_point(first_point)
+ active.append(first_point)
+ break
+ iteration = iteration + 1
+ if iteration >= max_iterations:
+ raise IncorrectParameterError(
+ "The first point could not be placed inside the polygon."
+ " Check if there is enough space for agents provided inside the polygon"
+ )
+
+ # while points are active a random reference point is selected
+ while active:
+ ref_point = active[np.random.randint(0, len(active))]
+ iteration = 0
+ # tries to find a point around the reference Point
+ while iteration < k:
+ # determines a random radius within a circle segment
+ # with radius from distance_to_agents to distance_to_agents * 2
+ rho = np.sqrt(
+ np.random.uniform(
+ distance_to_agents**2, 4 * distance_to_agents**2
+ )
+ )
+ # determines a random degree
+ theta = np.random.uniform(0, 2 * np.pi)
+ pt = ref_point[0] + rho * np.cos(theta), ref_point[
+ 1
+ ] + rho * np.sin(theta)
+ if __check_distance_constraints(
+ pt, distance_to_polygon, grid, polygon
+ ):
+ grid.append_point(pt)
+ active.append(pt)
+ break
+ iteration = iteration + 1
+
+ # if there was no point found around the reference point it is considered inactive
+ if iteration >= k:
+ active.remove(ref_point)
+
+ return grid.get_samples()
+
+
+[docs]def distribute_by_percentage(
+ *,
+ polygon,
+ percent,
+ distance_to_agents,
+ distance_to_polygon,
+ seed=None,
+ max_iterations=10000,
+ k=30,
+):
+ """returns points for the desired percentage of agents that fit inside the polygon (max possible number)
+ fills the polygon entirely using Bridson’s algorithm for Poisson-disc sampling and then selects the percentage of placed agents
+
+
+ :param polygon: shapely polygon in which the agents will be placed
+ :param percent: percentage of agents selected - 100% ≙ completely filled polygon 0% ≙ 0 placed points
+ :param distance_to_agents: minimal distance between the centers of agents
+ :param distance_to_polygon: minimal distance between the center of agents and the polygon edges
+ :param seed: define a seed for random generation, Default value is None which corresponds to a random value
+ :param max_iterations: no more than max_iterations must find a point inside the polygon, Default is 10_000
+ :return: list of created points"""
+ samples = distribute_till_full(
+ polygon=polygon,
+ distance_to_agents=distance_to_agents,
+ distance_to_polygon=distance_to_polygon,
+ seed=seed,
+ max_iterations=max_iterations,
+ k=k,
+ )
+ sample_amount = len(samples)
+ needed_amount = round(sample_amount * (percent / 100))
+ np.random.seed(seed)
+ np.random.shuffle(samples)
+
+ return samples[:needed_amount]
+
+
+def __check_distance_constraints(pt, wall_distance, grid, polygon):
+ """Determines if a point has enough distance to other points and to the walls
+ Uses a Grid to determine neighbours
+ :param grid: the grid of the polygon
+ :param pt: point that is being checked
+ :param wall_distance: minimal distance between point and the polygon
+ :param polygon: shapely Polygon in which the points must lie
+ :return:True or False"""
+ if not polygon.contains(shply.Point(pt)):
+ return False
+ if __min_distance_to_polygon(pt, polygon) < wall_distance:
+ return False
+ return grid.no_neighbours_in_distance(pt)
+
+
+def __box_of_intersection(polygon, center_point, outer_radius):
+ """returns an Axis Aligned Bounding Box containing the intersection of a Circle and the polygon
+ @:param polygon is a shapely Polygon
+ @:param center_point is the Center point of the Circle
+ @:param outer_radius is the radius of the Circle
+ @:return bounding box formatted like [(min(x_values), min(y_values)), (max(x_values), max(y_values))]
+ """
+ # creates a point
+ point = shply.Point(center_point)
+ # creates a layer with the size of the radius all around this point
+ circle = point.buffer(outer_radius)
+ # returns the size of the intersecting area
+ shapely_bounds = polygon.intersection(circle).bounds
+ return [shapely_bounds[:2], shapely_bounds[2:]]
+
+import distributions
+import matplotlib.pyplot as plt
+import streamlit as st
+
+
+[docs]def show_points(
+ s_polygon,
+ samples,
+ radius,
+ circle_segment_radii=None,
+ center_point=None,
+ obstacles=None,
+):
+ if obstacles is None:
+ obstacles = []
+ box = distributions.__get_bounding_box(s_polygon)
+ exterior = list(s_polygon.exterior.coords)
+ fig = plt.figure()
+ ax = fig.add_subplot(1, 1, 1)
+ for elem in samples:
+ ax.add_patch(plt.Circle(radius=radius / 2, xy=elem, fill=False))
+ ax.add_patch(plt.Circle(radius=0.1, xy=elem, color="r"))
+
+ if circle_segment_radii is not None:
+ for circle_segment in circle_segment_radii:
+ ax.add_patch(
+ plt.Circle(
+ radius=circle_segment[0], xy=center_point, fill=False
+ )
+ )
+ ax.add_patch(
+ plt.Circle(
+ radius=circle_segment[1], xy=center_point, fill=False
+ )
+ )
+
+ n = len(exterior)
+ i = 0
+ while True:
+ following = (i + 1) % n
+ x_value = [exterior[i][0], exterior[following][0]]
+ y_value = [exterior[i][1], exterior[following][1]]
+ plt.plot(x_value, y_value, color="blue")
+
+ i += 1
+ if following == 0:
+ break
+
+ for obstacle in obstacles:
+ n = len(obstacle)
+ i = 0
+ while True:
+ following = (i + 1) % n
+ x_value = [obstacle[i][0], obstacle[following][0]]
+ y_value = [obstacle[i][1], obstacle[following][1]]
+ plt.plot(x_value, y_value, color="black")
+
+ i += 1
+ if following == 0:
+ break
+
+ plt.xlim(box[0][0], box[1][0])
+ plt.ylim(box[0][1], box[1][1])
+ plt.axis("equal")
+ st.pyplot(fig)
+
+
+[docs]def main():
+ default_polygon = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)]
+ agents = density = 0
+
+ with st.sidebar:
+ st.text("General settings")
+ distance_to_agents = st.slider(
+ "Distance between the center points of the agents",
+ 0.1,
+ 2.0,
+ value=0.3,
+ )
+ distance_to_polygon = st.slider(
+ "Distance between agents and walls", 0.1, 2.0, value=0.3
+ )
+ st.text("Polygon settings")
+ corners = st.number_input(
+ "Number of edges in polygon", 3, value=len(default_polygon)
+ )
+ col1, col2 = st.columns(2)
+ x_values, y_values = [], []
+ with col1:
+ for i in range(corners):
+ x_values.append(
+ st.number_input(
+ f"X{i + 1}",
+ value=default_polygon[i][0]
+ if i < len(default_polygon)
+ else 0.0,
+ step=1.0,
+ )
+ )
+ with col2:
+ for i in range(corners):
+ y_values.append(
+ st.number_input(
+ f"Y{i + 1}",
+ value=default_polygon[i][1]
+ if i < len(default_polygon)
+ else 0.0,
+ step=1.0,
+ )
+ )
+ seed = st.number_input(
+ "Set a seed for random number generation.\n"
+ "Agent Distribution with the same settings will lead to the same output.\n"
+ "If the seed is set to zero the distribution will be random each time",
+ 0,
+ )
+ if seed == 0:
+ seed = None
+ st.text(
+ "Hole settings:\n"
+ "Holes define Spaces where no Agents must be placed."
+ )
+ obstacle_count = st.number_input("Number of Holes", 0, value=0)
+ obstacle_corners = []
+ obstacle_values = []
+ for i in range(obstacle_count):
+ obstacle_corners.append(
+ st.number_input(f"Number of edges in hole {i + 1}", 3, value=4)
+ )
+ col3, col4 = st.columns(2)
+ with col3:
+ for i in range(obstacle_count):
+ obstacle_values.append([])
+ st.text(f"Settings for")
+ # the rest of the text can be found in column 4
+ for j in range(obstacle_corners[i]):
+ obstacle_values[i].append(
+ [
+ st.number_input(
+ f"X{j + 1} in hole {i + 1}",
+ value=0.0,
+ step=1.0,
+ )
+ ]
+ )
+ with col4:
+ for i in range(obstacle_count):
+ st.text(f"hole {i + 1}")
+ # this text is placed so the text "Settings for hole <i+1> aligns through columns 3 & 4
+ for j in range(obstacle_corners[i]):
+ obstacle_values[i][j].append(
+ st.number_input(
+ f"Y{j + 1} in hole {i + 1}", value=0.0, step=1.0
+ )
+ )
+
+ obstacles = []
+ for i in range(obstacle_count):
+ obstacles.append([])
+ for j in range(obstacle_corners[i]):
+ obstacles[i].append(
+ (obstacle_values[i][j][0], obstacle_values[i][j][1])
+ )
+ polygon = []
+ for x, y in zip(x_values, y_values):
+ polygon.append((x, y))
+ s_polygon = distributions.shply.Polygon(polygon, obstacles)
+ area = s_polygon.area
+ distribution_type = st.radio(
+ "How to distribute agents?",
+ (
+ "place by number/density",
+ "place in circles",
+ "place till full",
+ "place by percentage",
+ ),
+ )
+
+ if distribution_type == "place in circles":
+ st.text(
+ "Instructions: "
+ "Agents will be distributed inside both the polygon and circle segments.\n"
+ "Agents do not keep distance to circle segment borders like polygons.\n"
+ "You need to select a center point for all circle segments.\n"
+ "You also need to select the range of each circle segment.\n"
+ "Circle segments musst not overlap or have negativ values.\n"
+ "General settings can be made in the left sidebar."
+ )
+ style = st.radio(
+ f"How to choose number of agents distributed inside circle segments?",
+ ("density", "number"),
+ )
+ circle_count = st.number_input("number of circles", 1)
+ col1, col2 = st.columns(2)
+ min_values, max_values = [], []
+ densities, agents = [], []
+ default_center = (5.0, 5.0)
+ with col1:
+ mid_x = st.number_input(
+ "Center X", value=default_center[0], step=1.0
+ )
+ for i in range(circle_count):
+ min_values.append(
+ st.number_input(
+ f"inner radius for Circle segment {i + 1}", step=1.0
+ )
+ )
+ with col2:
+ mid_y = st.number_input(
+ "Center Y", value=default_center[1], step=1.0
+ )
+ for i in range(circle_count):
+ max_values.append(
+ st.number_input(
+ f"outer radius for Circle segment {i + 1}", step=1.0
+ )
+ )
+
+ center_point = (mid_x, mid_y)
+ circle_segment_radii = []
+ for i in range(circle_count):
+ circle_segment_radii.append((min_values[i], max_values[i]))
+
+ for i in range(circle_count):
+ if style == "density":
+ densities.append(
+ st.slider(
+ f"Agents / m² in Circle segment {i + 1}",
+ 0.1,
+ round(
+ (
+ 69.2636
+ * (distance_to_agents ** (-1.89412))
+ / 100
+ ),
+ 2,
+ ),
+ key=i,
+ )
+ )
+ # the function for the maximum selectable density is an estimate of the real maximum density possible
+ # the estimation was done by Power-Regression
+ elif style == "number":
+ temp_min, temp_max = (
+ circle_segment_radii[i][0],
+ circle_segment_radii[i][1],
+ )
+ area_small = distributions.__intersecting_area_polygon_circle(
+ center_point, temp_min, s_polygon
+ )
+ area_big = distributions.__intersecting_area_polygon_circle(
+ center_point, temp_max, s_polygon
+ )
+ area_segment = area_big - area_small
+ agents.append(
+ st.slider(
+ f"Agents in Circle segment {i + 1}",
+ 1,
+ round(area_segment * 8),
+ key=i,
+ )
+ )
+
+ button_clicked = st.button("distribute agents")
+ if button_clicked:
+ samples = []
+ if style == "density":
+ samples = distributions.distribute_in_circles_by_density(
+ polygon=s_polygon,
+ distance_to_agents=distance_to_agents,
+ distance_to_polygon=distance_to_polygon,
+ center_point=center_point,
+ circle_segment_radii=circle_segment_radii,
+ densities=densities,
+ seed=seed,
+ )
+ elif style == "number":
+ samples = distributions.distribute_in_circles_by_number(
+ polygon=s_polygon,
+ distance_to_agents=distance_to_agents,
+ distance_to_polygon=distance_to_polygon,
+ center_point=center_point,
+ circle_segment_radii=circle_segment_radii,
+ numbers_of_agents=agents,
+ seed=seed,
+ )
+ st.text(f"Below should be a plot containing {len(samples)} agents")
+ show_points(
+ s_polygon,
+ samples,
+ distance_to_agents,
+ circle_segment_radii,
+ center_point,
+ obstacles,
+ )
+ else:
+ show_points(
+ s_polygon,
+ [],
+ distance_to_agents,
+ circle_segment_radii,
+ center_point,
+ obstacles,
+ )
+
+ if distribution_type == "place by number/density":
+ st.text(
+ "Instructions: "
+ "a certain amount of agents will be distributed inside the polygon.\n"
+ "You can enter this amount either by a specific number or a density.\n"
+ "General settings can be made in the left sidebar."
+ )
+ style = st.radio(
+ "How to choose number of agents?", ("density", "number")
+ )
+ if style == "density":
+ density = st.slider(
+ "Agents / m²",
+ 0.1,
+ round((69.2636 * (distance_to_agents ** (-1.89412)) / 100), 2),
+ )
+ # the function for the maximum selectable density is an estimate of the real maximum density possible
+ # the estimation was done by Power-Regression
+ elif style == "number":
+ agents = st.slider("Agents", 1, round(area * 8))
+
+ button_clicked = st.button("distribute agents")
+
+ if button_clicked:
+ samples = []
+ if style == "density":
+ samples = distributions.distribute_by_density(
+ polygon=s_polygon,
+ density=density,
+ distance_to_agents=distance_to_agents,
+ distance_to_polygon=distance_to_polygon,
+ seed=seed,
+ )
+ elif style == "number":
+ samples = distributions.distribute_by_number(
+ polygon=s_polygon,
+ number_of_agents=agents,
+ distance_to_agents=distance_to_agents,
+ distance_to_polygon=distance_to_polygon,
+ seed=seed,
+ )
+ st.text(f"Below should be a plot containing {len(samples)} agents")
+ show_points(
+ s_polygon=s_polygon,
+ samples=samples,
+ radius=distance_to_agents,
+ obstacles=obstacles,
+ )
+ else:
+ show_points(
+ s_polygon=s_polygon,
+ samples=[],
+ radius=distance_to_agents,
+ obstacles=obstacles,
+ )
+
+ if distribution_type == "place till full":
+ st.text(
+ "Instructions: "
+ "agents will be distributed all over the polygon.\n"
+ "You can enter how many tries there should be to "
+ "find another place to put an agent before a polygon is considered filled.\n"
+ "General settings can be made in the left sidebar"
+ )
+ max_iterations = st.number_input(
+ "maximum tries to find the first point", 1, value=10_000, step=100
+ )
+ k = st.number_input(
+ "maximum tries to find a point around a reference point",
+ 1,
+ value=30,
+ step=1,
+ )
+ button_clicked = st.button("distribute agents")
+
+ if button_clicked:
+ samples = distributions.distribute_till_full(
+ polygon=s_polygon,
+ distance_to_agents=distance_to_agents,
+ distance_to_polygon=distance_to_polygon,
+ seed=seed,
+ max_iterations=max_iterations,
+ k=k,
+ )
+ st.text(f"Below should be a plot containing {len(samples)} agents")
+ show_points(
+ s_polygon=s_polygon,
+ samples=samples,
+ radius=distance_to_agents,
+ obstacles=obstacles,
+ )
+ else:
+ show_points(
+ s_polygon=s_polygon,
+ samples=[],
+ radius=distance_to_agents,
+ obstacles=obstacles,
+ )
+
+ if distribution_type == "place by percentage":
+ st.text(
+ "Instructions: "
+ "distributes agents inside the polygon.\n"
+ "The polygon will be filled to a certain percentage.\n"
+ "You can also enter how many tries there should be to "
+ "find another place to put an agent before a polygon is considered filled.\n"
+ "General settings can be made in the left sidebar"
+ )
+ percent = st.slider("Percent filled: ", 1, 100)
+ max_iterations = st.number_input(
+ "maximum tries to find the first point", 1, value=10_000, step=100
+ )
+ k = st.number_input(
+ "maximum tries to find a point around a reference point",
+ 1,
+ value=30,
+ step=1,
+ )
+ button_clicked = st.button("distribute agents")
+
+ if button_clicked:
+ samples = distributions.distribute_by_percentage(
+ polygon=s_polygon,
+ percent=percent,
+ distance_to_agents=distance_to_agents,
+ distance_to_polygon=distance_to_polygon,
+ seed=seed,
+ max_iterations=max_iterations,
+ k=k,
+ )
+ st.text(f"Below should be a plot containing {len(samples)} agents")
+ show_points(
+ s_polygon=s_polygon,
+ samples=samples,
+ radius=distance_to_agents,
+ obstacles=obstacles,
+ )
+ else:
+ show_points(
+ s_polygon=s_polygon,
+ samples=[],
+ radius=distance_to_agents,
+ obstacles=obstacles,
+ )
+
+
+if __name__ == "__main__":
+ main()
+
+from math import sqrt
+
+import numpy as np
+
+
+[docs]class Grid:
+ """Class to save points and check for neighbours within a radius
+
+ box : an Axis Aligned Bounding Box where the Grid will be able to save points
+ distance_to_agents : radius in which points are searched for
+ """
+
+ def __init__(self, box, distance_to_agents):
+ self.box = box
+ self.a_r = distance_to_agents
+ width, height = box[1][0] - box[0][0], box[1][1] - box[0][1]
+ # Cell side length
+ # distance between points is divided by sqrt(2) so that only one point fits inside each Cell of the grid
+ # also the diagonal of each Cell is equal to the distance between points
+ # based on Robert Bridson: Fast Poisson Disk Sampling in Arbitrary Dimensions
+ self.c_s_l = distance_to_agents / np.sqrt(2)
+ # Number of cells in the x- and y-directions of the grid
+ self.nx, self.ny = (
+ int(width / self.c_s_l) + 1,
+ int(height / self.c_s_l) + 1,
+ )
+ # A list of coordinates in the grid of cells
+ self.coords_list = [
+ (ix, iy) for ix in range(self.nx) for iy in range(self.ny)
+ ]
+ # Initialize the dictionary of cells: each key is a cell's coordinates, the
+ # corresponding value is the index of that cell's point's coordinates in the
+ # samples list (or None if the cell is empty).
+ self.cells = {coords: None for coords in self.coords_list}
+ self.samples = []
+
+[docs] def append_point(self, pt):
+ cell_coords = self.get_cell_coords(pt)
+ self.cells[cell_coords] = pt
+ self.samples.append(pt)
+
+
+
+[docs] def get_cell_coords(self, pt):
+ """Get the coordinates of the cell that pt = (x,y) falls in.
+ box is bounding box containing the minimal/maximal x and y values"""
+ return int((pt[0] - self.box[0][0]) // self.c_s_l), int(
+ (pt[1] - self.box[0][1]) // self.c_s_l
+ )
+
+[docs] def no_neighbours_in_distance(self, pt):
+ coords = self.get_cell_coords(pt)
+ return not self.has_neighbour_in_distance(pt, coords)
+
+[docs] def has_neighbour_in_distance(self, pt, coords):
+ """ "returns true if there is any point in grid with lt or equal the distance `agent radius` to `pt`"""
+ dxdy = [
+ (-1, -2),
+ (0, -2),
+ (1, -2),
+ (-2, -1),
+ (-1, -1),
+ (0, -1),
+ (1, -1),
+ (2, -1),
+ (-2, 0),
+ (-1, 0),
+ (1, 0),
+ (2, 0),
+ (-2, 1),
+ (-1, 1),
+ (0, 1),
+ (1, 1),
+ (2, 1),
+ (-1, 2),
+ (0, 2),
+ (1, 2),
+ (0, 0),
+ ]
+ for dx, dy in dxdy:
+ neighbour_coords = coords[0] + dx, coords[1] + dy
+ if not (
+ 0 <= neighbour_coords[0] < self.nx
+ and 0 <= neighbour_coords[1] < self.ny
+ ):
+ # Points are not on the grid
+ continue
+ neighbour = self.cells[neighbour_coords]
+ if neighbour is not None:
+ # Inside a Cell close the the point is a potential neighbour
+ dif_y, dif_x = neighbour[1] - pt[1], neighbour[0] - pt[0]
+ distance = sqrt((dif_x**2) + (dif_y**2))
+ if distance < self.a_r:
+ return True
+ return False
+
+import sqlite3
+from dataclasses import dataclass
+
+import shapely
+
+from jupedsim.aabb import AABB
+
+
+@dataclass
+
+
+
+@dataclass
+
+
+
+[docs]class Recording:
+ __supported_database_version = 1
+ """Provides access to a simulation recording in a sqlite database"""
+
+ def __init__(self, db_connection_str: str, uri=False) -> None:
+ self.db = sqlite3.connect(
+ db_connection_str, uri=uri, isolation_level=None
+ )
+ self._check_version_compatible()
+
+[docs] def frame(self, index: int) -> RecordingFrame:
+ def agent_row(cursor, row):
+ return RecordingAgent(row[0], (row[1], row[2]), (row[3], row[4]))
+
+ cur = self.db.cursor()
+ cur.row_factory = agent_row
+ res = cur.execute(
+ "SELECT id, pos_x, pos_y, ori_x, ori_y FROM trajectory_data WHERE frame == (?) ORDER BY id ASC",
+ (index,),
+ )
+ return RecordingFrame(index, res.fetchall())
+
+[docs] def geometry(self) -> shapely.GeometryCollection:
+ cur = self.db.cursor()
+ res = cur.execute("SELECT wkt FROM geometry")
+ wkt_str = res.fetchone()[0]
+ return shapely.from_wkt(wkt_str)
+
+[docs] def bounds(self) -> AABB:
+ cur = self.db.cursor()
+ res = cur.execute("SELECT value FROM metadata WHERE key == 'xmin'")
+ xmin = float(res.fetchone()[0])
+ res = cur.execute("SELECT value FROM metadata WHERE key == 'xmax'")
+ xmax = float(res.fetchone()[0])
+ res = cur.execute("SELECT value FROM metadata WHERE key == 'ymin'")
+ ymin = float(res.fetchone()[0])
+ res = cur.execute("SELECT value FROM metadata WHERE key == 'ymax'")
+ ymax = float(res.fetchone()[0])
+ return AABB(xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax)
+
+ @property
+[docs] def num_frames(self) -> int:
+ cur = self.db.cursor()
+ res = cur.execute("SELECT MAX(frame) FROM trajectory_data")
+ return res.fetchone()[0]
+
+ @property
+[docs] def fps(self) -> float:
+ cur = self.db.cursor()
+ res = cur.execute("SELECT value from metadata WHERE key == 'fps'")
+ return float(res.fetchone()[0])
+
+ def _check_version_compatible(self) -> None:
+ cur = self.db.cursor()
+ res = cur.execute("SELECT value FROM metadata WHERE key == 'version'")
+ version_string = res.fetchone()[0]
+ try:
+ version_in_database = int(version_string)
+ if version_in_database != self.__supported_database_version:
+ raise Exception(
+ f"Incompatible database version. The database supplied is version {version_in_database}. "
+ f"This Program supports version {self.__supported_database_version}"
+ )
+ except ValueError:
+ raise Exception(
+ f"Database error, metadata version not an integer. Value found: {version_string}"
+ )
+
+""" Serialization/deserialization support
+
+In this file you will find interfaces and implementations to serialize and
+deserialize different forms of input / output commonly used.
+
+"""
+
+import abc
+import math
+from io import TextIOWrapper
+from pathlib import Path
+from typing import Optional, Tuple
+
+import shapely
+
+from jupedsim import (
+ GCFMModelAgentParameters,
+ Simulation,
+ VelocityModelAgentParameters,
+)
+
+
+[docs]class TrajectoryWriter(metaclass=abc.ABCMeta):
+ """Interface for trajectory serialization"""
+
+ @abc.abstractmethod
+[docs] def begin_writing(self) -> None:
+ """Begin writing trajectory data.
+
+ This method is intended to handle all data writing that has to be done
+ once before the trajectory data can be written. E.g. Meta information
+ such as framerate etc...
+ """
+ raise NotImplementedError
+
+ @abc.abstractmethod
+[docs] def write_iteration_state(self, simulation: Simulation) -> None:
+ """Write trajectory data of one simulation iteration.
+
+ This method is intended to handle serialization of the trajectory data
+ of a single iteration.
+ """
+ raise NotImplementedError
+
+ @abc.abstractmethod
+[docs] def end_writing(self) -> None:
+ """End writing trajectory data.
+
+ This method is intended to handle finalizing writing of trajectory
+ data, e.g. write closing tags, or footer meta data.
+ """
+ raise NotImplementedError
+
+ class Exception(Exception):
+ """Represents exceptions specific to the trajectory writer."""
+
+ pass
+
+
+[docs]class JpsCoreStyleTrajectoryWriter(TrajectoryWriter):
+ """Writes jpscore / jpsvis compatible trajectory files w.o. a referenced geometry.
+
+ This implementation tracks the number of calls to 'write_iteration_state'
+ and inserts the appropriate frame number, to write a useful file header the
+ fps the data is written in needs to be supplied on construction
+ """
+
+ def __init__(self, output_file: Path):
+ """JpsCoreStyleTrajectoryWriter constructor
+
+ Parameters
+ ----------
+ output_file : pathlib.Path
+ name of the output file.
+ Note: the file will not be written until the first call to 'begin_writing'
+
+ Returns
+ -------
+ JpsCoreStyleTrajectoryWriter
+ """
+ self._output_file = output_file
+ self._out: Optional[TextIOWrapper] = None
+ self._frame = 0
+
+[docs] def begin_writing(self, fps: float) -> None:
+ """Writes trajectory file header information
+
+ Parameters
+ ----------
+ fps: float
+ fps of the data to be written
+
+ Raises
+ ------
+ IOError
+ Opens the output file with pathlib.Path.open(). Any exception passed on.
+ """
+ self._out = self._output_file.open(mode="w", encoding="utf-8")
+ self._out.writelines(
+ [
+ "# written by JpsCoreStyleTrajectoryWriter\n",
+ f"#framerate: {fps}\n",
+ "#unit: m\n",
+ ]
+ )
+ self._out.flush()
+
+[docs] def write_iteration_state(self, simulation: Simulation) -> None:
+ """Writes trajectory information for a single iteration.
+
+ Parameters
+ ----------
+ simulation : jupedsim.Simulation
+ The simulation object to get the trajectory data from
+
+ Raises
+ ------
+ TrajectoryWriter.Exception
+ Will be raised if the output file is not yet opened, i.e.
+ 'being_writing' has not been called yet.
+
+ """
+ if not self._out:
+ raise TrajectoryWriter.Exception("Output file not opened")
+
+ def agent_orientaion_as_degrees(
+ agent: GCFMModelAgentParameters | VelocityModelAgentParameters,
+ ) -> float:
+ return JpsCoreStyleTrajectoryWriter._orientation_to_angle(
+ agent.orientation
+ )
+
+ self._out.writelines(
+ map(
+ lambda agent: f"{agent.id}\t{self._frame}\t{agent.position.x}\t{agent.position.y}\t{0}\t{0.3}\t{0.3}\t{agent_orientaion_as_degrees(agent)}\t{0.4}\n",
+ simulation.agents(),
+ ),
+ )
+ self._frame += 1
+
+[docs] def end_writing(self) -> None:
+ """End writing trajectory information
+
+ Will close the file handle and end writing.
+ """
+ if self._out:
+ self._out.close()
+
+ # TODO(kkratz): This should be externalized
+ @staticmethod
+ def _normalize(vec2: Tuple[float, float]) -> Tuple[float, float]:
+ len = math.sqrt(vec2[0] * vec2[0] + vec2[1] * vec2[1])
+ return (vec2[0] / len, vec2[1] / len)
+
+ # TODO(kkratz): This should be externalized
+ @staticmethod
+ def _orientation_to_angle(vec2: Tuple[float, float]) -> float:
+ vec2 = JpsCoreStyleTrajectoryWriter._normalize(vec2)
+ return math.degrees(math.atan2(vec2[1], vec2[0]))
+
+
+
+
+
+[docs]def parse_dlr_ad_hoc(input: str) -> shapely.GeometryCollection:
+ """
+ This function parses data from an ad-hoc file format as it was used by the DLR to
+ specify accessible areas, E.g:
+ Lane :J1_w0_0
+ 98.5
+ 5
+ 98.5
+ -5
+ 101.5
+ -1.5
+ 101.5
+ 1.5
+ Lane :J2_w0_0
+ 198.5
+ 1.5
+ 198.5
+ -1.5
+ 201.5
+ -5
+ 201.5
+ 5
+ Identifiers are followed by x and y coordinates, each specifying a polygon.
+
+ Parameters
+ ----------
+ input : str
+ text in the above mentioned format
+
+ Returns
+ -------
+ shapely.GeometryCollection that only contains polygons
+ """
+ polygons = []
+ laneName = None
+ laneCoordinates = []
+ for line in input.splitlines():
+ if line.startswith("Lane"):
+ if laneName:
+ polygons.append(laneCoordinates)
+ laneCoordinates = []
+ laneName = line[5:-1]
+ else:
+ laneCoordinates.append(float(line))
+ polygons.append(laneCoordinates)
+
+ def into_poly(values: list[float]) -> shapely.Polygon:
+ points = [(x, y) for x, y in zip(values[::2], values[1::2])]
+ return shapely.Polygon(points)
+
+ return shapely.GeometryCollection(geoms=[into_poly(p) for p in polygons])
+
+
+[docs]def parse_wkt(input: str) -> shapely.GeometryCollection:
+ """
+ Creates a Geometry collection from a WKT collection
+
+ Parameters
+ ----------
+ input : str
+ text containing one WKT GEOMETRYCOLLECTION
+
+ Raises
+ ------
+ ParseException will be raised on any errors parsing the input
+
+ Returns
+ -------
+ A shapely.GeometryCollection that only contains polygons
+ """
+
+ result = None
+ try:
+ result = shapely.from_wkt(input)
+ except Exception as e:
+ raise ParseException(f"Error parsing input: {e}")
+ if not isinstance(result, shapely.GeometryCollection):
+ raise ParseException(
+ f"Expected a WKT containing exactly one GeometryCollection"
+ )
+ return result
+
+import sqlite3
+from pathlib import Path
+from typing import Optional
+
+from jupedsim import Simulation
+from jupedsim.serialization import TrajectoryWriter
+
+
+[docs]class SqliteTrajectoryWriter(TrajectoryWriter):
+ """Write trajectory data into a sqlite db"""
+
+ def __init__(self, output_file: Path):
+ """SqliteTrajectoryWriter constructor
+
+ Parameters
+ ----------
+ output_file : pathlib.Path
+ name of the output file.
+ Note: the file will not be written until the first call to 'begin_writing'
+
+ Returns
+ -------
+ SqliteTrajectoryWriter
+ """
+ self._output_file = output_file
+ self._frame = 0
+ self._con = sqlite3.connect(self._output_file, isolation_level=None)
+
+[docs] def begin_writing(self, fps: float, geometry_as_wkt: str) -> None:
+ """Begin writing trajectory data.
+
+ This method is intended to handle all data writing that has to be done
+ once before the trajectory data can be written. E.g. Meta information
+ such as framerate etc...
+ """
+ cur = self._con.cursor()
+ try:
+ cur.execute("BEGIN")
+ cur.execute("DROP TABLE IF EXISTS trajectory_data")
+ cur.execute(
+ "CREATE TABLE trajectory_data ("
+ " frame INTEGER NOT NULL,"
+ " id INTEGER NOT NULL,"
+ " pos_x REAL NOT NULL,"
+ " pos_y REAL NOT NULL,"
+ " ori_x REAL NOT NULL,"
+ " ori_y REAL NOT NULL)"
+ )
+ cur.execute("DROP TABLE IF EXISTS metadata")
+ cur.execute(
+ "CREATE TABLE metadata(key TEXT NOT NULL UNIQUE, value TEXT NOT NULL)"
+ )
+ cur.executemany(
+ "INSERT INTO metadata VALUES(?, ?)",
+ (("version", "1"), ("fps", fps)),
+ )
+ cur.execute("DROP TABLE IF EXISTS geometry")
+ cur.execute("CREATE TABLE geometry(wkt TEXT NOT NULL)")
+ cur.execute("INSERT INTO geometry VALUES(?)", (geometry_as_wkt,))
+ cur.execute(
+ "CREATE INDEX frame_id_idx ON trajectory_data(frame, id)"
+ )
+ cur.execute("COMMIT")
+ except sqlite3.Error as e:
+ cur.execute("ROLLBACK")
+ raise TrajectoryWriter.Exception(f"Error creating database: {e}")
+
+[docs] def write_iteration_state(self, simulation: Simulation) -> None:
+ """Write trajectory data of one simulation iteration.
+
+ This method is intended to handle serialization of the trajectory data
+ of a single iteration.
+ """
+ if not self._con:
+ raise TrajectoryWriter.Exception("Database not opened.")
+
+ cur = self._con.cursor()
+ try:
+ cur.execute("BEGIN")
+ frame_data = [
+ (
+ self._frame,
+ agent.id,
+ agent.position.x,
+ agent.position.y,
+ agent.orientation.x,
+ agent.orientation.y,
+ )
+ for agent in simulation.agents()
+ ]
+ cur.executemany(
+ "INSERT INTO trajectory_data VALUES(?, ?, ?, ?, ?, ?)",
+ frame_data,
+ )
+ cur.execute("COMMIT")
+ except sqlite3.Error as e:
+ cur.execute("ROLLBACK")
+ raise TrajectoryWriter.Exception(f"Error writing to database: {e}")
+
+ self._frame += 1
+
+[docs] def end_writing(self) -> None:
+ """End writing trajectory data.
+
+ This method is intended to handle finalizing writing of trajectory
+ data, e.g. write closing tags, or footer meta data.
+ """
+ if not self._con:
+ raise TrajectoryWriter.Exception("Database not opened.")
+
+ cur = self._con.cursor()
+ try:
+ cur.execute("BEGIN")
+ res = cur.execute(
+ "SELECT MIN(pos_x), MAX(pos_x), MIN(pos_y), MAX(pos_y) FROM trajectory_data"
+ )
+ xmin, xmax, ymin, ymax = res.fetchone()
+ cur.execute(
+ "INSERT INTO metadata(key, value) VALUES(?,?)",
+ ("xmin", str(xmin)),
+ )
+ cur.execute(
+ "INSERT INTO metadata(key, value) VALUES(?,?)",
+ ("xmax", str(xmax)),
+ )
+ cur.execute(
+ "INSERT INTO metadata(key, value) VALUES(?,?)",
+ ("ymin", str(ymin)),
+ )
+ cur.execute(
+ "INSERT INTO metadata(key, value) VALUES(?,?)",
+ ("ymax", str(ymax)),
+ )
+ cur.execute("COMMIT")
+ except sqlite3.Error as e:
+ cur.execute("ROLLBACK")
+ raise TrajectoryWriter.Exception(f"Error writing to database: {e}")
+
+ self._con.close()
+
+
+
+import shapely
+
+from jupedsim import GeometryBuilder
+
+
+[docs]def build_jps_geometry(geo: shapely.GeometryCollection):
+ geo_builder = GeometryBuilder()
+
+ for obj in geo.geoms:
+ if obj.geom_type != "Polygon":
+ raise Exception(
+ "Unexpected geometry type found in GeometryCollection: {obj.type}"
+ )
+ geo_builder.add_accessible_area(obj.exterior.coords[:-1])
+ for hole in obj.interiors:
+ geo_builder.exclude_from_accssible_area(hole.coords[:-1])
+ return geo_builder.build()
+
Short
+ */ + .o-tooltip--left { + position: relative; + } + + .o-tooltip--left:after { + opacity: 0; + visibility: hidden; + position: absolute; + content: attr(data-tooltip); + padding: .2em; + font-size: .8em; + left: -.2em; + background: grey; + color: white; + white-space: nowrap; + z-index: 2; + border-radius: 2px; + transform: translateX(-102%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); +} + +.o-tooltip--left:hover:after { + display: block; + opacity: 1; + visibility: visible; + transform: translateX(-100%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); + transition-delay: .5s; +} + +/* By default the copy button shouldn't show up when printing a page */ +@media print { + button.copybtn { + display: none; + } +} diff --git a/v0.10.0/_static/copybutton.js b/v0.10.0/_static/copybutton.js new file mode 100644 index 0000000000..02c5c82d9d --- /dev/null +++ b/v0.10.0/_static/copybutton.js @@ -0,0 +1,248 @@ +// Localization support +const messages = { + 'en': { + 'copy': 'Copy', + 'copy_to_clipboard': 'Copy to clipboard', + 'copy_success': 'Copied!', + 'copy_failure': 'Failed to copy', + }, + 'es' : { + 'copy': 'Copiar', + 'copy_to_clipboard': 'Copiar al portapapeles', + 'copy_success': '¡Copiado!', + 'copy_failure': 'Error al copiar', + }, + 'de' : { + 'copy': 'Kopieren', + 'copy_to_clipboard': 'In die Zwischenablage kopieren', + 'copy_success': 'Kopiert!', + 'copy_failure': 'Fehler beim Kopieren', + }, + 'fr' : { + 'copy': 'Copier', + 'copy_to_clipboard': 'Copié dans le presse-papier', + 'copy_success': 'Copié !', + 'copy_failure': 'Échec de la copie', + }, + 'ru': { + 'copy': 'Скопировать', + 'copy_to_clipboard': 'Скопировать в буфер', + 'copy_success': 'Скопировано!', + 'copy_failure': 'Не удалось скопировать', + }, + 'zh-CN': { + 'copy': '复制', + 'copy_to_clipboard': '复制到剪贴板', + 'copy_success': '复制成功!', + 'copy_failure': '复制失败', + }, + 'it' : { + 'copy': 'Copiare', + 'copy_to_clipboard': 'Copiato negli appunti', + 'copy_success': 'Copiato!', + 'copy_failure': 'Errore durante la copia', + } +} + +let locale = 'en' +if( document.documentElement.lang !== undefined + && messages[document.documentElement.lang] !== undefined ) { + locale = document.documentElement.lang +} + +let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT; +if (doc_url_root == '#') { + doc_url_root = ''; +} + +/** + * SVG files for our copy buttons + */ +let iconCheck = `` + +// If the user specified their own SVG use that, otherwise use the default +let iconCopy = ``; +if (!iconCopy) { + iconCopy = `` +} + +/** + * Set up copy/paste for code blocks + */ + +const runWhenDOMLoaded = cb => { + if (document.readyState != 'loading') { + cb() + } else if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', cb) + } else { + document.attachEvent('onreadystatechange', function() { + if (document.readyState == 'complete') cb() + }) + } +} + +const codeCellId = index => `codecell${index}` + +// Clears selected text since ClipboardJS will select the text when copying +const clearSelection = () => { + if (window.getSelection) { + window.getSelection().removeAllRanges() + } else if (document.selection) { + document.selection.empty() + } +} + +// Changes tooltip text for a moment, then changes it back +// We want the timeout of our `success` class to be a bit shorter than the +// tooltip and icon change, so that we can hide the icon before changing back. +var timeoutIcon = 2000; +var timeoutSuccessClass = 1500; + +const temporarilyChangeTooltip = (el, oldText, newText) => { + el.setAttribute('data-tooltip', newText) + el.classList.add('success') + // Remove success a little bit sooner than we change the tooltip + // So that we can use CSS to hide the copybutton first + setTimeout(() => el.classList.remove('success'), timeoutSuccessClass) + setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon) +} + +// Changes the copy button icon for two seconds, then changes it back +const temporarilyChangeIcon = (el) => { + el.innerHTML = iconCheck; + setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon) +} + +const addCopyButtonToCodeCells = () => { + // If ClipboardJS hasn't loaded, wait a bit and try again. This + // happens because we load ClipboardJS asynchronously. + if (window.ClipboardJS === undefined) { + setTimeout(addCopyButtonToCodeCells, 250) + return + } + + // Add copybuttons to all of our code cells + const COPYBUTTON_SELECTOR = 'div.highlight pre'; + const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR) + codeCells.forEach((codeCell, index) => { + const id = codeCellId(index) + codeCell.setAttribute('id', id) + + const clipboardButton = id => + `` + codeCell.insertAdjacentHTML('afterend', clipboardButton(id)) + }) + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} + + +var copyTargetText = (trigger) => { + var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); + + // get filtered text + let exclude = '.linenos, .gp'; + + let text = filterText(target, exclude); + return formatCopyText(text, '', false, true, true, true, '', '') +} + + // Initialize with a callback so we can modify the text before copy + const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText}) + + // Update UI with error/success messages + clipboard.on('success', event => { + clearSelection() + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success']) + temporarilyChangeIcon(event.trigger) + }) + + clipboard.on('error', event => { + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure']) + }) +} + +runWhenDOMLoaded(addCopyButtonToCodeCells) \ No newline at end of file diff --git a/v0.10.0/_static/copybutton_funcs.js b/v0.10.0/_static/copybutton_funcs.js new file mode 100644 index 0000000000..dbe1aaad79 --- /dev/null +++ b/v0.10.0/_static/copybutton_funcs.js @@ -0,0 +1,73 @@ +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +export function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} diff --git a/v0.10.0/_static/css/custom.css b/v0.10.0/_static/css/custom.css new file mode 100644 index 0000000000..43f42311a3 --- /dev/null +++ b/v0.10.0/_static/css/custom.css @@ -0,0 +1,4 @@ +html[data-theme="light"] { + /*Set main color to match color of main logo color */ + --pst-color-primary: rgb(0 86 118); +} \ No newline at end of file diff --git a/v0.10.0/_static/doctools.js b/v0.10.0/_static/doctools.js new file mode 100644 index 0000000000..527b876ca6 --- /dev/null +++ b/v0.10.0/_static/doctools.js @@ -0,0 +1,156 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/v0.10.0/_static/documentation_options.js b/v0.10.0/_static/documentation_options.js new file mode 100644 index 0000000000..03c0d01dcb --- /dev/null +++ b/v0.10.0/_static/documentation_options.js @@ -0,0 +1,14 @@ +var DOCUMENTATION_OPTIONS = { + URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), + VERSION: '', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '', + NAVIGATION_WITH_KEYS: true, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/v0.10.0/_static/file.png b/v0.10.0/_static/file.png new file mode 100644 index 0000000000..a858a410e4 Binary files /dev/null and b/v0.10.0/_static/file.png differ diff --git a/v0.10.0/_static/graphviz.css b/v0.10.0/_static/graphviz.css new file mode 100644 index 0000000000..19e7afd385 --- /dev/null +++ b/v0.10.0/_static/graphviz.css @@ -0,0 +1,19 @@ +/* + * graphviz.css + * ~~~~~~~~~~~~ + * + * Sphinx stylesheet -- graphviz extension. + * + * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +img.graphviz { + border: 0; + max-width: 100%; +} + +object.graphviz { + max-width: 100%; +} diff --git a/v0.10.0/_static/images/logo_binder.svg b/v0.10.0/_static/images/logo_binder.svg new file mode 100644 index 0000000000..45fecf7511 --- /dev/null +++ b/v0.10.0/_static/images/logo_binder.svg @@ -0,0 +1,19 @@ + + + diff --git a/v0.10.0/_static/images/logo_colab.png b/v0.10.0/_static/images/logo_colab.png new file mode 100644 index 0000000000..b7560ec216 Binary files /dev/null and b/v0.10.0/_static/images/logo_colab.png differ diff --git a/v0.10.0/_static/images/logo_deepnote.svg b/v0.10.0/_static/images/logo_deepnote.svg new file mode 100644 index 0000000000..fa77ebfc25 --- /dev/null +++ b/v0.10.0/_static/images/logo_deepnote.svg @@ -0,0 +1 @@ + diff --git a/v0.10.0/_static/images/logo_jupyterhub.svg b/v0.10.0/_static/images/logo_jupyterhub.svg new file mode 100644 index 0000000000..60cfe9f222 --- /dev/null +++ b/v0.10.0/_static/images/logo_jupyterhub.svg @@ -0,0 +1 @@ + diff --git a/v0.10.0/_static/jquery-3.6.0.js b/v0.10.0/_static/jquery-3.6.0.js new file mode 100644 index 0000000000..fc6c299b73 --- /dev/null +++ b/v0.10.0/_static/jquery-3.6.0.js @@ -0,0 +1,10881 @@ +/*! + * jQuery JavaScript Library v3.6.0 + * https://jquery.com/ + * + * Includes Sizzle.js + * https://sizzlejs.com/ + * + * Copyright OpenJS Foundation and other contributors + * Released under the MIT license + * https://jquery.org/license + * + * Date: 2021-03-02T17:08Z + */ +( function( global, factory ) { + + "use strict"; + + if ( typeof module === "object" && typeof module.exports === "object" ) { + + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 +// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode +// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common +// enough that all such attempts are guarded in a try block. +"use strict"; + +var arr = []; + +var getProto = Object.getPrototypeOf; + +var slice = arr.slice; + +var flat = arr.flat ? function( array ) { + return arr.flat.call( array ); +} : function( array ) { + return arr.concat.apply( [], array ); +}; + + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var fnToString = hasOwn.toString; + +var ObjectFunctionString = fnToString.call( Object ); + +var support = {}; + +var isFunction = function isFunction( obj ) { + + // Support: Chrome <=57, Firefox <=52 + // In some browsers, typeof returns "function" for HTML