diff --git a/v0.13.0/.buildinfo b/v0.13.0/.buildinfo new file mode 100644 index 0000000000..cf0e0b4bbb --- /dev/null +++ b/v0.13.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: c9155017d04c6a77d7c6476628b8d290 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/v0.13.0/.doctrees/api/aabb/index.doctree b/v0.13.0/.doctrees/api/aabb/index.doctree new file mode 100644 index 0000000000..f59b241f79 Binary files /dev/null and b/v0.13.0/.doctrees/api/aabb/index.doctree differ diff --git a/v0.13.0/.doctrees/api/agent/index.doctree b/v0.13.0/.doctrees/api/agent/index.doctree new file mode 100644 index 0000000000..c2885aa09f Binary files /dev/null and b/v0.13.0/.doctrees/api/agent/index.doctree differ diff --git a/v0.13.0/.doctrees/api/geometry/index.doctree b/v0.13.0/.doctrees/api/geometry/index.doctree new file mode 100644 index 0000000000..fe5deda4dc Binary files /dev/null and b/v0.13.0/.doctrees/api/geometry/index.doctree differ diff --git a/v0.13.0/.doctrees/api/grid/index.doctree b/v0.13.0/.doctrees/api/grid/index.doctree new file mode 100644 index 0000000000..f325f8132c Binary files /dev/null and b/v0.13.0/.doctrees/api/grid/index.doctree differ diff --git a/v0.13.0/.doctrees/api/index.doctree b/v0.13.0/.doctrees/api/index.doctree new file mode 100644 index 0000000000..6d3b55cae4 Binary files /dev/null and b/v0.13.0/.doctrees/api/index.doctree differ diff --git a/v0.13.0/.doctrees/api/journey/index.doctree b/v0.13.0/.doctrees/api/journey/index.doctree new file mode 100644 index 0000000000..f441b9195f Binary files /dev/null and b/v0.13.0/.doctrees/api/journey/index.doctree differ diff --git a/v0.13.0/.doctrees/api/jupedsim/distributions/index.doctree b/v0.13.0/.doctrees/api/jupedsim/distributions/index.doctree new file mode 100644 index 0000000000..ff29e46007 Binary files /dev/null and b/v0.13.0/.doctrees/api/jupedsim/distributions/index.doctree differ diff --git a/v0.13.0/.doctrees/api/jupedsim/index.doctree b/v0.13.0/.doctrees/api/jupedsim/index.doctree new file mode 100644 index 0000000000..cf3c8ae403 Binary files /dev/null and b/v0.13.0/.doctrees/api/jupedsim/index.doctree differ diff --git a/v0.13.0/.doctrees/api/jupedsim/recording/index.doctree b/v0.13.0/.doctrees/api/jupedsim/recording/index.doctree new file mode 100644 index 0000000000..c323374b37 Binary files /dev/null and b/v0.13.0/.doctrees/api/jupedsim/recording/index.doctree differ diff --git a/v0.13.0/.doctrees/api/jupedsim/serialization/index.doctree b/v0.13.0/.doctrees/api/jupedsim/serialization/index.doctree new file mode 100644 index 0000000000..9f2a53df1e Binary files /dev/null and b/v0.13.0/.doctrees/api/jupedsim/serialization/index.doctree differ diff --git a/v0.13.0/.doctrees/api/jupedsim/sqlite_serialization/index.doctree b/v0.13.0/.doctrees/api/jupedsim/sqlite_serialization/index.doctree new file mode 100644 index 0000000000..b12de410c5 Binary files /dev/null and b/v0.13.0/.doctrees/api/jupedsim/sqlite_serialization/index.doctree differ diff --git a/v0.13.0/.doctrees/api/jupedsim/util/index.doctree b/v0.13.0/.doctrees/api/jupedsim/util/index.doctree new file mode 100644 index 0000000000..7f4ba96058 Binary files /dev/null and b/v0.13.0/.doctrees/api/jupedsim/util/index.doctree differ diff --git a/v0.13.0/.doctrees/api/library/index.doctree b/v0.13.0/.doctrees/api/library/index.doctree new file mode 100644 index 0000000000..a2e414896f Binary files /dev/null and b/v0.13.0/.doctrees/api/library/index.doctree differ diff --git a/v0.13.0/.doctrees/api/models/index.doctree b/v0.13.0/.doctrees/api/models/index.doctree new file mode 100644 index 0000000000..7d31195ddb Binary files /dev/null and b/v0.13.0/.doctrees/api/models/index.doctree differ diff --git a/v0.13.0/.doctrees/api/routing/index.doctree b/v0.13.0/.doctrees/api/routing/index.doctree new file mode 100644 index 0000000000..b1578faef7 Binary files /dev/null and b/v0.13.0/.doctrees/api/routing/index.doctree differ diff --git a/v0.13.0/.doctrees/api/simulation/index.doctree b/v0.13.0/.doctrees/api/simulation/index.doctree new file mode 100644 index 0000000000..5ec6b068b6 Binary files /dev/null and b/v0.13.0/.doctrees/api/simulation/index.doctree differ diff --git a/v0.13.0/.doctrees/api/stages/index.doctree b/v0.13.0/.doctrees/api/stages/index.doctree new file mode 100644 index 0000000000..dc3a4690d6 Binary files /dev/null and b/v0.13.0/.doctrees/api/stages/index.doctree differ diff --git a/v0.13.0/.doctrees/api/tracing/index.doctree b/v0.13.0/.doctrees/api/tracing/index.doctree new file mode 100644 index 0000000000..024f98df1c Binary files /dev/null and b/v0.13.0/.doctrees/api/tracing/index.doctree differ diff --git a/v0.13.0/.doctrees/disclaimer.doctree b/v0.13.0/.doctrees/disclaimer.doctree new file mode 100644 index 0000000000..12fd4b96ee Binary files /dev/null and b/v0.13.0/.doctrees/disclaimer.doctree differ diff --git a/v0.13.0/.doctrees/environment.pickle b/v0.13.0/.doctrees/environment.pickle new file mode 100644 index 0000000000..b31c6c1081 Binary files /dev/null and b/v0.13.0/.doctrees/environment.pickle differ diff --git a/v0.13.0/.doctrees/index.doctree b/v0.13.0/.doctrees/index.doctree new file mode 100644 index 0000000000..32547dadc9 Binary files /dev/null and b/v0.13.0/.doctrees/index.doctree differ diff --git a/v0.13.0/.doctrees/notebooks/index.doctree b/v0.13.0/.doctrees/notebooks/index.doctree new file mode 100644 index 0000000000..01c6a087d6 Binary files /dev/null and b/v0.13.0/.doctrees/notebooks/index.doctree differ diff --git a/v0.13.0/.doctrees/notebooks/test.doctree b/v0.13.0/.doctrees/notebooks/test.doctree new file mode 100644 index 0000000000..87f804b086 Binary files /dev/null and b/v0.13.0/.doctrees/notebooks/test.doctree differ diff --git a/v0.13.0/404.html b/v0.13.0/404.html new file mode 100644 index 0000000000..003c132349 --- /dev/null +++ b/v0.13.0/404.html @@ -0,0 +1,372 @@ + + + + + + + +
+ + +
+# Copyright © 2012-2023 Forschungszentrum Jülich GmbH
+# SPDX-License-Identifier: LGPL-3.0-or-later
+[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)
+
+# Copyright © 2012-2023 Forschungszentrum Jülich GmbH
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+try:
+ import py_jupedsim as py_jps
+except ImportError:
+ from .. import py_jupedsim as py_jps
+
+
+from jupedsim.native.models import (
+ GeneralizedCentrifugalForceModelState,
+ VelocityModelState,
+)
+
+
+[docs]class Agent:
+ def __init__(self, backing):
+ self._obj = backing
+
+ @property
+
+
+ @property
+
+
+ @journey_id.setter
+ def journey_id(self, id):
+ self._obj.journey_id = id
+
+ @property
+
+
+ @stage_id.setter
+ def stage_id(self, id):
+ self._obj.stage_id = id
+
+ @property
+
+
+ @position.setter
+ def position(self, position: tuple[float, float]):
+ self._obj.position = position
+
+ @property
+
+
+ @orientation.setter
+ def orientation(self, orientation: tuple[float, float]):
+ self._obj.orientation = orientation
+
+ @property
+[docs] def model(self):
+ model = self._obj.model
+ if isinstance(model, py_jps.GeneralizedCentrifugalForceModelState):
+ return GeneralizedCentrifugalForceModelState(model)
+ elif isinstance(model, py_jps.VelocityModelState):
+ return VelocityModelState(model)
+ else:
+ raise Exception("Internal error")
+
+# Copyright © 2012-2023 Forschungszentrum Jülich GmbH
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+try:
+ import py_jupedsim as py_jps
+except ImportError:
+ from .. import py_jupedsim as py_jps
+
+
+[docs]class Geometry:
+ """Geometry object for simulations."""
+
+ def __init__(self, obj: py_jps.Geometry):
+ self._obj = obj
+
+
+
+
+
+
+[docs]class GeometryBuilder:
+ def __init__(self):
+ self._obj = py_jps.GeometryBuilder()
+
+[docs] def add_accessible_area(self, polygon: list[tuple[float, float]]) -> None:
+ """Adds an area which can be accessed by the agents to the geometry.
+
+ Args:
+ polygon (list[tuple[float, float]]): list of x,y coordinates of
+ the points of a polygon
+ """
+ self._obj.add_accessible_area(polygon)
+
+[docs] def exclude_from_accessible_area(
+ self, polygon: list[tuple[float, float]]
+ ) -> None:
+ """Marks an area as un-accessible by the agents to the geometry.
+
+ Args:
+ polygon (list[tuple[float, float]]): list of x,y coordinates of
+ the points of a polygon
+ """
+
+ self._obj.exclude_from_accessible_area(polygon)
+
+[docs] def build(self) -> Geometry:
+ """Builds a Geometry from the given accessible and un-accessible areas.
+
+ Returns:
+ Geometry object
+ """
+ return Geometry(self._obj.build())
+
+# Copyright © 2012-2023 Forschungszentrum Jülich GmbH
+# SPDX-License-Identifier: LGPL-3.0-or-later
+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
+
+# Copyright © 2012-2023 Forschungszentrum Jülich GmbH
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+from typing import Optional
+
+try:
+ import py_jupedsim as py_jps
+except ImportError:
+ from .. import py_jupedsim as py_jps
+
+
+[docs]class Transition:
+ def __init__(self, backing) -> None:
+ self._obj = backing
+
+ @staticmethod
+[docs] def create_fixed_transition(stage_id):
+ return Transition(py_jps.Transition.create_fixed_transition(stage_id))
+
+ @staticmethod
+[docs] def create_round_robin_transition(stage_weights: list[tuple[int, int]]):
+ return Transition(
+ py_jps.Transition.create_round_robin_transition(stage_weights)
+ )
+
+ @staticmethod
+[docs] def create_least_targeted_transition(stage_ids: list[int]):
+ return Transition(
+ py_jps.Transition.create_least_targeted_transition(stage_ids)
+ )
+
+
+[docs]class JourneyDescription:
+ def __init__(self, stage_ids: Optional[list[int]] = None):
+ if stage_ids is None:
+ self._obj = py_jps.JourneyDescription()
+ else:
+ self._obj = py_jps.JourneyDescription(stage_ids)
+
+
+
+[docs] def set_transition_for_stage(self, stage_id: int, transition: Transition):
+ self._obj.set_transition_for_stage(stage_id, transition._obj)
+
+# Copyright © 2012-2023 Forschungszentrum Jülich GmbH
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+from jupedsim.distributions import (
+ AgentNumberError,
+ IncorrectParameterError,
+ NegativeValueError,
+ OverlappingCirclesError,
+ distribute_by_density,
+ distribute_by_number,
+ distribute_by_percentage,
+ distribute_in_circles_by_density,
+ distribute_in_circles_by_number,
+ distribute_till_full,
+)
+from jupedsim.native.agent import Agent
+from jupedsim.native.geometry import Geometry, GeometryBuilder
+from jupedsim.native.journey import JourneyDescription, Transition
+from jupedsim.native.library import (
+ BuildInfo,
+ get_build_info,
+ set_debug_callback,
+ set_error_callback,
+ set_info_callback,
+ set_warning_callback,
+)
+from jupedsim.native.models import (
+ GeneralizedCentrifugalForceModelAgentParameters,
+ GeneralizedCentrifugalForceModelParameters,
+ GeneralizedCentrifugalForceModelState,
+ VelocityModelAgentParameters,
+ VelocityModelParameters,
+ VelocityModelState,
+)
+from jupedsim.native.routing import RoutingEngine
+from jupedsim.native.simulation import Simulation
+from jupedsim.native.stages import (
+ ExitProxy,
+ NotifiableQueueProxy,
+ WaitingSetProxy,
+ WaitingSetState,
+ WaypointProxy,
+)
+from jupedsim.native.tracing import Trace
+from jupedsim.recording import Recording, RecordingAgent, RecordingFrame
+from jupedsim.serialization import TrajectoryWriter
+from jupedsim.sqlite_serialization import SqliteTrajectoryWriter
+from jupedsim.util import (
+ GeometryError,
+ geometry_from_coordinates,
+ geometry_from_shapely,
+ geometry_from_wkt,
+)
+
+__version__ = get_build_info().library_version
+__commit__ = get_build_info().git_commit_hash
+__compiler__ = (
+ f"{get_build_info().compiler} ({get_build_info().compiler_version})"
+)
+
+__all__ = [
+ "Agent",
+ "AgentNumberError",
+ "BuildInfo",
+ "ExitProxy",
+ "GeneralizedCentrifugalForceModelAgentParameters",
+ "GeneralizedCentrifugalForceModelParameters",
+ "GeneralizedCentrifugalForceModelState",
+ "Geometry",
+ "GeometryBuilder",
+ "GeometryError",
+ "IncorrectParameterError",
+ "JourneyDescription",
+ "NegativeValueError",
+ "NotifiableQueueProxy",
+ "OverlappingCirclesError",
+ "Recording",
+ "RecordingAgent",
+ "RecordingFrame",
+ "RoutingEngine",
+ "Simulation",
+ "SqliteTrajectoryWriter",
+ "Trace",
+ "TrajectoryWriter",
+ "Transition",
+ "VelocityModelAgentParameters",
+ "VelocityModelParameters",
+ "VelocityModelState",
+ "WaitingSetProxy",
+ "WaitingSetState",
+ "WaypointProxy",
+ "__commit__",
+ "__compiler__",
+ "__version__",
+ "distribute_by_density",
+ "distribute_by_number",
+ "distribute_by_percentage",
+ "distribute_in_circles_by_density",
+ "distribute_in_circles_by_number",
+ "distribute_till_full",
+ "geometry_from_coordinates",
+ "geometry_from_shapely",
+ "geometry_from_wkt",
+ "get_build_info",
+ "set_debug_callback",
+ "set_error_callback",
+ "set_info_callback",
+ "set_warning_callback",
+]
+
+# Copyright © 2012-2023 Forschungszentrum Jülich GmbH
+# SPDX-License-Identifier: LGPL-3.0-or-later
+import numpy as np
+import shapely.geometry as shply
+
+from jupedsim.internal.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:]]
+
+# Copyright © 2012-2023 Forschungszentrum Jülich GmbH
+# SPDX-License-Identifier: LGPL-3.0-or-later
+import sqlite3
+from dataclasses import dataclass
+
+import shapely
+
+from jupedsim.internal.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}"
+ )
+
+# Copyright © 2012-2023 Forschungszentrum Jülich GmbH
+# SPDX-License-Identifier: LGPL-3.0-or-later
+""" 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
+
+
+[docs]class TrajectoryWriter(metaclass=abc.ABCMeta):
+ """Interface for trajectory serialization"""
+
+ @abc.abstractmethod
+[docs] def begin_writing(self, simulation) -> 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) -> 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, simulation) -> 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
+
+ @abc.abstractmethod
+[docs] def every_nth_frame(self) -> int:
+ """Returns the intervall of this writer in frames between writes.
+
+ 1 indicates all frames are writen, 10 indicates every 10th frame is
+ writen and so on.
+
+ Returns:
+ Number of frames beween writes as int
+ """
+
+ class Exception(Exception):
+ """Represents exceptions specific to the trajectory writer."""
+
+ pass
+
+# Copyright © 2012-2023 Forschungszentrum Jülich GmbH
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+import sqlite3
+from pathlib import Path
+
+import shapely
+
+from jupedsim.native.simulation import Simulation
+from jupedsim.serialization import TrajectoryWriter
+
+
+[docs]class SqliteTrajectoryWriter(TrajectoryWriter):
+ """Write trajectory data into a sqlite db"""
+
+ def __init__(self, *, output_file: Path, every_nth_frame: int = 4):
+ """SqliteTrajectoryWriter constructor
+
+ Args:
+ output_file : pathlib.Path
+ name of the output file.
+ Note: the file will not be written until the first call to 'begin_writing'
+ every_nth_frame: int
+ indicates interval between writes, 1 means every frame, 5 every 5th
+
+ Returns:
+ SqliteTrajectoryWriter
+ """
+ self._output_file = output_file
+ if every_nth_frame < 1:
+ raise TrajectoryWriter.Exception("'every_nth_frame' has to be > 0")
+ self._every_nth_frame = every_nth_frame
+ self._con = sqlite3.connect(self._output_file, isolation_level=None)
+
+[docs] def begin_writing(self, simulation: Simulation) -> 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...
+ """
+ fps = 1 / simulation.delta_time() / self._every_nth_frame
+ geometry = simulation.get_geometry()
+ geo = shapely.to_wkt(
+ shapely.Polygon(geometry.boundary(), holes=geometry.holes()),
+ rounding_precision=-1,
+ )
+
+ 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 PRIMARY KEY, 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(?)", (geo,))
+ 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.")
+
+ iteration = simulation.iteration_count()
+ if iteration % self.every_nth_frame() != 0:
+ return
+ frame = iteration / self.every_nth_frame()
+ cur = self._con.cursor()
+ try:
+ cur.execute("BEGIN")
+ frame_data = [
+ (
+ frame,
+ agent.id,
+ agent.position[0],
+ agent.position[1],
+ agent.orientation[0],
+ agent.orientation[1],
+ )
+ for agent in simulation.agents()
+ ]
+ cur.executemany(
+ "INSERT INTO trajectory_data VALUES(?, ?, ?, ?, ?, ?)",
+ frame_data,
+ )
+ 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()
+
+ old_xmin = self._x_min(cur)
+ old_xmax = self._x_max(cur)
+ old_ymin = self._y_min(cur)
+ old_ymax = self._y_max(cur)
+
+ cur.executemany(
+ "INSERT OR REPLACE INTO metadata(key, value) VALUES(?,?)",
+ [
+ ("xmin", str(min(xmin, float(old_xmin)))),
+ ("xmax", str(max(xmax, float(old_xmax)))),
+ ("ymin", str(min(ymin, float(old_ymin)))),
+ ("ymax", str(max(ymax, float(old_ymax)))),
+ ],
+ )
+ cur.execute("COMMIT")
+ except sqlite3.Error as e:
+ cur.execute("ROLLBACK")
+ raise TrajectoryWriter.Exception(f"Error writing to database: {e}")
+
+
+
+
+
+
+
+ def _value_or_default(self, cur, key, default: float | int | str):
+ res = cur.execute(
+ "SELECT value FROM metadata WHERE key = ?", (key,)
+ ).fetchone()
+ if res is None:
+ return default
+ else:
+ text = res[0]
+ return type(default)(text)
+
+ def _x_min(self, cur):
+ return self._value_or_default(cur, "xmin", float("inf"))
+
+ def _x_max(self, cur):
+ return self._value_or_default(cur, "xmax", float("-inf"))
+
+ def _y_min(self, cur):
+ return self._value_or_default(cur, "ymin", float("inf"))
+
+ def _y_max(self, cur):
+ return self._value_or_default(cur, "ymax", float("-inf"))
+
+# Copyright © 2012-2023 Forschungszentrum Jülich GmbH
+# SPDX-License-Identifier: LGPL-3.0-or-later
+from typing import Any, List, Optional, Tuple
+
+import shapely
+
+from jupedsim.native.geometry import Geometry, GeometryBuilder
+
+
+[docs]class GeometryError(Exception):
+ """Class reflecting errors when creating JuPedSim geometry objects."""
+
+ def __init__(self, message):
+ """Create GeometryError with the given message.
+
+ Args:
+ message: Error message
+ """
+ self.message = message
+
+
+[docs]def geometry_from_wkt(wkt_input: str) -> Geometry:
+ geometry_collection = None
+ try:
+ wkt_type = shapely.from_wkt(wkt_input)
+ except Exception as exc:
+ raise GeometryError(
+ f"Could not create geometry objects from the given WKT: "
+ f"{wkt_input}. See following error message:\n{exc}"
+ ) from exc
+
+ if isinstance(wkt_type, shapely.GeometryCollection):
+ geometry_collection = wkt_type
+ else:
+ try:
+ geometry_collection = shapely.GeometryCollection([wkt_type])
+ except Exception as exc:
+ raise GeometryError(
+ f"Could not create a geometry collection from the given WKT: "
+ f"{wkt_input}. See following error message:\n{exc}"
+ ) from exc
+
+ polygons = _polygons_from_geometry_collection(geometry_collection)
+ return _build_geometry(polygons)
+
+
+[docs]def geometry_from_shapely(
+ geometry_input: shapely.Polygon
+ | shapely.MultiPolygon
+ | shapely.GeometryCollection
+ | shapely.MultiPoint,
+) -> Geometry:
+ polygons = _polygons_from_geometry_collection(
+ shapely.GeometryCollection([geometry_input])
+ )
+ return _build_geometry(polygons)
+
+
+[docs]def geometry_from_coordinates(
+ coordinates: List[Tuple], *, excluded_areas: Optional[List[Tuple]] = None
+) -> Geometry:
+ polygon = shapely.Polygon(coordinates, holes=excluded_areas)
+ return _build_geometry([polygon])
+
+
+def _polygons_from_geometry_collection(
+ geometry_collection: shapely.GeometryCollection,
+) -> List[shapely.Polygon]:
+ def _polygons_from_multi_polygon(
+ multi_polygon: shapely.MultiPolygon,
+ ) -> List[shapely.Polygon]:
+ result = []
+ for polygon in multi_polygon.geoms:
+ result += _polygons_from_polygon(polygon)
+ return result
+
+ def _polygons_from_linear_ring(
+ linear_ring: shapely.LinearRing,
+ ) -> List[shapely.Polygon]:
+ return _polygons_from_polygon(shapely.Polygon(linear_ring))
+
+ def _polygons_from_polygon(
+ polygon: shapely.Polygon,
+ ) -> List[shapely.Polygon]:
+ return [polygon]
+
+ polygons = []
+ for geo in geometry_collection.geoms:
+ if shapely.get_type_id(geo) == shapely.GeometryType.GEOMETRYCOLLECTION:
+ polygons += _polygons_from_geometry_collection(geo)
+ elif shapely.get_type_id(geo) == shapely.GeometryType.MULTIPOLYGON:
+ polygons += _polygons_from_multi_polygon(geo)
+ elif shapely.get_type_id(geo) == shapely.GeometryType.LINEARRING:
+ polygons += _polygons_from_linear_ring(geo)
+ elif shapely.get_type_id(geo) == shapely.GeometryType.POLYGON:
+ polygons += _polygons_from_polygon(geo)
+ else:
+ raise GeometryError(
+ f"Unexpected geometry type found in GeometryCollection: "
+ f"{geo.geom_type}. Only Polygon types are allowed."
+ )
+
+ return polygons
+
+
+def _build_geometry(polygons: List[shapely.Polygon]) -> Geometry:
+ geo_builder = GeometryBuilder()
+
+ for polygon in polygons:
+ geo_builder.add_accessible_area(polygon.exterior.coords[:-1])
+ for hole in polygon.interiors:
+ geo_builder.exclude_from_accessible_area(hole.coords[:-1])
+ return geo_builder.build()
+
+# Copyright © 2012-2023 Forschungszentrum Jülich GmbH
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+try:
+ import py_jupedsim as py_jps
+except ImportError:
+ from .. import py_jupedsim as py_jps
+
+from textwrap import dedent
+
+
+# TODO(kkratz): add typehints for function params
+[docs]def set_debug_callback(fn) -> None:
+ """
+ Set reciever for debug messages.
+
+ Parameters
+ ----------
+ fn: fn<str>
+ function that accepts a msg as string
+ """
+ py_jps.set_debug_callback(fn)
+
+
+[docs]def set_info_callback(fn) -> None:
+ """
+ Set reciever for info messages.
+
+ Parameters
+ ----------
+ fn: fn<str>
+ function that accepts a msg as string
+ """
+ py_jps.set_info_callback(fn)
+
+
+[docs]def set_warning_callback(fn) -> None:
+ """
+ Set reciever for warning messages.
+
+ Parameters
+ ----------
+ fn: fn<str>
+ function that accepts a msg as string
+ """
+ py_jps.set_warning_callback(fn)
+
+
+[docs]def set_error_callback(fn) -> None:
+ """
+ Set reciever for error messages.
+
+ Parameters
+ ----------
+ fn: fn<str>
+ function that accepts a msg as string
+ """
+ py_jps.set_error_callback(fn)
+
+
+# TODO(kkratz): refactor this into free functions in C-API / bindings
+[docs]class BuildInfo:
+ def __init__(self):
+ self.__obj = py_jps.get_build_info()
+
+ @property
+
+
+ @property
+
+
+ @property
+
+
+ @property
+
+
+ @property
+
+
+ @property
+
+
+ def __repr__(self):
+ return dedent(
+ f"""\
+ JuPedSim {self.library_version}:
+ --------------------------------
+ Commit: {self.git_commit_hash} from {self.git_branch} on {self.git_commit_date}
+ Compiler: {self.compiler} ({self.compiler_version})"""
+ )
+
+
+
+
+# Copyright © 2012-2023 Forschungszentrum Jülich GmbH
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+from dataclasses import dataclass
+
+try:
+ import py_jupedsim as py_jps
+except ImportError:
+ from .. import py_jupedsim as py_jps
+
+
+@dataclass(kw_only=True)
+[docs]class VelocityModelParameters:
+ """Parameters for Velocity Model
+
+ All attributes are initialized with reasonably good defaults.
+
+ Attributes:
+ a_ped (float): TODO
+ d_ped (float): TODO
+ a_wall (float):
+ d_wall (float):
+ """
+
+
+
+
+
+
+
+@dataclass(kw_only=True)
+[docs]class GeneralizedCentrifugalForceModelParameters:
+ """Parameters for Generalized Centrifugal Force Model
+
+ All attributes are initialized with reasonably good defaults.
+
+ Attributes:
+ nu_ped (float):
+ nu_wall (float):
+ dist_eff_ped (float)
+ dist_eff_wall (float)
+ intp_width_ped (float)
+ intp_width_wall (float)
+ maxf_ped (float)
+ maxf_wall (float)
+ """
+
+
+
+
+
+
+
+
+
+
+
+[docs]class GeneralizedCentrifugalForceModelAgentParameters:
+ """
+ Agent parameters for Generalized Centrifugal Force Model.
+
+ See the scientifc publication for more details about this model
+ https://arxiv.org/abs/1008.4297
+
+ Objects of this type can be used to add new agents to the simulation and are
+ returned by the simulation when inspecting agent state. Setting properties on
+ objects returned by the simulation has no effect on the agents as this object
+ is a copy of internal state.
+
+ Setting properties on this object is only useful when adding multiple agents
+ and they share many properties without reprating them on each 'add_agent'
+ call
+ """
+
+ def __init__(self):
+ self._obj = py_jps.GCFMModelAgentParameters()
+
+ @property
+[docs] def speed(self) -> float:
+ """
+ Current speed
+
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.speed
+
+ @speed.setter
+ def speed(self, value: float) -> None:
+ self._obj.speed = value
+
+ @property
+[docs] def e0(self) -> tuple[float, float]:
+ """
+ e0 (Currently desired orientation)
+
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.e0
+
+ @e0.setter
+ def e0(self, value: tuple[float, float]) -> None:
+ self._obj.e0 = value
+
+ @property
+[docs] def position(self) -> tuple[float, float]:
+ """
+ Current position
+
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.position
+
+ @position.setter
+ def position(self, value: tuple[float, float]) -> None:
+ self._obj.position = value
+
+ @property
+[docs] def orientation(self) -> tuple[float, float]:
+ """
+ Current orientation
+
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.orientation
+
+ @orientation.setter
+ def orientation(self, value: tuple[float, float]) -> None:
+ self._obj.orientation = value
+
+ @property
+[docs] def journey_id(self) -> int:
+ """
+ Id of curently followed journey
+
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.journey_id
+
+ @journey_id.setter
+ def journey_id(self, value: int) -> None:
+ self._obj.journey_id = value
+
+ @property
+[docs] def stage_id(self) -> int:
+ """
+ Id of curently followed stage
+
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.stage_id
+
+ @stage_id.setter
+ def stage_id(self, value: int) -> None:
+ self._obj.stage_id = value
+
+ @property
+[docs] def mass(self) -> float:
+ """
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.mass
+
+ @mass.setter
+ def mass(self, value: float) -> None:
+ self._obj.mass = value
+
+ @property
+[docs] def tau(self) -> float:
+ """
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.tau
+
+ @tau.setter
+ def tau(self, value: float) -> None:
+ self._obj.tau = value
+
+ @property
+[docs] def v0(self) -> float:
+ """
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.v0
+
+ @v0.setter
+ def v0(self, value: float) -> None:
+ self._obj.v0 = value
+
+ @property
+[docs] def a_v(self) -> float:
+ """
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.a_v
+
+ @a_v.setter
+ def a_v(self, value: float) -> None:
+ self._obj.a_v = value
+
+ @property
+[docs] def a_min(self) -> float:
+ """
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.a_min
+
+ @a_min.setter
+ def a_min(self, value: float) -> None:
+ self._obj.a_min = value
+
+ @property
+[docs] def b_min(self) -> float:
+ """
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.b_min
+
+ @b_min.setter
+ def b_min(self, value: float) -> None:
+ self._obj.b_min = value
+
+ @property
+[docs] def b_max(self) -> float:
+ """
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.b_max
+
+ @b_max.setter
+ def b_max(self, value: float) -> None:
+ self._obj.b_max = value
+
+ @property
+[docs] def id(self) -> int:
+ """
+ Id of this Agent
+
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.id
+
+ @id.setter
+ def id(self, value: int) -> None:
+ self._obj.id = value
+
+ def __str__(self) -> str:
+ return self._obj.__repr__()
+
+
+[docs]class VelocityModelAgentParameters:
+ """
+ Agent parameters for Velocity Model.
+
+ See the scientifc publication for more details about this model
+ https://arxiv.org/abs/1512.05597
+
+ Objects of this type can be used to add new agents to the simulation and are
+ returned by the simulation when inspecting agent state. Setting properties on
+ objects returned by the simulation has no effect on the agents as this object
+ is a copy of internal state.
+
+ Setting properties on this object is only useful when adding multiple agents
+ and they share many properties without reprating them on each 'add_agent'
+ call
+ """
+
+ def __init__(self):
+ self._obj = py_jps.VelocityModelAgentParameters()
+
+ @property
+[docs] def position(self) -> tuple[float, float]:
+ """
+ Current position
+
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.position
+
+ @position.setter
+ def position(self, value: tuple[float, float]) -> None:
+ self._obj.position = value
+
+ @property
+[docs] def orientation(self) -> tuple[float, float]:
+ """
+ Current orientation
+
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.orientation
+
+ @orientation.setter
+ def orientation(self, value: tuple[float, float]) -> None:
+ self._obj.orientation = value
+
+ @property
+[docs] def journey_id(self) -> int:
+ """
+ Id of curently followed journey
+
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.journey_id
+
+ @journey_id.setter
+ def journey_id(self, value: int) -> None:
+ self._obj.journey_id = value
+
+ @property
+[docs] def stage_id(self) -> int:
+ """
+ Id of curently followed stage
+
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.stage_id
+
+ @stage_id.setter
+ def stage_id(self, value: int) -> None:
+ self._obj.stage_id = value
+
+ @property
+[docs] def time_gap(self) -> float:
+ """
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.time_gap
+
+ @time_gap.setter
+ def time_gap(self, value: float) -> None:
+ self._obj.time_gap = value
+
+ @property
+[docs] def v0(self) -> float:
+ """
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.v0
+
+ @v0.setter
+ def v0(self, value: float) -> None:
+ self._obj.v0 = value
+
+ @property
+[docs] def radius(self) -> float:
+ """
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.radius
+
+ @radius.setter
+ def radius(self, value: float) -> None:
+ self._obj.radius = value
+
+ @property
+[docs] def id(self) -> int:
+ """
+ Id of this Agent
+
+ NOTE: Setting this property has no effect on agents that are already part of the simulation
+ """
+ return self._obj.id
+
+ @id.setter
+ def id(self, value: int) -> None:
+ self._obj.id = value
+
+ def __str__(self) -> str:
+ return self._obj.__repr__()
+
+
+[docs]class GeneralizedCentrifugalForceModelState:
+ def __init__(self, backing):
+ self._obj = backing
+
+ @property
+
+
+ @speed.setter
+ def speed(self, speed):
+ self._obj.speed = speed
+
+ @property
+
+
+ @e0.setter
+ def e0(self, e0):
+ self._obj.e0 = e0
+
+ @property
+
+
+ @tau.setter
+ def tau(self, tau):
+ self._obj.tau = tau
+
+ @property
+
+
+ @v0.setter
+ def v0(self, v0):
+ self._obj.v0 = v0
+
+ @property
+
+
+ @a_v.setter
+ def a_v(self, a_v):
+ self._obj.a_v = a_v
+
+ @property
+
+
+ @a_min.setter
+ def a_min(self, a_min):
+ self._obj.a_min = a_min
+
+ @property
+
+
+ @b_min.setter
+ def b_min(self, b_min):
+ self._obj.b_min = b_min
+
+ @property
+
+
+ @b_max.setter
+ def b_max(self, b_max):
+ self._obj.b_max = b_max
+
+
+[docs]class VelocityModelState:
+ def __init__(self, backing):
+ self._obj = backing
+
+ @property
+
+
+ @e0.setter
+ def e0(self, e0):
+ self._obj.e0 = e0
+
+ @property
+
+
+ @time_gap.setter
+ def time_gap(self, time_gap):
+ self._obj.time_gap = time_gap
+
+ @property
+
+
+ @tau.setter
+ def tau(self, tau):
+ self._obj.tau = tau
+
+ @property
+
+
+ @v0.setter
+ def v0(self, v0):
+ self._obj.v0 = v0
+
+ @property
+
+
+ @radius.setter
+ def radius(self, radius):
+ self._obj.radius = radius
+
+# Copyright © 2012-2023 Forschungszentrum Jülich GmbH
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+try:
+ import py_jupedsim as py_jps
+except ImportError:
+ from .. import py_jupedsim as py_jps
+
+from jupedsim.native.geometry import Geometry
+
+
+[docs]class RoutingEngine:
+ def __init__(self, geo: Geometry) -> None:
+ self._obj = py_jps.RoutingEngine(geo._obj)
+
+[docs] def compute_waypoints(
+ self, frm: tuple[float, float], to: tuple[float, float]
+ ) -> list[tuple[float, float]]:
+ return self._obj.compute_waypoints(frm, to)
+
+
+
+[docs] def mesh(
+ self,
+ ) -> list[
+ tuple[tuple[float, float], tuple[float, float], tuple[float, float]]
+ ]:
+ return self._obj.mesh()
+
+
+
+# Copyright © 2012-2023 Forschungszentrum Jülich GmbH
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+try:
+ import py_jupedsim as py_jps
+except ImportError:
+ from .. import py_jupedsim as py_jps
+
+from jupedsim.native.geometry import Geometry
+from jupedsim.native.journey import JourneyDescription
+from jupedsim.native.models import (
+ GeneralizedCentrifugalForceModelAgentParameters,
+ GeneralizedCentrifugalForceModelParameters,
+ VelocityModelAgentParameters,
+ VelocityModelParameters,
+)
+from jupedsim.native.stages import (
+ ExitProxy,
+ NotifiableQueueProxy,
+ WaitingSetProxy,
+ WaypointProxy,
+)
+from jupedsim.native.tracing import Trace
+from jupedsim.serialization import TrajectoryWriter
+
+
+[docs]class Simulation:
+ def __init__(
+ self,
+ *,
+ model: VelocityModelParameters
+ | GeneralizedCentrifugalForceModelParameters,
+ geometry: Geometry,
+ dt: float = 0.01,
+ trajectory_writer: TrajectoryWriter | None = None,
+ ) -> None:
+ if isinstance(model, VelocityModelParameters):
+ model_builder = py_jps.VelocityModelBuilder(
+ a_ped=model.a_ped,
+ d_ped=model.d_ped,
+ a_wall=model.a_wall,
+ d_wall=model.d_wall,
+ )
+ py_jps_model = model_builder.build()
+ elif isinstance(model, GeneralizedCentrifugalForceModelParameters):
+ model_builder = py_jps.GCFMModelBuilder(
+ nu_ped=model.nu_ped,
+ nu_wall=model.nu_wall,
+ dist_eff_ped=model.dist_eff_ped,
+ dist_eff_wall=model.dist_eff_wall,
+ intp_width_ped=model.intp_width_ped,
+ intp_width_wall=model.intp_width_wall,
+ maxf_ped=model.maxf_ped,
+ maxf_wall=model.maxf_wall,
+ )
+ py_jps_model = model_builder.build()
+ pass
+ else:
+ raise Exception("Unknown model type supplied")
+ self._writer = trajectory_writer
+ self._obj = py_jps.Simulation(
+ model=py_jps_model, geometry=geometry._obj, dt=dt
+ )
+
+[docs] def add_waypoint_stage(
+ self, position: tuple[float, float], distance
+ ) -> int:
+ return self._obj.add_waypoint_stage(position, distance)
+
+[docs] def add_queue_stage(self, positions: list[tuple[float, float]]) -> int:
+ return self._obj.add_queue_stage(positions)
+
+[docs] def add_waiting_set_stage(
+ self, positions: list[tuple[float, float]]
+ ) -> int:
+ return self._obj.add_waiting_set_stage(positions)
+
+[docs] def add_exit_stage(self, polygon: list[tuple[float, float]]) -> int:
+ return self._obj.add_exit_stage(polygon)
+
+[docs] def add_journey(self, journey: JourneyDescription) -> int:
+ return self._obj.add_journey(journey._obj)
+
+[docs] def add_agent(
+ self,
+ parameters: GeneralizedCentrifugalForceModelAgentParameters
+ | VelocityModelAgentParameters,
+ ) -> int:
+ return self._obj.add_agent(parameters._obj)
+
+
+
+
+
+[docs] def iterate(self, count: int = 1) -> None:
+ if self._writer and self.iteration_count() == 0:
+ self._writer.begin_writing(self)
+ self._writer.write_iteration_state(self)
+
+ for _ in range(0, count):
+ self._obj.iterate()
+ if self._writer:
+ self._writer.write_iteration_state(self)
+
+[docs] def switch_agent_journey(
+ self, agent_id: int, journey_id: int, stage_id: int
+ ) -> None:
+ self._obj.switch_agent_journey(
+ agent_id=agent_id, journey_id=journey_id, stage_id=stage_id
+ )
+
+
+
+
+
+
+
+
+
+
+
+
+
+[docs] def agents_in_range(self, pos: tuple[float, float], distance: float):
+ return self._obj.agents_in_range(pos, distance)
+
+[docs] def agents_in_polygon(self, poly: list[tuple[float, float]]):
+ return self._obj.agents_in_polygon(poly)
+
+[docs] def get_stage_proxy(self, stage_id: int):
+ stage = self._obj.get_stage_proxy(stage_id)
+ match stage:
+ case py_jps.WaypointProxy():
+ return WaypointProxy(stage)
+ case py_jps.ExitProxy():
+ return ExitProxy(stage)
+ case py_jps.NotifiableQueueProxy():
+ return NotifiableQueueProxy(stage)
+ case py_jps.WaitingSetProxy():
+ return WaitingSetProxy(stage)
+ case _:
+ raise Exception(
+ f"Internal error, unexpected type: {type(stage)}"
+ )
+
+
+
+
+
+
+
+# Copyright © 2012-2023 Forschungszentrum Jülich GmbH
+# SPDX-License-Identifier: LGPL-3.0-or-later
+from enum import Enum
+
+try:
+ import py_jupedsim as py_jps
+except ImportError:
+ from .. import py_jupedsim as py_jps
+
+
+[docs]class NotifiableQueueProxy:
+ def __init__(self, backing):
+ self._obj = backing
+
+
+
+
+
+
+
+
+
+
+
+
+
+[docs]class WaitingSetProxy:
+ def __init__(self, backing):
+ self._obj = backing
+
+
+
+
+
+
+
+ @property
+
+
+ @state.setter
+ def state(self, new_state: WaitingSetState):
+ self._obj.state = new_state.value
+
+
+
+
+
+
+
+# Copyright © 2012-2023 Forschungszentrum Jülich GmbH
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+try:
+ import py_jupedsim as py_jps
+except ImportError:
+ from .. import py_jupedsim as py_jps
+
+
+[docs]class Trace:
+ def __init__(self) -> None:
+ self._obj = py_jps.Trace
+
+ @property
+[docs] def iteration_duration(self):
+ """Time for one simulation iteration in us.
+
+ Returns:
+ Time for one simulation iteration in us
+ """
+ return self._obj.iteration_duration
+
+ @property
+[docs] def operational_level_duration(self):
+ """Time for one simulation iteration in the operational level in us.
+
+ Returns:
+ Time for one simulation iteration in the operational level in us
+ """
+
+ return self._obj.operational_level_duration
+
+ def __str__(self) -> str:
+ return self._obj.__repr__()
+
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.13.0/_static/copybutton.js b/v0.13.0/_static/copybutton.js new file mode 100644 index 0000000000..02c5c82d9d --- /dev/null +++ b/v0.13.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.13.0/_static/copybutton_funcs.js b/v0.13.0/_static/copybutton_funcs.js new file mode 100644 index 0000000000..dbe1aaad79 --- /dev/null +++ b/v0.13.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.13.0/_static/css/custom.css b/v0.13.0/_static/css/custom.css new file mode 100644 index 0000000000..f8a6710e4d --- /dev/null +++ b/v0.13.0/_static/css/custom.css @@ -0,0 +1,15 @@ +/* Provided by Sphinx's 'basic' theme, and included in the final set of assets */ +@import "../basic.css"; + +html[data-theme="light"] { + /*Set main color to match color of main logo color */ + --pst-color-primary: rgb(0 86 118); +} + +h1, h2 { + color: #005676 !important; + font-weight: bolder; +} +h3, h4, h5, h6 { + color: #005676 !important; +} diff --git a/v0.13.0/_static/doctools.js b/v0.13.0/_static/doctools.js new file mode 100644 index 0000000000..527b876ca6 --- /dev/null +++ b/v0.13.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.13.0/_static/documentation_options.js b/v0.13.0/_static/documentation_options.js new file mode 100644 index 0000000000..03c0d01dcb --- /dev/null +++ b/v0.13.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.13.0/_static/file.png b/v0.13.0/_static/file.png new file mode 100644 index 0000000000..a858a410e4 Binary files /dev/null and b/v0.13.0/_static/file.png differ diff --git a/v0.13.0/_static/graphviz.css b/v0.13.0/_static/graphviz.css new file mode 100644 index 0000000000..19e7afd385 --- /dev/null +++ b/v0.13.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.13.0/_static/images/logo_binder.svg b/v0.13.0/_static/images/logo_binder.svg new file mode 100644 index 0000000000..45fecf7511 --- /dev/null +++ b/v0.13.0/_static/images/logo_binder.svg @@ -0,0 +1,19 @@ + + + diff --git a/v0.13.0/_static/images/logo_colab.png b/v0.13.0/_static/images/logo_colab.png new file mode 100644 index 0000000000..b7560ec216 Binary files /dev/null and b/v0.13.0/_static/images/logo_colab.png differ diff --git a/v0.13.0/_static/images/logo_deepnote.svg b/v0.13.0/_static/images/logo_deepnote.svg new file mode 100644 index 0000000000..fa77ebfc25 --- /dev/null +++ b/v0.13.0/_static/images/logo_deepnote.svg @@ -0,0 +1 @@ + diff --git a/v0.13.0/_static/images/logo_jupyterhub.svg b/v0.13.0/_static/images/logo_jupyterhub.svg new file mode 100644 index 0000000000..60cfe9f222 --- /dev/null +++ b/v0.13.0/_static/images/logo_jupyterhub.svg @@ -0,0 +1 @@ + diff --git a/v0.13.0/_static/jquery-3.6.0.js b/v0.13.0/_static/jquery-3.6.0.js new file mode 100644 index 0000000000..fc6c299b73 --- /dev/null +++ b/v0.13.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