├── src └── mnms │ ├── io │ ├── __init__.py │ ├── utils.py │ └── graph.py │ ├── graph │ ├── __init__.py │ ├── specific_layers.py │ ├── zone.py │ └── road.py │ ├── tools │ ├── __init__.py │ ├── dict_tools.py │ ├── cost.py │ ├── exceptions.py │ ├── progress.py │ ├── preprocessing.py │ ├── geometry.py │ └── observer.py │ ├── generation │ ├── __init__.py │ ├── mlgraph.py │ ├── demand.py │ ├── zones.py │ └── layers.py │ ├── vehicles │ ├── __init__.py │ ├── fleet.py │ └── manager.py │ ├── flow │ ├── __init__.py │ └── abstract.py │ ├── __init__.py │ ├── mobility_service │ ├── __init__.py │ ├── interfaces.py │ └── personal_vehicle.py │ ├── demand │ ├── __init__.py │ └── horizon.py │ ├── travel_decision │ ├── __init__.py │ └── dummy.py │ └── log.py ├── examples ├── manhattan │ ├── demand.csv │ └── run.py ├── public_transport │ ├── demand.csv │ └── run.py ├── on_demand_car │ ├── demand.csv │ └── run.py ├── snapshot │ ├── OUTPUTS │ │ └── snapshot.mnms │ ├── snapshot_monolink.py │ ├── load_snapshot_and_run.py │ ├── run_monolink.py │ └── run_and_take_snapshot.py ├── vehicle_sharing │ ├── demand.csv │ ├── run_freefloating.py │ ├── run.py │ └── run_intermodality.py ├── Lyon6 │ └── param.json ├── Lyon63V │ ├── run_Lyon63V_CarOnly.py │ └── run_Lyon63V.py ├── mulitmodal_nested_manhattan │ ├── inputs │ │ └── mobility_services_events_graphs.json │ └── layers_connection.py ├── congested_mfd │ └── run.py └── intermodal │ └── run.py ├── tests ├── demand │ ├── data_demand │ │ ├── test_demand_bad_type2.csv │ │ ├── test_demand_bad_type1.csv │ │ ├── test_demand_bad_departure_format1.csv │ │ ├── test_demand_bad_departure_format2.csv │ │ ├── test_demand_node.csv │ │ ├── test_demand_mobility_services.csv │ │ ├── test_demand_mobility_services_graph.csv │ │ ├── test_bad_optional_column2.csv │ │ ├── test_demand_coordinate.csv │ │ └── test_bad_optional_column1.csv │ └── test_csv_demand.py ├── travel_decision │ └── data │ │ ├── test_forced_path.csv │ │ ├── test_wrong_forced_path.csv │ │ ├── mobility_services_graph_order1.json │ │ └── mobility_services_graph_order2.json ├── generation │ └── test_generation_demand.py ├── tools │ ├── test_render.py │ └── test_time.py ├── graph │ ├── test_roads.py │ ├── test_transit_layer.py │ └── test_layers.py ├── io │ ├── test_io_graph.py │ └── test_save_load_transit_link.py ├── mobility_services │ ├── test_personal_car.py │ ├── test_public_transport_several_pickups.py │ ├── test_on_demand_depot.py │ └── test_station_based_vehicle_sharing.py ├── flow │ └── test_user_flow.py └── simulation │ └── test_multi_modal.py ├── doc └── MnMS_detailed_documentation.pdf ├── conda ├── doc.yaml └── env.yaml ├── setup.py ├── .coveragerc ├── mkdocs.yml ├── docs ├── index.md └── gen_ref_pages.py ├── README.md ├── .gitignore └── script └── conversion └── bison └── coordinates.py /src/mnms/io/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/mnms/graph/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/mnms/tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/mnms/generation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/mnms/vehicles/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/manhattan/demand.csv: -------------------------------------------------------------------------------- 1 | ID;DEPARTURE;ORIGIN;DESTINATION 2 | U0;07:00:00;0 0;1000 1000 3 | -------------------------------------------------------------------------------- /src/mnms/flow/__init__.py: -------------------------------------------------------------------------------- 1 | from mnms.log import create_logger 2 | 3 | log = create_logger(__name__) -------------------------------------------------------------------------------- /examples/public_transport/demand.csv: -------------------------------------------------------------------------------- 1 | ID;DEPARTURE;ORIGIN;DESTINATION 2 | U0;07:00:00;1500 0;3000 0 3 | -------------------------------------------------------------------------------- /src/mnms/__init__.py: -------------------------------------------------------------------------------- 1 | from .log import create_logger, LOGLEVEL 2 | 3 | log = create_logger(__name__) -------------------------------------------------------------------------------- /tests/demand/data_demand/test_demand_bad_type2.csv: -------------------------------------------------------------------------------- 1 | ID;DEPARTURE;ORIGIN;DESTINATION 2 | U0;07:00:00;0 0;B 3 | -------------------------------------------------------------------------------- /tests/demand/data_demand/test_demand_bad_type1.csv: -------------------------------------------------------------------------------- 1 | ID;DEPARTURE;ORIGIN;DESTINATION 2 | U0;07:00:00;12;1000 1000 -------------------------------------------------------------------------------- /src/mnms/mobility_service/__init__.py: -------------------------------------------------------------------------------- 1 | from mnms.log import create_logger 2 | 3 | log = create_logger(__name__) 4 | -------------------------------------------------------------------------------- /doc/MnMS_detailed_documentation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EMob-Lab/MnMS/HEAD/doc/MnMS_detailed_documentation.pdf -------------------------------------------------------------------------------- /tests/demand/data_demand/test_demand_bad_departure_format1.csv: -------------------------------------------------------------------------------- 1 | ID;DEPARTURE;ORIGIN;DESTINATION 2 | U0;07:00;0 0;1000 1000 -------------------------------------------------------------------------------- /tests/demand/data_demand/test_demand_bad_departure_format2.csv: -------------------------------------------------------------------------------- 1 | ID;DEPARTURE;ORIGIN;DESTINATION 2 | U0;2024;0 0;1000 1000 -------------------------------------------------------------------------------- /examples/on_demand_car/demand.csv: -------------------------------------------------------------------------------- 1 | ID;DEPARTURE;ORIGIN;DESTINATION 2 | U0;07:00:00;0 0;300 300 3 | U1;07:05:00;0 0;300 300 4 | -------------------------------------------------------------------------------- /examples/snapshot/OUTPUTS/snapshot.mnms: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EMob-Lab/MnMS/HEAD/examples/snapshot/OUTPUTS/snapshot.mnms -------------------------------------------------------------------------------- /tests/demand/data_demand/test_demand_node.csv: -------------------------------------------------------------------------------- 1 | ID;DEPARTURE;ORIGIN;DESTINATION 2 | U0;07:00:00;A;B 3 | U1;07:03:00;A;B 4 | U2;08:00:00;A;B 5 | U3:08:00:01;A;B 6 | -------------------------------------------------------------------------------- /src/mnms/demand/__init__.py: -------------------------------------------------------------------------------- 1 | from .manager import BaseDemandManager, CSVDemandManager 2 | from .user import User 3 | 4 | from mnms.log import create_logger 5 | 6 | log = create_logger(__name__) -------------------------------------------------------------------------------- /tests/demand/data_demand/test_demand_mobility_services.csv: -------------------------------------------------------------------------------- 1 | ID;DEPARTURE;ORIGIN;DESTINATION;MOBILITY SERVICES 2 | U0;07:00:00;0 0;1000 1000;CAR RIDEHAILING 3 | U1;07:05:00;0 0;1000 1000;CAR 4 | -------------------------------------------------------------------------------- /tests/demand/data_demand/test_demand_mobility_services_graph.csv: -------------------------------------------------------------------------------- 1 | ID;DEPARTURE;ORIGIN;DESTINATION;MOBILITY SERVICES GRAPH 2 | U0;07:00:00;0 0;1000 1000;G1 3 | U1;07:05:00;0 0;1000 1000;G2 4 | -------------------------------------------------------------------------------- /src/mnms/travel_decision/__init__.py: -------------------------------------------------------------------------------- 1 | from .dummy import DummyDecisionModel 2 | from .logit import LogitDecisionModel 3 | 4 | from mnms.log import create_logger 5 | 6 | log = create_logger(__name__) -------------------------------------------------------------------------------- /tests/demand/data_demand/test_bad_optional_column2.csv: -------------------------------------------------------------------------------- 1 | ID;ORIGIN;DESTINATION;MOBILITY SERVICES GRAPH;DEPARTURE 2 | U0;0 0;1000 1000;G1;CAR RIDEHAILING;07:00:00 3 | U1;0 0;1000 1000;G2;CAR;07:05:00 4 | -------------------------------------------------------------------------------- /examples/vehicle_sharing/demand.csv: -------------------------------------------------------------------------------- 1 | ID;DEPARTURE;ORIGIN;DESTINATION; 2 | U0;07:00:00;ORIGIN_21;DESTINATION_4;velov 3 | U1;07:15:00;ORIGIN_21;DESTINATION_4 4 | U2;07:40:00;ORIGIN_4;DESTINATION_21;velov 5 | -------------------------------------------------------------------------------- /tests/demand/data_demand/test_demand_coordinate.csv: -------------------------------------------------------------------------------- 1 | ID;DEPARTURE;ORIGIN;DESTINATION 2 | U0;07:00:00;0 0;1000 1000 3 | U1;07:05:00;0 0;1000 1000 4 | U2;08:00:00;0 0;1000 1000 5 | U3;08:00:01;0 0;1000 1000 6 | -------------------------------------------------------------------------------- /tests/demand/data_demand/test_bad_optional_column1.csv: -------------------------------------------------------------------------------- 1 | ID;DEPARTURE;ORIGIN;DESTINATION;MOBILITY SERVICES GRAPH;MOBILITY SERVICES 2 | U0;07:00:00;0 0;1000 1000;G1;CAR RIDEHAILING 3 | U1;07:05:00;0 0;1000 1000;G2;CAR 4 | -------------------------------------------------------------------------------- /tests/travel_decision/data/test_forced_path.csv: -------------------------------------------------------------------------------- 1 | ID;DEPARTURE;ORIGIN;DESTINATION;PATH;CHOSEN SERVICES 2 | U0;07:00:00;0 0;0 5000;; 3 | U1;07:00:00;0 0;0 5000;ORIGIN_0 BUSL_S0 BUSL_S1 DESTINATION_1;CAR:CAR BUS:BUS 4 | -------------------------------------------------------------------------------- /tests/travel_decision/data/test_wrong_forced_path.csv: -------------------------------------------------------------------------------- 1 | ID;DEPARTURE;ORIGIN;DESTINATION;PATH;CHOSEN SERVICES 2 | U0;07:00:00;0 0;0 5000;; 3 | U1;07:00:00;0 0;0 5000;ORIGIN_1 BUSL_S0 BUSL_S1 DESTINATION_1;CAR:CAR BUS:BUS 4 | -------------------------------------------------------------------------------- /conda/doc.yaml: -------------------------------------------------------------------------------- 1 | channels: 2 | - conda-forge 3 | - anaconda 4 | dependencies: 5 | - notebook 6 | - mkdocs 7 | - pip 8 | - pip: 9 | - mkdocstrings 10 | - mkdocs-jupyter 11 | - mkdocs-gen-files 12 | - mkdocstrings-python 13 | 14 | 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | pkgs = find_packages('src') 4 | 5 | setup_kwds = dict( 6 | name='mnms', 7 | version="1.0.1", 8 | zip_safe=False, 9 | packages=pkgs, 10 | package_dir={'': 'src'}, 11 | entry_points={}, 12 | ) 13 | 14 | setup(**setup_kwds) 15 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [html] 2 | title = mnms's coverage 3 | directory = build/htmlcov 4 | 5 | [run] 6 | source = mnms 7 | 8 | [report] 9 | show_missing = True 10 | exclude_lines = 11 | # Don't complain if tests don't hit defensive assertion code: 12 | raise AssertionError 13 | raise NotImplemented 14 | raise NotImplementedError 15 | __main__ 16 | 17 | -------------------------------------------------------------------------------- /conda/env.yaml: -------------------------------------------------------------------------------- 1 | name: mnms 2 | channels: 3 | - conda-forge 4 | - anaconda 5 | dependencies: 6 | - python=3.10 7 | - ipython 8 | - matplotlib 9 | - numpy 10 | - pyarrow 11 | - pandas 12 | - pytest 13 | - pytest-cov 14 | - llvm-openmp 15 | - cmake 16 | - pybind11 17 | - ipykernel 18 | - notebook 19 | - scipy 20 | - shapely 21 | - mpl-scatter-density 22 | - seaborn 23 | - jsonpickle 24 | - dill 25 | 26 | -------------------------------------------------------------------------------- /src/mnms/tools/dict_tools.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | def sum_dict(*dicts: Dict[str, float]) -> Dict[str, float]: 4 | """ 5 | Sum per key of several dictionaries 6 | Parameters 7 | ---------- 8 | dicts 9 | 10 | Returns 11 | ------- 12 | Dictionary containing the sum 13 | """ 14 | keys = [d.keys() for d in dicts] 15 | keys = set().union(*keys) 16 | return {k: sum(d.get(k, 0) for d in dicts) for k in keys} -------------------------------------------------------------------------------- /src/mnms/tools/cost.py: -------------------------------------------------------------------------------- 1 | def create_link_costs(travel_time=0, waiting_time=0, speed=0, length=0): 2 | return dict(travel_time=travel_time, 3 | waiting_time=waiting_time, 4 | speed=speed, 5 | length=length, 6 | _default=1) 7 | 8 | 9 | def create_service_costs(waiting_time=0, environmental=0, currency=0): 10 | return dict(waiting_time=waiting_time, 11 | environmental=environmental, 12 | currency=currency) -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: MnMS 2 | site_url: "" 3 | use_directory_urls: false 4 | nav: 5 | - Home: index.md 6 | - Code Reference: 7 | - mnms: reference/SUMMARY.md 8 | 9 | theme: 10 | name: material 11 | palette: 12 | scheme: slate 13 | primary: black 14 | 15 | plugins: 16 | - search 17 | - gen-files: 18 | scripts: 19 | - docs/gen_ref_pages.py # 20 | - mkdocstrings: 21 | watch: 22 | - src/mnms # 23 | - mkdocs-jupyter: 24 | include: ["*.ipynb"] 25 | # execute: true 26 | include_source: True 27 | -------------------------------------------------------------------------------- /tests/generation/test_generation_demand.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from mnms.generation.demand import generate_random_demand 4 | from mnms.generation.mlgraph import generate_manhattan_passenger_car 5 | 6 | class TestDemandGeneration(unittest.TestCase): 7 | def setUp(self): 8 | """Initiates the test. 9 | """ 10 | 11 | def tearDown(self): 12 | """Concludes and closes the test. 13 | """ 14 | 15 | def test_random_demand(self): 16 | mlgraph = generate_manhattan_passenger_car(10, 1) 17 | 18 | demand = generate_random_demand(mlgraph, 10) -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to MnMS's technical documentation! 2 | 3 | 4 | ## Description 5 | 6 | MnMS (**M**ultimodal **N**etwork **M**odelling and **S**imulation) is a multimodal dynamic traffic simulator designed for a large urban scale. It results from all research activities of the ERC MAGnUM project. Further extensions related to on-demand mobility have been developped with the DIT4TraM project. 7 | 8 | 9 | ## Requirements 10 | 11 | - [HiPOP](https://github.com/licit-lab/HiPOP) 12 | - python=3.10 13 | - ipython 14 | - matplotlib 15 | - numpy 16 | - pyarrow 17 | - pandas 18 | - pytest 19 | - pytest-cov 20 | - openmp 21 | - cmake 22 | - pybind11 23 | - ipykernel 24 | - notebook 25 | - scipy 26 | - shapely 27 | - mpl-scatter-density 28 | -------------------------------------------------------------------------------- /src/mnms/io/utils.py: -------------------------------------------------------------------------------- 1 | from json import JSONEncoder 2 | from importlib import import_module 3 | 4 | import numpy as np 5 | 6 | 7 | def load_class_by_module_name(cls): 8 | cls_name = cls.split('.')[-1] 9 | cls_module_name = cls.removesuffix('.' + cls_name) 10 | cls_module = import_module(cls_module_name) 11 | cls_class = getattr(cls_module, cls_name) 12 | 13 | return cls_class 14 | 15 | 16 | class MNMSEncoder(JSONEncoder): 17 | def default(self, obj): 18 | if isinstance(obj, set): 19 | return list(obj) 20 | if isinstance(obj, np.ndarray): 21 | return obj.tolist() 22 | if isinstance(obj, np.int64): 23 | return int(obj) 24 | 25 | return super().default(obj) 26 | -------------------------------------------------------------------------------- /docs/gen_ref_pages.py: -------------------------------------------------------------------------------- 1 | """Generate the code reference pages and navigation.""" 2 | 3 | from pathlib import Path 4 | 5 | import mkdocs_gen_files 6 | 7 | nav = mkdocs_gen_files.Nav() 8 | 9 | for path in sorted(Path("src").rglob("*.py")): 10 | module_path = path.relative_to("src").with_suffix("") 11 | doc_path = path.relative_to("src").with_suffix(".md") 12 | full_doc_path = Path("reference", doc_path) 13 | 14 | parts = tuple(module_path.parts) 15 | 16 | if parts[-1] == "__init__": 17 | parts = parts[:-1] 18 | elif parts[-1] == "__main__": 19 | continue 20 | 21 | nav[parts] = doc_path.as_posix() # 22 | 23 | with mkdocs_gen_files.open(full_doc_path, "w") as fd: 24 | ident = ".".join(parts) 25 | fd.write(f"::: {ident}") 26 | 27 | mkdocs_gen_files.set_edit_path(full_doc_path, path) 28 | 29 | with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: # 30 | nav_file.writelines(nav.build_literate_nav()) # 31 | -------------------------------------------------------------------------------- /src/mnms/tools/exceptions.py: -------------------------------------------------------------------------------- 1 | class DuplicateNodesError(Exception): 2 | def __init__(self, nids:set): 3 | msg = f'Nodes {nids} are not unique' 4 | super().__init__(msg) 5 | 6 | 7 | class DuplicateLinksError(Exception): 8 | def __init__(self, lids:set): 9 | msg = f'Links {lids} are not unique' 10 | super().__init__(msg) 11 | 12 | 13 | class PathNotFound(Exception): 14 | def __init__(self, origin, destination): 15 | msg = f'No paths has been found between {origin} -> {destination}' 16 | super().__init__(msg) 17 | 18 | 19 | class VehicleNotFoundError(Exception): 20 | def __init__(self, user, mobility_service): 21 | msg = f"{mobility_service.id} did not found any vehicle for {user}" 22 | super().__init__(msg) 23 | 24 | 25 | class CSVDemandParseError(Exception): 26 | def __init__(self, file): 27 | msg = f"Cannot parse the origin or destination for demand_type for the file {file}" 28 | super(CSVDemandParseError, self).__init__(msg) -------------------------------------------------------------------------------- /src/mnms/generation/mlgraph.py: -------------------------------------------------------------------------------- 1 | from mnms.generation.roads import generate_manhattan_road 2 | from mnms.generation.layers import generate_layer_from_roads, generate_matching_origin_destination_layer 3 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 4 | from mnms.graph.layers import MultiLayerGraph 5 | 6 | 7 | def generate_manhattan_passenger_car(n, link_length, resid="RES") -> MultiLayerGraph: 8 | """ 9 | 10 | Parameters 11 | ---------- 12 | n 13 | link_length 14 | resid 15 | 16 | Returns 17 | ------- 18 | 19 | """ 20 | roads = generate_manhattan_road(n, link_length, resid) 21 | layer_car = generate_layer_from_roads(roads, 22 | "CAR", 23 | mobility_services=[PersonalMobilityService()]) 24 | 25 | odlayer = generate_matching_origin_destination_layer(roads) 26 | 27 | mlgraph = MultiLayerGraph([layer_car], 28 | odlayer, 29 | 1e-5) 30 | 31 | return mlgraph 32 | -------------------------------------------------------------------------------- /src/mnms/graph/specific_layers.py: -------------------------------------------------------------------------------- 1 | from mnms.log import create_logger 2 | import numpy as np 3 | 4 | log = create_logger(__name__) 5 | 6 | class OriginDestinationLayer(object): 7 | def __init__(self): 8 | self.origins = dict() 9 | self.destinations = dict() 10 | self.id = "ODLAYER" 11 | 12 | def create_origin_node(self, nid, pos: np.ndarray): 13 | # new_node = Node(nid, pos[0], pos[1], self.id) 14 | 15 | self.origins[nid] = pos 16 | 17 | def create_destination_node(self, nid, pos: np.ndarray): 18 | # new_node = Node(nid, pos[0], pos[1], self.id) 19 | 20 | self.destinations[nid] = pos 21 | 22 | def __dump__(self): 23 | return {'ORIGINS': {node: self.origins[node] for node in self.origins}, 24 | 'DESTINATIONS': {node: self.destinations[node] for node in self.destinations}} 25 | 26 | @classmethod 27 | def __load__(cls, data) -> "OriginDestinationLayer": 28 | new_obj = cls() 29 | for nid, pos in data['ORIGINS'].items(): 30 | new_obj.create_origin_node(nid, pos) 31 | for nid, pos in data['DESTINATIONS'].items(): 32 | new_obj.create_destination_node(nid, pos) 33 | 34 | return new_obj -------------------------------------------------------------------------------- /tests/tools/test_render.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import matplotlib.pyplot as plt 4 | 5 | from mnms.demand.user import Path 6 | from mnms.generation.mlgraph import generate_manhattan_passenger_car 7 | from mnms.generation.roads import generate_line_road 8 | from mnms.tools.render import draw_roads, draw_path 9 | 10 | 11 | class TestRenderRoads(unittest.TestCase): 12 | def setUp(self) -> None: 13 | roads = generate_line_road([0, 0], [0, 3000], 4) 14 | roads.register_stop('S0', '0_1', 0.10) 15 | roads.register_stop('S1', '1_2', 0.50) 16 | roads.register_stop('S2', '2_3', 0.99) 17 | 18 | self.roads = roads 19 | 20 | def tearDown(self) -> None: 21 | pass 22 | 23 | def test_draw_roads(self): 24 | fig, ax = plt.subplots() 25 | draw_roads(ax, self.roads) 26 | 27 | 28 | class TestRenderPath(unittest.TestCase): 29 | def setUp(self) -> None: 30 | mlgraph = generate_manhattan_passenger_car(4, 10) 31 | self.mlgraph = mlgraph 32 | 33 | def tearDown(self) -> None: 34 | pass 35 | 36 | def test_draw_path(self): 37 | path = Path(cost=0, nodes=["CAR_NORTH_0", "CAR_3", "CAR_2", "CAR_1", "CAR_0"]) 38 | fig, ax = plt.subplots() 39 | draw_path(ax, self.mlgraph, path) 40 | -------------------------------------------------------------------------------- /src/mnms/demand/horizon.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import List 3 | 4 | from mnms.demand import User 5 | from mnms.demand.manager import AbstractDemandManager 6 | from mnms.time import Dt, Time 7 | 8 | 9 | class AbstractDemandHorizon(ABC): 10 | def __init__(self, manager: AbstractDemandManager, dt: Dt): 11 | """ 12 | Abstraction of the demand horizon, it provides the next demand for the current time until current time + dt 13 | 14 | Args: 15 | manager: The demand manager 16 | dt: The time window of the horizon 17 | 18 | """ 19 | self.dt: Dt = dt 20 | self.manager: AbstractDemandManager = manager.copy() 21 | 22 | @abstractmethod 23 | def get(self, tstart: Time) -> List[User]: 24 | """ 25 | Return a list of User from tsart to start + dt 26 | 27 | Args: 28 | tstart: the start time of the horizon 29 | 30 | Returns: 31 | A list of Users 32 | 33 | """ 34 | pass 35 | 36 | 37 | class DemandHorizon(AbstractDemandHorizon): 38 | """ 39 | A simple implementation of the Demand Horizon that returns the exact demand 40 | between tstart and tstart + dt 41 | """ 42 | def get(self, tstart: Time): 43 | return self.manager.get_next_departures(tstart, tstart.add_time(self.dt)) 44 | -------------------------------------------------------------------------------- /examples/snapshot/snapshot_monolink.py: -------------------------------------------------------------------------------- 1 | from mnms.simulation import Supervisor, load_snaphshot 2 | from mnms.generation.roads import generate_line_road 3 | from mnms.graph.layers import MultiLayerGraph, CarLayer 4 | from mnms.flow.MFD import Reservoir, MFDFlowMotor 5 | from mnms.log import attach_log_file, LOGLEVEL, get_logger, set_all_mnms_logger_level 6 | from mnms.time import Time, Dt 7 | from mnms.travel_decision.logit import LogitDecisionModel 8 | from mnms.tools.observer import CSVUserObserver, CSVVehicleObserver 9 | from mnms.graph.specific_layers import OriginDestinationLayer 10 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 11 | from mnms.demand import BaseDemandManager, User 12 | from mnms.graph.zone import Zone, construct_zone_from_sections 13 | from mnms.io.graph import save_graph 14 | import logging 15 | import pandas as pd 16 | import os 17 | 18 | if __name__ == '__main__': 19 | 20 | outdir = "OUTPUTS" 21 | 22 | # Outputs 23 | outdir_path = os.getcwd() + '/' + outdir 24 | if not os.path.isdir(outdir_path): 25 | os.mkdir(outdir_path) 26 | 27 | set_all_mnms_logger_level(LOGLEVEL.INFO) 28 | get_logger("mnms.graph.shortest_path").setLevel(LOGLEVEL.WARNING) 29 | attach_log_file(outdir_path + '/simulation.log') 30 | 31 | supervisor = load_snaphshot(outdir_path+'/snapshot') 32 | 33 | supervisor.run(Time('08:30:00'), Time('08:45:00'), Dt(seconds=1), 10) -------------------------------------------------------------------------------- /tests/travel_decision/data/mobility_services_graph_order1.json: -------------------------------------------------------------------------------- 1 | { 2 | "G1": { 3 | "None": { 4 | "DEPARTURE": [ 5 | "CAR", 6 | "RIDEHAILING1", 7 | "RIDEHAILING2" 8 | ] 9 | }, 10 | "CAR RIDEHAILING1 RIDEHAILING2": { 11 | "RIDEHAILING1": [ 12 | "CAR" 13 | ], 14 | "RIDEHAILING2": [ 15 | "CAR", 16 | "RIDEHAILING1" 17 | ] 18 | }, 19 | "CAR RIDEHAILING1": { 20 | "RIDEHAILING1": [ 21 | "CAR" 22 | ] 23 | } 24 | }, 25 | "G2": { 26 | "None": { 27 | "DEPARTURE": [ 28 | "RIDEHAILING1", 29 | "RIDEHAILING2" 30 | ] 31 | }, 32 | "RIDEHAILING1 RIDEHAILING2": { 33 | "RIDEHAILING1": [ 34 | "RIDEHAILING2" 35 | ], 36 | "RIDEHAILING2": [ 37 | "RIDEHAILING1" 38 | ] 39 | }, 40 | "RIDEHAILING1": { 41 | "RIDEHAILING1": [ 42 | ] 43 | }, 44 | "RIDEHAILING2": { 45 | "RIDEHAILING2": [ 46 | ] 47 | } 48 | }, 49 | "G3": { 50 | "None": { 51 | "DEPARTURE": [ 52 | "CAR" 53 | ] 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/travel_decision/data/mobility_services_graph_order2.json: -------------------------------------------------------------------------------- 1 | { 2 | "G1": { 3 | "None": { 4 | "DEPARTURE": [ 5 | "CAR", 6 | "RIDEHAILING1", 7 | "RIDEHAILING2" 8 | ] 9 | }, 10 | "RIDEHAILING1 RIDEHAILING2 CAR": { 11 | "RIDEHAILING1": [ 12 | "CAR" 13 | ], 14 | "RIDEHAILING2": [ 15 | "CAR", 16 | "RIDEHAILING1" 17 | ] 18 | }, 19 | "RIDEHAILING1 CAR": { 20 | "RIDEHAILING1": [ 21 | "CAR" 22 | ] 23 | } 24 | }, 25 | "G2": { 26 | "None": { 27 | "DEPARTURE": [ 28 | "RIDEHAILING1", 29 | "RIDEHAILING2" 30 | ] 31 | }, 32 | "RIDEHAILING2 RIDEHAILING1": { 33 | "RIDEHAILING1": [ 34 | "RIDEHAILING2" 35 | ], 36 | "RIDEHAILING2": [ 37 | "RIDEHAILING1" 38 | ] 39 | }, 40 | "RIDEHAILING1": { 41 | "RIDEHAILING1": [ 42 | ] 43 | }, 44 | "RIDEHAILING2": { 45 | "RIDEHAILING2": [ 46 | ] 47 | } 48 | }, 49 | "G3": { 50 | "None": { 51 | "DEPARTURE": [ 52 | "CAR" 53 | ] 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/snapshot/load_snapshot_and_run.py: -------------------------------------------------------------------------------- 1 | from mnms.simulation import Supervisor, load_snaphshot 2 | from mnms.generation.roads import generate_line_road 3 | from mnms.graph.layers import MultiLayerGraph, CarLayer 4 | from mnms.flow.MFD import Reservoir, MFDFlowMotor 5 | from mnms.log import attach_log_file, LOGLEVEL, get_logger, set_all_mnms_logger_level 6 | from mnms.time import Time, Dt 7 | from mnms.travel_decision.logit import LogitDecisionModel 8 | from mnms.tools.observer import CSVUserObserver, CSVVehicleObserver 9 | from mnms.graph.specific_layers import OriginDestinationLayer 10 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 11 | from mnms.demand import BaseDemandManager, User 12 | from mnms.graph.zone import Zone, construct_zone_from_sections 13 | from mnms.io.graph import save_graph 14 | import logging 15 | import pandas as pd 16 | import os 17 | 18 | if __name__ == '__main__': 19 | 20 | outdir = "OUTPUTS" 21 | 22 | # Outputs 23 | outdir_path = os.getcwd() + '/' + outdir 24 | if not os.path.isdir(outdir_path): 25 | os.mkdir(outdir_path) 26 | 27 | set_all_mnms_logger_level(LOGLEVEL.INFO) 28 | get_logger("mnms.graph.shortest_path").setLevel(LOGLEVEL.WARNING) 29 | attach_log_file(outdir_path + '/simulation.log') 30 | 31 | supervisor = load_snaphshot(outdir_path+'/snapshot') 32 | 33 | supervisor.run(Time('07:00:00'), Time('07:30:00'), Dt(minutes=1), 10) -------------------------------------------------------------------------------- /src/mnms/mobility_service/interfaces.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from dataclasses import dataclass, field 3 | from typing import List, Deque, Tuple 4 | 5 | from mnms.time import Time 6 | from mnms.vehicles.veh_type import Vehicle 7 | 8 | ItemVehicleQueue = Tuple[Vehicle, Time] 9 | 10 | 11 | @dataclass(slots=True) 12 | class Depot: 13 | id: str 14 | node: str 15 | capacity: int 16 | vehicles: Deque[ItemVehicleQueue] = field(default_factory=deque) 17 | 18 | def add_vehicle(self, vehicle: Vehicle, time: Time) -> None: 19 | if self.contains(vehicle): 20 | log.warning(f'Depot {self.id} already contains vehicle {vehicle}') 21 | else: 22 | self.vehicles.appendleft((vehicle, time)) 23 | 24 | def remove_vehicle_by_index(self, index: int) -> ItemVehicleQueue: 25 | veh, time = self.vehicles[index] 26 | del self.vehicles[index] 27 | return veh, time 28 | 29 | def remove_vehicle(self, veh: Vehicle) -> ItemVehicleQueue: 30 | veh_index = [i for i,item in enumerate(self.vehicles) if item[0] == veh][0] 31 | _, time = self.remove_vehicle_by_index(veh_index) 32 | return veh, time 33 | 34 | def get_first_vehicle(self) -> ItemVehicleQueue: 35 | return self.vehicles[-1] 36 | 37 | def is_full(self) -> bool: 38 | return self.capacity <= len(self.vehicles) 39 | 40 | def contains(self, veh: Vehicle) -> bool: 41 | return sum([1 for v,_ in self.vehicles if v == veh]) 42 | -------------------------------------------------------------------------------- /examples/Lyon6/param.json: -------------------------------------------------------------------------------- 1 | { 2 | "INPUT": { 3 | "indir": "./INPUTS", 4 | "network_file": "/network_lyon6_V3-test.json", 5 | "demand_file": "/demand_coords.csv", 6 | "mfd_file": "/MFD_10_201802.csv" 7 | }, 8 | "OUTPUT": { 9 | "output_dir": "./OUTPUTS/no_roads_typo/", 10 | "log_file": "/simulation.log", 11 | "path_file": "/path.csv", 12 | "user_file": "/user.csv", 13 | "flow_file": "/flow.csv", 14 | "travel_time_file": "/travel_time_link.csv", 15 | "vehicle_file": "/veh.csv" 16 | }, 17 | "SUPERVISOR": { 18 | "log_level": "LOGLEVEL.WARNING", 19 | "demand_type": "coordinate", 20 | "start_time": "00:00:00", 21 | "end_time": "01:00:00", 22 | "flow_dt": 1, 23 | "unit_flow_dt": "minutes", 24 | "affectation_factor": 10}, 25 | "GRAPH": { 26 | "roads_typo": {} 27 | }, 28 | "COSTS": { 29 | "roads_typo_cost_multipliers": {}, 30 | "cost": "travel_time" 31 | }, 32 | "RESERVOIRS": [ 33 | { 34 | "id": "RES1", 35 | "zone": "1", 36 | "function": "" 37 | }, 38 | { 39 | "id": "RES2", 40 | "zone": "2", 41 | "function": "" 42 | } 43 | ], 44 | "TRAVEL_DECISION": { 45 | "n_shortest_path": 3, 46 | "radius_sp": 500.0, 47 | "radius_growth_sp": 50.0, 48 | "walk_speed": 1.4, 49 | "scale_factor_sp": 10, 50 | "algorithm": "astar", 51 | "decision_model": "DummyDecisionModel", 52 | "available_mobility_services": [ 53 | "WALK", 54 | "PersonalCar" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/mnms/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class LOGLEVEL(): 5 | CRITICAL = 50 6 | ERROR = 40 7 | WARNING = 30 8 | INFO = 20 9 | DEBUG = 10 10 | NOTSET = 0 11 | 12 | 13 | def create_logger(logname, 14 | base_level=LOGLEVEL.WARNING, 15 | # stream_level=LOGLEVEL.INFO, 16 | ): 17 | # format = f'%(levelname)s(%(name)s): %(message)s' 18 | logger = logging.getLogger(logname) 19 | logger.setLevel(base_level) 20 | # formatter = logging.Formatter(format) 21 | # stream = logging.StreamHandler() 22 | # stream.setLevel(stream_level) 23 | # stream.setFormatter(formatter) 24 | # logger.addHandler(stream) 25 | logger.propagate = False 26 | return logger 27 | 28 | 29 | def get_all_mnms_logger(): 30 | return [logging.getLogger(name) for name in logging.root.manager.loggerDict if name.startswith('mnms')] 31 | 32 | 33 | def get_logger(logger_name): 34 | return logging.getLogger(logger_name) 35 | 36 | 37 | def set_mnms_logger_level(level, loggers=[]): 38 | [logging.getLogger(logger).setLevel(level) if isinstance(logger, str) else logger.setLevel(level) for logger in loggers] 39 | 40 | 41 | def set_all_mnms_logger_level(level): 42 | set_mnms_logger_level(level, get_all_mnms_logger()) 43 | 44 | 45 | def attach_log_file(filename:str, file_level=LOGLEVEL.INFO): 46 | loggers = get_all_mnms_logger() 47 | file_handler = logging.FileHandler(filename, mode="w") 48 | file_handler.setLevel(file_level) 49 | format = f'%(levelname)s(%(name)s): %(message)s' 50 | formatter = logging.Formatter(format) 51 | file_handler.setFormatter(formatter) 52 | for l in loggers: 53 | l.addHandler(file_handler) -------------------------------------------------------------------------------- /src/mnms/vehicles/fleet.py: -------------------------------------------------------------------------------- 1 | from typing import Type, Dict, Optional, List 2 | 3 | from mnms.vehicles.manager import VehicleManager 4 | from mnms.vehicles.veh_type import Vehicle, VehicleActivity, VehicleActivityStop 5 | 6 | 7 | class FleetManager(object): 8 | def __init__(self, 9 | veh_type: Type[Vehicle], 10 | mobility_service: str, 11 | is_personal: bool): 12 | """ 13 | Manage a fleet of Vehicles 14 | 15 | Args: 16 | -veh_type: Type of vehicle 17 | -mobility_service: the associated mobility service 18 | -is_personal: bool specifying of the fleet manages personal vehicles or not 19 | """ 20 | self.__veh_manager = VehicleManager() 21 | self.vehicles: Dict[str, Vehicle] = dict() 22 | self._constructor: Type[Vehicle] = veh_type 23 | self._mobility_service = mobility_service 24 | self._is_personal = is_personal 25 | 26 | def create_vehicle(self, node: str, capacity: int, activities: Optional[List[VehicleActivity]]): 27 | new_veh = self._constructor(node, capacity, self._mobility_service, self._is_personal, activities=activities) 28 | self.vehicles[new_veh.id] = new_veh 29 | self.__veh_manager.add_vehicle(new_veh) 30 | return new_veh 31 | 32 | def create_waiting_vehicle(self, node: str, capacity: int): 33 | return self.create_vehicle(node, capacity, [VehicleActivityStop(node, is_done=False)]) 34 | 35 | def delete_vehicle(self, vehid:str): 36 | self.__veh_manager.remove_vehicle(self.vehicles[vehid]) 37 | del self.vehicles[vehid] 38 | 39 | def vehicle_type(self): 40 | return self._constructor.__name__ if self._constructor is not None else None 41 | 42 | -------------------------------------------------------------------------------- /src/mnms/tools/progress.py: -------------------------------------------------------------------------------- 1 | from math import ceil 2 | from time import perf_counter_ns 3 | 4 | 5 | def _format(timing): 6 | timing = float(timing) 7 | if timing < 6e10: 8 | return f"{int(timing / 1e9)} s" 9 | elif timing < 3.6e+12: 10 | return f"{int(timing / 6e10)} min" 11 | else: 12 | return f"{int(timing / 3.6e+12)} hours" 13 | 14 | 15 | class ProgressBar(object): 16 | def __init__(self, stop: int, start=0, text='Run', size_bar=20, item='■'): 17 | self._max = stop 18 | self._index = start 19 | self._text = text 20 | self._size_bar = size_bar 21 | self._item = item 22 | 23 | self._bar = None 24 | self._ptime = None 25 | self._mean_time = 0 26 | 27 | def update(self): 28 | timing = perf_counter_ns() 29 | cur = (self._index/self._max)*self._size_bar 30 | nb_hash = ceil(cur) 31 | perc = round(self._index/self._max*100) 32 | prog = self._item*nb_hash+' '*(self._size_bar-nb_hash) 33 | self._bar = f"\r{self._text} |{prog}| {perc} %" 34 | 35 | if self._ptime is not None: 36 | new_iter_time = timing - self._ptime 37 | self._mean_time = self._mean_time + (new_iter_time - self._mean_time)/self._index 38 | remaining_time = self._mean_time*(self._max - self._index) 39 | self._bar += f" | remain ~ {_format(remaining_time)}" 40 | 41 | self._index += 1 42 | self._ptime = perf_counter_ns() 43 | 44 | def show(self): 45 | print(self._bar, end='', flush=True) 46 | 47 | def end(self) -> None: 48 | print("") 49 | 50 | def execute(self, func, *args, **kwargs): 51 | func(*args, **kwargs) 52 | self.update() 53 | self.show() 54 | 55 | 56 | if __name__ == '__main__': 57 | from time import sleep 58 | 59 | n = 100 60 | p = ProgressBar(n) 61 | for i in range(n): 62 | p.execute(sleep, 1) 63 | -------------------------------------------------------------------------------- /tests/tools/test_time.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from decimal import Decimal 3 | 4 | from mnms.time import Time, Dt 5 | 6 | 7 | class TestTime(unittest.TestCase): 8 | def setUp(self) -> None: 9 | pass 10 | 11 | def tearDown(self) -> None: 12 | pass 13 | 14 | def test_time(self): 15 | t = Time("07:34:23.67") 16 | self.assertEqual(7, t._hours) 17 | self.assertEqual(34, t._minutes) 18 | self.assertAlmostEqual(23.67, t.seconds) 19 | 20 | def test_time_from_seconds(self): 21 | t = Time.from_seconds(12345) 22 | self.assertEqual(3, t.hours) 23 | self.assertEqual(25, t.minutes) 24 | self.assertAlmostEqual(45, t.seconds) 25 | 26 | def test_time_operator(self): 27 | t1 = Time("07:34:23.67") 28 | t2 = Time("07:34:23.69") 29 | 30 | self.assertTrue(t1 < t2) 31 | self.assertTrue(t1 <= t2) 32 | self.assertTrue(t2 > t1) 33 | self.assertTrue(t2 >= t1) 34 | 35 | self.assertTrue(t1 < t2) 36 | self.assertTrue(t2 > t1) 37 | 38 | t2 = Time("07:34:23.67") 39 | self.assertTrue(t1 >= t2) 40 | self.assertTrue(t1 <= t2) 41 | 42 | 43 | class TestDt(unittest.TestCase): 44 | def setUp(self) -> None: 45 | pass 46 | 47 | def tearDown(self) -> None: 48 | pass 49 | 50 | def test_dt(self): 51 | dt = Dt(12, 35, 13.45) 52 | 53 | self.assertEqual(12, dt._hours) 54 | self.assertEqual(35, dt._minutes) 55 | self.assertAlmostEqual(Decimal(13.45), dt._seconds) 56 | 57 | dt = Dt(12, 135, 73.45) 58 | 59 | self.assertEqual(14, dt._hours) 60 | self.assertEqual(16, dt._minutes) 61 | self.assertAlmostEqual(Decimal(13.45), dt._seconds) 62 | 63 | def test_to_sec(self): 64 | dt = Dt(12, 35, 13.45) 65 | self.assertAlmostEqual(12*3600+35*60+13.45, dt.to_seconds()) 66 | 67 | def test_mul_dt(self): 68 | dt = Dt(12, 35, 13.45)*2 69 | self.assertEqual(25, dt._hours) 70 | self.assertEqual(10, dt._minutes) 71 | self.assertAlmostEqual(Decimal(13.45*2), dt._seconds) -------------------------------------------------------------------------------- /tests/graph/test_roads.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from dataclasses import asdict 3 | 4 | from mnms.graph.road import RoadDescriptor 5 | from mnms.graph.zone import Zone 6 | 7 | 8 | class TestLayers(unittest.TestCase): 9 | def setUp(self): 10 | """Initiates the test. 11 | """ 12 | 13 | self.roads = RoadDescriptor() 14 | self.roads.register_node("0", [0, 0]) 15 | self.roads.register_node("1", [1, 0]) 16 | self.roads.register_node("2", [2, 0]) 17 | 18 | self.roads.register_section("0_1", "0", "1", 1) 19 | self.roads.register_section("1_2", "1", "2", 1) 20 | 21 | self.roads.register_stop("S0", "0_1", 0.4) 22 | self.roads.register_stop("S1", "1_2", 0.9) 23 | 24 | self.roads.add_zone(Zone("Z0", {"0_1"}, [])) 25 | self.roads.add_zone(Zone("Z1", {"1_2"}, [])) 26 | 27 | def tearDown(self): 28 | """Concludes and closes the test. 29 | """ 30 | 31 | def test_fill(self): 32 | self.assertIn("0", self.roads.nodes) 33 | self.assertIn("1", self.roads.nodes) 34 | self.assertIn("2", self.roads.nodes) 35 | 36 | self.assertListEqual([0, 0], self.roads.nodes["0"].position.tolist()) 37 | self.assertListEqual([1, 0], self.roads.nodes["1"].position.tolist()) 38 | self.assertListEqual([2, 0], self.roads.nodes["2"].position.tolist()) 39 | 40 | self.assertIn("0_1", self.roads.sections) 41 | self.assertIn("1_2", self.roads.sections) 42 | 43 | self.assertEqual("Z0", self.roads.sections["0_1"].zone) 44 | self.assertEqual("Z1", self.roads.sections["1_2"].zone) 45 | 46 | self.assertIn("S0", self.roads.stops) 47 | self.assertIn("S1", self.roads.stops) 48 | 49 | self.assertAlmostEqual([0.4, 0], self.roads.stops["S0"].absolute_position.tolist()) 50 | self.assertAlmostEqual([1.9, 0], self.roads.stops["S1"].absolute_position.tolist()) 51 | 52 | def test_serialization(self): 53 | data_dict = self.roads.__dump__() 54 | new_roads = RoadDescriptor.__load__(data_dict) 55 | 56 | self.assertListEqual(list(data_dict["NODES"].keys()), list(new_roads.nodes.keys())) 57 | self.assertListEqual(list(data_dict["SECTIONS"].keys()), list(new_roads.sections.keys())) 58 | self.assertListEqual(list(data_dict["STOPS"].keys()), list(new_roads.stops.keys())) 59 | self.assertListEqual(list(data_dict["ZONES"].keys()), list(new_roads.zones.keys())) 60 | -------------------------------------------------------------------------------- /examples/Lyon63V/run_Lyon63V_CarOnly.py: -------------------------------------------------------------------------------- 1 | from mnms.simulation import Supervisor 2 | from mnms.demand import CSVDemandManager 3 | from mnms.flow.MFD import Reservoir, MFDFlowMotor 4 | from mnms.log import attach_log_file, LOGLEVEL, set_mnms_logger_level, set_all_mnms_logger_level 5 | from mnms.time import Time, Dt 6 | from mnms.io.graph import load_graph, load_odlayer 7 | from mnms.travel_decision.logit import LogitDecisionModel 8 | from mnms.tools.observer import CSVUserObserver, CSVVehicleObserver 9 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 10 | 11 | indir = "INPUTS" 12 | outdir = "OUTPUTS" 13 | 14 | set_all_mnms_logger_level(LOGLEVEL.INFO) 15 | attach_log_file(outdir+'/simulation.log') 16 | 17 | def calculate_V_MFD(acc): 18 | #V = 10.3*(1-N/57000) # data from fit prop 19 | V = 0 # data from fit dsty 20 | N = acc["CAR"] 21 | if N<18000: 22 | V=11.5-N*6/18000 23 | elif N<55000: 24 | V=11.5-6 - (N-18000)*4.5/(55000-18000) 25 | elif N<80000: 26 | V= 11.5-6-4.5-(N-55000)*1/(80000-55000) 27 | #V = 11.5*(1-N/60000) 28 | V = max(V,0.001) # min speed to avoid gridlock 29 | return {"CAR": V} 30 | 31 | if __name__ == '__main__': 32 | mmgraph = load_graph(indir + "/network-Lyon63V_CarOnly.json") 33 | 34 | odlayer = load_odlayer(indir + "/odlayer-Lyon63V.json") 35 | mmgraph.add_origin_destination_layer(odlayer) 36 | mmgraph.connect_origindestination_layers(250) 37 | 38 | veh_observer = CSVVehicleObserver(outdir+"/veh.csv") 39 | 40 | personal_car = PersonalMobilityService() 41 | personal_car.attach_vehicle_observer(veh_observer) 42 | mmgraph.layers["CAR"].add_mobility_service(personal_car) 43 | 44 | demand_file_name = indir + "/demand-Lyon63V-coords.csv" 45 | demand = CSVDemandManager(demand_file_name) 46 | demand.add_user_observer(CSVUserObserver(outdir+"/user.csv"), user_ids="all") 47 | 48 | flow_motor = MFDFlowMotor(outfile=outdir+"/flow.csv") 49 | 50 | for k, res in mmgraph.roads.zones.items(): 51 | flow_motor.add_reservoir(Reservoir(res, ["CAR"], calculate_V_MFD)) 52 | 53 | travel_decision = LogitDecisionModel(mmgraph, outfile=outdir+"/path.csv", n_shortest_path=3) 54 | 55 | supervisor = Supervisor(graph=mmgraph, 56 | flow_motor=flow_motor, 57 | demand=demand, 58 | decision_model=travel_decision, 59 | outfile=outdir + "/travel_time_link.csv") 60 | 61 | supervisor.run(Time('06:30:00'), Time('10:30:00'), Dt(minutes=1), 10) 62 | -------------------------------------------------------------------------------- /src/mnms/vehicles/manager.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Set, List 2 | from collections import defaultdict 3 | 4 | from mnms.vehicles.veh_type import Vehicle 5 | from mnms.log import create_logger 6 | 7 | log = create_logger(__name__) 8 | 9 | 10 | class VehicleManager(object): 11 | 12 | # Class attribute (shared by all instances) 13 | _vehicles: Dict[str, Vehicle] = dict() # id_veh, Vehicle 14 | _type_vehicles: Dict[str, Set[str]] = defaultdict(set) 15 | _new_vehicles: List[Vehicle] = list() 16 | 17 | def __reduce__(self): 18 | """ 19 | Customize the pickling process by ensuring class attributes are also pickled. 20 | """ 21 | # return a tuple of (constructor, args, state) 22 | state = self.__dict__.copy() # Copy instance attributes 23 | state['_vehicles'] = VehicleManager._vehicles # Add class attribute explicitly 24 | state['_type_vehicles'] = VehicleManager._type_vehicles # Add class attribute explicitly 25 | state['_new_vehicles'] = VehicleManager._new_vehicles # Add class attribute explicitly 26 | return (self.__class__, () , state) 27 | 28 | def __setstate__(self, state): 29 | """ 30 | This method is used during unpickling to restore the object’s state. 31 | """ 32 | self.__dict__.update(state) # Restore instance attributes 33 | VehicleManager._vehicles = state['_vehicles'] # Restore class attribute 34 | VehicleManager._type_vehicles = state['_type_vehicles'] # Restore class attribute 35 | VehicleManager._new_vehicles = state['_new_vehicles'] # Restore class attribute 36 | 37 | @property 38 | def number(self): 39 | return len(self._vehicles) 40 | 41 | def add_vehicle(self, veh:Vehicle) -> None: 42 | self.add_new_vehicle(veh) 43 | VehicleManager._vehicles[veh._global_id] = veh 44 | VehicleManager._type_vehicles[veh.type].add(veh._global_id) 45 | 46 | def add_new_vehicle(self, veh): 47 | VehicleManager._new_vehicles.append(veh) 48 | 49 | def remove_vehicle(self, veh:Vehicle) -> None: 50 | log.info(f"Deleting {veh}") 51 | del VehicleManager._vehicles[veh._global_id] 52 | VehicleManager._type_vehicles[veh.type].remove(veh._global_id) 53 | 54 | @property 55 | def has_new_vehicles(self): 56 | return bool(VehicleManager._new_vehicles) 57 | 58 | @classmethod 59 | def empty(cls): 60 | VehicleManager._vehicles = dict() 61 | VehicleManager._type_vehicles = defaultdict(set) 62 | VehicleManager._new_vehicles = list() 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MnMS :candy: 2 | 3 | MnMS (**M**ultimodal **N**etwork **M**odelling and **S**imulation) is a multimodal dynamic traffic simulator designed for a large urban scale. It results from all research activities of the ERC MAGnUM project. Further extensions related to on-demand mobility have been developped with the DIT4TraM project. 4 | 5 | MnMS is an agent-based dynamic simulator for urban mobility. Travelers make mode and route choices considering all multimodal options on the city transportation network, including traditional modes, such as personal cars or public transportation, and new mobility services, such as ride-hailing, ride-sharing, or vehicle sharing. Vehicles motion is governed by regional multimodal MFD (Macroscopic Fundamental Diagram) curves, so all vehicles of the same type (car, bus, etc.) share the same speed within a specific region at a given time. The adoption of this traffic flow modeling framework allows to address at large urban scale timely research topics such as the management of new mobility services (operation, optimization, regulation), the design of regulatory policies taking into account the multiple stakeholders setting of today's urban transportation system, and beyond! 6 | 7 | ## Installation 8 | 9 | ### From sources 10 | 11 | MnMS relies on [HiPOP](https://github.com/EMob-Lab/HiPOP.git), make sure to clone it before going through the following installation guidelines. 12 | 13 | Clone MnMS. Then, using [conda](https://docs.conda.io/en/latest/miniconda.html), create and configure a new environment: 14 | 15 | ````bash 16 | cd MnMS 17 | conda env create -f conda/env.yaml 18 | ```` 19 | 20 | Activate it: 21 | ````bash 22 | conda activate mnms 23 | ```` 24 | 25 | Install the MnMS and HiPOP sources in the activated environment: 26 | 27 | ````bash 28 | python -m pip install -e . 29 | cd $path_to_HiPOP$/HiPOP 30 | python python/install_cpp.py 31 | python -m pip install python/ 32 | ```` 33 | 34 | ## Tutorials and examples 35 | 36 | Tutorials can be found in the doc/tutorials folder as jupyter notebook. Some simulation examples can be found in the examples folder. 37 | 38 | ## Tests 39 | 40 | To launch tests run the following command at the root of the project: 41 | ```bash 42 | pytest tests --cov=mnms -v 43 | ``` 44 | 45 | ## Documentation 46 | 47 | ### Built 48 | 49 | To build the documentation using mkdocs, first update your conda environment with the doc dependencies: 50 | 51 | ```bash 52 | conda activate mnms 53 | conda env update --file conda/doc.yaml 54 | ``` 55 | 56 | Then build the doc: 57 | 58 | ```bash 59 | mkdocs serve 60 | ``` 61 | 62 | ### Detailed 63 | 64 | The detailed documentation is available [there](https://github.com/EMob-Lab/MnMS/blob/develop/doc/MnMS_detailed_documentation.pdf). 65 | -------------------------------------------------------------------------------- /examples/mulitmodal_nested_manhattan/inputs/mobility_services_events_graphs.json: -------------------------------------------------------------------------------- 1 | { 2 | "G1": { 3 | "None": { 4 | "DEPARTURE": [ 5 | "CAR", 6 | "BUS", 7 | "METRO", 8 | "TRAIN", 9 | "RIDEHAILING1", 10 | "RIDEHAILING2" 11 | ] 12 | }, 13 | "CAR BUS METRO TRAIN RIDEHAILING1 RIDEHAILING2": { 14 | "RIDEHAILING1": [ 15 | "CAR", 16 | "BUS", 17 | "METRO", 18 | "TRAIN" 19 | ], 20 | "RIDEHAILING2": [ 21 | "CAR", 22 | "BUS", 23 | "METRO", 24 | "TRAIN", 25 | "RIDEHAILING1" 26 | ] 27 | }, 28 | "CAR BUS METRO TRAIN RIDEHAILING1": { 29 | "RIDEHAILING1": [ 30 | "CAR", 31 | "BUS", 32 | "METRO", 33 | "TRAIN" 34 | ] 35 | }, 36 | "BUS METRO TRAIN RIDEHAILING1": { 37 | "RIDEHAILING1": [ 38 | "BUS", 39 | "METRO", 40 | "TRAIN" 41 | ] 42 | }, 43 | "BUS METRO TRAIN RIDEHAILING2": { 44 | "RIDEHAILING2": [ 45 | "BUS", 46 | "METRO", 47 | "TRAIN" 48 | ] 49 | }, 50 | "BUS METRO TRAIN RIDEHAILING1 RIDEHAILING2": { 51 | "RIDEHAILING1": [ 52 | "BUS", 53 | "METRO", 54 | "TRAIN" 55 | ], 56 | "RIDEHAILING2": [ 57 | "BUS", 58 | "METRO", 59 | "TRAIN", 60 | "RIDEHAILING1" 61 | ] 62 | } 63 | }, 64 | "G2": { 65 | "None": { 66 | "DEPARTURE": [ 67 | "BUS", 68 | "METRO", 69 | "TRAIN", 70 | "RIDEHAILING1", 71 | "RIDEHAILING2" 72 | ] 73 | }, 74 | "BUS METRO TRAIN RIDEHAILING1 RIDEHAILING2": { 75 | "RIDEHAILING1": [ 76 | "BUS", 77 | "METRO", 78 | "TRAIN", 79 | "RIDEHAILING2" 80 | ], 81 | "RIDEHAILING2": [ 82 | "BUS", 83 | "METRO", 84 | "TRAIN", 85 | "RIDEHAILING1" 86 | ] 87 | }, 88 | "BUS METRO TRAIN RIDEHAILING2": { 89 | "RIDEHAILING2": [ 90 | "BUS", 91 | "METRO", 92 | "TRAIN" 93 | ] 94 | }, 95 | "BUS METRO TRAIN RIDEHAILING1": { 96 | "RIDEHAILING1": [ 97 | "BUS", 98 | "METRO", 99 | "TRAIN" 100 | ] 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /examples/snapshot/run_monolink.py: -------------------------------------------------------------------------------- 1 | from mnms.simulation import Supervisor 2 | from mnms.generation.roads import generate_line_road 3 | from mnms.graph.layers import MultiLayerGraph, CarLayer 4 | from mnms.flow.MFD import Reservoir, MFDFlowMotor 5 | from mnms.log import attach_log_file, LOGLEVEL, get_logger, set_all_mnms_logger_level 6 | from mnms.time import Time, Dt 7 | from mnms.travel_decision.logit import LogitDecisionModel 8 | from mnms.tools.observer import CSVUserObserver, CSVVehicleObserver 9 | from mnms.graph.specific_layers import OriginDestinationLayer 10 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 11 | from mnms.demand import BaseDemandManager, User 12 | from mnms.graph.zone import Zone, construct_zone_from_sections 13 | from mnms.io.graph import save_graph 14 | import logging 15 | import pandas as pd 16 | import os 17 | 18 | def mfdspeed(dacc): 19 | dspeed = {'CAR': 13.8} 20 | return dspeed 21 | 22 | if __name__ == '__main__': 23 | 24 | outdir = "OUTPUTS" 25 | 26 | # Outputs 27 | outdir_path = os.getcwd() + '/' + outdir 28 | if not os.path.isdir(outdir_path): 29 | os.mkdir(outdir_path) 30 | 31 | set_all_mnms_logger_level(LOGLEVEL.INFO) 32 | get_logger("mnms.graph.shortest_path").setLevel(LOGLEVEL.WARNING) 33 | attach_log_file(outdir_path + '/simulation.log') 34 | 35 | # Network creation 36 | roads = generate_line_road([0,0], [1500,0], 2, 'Z') 37 | 38 | pv = PersonalMobilityService() 39 | pv.attach_vehicle_observer(CSVVehicleObserver(outdir_path+"/veh.csv")) 40 | car_layer = CarLayer(roads, services=[pv]) 41 | 42 | car_layer.create_node("CAR_0", "0") 43 | car_layer.create_node("CAR_1", "1") 44 | car_layer.create_link("CAR_0_1", "CAR_0", "CAR_1", {}, ["0_1"]) 45 | car_layer.create_link("CAR_1_0", "CAR_1", "CAR_0", {}, ["1_0"]) 46 | 47 | odlayer = OriginDestinationLayer() 48 | odlayer.create_origin_node('O',[-200,0]) 49 | odlayer.create_destination_node('D',[1700,0]) 50 | 51 | mlgraph = MultiLayerGraph([car_layer], 52 | odlayer, 53 | 250) 54 | 55 | # Demand 56 | demand = BaseDemandManager([User("U0", [-230, 0], [1730, 0], Time("08:16:00"))]) 57 | demand.add_user_observer(CSVUserObserver(outdir_path+"/user.csv"), user_ids="all") 58 | 59 | flow_motor = MFDFlowMotor(outfile=outdir_path+"/flow.csv") 60 | 61 | flow_motor.add_reservoir(Reservoir(roads.zones["Z"], ['CAR'], mfdspeed)) 62 | 63 | travel_decision = LogitDecisionModel(mlgraph, outfile=outdir_path+"/path.csv",verbose_file=True) 64 | 65 | save_graph(mlgraph, outdir_path+'/monolink.json') 66 | 67 | supervisor = Supervisor(graph=mlgraph, 68 | flow_motor=flow_motor, 69 | demand=demand, 70 | decision_model=travel_decision, 71 | outfile=outdir_path + "/travel_time_link.csv") 72 | 73 | supervisor.run(Time('08:15:00'), Time('08:30:00'), Dt(seconds=1), 10, snapshot=True, snapshot_folder=outdir_path) -------------------------------------------------------------------------------- /src/mnms/graph/zone.py: -------------------------------------------------------------------------------- 1 | from typing import Set, List, Annotated 2 | from dataclasses import dataclass 3 | 4 | import numpy as np 5 | 6 | from mnms.tools.geometry import points_in_polygon 7 | 8 | Point = Annotated[List[float], 2] 9 | PointList = List[Point] 10 | 11 | 12 | @dataclass(slots=True) 13 | class Zone(object): 14 | id: str 15 | sections: Set[str] 16 | contour: PointList 17 | 18 | def is_inside(self, points: List[Point]): 19 | return points_in_polygon(self.contour, points) 20 | 21 | def centroid(self): 22 | arr = np.array(self.contour) 23 | length = arr.shape[0] 24 | sum_x = np.sum(arr[:, 0]) 25 | sum_y = np.sum(arr[:, 1]) 26 | return np.array([sum_x/length, sum_y/length]) 27 | 28 | @dataclass 29 | class MLZone(object): 30 | id: str 31 | links: Set[str] 32 | contour: PointList 33 | 34 | @dataclass 35 | class LayerZone(object): 36 | """Zone for the AbstractLayer objects : it gathers links belonging to the same 37 | layer. 38 | """ 39 | id: str 40 | links: Set[str] 41 | contour : PointList 42 | detour_ratio : float = 1.343 43 | 44 | def is_inside(self, points: List[Point]): 45 | return points_in_polygon(self.contour, points) 46 | 47 | def construct_zone_from_contour(roads: "RoadDescriptor", id: str, contour: PointList, graph=None, zone_type='Zone'): 48 | sections = roads.sections if graph is None else graph.links 49 | nodes = roads.nodes if graph is None else graph.nodes 50 | section_centers = [0 for _ in range(len(sections))] 51 | section_ids = np.array([s for s in sections]) 52 | 53 | for i, data in enumerate(sections.values()): 54 | up_pos = nodes[data.upstream].position 55 | down_pos = nodes[data.downstream].position 56 | section_centers[i] = np.array([(up_pos[0] + down_pos[0]) / 2., (up_pos[1] + down_pos[1]) / 2.]) 57 | 58 | section_centers = np.array(section_centers) 59 | 60 | contour_array = np.array(contour) 61 | mask = points_in_polygon(contour_array, section_centers) 62 | zone_links = section_ids[mask].tolist() 63 | if graph is None: 64 | assert zone_type == 'Zone', 'Inconsistent arguments in construct_zone_from_contour function...' 65 | return Zone(id, zone_links, contour) 66 | else: 67 | assert zone_type in ['MLZone', 'LayerZone'], 'Unknown zone type argument in construct_zone_from_contour function...' 68 | if zone_type == 'MLZone': 69 | return MLZone(id, zone_links, contour) 70 | else: 71 | return LayerZone(id, zone_links, contour) 72 | 73 | 74 | def construct_zone_from_sections(roads: "RoadDescriptor", _id: str, sections: List[str]): 75 | nodes = [] 76 | for sec in sections: 77 | section = roads.sections[sec] 78 | nodes.append(roads.nodes[section.upstream].position) 79 | nodes.append(roads.nodes[section.downstream].position) 80 | 81 | nodes = np.array(nodes) 82 | 83 | min_x, min_y = np.min(nodes, axis=0) 84 | max_x, max_y = np.max(nodes, axis=0) 85 | bbox = [[min_x, min_y], [max_x, min_y], [max_x, max_y], [min_x, max_y]] 86 | return Zone(_id, set(sections), bbox) 87 | -------------------------------------------------------------------------------- /tests/io/test_io_graph.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from tempfile import TemporaryDirectory 3 | 4 | from mnms.generation.layers import generate_matching_origin_destination_layer 5 | from mnms.graph.layers import CarLayer, BusLayer, MultiLayerGraph 6 | from mnms.graph.road import RoadDescriptor 7 | from mnms.graph.zone import Zone 8 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 9 | from mnms.time import TimeTable, Dt 10 | from mnms.io.graph import save_graph, load_graph 11 | 12 | 13 | class TestIOGraph(unittest.TestCase): 14 | def setUp(self): 15 | """Initiates the test. 16 | """ 17 | self.roads = RoadDescriptor() 18 | self.roads.register_node("0", [0, 0]) 19 | self.roads.register_node("1", [1, 0]) 20 | self.roads.register_node("2", [2, 0]) 21 | 22 | self.roads.register_section("0_1", "0", "1", 1) 23 | self.roads.register_section("1_2", "1", "2", 1) 24 | 25 | self.roads.register_stop("S0", "0_1", 0.4) 26 | self.roads.register_stop("S1", "1_2", 0.9) 27 | 28 | self.roads.add_zone(Zone("Z0", {"0_1"}, [])) 29 | self.roads.add_zone(Zone("Z1", {"1_2"}, [])) 30 | 31 | car_layer = CarLayer(self.roads, 32 | services=[PersonalMobilityService()]) 33 | 34 | car_layer.create_node("C0", "0") 35 | car_layer.create_node("C1", "1", {"0": {"2"}}) 36 | car_layer.create_node("C2", "2") 37 | car_layer.create_link("C0_C1", "C0", "C1", {"PersonalVehicle": {"test": 34.3}}, ["0_1"]) 38 | 39 | bus_layer = BusLayer(self.roads) 40 | 41 | bus_layer.create_line("L0", 42 | ["S0", "S1"], 43 | [["0_1", "1_2"]], 44 | TimeTable.create_table_freq("08:00:00", "18:00:00", Dt(minutes=10)), 45 | True) 46 | 47 | odlayer = generate_matching_origin_destination_layer(self.roads) 48 | 49 | self.mlgraph = MultiLayerGraph([car_layer, bus_layer], 50 | odlayer, 51 | 1e-3) 52 | 53 | self.mlgraph.connect_layers("TEST", "C0", "L0_S0", 156, {"test": 1.43}) 54 | 55 | def tearDown(self): 56 | """Concludes and closes the test. 57 | """ 58 | 59 | def test_read_write(self): 60 | tempdir = TemporaryDirectory() 61 | tempdir_name = tempdir.name 62 | 63 | save_graph(self.mlgraph, tempdir_name+"/graph.json") 64 | new_graph = load_graph(tempdir_name+"/graph.json") 65 | 66 | odlayer = generate_matching_origin_destination_layer(self.roads) 67 | new_graph.add_origin_destination_layer(odlayer) 68 | new_graph.connect_origindestination_layers(1e-3) 69 | 70 | self.assertEqual(set(self.mlgraph.graph.nodes.keys()), set(new_graph.graph.nodes.keys())) 71 | self.assertEqual(set(self.mlgraph.graph.links.keys()), set(new_graph.graph.links.keys())) 72 | self.assertDictEqual(self.mlgraph.transitlayer.links, new_graph.transitlayer.links) 73 | 74 | try: 75 | tempdir.cleanup() 76 | except: 77 | pass 78 | -------------------------------------------------------------------------------- /examples/manhattan/run.py: -------------------------------------------------------------------------------- 1 | ############### 2 | ### Imports ### 3 | ############### 4 | ## Casuals 5 | import pathlib 6 | 7 | ## MnMS 8 | from mnms.generation.roads import generate_manhattan_road 9 | from mnms.generation.layers import generate_layer_from_roads, generate_grid_origin_destination_layer 10 | from mnms.graph.layers import MultiLayerGraph 11 | from mnms.demand.manager import CSVDemandManager 12 | from mnms.log import set_all_mnms_logger_level, LOGLEVEL 13 | from mnms.travel_decision.dummy import DummyDecisionModel 14 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 15 | from mnms.flow.MFD import MFDFlowMotor, Reservoir 16 | from mnms.simulation import Supervisor 17 | from mnms.time import Time, Dt 18 | from mnms.tools.observer import CSVUserObserver, CSVVehicleObserver 19 | 20 | ################## 21 | ### Parameters ### 22 | ################## 23 | 24 | demand_file = pathlib.Path(__file__).parent.joinpath('demand.csv').resolve() 25 | log_file = pathlib.Path(__file__).parent.joinpath('sim.log').resolve() 26 | n_nodes_per_dir = 3 27 | mesh_size = 100 # m 28 | n_odnodes_x = 3 29 | n_odnodes_y = 3 30 | odlayer_xmin = 0 31 | odlayer_ymin = 0 32 | odlayer_xmax = 300 # m 33 | odlayer_ymax = 300 # m 34 | odlayer_connection_dist = 1e-3 # m 35 | def mfdspeed(dacc): 36 | dspeed = {'CAR': 3} # m/s 37 | return dspeed 38 | tstart = Time("07:00:00") 39 | tend = Time("07:03:03") 40 | dt_flow = Dt(seconds=10) 41 | affectation_factor = 10 42 | 43 | ######################### 44 | ### Scenario creation ### 45 | ######################### 46 | 47 | #### RoadDescriptor #### 48 | road_db = generate_manhattan_road(n_nodes_per_dir, mesh_size) # one zone automatically generated 49 | 50 | #### MlGraph #### 51 | personal_car = PersonalMobilityService() 52 | personal_car.attach_vehicle_observer(CSVVehicleObserver("veh.csv")) 53 | car_layer = generate_layer_from_roads(road_db, 54 | 'CAR', 55 | mobility_services=[personal_car]) 56 | 57 | odlayer = generate_grid_origin_destination_layer(odlayer_xmin, odlayer_ymin, odlayer_xmax, 58 | odlayer_ymax, n_odnodes_x, n_odnodes_y) 59 | 60 | mlgraph = MultiLayerGraph([car_layer], 61 | odlayer, 62 | odlayer_connection_dist) 63 | 64 | # save_graph(mlgraph, demand_file.parent.joinpath('graph.json')) 65 | 66 | # load_graph(demand_file.parent.joinpath('graph.json')) 67 | 68 | 69 | #### Demand #### 70 | demand = CSVDemandManager(demand_file) 71 | demand.add_user_observer(CSVUserObserver('user.csv')) 72 | 73 | #### Decision model #### 74 | decision_model = DummyDecisionModel(mlgraph, outfile="path.csv") 75 | 76 | #### Flow motor #### 77 | flow_motor = MFDFlowMotor() 78 | flow_motor.add_reservoir(Reservoir(road_db.zones["RES"], 'CAR', mfdspeed)) 79 | 80 | #### Supervisor #### 81 | supervisor = Supervisor(mlgraph, 82 | demand, 83 | flow_motor, 84 | decision_model, 85 | logfile=log_file, 86 | loglevel=LOGLEVEL.INFO) 87 | 88 | ###################### 89 | ### Run simulation ### 90 | ###################### 91 | 92 | set_all_mnms_logger_level(LOGLEVEL.INFO) 93 | 94 | supervisor.run(tstart, 95 | tend, 96 | dt_flow, 97 | affectation_factor) 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | 132 | #doc 133 | doc/_build/ 134 | doc/_dvlpt/ 135 | 136 | #pycharm 137 | .idea/ 138 | 139 | #OSX 140 | .DS_Store 141 | 142 | doc/tutorials/results/* 143 | docs/reference/* 144 | *.csv 145 | docs/Untitled.ipynb 146 | examples/vehicle_sharing/mlgraph.json 147 | examples/vehicle_sharing/mlgraph.json 148 | examples/on_demand_car/demand.csv 149 | examples/on_demand_car/graph.json 150 | examples/vehicle_sharing/free_floating_example.json 151 | Untitled.ipynb 152 | examples/vehicle_sharing/free_floating_example.json 153 | examples/vehicle_sharing/free_floating_example.json 154 | examples/vehicle_sharing/free_floating_example.json 155 | examples/vehicle_sharing/free_floating_example.json 156 | script/conversion/symuflow/Lyon_symuviainput_1.json 157 | script/validation/res.txt 158 | log.txt 159 | log.txt 160 | 161 | examples/snapshot/OUTPUTS/snapshot.graph 162 | -------------------------------------------------------------------------------- /src/mnms/generation/demand.py: -------------------------------------------------------------------------------- 1 | from operator import attrgetter 2 | from itertools import count 3 | import numpy as np 4 | from random import choice 5 | 6 | 7 | from hipop.shortest_path import dijkstra 8 | from mnms.demand.user import User 9 | from mnms.time import Time 10 | from mnms.demand.manager import BaseDemandManager 11 | 12 | 13 | def generate_random_demand(mlgraph: "MultiLayerGraph", 14 | nb_user: int, 15 | tstart="07:00:00", 16 | tend="18:00:00", 17 | min_cost=0, 18 | cost_path=None, 19 | distrib_time=np.random.uniform, 20 | repeat=1, seed=None) -> BaseDemandManager: 21 | """Create a random demand by using the extremities of the mobility_graph as origin destination pair, the departure 22 | time use a distribution function to generate the departure between tstart and tend. 23 | 24 | Args: 25 | mmgraph: The graph use to generate the demand 26 | tstart: Lower bound of departure time 27 | tend: Upper boumd of departure time 28 | min_cost: Minimal cost to accept an origin destination pair 29 | cost_path: The name of the cost to use for shortest path 30 | distrib_time: Distribution function to generate random departure dates 31 | repeat: Repeat each origin destination pair 32 | seed: Random seed 33 | 34 | Returns: 35 | The generated demand 36 | 37 | """ 38 | if cost_path is None: 39 | cost_path = "length" 40 | 41 | if seed is not None: 42 | np.random.seed(seed) 43 | 44 | tstart = Time(tstart).to_seconds() 45 | tend = Time(tend).to_seconds() 46 | 47 | demand = [] 48 | origins = list(mlgraph.odlayer.origins.keys()) 49 | destinations = list(mlgraph.odlayer.destinations.keys()) 50 | uid = count(0) 51 | 52 | graph = mlgraph.graph 53 | user_count = 0 54 | 55 | map_layer_services = {lid:list(layer.mobility_services.keys())[0] for lid, layer in mlgraph.layers.items()} 56 | map_layer_services["TRANSIT"] = "WALK" 57 | 58 | while user_count <= nb_user: 59 | unode = choice(origins) 60 | dnode = choice(destinations) 61 | try: 62 | _, path_cost = dijkstra(graph, unode, dnode, cost_path, map_layer_services) 63 | except ValueError as ex: 64 | log.error(f'HiPOP.Error: {ex}') 65 | sys.exit(-1) 66 | if min_cost <= path_cost < float('inf'): 67 | demand.extend([User(str(next(uid)), unode, dnode, Time.from_seconds(distrib_time(tstart, tend))) for _ in 68 | range(repeat)]) 69 | user_count+=repeat 70 | 71 | demand.sort(key=attrgetter('departure_time')) 72 | 73 | uid = count(0) 74 | for u in demand: 75 | u.id = str(next(uid)) 76 | return BaseDemandManager(demand) 77 | 78 | 79 | if __name__ == "__main__": 80 | 81 | from mnms.generation.mlgraph import generate_manhattan_passenger_car 82 | from mnms.io.graph import save_odlayer, save_graph 83 | 84 | mlgraph = generate_manhattan_passenger_car(20, 100) 85 | 86 | demand = generate_random_demand(mlgraph, 87 | 500, 88 | min_cost=300) 89 | 90 | demand.to_csv("random_demand_20x20.csv") 91 | save_odlayer(mlgraph.odlayer, "odlayer_20x20.json") 92 | save_graph(mlgraph, "manhattan_20x20.json") 93 | -------------------------------------------------------------------------------- /tests/mobility_services/test_personal_car.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import tempfile 3 | from pathlib import Path 4 | import pandas as pd 5 | 6 | from mnms.demand import BaseDemandManager, User 7 | from mnms.generation.roads import generate_manhattan_road 8 | from mnms.generation.layers import generate_layer_from_roads, generate_grid_origin_destination_layer 9 | from mnms.graph.layers import MultiLayerGraph 10 | 11 | from mnms.travel_decision.dummy import DummyDecisionModel 12 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 13 | from mnms.flow.MFD import MFDFlowMotor, Reservoir 14 | from mnms.simulation import Supervisor 15 | from mnms.time import Time, Dt 16 | from mnms.tools.observer import CSVUserObserver, CSVVehicleObserver 17 | from mnms.vehicles.manager import VehicleManager 18 | 19 | 20 | class TestPersonalCar(unittest.TestCase): 21 | def setUp(self): 22 | """Initiates the test. 23 | """ 24 | self.temp_dir_results = tempfile.TemporaryDirectory() 25 | self.dir_results = Path(self.temp_dir_results.name) 26 | 27 | 28 | road_db = generate_manhattan_road(3, 100) 29 | 30 | personal_car = PersonalMobilityService() 31 | personal_car.attach_vehicle_observer(CSVVehicleObserver(self.dir_results / "veh.csv")) 32 | 33 | car_layer = generate_layer_from_roads(road_db, 34 | 'CAR', 35 | mobility_services=[personal_car]) 36 | 37 | odlayer = generate_grid_origin_destination_layer(0, 0, 300, 300, 3, 3) 38 | # 39 | mlgraph = MultiLayerGraph([car_layer], 40 | odlayer, 41 | 1e-3) 42 | # 43 | # save_graph(mlgraph, cwd.parent.joinpath('graph.json')) 44 | # 45 | # load_graph(cwd.parent.joinpath('graph.json')) 46 | 47 | # Demand 48 | 49 | demand = BaseDemandManager([User("U0", [0, 0], [1000, 1000], Time("07:00:00"))]) 50 | demand.add_user_observer(CSVUserObserver(self.dir_results / 'user.csv')) 51 | 52 | # Decison Model 53 | 54 | decision_model = DummyDecisionModel(mlgraph, outfile=self.dir_results / "path.csv") 55 | 56 | # Flow Motor 57 | 58 | def mfdspeed(dacc): 59 | dspeed = {'CAR': 3} 60 | return dspeed 61 | 62 | flow_motor = MFDFlowMotor() 63 | flow_motor.add_reservoir(Reservoir(road_db.zones["RES"], ['CAR'], mfdspeed)) 64 | 65 | supervisor = Supervisor(mlgraph, 66 | demand, 67 | flow_motor, 68 | decision_model) 69 | 70 | self.flow_dt = Dt(seconds=10) 71 | supervisor.run(Time("07:00:00"), 72 | Time("07:03:00"), 73 | self.flow_dt, 74 | 10) 75 | 76 | def tearDown(self): 77 | """Concludes and closes the test. 78 | """ 79 | self.temp_dir_results.cleanup() 80 | VehicleManager.empty() 81 | 82 | def test_run_and_results(self): 83 | with open(self.dir_results / "user.csv") as f: 84 | df = pd.read_csv(f, sep=';') 85 | self.assertEqual(df['STATE'].iloc[-1], 'ARRIVED') 86 | arrival_time = Time(df['TIME'].iloc[-1]) 87 | self.assertGreater(arrival_time, Time('07:02:13').remove_time(self.flow_dt)) 88 | self.assertLess(arrival_time, Time('07:02:13').add_time(self.flow_dt)) 89 | -------------------------------------------------------------------------------- /examples/Lyon63V/run_Lyon63V.py: -------------------------------------------------------------------------------- 1 | from mnms.simulation import Supervisor 2 | from mnms.demand import CSVDemandManager 3 | from mnms.flow.MFD import Reservoir, MFDFlowMotor 4 | from mnms.log import attach_log_file, LOGLEVEL, set_mnms_logger_level, set_all_mnms_logger_level 5 | from mnms.time import Time, Dt 6 | from mnms.io.graph import load_graph, load_odlayer 7 | from mnms.travel_decision.logit import LogitDecisionModel 8 | from mnms.tools.observer import CSVUserObserver, CSVVehicleObserver 9 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 10 | from mnms.mobility_service.public_transport import PublicTransportMobilityService 11 | 12 | indir = "INPUTS" 13 | outdir = "OUTPUTS" 14 | 15 | set_all_mnms_logger_level(LOGLEVEL.INFO) 16 | #set_mnms_logger_level(LOGLEVEL.INFO, ["mnms.simulation","mnms.mobility_service.public_transport"]) 17 | attach_log_file(outdir+'/simulation.log') 18 | 19 | def calculate_V_MFD(acc): 20 | #V = 10.3*(1-N/57000) # data from fit prop 21 | V = 0 # data from fit dsty 22 | N = acc["CAR"] 23 | if N<18000: 24 | V=11.5-N*6/18000 25 | elif N<55000: 26 | V=11.5-6 - (N-18000)*4.5/(55000-18000) 27 | elif N<80000: 28 | V= 11.5-6-4.5-(N-55000)*1/(80000-55000) 29 | #V = 11.5*(1-N/60000) 30 | V = max(V,0.001) # min speed to avoid gridlock 31 | #return {"CAR": V, "BUS":8,"METRO":40,"TRAM":15} 32 | return {"CAR": 12, "BUS": 8, "METRO": 25, "TRAM": 15} 33 | 34 | 35 | if __name__ == '__main__': 36 | mmgraph = load_graph(indir + "/network-Lyon63V.json") 37 | 38 | odlayer = load_odlayer(indir + "/odlayer-Lyon63V.json") 39 | mmgraph.add_origin_destination_layer(odlayer) 40 | mmgraph.connect_origindestination_layers(250) 41 | 42 | veh_observer = CSVVehicleObserver(outdir+"/veh.csv") 43 | 44 | personal_car = PersonalMobilityService() 45 | personal_car.attach_vehicle_observer(veh_observer) 46 | mmgraph.layers["CARLayer"].add_mobility_service(personal_car) 47 | 48 | bus_service = PublicTransportMobilityService("BUS_MS") 49 | bus_service.attach_vehicle_observer(veh_observer) 50 | mmgraph.layers["BUSLayer"].add_mobility_service(bus_service) 51 | 52 | tram_service = PublicTransportMobilityService("TRAM_MS") 53 | tram_service.attach_vehicle_observer(veh_observer) 54 | mmgraph.layers["TRAMLayer"].add_mobility_service(tram_service) 55 | 56 | metro_service = PublicTransportMobilityService("METRO_MS") 57 | metro_service.attach_vehicle_observer(veh_observer) 58 | mmgraph.layers["METROLayer"].add_mobility_service(metro_service) 59 | 60 | mmgraph.connect_intra_layer("BUSLayer", 200) 61 | mmgraph.connect_inter_layers(["BUSLayer","TRAMLayer","METROLayer"],200) 62 | 63 | demand_file_name = indir + "/demand-Lyon63V-coords.csv" 64 | demand = CSVDemandManager(demand_file_name) 65 | demand.add_user_observer(CSVUserObserver(outdir+"/user.csv"), user_ids="all") 66 | 67 | flow_motor = MFDFlowMotor(outfile=outdir+"/flow.csv") 68 | 69 | for k, res in mmgraph.roads.zones.items(): 70 | flow_motor.add_reservoir(Reservoir(res, ["CAR","BUS","TRAM","METRO"], calculate_V_MFD)) 71 | 72 | travel_decision = LogitDecisionModel(mmgraph, outfile=outdir+"/path.csv", n_shortest_path=3) 73 | 74 | supervisor = Supervisor(graph=mmgraph, 75 | flow_motor=flow_motor, 76 | demand=demand, 77 | decision_model=travel_decision, 78 | outfile=outdir + "/travel_time_link.csv") 79 | 80 | supervisor.run(Time('06:30:00'), Time('10:30:00'), Dt(minutes=1), 10) 81 | -------------------------------------------------------------------------------- /src/mnms/generation/zones.py: -------------------------------------------------------------------------------- 1 | from mnms.graph.road import RoadDescriptor 2 | from mnms.tools.geometry import get_bounding_box 3 | from mnms.graph.zone import construct_zone_from_contour 4 | 5 | 6 | def generate_one_zone(zid: str, roads: RoadDescriptor): 7 | """ 8 | Generate a simple zone from the bounding box of the RoadDedscriptor 9 | 10 | Args: 11 | zid: The id of the zone 12 | roads: The RoadDescriptor 13 | 14 | Returns: 15 | The generated RoadDescriptor 16 | 17 | """ 18 | bbox = get_bounding_box(roads) 19 | bbox.xmin -= 1 20 | bbox.ymin -= 1 21 | bbox.xmax += 1 22 | bbox.ymax += 1 23 | contour = [[bbox.xmin, bbox.ymin], 24 | [bbox.xmax, bbox.ymin], 25 | [bbox.xmax, bbox.ymax], 26 | [bbox.xmin, bbox.ymax]] 27 | return construct_zone_from_contour(roads, zid, contour) 28 | 29 | def generate_grid_zones(zid_prefix: str, roads: RoadDescriptor, Nx: int, Ny: int, mlgraph=None): 30 | """ 31 | Generate Nx * Ny rectangle pricing zones within the bounding box of the RoadDescriptor 32 | on the RoadDescriptor. 33 | 34 | Args: 35 | zid_prefix: The prefix for the ids of the zones 36 | roads: The RoadDescriptor 37 | Nx: number of zones along the x axis 38 | Ny: number of zones along the y axis 39 | mlgraph: specified when the zoning should be built on the MultiLayerGraph 40 | 41 | Returns: 42 | The generated zones (Zone objects are built on RoadDescriptor, MLZone objects are 43 | built on the MultiLayerGraph) 44 | """ 45 | if mlgraph is not None: 46 | roads = mlgraph.roads 47 | bbox = get_bounding_box(roads) if mlgraph is None else get_bounding_box(mlgraph.roads) 48 | dx = (bbox.xmax - bbox.xmin) / Nx 49 | dy = (bbox.ymax - bbox.ymin) / Ny 50 | zones = [] 51 | startx = bbox.xmin - 1 52 | starty = bbox.ymin - 1 53 | for nx in range(Nx): 54 | for ny in range(Ny): 55 | if nx == Nx - 1 and ny < Ny - 1: 56 | c = [[startx + nx * dx, starty + ny * dy], 57 | [startx + (nx+1) * dx + 2, starty + ny * dy], 58 | [startx + (nx+1) * dx + 2, starty + (ny+1) * dy], 59 | [startx + nx * dx, starty + (ny+1) * dy]] 60 | elif nx < Nx - 1 and ny == Ny - 1: 61 | c = [[startx + nx * dx, starty + ny * dy], 62 | [startx + (nx+1) * dx, starty + ny * dy], 63 | [startx + (nx+1) * dx, starty + (ny+1) * dy + 2], 64 | [startx + nx * dx, starty + (ny+1) * dy + 2]] 65 | elif nx == Nx - 1 and ny == Ny - 1: 66 | c = [[startx + nx * dx, starty + ny * dy], 67 | [startx + (nx+1) * dx + 2, starty + ny * dy], 68 | [startx + (nx+1) * dx + 2, starty + (ny+1) * dy + 2], 69 | [startx + nx * dx, starty + (ny+1) * dy + 2]] 70 | else: 71 | c = [[startx + nx * dx, starty + ny * dy], 72 | [startx + (nx+1) * dx, starty + ny * dy], 73 | [startx + (nx+1) * dx, starty + (ny+1) * dy], 74 | [startx + nx * dx, starty + (ny+1) * dy]] 75 | graph = None if mlgraph is None else mlgraph.graph 76 | zone_type = 'Zone' if mlgraph is None else 'MLZone' 77 | zones.append(construct_zone_from_contour(roads, zid_prefix+str(nx)+'-'+str(ny), c, graph=graph, zone_type=zone_type)) 78 | return zones 79 | -------------------------------------------------------------------------------- /tests/flow/test_user_flow.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from tempfile import TemporaryDirectory 3 | 4 | from mnms.demand.user import User, Path 5 | from mnms.flow.user_flow import UserFlow 6 | from mnms.graph.layers import MultiLayerGraph, CarLayer, BusLayer 7 | from mnms.graph.road import RoadDescriptor 8 | from mnms.graph.zone import construct_zone_from_sections 9 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 10 | from mnms.mobility_service.public_transport import PublicTransportMobilityService 11 | from mnms.time import Time, Dt, TimeTable 12 | from mnms.vehicles.manager import VehicleManager 13 | 14 | 15 | class TestUserFlow(unittest.TestCase): 16 | def setUp(self): 17 | """Initiates the test. 18 | """ 19 | self.tempfile = TemporaryDirectory() 20 | self.pathdir = self.tempfile.name+'/' 21 | 22 | roads = RoadDescriptor() 23 | 24 | roads.register_node('0', [0, 0]) 25 | roads.register_node('1', [0, 40000]) 26 | roads.register_node('2', [1200, 0]) 27 | roads.register_node('3', [1400, 0]) 28 | roads.register_node('4', [3400, 0]) 29 | 30 | roads.register_section('0_1', '0', '1') 31 | roads.register_section('0_2', '0', '2') 32 | roads.register_section('2_3', '2', '3') 33 | roads.register_section('3_4', '3', '4') 34 | 35 | roads.register_stop("B2", "2_3", 0) 36 | roads.register_stop("B3", "3_4", 0) 37 | roads.register_stop("B4", "3_4", 1) 38 | 39 | roads.add_zone(construct_zone_from_sections(roads, "res1", ["0_1", "0_2", "2_3"])) 40 | roads.add_zone(construct_zone_from_sections(roads, "res2", ["3_4"])) 41 | 42 | car_layer = CarLayer(roads, services=[PersonalMobilityService()]) 43 | car_layer.create_node('C0', '0') 44 | car_layer.create_node('C1', '1') 45 | car_layer.create_node('C2', '2') 46 | 47 | car_layer.create_link('C0_C1', 'C0', 'C1', costs={"PersonalVehicle": {'length': 40000}}, road_links=['0_1']) 48 | car_layer.create_link('C0_C2', 'C0', 'C2', costs={"PersonalVehicle": {'length': 1200}}, road_links=['0_2']) 49 | 50 | bus_layer = BusLayer(roads, 51 | services=[PublicTransportMobilityService('Bus')]) 52 | 53 | bus_layer.create_line("L1", 54 | ["B2", "B3", "B4"], 55 | [["2_3"], ["3_4"]], 56 | TimeTable.create_table_freq('00:00:00', '01:00:00', Dt(minutes=2))) 57 | 58 | mlgraph = MultiLayerGraph([car_layer, bus_layer]) 59 | 60 | mlgraph.connect_layers('CAR_BUS', 'C2', 'L1_B2', 0, {'time': 0}) 61 | 62 | self.mlgraph = mlgraph 63 | self.user_flow = UserFlow(1.42) 64 | self.user_flow.set_graph(mlgraph) 65 | self.user_flow.set_time(Time('00:01:00')) 66 | # self.user_flow.initialize() 67 | 68 | def tearDown(self): 69 | """Concludes and closes the test. 70 | """ 71 | self.tempfile.cleanup() 72 | VehicleManager.empty() 73 | 74 | def test_fill(self): 75 | self.assertTrue(self.mlgraph is self.user_flow._graph) 76 | self.assertEqual(Time('00:01:00'), self.user_flow._tcurrent) 77 | self.assertEqual(1.42, self.user_flow._walk_speed) 78 | 79 | def test_request_veh(self): 80 | user = User('U0', '0', '4', Time('00:01:00')) 81 | user._current_node = 'C0' 82 | user.set_path(Path(cost=3400, 83 | nodes=['C0', 'C1', 'C2', 'B2', 'B3', 'B4'])) 84 | self.user_flow.step(Dt(minutes=1), [user]) 85 | 86 | self.assertIn('U0', self.user_flow.users) 87 | -------------------------------------------------------------------------------- /examples/snapshot/run_and_take_snapshot.py: -------------------------------------------------------------------------------- 1 | from mnms.simulation import Supervisor 2 | from mnms.demand import CSVDemandManager 3 | from mnms.flow.MFD import Reservoir, MFDFlowMotor 4 | from mnms.log import attach_log_file, LOGLEVEL, set_mnms_logger_level, set_all_mnms_logger_level 5 | from mnms.time import Time, Dt 6 | from mnms.io.graph import load_graph, load_odlayer 7 | from mnms.travel_decision.logit import LogitDecisionModel 8 | from mnms.tools.observer import CSVUserObserver, CSVVehicleObserver 9 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 10 | from mnms.mobility_service.public_transport import PublicTransportMobilityService 11 | 12 | indir = "INPUTS" 13 | outdir = "OUTPUTS" 14 | 15 | set_all_mnms_logger_level(LOGLEVEL.INFO) 16 | #set_mnms_logger_level(LOGLEVEL.INFO, ["mnms.simulation","mnms.mobility_service.public_transport"]) 17 | attach_log_file(outdir+'/simulation.log') 18 | 19 | def calculate_V_MFD(acc): 20 | #V = 10.3*(1-N/57000) # data from fit prop 21 | V = 0 # data from fit dsty 22 | N = acc["CAR"] 23 | if N<18000: 24 | V=11.5-N*6/18000 25 | elif N<55000: 26 | V=11.5-6 - (N-18000)*4.5/(55000-18000) 27 | elif N<80000: 28 | V= 11.5-6-4.5-(N-55000)*1/(80000-55000) 29 | #V = 11.5*(1-N/60000) 30 | V = max(V,0.001) # min speed to avoid gridlock 31 | #return {"CAR": V, "BUS":8,"METRO":40,"TRAM":15} 32 | return {"CAR": 12, "BUS": 8, "METRO": 25, "TRAM": 15} 33 | 34 | 35 | if __name__ == '__main__': 36 | mmgraph = load_graph(indir + "/network-Lyon63V.json") 37 | 38 | odlayer = load_odlayer(indir + "/odlayer-Lyon63V.json") 39 | mmgraph.add_origin_destination_layer(odlayer) 40 | mmgraph.connect_origindestination_layers(250) 41 | 42 | veh_observer = CSVVehicleObserver(outdir+"/veh.csv") 43 | 44 | personal_car = PersonalMobilityService() 45 | personal_car.attach_vehicle_observer(veh_observer) 46 | mmgraph.layers["CARLayer"].add_mobility_service(personal_car) 47 | 48 | bus_service = PublicTransportMobilityService("BUS_MS") 49 | bus_service.attach_vehicle_observer(veh_observer) 50 | mmgraph.layers["BUSLayer"].add_mobility_service(bus_service) 51 | 52 | tram_service = PublicTransportMobilityService("TRAM_MS") 53 | tram_service.attach_vehicle_observer(veh_observer) 54 | mmgraph.layers["TRAMLayer"].add_mobility_service(tram_service) 55 | 56 | metro_service = PublicTransportMobilityService("METRO_MS") 57 | metro_service.attach_vehicle_observer(veh_observer) 58 | mmgraph.layers["METROLayer"].add_mobility_service(metro_service) 59 | 60 | mmgraph.connect_intra_layer("BUSLayer", 200) 61 | mmgraph.connect_inter_layers(["BUSLayer","TRAMLayer","METROLayer"],200) 62 | 63 | demand_file_name = indir + "/demand-Lyon63V-coords.csv" 64 | demand = CSVDemandManager(demand_file_name) 65 | demand.add_user_observer(CSVUserObserver(outdir+"/user.csv"), user_ids="all") 66 | 67 | flow_motor = MFDFlowMotor(outfile=outdir+"/flow.csv") 68 | 69 | for k, res in mmgraph.roads.zones.items(): 70 | flow_motor.add_reservoir(Reservoir(res, ["CAR","BUS","TRAM","METRO"], calculate_V_MFD)) 71 | 72 | travel_decision = LogitDecisionModel(mmgraph, outfile=outdir+"/path.csv", n_shortest_path=1) 73 | 74 | supervisor = Supervisor(graph=mmgraph, 75 | flow_motor=flow_motor, 76 | demand=demand, 77 | decision_model=travel_decision, 78 | outfile=outdir + "/travel_time_link.csv") 79 | 80 | supervisor.run(Time('06:30:00'), Time('07:30:00'), Dt(minutes=1), 10, snapshot=True, snapshot_folder=outdir) 81 | -------------------------------------------------------------------------------- /examples/public_transport/run.py: -------------------------------------------------------------------------------- 1 | ############### 2 | ### Imports ### 3 | ############### 4 | ## Casuals 5 | import pathlib 6 | 7 | ## MnMS 8 | from mnms.log import set_all_mnms_logger_level, LOGLEVEL 9 | from mnms.demand import CSVDemandManager 10 | from mnms.flow.MFD import Reservoir, MFDFlowMotor 11 | from mnms.generation.layers import generate_matching_origin_destination_layer, generate_layer_from_roads, \ 12 | generate_grid_origin_destination_layer 13 | from mnms.generation.roads import generate_line_road, generate_manhattan_road 14 | from mnms.graph.layers import PublicTransportLayer, MultiLayerGraph 15 | from mnms.io.graph import save_graph, load_graph 16 | from mnms.log import set_mnms_logger_level, attach_log_file 17 | from mnms.mobility_service.public_transport import PublicTransportMobilityService 18 | from mnms.simulation import Supervisor 19 | from mnms.time import TimeTable, Time, Dt 20 | from mnms.tools.observer import CSVUserObserver, CSVVehicleObserver 21 | from mnms.travel_decision import DummyDecisionModel 22 | from mnms.vehicles.veh_type import Bus 23 | 24 | ################## 25 | ### Parameters ### 26 | ################## 27 | demand_file = pathlib.Path(__file__).parent.joinpath('demand.csv').resolve() 28 | log_file = pathlib.Path(__file__).parent.joinpath('sim.log').resolve() 29 | roads_xmin = [0, 0] 30 | roads_xmax = [3000, 0] 31 | roads_nb_nodes = 2 32 | bus_default_speed = 10 # m/s 33 | bus_frequency = Dt(minutes=5) 34 | bus_tstart = '07:01:00' 35 | bus_tend = '08:01:00' 36 | odlayer_connection_dist = 301 # m 37 | def mfdspeed(dacc): 38 | dacc['BUS'] = 5 # m/s 39 | return dacc 40 | tstart = Time("07:00:00") 41 | tend = Time("08:30:00") 42 | dt_flow = Dt(minutes=1) 43 | affectation_factor = 10 44 | 45 | ######################### 46 | ### Scenario creation ### 47 | ######################### 48 | 49 | #### RoadDescriptor #### 50 | roads = generate_line_road(roads_xmin, roads_xmax, roads_nb_nodes) 51 | roads.register_stop('SO', '0_1', 0.1) 52 | roads.register_stop('S1', '0_1', 0.4) 53 | roads.register_stop('S2', '0_1', 0.6) 54 | roads.register_stop('SD', '0_1', 0.9) 55 | 56 | #### MlGraph #### 57 | bus_service = PublicTransportMobilityService('BUS') 58 | ptlayer = PublicTransportLayer(roads, 'BUS', Bus, bus_default_speed, services=[bus_service], 59 | observer=CSVVehicleObserver("vehs.csv")) 60 | 61 | ptlayer.create_line('L0', 62 | ['SO', 'S1', 'S2','SD'], 63 | [['0_1'], ['0_1'],['0_1']], 64 | TimeTable.create_table_freq(bus_tstart, bus_tend, bus_frequency)) 65 | 66 | odlayer = generate_matching_origin_destination_layer(roads) 67 | 68 | mlgraph = MultiLayerGraph([ptlayer], 69 | odlayer, 70 | odlayer_connection_dist) 71 | 72 | #### Demand #### 73 | demand = CSVDemandManager(demand_file) 74 | demand.add_user_observer(CSVUserObserver('users.csv')) 75 | 76 | #### Decision model #### 77 | decision_model = DummyDecisionModel(mlgraph, outfile="paths.csv") 78 | 79 | #### Flow motor #### 80 | flow_motor = MFDFlowMotor() 81 | flow_motor.add_reservoir(Reservoir(roads.zones['RES'], ['BUS'], mfdspeed)) 82 | 83 | #### Supervisor #### 84 | supervisor = Supervisor(mlgraph, 85 | demand, 86 | flow_motor, 87 | decision_model, 88 | logfile=log_file, 89 | loglevel=LOGLEVEL.INFO) 90 | 91 | ###################### 92 | ### Run simulation ### 93 | ###################### 94 | 95 | set_all_mnms_logger_level(LOGLEVEL.INFO) 96 | 97 | supervisor.run(tstart, 98 | tend, 99 | dt_flow, 100 | affectation_factor) 101 | -------------------------------------------------------------------------------- /src/mnms/travel_decision/dummy.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | import numpy as np 3 | 4 | from mnms.graph.layers import MultiLayerGraph 5 | from mnms.demand.user import Path 6 | from mnms.travel_decision.abstract import AbstractDecisionModel 7 | 8 | 9 | class DummyDecisionModel(AbstractDecisionModel): 10 | def __init__(self, mmgraph: MultiLayerGraph, considered_modes=None, cost='travel_time', outfile:str=None, 11 | verbose_file=False, personal_mob_service_park_radius:float=100, random_choice_for_equal_costs:bool=False, 12 | save_routes_dynamically_and_reapply: bool = False): 13 | """ 14 | Deterministic decision model: the path with the lowest cost is chosen. 15 | 16 | Args: 17 | -mmgraph: The graph on which the model compute the path 18 | -considered_modes: List of guidelines for the guided paths discovery, 19 | if None, the default paths discovery is applied 20 | -cost: name of the cost to consider 21 | -outfile: If specified the file in which chosen paths are written 22 | -verbose_file: If True write all the computed shortest path, not only the one that is selected 23 | -personal_mob_service_park_radius: radius around user's personal veh parking location in which 24 | she can still have access to her vehicle 25 | -random_choice_for_equal_costs: boolean specifying if the choice among paths with 26 | equal costs should be random or deterministic 27 | -save_routes_dynamically_and_reapply: boolean specifying if the k shortest paths computed 28 | for an origin, destination, and mode should be saved 29 | dynamically and reapply for next departing users with 30 | the same origin, destination and mode 31 | """ 32 | super(DummyDecisionModel, self).__init__(mmgraph, considered_modes=considered_modes, 33 | n_shortest_path=1, outfile=outfile, 34 | verbose_file=verbose_file, 35 | cost=cost, personal_mob_service_park_radius=personal_mob_service_park_radius, 36 | save_routes_dynamically_and_reapply=save_routes_dynamically_and_reapply) 37 | self.random_choice_for_equal_costs = random_choice_for_equal_costs 38 | self._seed = None 39 | self._rng = None 40 | 41 | def set_random_seed(self, seed): 42 | """Method that sets the random seed for this decision model. 43 | 44 | Args: 45 | -seed: seed as an integer 46 | """ 47 | if seed is not None and self.random_choice_for_equal_costs: 48 | self._seed = seed 49 | rng = np.random.default_rng(self._seed) 50 | self._rng = rng 51 | 52 | def path_choice(self, paths:List[Path]) -> Path: 53 | """Method that proceeds to the selection of the path. 54 | 55 | Args: 56 | -paths: list of paths to consider for the choice 57 | """ 58 | # Sort paths by ascending cost before returning the best path 59 | paths.sort(key=lambda p: ' '.join(p.mobility_services)) # to prevent different results between 60 | # two executions if several equal path costs 61 | paths.sort(key=lambda p: p.path_cost) 62 | if self.random_choice_for_equal_costs: 63 | min_cost = paths[0].path_cost 64 | min_cost_paths = [p for p in paths if p.path_cost == min_cost] 65 | return self._rng.choice(min_cost_paths) 66 | else: 67 | return paths[0] 68 | -------------------------------------------------------------------------------- /examples/vehicle_sharing/run_freefloating.py: -------------------------------------------------------------------------------- 1 | ############### 2 | ### Imports ### 3 | ############### 4 | ## Casuals 5 | import pathlib 6 | 7 | # MnMS 8 | from mnms.generation.roads import generate_manhattan_road 9 | from mnms.mobility_service.vehicle_sharing import VehicleSharingMobilityService 10 | from mnms.tools.observer import CSVVehicleObserver, CSVUserObserver 11 | from mnms.generation.layers import generate_layer_from_roads, generate_grid_origin_destination_layer 12 | from mnms.graph.layers import SharedVehicleLayer, MultiLayerGraph 13 | from mnms.vehicles.veh_type import Bike 14 | from mnms.generation.demand import generate_random_demand 15 | from mnms.travel_decision.dummy import DummyDecisionModel 16 | from mnms.flow.MFD import MFDFlowMotor, Reservoir 17 | from mnms.simulation import Supervisor 18 | from mnms.demand import CSVDemandManager 19 | from mnms.time import TimeTable, Time, Dt 20 | from mnms.log import set_mnms_logger_level, LOGLEVEL, attach_log_file 21 | from mnms.io.graph import save_graph 22 | 23 | # set_all_mnms_logger_level(LOGLEVEL.WARNING) 24 | set_mnms_logger_level(LOGLEVEL.INFO, ["mnms.simulation"]) 25 | 26 | # get_logger("mnms.graph.shortest_path").setLevel(LOGLEVEL.WARNING) 27 | attach_log_file('simulation.log') 28 | 29 | ################## 30 | ### Parameters ### 31 | ################## 32 | demand_file = pathlib.Path(__file__).parent.joinpath('demand.csv').resolve() 33 | log_file = pathlib.Path(__file__).parent.joinpath('sim.log').resolve() 34 | n_nodes_per_dir = 5 35 | mesh_size = 1000 # m 36 | velov_default_speed = 3 # m/s 37 | b_freefloating = 1 38 | velov_dt_matching = 1 39 | n_odnodes_x = 5 40 | n_odnodes_y = 5 41 | odlayer_xmin = -1000 42 | odlayer_ymin = -1000 43 | odlayer_xmax = 3000 # m 44 | odlayer_ymax = 3000 # m 45 | odlayer_connection_dist = 500 # m 46 | def mfdspeed(dacc): 47 | dspeed = {'BIKE': 3} 48 | return dspeed 49 | tstart = Time("07:00:00") 50 | tend = Time("09:00:00") 51 | dt_flow = Dt(minutes=1) 52 | affectation_factor = 1 53 | 54 | ######################### 55 | ### Scenario creation ### 56 | ######################### 57 | 58 | #### RoadDescriptor #### 59 | road_db = generate_manhattan_road(n_nodes_per_dir, mesh_size, prefix='I_') 60 | 61 | #### MLGraph #### 62 | ff_velov = VehicleSharingMobilityService("ff_velov", b_freefloating, velov_dt_matching) 63 | ff_velov.attach_vehicle_observer(CSVVehicleObserver("velov_vehs.csv")) 64 | velov_layer = generate_layer_from_roads(road_db, 'velov_layer', SharedVehicleLayer, Bike, velov_default_speed, [ff_velov]) 65 | 66 | odlayer = generate_grid_origin_destination_layer(odlayer_xmin, odlayer_ymin, 67 | odlayer_xmax, odlayer_ymax, n_odnodes_x, n_odnodes_y) 68 | 69 | mlgraph = MultiLayerGraph([velov_layer], odlayer) 70 | 71 | # Add free-floating vehicle 72 | ff_velov.init_free_floating_vehicles('I_2', 1) 73 | 74 | # Connect od layer and velov layer 75 | mlgraph.connect_origindestination_layers(odlayer_connection_dist) 76 | 77 | #### Decision model #### 78 | decision_model = DummyDecisionModel(mlgraph, outfile="paths.csv") 79 | 80 | #### Flow motor #### 81 | flow_motor = MFDFlowMotor(outfile="flow.csv") 82 | flow_motor.add_reservoir(Reservoir(road_db.zones["RES"], ['BIKE'], mfdspeed)) 83 | 84 | #### Demand #### 85 | demand = CSVDemandManager(demand_file) 86 | demand.add_user_observer(CSVUserObserver('users.csv')) 87 | 88 | #### Supervisor #### 89 | supervisor = Supervisor(mlgraph, 90 | demand, 91 | flow_motor, 92 | decision_model, 93 | logfile=log_file, 94 | loglevel=LOGLEVEL.INFO) 95 | 96 | ###################### 97 | ### Run simulation ### 98 | ###################### 99 | 100 | supervisor.run(tstart, 101 | tend, 102 | dt_flow, 103 | affectation_factor) 104 | -------------------------------------------------------------------------------- /examples/vehicle_sharing/run.py: -------------------------------------------------------------------------------- 1 | ############### 2 | ### Imports ### 3 | ############### 4 | ## Casuals 5 | import pathlib 6 | 7 | # MnMS 8 | from mnms.generation.roads import generate_manhattan_road 9 | from mnms.mobility_service.vehicle_sharing import VehicleSharingMobilityService 10 | from mnms.tools.observer import CSVVehicleObserver, CSVUserObserver 11 | from mnms.generation.layers import generate_layer_from_roads, generate_grid_origin_destination_layer 12 | from mnms.graph.layers import SharedVehicleLayer, MultiLayerGraph 13 | from mnms.vehicles.veh_type import Bike 14 | from mnms.travel_decision.dummy import DummyDecisionModel 15 | from mnms.flow.MFD import MFDFlowMotor, Reservoir 16 | from mnms.simulation import Supervisor 17 | from mnms.demand import CSVDemandManager 18 | from mnms.time import Time, Dt 19 | from mnms.log import set_mnms_logger_level, LOGLEVEL, attach_log_file 20 | from mnms.io.graph import save_graph, load_graph 21 | 22 | 23 | ################## 24 | ### Parameters ### 25 | ################## 26 | b_from_json=False 27 | mlgraph_file = 'velov_example.json' 28 | demand_file = pathlib.Path(__file__).parent.joinpath('demand.csv').resolve() 29 | log_file = 'sim.log' 30 | n_nodes_per_dir = 3 31 | mesh_size = 1000 # m 32 | n_odnodes_x = 5 33 | n_odnodes_y = 5 34 | odlayer_xmin = -1000 35 | odlayer_ymin = -1000 36 | odlayer_xmax = 3000 # m 37 | odlayer_ymax = 3000 # m 38 | odlayer_connection_dist = 500 # m 39 | velov_default_speed = 3 # m/s 40 | b_freefloating = 0 41 | velov_dt_matching = 1 42 | def mfdspeed(dacc): 43 | dspeed = {'BIKE': 3} 44 | return dspeed 45 | tstart = Time("06:50:00") 46 | tend = Time("09:00:00") 47 | dt_flow = Dt(minutes=1) 48 | affectation_factor = 10 49 | 50 | ######################### 51 | ### Scenario creation ### 52 | ######################### 53 | 54 | #### RoadDescriptor and MLGraph #### 55 | if b_from_json: 56 | mlgraph=load_graph(mlgraph_file) 57 | 58 | # OD layer 59 | odlayer = generate_grid_origin_destination_layer(odlayer_xmin, odlayer_ymin, 60 | odlayer_xmax, odlayer_ymax, n_odnodes_x, n_odnodes_y) 61 | mlgraph.add_origin_destination_layer(odlayer) 62 | else: 63 | # Graph 64 | road_db = generate_manhattan_road(n_nodes_per_dir, mesh_size) 65 | 66 | # OD layer 67 | odlayer = generate_grid_origin_destination_layer(odlayer_xmin, odlayer_ymin, 68 | odlayer_xmax, odlayer_ymax, n_odnodes_x, n_odnodes_y) 69 | 70 | # Vehicle sharing mobility service 71 | velov = VehicleSharingMobilityService("velov", b_freefloating, velov_dt_matching) 72 | velov.attach_vehicle_observer(CSVVehicleObserver("velov_vehs.csv")) 73 | velov_layer = generate_layer_from_roads(road_db, 'velov_layer', SharedVehicleLayer, 74 | Bike, velov_default_speed, [velov]) 75 | 76 | # Multilayer graph 77 | mlgraph = MultiLayerGraph([velov_layer], odlayer) 78 | 79 | if not b_from_json: 80 | save_graph(mlgraph, mlgraph_file) 81 | 82 | # Add stations 83 | mlgraph.layers['velov_layer'].mobility_services['velov'].create_station('S1', '2', capacity=20, nb_initial_veh=5) 84 | mlgraph.layers['velov_layer'].mobility_services['velov'].create_station('S2', 'SOUTH_2', capacity=20, nb_initial_veh=0) 85 | 86 | # Connect od layer and velov layer 87 | mlgraph.connect_origindestination_layers(odlayer_connection_dist) 88 | 89 | #### Decision model #### 90 | decision_model = DummyDecisionModel(mlgraph, outfile="paths.csv") 91 | 92 | #### Flow motor #### 93 | flow_motor = MFDFlowMotor(outfile="flow.csv") 94 | flow_motor.add_reservoir(Reservoir(mlgraph.roads.zones['RES'], ['BIKE'], mfdspeed)) 95 | 96 | #### Demand #### 97 | demand = CSVDemandManager(demand_file) 98 | demand.add_user_observer(CSVUserObserver('users.csv')) 99 | 100 | #### Supervisor #### 101 | supervisor = Supervisor(mlgraph, 102 | demand, 103 | flow_motor, 104 | decision_model) 105 | 106 | 107 | ###################### 108 | ### Run simulation ### 109 | ###################### 110 | 111 | # set_all_mnms_logger_level(LOGLEVEL.WARNING) 112 | set_mnms_logger_level(LOGLEVEL.INFO, ["mnms.simulation"]) 113 | # get_logger("mnms.graph.shortest_path").setLevel(LOGLEVEL.WARNING) 114 | attach_log_file(log_file) 115 | 116 | supervisor.run(tstart, 117 | tend, 118 | dt_flow, 119 | affectation_factor) 120 | -------------------------------------------------------------------------------- /src/mnms/flow/abstract.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from collections import defaultdict 3 | from typing import List, Dict, Optional, Callable 4 | import csv 5 | 6 | from mnms.graph.zone import Zone 7 | from mnms.time import Time, Dt 8 | from mnms.graph.layers import MultiLayerGraph 9 | 10 | class AbstractReservoir(ABC): 11 | 12 | def __init__(self, zone: Zone, modes: List[str]): 13 | """ 14 | Abstract Reservoir class defining the interface for a MFD reservoir 15 | 16 | Args: 17 | zone: The zone object associated to the Reservoir 18 | modes: The modes in the Reservoir 19 | """ 20 | self.id: str = zone.id 21 | self.zone = zone 22 | self.modes = modes 23 | self.dict_accumulations = defaultdict(lambda: 0) 24 | self.dict_speeds = defaultdict(lambda: 0.) 25 | 26 | self.ghost_accumulation: Callable[[Time], Dict[str, float]] = lambda x: {} 27 | 28 | self.trip_lengths = {} 29 | 30 | @abstractmethod 31 | def update_accumulations(self, dict_accumulations: Dict[str, int]): 32 | """ 33 | Method updating the accumulation inside the Reservoir 34 | 35 | Args: 36 | dict_accumulations: The new accumulation 37 | 38 | Returns 39 | ------- 40 | 41 | """ 42 | pass 43 | 44 | @abstractmethod 45 | def update_speeds(self): 46 | """ 47 | Method updating the speed inside the Reservoir 48 | 49 | Returns 50 | ------- 51 | 52 | """ 53 | pass 54 | 55 | def set_ghost_accumulation(self, f_acc: Callable[[Time], Dict[str, float]]): 56 | 57 | self.ghost_accumulation = f_acc 58 | 59 | def add_trip_length(self, l, mode): 60 | """Method that registers a new trip length in this reservoir. 61 | 62 | Args: 63 | -l: the trip length 64 | -mode: the vehicle type that achieved this trip 65 | """ 66 | if mode in self.trip_lengths: 67 | self.trip_lengths[mode].append(l) 68 | else: 69 | self.trip_lengths[mode] = [l] 70 | 71 | def flush_trip_lengths(self): 72 | """Method that clean all trip lengths registered in this reservoir. 73 | """ 74 | self.trip_lengths = {} 75 | 76 | class AbstractMFDFlowMotor(ABC): 77 | def __init__(self, outfile:str=None): 78 | """Abstraction of a flow motor, two methods must be overridden `step` and `update_graph`. 79 | `step` define the core of the motor, i.e. the way `Vehicle` move. `update_graph` must update the cost of the graph. 80 | 81 | Args: 82 | outfile: If not `None` store the `User` position at each `step` 83 | """ 84 | self._graph: MultiLayerGraph = None 85 | 86 | self._tcurrent: Time = Time() 87 | 88 | if outfile is None: 89 | self._write = False 90 | else: 91 | self._write = True 92 | self._outfile = open(outfile, "w") 93 | self._csvhandler = csv.writer(self._outfile, delimiter=';', quotechar='|') 94 | 95 | def __getstate__(self): 96 | state = self.__dict__.copy() 97 | 98 | if self._write == True: 99 | if '_csvhandler' in state: 100 | del state['_csvhandler'] 101 | return state 102 | 103 | def __setstate__(self, state): 104 | self.__dict__.update(state) 105 | 106 | if self._write == True: 107 | self._csvhandler = csv.writer(self._outfile, delimiter=';', quotechar='|') 108 | 109 | def set_graph(self, mlgraph: MultiLayerGraph): 110 | self._graph = mlgraph 111 | 112 | def set_time(self, time:Time): 113 | self._tcurrent = time.copy() 114 | 115 | @property 116 | def time(self): 117 | return self._tcurrent.time 118 | 119 | def update_time(self, dt:Dt): 120 | self._tcurrent = self._tcurrent.add_time(dt) 121 | 122 | @abstractmethod 123 | def step(self, dt:float): 124 | pass 125 | 126 | @abstractmethod 127 | def update_graph(self, threshold): 128 | pass 129 | 130 | def write_result(self, step_affectation:int, step_flow:int): 131 | raise NotImplementedError(f"{self.__class__.__name__} do not implement a write_result method") 132 | 133 | def initialize(self): 134 | pass 135 | 136 | def add_reservoir(self, res: AbstractReservoir): 137 | pass 138 | 139 | def finalize(self): 140 | if self._write: 141 | self._outfile.close() 142 | -------------------------------------------------------------------------------- /examples/congested_mfd/run.py: -------------------------------------------------------------------------------- 1 | ############### 2 | ### Imports ### 3 | ############### 4 | ## Casuals 5 | import pathlib 6 | 7 | ## MnMS 8 | from mnms import LOGLEVEL 9 | from mnms.demand import BaseDemandManager, User 10 | from mnms.flow.congested_MFD import CongestedMFDFlowMotor, CongestedReservoir 11 | from mnms.generation.layers import generate_matching_origin_destination_layer 12 | from mnms.generation.roads import generate_line_road 13 | from mnms.graph.layers import MultiLayerGraph, CarLayer 14 | from mnms.graph.zone import construct_zone_from_sections 15 | from mnms.log import set_mnms_logger_level 16 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 17 | from mnms.simulation import Supervisor 18 | from mnms.time import Time, Dt 19 | from mnms.tools.observer import CSVUserObserver, CSVVehicleObserver 20 | from mnms.travel_decision.dummy import DummyDecisionModel 21 | 22 | set_mnms_logger_level(LOGLEVEL.INFO, ['mnms.simulation', 23 | 'mnms.vehicles.veh_type', 24 | 'mnms.flow.user_flow', 25 | 'mnms.flow.MFD', 26 | 'mnms.layer.public_transport', 27 | 'mnms.travel_decision.model', 28 | 'mnms.tools.observer']) 29 | 30 | ################## 31 | ### Parameters ### 32 | ################## 33 | log_file = pathlib.Path(__file__).parent.joinpath('sim.log').resolve() 34 | roads_xmin = [0, 0] 35 | roads_xmax = [0, 2000] 36 | roads_n_nodes = 3 37 | odlayer_connection_dist = 1e-3 # m 38 | n_users = 2000 39 | dt_inter_users = 1 40 | users_origin = [0, 0] 41 | users_destination = [0, 2000] 42 | users_tstart = Time("07:00:00") 43 | n_car_max = 80 44 | 45 | def speed_MFD(acc, n_car_max): 46 | n_car = acc["CAR"] 47 | v_car = 0 48 | if n_car < 180/3: 49 | v_car = 11.5 - n_car * 6 / 18000 50 | elif n_car < 550/3: 51 | v_car = 11.5 - 6 - (n_car - 18000) * 4.5 / (55000 - 18000) 52 | elif n_car < 800/3: 53 | v_car = 11.5 - 6 - 4.5 - (n_car - 55000) * 1 / (80000 - 55000) 54 | return {"CAR": max(v_car, 0.001)} 55 | 56 | def entry_MFD(acc_car, n_car_max): 57 | return 1/max(acc_car, 1e-3) 58 | 59 | tstart = Time("07:00:00") 60 | tend = Time("09:10:00") 61 | dt_flow = Dt(seconds=1) 62 | affectation_factor = 10 63 | 64 | ######################### 65 | ### Scenario creation ### 66 | ######################### 67 | 68 | #### RoadDescriptor #### 69 | roads = generate_line_road(roads_xmin, roads_xmax, roads_n_nodes) 70 | roads.add_zone(construct_zone_from_sections(roads, "LEFT", ["0_1"])) 71 | roads.add_zone(construct_zone_from_sections(roads, "RIGHT", ["1_2"])) 72 | 73 | #### MlGraph #### 74 | personal_car = PersonalMobilityService() 75 | personal_car.attach_vehicle_observer(CSVVehicleObserver("car_vehs.csv")) 76 | car_layer = CarLayer(roads, services=[personal_car]) 77 | car_layer.create_node("CAR_0", "0") 78 | car_layer.create_node("CAR_1", "1") 79 | car_layer.create_node("CAR_2", "2") 80 | car_layer.create_link("CAR_0_1", "CAR_0", "CAR_1", {}, ["0_1"]) 81 | car_layer.create_link("CAR_1_2", "CAR_1", "CAR_2", {}, ["1_2"]) 82 | 83 | odlayer = generate_matching_origin_destination_layer(roads) 84 | 85 | mlgraph = MultiLayerGraph([car_layer], 86 | odlayer, 87 | odlayer_connection_dist) 88 | 89 | #### Demand #### 90 | demand = BaseDemandManager([User(f"U{i}", users_origin, users_destination, users_tstart.add_time(Dt(seconds=i*dt_inter_users))) for i in range(n_users)]) 91 | demand.add_user_observer(CSVUserObserver('users.csv')) 92 | 93 | #### Decision model #### 94 | decision_model = DummyDecisionModel(mlgraph, outfile="paths.csv") 95 | 96 | #### Flow motor #### 97 | flow_motor = CongestedMFDFlowMotor(outfile="mfd.csv") 98 | flow_motor.add_reservoir(CongestedReservoir(roads.zones["LEFT"], ['CAR'], speed_MFD, entry_MFD, n_car_max)) 99 | flow_motor.add_reservoir(CongestedReservoir(roads.zones["RIGHT"], ['CAR'], speed_MFD, entry_MFD, n_car_max)) 100 | 101 | #### Supervisor #### 102 | supervisor = Supervisor(mlgraph, 103 | demand, 104 | flow_motor, 105 | decision_model, 106 | logfile=log_file, 107 | loglevel=LOGLEVEL.INFO) 108 | 109 | ###################### 110 | ### Run simulation ### 111 | ###################### 112 | 113 | supervisor.run(tstart, 114 | tend, 115 | dt_flow, 116 | affectation_factor) 117 | -------------------------------------------------------------------------------- /examples/intermodal/run.py: -------------------------------------------------------------------------------- 1 | ############### 2 | ### Imports ### 3 | ############### 4 | ## Casuals 5 | import pathlib 6 | 7 | ## MnMS 8 | from mnms.log import set_all_mnms_logger_level, LOGLEVEL 9 | from mnms.demand import BaseDemandManager, User 10 | from mnms.generation.roads import generate_line_road 11 | from mnms.generation.layers import generate_matching_origin_destination_layer 12 | from mnms.graph.layers import MultiLayerGraph, PublicTransportLayer, CarLayer 13 | from mnms.mobility_service.public_transport import PublicTransportMobilityService 14 | from mnms.travel_decision.dummy import DummyDecisionModel 15 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 16 | from mnms.flow.MFD import MFDFlowMotor, Reservoir 17 | from mnms.simulation import Supervisor 18 | from mnms.time import Time, Dt, TimeTable 19 | from mnms.tools.observer import CSVUserObserver, CSVVehicleObserver 20 | from mnms.vehicles.veh_type import Bus 21 | 22 | ################## 23 | ### Parameters ### 24 | ################## 25 | log_file = pathlib.Path(__file__).parent.joinpath('sim.log').resolve() 26 | roads_xmin = [0, 0] 27 | roads_xmax = [0, 5000] 28 | roads_n_nodes = 6 29 | bus_default_speed = 13 # m/s 30 | bus_tstart = '07:00:00' 31 | bus_tend = '08:00:00' 32 | bus_frequency = Dt(minutes=1) 33 | odlayer_connection_dist = 1e-3 # m 34 | def mfdspeed(dacc): 35 | dspeed = {'CAR': 13.8, 36 | 'BUS': 12} 37 | return dspeed 38 | tstart = Time("06:59:00") 39 | tend = Time("07:10:00") 40 | dt_flow = Dt(seconds=1) 41 | affectation_factor = 10 42 | 43 | ######################### 44 | ### Scenario creation ### 45 | # a line road network where a car layer extends on the left and a bus line extends on the right 46 | # one reservoir with constant speeds for cars and buses and covering the whole network 47 | # a personal car mobility service runs on the car layer and a public transportation service runs on another layer 48 | # an origin-destination layer matching with the roads nodes 49 | # only one user who should travel from the left to the right by taking her car, then the bus line 50 | ######################### 51 | 52 | #### RoadDescriptor #### 53 | roads = generate_line_road(roads_xmin, roads_xmax, roads_n_nodes) 54 | roads.register_stop('S0', '3_4', 0.10) 55 | roads.register_stop('S1', '3_4', 1) 56 | roads.register_stop('S2', '4_5', 1) 57 | 58 | #### MlGraph #### 59 | pv = PersonalMobilityService() 60 | pv.attach_vehicle_observer(CSVVehicleObserver("pv_vehs.csv")) 61 | car_layer = CarLayer(roads, services=[pv]) 62 | car_layer.create_node("CAR_0", "0") 63 | car_layer.create_node("CAR_1", "1") 64 | car_layer.create_node("CAR_2", "2") 65 | car_layer.create_node("CAR_3", "3") 66 | car_layer.create_link("CAR_0_1", "CAR_0", "CAR_1", {}, ["0_1"]) 67 | car_layer.create_link("CAR_1_2", "CAR_1", "CAR_2", {}, ["1_2"]) 68 | car_layer.create_link("CAR_2_3", "CAR_2", "CAR_3", {}, ["2_3"]) 69 | 70 | 71 | bus_service = PublicTransportMobilityService('Bus') 72 | ptlayer = PublicTransportLayer(roads, 'BUS', Bus, bus_default_speed, services=[bus_service], 73 | observer=CSVVehicleObserver("bus_vehs.csv")) 74 | ptlayer.create_line("L0", 75 | ["S0", "S1", "S2"], 76 | [["3_4"], ["4_5"]], 77 | timetable=TimeTable.create_table_freq(bus_tstart, bus_tend, bus_frequency)) 78 | 79 | odlayer = generate_matching_origin_destination_layer(roads) 80 | 81 | mlgraph = MultiLayerGraph([car_layer, ptlayer], 82 | odlayer, 83 | odlayer_connection_dist) 84 | 85 | mlgraph.connect_layers("TRANSIT_LINK", "CAR_3", "L0_S0", 100, {}) 86 | 87 | #### Demand #### 88 | demand = BaseDemandManager([User("U0", [0, 0], [0, 5000], Time("07:00:00"))]) 89 | demand.add_user_observer(CSVUserObserver('users.csv')) 90 | 91 | #### Decision model #### 92 | decision_model = DummyDecisionModel(mlgraph, outfile="paths.csv", verbose_file=True) 93 | 94 | #### Flow motor #### 95 | flow_motor = MFDFlowMotor() 96 | flow_motor.add_reservoir(Reservoir(roads.zones["RES"], ['CAR', 'BUS'], mfdspeed)) 97 | 98 | #### Supervisor #### 99 | supervisor = Supervisor(mlgraph, 100 | demand, 101 | flow_motor, 102 | decision_model, 103 | logfile=log_file, 104 | loglevel=LOGLEVEL.INFO) 105 | 106 | ###################### 107 | ### Run simulation ### 108 | ###################### 109 | 110 | set_all_mnms_logger_level(LOGLEVEL.INFO) 111 | 112 | supervisor.run(tstart, 113 | tend, 114 | dt_flow, 115 | affectation_factor) 116 | -------------------------------------------------------------------------------- /examples/on_demand_car/run.py: -------------------------------------------------------------------------------- 1 | ############### 2 | ### Imports ### 3 | ############### 4 | ## Casuals 5 | import pathlib 6 | 7 | ## MnMS 8 | from mnms.generation.roads import generate_manhattan_road 9 | from mnms.generation.layers import generate_layer_from_roads, generate_grid_origin_destination_layer 10 | from mnms.graph.layers import MultiLayerGraph 11 | from mnms.demand.manager import CSVDemandManager 12 | from mnms.io.graph import save_graph 13 | from mnms.log import set_all_mnms_logger_level, LOGLEVEL 14 | from mnms.travel_decision.dummy import DummyDecisionModel 15 | from mnms.mobility_service.on_demand import OnDemandDepotMobilityService 16 | from mnms.flow.MFD import MFDFlowMotor, Reservoir 17 | from mnms.simulation import Supervisor 18 | from mnms.time import Time, Dt 19 | from mnms.tools.observer import CSVUserObserver, CSVVehicleObserver 20 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 21 | 22 | ################## 23 | ### Parameters ### 24 | ################## 25 | demand_file = pathlib.Path(__file__).parent.joinpath('demand.csv').resolve() 26 | log_file = pathlib.Path(__file__).parent.joinpath('sim.log').resolve() 27 | n_nodes_per_dir = 3 28 | mesh_size = 100 # m 29 | uber_dt_matching = 0 30 | n_odnodes_x = 3 31 | n_odnodes_y = 3 32 | odlayer_xmin = 0 33 | odlayer_ymin = 0 34 | odlayer_xmax = 300 # m 35 | odlayer_ymax = 300 # m 36 | odlayer_connection_dist = 1e-3 # m 37 | def gc_pv(gnodes, layer, link, costs, car_kmcost=0.0005, vot=0.003): 38 | gc = link.length * car_kmcost + vot * link.length / costs["PV"]['speed'] 39 | return gc 40 | def gc_uber(gnodes, layer, link, costs, uber_kmcost=0.0001, vot=0.003): 41 | gc = link.length * uber_kmcost + vot * link.length / costs["UBER"]['speed'] 42 | return gc 43 | def gc_transit(gnodes, layer, link, costs, vot=0.003): 44 | gc = vot * link.length / costs["WALK"]['speed'] 45 | return gc 46 | def gc_waiting(wt, vot=0.003): 47 | return vot * wt 48 | def mfdspeed(dacc): 49 | dspeed = {'CAR': 3} 50 | return dspeed 51 | tstart = Time("06:59:00") 52 | tend = Time("08:00:00") 53 | dt_flow = Dt(minutes=1) 54 | affectation_factor = 1 55 | 56 | ######################### 57 | ### Scenario creation ### 58 | ######################### 59 | 60 | #### RoadDescriptor #### 61 | road_db = generate_manhattan_road(n_nodes_per_dir, mesh_size) # one zone automatically generated 62 | 63 | #### MlGraph #### 64 | uber = OnDemandDepotMobilityService("UBER", uber_dt_matching) 65 | uber.attach_vehicle_observer(CSVVehicleObserver("uber_vehs.csv")) 66 | 67 | pv = PersonalMobilityService('PV') 68 | pv.attach_vehicle_observer(CSVVehicleObserver("pv_vehs.csv")) 69 | 70 | car_layer = generate_layer_from_roads(road_db, 71 | 'CAR', 72 | mobility_services=[pv,uber]) 73 | 74 | odlayer = generate_grid_origin_destination_layer(odlayer_xmin, odlayer_ymin, odlayer_xmax, 75 | odlayer_ymax, n_odnodes_x, n_odnodes_y) 76 | 77 | mlgraph = MultiLayerGraph([car_layer], 78 | odlayer, 79 | odlayer_connection_dist) 80 | 81 | mlgraph.add_cost_function('CAR', 'generalized_cost', gc_pv, mobility_service='PV') 82 | mlgraph.add_cost_function('CAR', 'generalized_cost', gc_uber, mobility_service='UBER') 83 | mlgraph.add_cost_function('TRANSIT', 'generalized_cost', gc_transit) 84 | 85 | #save_graph(mlgraph, demand_file.parent.joinpath('graph.json')) 86 | 87 | # load_graph(demand_file.parent.joinpath('graph.json')) 88 | 89 | # uber.create_waiting_vehicle("CAR_1") # creation of 1 vehicle outside of any depot at CAR_1 node 90 | uber.add_depot("C_82611914", 1) # creation of 1 depot at CAR_1 node with 1 vehicle inside 91 | 92 | #### Demand #### 93 | demand = CSVDemandManager(demand_file) 94 | demand.add_user_observer(CSVUserObserver('user.csv')) 95 | 96 | #### Decision model #### 97 | decision_model = DummyDecisionModel(mlgraph, cost='generalized_cost', 98 | outfile="path.csv", verbose_file=True) 99 | decision_model.add_waiting_cost_function('generalized_cost', gc_waiting) 100 | 101 | #### Flow motor #### 102 | flow_motor = MFDFlowMotor() 103 | flow_motor.add_reservoir(Reservoir(road_db.zones["RES"], ['CAR'], mfdspeed)) 104 | 105 | #### Supervisor #### 106 | supervisor = Supervisor(mlgraph, 107 | demand, 108 | flow_motor, 109 | decision_model, 110 | logfile=log_file, 111 | loglevel=LOGLEVEL.INFO) 112 | 113 | ###################### 114 | ### Run simulation ### 115 | ###################### 116 | 117 | set_all_mnms_logger_level(LOGLEVEL.INFO) 118 | 119 | supervisor.run(tstart, 120 | tend, 121 | dt_flow, 122 | affectation_factor) 123 | -------------------------------------------------------------------------------- /tests/simulation/test_multi_modal.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import tempfile 3 | from pathlib import Path 4 | import pandas as pd 5 | 6 | from mnms.demand import BaseDemandManager, User 7 | from mnms.generation.roads import generate_line_road 8 | from mnms.generation.layers import generate_layer_from_roads, generate_grid_origin_destination_layer, \ 9 | generate_matching_origin_destination_layer 10 | from mnms.graph.layers import MultiLayerGraph, PublicTransportLayer, CarLayer 11 | from mnms.mobility_service.public_transport import PublicTransportMobilityService 12 | 13 | from mnms.travel_decision.dummy import DummyDecisionModel 14 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 15 | from mnms.flow.MFD import MFDFlowMotor, Reservoir 16 | from mnms.simulation import Supervisor 17 | from mnms.time import Time, Dt, TimeTable 18 | from mnms.tools.observer import CSVUserObserver, CSVVehicleObserver 19 | from mnms.vehicles.manager import VehicleManager 20 | from mnms.vehicles.veh_type import Bus 21 | 22 | 23 | class TestMultiModal(unittest.TestCase): 24 | def setUp(self): 25 | """Initiates the test. 26 | """ 27 | self.temp_dir_results = tempfile.TemporaryDirectory() 28 | self.dir_results = Path(self.temp_dir_results.name) 29 | 30 | roads = generate_line_road([0, 0], [0, 5000], 6) 31 | roads.register_stop('S0', '3_4', 0.10) 32 | roads.register_stop('S1', '3_4', 1) 33 | roads.register_stop('S2', '4_5', 1) 34 | 35 | personal_car = PersonalMobilityService() 36 | personal_car.attach_vehicle_observer(CSVVehicleObserver(self.dir_results / "veh_car.csv")) 37 | car_layer = CarLayer(roads, services=[personal_car]) 38 | 39 | car_layer.create_node("CAR_0", "0") 40 | car_layer.create_node("CAR_1", "1") 41 | car_layer.create_node("CAR_2", "2") 42 | car_layer.create_node("CAR_3", "3") 43 | 44 | car_layer.create_link("CAR_0_1", "CAR_0", "CAR_1", {}, ["0_1"]) 45 | car_layer.create_link("CAR_1_2", "CAR_1", "CAR_2", {}, ["1_2"]) 46 | car_layer.create_link("CAR_2_3", "CAR_2", "CAR_3", {}, ["2_3"]) 47 | 48 | bus_service = PublicTransportMobilityService('B0') 49 | pblayer = PublicTransportLayer(roads, 'BUS', Bus, 5, services=[bus_service], 50 | observer=CSVVehicleObserver(self.dir_results / "veh_bus.csv")) 51 | pblayer.create_line("L0", 52 | ["S0", "S1", "S2"], 53 | [["3_4"], ["3_4", "4_5"]], 54 | timetable=TimeTable.create_table_freq('07:00:00', '08:00:00', Dt(minutes=10))) 55 | 56 | odlayer = generate_matching_origin_destination_layer(roads) 57 | # 58 | mlgraph = MultiLayerGraph([car_layer, pblayer], 59 | odlayer, 60 | 1e-3) 61 | 62 | mlgraph.connect_layers("TRANSIT_LINK", "CAR_3", "L0_S0", 100, {}) 63 | 64 | 65 | # Demand 66 | demand = BaseDemandManager([User("U0", [0, 0], [0, 5000], Time("07:00:00"))]) 67 | demand.add_user_observer(CSVUserObserver(self.dir_results / 'user.csv')) 68 | 69 | # Decison Model 70 | decision_model = DummyDecisionModel(mlgraph, outfile=self.dir_results / "path.csv") 71 | 72 | # Flow Motor 73 | def mfdspeed(dacc): 74 | dspeed = {'CAR': 10, 'BUS': 5} 75 | return dspeed 76 | 77 | flow_motor = MFDFlowMotor() 78 | flow_motor.add_reservoir(Reservoir(roads.zones["RES"], ['CAR'], mfdspeed)) 79 | 80 | supervisor = Supervisor(mlgraph, 81 | demand, 82 | flow_motor, 83 | decision_model) 84 | 85 | self.flow_dt = Dt(seconds=10) 86 | supervisor.run(Time("07:00:00"), 87 | Time("07:20:00"), 88 | self.flow_dt, 89 | 10) 90 | 91 | def tearDown(self): 92 | """Concludes and closes the test. 93 | """ 94 | self.temp_dir_results.cleanup() 95 | VehicleManager.empty() 96 | 97 | def test_run_and_results(self): 98 | with open(self.dir_results / "user.csv") as f: 99 | df = pd.read_csv(f, sep=';') 100 | link_list = [l for i,l in enumerate(df['LINK'].tolist()) if i == 0 or (i > 0 and l != df['LINK'].tolist()[i-1])] 101 | self.assertEqual(link_list, ['ORIGIN_0 CAR_0', 'CAR_0 CAR_1', 'CAR_1 CAR_2', 'CAR_2 CAR_3', 'CAR_3 L0_S0', 'L0_S0 L0_S1', 'L0_S1 L0_S2', 'L0_S2 DESTINATION_5']) 102 | 103 | arrival_time = Time(df['TIME'].iloc[-1]) 104 | self.assertGreaterEqual(arrival_time, Time('07:16:40').remove_time(self.flow_dt)) 105 | self.assertLessEqual(arrival_time, Time('07:16:40').add_time(self.flow_dt)) 106 | -------------------------------------------------------------------------------- /tests/demand/test_csv_demand.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pathlib import Path 3 | from mnms.demand.manager import CSVDemandManager, CSVDemandParseError 4 | from mnms.time import Time 5 | 6 | import numpy as np 7 | 8 | 9 | class TestCSVDemand(unittest.TestCase): 10 | def setUp(self): 11 | """Initiates the test. 12 | """ 13 | 14 | self.cwd = Path(__file__).parent.resolve() 15 | 16 | self.file_node = self.cwd.joinpath("data_demand/test_demand_node.csv") 17 | self.file_coordinate = self.cwd.joinpath("data_demand/test_demand_coordinate.csv") 18 | self.file_bad_type1 = self.cwd.joinpath("data_demand/test_demand_bad_type1.csv") 19 | self.file_bad_type2 = self.cwd.joinpath("data_demand/test_demand_bad_type2.csv") 20 | self.file_bad_departure_format1 = self.cwd.joinpath("data_demand/test_demand_bad_departure_format1.csv") 21 | self.file_bad_departure_format2 = self.cwd.joinpath("data_demand/test_demand_bad_departure_format2.csv") 22 | self.file_mobility_services = self.cwd.joinpath("data_demand/test_demand_mobility_services.csv") 23 | self.file_mobility_services_graph = self.cwd.joinpath("data_demand/test_demand_mobility_services_graph.csv") 24 | self.file_bad_optional_columns1 = self.cwd.joinpath("data_demand/test_bad_optional_column1.csv") 25 | self.file_bad_optional_columns2 = self.cwd.joinpath("data_demand/test_bad_optional_column2.csv") 26 | 27 | def tearDown(self): 28 | """Concludes and closes the test. 29 | """ 30 | 31 | def test_demand_node(self): 32 | demand = CSVDemandManager(self.file_node) 33 | user = demand.get_next_departures(Time("07:00:00"), Time("08:00:00")) 34 | 35 | self.assertEqual(demand._demand_type, "node") 36 | self.assertEqual(len(user), 2, "User not loaded") 37 | 38 | user = user[0] 39 | self.assertTrue(isinstance(user.origin, str)) 40 | self.assertTrue(isinstance(user.destination, str)) 41 | 42 | self.assertEqual(user.origin, "A") 43 | self.assertEqual(user.destination, "B") 44 | 45 | def test_demand_coordinate(self): 46 | demand = CSVDemandManager(self.file_coordinate) 47 | user = demand.get_next_departures(Time("07:00:00"), Time("08:00:00")) 48 | 49 | self.assertEqual(demand._demand_type, "coordinate") 50 | self.assertEqual(len(user), 2, "User not loaded") 51 | 52 | user = user[0] 53 | self.assertTrue(isinstance(user.origin, np.ndarray)) 54 | self.assertTrue(isinstance(user.destination, np.ndarray)) 55 | 56 | self.assertEqual(user.origin[0], 0.) 57 | self.assertEqual(user.origin[1], 0.) 58 | self.assertEqual(user.destination[0], 1000.) 59 | self.assertEqual(user.destination[1], 1000.) 60 | 61 | def test_demand_type_error(self): 62 | with self.assertRaises(CSVDemandParseError): 63 | CSVDemandManager(self.file_bad_type1) 64 | 65 | with self.assertRaises(CSVDemandParseError): 66 | CSVDemandManager(self.file_bad_type2) 67 | 68 | def test_demand_departure_format_error(self): 69 | with self.assertRaises(CSVDemandParseError): 70 | CSVDemandManager(self.file_bad_departure_format1) 71 | 72 | with self.assertRaises(CSVDemandParseError): 73 | CSVDemandManager(self.file_bad_departure_format2) 74 | 75 | def test_optional_columns(self): 76 | """Check the definition of users in a CSV file with the list of available mobility services 77 | or the mobility services graph. 78 | """ 79 | demand = CSVDemandManager(self.file_mobility_services) 80 | users = demand.get_next_departures(Time("07:00:00"), Time("08:00:00")) 81 | self.assertEqual(users[0].available_mobility_services, {'CAR', 'RIDEHAILING'}) 82 | self.assertEqual(users[0].mobility_services_graph, None) 83 | self.assertEqual(users[1].available_mobility_services, {'CAR'}) 84 | self.assertEqual(users[1].mobility_services_graph, None) 85 | 86 | demand = CSVDemandManager(self.file_mobility_services_graph) 87 | users = demand.get_next_departures(Time("07:00:00"), Time("08:00:00")) 88 | self.assertEqual(users[0].available_mobility_services, None) 89 | self.assertEqual(users[0].mobility_services_graph, 'G1') 90 | self.assertEqual(users[1].available_mobility_services, None) 91 | self.assertEqual(users[1].mobility_services_graph, 'G2') 92 | 93 | 94 | def test_optional_columns_error(self): 95 | """Check that bad definition of optional columns in the CSV file triggers an error. 96 | """ 97 | with self.assertRaises(CSVDemandParseError): 98 | CSVDemandManager(self.file_bad_optional_columns1) 99 | 100 | with self.assertRaises(CSVDemandParseError): 101 | CSVDemandManager(self.file_bad_optional_columns2) 102 | -------------------------------------------------------------------------------- /examples/vehicle_sharing/run_intermodality.py: -------------------------------------------------------------------------------- 1 | ############### 2 | ### Imports ### 3 | ############### 4 | ## Casuals 5 | import pathlib 6 | 7 | # MnMS 8 | from mnms.generation.roads import generate_manhattan_road 9 | from mnms.generation.layers import generate_layer_from_roads, generate_grid_origin_destination_layer 10 | from mnms.mobility_service.vehicle_sharing import VehicleSharingMobilityService 11 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 12 | from mnms.tools.observer import CSVVehicleObserver, CSVUserObserver 13 | from mnms.graph.layers import SharedVehicleLayer, MultiLayerGraph 14 | from mnms.vehicles.veh_type import Bike 15 | from mnms.travel_decision.dummy import DummyDecisionModel 16 | from mnms.flow.MFD import MFDFlowMotor, Reservoir 17 | from mnms.simulation import Supervisor 18 | from mnms.demand import CSVDemandManager 19 | from mnms.time import Time, Dt 20 | from mnms.log import set_mnms_logger_level, LOGLEVEL, attach_log_file 21 | from mnms.io.graph import save_graph, load_graph 22 | 23 | ################## 24 | ### Parameters ### 25 | ################## 26 | demand_file = pathlib.Path(__file__).parent.joinpath('demand.csv').resolve() 27 | mlgraph_file = 'intermodality_example.json' 28 | log_file = 'sim.log' 29 | b_from_json=False 30 | n_nodes_per_dir = 3 31 | mesh_size = 1000 # m 32 | n_odnodes_x = 5 33 | n_odnodes_y = 5 34 | odlayer_xmin = -1000 35 | odlayer_ymin = -1000 36 | odlayer_xmax = 3000 # m 37 | odlayer_ymax = 3000 # m$ 38 | b_freefloating = 0 39 | velov_dt_matching = 1 40 | velov_default_speed = 3 # m/s 41 | odlayer_connection_dist = 500 # m 42 | car_velov_connection_dist = 300 # m 43 | def mfdspeed(dacc): 44 | dspeed = {'CAR':5.55, 'BIKE': 6.66} 45 | return dspeed 46 | tstart = Time("06:50:00") 47 | tend = Time("09:00:00") 48 | dt_flow = Dt(minutes=1) 49 | affectation_factor = 10 50 | 51 | ######################### 52 | ### Scenario creation ### 53 | ######################### 54 | 55 | #### RoadDescriptor and MLGraph #### 56 | if b_from_json: 57 | mlgraph=load_graph(mlgraph_file) 58 | 59 | # OD layer 60 | odlayer = generate_grid_origin_destination_layer(odlayer_xmin, odlayer_ymin, 61 | odlayer_xmax, odlayer_ymax, n_odnodes_x, n_odnodes_y) 62 | mlgraph.add_origin_destination_layer(odlayer) 63 | else: 64 | # Graph 65 | road_db = generate_manhattan_road(n_nodes_per_dir, mesh_size) 66 | 67 | # OD layer 68 | odlayer = generate_grid_origin_destination_layer(odlayer_xmin, odlayer_ymin, 69 | odlayer_xmax, odlayer_ymax, n_odnodes_x, n_odnodes_y) 70 | 71 | # Personnal car mobility service 72 | personal_car = PersonalMobilityService() 73 | personal_car.attach_vehicle_observer(CSVVehicleObserver("pv_vehs.csv")) 74 | car_layer = generate_layer_from_roads(road_db, 75 | 'car_layer', 76 | mobility_services=[personal_car]) 77 | 78 | # Vehicle sharing mobility service 79 | velov = VehicleSharingMobilityService("velov", b_freefloating, velov_dt_matching) 80 | velov.attach_vehicle_observer(CSVVehicleObserver("velov_vehs.csv")) 81 | velov_layer = generate_layer_from_roads(road_db, 'velov_layer', SharedVehicleLayer, Bike, velov_default_speed, [velov]) 82 | 83 | # Multilayer graph 84 | mlgraph = MultiLayerGraph([car_layer,velov_layer], odlayer) 85 | 86 | if not b_from_json: 87 | save_graph(mlgraph, mlgraph_file) 88 | 89 | # Add stations 90 | mlgraph.layers['velov_layer'].mobility_services['velov'].create_station('S1', '1', capacity=20, nb_initial_veh=5) 91 | mlgraph.layers['velov_layer'].mobility_services['velov'].create_station('S2', 'SOUTH_2', capacity=20, nb_initial_veh=0) 92 | 93 | # Connect od layer and velov layer 94 | mlgraph.connect_origindestination_layers(odlayer_connection_dist) 95 | 96 | # Connect personnal car layer and velov layer 97 | mlgraph.connect_inter_layers(['car_layer','velov_layer'], car_velov_connection_dist) 98 | 99 | #### Decision model #### 100 | decision_model = DummyDecisionModel(mlgraph, outfile="paths.csv", verbose_file=True) 101 | 102 | #### Flow motor #### 103 | flow_motor = MFDFlowMotor(outfile="flow.csv") 104 | flow_motor.add_reservoir(Reservoir(mlgraph.roads.zones['RES'], ['BIKE', 'CAR'], mfdspeed)) 105 | 106 | #### Demand #### 107 | demand = CSVDemandManager(demand_file) 108 | demand.add_user_observer(CSVUserObserver('users.csv')) 109 | 110 | #### Supervisor #### 111 | supervisor = Supervisor(mlgraph, 112 | demand, 113 | flow_motor, 114 | decision_model) 115 | 116 | ###################### 117 | ### Run simulation ### 118 | ###################### 119 | 120 | # set_all_mnms_logger_level(LOGLEVEL.WARNING) 121 | set_mnms_logger_level(LOGLEVEL.INFO, ["mnms.simulation"]) 122 | 123 | # get_logger("mnms.graph.shortest_path").setLevel(LOGLEVEL.WARNING) 124 | attach_log_file(log_file) 125 | 126 | supervisor.run(tstart, 127 | tend, 128 | dt_flow, 129 | affectation_factor) 130 | -------------------------------------------------------------------------------- /script/conversion/bison/coordinates.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | X0 = 155000 4 | Y0 = 463000 5 | PHI0 = 52.15517440 6 | LAM0 = 5.38720621 7 | 8 | K0 = 0.9996 9 | 10 | E = 0.00669438 11 | E2 = E * E 12 | E3 = E2 * E 13 | E_P2 = E / (1 - E) 14 | 15 | SQRT_E = np.sqrt(1 - E) 16 | _E = (1 - SQRT_E) / (1 + SQRT_E) 17 | _E2 = _E * _E 18 | _E3 = _E2 * _E 19 | _E4 = _E3 * _E 20 | _E5 = _E4 * _E 21 | 22 | M1 = (1 - E / 4 - 3 * E2 / 64 - 5 * E3 / 256) 23 | M2 = (3 * E / 8 + 3 * E2 / 32 + 45 * E3 / 1024) 24 | M3 = (15 * E2 / 256 + 45 * E3 / 1024) 25 | M4 = (35 * E3 / 3072) 26 | 27 | P2 = (3 / 2 * _E - 27 / 32 * _E3 + 269 / 512 * _E5) 28 | P3 = (21 / 16 * _E2 - 55 / 32 * _E4) 29 | P4 = (151 / 96 * _E3 - 417 / 128 * _E5) 30 | P5 = (1097 / 512 * _E4) 31 | 32 | R = 6378137 33 | 34 | def latlon_to_zone_number(latitude, longitude): 35 | 36 | if isinstance(latitude, np.ndarray): 37 | latitude = latitude.flat[0] 38 | if isinstance(longitude, np.ndarray): 39 | longitude = longitude.flat[0] 40 | 41 | if 56 <= latitude < 64 and 3 <= longitude < 12: 42 | return 32 43 | 44 | if 72 <= latitude <= 84 and longitude >= 0: 45 | if longitude < 9: 46 | return 31 47 | elif longitude < 21: 48 | return 33 49 | elif longitude < 33: 50 | return 35 51 | elif longitude < 42: 52 | return 37 53 | 54 | return int((longitude + 180) / 6) + 1 55 | 56 | def zone_number_to_central_longitude(zone_number): 57 | return (zone_number - 1) * 6 - 180 + 3 58 | 59 | def mod_angle(value): 60 | return (value + np.pi) % (2 * np.pi) - np.pi 61 | 62 | def rd_to_wgs(x, y): 63 | 64 | if isinstance(x, (list, tuple)): 65 | x, y = x 66 | 67 | pqk = [(0, 1, 3235.65389), 68 | (2, 0, -32.58297), 69 | (0, 2, -0.24750), 70 | (2, 1, -0.84978), 71 | (0, 3, -0.06550), 72 | (2, 2, -0.01709), 73 | (1, 0, -0.00738), 74 | (4, 0, 0.00530), 75 | (2, 3, -0.00039), 76 | (4, 1, 0.00033), 77 | (1, 1, -0.00012)] 78 | 79 | pql = [(1, 0, 5260.52916), 80 | (1, 1, 105.94684), 81 | (1, 2, 2.45656), 82 | (3, 0, -0.81885), 83 | (1, 3, 0.05594), 84 | (3, 1, -0.05607), 85 | (0, 1, 0.01199), 86 | (3, 2, -0.00256), 87 | (1, 4, 0.00128), 88 | (0, 2, 0.00022), 89 | (2, 0, -0.00022), 90 | (5, 0, 0.00026)] 91 | 92 | dx = 1E-5 * (x - X0) 93 | dy = 1E-5 * (y - Y0) 94 | 95 | phi = PHI0 96 | lam = LAM0 97 | 98 | for p, q, k in pqk: 99 | phi += k * dx ** p * dy ** q / 3600 100 | 101 | for p, q, l in pql: 102 | lam += l * dx ** p * dy ** q / 3600 103 | 104 | return (phi, lam) 105 | 106 | def wgs_to_utm(latitude, longitude): 107 | lat_rad = np.radians(latitude) 108 | lat_sin = np.sin(lat_rad) 109 | lat_cos = np.cos(lat_rad) 110 | 111 | lat_tan = lat_sin / lat_cos 112 | lat_tan2 = lat_tan * lat_tan 113 | lat_tan4 = lat_tan2 * lat_tan2 114 | 115 | zone_number = latlon_to_zone_number(latitude, longitude) 116 | 117 | lon_rad = np.radians(longitude) 118 | central_lon = zone_number_to_central_longitude(zone_number) 119 | central_lon_rad = np.radians(central_lon) 120 | 121 | n = R / np.sqrt(1 - E * lat_sin**2) 122 | c = E_P2 * lat_cos**2 123 | 124 | a = lat_cos * mod_angle(lon_rad - central_lon_rad) 125 | a2 = a * a 126 | a3 = a2 * a 127 | a4 = a3 * a 128 | a5 = a4 * a 129 | a6 = a5 * a 130 | 131 | m = R * (M1 * lat_rad - 132 | M2 * np.sin(2 * lat_rad) + 133 | M3 * np.sin(4 * lat_rad) - 134 | M4 * np.sin(6 * lat_rad)) 135 | 136 | easting = K0 * n * (a + 137 | a3 / 6 * (1 - lat_tan2 + c) + 138 | a5 / 120 * (5 - 18 * lat_tan2 + lat_tan4 + 72 * c - 58 * E_P2)) + 500000 139 | 140 | northing = K0 * (m + n * lat_tan * (a2 / 2 + 141 | a4 / 24 * (5 - lat_tan2 + 9 * c + 4 * c**2) + 142 | a6 / 720 * (61 - 58 * lat_tan2 + lat_tan4 + 600 * c - 330 * E_P2))) 143 | 144 | return (easting, northing) 145 | 146 | 147 | def rd_to_utm(x, y): 148 | phi, lam = rd_to_wgs(x, y) 149 | easting, northing = wgs_to_utm(phi, lam) 150 | return (easting, northing) 151 | 152 | 153 | ## EXAMPLE 154 | 155 | # coord_rd = [[121687, 487484], # Amsterdam 156 | # [92565, 437428], # Rotterdam 157 | # [176331, 317462]] # Maastricht 158 | # 159 | # coord_wgs = [[52.37422, 4.89801], # Amsterdam 160 | # [51.92183, 4.47959], # Rotterdam 161 | # [50.84660, 5.69006]] # Maastricht 162 | # 163 | # for x, y in coord_wgs: 164 | # print(wgs_to_utm(x, y)) 165 | # rd_to_utm(121687, 487484) 166 | 167 | -------------------------------------------------------------------------------- /src/mnms/mobility_service/personal_vehicle.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, List, Dict 2 | 3 | from mnms.demand import User 4 | from mnms.demand.user import UserState 5 | from mnms.mobility_service.abstract import AbstractMobilityService, Request 6 | from mnms.time import Dt 7 | from mnms.vehicles.veh_type import VehicleActivityServing, ActivityType, Vehicle, VehicleActivity 8 | from mnms.tools.cost import create_service_costs 9 | 10 | class PersonalMobilityService(AbstractMobilityService): 11 | def __init__(self, id: str = 'PersonalVehicle'): 12 | super(PersonalMobilityService, self).__init__(id, veh_capacity=1, dt_matching=0, dt_periodic_maintenance=0) 13 | 14 | def is_personal(self): 15 | return True 16 | 17 | def step_maintenance(self, dt: Dt): 18 | """Method that proceeds to the maintenance phase. For PersonalMobilityService, 19 | it consists in deleting the vehicles of users who already arrived at their 20 | destination. 21 | 22 | Args: 23 | -dt: time elapsed since the previous maintenance phase 24 | """ 25 | for veh in list(self.fleet.vehicles.values()): 26 | if veh.activity_type is ActivityType.STOP: 27 | if veh.last_dropped_off_user is None or (veh.last_dropped_off_user is not None and veh.last_dropped_off_user.state == UserState.ARRIVED): 28 | self.fleet.delete_vehicle(veh.id) 29 | 30 | def periodic_maintenance(self, dt: Dt): 31 | pass 32 | 33 | def request(self, user: User, drop_node: str) -> Dt: 34 | """Method that returns the expected pick-up time. 35 | 36 | Args: 37 | -user: user who requested the service 38 | -drop_node: node where user would like to be droppped off 39 | 40 | Returns: 41 | -pickup_time: expected time for user to be picked up 42 | """ 43 | pickup_time = Dt() 44 | return pickup_time 45 | 46 | def matching(self, request: Request, dt: Dt): 47 | """Method that proceeds to the matching of a user with a personal vehicle. 48 | 49 | Args: 50 | -request: the request to match 51 | -dt: the flow time step 52 | """ 53 | user = request.user 54 | drop_node = request.drop_node 55 | upath = list(user.path.nodes) 56 | upath = upath[user.get_current_node_index():user.get_node_index_in_path(drop_node) + 1] 57 | veh_path = self.construct_veh_path(upath) 58 | # Check if user has already used her personal vehicle 59 | if self.id in user.personal_vehicles: 60 | # If so, match her with her own vehicle 61 | veh = user.personal_vehicles[self.id] 62 | assert veh.activity_type is ActivityType.STOP, f'Personal vehicle of user {user.id} is not stopped...' 63 | assert veh.current_node == upath[0], f'User {user.id} tried to take her personal vehcile back at the wrong node ({veh.current_node} != {upath[0]})' 64 | activities = [VehicleActivityServing(node=upath[-1], 65 | path=veh_path, 66 | user=user)] 67 | veh.add_activities(activities) 68 | veh.activity.is_done = True 69 | # For personal vehicle, this is always an immediate match because 70 | # dt_matching is null, so take into account effective remaining 71 | # duration to move during the current flow step 72 | veh.dt_move = self._tcurrent - request.request_time 73 | else: 74 | # If not, create a new personal vehicle 75 | new_veh = self.fleet.create_vehicle(upath[0], 76 | capacity=self._veh_capacity, 77 | activities=[VehicleActivityServing(node=upath[-1], 78 | path=veh_path, 79 | user=user)]) 80 | user.add_personal_vehicle(self.id, new_veh) 81 | if self._observer is not None: 82 | new_veh.attach(self._observer) 83 | # Take into account effective remaining duration to move during the 84 | # current flow step 85 | new_veh.dt_move = self._tcurrent - request.request_time if self._tcurrent is not None else None 86 | 87 | def replanning(self, veh: Vehicle, new_activities: List[VehicleActivity]) -> List[VehicleActivity]: 88 | pass 89 | 90 | def rebalancing(self, next_demand: List[User], horizon: List[Vehicle]): 91 | pass 92 | 93 | def service_level_costs(self, nodes: List[str]) -> dict: 94 | return create_service_costs() 95 | 96 | @classmethod 97 | def __load__(cls, data): 98 | new_obj = cls(data['ID']) 99 | return new_obj 100 | 101 | def __dump__(self): 102 | return {"TYPE": ".".join([PersonalMobilityService.__module__, PersonalMobilityService.__name__]), 103 | "ID": self.id} 104 | -------------------------------------------------------------------------------- /src/mnms/graph/road.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, asdict 2 | from typing import List, Optional, Dict 3 | 4 | import numpy as np 5 | 6 | from mnms.graph.zone import Zone, construct_zone_from_contour 7 | 8 | 9 | def _compute_dist(pos1: np.ndarray, pos2: np.ndarray): 10 | return np.linalg.norm(pos2-pos1) 11 | 12 | 13 | @dataclass(slots=True) 14 | class RoadNode: 15 | id: str 16 | position: np.ndarray 17 | 18 | 19 | @dataclass(slots=True) 20 | class RoadSection: 21 | id: str 22 | upstream: str 23 | downstream: str 24 | length: float 25 | zone: Optional[str] = None 26 | 27 | 28 | @dataclass(slots=True) 29 | class RoadStop: 30 | id: str 31 | section: str 32 | relative_position: float 33 | absolute_position: np.ndarray 34 | 35 | 36 | class RoadDescriptor(object): 37 | __slots__ = ('nodes', 'sections', 'zones', 'stops') 38 | 39 | def __init__(self): 40 | """ 41 | Object describing the physical roads 42 | """ 43 | self.nodes: Dict[str, RoadNode] = dict() 44 | self.stops: Dict[str, RoadStop] = dict() 45 | self.sections: Dict[str, RoadSection] = dict() 46 | 47 | self.zones = dict() 48 | 49 | def register_node(self, nid: str, pos: List[float]): 50 | self.nodes[nid] = RoadNode(nid, np.array(pos)) 51 | 52 | def register_stop(self, sid: str, lid: str, relative_position: float): 53 | assert 0 <= relative_position <= 1, f"relative_position must be between 0 and 1" 54 | 55 | sec = self.sections[lid] 56 | up_node_pos = self.nodes[sec.upstream].position 57 | down_node_pos = self.nodes[sec.downstream].position 58 | 59 | abs_pos = up_node_pos + (down_node_pos - up_node_pos)*relative_position 60 | 61 | self.stops[sid] = RoadStop(sid, lid, relative_position, abs_pos) 62 | 63 | def register_stop_abs(self, sid: str, lid: str, relative_position: float, abs_pos): 64 | assert 0 <= relative_position <= 1, f"relative_position must be between 0 and 1" 65 | 66 | self.stops[sid] = RoadStop(sid, lid, relative_position, abs_pos) 67 | 68 | def register_section(self, lid: str, upstream: str, downstream: str, length: Optional[float] = None): 69 | assert upstream in self.nodes, f"{upstream} node is not registered" 70 | assert downstream in self.nodes, f"{downstream} node is not registered " 71 | 72 | section_length = length if length is not None else _compute_dist(self.nodes[upstream].position, self.nodes[downstream].position) 73 | 74 | self.sections[lid] = RoadSection(lid, 75 | upstream, 76 | downstream, 77 | section_length) 78 | 79 | def add_zone(self, zone: Zone): 80 | self.zones[zone.id] = zone 81 | zid = zone.id 82 | for l in zone.sections: 83 | self.sections[l].zone = zid 84 | 85 | def delete_nodes(self, nids: List[str]): 86 | for nid in nids: 87 | assert nid in list(self.nodes.keys()), f'Node {nid} does not exists in RoadDescriptor' 88 | # Remove node and all links from and to this node 89 | del self.nodes[nid] 90 | links_to_remove = [] 91 | for lid, rsect in self.sections.items(): 92 | if rsect.upstream == nid or rsect.downstream == nid: 93 | links_to_remove.append(lid) 94 | for lid in links_to_remove: 95 | self.delete_section(lid) 96 | 97 | def delete_section(self, lid: str): 98 | assert lid in self.sections.keys(), f'In delete_section: section id {lid} not found in roads sections' 99 | del self.sections[lid] 100 | 101 | def translate(self, v: List[float]): 102 | for n in self.nodes.keys(): 103 | self.nodes[n].position = np.add(self.nodes[n].position, v) 104 | 105 | def __dump__(self): 106 | return {'NODES': {key: asdict(val) for key, val in self.nodes.items()}, 107 | 'STOPS': {key: asdict(val) for key, val in self.stops.items()}, 108 | 'SECTIONS': {key: asdict(val) for key, val in self.sections.items()}, 109 | 'ZONES': {key: asdict(val) for key, val in self.zones.items()}} 110 | 111 | @classmethod 112 | def __load__(cls, data): 113 | new_obj = cls() 114 | new_obj.nodes = {key: RoadNode(val["id"], np.array(val["position"])) for key, val in data['NODES'].items()} 115 | new_obj.stops = {key: RoadStop(val["id"], val["section"], val["relative_position"], np.array(val["absolute_position"])) for key, val in data['STOPS'].items()} 116 | 117 | for lid, d in data['SECTIONS'].items(): 118 | new_obj.register_section(lid, d['upstream'], d['downstream'], d['length']) 119 | 120 | for z in data["ZONES"].values(): 121 | if z["sections"]: 122 | new_obj.add_zone(Zone(z["id"], set(z["sections"]), z["contour"])) 123 | else: 124 | new_obj.add_zone(construct_zone_from_contour(new_obj, z["id"], z["contour"])) 125 | 126 | return new_obj 127 | -------------------------------------------------------------------------------- /tests/mobility_services/test_public_transport_several_pickups.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import unittest 3 | from pathlib import Path 4 | 5 | from tempfile import TemporaryDirectory 6 | from mnms.demand import BaseDemandManager, User 7 | from mnms.demand.user import UserState 8 | from mnms.flow.MFD import MFDFlowMotor, Reservoir 9 | from mnms.generation.layers import generate_matching_origin_destination_layer 10 | from mnms.generation.roads import generate_line_road 11 | from mnms.graph.layers import MultiLayerGraph, PublicTransportLayer 12 | from mnms.mobility_service.public_transport import PublicTransportMobilityService 13 | from mnms.simulation import Supervisor 14 | from mnms.time import Time, Dt, TimeTable 15 | from mnms.tools.observer import CSVUserObserver, CSVVehicleObserver 16 | from mnms.travel_decision.dummy import DummyDecisionModel 17 | from mnms.vehicles.veh_type import Bus 18 | from mnms.vehicles.veh_type import ActivityType 19 | 20 | 21 | class TestPublicTransportSeveralPickups(unittest.TestCase): 22 | def setUp(self): 23 | """Initiates the test. 24 | """ 25 | self.temp_dir_results = TemporaryDirectory() 26 | self.dir_results = Path(self.temp_dir_results.name) 27 | 28 | roads = generate_line_road([0, 0], [0, 5000], 2) 29 | roads.register_stop('S0', '0_1', 0) 30 | roads.register_stop('S1', '0_1', 0.2) 31 | roads.register_stop('S2', '0_1', 0.4) 32 | roads.register_stop('S3', '0_1', 0.6) 33 | roads.register_stop('S4', '0_1', 0.8) 34 | roads.register_stop('S5', '0_1', 1) 35 | self.roads = roads 36 | 37 | bus_service = PublicTransportMobilityService('B0') 38 | pblayer = PublicTransportLayer(roads, 'BUS', Bus, 10, services=[bus_service], 39 | observer=CSVVehicleObserver(self.dir_results / "veh.csv")) 40 | 41 | pblayer.create_line('L0', 42 | ['S0', 'S1', 'S2', 'S3', 'S4', 'S5'], 43 | [['0_1'], ['0_1'], ['0_1'], ['0_1'], ['0_1']], 44 | TimeTable.create_table_freq('07:00:00', '08:30:00', Dt(minutes=10))) 45 | 46 | odlayer = generate_matching_origin_destination_layer(roads) 47 | 48 | 49 | mlgraph = MultiLayerGraph([pblayer], 50 | odlayer, 51 | 100) 52 | self.mlgraph = mlgraph 53 | 54 | # Demand 55 | demand = BaseDemandManager([User("U0", [0, 1000], [0, 3000], Time("07:00:00")), 56 | User("U1", [0, 2000], [0, 4000], Time("07:00:00")), 57 | User("U2", [0, 2000], [0, 4000], Time("07:10:00")), 58 | User("U3", [0, 1000], [0, 3000], Time("07:10:00")), 59 | User("U4", [0, 1000], [0, 3000], Time("07:19:00")), 60 | User("U5", [0, 2000], [0, 4000], Time("07:21:00")), 61 | User("U6", [0, 3000], [0, 4000], Time("07:30:00")), 62 | User("U7", [0, 1000], [0, 2000], Time("07:30:00")), 63 | User("U8", [0, 1000], [0, 5000], Time("07:38:00")), 64 | User("U9", [0, 2000], [0, 3000], Time("07:41:00")), 65 | User("U10", [0, 3000], [0, 4000], Time("07:45:00")), 66 | User("U11", [0, 3000], [0, 4000], Time("07:49:00")), 67 | User("U12", [0, 2000], [0, 4000], Time("07:50:40")), 68 | User("U13", [0, 2000], [0, 4000], Time("07:59:00")), 69 | User("U14", [0, 2000], [0, 4000], Time("07:59:00"))]) 70 | demand.add_user_observer(CSVUserObserver(self.dir_results / 'user.csv')) 71 | self.demand = demand 72 | 73 | # Decison Model 74 | decision_model = DummyDecisionModel(mlgraph, outfile=self.dir_results / "path.csv") 75 | 76 | # Flow Motor 77 | def mfdspeed(dacc): 78 | dacc['BUS'] = 10 79 | return dacc 80 | 81 | flow_motor = MFDFlowMotor() 82 | flow_motor.add_reservoir(Reservoir(roads.zones['RES'], ['BUS'], mfdspeed)) 83 | 84 | supervisor = Supervisor(mlgraph, 85 | demand, 86 | flow_motor, 87 | decision_model) 88 | self.supervisor = supervisor 89 | self.supervisor.run(Time("07:00:00"), 90 | Time("08:30:00"), 91 | Dt(minutes=1), 92 | 1) 93 | 94 | def tearDown(self): 95 | """Concludes and closes the test. 96 | """ 97 | self.temp_dir_results.cleanup() 98 | 99 | def test_run_and_results(self): 100 | # Check that simulation has run till the end 101 | pass 102 | 103 | def test_users_arrival(self): 104 | # Check that all users have arrived at destination 105 | for user in self.demand._users: 106 | self.assertEqual(user.state,UserState.ARRIVED) 107 | 108 | def test_buses_arrival(self): 109 | # Check that buses from id 0 to id 5 have arrived 110 | for veh in self.supervisor._flow_motor.veh_manager._vehicles.values(): 111 | self.assertEqual(veh.activity_type, ActivityType.STOP) 112 | self.assertEqual(veh._current_node, 'L0_S5') 113 | -------------------------------------------------------------------------------- /src/mnms/io/graph.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Union 3 | from pathlib import Path 4 | 5 | from hipop.graph import link_to_dict, dict_to_link 6 | 7 | from mnms.graph.layers import OriginDestinationLayer 8 | from mnms.graph.layers import MultiLayerGraph 9 | from mnms.graph.road import RoadDescriptor 10 | from mnms.io.utils import MNMSEncoder, load_class_by_module_name 11 | 12 | 13 | def save_graph(mlgraph: MultiLayerGraph, filename: Union[str, Path], indent=2): 14 | """Save a MultiModalGraph as a JSON file 15 | 16 | Args: 17 | mmgraph: Graph to save 18 | filename: Name of the JSON file 19 | indent: Indentation of the JSON 20 | 21 | Returns: 22 | 23 | """ 24 | 25 | d = {'ROADS': mlgraph.roads.__dump__(), 26 | 'LAYERS': [l.__dump__() for l in mlgraph.layers.values()], 27 | 'TRANSIT': [link_to_dict(mlgraph.graph.links[lid]) for lid in mlgraph.transitlayer.iter_inter_links()]} 28 | 29 | with open(filename, 'w') as f: 30 | json.dump(d, f, indent=indent, cls=MNMSEncoder) 31 | 32 | 33 | def load_graph(filename: Union[str, Path]): 34 | """ 35 | Load the graph from a JSON file 36 | 37 | Args: 38 | filename: the path to the JSON 39 | 40 | Returns: 41 | 42 | """ 43 | with open(filename, 'r') as f: 44 | data = json.load(f) 45 | 46 | roads = RoadDescriptor.__load__(data['ROADS']) 47 | layers = [] 48 | for ldata in data['LAYERS']: 49 | layer_type = load_class_by_module_name(ldata['TYPE']) 50 | layers.append(layer_type.__load__(ldata, roads)) 51 | 52 | mlgraph = MultiLayerGraph(layers) 53 | 54 | if "TRANSIT" in data: 55 | for data_link in data["TRANSIT"]: 56 | mlgraph.connect_layers(data_link["ID"], 57 | data_link["UPSTREAM"], 58 | data_link["DOWNSTREAM"], 59 | data_link["LENGTH"], 60 | data_link["COSTS"]) 61 | 62 | return mlgraph 63 | 64 | 65 | def save_odlayer(odlayer: OriginDestinationLayer, filename: Union[str, Path], indent=2): 66 | """ 67 | Save the OriginDestinationLayer 68 | 69 | Args: 70 | odlayer: The OriginDestinationLayer 71 | filename: the path where to save the file 72 | indent: the indentation of the JSON 73 | 74 | Returns: 75 | 76 | 77 | """ 78 | d = odlayer.__dump__() 79 | with open(filename, 'w') as f: 80 | json.dump(d, f, indent=indent, cls=MNMSEncoder) 81 | 82 | 83 | def save_transit_link_odlayer(mlgraph: MultiLayerGraph, filename: Union[str, Path], indent=2): 84 | """ 85 | Save only the transit links between the OriginDestinationLayer and the MultiLayerGraph 86 | 87 | Args: 88 | mlgraph: the MultiLayerGraph 89 | filename: the path where to save the file 90 | indent: the indentation of the JSON 91 | 92 | Returns: 93 | 94 | """ 95 | links = [] 96 | gnodes = mlgraph.graph.nodes 97 | for origin in mlgraph.odlayer.origins: 98 | for link in gnodes[origin].adj.values(): 99 | links.append(link_to_dict(link)) 100 | 101 | for destination in mlgraph.odlayer.destinations: 102 | for link in gnodes[destination].radj.values(): 103 | links.append(link_to_dict(link)) 104 | 105 | data = {"LINKS": links} 106 | with open(filename, 'w') as f: 107 | json.dump(data, f, indent=indent, cls=MNMSEncoder) 108 | 109 | 110 | def save_transit_links(mlgraph: MultiLayerGraph, filename: Union[str, Path], indent=2): 111 | """ 112 | Save all the transit links in the MultiLayerGraph 113 | 114 | Args: 115 | mlgraph: the MultiLayerGraph 116 | filename: the path where to save the file 117 | indent: the indentation of the JSON 118 | 119 | Returns: 120 | 121 | """ 122 | links = [] 123 | for link in mlgraph.graph.links.values(): 124 | if link.label == "TRANSIT": 125 | links.append(link_to_dict(link)) 126 | 127 | data = {"LINKS": links} 128 | with open(filename, 'w') as f: 129 | json.dump(data, f, indent=indent, cls=MNMSEncoder) 130 | 131 | 132 | def load_transit_links(mlgraph: MultiLayerGraph, filename: Union[str, Path]): 133 | """ 134 | Load the transit kinks in a MultiLayerGraph 135 | 136 | Args: 137 | mlgraph: The MultiLayerGraph in which to insert the links 138 | filename: The file where the links are stored 139 | 140 | Returns: 141 | 142 | 143 | """ 144 | with open(filename, 'r') as f: 145 | data = json.load(f) 146 | 147 | oriented_graph = mlgraph.graph 148 | for link in data['LINKS']: 149 | dict_to_link(oriented_graph, link) 150 | 151 | 152 | def load_odlayer(filename: Union[str, Path]) -> OriginDestinationLayer: 153 | """ 154 | Load the OriginDestinationLayer from a file 155 | 156 | Args: 157 | filename: The path to the file 158 | 159 | Returns: 160 | The loaded OriginDestinationLayer 161 | 162 | """ 163 | with open(filename, 'r') as f: 164 | data = json.load(f) 165 | 166 | return OriginDestinationLayer.__load__(data) 167 | -------------------------------------------------------------------------------- /src/mnms/tools/preprocessing.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | import json 3 | import time 4 | 5 | from hipop.shortest_path import parallel_dijkstra, parallel_dijkstra_single_source, floyd_warshall 6 | 7 | def compute_all_shortest_paths_naive(graph, chosen_mservice, layer_name, outfile): 8 | """Fonction that pre-computes the shortest paths for each pair of nodes of the 9 | graph of a layer and write them to a file. By shortest we mean in distance. 10 | It is a naive approach that launches in parallel dijkstra for all pairs. 11 | 12 | Args: 13 | -graph: graph of the layer 14 | -chosen_mservice: dict with a unique element {'layer_name': 'mob_service_name'} 15 | -layer_name: name of the layer 16 | -outfile: file where the shortest paths will be saved 17 | """ 18 | # Launch parallel dijkstra on all od pairs 19 | all_ods = [(n1,n2) for n1 in graph.nodes for n2 in graph.nodes if n1 != n2] 20 | origins = [t[0] for t in all_ods] 21 | destinations = [t[1] for t in all_ods] 22 | print(f'There are {len(origins)} shortest paths to compute...') 23 | 24 | paths = parallel_dijkstra(graph, 25 | origins, 26 | destinations, 27 | [chosen_mservice]*len(origins), 28 | 'length', 29 | multiprocessing.cpu_count(), 30 | [{layer_name}]*len(origins)) 31 | 32 | # Build a dict of shortest paths 33 | sps = {} 34 | for i,(o,d) in enumerate(all_ods): 35 | if o in sps.keys(): 36 | sps[o][d] = paths[i][0][0] 37 | else: 38 | sps[o] = {d: paths[i][0][0]} 39 | 40 | # Dump dict into json 41 | with open(outfile, 'w') as f: 42 | json.dump(sps, f) 43 | 44 | def compute_all_shortest_paths_floyd_warshall(graph, chosen_mservice, layer_name, outfile): 45 | """Fonction that pre-computes the shortest paths for each pair of nodes of the 46 | graph of a layer and write them to a file. By shortest we mean in distance. 47 | It launches the Floyd-Warshall algorithm to do so. 48 | 49 | Args: 50 | -graph: graph of the layer 51 | -chosen_mservice: dict with a unique element {'layer_name': 'mob_service_name'} 52 | -layer_name: name of the layer 53 | -outfile: file where the shortest paths will be saved 54 | """ 55 | # Call Floyd Warshall 56 | st = time.time() 57 | pair = floyd_warshall(graph, 58 | 'length', 59 | chosen_mservice, 60 | {layer_name}) 61 | print(f'Flyod-Warshall done in {time.time()-st}') 62 | 63 | # Create the shortest paths trees 64 | st = time.time() 65 | prev_table = pair[0] 66 | vnodemap = pair[1] 67 | spts = {} 68 | null = max(list(vnodemap.keys())) + 1 69 | for v, node in vnodemap.items(): 70 | spt = {} 71 | for v_, node_ in vnodemap.items(): 72 | if prev_table[v][v_] == null: 73 | spt[node_] = '' 74 | else: 75 | spt[node_] = vnodemap[prev_table[v][v_]] 76 | spts[node] = spt 77 | print(f'Building of shortest paths trees done in {time.time()-st}') 78 | 79 | # Dump them into json file 80 | with open(outfile, 'w') as f: 81 | json.dump(spts, f) 82 | 83 | 84 | def compute_all_shortest_paths(graph, chosen_mservice, layer_name, outfile): 85 | """Fonction that pre-computes the shortest paths for each pair of nodes of the 86 | graph of a layer and write them to a file. By shortest we mean in distance. 87 | It launches in parallel the single source dijkstra for all pairs. 88 | 89 | Args: 90 | -graph: graph of the layer 91 | -chosen_mservice: dict with a unique element {'layer_name': 'mob_service_name'} 92 | -layer_name: name of the layer 93 | -outfile: file where the shortest paths will be saved 94 | """ 95 | origins = list(graph.nodes.keys()) 96 | st = time.time() 97 | spts = parallel_dijkstra_single_source(graph, 98 | origins, 99 | [chosen_mservice]*len(origins), 100 | 'length', 101 | multiprocessing.cpu_count(), 102 | [{layer_name}]*len(origins)) 103 | print(f'Done in {time.time()-st}') 104 | spts_dict = {} 105 | for i,o in enumerate(origins): 106 | spts_dict[o] = spts[i] 107 | 108 | # Dump dict into json the shortest path tree, not the paths as it is smaller 109 | # in memory size 110 | with open(outfile, 'w') as f: 111 | json.dump(spts_dict, f) 112 | 113 | def decode_shortest_path_tree(spts, origin, destination): 114 | """Function to build the shortest path from the shortest path tree. 115 | 116 | Args: 117 | -spt: the shortest path tree 118 | -origin: origin of the shortest path 119 | -destination: destination of the shortest path 120 | 121 | Returns: 122 | -sp: the shortest path (list of nodes) 123 | """ 124 | spt = spts[origin] 125 | d = destination 126 | path = [d] 127 | valid = True 128 | while d != origin: 129 | d = spt[d] 130 | if d == '': 131 | return [] 132 | path.append(d) 133 | path = list(reversed(path)) 134 | 135 | return path 136 | -------------------------------------------------------------------------------- /tests/graph/test_transit_layer.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from pathlib import Path 3 | import unittest 4 | 5 | from mnms.generation.layers import generate_matching_origin_destination_layer 6 | from mnms.graph.layers import CarLayer, BusLayer, MultiLayerGraph 7 | from mnms.graph.road import RoadDescriptor 8 | from mnms.graph.zone import Zone 9 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 10 | from mnms.time import TimeTable, Dt 11 | from mnms.io.graph import save_graph, load_graph 12 | 13 | 14 | class TestTransitLayer(unittest.TestCase): 15 | def setUp(self): 16 | """Initiates the test. 17 | """ 18 | self.temp_dir_results = tempfile.TemporaryDirectory() 19 | self.dir_results = Path(self.temp_dir_results.name) 20 | 21 | def tearDown(self): 22 | """Concludes and closes the test. 23 | """ 24 | self.temp_dir_results.cleanup() 25 | 26 | def init_test(self, sc): 27 | """Method to initiate the different tests of this class. 28 | """ 29 | if sc in ['1', '2', '3']: 30 | self.roads = RoadDescriptor() 31 | self.roads.register_node("0", [0, 0]) 32 | self.roads.register_node("1", [1, 0]) 33 | self.roads.register_node("2", [2, 0]) 34 | 35 | self.roads.register_section("0_1", "0", "1", 1) 36 | self.roads.register_section("1_2", "1", "2", 1) 37 | 38 | self.roads.register_stop("S0", "0_1", 0.4) 39 | self.roads.register_stop("S1", "1_2", 0.9) 40 | 41 | self.roads.add_zone(Zone("Z0", {"0_1"}, [])) 42 | self.roads.add_zone(Zone("Z1", {"1_2"}, [])) 43 | 44 | car_layer = CarLayer(self.roads, 45 | services=[PersonalMobilityService()]) 46 | car_layer.create_node("C0", "0") 47 | car_layer.create_node("C1", "1") 48 | car_layer.create_node("C2", "2") 49 | car_layer.create_link("C0_C1", "C0", "C1", {"PersonalVehicle": {"test": 34.3}}, ["0_1"]) 50 | 51 | bus_layer = BusLayer(self.roads) 52 | bus_layer.create_line("L0", 53 | ["S0", "S1"], 54 | [["0_1", "1_2"]], 55 | TimeTable.create_table_freq("08:00:00", "18:00:00", Dt(minutes=10)), 56 | True) 57 | 58 | odlayer = generate_matching_origin_destination_layer(self.roads) 59 | 60 | self.mlgraph = MultiLayerGraph([car_layer, bus_layer], 61 | odlayer, 62 | 1e-3) 63 | 64 | self.mlgraph.connect_layers("TEST", "C0", "L0_S0", 156, {"test": 1.43}) 65 | if sc in ['2']: 66 | self.mlgraph.connect_layers("TEST_", "C0", "L0_S0", 156, {"test": 0}) 67 | if sc in ['3']: 68 | self.mlgraph.connect_origindestination_layers(1e-1) 69 | 70 | def test_existence(self): 71 | self.init_test('1') 72 | transit_layer = self.mlgraph.transitlayer 73 | 74 | self.assertEqual(len(transit_layer.links), 3) 75 | self.assertEqual(len(transit_layer.links["ODLAYER"]["CAR"]), 3) 76 | self.assertEqual(len(transit_layer.links["ODLAYER"]["BUS"]), 2) 77 | self.assertEqual(len(transit_layer.links["BUS"]["ODLAYER"]), 2) 78 | self.assertEqual(len(transit_layer.links["BUS"]["CAR"]), 0) 79 | self.assertEqual(len(transit_layer.links["CAR"]["ODLAYER"]), 3) 80 | self.assertEqual(len(transit_layer.links["CAR"]["BUS"]), 1) 81 | 82 | def test_iter(self): 83 | self.init_test('1') 84 | transit_layer = self.mlgraph.transitlayer 85 | iterator = list(transit_layer.iter_links()) 86 | 87 | self.assertEqual(len(iterator), 11) 88 | 89 | def test_iter_inter(self): 90 | self.init_test('1') 91 | transit_layer = self.mlgraph.transitlayer 92 | iterator = list(transit_layer.iter_inter_links()) 93 | 94 | self.assertEqual(len(iterator), 1) 95 | 96 | link = self.mlgraph.graph.links[iterator[0]] 97 | 98 | self.assertDictEqual(link.costs["WALK"], {"test": 1.43}) 99 | self.assertEqual(link.length, 156) 100 | 101 | def test_double_transit_link_connect_layers(self): 102 | """Check that we cannot create two times the same transit links. 103 | """ 104 | self.init_test('2') 105 | transit_layer = self.mlgraph.transitlayer 106 | self.assertEqual(len(transit_layer.links["CAR"]["BUS"]), 1) 107 | self.assertEqual(transit_layer.links["CAR"]["BUS"], ['TEST']) 108 | self.assertEqual(self.mlgraph.graph.nodes["C0"].adj["L0_S0"].costs, {'WALK': {'test': 1.43}}) 109 | 110 | def test_double_transit_link_connect_origindestination_layers(self): 111 | """Check that we cannot create two times the same transit links. 112 | """ 113 | self.init_test('3') 114 | transit_layer = self.mlgraph.transitlayer 115 | self.assertEqual(len(transit_layer.links), 3) 116 | print(transit_layer.links["ODLAYER"]["CAR"]) 117 | self.assertEqual(len(transit_layer.links["ODLAYER"]["CAR"]), 3) 118 | self.assertEqual(len(transit_layer.links["ODLAYER"]["BUS"]), 2) 119 | self.assertEqual(len(transit_layer.links["BUS"]["ODLAYER"]), 2) 120 | self.assertEqual(len(transit_layer.links["CAR"]["ODLAYER"]), 3) 121 | -------------------------------------------------------------------------------- /src/mnms/tools/geometry.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from collections import defaultdict 3 | from shapely.geometry import Polygon, mapping 4 | from scipy.spatial import Voronoi 5 | import numpy as np 6 | from typing import List, Annotated 7 | 8 | Point = Annotated[List[float], 2] 9 | PointList = List[Point] 10 | 11 | 12 | @dataclass 13 | class BoundingBox: 14 | xmin: float 15 | ymin: float 16 | xmax: float 17 | ymax: float 18 | 19 | def polygon(self): 20 | """Method that returns the bounding box polygon. 21 | """ 22 | return [[self.xmin, self.ymin], [self.xmax, self.ymin], 23 | [self.xmax, self.ymax], [self.xmin, self.ymax]] 24 | 25 | 26 | def get_bounding_box(roads: "RoadDescriptor", graph = None): 27 | if graph is None: 28 | positions = np.array([node.position for node in roads.nodes.values()]) 29 | else: 30 | positions = np.array([node.position for node in graph.nodes.values()]) 31 | return BoundingBox(np.min(positions[:, 0]), 32 | np.min(positions[:, 1]), 33 | np.max(positions[:, 0]), 34 | np.max(positions[:, 1])) 35 | 36 | 37 | def points_in_polygon(polygon, pts): 38 | if len(pts) == 0: 39 | return [] 40 | pts = np.asarray(pts, dtype='float32') 41 | polygon = np.asarray(polygon, dtype='float32') 42 | contour2 = np.vstack((polygon[1:], polygon[:1])) 43 | test_diff = contour2 - polygon 44 | mask1 = (pts[:, None] == polygon).all(-1).any(-1) 45 | m1 = (polygon[:, 1] > pts[:, None, 1]) != (contour2[:, 1] > pts[:, None, 1]) 46 | slope = ((pts[:, None, 0] - polygon[:, 0]) * test_diff[:, 1]) - ( 47 | test_diff[:, 0] * (pts[:, None, 1] - polygon[:, 1])) 48 | m2 = slope == 0 49 | mask2 = (m1 & m2).any(-1) 50 | m3 = (slope < 0) != (contour2[:, 1] < polygon[:, 1]) 51 | m4 = m1 & m3 52 | count = np.count_nonzero(m4, axis=-1) 53 | mask3 = ~(count % 2 == 0) 54 | mask = mask1 | mask2 | mask3 55 | return mask 56 | 57 | def polygon_area(polygon): 58 | """Method that computes the area of a polygon based on Schoelace formula. 59 | 60 | Args: 61 | -polygon: defined by a list of points 62 | """ 63 | x = [p[0] for p in polygon] 64 | y = [p[1] for p in polygon] 65 | area = 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1))) 66 | return area 67 | 68 | def voronoi_zones(points: PointList, bbox: BoundingBox): 69 | """Method that produces a list of polygons corresponding to the Voronoi regions 70 | of the points passed. It is based on Gareth Rees's function. 71 | 72 | Args: 73 | -points: input points to compute the Voronoi diagram of 74 | -bbox: bounding box of the output polygons 75 | 76 | Returns: 77 | -polygons: the list of Voronoi zones contours 78 | """ 79 | # Compute the Voronoi diagram of the points 80 | voronoi = Voronoi(points) 81 | 82 | # Compute the directions of inifinite ridges 83 | centroid = voronoi.points.mean(axis=0) 84 | ridge_direction = defaultdict(list) 85 | for (p, q), rv in zip(voronoi.ridge_points, voronoi.ridge_vertices): 86 | u, v = sorted(rv) 87 | if u == -1: 88 | # Infinite ridge starting at ridge point with index v, 89 | # equidistant from input points with indexes p and q 90 | tangent = voronoi.points[q] - voronoi.points[p] 91 | normal = np.array([-tangent[1], tangent[0]]) / np.linalg.norm(tangent) 92 | midpoint = voronoi.points[[p, q]].mean(axis=0) 93 | direction = np.sign(np.dot(midpoint - centroid, normal)) * normal 94 | ridge_direction[p, v].append(direction) 95 | ridge_direction[q, v].append(direction) 96 | 97 | # Build the Voronoi polygons 98 | polygons = [] 99 | for i, r in enumerate(voronoi.point_region): 100 | region = voronoi.regions[r] 101 | if -1 not in region: 102 | # Finite region, nothing more to do 103 | pol = Polygon(voronoi.vertices[region]) 104 | pol = [list(c) for c in mapping(pol)['coordinates'][0][:-1]] 105 | polygons.append(pol) 106 | continue 107 | # Infinite region 108 | inf = region.index(-1) # Index of vertex at infinity 109 | j = region[(inf - 1) % len(region)] # Index of previous vertex 110 | k = region[(inf + 1) % len(region)] # Index of next vertex 111 | if j == k: 112 | # Region has one Voronoi vertex with two ridges 113 | dir_j, dir_k = ridge_direction[i, j] 114 | else: 115 | # Region has two Voronoi vertices, each with one ridge 116 | dir_j, = ridge_direction[i, j] 117 | dir_k, = ridge_direction[i, k] 118 | 119 | # Length of ridges needed for the extra edge to fill the bounding box 120 | diameter = max(bbox.xmax-bbox.xmin, bbox.ymax-bbox.ymin) 121 | length = 2 * diameter / np.linalg.norm(dir_j + dir_k) 122 | 123 | # Polygon consists of finite part plus an extra edge 124 | finite_part = voronoi.vertices[region[inf + 1:] + region[:inf]] 125 | extra_edge = [voronoi.vertices[j] + dir_j * length, 126 | voronoi.vertices[k] + dir_k * length] 127 | pol = Polygon(np.concatenate((finite_part, extra_edge))) 128 | 129 | # Reduce polygon to the bounding box 130 | pol = pol.intersection(Polygon(bbox.polygon())) 131 | pol = [list(c) for c in mapping(pol)['coordinates'][0][:-1]] 132 | polygons.append(pol) 133 | 134 | return polygons 135 | -------------------------------------------------------------------------------- /tests/mobility_services/test_on_demand_depot.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import unittest 3 | from pathlib import Path 4 | import pandas as pd 5 | import numpy as np 6 | import math 7 | 8 | from mnms.generation.roads import generate_line_road, RoadDescriptor 9 | from mnms.graph.zone import Zone 10 | from mnms.graph.zone import construct_zone_from_sections, construct_zone_from_contour 11 | from mnms.graph.layers import MultiLayerGraph, SharedVehicleLayer, CarLayer 12 | from mnms.generation.roads import generate_manhattan_road 13 | from mnms.generation.layers import generate_matching_origin_destination_layer, generate_layer_from_roads 14 | from mnms.mobility_service.vehicle_sharing import VehicleSharingMobilityService 15 | from mnms.mobility_service.on_demand import OnDemandDepotMobilityService 16 | from mnms.tools.observer import CSVUserObserver, CSVVehicleObserver 17 | from mnms.vehicles.veh_type import Bike, Bus 18 | from mnms.travel_decision.dummy import DummyDecisionModel 19 | from mnms.flow.MFD import MFDFlowMotor, Reservoir 20 | from mnms.simulation import Supervisor 21 | from mnms.demand import BaseDemandManager, User 22 | from mnms.time import TimeTable, Time, Dt 23 | from mnms.mobility_service.public_transport import PublicTransportMobilityService 24 | from mnms.mobility_service.vehicle_sharing import VehicleSharingMobilityService 25 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 26 | from mnms.graph.layers import MultiLayerGraph, PublicTransportLayer 27 | from mnms.log import set_all_mnms_logger_level, LOGLEVEL 28 | 29 | 30 | class TestOnDemandDepotMobilityService(unittest.TestCase): 31 | def setUp(self): 32 | """Initiates the test. 33 | """ 34 | self.temp_dir_results = tempfile.TemporaryDirectory() 35 | self.dir_results = Path(self.temp_dir_results.name) 36 | 37 | def tearDown(self): 38 | """Concludes and closes the test. 39 | """ 40 | self.temp_dir_results.cleanup() 41 | 42 | def create_supervisor(self, sc): 43 | """Method to create a common supervisor for the different tests of this class. 44 | """ 45 | roads = generate_manhattan_road(4, 500, extended=False) 46 | ridehailing = OnDemandDepotMobilityService('RIDEHAILING', 0) 47 | ridehailing_layer = generate_layer_from_roads(roads, 'RIDEHAILING', mobility_services=[ridehailing]) 48 | ridehailing.attach_vehicle_observer(CSVVehicleObserver(self.dir_results / "vehs.csv")) 49 | ridehailing.add_depot('RIDEHAILING_3', 1) 50 | ridehailing.add_depot('RIDEHAILING_4', 0) 51 | ridehailing.add_depot('RIDEHAILING_10', 1) 52 | ridehailing.add_depot('RIDEHAILING_12', 1) 53 | 54 | odlayer = generate_matching_origin_destination_layer(roads) 55 | 56 | mlgraph = MultiLayerGraph([ridehailing_layer], odlayer) 57 | 58 | mlgraph.connect_origindestination_layers(1) 59 | 60 | ridehailing.add_zoning() 61 | 62 | demand = BaseDemandManager([User("U1", [0, 1500], [0, 0], Time("07:00:00"), pickup_dt=Dt(minutes=10)), 63 | User("U2", [500, 500], [0, 0], Time("07:00:00"), pickup_dt=Dt(minutes=10)), 64 | User("U3", [1000, 1000], [0, 0], Time("07:00:00"), pickup_dt=Dt(minutes=10)), 65 | User("U4", [1500, 0], [0, 0], Time("07:00:00"), pickup_dt=Dt(minutes=10))]) 66 | demand.add_user_observer(CSVUserObserver(self.dir_results / 'users.csv')) 67 | 68 | decision_model = DummyDecisionModel(mlgraph, outfile=self.dir_results / "paths.csv", 69 | verbose_file=True) 70 | 71 | def mfdspeed(dacc): 72 | dspeed = {'CAR': 5} 73 | return dspeed 74 | 75 | flow_motor = MFDFlowMotor() 76 | veh_types = ['CAR'] 77 | flow_motor.add_reservoir(Reservoir(roads.zones["RES"], veh_types, mfdspeed)) 78 | 79 | supervisor = Supervisor(mlgraph, 80 | demand, 81 | flow_motor, 82 | decision_model, 83 | logfile='log.txt', 84 | loglevel=LOGLEVEL.INFO) 85 | set_all_mnms_logger_level(LOGLEVEL.INFO) 86 | 87 | return supervisor 88 | 89 | def test_on_demand_depot_automatic_zoning(self): 90 | """Test the automatic zoning of the OnDemandDepotMObilityService based on 91 | the depots. 92 | """ 93 | ## Create supervisor 94 | supervisor = self.create_supervisor('1') 95 | 96 | ## Run 97 | flow_dt = Dt(seconds=30) 98 | affectation_factor = 1 99 | supervisor.run(Time("06:55:00"), 100 | Time("07:30:00"), 101 | flow_dt, 102 | affectation_factor) 103 | 104 | ## Get and check result 105 | w_0 = 15 + 1.343 / (2 * 5 * math.sqrt(1/338541.6666666667)) 106 | w_1 = 0 107 | w_2 = 15 + 1.343 / (2 * 5 * math.sqrt(1/1062500)) 108 | w_3 = 15 + 1.343 / (2 * 5 * math.sqrt(1/250000)) 109 | 110 | tt0 = 1500 / 5 111 | tt1 = 1000 / 5 112 | tt2 = 2000 / 5 113 | tt3 = 1500 / 5 114 | 115 | with open(self.dir_results / "paths.csv") as f: 116 | df = pd.read_csv(f, sep=';') 117 | df = df[df['EVENT'] == 'DEPARTURE'] 118 | 119 | df1 = df[df['ID'] == 'U1'] 120 | self.assertAlmostEqual(df1['COST'].iloc[0], w_0 + tt0) 121 | df2 = df[df['ID'] == 'U2'] 122 | self.assertAlmostEqual(df2['COST'].iloc[0], w_1 + tt1) 123 | df3 = df[df['ID'] == 'U3'] 124 | self.assertAlmostEqual(df3['COST'].iloc[0], w_2 + tt2) 125 | df4 = df[df['ID'] == 'U4'] 126 | self.assertAlmostEqual(df4['COST'].iloc[0], w_3 + tt3) 127 | -------------------------------------------------------------------------------- /tests/io/test_save_load_transit_link.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | from tempfile import TemporaryDirectory 4 | 5 | from mnms.generation.layers import generate_matching_origin_destination_layer 6 | from mnms.graph.layers import CarLayer, BusLayer, MultiLayerGraph 7 | from mnms.graph.road import RoadDescriptor 8 | from mnms.graph.zone import Zone 9 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 10 | from mnms.time import TimeTable, Dt 11 | from mnms.io.graph import save_graph, load_graph, save_transit_links, save_transit_link_odlayer, load_transit_links 12 | 13 | 14 | class TestIOGraphTransit(unittest.TestCase): 15 | def setUp(self): 16 | """Initiates the test. 17 | """ 18 | 19 | self.tempdir = TemporaryDirectory() 20 | 21 | self.roads = RoadDescriptor() 22 | self.roads.register_node("0", [0, 0]) 23 | self.roads.register_node("1", [1, 0]) 24 | self.roads.register_node("2", [2, 0]) 25 | 26 | self.roads.register_section("0_1", "0", "1", 1) 27 | self.roads.register_section("1_2", "1", "2", 1) 28 | 29 | self.roads.register_stop("S0", "0_1", 0.4) 30 | self.roads.register_stop("S1", "1_2", 0.9) 31 | 32 | self.roads.add_zone(Zone("Z0", {"0_1"}, [])) 33 | self.roads.add_zone(Zone("Z1", {"1_2"}, [])) 34 | 35 | car_layer = CarLayer(self.roads, 36 | services=[PersonalMobilityService()]) 37 | 38 | car_layer.create_node("C0", "0") 39 | car_layer.create_node("C1", "1", {"0": {"2"}}) 40 | car_layer.create_node("C2", "2") 41 | car_layer.create_link("C0_C1", "C0", "C1", {"PersonalVehicle": {"test": 34.3}}, ["0_1"]) 42 | 43 | bus_layer = BusLayer(self.roads) 44 | 45 | bus_layer.create_line("L0", 46 | ["S0", "S1"], 47 | [["0_1", "1_2"]], 48 | TimeTable.create_table_freq("08:00:00", "18:00:00", Dt(minutes=10)), 49 | True) 50 | 51 | odlayer = generate_matching_origin_destination_layer(self.roads) 52 | 53 | self.mlgraph = MultiLayerGraph([car_layer, bus_layer], 54 | odlayer, 55 | 1e-3) 56 | 57 | self.mlgraph.connect_layers("TEST", "C0", "L0_S0", 156, {"test": 1.43}) 58 | 59 | def tearDown(self): 60 | """Concludes and closes the test. 61 | """ 62 | try: 63 | self.tempdir.cleanup() 64 | except: 65 | pass 66 | 67 | def test_read_write_all(self): 68 | save_transit_links(self.mlgraph, self.tempdir.name+"/all_transit.json") 69 | with open(self.tempdir.name+"/all_transit.json", "r") as f: 70 | data = json.load(f) 71 | 72 | car_layer = CarLayer(self.roads, 73 | services=[PersonalMobilityService()]) 74 | 75 | car_layer.create_node("C0", "0") 76 | car_layer.create_node("C1", "1", {"0": {"2"}}) 77 | car_layer.create_node("C2", "2") 78 | car_layer.create_link("C0_C1", "C0", "C1", {"PersonalVehicle": {"test": 34.3}}, ["0_1"]) 79 | 80 | bus_layer = BusLayer(self.roads) 81 | 82 | bus_layer.create_line("L0", 83 | ["S0", "S1"], 84 | [["0_1", "1_2"]], 85 | TimeTable.create_table_freq("08:00:00", "18:00:00", Dt(minutes=10)), 86 | True) 87 | 88 | odlayer = generate_matching_origin_destination_layer(self.roads) 89 | 90 | new_mlgraph = MultiLayerGraph([car_layer, bus_layer]) 91 | new_mlgraph.add_origin_destination_layer(odlayer) 92 | load_transit_links(new_mlgraph, self.tempdir.name+"/all_transit.json") 93 | 94 | for link in data['LINKS']: 95 | link_id = link['ID'] 96 | assert link_id in self.mlgraph.graph.links 97 | assert link_id in new_mlgraph.graph.links 98 | 99 | def test_read_write_odlayer_links(self): 100 | save_transit_link_odlayer(self.mlgraph, self.tempdir.name+"/odlayer_transit.json") 101 | with open(self.tempdir.name+"/odlayer_transit.json", "r") as f: 102 | data = json.load(f) 103 | 104 | data = set(l["ID"] for l in data["LINKS"]) 105 | 106 | for origin in self.mlgraph.odlayer.origins: 107 | for link in self.mlgraph.graph.nodes[origin].adj.values(): 108 | assert link.id in data 109 | for destination in self.mlgraph.odlayer.destinations: 110 | for link in self.mlgraph.graph.nodes[destination].radj.values(): 111 | assert link.id in data 112 | 113 | 114 | car_layer = CarLayer(self.roads, 115 | services=[PersonalMobilityService()]) 116 | 117 | car_layer.create_node("C0", "0") 118 | car_layer.create_node("C1", "1", {"0": {"2"}}) 119 | car_layer.create_node("C2", "2") 120 | car_layer.create_link("C0_C1", "C0", "C1", {"PersonalVehicle": {"test": 34.3}}, ["0_1"]) 121 | 122 | bus_layer = BusLayer(self.roads) 123 | 124 | bus_layer.create_line("L0", 125 | ["S0", "S1"], 126 | [["0_1", "1_2"]], 127 | TimeTable.create_table_freq("08:00:00", "18:00:00", Dt(minutes=10)), 128 | True) 129 | 130 | odlayer = generate_matching_origin_destination_layer(self.roads) 131 | 132 | new_mlgraph = MultiLayerGraph([car_layer, bus_layer]) 133 | new_mlgraph.add_origin_destination_layer(odlayer) 134 | load_transit_links(new_mlgraph, self.tempdir.name+"/odlayer_transit.json") 135 | 136 | assert "TEST" not in new_mlgraph.graph.links 137 | -------------------------------------------------------------------------------- /src/mnms/generation/layers.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Type, List, Annotated 2 | 3 | import numpy as np 4 | 5 | from mnms.graph.layers import AbstractLayer 6 | from mnms.graph.layers import OriginDestinationLayer, SimpleLayer 7 | from mnms.graph.road import RoadDescriptor 8 | from mnms.mobility_service.abstract import AbstractMobilityService 9 | from mnms.tools.geometry import get_bounding_box, points_in_polygon 10 | from mnms.vehicles.veh_type import Vehicle, Car 11 | 12 | Point = Annotated[List[float], 2] 13 | PointList = List[Point] 14 | 15 | def generate_layer_from_roads(roads: RoadDescriptor, 16 | layer_id: str, 17 | class_layer = SimpleLayer, 18 | veh_type:Type[Vehicle] = Car, 19 | default_speed: float = 14, 20 | mobility_services: Optional[List[AbstractMobilityService]] = None, 21 | banned_nodes: List[str] = None, 22 | banned_sections: List[str] = None) -> AbstractLayer: 23 | """ 24 | Generate a whole layer from the RoadDescriptor. 25 | 26 | Args 27 | roads: The roads object 28 | layer_id: the id of the generated layer 29 | veh_type: the type of vehicle 30 | default_speed: the default speed 31 | mobility_services: the mobility services on the generated layer 32 | banned_nodes: specifies the list of nodes for which no layer node should 33 | be created 34 | banned_sections: specifies the list of sections for which no layer link 35 | should be created 36 | 37 | Returns: 38 | The generated Layer 39 | """ 40 | 41 | layer = class_layer(roads, layer_id, veh_type, default_speed, mobility_services) 42 | 43 | for n in roads.nodes: 44 | if banned_nodes is None or (banned_nodes is not None and n not in banned_nodes): 45 | layer.create_node(f"{layer_id}_{n}", n, {}) 46 | 47 | for lid, data in roads.sections.items(): 48 | if banned_sections is None or (banned_sections is not None and lid not in banned_sections): 49 | cost = {} 50 | if mobility_services is not None: 51 | for mservice in mobility_services: 52 | cost[mservice.id] = {'length': data.length} 53 | else: 54 | cost["_DEFAULT"] = {'length': data.length} 55 | 56 | layer.create_link(f"{layer_id}_{lid}", 57 | f"{layer_id}_{data.upstream}", 58 | f"{layer_id}_{data.downstream}", 59 | cost, 60 | [lid]) 61 | return layer 62 | 63 | 64 | def generate_matching_origin_destination_layer(roads: RoadDescriptor, with_stops: bool = True): 65 | 66 | odlayer = OriginDestinationLayer() 67 | 68 | for node in roads.nodes.values(): 69 | odlayer.create_origin_node(f"ORIGIN_{node.id}", node.position) 70 | odlayer.create_destination_node(f"DESTINATION_{node.id}", node.position) 71 | 72 | if with_stops: 73 | for stop in roads.stops.values(): 74 | odlayer.create_origin_node(f"ORIGIN_{stop.id}", stop.absolute_position) 75 | odlayer.create_destination_node(f"DESTINATION_{stop.id}", stop.absolute_position) 76 | 77 | return odlayer 78 | 79 | 80 | def generate_grid_origin_destination_layer(xmin: float, 81 | ymin: float, 82 | xmax: float, 83 | ymax: float, 84 | nx: int, 85 | ny: Optional[int] = None, 86 | polygon: Optional[PointList] = None): 87 | """ 88 | Generate a rectangular structured grid for the OriginDestinationLayer 89 | 90 | Args: 91 | xmin: the min x of the grid 92 | ymin: the min y of the grid 93 | xmax: the max x of the grid 94 | ymax: the max y of the grid 95 | nx: the number of point in the x direction 96 | ny: the number of point in the y direction 97 | polygon: optional arg that specifies a polygon outside of which the 98 | origin and destination should not be created 99 | 100 | Returns: 101 | The OriginDestinationLayer 102 | 103 | """ 104 | if ny is None: 105 | ny = nx 106 | 107 | x_dist = xmax - xmin 108 | y_dist = ymax - ymin 109 | 110 | dx = x_dist / nx 111 | dy = y_dist / ny 112 | 113 | odlayer = OriginDestinationLayer() 114 | 115 | for j in range(ny): 116 | for i in range(nx): 117 | pos = np.array([xmin + i * dx, ymin + j * dy]) 118 | if polygon is not None: 119 | if not points_in_polygon(np.array(polygon), [pos])[0]: 120 | continue 121 | odlayer.create_destination_node(f"DESTINATION_{str(i + j * nx)}", pos) 122 | odlayer.create_origin_node(f"ORIGIN_{str(i + j * nx)}", pos) 123 | 124 | return odlayer 125 | 126 | 127 | def generate_bbox_origin_destination_layer(roads: RoadDescriptor, nx: int, ny: Optional[int] = None, polygon: Optional[PointList] = None) -> OriginDestinationLayer: 128 | """ 129 | Generate a grid OriginDestinationLayer based on the bounding box of the roads nodes 130 | 131 | Args: 132 | roads: The OriginDestinationLayer from which the odlayer is created 133 | nx: The number of point in the x direction 134 | ny: The number of point in the y direction 135 | polygon: Optional arg that specifies a polygon outside of which the origins and destinations are not created 136 | 137 | Returns: 138 | The generated layer 139 | 140 | """ 141 | bbox = get_bounding_box(roads) 142 | return generate_grid_origin_destination_layer(bbox.xmin - 1, bbox.ymin - 1, bbox.xmax + 1, bbox.ymax + 1, nx, ny, polygon=polygon) 143 | -------------------------------------------------------------------------------- /tests/graph/test_layers.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from mnms.graph.layers import CarLayer, BusLayer 4 | from mnms.graph.road import RoadDescriptor 5 | from mnms.graph.zone import construct_zone_from_sections 6 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 7 | from mnms.mobility_service.public_transport import PublicTransportMobilityService 8 | from mnms.time import TimeTable, Dt 9 | 10 | 11 | class TestLayers(unittest.TestCase): 12 | def setUp(self): 13 | """Initiates the test. 14 | """ 15 | 16 | self.roads = RoadDescriptor() 17 | self.roads.register_node("0", [0, 0]) 18 | self.roads.register_node("1", [1, 0]) 19 | self.roads.register_node("2", [2, 0]) 20 | 21 | self.roads.register_section("0_1", "0", "1", 1) 22 | self.roads.register_section("1_2", "1", "2", 1) 23 | 24 | self.roads.register_stop("S0", "0_1", 0.4) 25 | self.roads.register_stop("S1", "1_2", 0.9) 26 | 27 | self.roads.add_zone(construct_zone_from_sections(self.roads, "Z0", ["0_1"])) 28 | self.roads.add_zone(construct_zone_from_sections(self.roads, "Z1", ["1_2"])) 29 | 30 | def tearDown(self): 31 | """Concludes and closes the test. 32 | """ 33 | 34 | def test_car_layer(self): 35 | car_layer = CarLayer(self.roads, 36 | services=[PersonalMobilityService()]) 37 | 38 | self.assertEqual(car_layer.id, "CAR") 39 | self.assertEqual(car_layer.default_speed, 13.8) 40 | 41 | car_layer.create_node("C0", "0") 42 | car_layer.create_node("C1", "1", {"0": {"2"}}) 43 | car_layer.create_node("C2", "2") 44 | costs = {"PersonalVehicle": {"test": 34.3}} 45 | car_layer.create_link("C0_C1", "C0", "C1", costs, ["0_1"]) 46 | 47 | gnodes = car_layer.graph.nodes 48 | 49 | self.assertIn("C0", gnodes) 50 | self.assertIn("C1", gnodes) 51 | self.assertIn("C2", gnodes) 52 | 53 | self.assertEqual(gnodes["C0"].position, [0, 0]) 54 | self.assertEqual(gnodes["C1"].position, [1, 0]) 55 | self.assertEqual(gnodes["C2"].position, [2, 0]) 56 | 57 | glinks = car_layer.graph.links 58 | 59 | self.assertIn("C0_C1", glinks) 60 | 61 | self.assertDictEqual(costs, glinks["C0_C1"].costs) 62 | 63 | def test_public_transport_layer(self): 64 | bus_layer = BusLayer(self.roads, 65 | services=[PublicTransportMobilityService("BUS")]) 66 | 67 | bus_layer.create_line("L0", 68 | ["S0", "S1"], 69 | [["0_1", "1_2"]], 70 | TimeTable.create_table_freq("08:00:00", "18:00:00", Dt(minutes=10)), 71 | True) 72 | 73 | gnodes = bus_layer.graph.nodes 74 | glinks = bus_layer.graph.links 75 | 76 | self.assertIn("L0_S0", gnodes) 77 | self.assertIn("L0_S1", gnodes) 78 | 79 | self.assertAlmostEqual(0.4, gnodes['L0_S0'].position[0]) 80 | self.assertAlmostEqual(1.9, gnodes['L0_S1'].position[0]) 81 | 82 | self.assertIn("L0_S0_S1", glinks) 83 | self.assertIn("L0_S1_S0", glinks) 84 | 85 | self.assertListEqual(["0_1", "1_2"], bus_layer.map_reference_links["L0_S0_S1"]) 86 | self.assertListEqual(["1_2", "0_1"], bus_layer.map_reference_links["L0_S1_S0"]) 87 | 88 | 89 | class TestSerializationLayers(unittest.TestCase): 90 | def setUp(self): 91 | """Initiates the test. 92 | """ 93 | self.roads = RoadDescriptor() 94 | self.roads.register_node("0", [0, 0]) 95 | self.roads.register_node("1", [1, 0]) 96 | self.roads.register_node("2", [2, 0]) 97 | 98 | self.roads.register_section("0_1", "0", "1", 1) 99 | self.roads.register_section("1_2", "1", "2", 1) 100 | 101 | self.roads.register_stop("S0", "0_1", 0.4) 102 | self.roads.register_stop("S1", "1_2", 0.9) 103 | 104 | self.roads.add_zone(construct_zone_from_sections(self.roads, "Z0", ["0_1"])) 105 | self.roads.add_zone(construct_zone_from_sections(self.roads, "Z1", ["1_2"])) 106 | 107 | def tearDown(self): 108 | """Concludes and closes the test. 109 | """ 110 | 111 | def test_serialization_car(self): 112 | car_layer = CarLayer(self.roads, 113 | services=[PersonalMobilityService()]) 114 | 115 | car_layer.create_node("C0", "0") 116 | car_layer.create_node("C1", "1", {"0": {"2"}}) 117 | car_layer.create_node("C2", "2") 118 | car_layer.create_link("C0_C1", "C0", "C1", {"PersonalVehicle": {"test": 34.3}}, ["0_1"]) 119 | 120 | data_dict = car_layer.__dump__() 121 | 122 | new_car_layer = CarLayer.__load__(data_dict, self.roads) 123 | 124 | self.assertDictEqual(new_car_layer.map_reference_links, car_layer.map_reference_links) 125 | self.assertDictEqual(new_car_layer.map_reference_nodes, car_layer.map_reference_nodes) 126 | self.assertSetEqual(set(new_car_layer.graph.nodes.keys()), set(car_layer.graph.nodes.keys())) 127 | self.assertSetEqual(set(new_car_layer.graph.links.keys()), set(car_layer.graph.links.keys())) 128 | 129 | def test_serialization_public_transport(self): 130 | bus_layer = BusLayer(self.roads) 131 | 132 | bus_layer.create_line("L0", 133 | ["S0", "S1"], 134 | [["0_1", "1_2"]], 135 | TimeTable.create_table_freq("08:00:00", "18:00:00", Dt(minutes=10)), 136 | True) 137 | 138 | data_dict = bus_layer.__dump__() 139 | 140 | new_bus_layer = BusLayer.__load__(data_dict, self.roads) 141 | 142 | self.assertDictEqual(new_bus_layer.map_reference_links, bus_layer.map_reference_links) 143 | self.assertDictEqual(new_bus_layer.map_reference_nodes, bus_layer.map_reference_nodes) 144 | self.assertSetEqual(set(new_bus_layer.graph.nodes.keys()), set(bus_layer.graph.nodes.keys())) 145 | self.assertSetEqual(set(new_bus_layer.graph.links.keys()), set(bus_layer.graph.links.keys())) 146 | -------------------------------------------------------------------------------- /examples/mulitmodal_nested_manhattan/layers_connection.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from mnms.graph.layers import PublicTransportLayer 3 | 4 | def connect_layers(mlgraph, max_transfer_dist, personal_mob_service_park_radius): 5 | _norm = np.linalg.norm 6 | 7 | # Transit between two pt lines (alight, walk, wait and board) 8 | pt_layers = [l for l in mlgraph.layers.values() if isinstance(l, PublicTransportLayer)] 9 | pt_layers_ids = [l.id for l in pt_layers] 10 | pt_nodes = np.array([n for n in mlgraph.graph.nodes.values() if n.label in pt_layers_ids]) 11 | pt_nodes_pos = np.array([n.position for n in mlgraph.graph.nodes.values() if n.label in pt_layers_ids]) 12 | pt_nodes_label = np.array([n.label for n in mlgraph.graph.nodes.values() if n.label in pt_layers_ids]) 13 | pt_lines = [line for ly in pt_layers for line in ly.lines.values()] 14 | for node in pt_nodes: 15 | node_pos = np.array(node.position) 16 | node_label = np.array(node.label) 17 | dist_nodes = _norm(pt_nodes_pos-node_pos, axis=1) 18 | mask = dist_nodes < max_transfer_dist 19 | for node_, dist in zip(pt_nodes[mask], dist_nodes[mask]): 20 | if node != node_: 21 | lid = f"{node.id}_{node_.id}" 22 | mlgraph.connect_layers(lid, node.id, node_.id, dist, {'length': dist}) 23 | 24 | # Transit from car to pt layer (park, walk, wait and board) 25 | car_nodes = np.array([n for n in mlgraph.graph.nodes.values() if n.label == 'CAR']) 26 | for cn in car_nodes: 27 | cn_pos = np.array(cn.position) 28 | dist_nodes = _norm(pt_nodes_pos-cn_pos, axis=1) 29 | mask = dist_nodes < max_transfer_dist 30 | for node, dist in zip(pt_nodes[mask], dist_nodes[mask]): 31 | lid = f"{cn.id}_{node.id}" 32 | mlgraph.connect_layers(lid, cn.id, node.id, dist, {'length': dist}) 33 | 34 | # Transit from car to ridehailing layer (park, walk, request AV, wait and ride) 35 | rh_nodes = np.array([n for n in mlgraph.graph.nodes.values() if (n.label == 'RIDEHAILING')]) 36 | rh_nodes_pos = np.array([n.position for n in rh_nodes]) 37 | for cn in car_nodes: 38 | cn_pos = np.array(cn.position) 39 | dist_nodes = _norm(rh_nodes_pos-cn_pos, axis=1) 40 | mask = dist_nodes < 10 # no need to walk far away since RIDEHAILING and CAR layers are both based on roads 41 | for node, dist in zip(rh_nodes[mask], dist_nodes[mask]): 42 | lid = f"{cn.id}_{node.id}" 43 | mlgraph.connect_layers(lid, cn.id, node.id, dist, {'length': dist}) 44 | 45 | # Transit from pt to ridehailing layer (alight, walk, request AV, wait and ride) 46 | for ptn in pt_nodes: 47 | ptn_pos = np.array(ptn.position) 48 | dist_nodes = _norm(rh_nodes_pos-ptn_pos, axis=1) 49 | mask = dist_nodes < max_transfer_dist 50 | for node, dist in zip(rh_nodes[mask], dist_nodes[mask]): 51 | lid = f"{ptn.id}_{node.id}" 52 | mlgraph.connect_layers(lid, ptn.id, node.id, dist, {'length': dist}) 53 | 54 | # Transit from ridehailing to pt layer (be dropped off, walk, wait and board) 55 | for rhn in rh_nodes: 56 | rhn_pos = np.array(rhn.position) 57 | dist_nodes = _norm(pt_nodes_pos-rhn_pos, axis=1) 58 | mask = dist_nodes < max_transfer_dist 59 | for node, dist in zip(pt_nodes[mask], dist_nodes[mask]): 60 | lid = f"{rhn.id}_{node.id}" 61 | mlgraph.connect_layers(lid, rhn.id, node.id, dist, {'length': dist}) 62 | # If a ride hailing node is connected to no pt node, select the closest pt stations 63 | # and connect the rh node with them to prevent having a user stuck on a rh node 64 | if len(pt_nodes[mask]) == 0: 65 | min_dist = min(dist_nodes) 66 | mask = dist_nodes <= min_dist 67 | for node, dist in zip(pt_nodes[mask], dist_nodes[mask]): 68 | lid = f"{rhn.id}_{node.id}" 69 | mlgraph.connect_layers(lid, rhn.id, node.id, dist, {'length': dist}) 70 | 71 | # Transit from ridehailing to origin (be refused, go back to home to be able to take car) 72 | #origin_nodes = np.array([n for n in mlgraph.graph.nodes.values() if n.label == 'ODLAYER' and n.radj == {}]) 73 | #origin_nodes_pos = np.array([n.position for n in origin_nodes]) 74 | #for rhn in rh_nodes: 75 | # rhn_pos = np.array(rhn.position) 76 | # dist_nodes = _norm(origin_nodes_pos-rhn_pos, axis=1) 77 | # mask = dist_nodes < max_transfer_dist 78 | # for node, dist in zip(origin_nodes[mask], dist_nodes[mask]): 79 | # lid = f"{rhn.id}_{node.id}" 80 | # mlgraph.connect_layers(lid, rhn.id, node.id, dist, {'length': dist}) 81 | 82 | # From origin to pt : select the closest pt stations for 83 | # origin nodes that are not yet connected to a PT station 84 | origins = mlgraph.odlayer.origins 85 | for oid,coord in origins.items(): 86 | ly_connected_to = [mlgraph.graph.nodes[adjn.downstream].label for adjn in mlgraph.graph.nodes[oid].adj.values()] 87 | if [ly for ly in ly_connected_to if ly in pt_layers_ids] == []: 88 | dist_nodes = _norm(pt_nodes_pos-coord, axis=1) 89 | min_dist = min(dist_nodes) 90 | mask = dist_nodes <= min_dist 91 | for pt_node_to_link, dist in zip(pt_nodes[mask], dist_nodes[mask]): 92 | mlgraph.connect_layers(f'{oid}_{pt_node_to_link.id}', oid, pt_node_to_link.id, dist, {'length': dist}) 93 | 94 | # From pt to destination : select the closest pt station for destination nodes 95 | # that are not yet connected to a PT station 96 | destinations = mlgraph.odlayer.destinations 97 | for oid,coord in destinations.items(): 98 | ly_back_connected_to = [mlgraph.graph.nodes[radjn.upstream].label for radjn in mlgraph.graph.nodes[oid].radj.values()] 99 | if [ly for ly in ly_back_connected_to if ly in pt_layers_ids] == []: 100 | dist_nodes = _norm(pt_nodes_pos-coord, axis=1) 101 | min_dist = min(dist_nodes) 102 | mask = dist_nodes <= min_dist 103 | for pt_node_to_link, dist in zip(pt_nodes[mask], dist_nodes[mask]): 104 | mlgraph.connect_layers(f'{pt_node_to_link.id}_{oid}', pt_node_to_link.id, oid, dist, {'length': dist}) 105 | -------------------------------------------------------------------------------- /src/mnms/tools/observer.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | import csv 3 | from typing import List 4 | 5 | from mnms.time import Time 6 | from mnms.log import create_logger 7 | 8 | log = create_logger(__name__) 9 | 10 | 11 | class Observer(ABC): 12 | """ 13 | Represents a observer 14 | """ 15 | @abstractmethod 16 | def update(self, subject:'Subject'): 17 | pass 18 | 19 | @abstractmethod 20 | def finish(self): 21 | pass 22 | 23 | 24 | class TimeDependentObserver(ABC): 25 | """ 26 | Represents a time dependant observer 27 | """ 28 | @abstractmethod 29 | def update(self, subject:'TimeDependentSubject', time:Time): 30 | pass 31 | 32 | @abstractmethod 33 | def finish(self): 34 | pass 35 | 36 | 37 | class Subject(ABC): 38 | """ 39 | Represents what is being observed 40 | """ 41 | 42 | def __init__(self): 43 | """Create an empty observer list""" 44 | self._observers: List[Observer] = [] 45 | 46 | def attach(self, obs): 47 | """If the observer is not in the list, 48 | append it into the list""" 49 | if obs not in self._observers: 50 | self._observers.append(obs) 51 | 52 | def detach(self, obs): 53 | """Remove the observer from the observer list""" 54 | self._observers.remove(obs) 55 | 56 | def notify(self): 57 | """Alerts the observers""" 58 | for obs in self._observers: 59 | obs.update(self) 60 | 61 | 62 | class TimeDependentSubject(ABC): 63 | """ 64 | Represents time-dependent observations 65 | """ 66 | 67 | def __init__(self): 68 | """Create an empty observer list""" 69 | self._observers: List[TimeDependentObserver] = [] 70 | 71 | def attach(self, obs): 72 | """If the observer is not in the list, 73 | append it into the list""" 74 | if obs not in self._observers: 75 | self._observers.append(obs) 76 | 77 | def detach(self, obs): 78 | """Remove the observer from the observer list""" 79 | self._observers.remove(obs) 80 | 81 | def notify(self, time: Time): 82 | """Alerts the observers""" 83 | for obs in self._observers: 84 | obs.update(self, time) 85 | 86 | 87 | class CSVUserObserver(TimeDependentObserver): 88 | def __init__(self, filename: str, prec:int=3): 89 | """ 90 | Observer class to write information about users during a simulation 91 | 92 | Args: 93 | filename: The name of the file 94 | prec: The precision for floating point number 95 | """ 96 | self._header = ["TIME", "ID", "LINK", "POSITION", "DISTANCE", "STATE", "VEHICLE"] 97 | self._filename = filename 98 | self._file = open(self._filename, "w") 99 | self._csvhandler = csv.writer(self._file, delimiter=';', quotechar='|') 100 | self._csvhandler.writerow(self._header) 101 | self._prec = prec 102 | 103 | def __getstate__(self): 104 | 105 | state = self.__dict__.copy() 106 | if '_csvhandler' in state: 107 | del state['_csvhandler'] 108 | return state 109 | 110 | def __setstate__(self, state): 111 | 112 | self.__dict__.update(state) 113 | 114 | self._csvhandler = csv.writer(self._file, delimiter=';', quotechar='|') 115 | self._csvhandler.writerow(self._header) 116 | 117 | def finish(self): 118 | self._file.close() 119 | 120 | def update(self, subject: 'User', t: Time): 121 | row = [t.time, 122 | subject.id, 123 | f"{subject.current_link[0]} {subject.current_link[1]}" if subject.current_link is not None else None, 124 | f"{subject.position[0]:.{self._prec}f} {subject.position[1]:.{self._prec}f}" if subject.position is not None else None, 125 | f"{subject.distance:.{self._prec}f}", 126 | subject.state.name, 127 | str(subject.vehicle.id) if subject.vehicle is not None else None] 128 | # log.info(f"OBS {time}: {row}") 129 | 130 | self._csvhandler.writerow(row) 131 | 132 | 133 | class CSVVehicleObserver(TimeDependentObserver): 134 | def __init__(self, filename: str, prec:int=3): 135 | """ 136 | Observer class to write information about vehicles during a simulation 137 | 138 | Args: 139 | filename: The name of the file 140 | prec: The precision for floating point number 141 | """ 142 | self._header = ["TIME", "ID", "TYPE", "LINK", "POSITION", "SPEED", "STATE", "DISTANCE", "PASSENGERS", "TRAVELED_NODES"] 143 | self._filename = filename 144 | self._file = open(self._filename, "w") 145 | self._csvhandler = csv.writer(self._file, delimiter=';', quotechar='|') 146 | self._csvhandler.writerow(self._header) 147 | self._prec = prec 148 | 149 | def __getstate__(self): 150 | 151 | state = self.__dict__.copy() 152 | if '_csvhandler' in state: 153 | del state['_csvhandler'] 154 | return state 155 | 156 | def __setstate__(self, state): 157 | 158 | self.__dict__.update(state) 159 | 160 | self._csvhandler = csv.writer(self._file, delimiter=';', quotechar='|') 161 | self._csvhandler.writerow(self._header) 162 | 163 | 164 | def finish(self): 165 | self._file.close() 166 | 167 | def update(self, subject: 'Vehicle', t:Time): 168 | row = [t.time, 169 | subject.id, 170 | subject.type, 171 | f"{subject.current_link[0]} {subject.current_link[1]}" if subject.current_link is not None else None, 172 | f"{subject.position[0]:.{self._prec}f} {subject.position[1]:.{self._prec}f}" if subject.position is not None else None, 173 | f"{subject.speed:.{self._prec}f}" if subject.speed is not None else None, 174 | subject.activity_type.name if subject.activity_type is not None else None, 175 | f"{subject.distance:.{self._prec}f}", 176 | ' '.join(p for p in subject.passengers), 177 | ' '.join(subject._achieved_path_since_last_notify)] 178 | subject.flush_achieved_path_since_last_notify() 179 | # log.info(f"OBS {time}: {row}") 180 | self._csvhandler.writerow(row) 181 | -------------------------------------------------------------------------------- /tests/mobility_services/test_station_based_vehicle_sharing.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import unittest 3 | from pathlib import Path 4 | import pandas as pd 5 | import numpy as np 6 | import math 7 | 8 | from mnms.generation.roads import generate_line_road, RoadDescriptor 9 | from mnms.graph.zone import Zone 10 | from mnms.graph.zone import construct_zone_from_sections, construct_zone_from_contour 11 | from mnms.graph.layers import MultiLayerGraph, SharedVehicleLayer, CarLayer 12 | from mnms.generation.roads import generate_manhattan_road 13 | from mnms.generation.layers import generate_matching_origin_destination_layer, generate_layer_from_roads 14 | from mnms.mobility_service.vehicle_sharing import VehicleSharingMobilityService 15 | from mnms.mobility_service.on_demand import OnDemandDepotMobilityService 16 | from mnms.tools.observer import CSVUserObserver, CSVVehicleObserver 17 | from mnms.vehicles.veh_type import Bike, Bus 18 | from mnms.travel_decision.dummy import DummyDecisionModel 19 | from mnms.flow.MFD import MFDFlowMotor, Reservoir 20 | from mnms.simulation import Supervisor 21 | from mnms.demand import BaseDemandManager, User 22 | from mnms.time import TimeTable, Time, Dt 23 | from mnms.mobility_service.public_transport import PublicTransportMobilityService 24 | from mnms.mobility_service.vehicle_sharing import VehicleSharingMobilityService 25 | from mnms.mobility_service.personal_vehicle import PersonalMobilityService 26 | from mnms.graph.layers import MultiLayerGraph, PublicTransportLayer 27 | from mnms.log import set_all_mnms_logger_level, LOGLEVEL 28 | 29 | 30 | class TestOnDemandDepotMobilityService(unittest.TestCase): 31 | def setUp(self): 32 | """Initiates the test. 33 | """ 34 | self.temp_dir_results = tempfile.TemporaryDirectory() 35 | self.dir_results = Path(self.temp_dir_results.name) 36 | 37 | def tearDown(self): 38 | """Concludes and closes the test. 39 | """ 40 | self.temp_dir_results.cleanup() 41 | 42 | def create_supervisor(self, sc): 43 | """Method to create a common supervisor for the different tests of this class. 44 | """ 45 | roads = generate_manhattan_road(3, 500, extended=False) 46 | 47 | if sc in ['1']: 48 | velov = VehicleSharingMobilityService("VELOV", 0, 0) 49 | elif sc in ['2']: 50 | velov = VehicleSharingMobilityService("VELOV", 0, 0, critical_nb_vehs=4, alpha=100, beta=0.15) 51 | velov.attach_vehicle_observer(CSVVehicleObserver(self.dir_results / "veh_velov.csv")) 52 | velov_layer = generate_layer_from_roads(roads, 'BIKESHARING', SharedVehicleLayer, Bike, 5, [velov]) 53 | 54 | odlayer = generate_matching_origin_destination_layer(roads) 55 | 56 | mlgraph = MultiLayerGraph([velov_layer], odlayer) 57 | 58 | velov.create_station('S0', '', 'BIKESHARING_0', capacity=20, nb_initial_veh=12) 59 | velov.create_station('S8', '', 'BIKESHARING_8', capacity=20, nb_initial_veh=12) 60 | 61 | mlgraph.connect_origindestination_layers(1) 62 | 63 | users_a = [User(f"UA{i}", [0, 0], [1500, 1500], Time("07:00:00").add_time(Dt(seconds=30*i)), pickup_dt=Dt(minutes=10)) for i in range(12)] 64 | users_b = [User(f"UB{i}", [1500, 1500], [0, 0], Time("07:00:00").add_time(Dt(seconds=30*i)), pickup_dt=Dt(minutes=10)) for i in range(12)] 65 | users = users_a + users_b 66 | users = sorted(users, key= lambda u: u.departure_time) 67 | demand = BaseDemandManager(users) 68 | demand.add_user_observer(CSVUserObserver(self.dir_results / 'users.csv')) 69 | 70 | decision_model = DummyDecisionModel(mlgraph, outfile=self.dir_results / "paths.csv", 71 | verbose_file=True) 72 | 73 | def mfdspeed(dacc): 74 | dspeed = {'BIKE': 5} 75 | return dspeed 76 | 77 | flow_motor = MFDFlowMotor() 78 | veh_types = ['BIKE'] 79 | flow_motor.add_reservoir(Reservoir(roads.zones["RES"], veh_types, mfdspeed)) 80 | 81 | supervisor = Supervisor(mlgraph, 82 | demand, 83 | flow_motor, 84 | decision_model, 85 | logfile='log.txt', 86 | loglevel=LOGLEVEL.INFO) 87 | set_all_mnms_logger_level(LOGLEVEL.INFO) 88 | 89 | return supervisor 90 | 91 | def test_station_based_vehicle_sharing_default_waiting_time_func(self): 92 | """Test the automatic zoning of the OnDemandDepotMobilityService based on 93 | the depots. 94 | """ 95 | ## Create supervisor 96 | supervisor = self.create_supervisor('1') 97 | 98 | ## Run 99 | flow_dt = Dt(seconds=30) 100 | affectation_factor = 1 101 | supervisor.run(Time("06:55:00"), 102 | Time("07:07:01"), 103 | flow_dt, 104 | affectation_factor) 105 | 106 | ## Get and check result 107 | w = lambda nv: 600 * (1 - (nv/10)**0.1) 108 | tt = 2000 / 5 109 | 110 | with open(self.dir_results / "paths.csv") as f: 111 | df = pd.read_csv(f, sep=';') 112 | df = df[df['EVENT'] == 'DEPARTURE'] 113 | 114 | for i in range(12): 115 | dfai = df[df['ID'] == f'UA{i}'] 116 | dfbi = df[df['ID'] == f'UB{i}'] 117 | self.assertAlmostEqual(dfai['COST'].iloc[0], tt + w(12-i)) 118 | self.assertAlmostEqual(dfbi['COST'].iloc[0], tt + w(12-i)) 119 | 120 | velov_stations = supervisor._mlgraph.layers['BIKESHARING'].mobility_services['VELOV'].stations 121 | self.assertEqual(len(velov_stations['S0'].waiting_vehicles), 1) 122 | self.assertEqual(len(velov_stations['S8'].waiting_vehicles), 1) 123 | 124 | def test_station_based_vehicle_sharing_custom_waiting_time_func(self): 125 | """Test the automatic zoning of the OnDemandDepotMobilityService based on 126 | the depots. 127 | """ 128 | ## Create supervisor 129 | supervisor = self.create_supervisor('2') 130 | 131 | ## Run 132 | flow_dt = Dt(seconds=30) 133 | affectation_factor = 1 134 | supervisor.run(Time("06:55:00"), 135 | Time("07:30:00"), 136 | flow_dt, 137 | affectation_factor) 138 | 139 | ## Get and check result 140 | w = lambda nv: 100 * (1 - (nv/4)**0.15) 141 | tt = 2000 / 5 142 | 143 | with open(self.dir_results / "paths.csv") as f: 144 | df = pd.read_csv(f, sep=';') 145 | df = df[df['EVENT'] == 'DEPARTURE'] 146 | 147 | for i in range(12): 148 | dfai = df[df['ID'] == f'UA{i}'] 149 | dfbi = df[df['ID'] == f'UB{i}'] 150 | self.assertAlmostEqual(dfai['COST'].iloc[0], tt + w(12-i)) 151 | self.assertAlmostEqual(dfbi['COST'].iloc[0], tt + w(12-i)) 152 | --------------------------------------------------------------------------------