From 2946b6fdd497865590560e3694e201ea4813fbfe Mon Sep 17 00:00:00 2001 From: 1kastner Date: Thu, 14 Dec 2023 20:06:37 +0100 Subject: [PATCH 1/7] add workflow_dispatch (#204) --- .github/workflows/installation-from-remote.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/installation-from-remote.yaml b/.github/workflows/installation-from-remote.yaml index 6435eb3..44cb46d 100644 --- a/.github/workflows/installation-from-remote.yaml +++ b/.github/workflows/installation-from-remote.yaml @@ -5,6 +5,7 @@ on: - cron: '42 23 * * 3' # every Wednesday at 23:42 pull_request: types: [opened, reopened, edited, synchronize] + workflow_dispatch: jobs: build-conda-on-windows: From 6fa453f79de3ab82af9d7988c8747666b11ea777 Mon Sep 17 00:00:00 2001 From: 1kastner Date: Sat, 10 Feb 2024 15:09:30 +0100 Subject: [PATCH 2/7] Improve explanation of how to build the documentation --- Contributing.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Contributing.md b/Contributing.md index 9f7f84b..a2d9ef5 100644 --- a/Contributing.md +++ b/Contributing.md @@ -58,14 +58,14 @@ Each new feature should be covered by tests unless there are very good reasons w For generating the documentation, [sphinx](https://www.sphinx-doc.org/) -is used - mostly the default settings are maintained. -The documentation generation process is based on the sphinx boilerplate and the `make` process is unchanged. -To generate the documentation, move to the directory `/docs`. +is used and mostly the default settings are maintained. + +To generate the documentation, change your working directory to `/docs`. First, please make sure that you have up-to-date prepared sqlite databases in `/docs/notebooks/data/prepared_dbs/`. The sqlite databases compatible with the latest version of ConFlowGen are available at https://media.tuhh.de/mls/software/conflowgen/docs/data/prepared_dbs/. -In `./docs/download_prepared_sqlite_databases.ps1`, you find the instructions for how to download the latest databases +In `/docs/download_prepared_sqlite_databases.ps1`, you find the instructions for how to download the latest databases and where to store them. In case you have updated the sqlite scheme, you might need to create these databases on your own with your latest adaptions. @@ -74,8 +74,11 @@ This is achieved by running the scripts stored in and copy the resulting sqlite database into `/docs/notebooks/data/prepared_dbs/`. -Once the prepared databases are in place, the documentation can be created. -As a Windows user you run `.\make.bat html` from the PowerShell or CMD. +Once the prepared databases are in place, the documentation can be built. +The documentation generation process is based on the sphinx boilerplate and the `make` process is unchanged. +For more information on that, see the +[Sphinx documentation on the build process](https://www.sphinx-doc.org/en/master/usage/quickstart.html#running-the-build). +As a Windows user, you run `.\make.bat html` from the PowerShell or CMD inside the directory `/docs`. Linux users invoke `make html` instead. The landing page of the documentation is created at `/docs/_build/html/index.html`. It is advised to use a strict approach by using the additional argument `SPHINXOPTS="-W --keep-going` From 8297d9fd2fa0f04486231b6c19fa720caa5c378a Mon Sep 17 00:00:00 2001 From: 1kastner Date: Mon, 11 Mar 2024 18:27:56 +0100 Subject: [PATCH 3/7] Adapt citation and add cff test (#206) * Add CITATION.cff workflow * Add abstract to CITATION.cff * adjust editors in CITATION.cff * Add contact person in CITATION.cff * Minor fix with wrongly named variable in demo workflow: rename matrix.python-inplace to matrix.inplace --- .github/workflows/citation.yml | 36 ++++++++++++++++++++++++++++++++++ .github/workflows/demo.yaml | 2 +- CITATION.cff | 8 +++++++- Readme.md | 3 ++- 4 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/citation.yml diff --git a/.github/workflows/citation.yml b/.github/workflows/citation.yml new file mode 100644 index 0000000..54eee2e --- /dev/null +++ b/.github/workflows/citation.yml @@ -0,0 +1,36 @@ +name: Validate CITATION.cff + +on: + push: + branches: + - main + pull_request: + branches: + - main + types: [opened, reopened, edited, synchronize] + +jobs: + build: + name: Validate CITATION.cff + runs-on: ubuntu-latest + + steps: + + - name: Skip Duplicate Actions + uses: fkirc/skip-duplicate-actions@v5 + + - uses: actions/checkout@v3 + + - name: Set up Python 3.8 + uses: actions/setup-python@v4 + with: + python-version: 3.8 + + - name: Install cffconvert + run: | + pip3 install --user cffconvert + cffconvert --version + + - name: Validate schema + run: | + cffconvert --validate -i CITATION.cff diff --git a/.github/workflows/demo.yaml b/.github/workflows/demo.yaml index d35029b..293f0d7 100644 --- a/.github/workflows/demo.yaml +++ b/.github/workflows/demo.yaml @@ -36,7 +36,7 @@ jobs: - name: Install Python dependencies run: | - pip3 install --user ${{ matrix.python-inplace }} . + pip3 install --user ${{ matrix.inplace }} . - name: Run demo run: | diff --git a/CITATION.cff b/CITATION.cff index 0970373..7032e81 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -3,6 +3,12 @@ cff-version: 1.2.0 title: ConFlowGen +abstract: A generator for synthetic container flows at maritime container terminals with a focus on yard operations +contact: + - affiliation: "Institute of Maritime Logistics, Hamburg University of Technology (TUHH)" + email: marvin.kastner@tuhh.de + family-names: Kastner + given-names: Marvin message: >- Please cite this software using the metadata from 'preferred-citation'. @@ -47,7 +53,7 @@ preferred-citation: country: DE date-start: 2022-02-23 date-end: 2022-02-25 - editors-series: + editors: - family-names: Freitag given-names: Michael - family-names: Kinra diff --git a/Readme.md b/Readme.md index 3f00dee..95dc8f2 100644 --- a/Readme.md +++ b/Readme.md @@ -1,5 +1,6 @@ [![Documentation Status](https://readthedocs.org/projects/conflowgen/badge/?version=latest)](https://conflowgen.readthedocs.io/en/latest/?badge=latest) -[![Docs](https://github.com/1kastner/conflowgen/actions/workflows/docs.yaml/badge.svg)](https://github.com/1kastner/conflowgen/actions/workflows/docs.yaml) +[![documentation built](https://github.com/1kastner/conflowgen/actions/workflows/docs.yaml/badge.svg)](https://github.com/1kastner/conflowgen/actions/workflows/docs.yaml) +[![CITATION.cff valid](https://github.com/1kastner/conflowgen/actions/workflows/citation.yml/badge.svg)](https://github.com/1kastner/conflowgen/actions/workflows/citation.yml) [![Tests](https://github.com/1kastner/conflowgen/actions/workflows/unittests.yaml/badge.svg)](https://github.com/1kastner/conflowgen/actions/workflows/unittests.yaml) [![codecov](https://codecov.io/gh/1kastner/conflowgen/branch/main/graph/badge.svg?token=GICVMYHJ42)](https://codecov.io/gh/1kastner/conflowgen) From 1abeefe76af824184ae27d4ee2e4cf2fc80e0ee0 Mon Sep 17 00:00:00 2001 From: 1kastner Date: Tue, 2 Apr 2024 10:32:13 +0200 Subject: [PATCH 4/7] Add Luc's publication (#207) * Add new publication to references * Describe each publication that covers ConFlowGen in a list format * Fix replacing labels with numbered list (must have missed it at some package update) --- docs/_static/css/custom.css | 6 +++--- docs/background.rst | 27 ++++++++++++++++----------- docs/references.bib | 26 ++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index f4e0a85..ec4973e 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -21,12 +21,12 @@ div.wy-nav-content { - https://sphinxcontrib-bibtex.readthedocs.io/en/latest/usage.html#bullet-lists-and-enumerated-lists - https://github.com/mcmtroffaes/sphinxcontrib-bibtex/issues/8#issuecomment-300141183 for more details. */ -dl.citation dt.label { +div.citation span.label { display: none !important; } -dl.citation dd { - display: list-item; +div.citation { + display: list-item !important; list-style-type: arabic; } diff --git a/docs/background.rst b/docs/background.rst index 2c00757..497fbc8 100644 --- a/docs/background.rst +++ b/docs/background.rst @@ -203,7 +203,8 @@ We are more than pleased to discuss the topic and add it to the list if suitable Presentation of ConFlowGen ~~~~~~~~~~~~~~~~~~~~~~~~~~ -ConFlowGen has been first presented at the International Conference on Dynamics in Logistics in February 2022. +ConFlowGen has been first presented at the International Conference on Dynamics in Logistics in February 2022 +:cite:`kastner2022conflowgen`. If ConFlowGen served you well in your research, and you would like to acknowledge the project in your publication, we would be glad if you mention our work as defined in our `CITATION.cff `_. @@ -225,13 +226,17 @@ If you just need a BibTeX entry for your citation software, this one should do t year = {2022} } -At a second occasion, ConFlowGen has been presented at the Annual General Assembly of the -World Association for Waterborne Transport Infrastructure (PIANC) -in 2023 in Oslo. -The contribution -`Synthetically generating traffic scenarios for simulation-based container terminal planning \ -`_ -has been awarded with the -`De Paepe-Willems Award `_. -The paper highlights how ConFlowGen can support terminal planners in designing terminal interfaces and determining -the required yard capacity. +If you are curious about what else has been achieved with ConFlowGen, these selected papers might be of interest for you: + +- ConFlowGen can support terminal planners in designing the seaside and landside of terminals as well as determining + the required yard capacity + :cite:`kastner2023synthetically`. + In 2023, the publication has been awarded with the + `De Paepe-Willems Award `_ + of the + World Association for Waterborne Transport Infrastructure (PIANC). + +- ConFlowGen can be used to estimate the variations in yard utilization over time + :cite:`edes2024estimating`. + The arrival patterns of actual sailing lists are used to estimate seaside throughput variations over several weeks. + These, in turn, affect the yard throughput and yard utilization. diff --git a/docs/references.bib b/docs/references.bib index e6750ac..c598277 100644 --- a/docs/references.bib +++ b/docs/references.bib @@ -113,3 +113,29 @@ @online{meisel2011unified-software note = {Accessed: 2022-07-20}, year = {2022} } + +@inproceedings{edes2024estimating, + abstract = {Vessel delays and increased terminal call sizes negatively impact the ability to properly plan daily operations at seaport container terminals. Such traffic patterns lead to, among others, infrequent peak loads at the seaside of container terminals, complicating terminal operations. Thus, relying on annual or monthly statistics fails to account for these day-to-day fluctuations. When container terminals are planned, be it a greenfield or brownfield terminal, these variations in operations need to be accounted for. The traditional formula-based approach to design terminals uses annual statistics. In this study, it is first used to produce estimates for the required yard capacity for three existing exemplary container terminals. These are then compared to the results of numerical experiments using the synthetic container flow generator ConFlowGen. The findings reveal that yard capacity requirements fluctuate considerably depending on the timing of vessel arrivals and their call sizes. This dynamic modeling proved particularly beneficial for planning gateway traffic, offering more accurate storage capacity predictions. Suggestions are made for how to further develop ConFlowGen for handling transshipment traffic better in future versions.}, + author = {Édes, Luc and Kastner, Marvin and Jahn, Carlos}, + title = {On Estimating the Required Yard Capacity for Container Terminals}, + url = {https://link.springer.com/chapter/10.1007/978-3-031-56826-8_13}, + pages = {171--182}, + publisher = {{Springer, Cham} and {Springer Nature Switzerland}}, + isbn = {978-3-031-56826-8}, + editor = {Freitag, Michael and Kinra, Aseem and Kotzab, Herbert and Megow, Nicole}, + booktitle = {Dynamics in Logistics}, + booksubtitle = {Proceedings of the 9th International Conference LDIC 2024, Bremen, Germany}, + year = {2024}, + address = {Cham, DE}, + doi = {10.1007/978-3-031-56826-8_13}, +} + +@incollection{kastner2023synthetically, + abstract = {More than 80 % of world trade is delivered via sea, making the maritime supply chain a very important backbone for the economy (UNCTAD 2020). Containerized trade regularly outperforms other types of transport in terms of growth, coinciding with consistent increases of average container vessel sizes (UNCTAD 2020). Container terminal operations are heavily affected by this development, since less but larger port calls create unwanted peaks and stress on the terminals and the hinterland. Not all container terminals are affected equally by the described situation. Economic cycles and events such as the global COVID-19 pandemic or the Russian war in Ukraine change the global supply chains, trade characteristics and transport demands between ports in the world. In 2004, Hartmann proposed an approach to create scenarios for simulation and optimization in the sense of container terminal planning and logistics. Due to the significant changes in maritime trade over the years, a new approach for generating synthetic container flow data became practical. In 2021, we introduced a rethought and reworked approach on this topic.The proposed tool, named ConFlowGen, aims to assist planners, scientists, and other maritime experts with providing comprehensive container flow scenarios based on minimal inputs and assumptions of the user. In this paper, we introduce ConFlowGen's general principle of operation in an exemplary use case in the context of container terminal planning.}, + author = {Kastner, Marvin and Grasse, Ole}, + title = {{Synthetically generating traffic scenarios for simulation-based container terminal planning}}, + booktitle = {{PIANC Yearbook 2023 [Preprint]}}, + year = {2023}, + doi = {10.15480/882.5156}, + url = {https://tore.tuhh.de/entities/publication/c7ae66d0-4165-44e9-8481-7057af2bc775} +} From 97447c9814d8f17a640a9c91f977257f19622fee Mon Sep 17 00:00:00 2001 From: 1kastner Date: Fri, 2 Aug 2024 17:58:58 +0200 Subject: [PATCH 5/7] Fix pillow issue in unittest (#212) --- .github/workflows/installation-from-remote.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/installation-from-remote.yaml b/.github/workflows/installation-from-remote.yaml index 44cb46d..85939fd 100644 --- a/.github/workflows/installation-from-remote.yaml +++ b/.github/workflows/installation-from-remote.yaml @@ -29,13 +29,15 @@ jobs: - name: Install ConFlowGen run: | + conda info + conda update conda conda info conda create -n test-install-conflowgen -c conda-forge conflowgen pytest - name: Prepare tests run: | conda activate test-install-conflowgen - conda install pillow>=9.0 + conda install -c conda-forge pillow>=9.0 - name: Run tests run: | @@ -66,6 +68,8 @@ jobs: conda init bash eval "$(conda shell.bash hook)" conda info + conda update conda + conda info conda activate base conda create -n test-install-conflowgen -c conda-forge conflowgen pytest From 147c40e958ba9cb6780cf492b48b17e67d879de8 Mon Sep 17 00:00:00 2001 From: Shubhangi Gupta <77153738+shubhs-93@users.noreply.github.com> Date: Tue, 20 Aug 2024 16:48:56 +0200 Subject: [PATCH 6/7] Add modification of the algorithm for an improved ramp-up and ramp-down phases (#209) * Add flow_direction as a container property * Modify how the onward transport assignment algorithm treats transshipment containers * Save ConFlowGen version in SQLITE table * Add happy path with ramp-up and ramp-down * drop unnecessary save statements * Modify CTA script and add 10 days of ramp-up and ramp-down * Change API of PortCallManager and adjust demo scripts to new API * Create VehicleCapacityManager and VehicleContainerVolumeCalculator to decrease the complexity of the LargeScheduledVehicleRepository - that one was just dealing with too many aspects at the same time! * Keep internal files in .tools subfolder * Fix TKinter issue in CI * PIANC Yearbook 2023 is now published - use updated citation * Update URL of DESTATIS * Renamed logging to logger --------- Co-authored-by: Shubhangi Gupta Co-authored-by: Marvin Kastner --- .gitignore | 1 + .../logging/__init__.py => .tools/.gitkeep | 0 conflowgen/__init__.py | 13 +- conflowgen/analyses/__init__.py | 8 +- conflowgen/analyses/abstract_analysis.py | 14 +- .../analyses/container_dwell_time_analysis.py | 4 +- .../container_dwell_time_analysis_report.py | 3 +- ...by_vehicle_type_analysis_summary_report.py | 4 +- ...ainer_flow_by_vehicle_instance_analysis.py | 125 +++++++ ...low_by_vehicle_instance_analysis_report.py | 155 ++++++++ ...le_type_adjustment_per_vehicle_analysis.py | 4 +- ..._adjustment_per_vehicle_analysis_report.py | 4 +- ..._and_outbound_vehicle_capacity_analysis.py | 2 +- .../analyses/modal_split_analysis_report.py | 4 +- ..._vehicle_capacity_utilization_analysis.py} | 5 +- ...e_capacity_utilization_analysis_report.py} | 10 +- .../api/container_flow_generation_manager.py | 26 +- conflowgen/api/port_call_manager.py | 98 ++++- .../container_flow_generation_properties.py | 17 +- .../container_flow_statistics_report.py | 20 +- ...und_vehicle_capacity_calculator_service.py | 21 +- .../services/vehicle_capacity_manager.py | 212 +++++++++++ .../vehicle_container_volume_calculator.py | 115 ++++++ conflowgen/descriptive_datatypes/__init__.py | 25 +- conflowgen/domain_models/container.py | 14 + .../factories/container_factory.py | 33 +- .../domain_models/factories/fleet_factory.py | 16 +- .../factories/schedule_factory.py | 6 +- .../factories/vehicle_factory.py | 28 +- .../domain_models/large_vehicle_schedule.py | 2 +- .../large_scheduled_vehicle_repository.py | 169 +-------- .../repositories/schedule_repository.py | 31 +- conflowgen/domain_models/vehicle.py | 2 +- ...r_containers_delivered_by_truck_service.py | 16 +- .../container_flow_generation_service.py | 21 +- ...arge_scheduled_vehicle_creation_service.py | 11 +- ...hicle_for_onward_transportation_manager.py | 30 +- conflowgen/logger/__init__.py | 0 .../{logging/logging.py => logger/logger.py} | 0 .../test_container_dwell_time_analysis.py | 16 +- ...st_container_dwell_time_analysis_report.py | 4 +- ...container_flow_by_vehicle_type_analysis.py | 8 +- ...er_flow_by_vehicle_type_analysis_report.py | 4 +- ...le_type_adjustment_per_vehicle_analysis.py | 22 +- ..._adjustment_per_vehicle_analysis_report.py | 6 +- ..._and_outbound_vehicle_capacity_analysis.py | 8 +- ...tbound_vehicle_capacity_analysis_report.py | 4 +- ..._outbound_capacity_utilization_analysis.py | 12 +- ...nd_capacity_utilization_analysis_report.py | 10 +- .../analyses/test_modal_split_analysis.py | 12 +- .../test_modal_split_analysis_report.py | 4 +- .../test_quay_side_throughput_analysis.py | 8 +- ...st_quay_side_throughput_analysis_report.py | 8 +- .../tests/analyses/test_run_all_analyses.py | 8 +- .../test_truck_gate_throughput_analysis.py | 12 +- ...t_truck_gate_throughput_analysis_report.py | 8 +- .../analyses/test_yard_capacity_analysis.py | 14 +- .../test_yard_capacity_analysis_report.py | 4 +- .../test_container_flow_generation_manager.py | 8 +- .../tests/api/test_port_call_manager.py | 50 ++- .../test_container_flow_statistics_report.py | 20 +- .../services/test_vehicle_capacity_manager.py | 274 ++++++++++++++ ...st_vehicle_countainer_volume_calculator.py | 189 ++++++++++ .../test_data_summaries_cache.py | 4 +- ...ner_destination_distribution_repository.py | 16 +- ...ory__create_for_large_scheduled_vehicle.py | 71 +++- ...est_container_factory__create_for_truck.py | 2 +- ...test_fleet_factory__create_feeder_fleet.py | 2 +- .../factories/test_schedule_factory.py | 12 +- .../test_vehicle_factory__create_barge.py | 10 +- ...vehicle_factory__create_deep_sea_vessel.py | 12 +- .../test_vehicle_factory__create_feeder.py | 10 +- .../test_vehicle_factory__create_train.py | 8 +- ...test_large_scheduled_vehicle_repository.py | 104 ------ .../reposistories/test_schedule_repository.py | 101 +++--- .../tests/domain_models/test_container.py | 63 ++++ .../tests/domain_models/test_vehicle.py | 24 +- ...r_containers_delivered_by_truck_service.py | 15 +- ...assign_destination_to_container_service.py | 8 +- ...tainer_flow_generator_service__generate.py | 67 +++- ...xport_container_flow_service__container.py | 2 +- ...hicle_for_onward_transportation_manager.py | 167 ++++++++- ...est_truck_for_export_containers_manager.py | 4 +- ...est_truck_for_import_containers_manager.py | 4 +- .../analyses_with_missing_data.ipynb | 2 +- ..._container_flow_by_vehicle_type_preview.py | 2 +- ...ner_flow_by_vehicle_type_preview_report.py | 4 +- ...d_and_outbound_vehicle_capacity_preview.py | 10 +- ...utbound_vehicle_capacity_preview_report.py | 4 +- ...preview__get_modal_split_for_hinterland.py | 2 +- ..._modal_split_preview__get_transshipment.py | 2 +- .../test_modal_split_preview_report.py | 4 +- .../test_quay_side_throughput_preview.py | 2 +- ...est_quay_side_throughput_preview_report.py | 4 +- .../test_truck_gate_throughput_preview.py | 6 +- ...st_truck_gate_throughput_preview_report.py | 4 +- .../test_vehicle_capacity_exceeded_preview.py | 2 +- ...ehicle_capacity_exceeded_preview_report.py | 4 +- docs/api.rst | 10 +- docs/background.rst | 3 +- docs/notebooks/analyses.ipynb | 6 +- docs/notebooks/first_steps.ipynb | 16 +- docs/references.bib | 35 +- examples/Python_Script/demo_DEHAM_CTA.py | 28 +- ..._CTA__with_ramp_up_and_ramp_down_period.py | 335 ++++++++++++++++++ .../Python_Script/demo_continental_gateway.py | 12 +- examples/Python_Script/demo_poc.py | 22 +- run_ci_light.bat | 1 + setup.py | 1 + 109 files changed, 2493 insertions(+), 716 deletions(-) rename conflowgen/logging/__init__.py => .tools/.gitkeep (100%) create mode 100644 conflowgen/analyses/container_flow_by_vehicle_instance_analysis.py create mode 100644 conflowgen/analyses/container_flow_by_vehicle_instance_analysis_report.py rename conflowgen/analyses/{inbound_to_outbound_vehicle_capacity_utilization_analysis.py => outbound_to_inbound_vehicle_capacity_utilization_analysis.py} (95%) rename conflowgen/analyses/{inbound_to_outbound_vehicle_capacity_utilization_analysis_report.py => outbound_to_inbound_vehicle_capacity_utilization_analysis_report.py} (97%) create mode 100644 conflowgen/application/services/vehicle_capacity_manager.py create mode 100644 conflowgen/application/services/vehicle_container_volume_calculator.py create mode 100644 conflowgen/logger/__init__.py rename conflowgen/{logging/logging.py => logger/logger.py} (100%) create mode 100644 conflowgen/tests/application/services/test_vehicle_capacity_manager.py create mode 100644 conflowgen/tests/application/services/test_vehicle_countainer_volume_calculator.py delete mode 100644 conflowgen/tests/domain_models/reposistories/test_large_scheduled_vehicle_repository.py create mode 100644 examples/Python_Script/demo_DEHAM_CTA__with_ramp_up_and_ramp_down_period.py diff --git a/.gitignore b/.gitignore index f1eb505..487aaeb 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,4 @@ examples/Python_Script/databases/ # Ignore local changes as they happen with every execution. If something changes, the commit must be forced. conflowgen/data/tools/ docs/notebooks/data/prepared_dbs/ +.tools/ diff --git a/conflowgen/logging/__init__.py b/.tools/.gitkeep similarity index 100% rename from conflowgen/logging/__init__.py rename to .tools/.gitkeep diff --git a/conflowgen/__init__.py b/conflowgen/__init__.py index 7eaeb02..dc1d3b8 100644 --- a/conflowgen/__init__.py +++ b/conflowgen/__init__.py @@ -35,10 +35,10 @@ InboundAndOutboundVehicleCapacityAnalysis from conflowgen.analyses.inbound_and_outbound_vehicle_capacity_analysis_report import \ InboundAndOutboundVehicleCapacityAnalysisReport -from conflowgen.analyses.inbound_to_outbound_vehicle_capacity_utilization_analysis import \ - InboundToOutboundVehicleCapacityUtilizationAnalysis -from conflowgen.analyses.inbound_to_outbound_vehicle_capacity_utilization_analysis_report import \ - InboundToOutboundVehicleCapacityUtilizationAnalysisReport +from conflowgen.analyses.outbound_to_inbound_vehicle_capacity_utilization_analysis import \ + OutboundToInboundVehicleCapacityUtilizationAnalysis +from conflowgen.analyses.outbound_to_inbound_vehicle_capacity_utilization_analysis_report import \ + OutboundToInboundVehicleCapacityUtilizationAnalysisReport from conflowgen.analyses.container_flow_by_vehicle_type_analysis import ContainerFlowByVehicleTypeAnalysis from conflowgen.analyses.container_flow_by_vehicle_type_analysis_report import \ ContainerFlowByVehicleTypeAnalysisReport @@ -64,6 +64,9 @@ ContainerFlowVehicleTypeAdjustmentPerVehicleAnalysis from conflowgen.analyses.container_flow_vehicle_type_adjustment_per_vehicle_analysis_report import \ ContainerFlowVehicleTypeAdjustmentPerVehicleAnalysisReport +from conflowgen.analyses.container_flow_by_vehicle_instance_analysis import ContainerFlowByVehicleInstanceAnalysis +from conflowgen.analyses.container_flow_by_vehicle_instance_analysis_report import ( + ContainerFlowByVehicleInstanceAnalysisReport) # Cache for analyses and previews from conflowgen.data_summaries.data_summaries_cache import DataSummariesCache @@ -83,7 +86,7 @@ from conflowgen.domain_models.data_types.storage_requirement import StorageRequirement # List of functions -from conflowgen.logging.logging import setup_logger +from conflowgen.logger.logger import setup_logger from conflowgen.analyses import run_all_analyses from conflowgen.previews import run_all_previews diff --git a/conflowgen/analyses/__init__.py b/conflowgen/analyses/__init__.py index 000026d..5cb8f4a 100644 --- a/conflowgen/analyses/__init__.py +++ b/conflowgen/analyses/__init__.py @@ -7,12 +7,13 @@ ContainerFlowAdjustmentByVehicleTypeAnalysisReport from .container_flow_adjustment_by_vehicle_type_analysis_summary_report import \ ContainerFlowAdjustmentByVehicleTypeAnalysisSummaryReport +from .container_flow_by_vehicle_instance_analysis_report import ContainerFlowByVehicleInstanceAnalysisReport from .container_flow_by_vehicle_type_analysis_report import ContainerFlowByVehicleTypeAnalysisReport from .container_flow_vehicle_type_adjustment_per_vehicle_analysis_report import \ ContainerFlowVehicleTypeAdjustmentPerVehicleAnalysisReport from .inbound_and_outbound_vehicle_capacity_analysis_report import InboundAndOutboundVehicleCapacityAnalysisReport -from .inbound_to_outbound_vehicle_capacity_utilization_analysis_report import \ - InboundToOutboundVehicleCapacityUtilizationAnalysisReport +from .outbound_to_inbound_vehicle_capacity_utilization_analysis_report import \ + OutboundToInboundVehicleCapacityUtilizationAnalysisReport from .modal_split_analysis_report import ModalSplitAnalysisReport from .quay_side_throughput_analysis_report import QuaySideThroughputAnalysisReport from .truck_gate_throughput_analysis_report import TruckGateThroughputAnalysisReport @@ -26,6 +27,7 @@ reports: typing.Iterable[typing.Type[AbstractReport]] = [ InboundAndOutboundVehicleCapacityAnalysisReport, ContainerFlowByVehicleTypeAnalysisReport, + ContainerFlowByVehicleInstanceAnalysisReport, ContainerFlowAdjustmentByVehicleTypeAnalysisReport, ContainerFlowAdjustmentByVehicleTypeAnalysisSummaryReport, ContainerFlowVehicleTypeAdjustmentPerVehicleAnalysisReport, @@ -34,7 +36,7 @@ QuaySideThroughputAnalysisReport, TruckGateThroughputAnalysisReport, YardCapacityAnalysisReport, - InboundToOutboundVehicleCapacityUtilizationAnalysisReport + OutboundToInboundVehicleCapacityUtilizationAnalysisReport ] diff --git a/conflowgen/analyses/abstract_analysis.py b/conflowgen/analyses/abstract_analysis.py index e8e466e..c5fbad4 100644 --- a/conflowgen/analyses/abstract_analysis.py +++ b/conflowgen/analyses/abstract_analysis.py @@ -98,35 +98,39 @@ def _restrict_storage_requirement(selected_containers: ModelSelect, storage_requ @staticmethod def _restrict_container_delivered_by_vehicle_type( selected_containers: ModelSelect, container_delivered_by_vehicle_type: typing.Any - ) -> ModelSelect: + ) -> (ModelSelect, list[ModeOfTransport]): if hashable(container_delivered_by_vehicle_type) \ and container_delivered_by_vehicle_type in set(ModeOfTransport): selected_containers = selected_containers.where( Container.delivered_by == container_delivered_by_vehicle_type ) + list_of_vehicle_types = [container_delivered_by_vehicle_type] else: # assume it is some kind of collection (list, set, ...) selected_containers = selected_containers.where( Container.delivered_by << container_delivered_by_vehicle_type ) - return selected_containers + list_of_vehicle_types = list(container_delivered_by_vehicle_type) + return selected_containers, list_of_vehicle_types @staticmethod def _restrict_container_picked_up_by_vehicle_type( selected_containers: ModelSelect, container_picked_up_by_vehicle_type: typing.Any - ) -> ModelSelect: + ) -> (ModelSelect, list[ModeOfTransport]): if container_picked_up_by_vehicle_type == "scheduled vehicles": container_picked_up_by_vehicle_type = ModeOfTransport.get_scheduled_vehicles() - + list_of_vehicle_types = container_picked_up_by_vehicle_type if hashable(container_picked_up_by_vehicle_type) \ and container_picked_up_by_vehicle_type in set(ModeOfTransport): selected_containers = selected_containers.where( Container.picked_up_by == container_picked_up_by_vehicle_type ) + list_of_vehicle_types = [container_picked_up_by_vehicle_type] else: # assume it is some kind of collection (list, set, ...) selected_containers = selected_containers.where( Container.picked_up_by << container_picked_up_by_vehicle_type ) - return selected_containers + list_of_vehicle_types = list(container_picked_up_by_vehicle_type) + return selected_containers, list_of_vehicle_types @staticmethod def _restrict_container_picked_up_by_initial_vehicle_type( diff --git a/conflowgen/analyses/container_dwell_time_analysis.py b/conflowgen/analyses/container_dwell_time_analysis.py index 822e42b..8a30db7 100644 --- a/conflowgen/analyses/container_dwell_time_analysis.py +++ b/conflowgen/analyses/container_dwell_time_analysis.py @@ -64,12 +64,12 @@ def get_container_dwell_times( ) if container_delivered_by_vehicle_type != "all": - selected_containers = self._restrict_container_delivered_by_vehicle_type( + selected_containers, vehicle_types = self._restrict_container_delivered_by_vehicle_type( selected_containers, container_delivered_by_vehicle_type ) if container_picked_up_by_vehicle_type != "all": - selected_containers = self._restrict_container_picked_up_by_vehicle_type( + selected_containers, vehicle_types = self._restrict_container_picked_up_by_vehicle_type( selected_containers, container_picked_up_by_vehicle_type ) diff --git a/conflowgen/analyses/container_dwell_time_analysis_report.py b/conflowgen/analyses/container_dwell_time_analysis_report.py index ce494cd..9e479dd 100644 --- a/conflowgen/analyses/container_dwell_time_analysis_report.py +++ b/conflowgen/analyses/container_dwell_time_analysis_report.py @@ -4,8 +4,9 @@ import statistics import typing # noqa, pylint: disable=unused-import # lgtm [py/unused-import] # used in the docstring -import matplotlib.axis import pandas as pd +import matplotlib.axis + from conflowgen.analyses.container_dwell_time_analysis import ContainerDwellTimeAnalysis from conflowgen.reporting import AbstractReportWithMatplotlib diff --git a/conflowgen/analyses/container_flow_adjustment_by_vehicle_type_analysis_summary_report.py b/conflowgen/analyses/container_flow_adjustment_by_vehicle_type_analysis_summary_report.py index c1fb92c..da9d9f0 100644 --- a/conflowgen/analyses/container_flow_adjustment_by_vehicle_type_analysis_summary_report.py +++ b/conflowgen/analyses/container_flow_adjustment_by_vehicle_type_analysis_summary_report.py @@ -2,9 +2,9 @@ import typing # noqa, pylint: disable=unused-import # lgtm [py/unused-import] # used in the docstring -import matplotlib.axis -import numpy as np import pandas as pd +import numpy as np +import matplotlib.axis from conflowgen.analyses.container_flow_adjustment_by_vehicle_type_analysis_summary import \ ContainerFlowAdjustmentByVehicleTypeAnalysisSummary, ContainerFlowAdjustedToVehicleType diff --git a/conflowgen/analyses/container_flow_by_vehicle_instance_analysis.py b/conflowgen/analyses/container_flow_by_vehicle_instance_analysis.py new file mode 100644 index 0000000..4e92e4d --- /dev/null +++ b/conflowgen/analyses/container_flow_by_vehicle_instance_analysis.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +import datetime +import typing + +from peewee import ModelSelect + +from conflowgen.data_summaries.data_summaries_cache import DataSummariesCache +from conflowgen.domain_models.container import Container +from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport +from conflowgen.analyses.abstract_analysis import AbstractAnalysis +from conflowgen.descriptive_datatypes import VehicleIdentifier, FlowDirection + + +class ContainerFlowByVehicleInstanceAnalysis(AbstractAnalysis): + """ + This analysis can be run after the synthetic data has been generated. + The analysis returns a data structure that can be used for generating reports (e.g., in text or as a figure) + as it is the case with :class:`.ContainerFlowByVehicleInstanceAnalysisReport`. + """ + + @DataSummariesCache.cache_result + def get_container_flow_by_vehicle( + self, + vehicle_types: typing.Collection[ModeOfTransport] = ( + ModeOfTransport.train, + ModeOfTransport.feeder, + ModeOfTransport.deep_sea_vessel, + ModeOfTransport.barge + ), + start_date: typing.Optional[datetime.datetime] = None, + end_date: typing.Optional[datetime.datetime] = None, + ) -> [ + ModeOfTransport, typing.Dict[VehicleIdentifier, typing.Dict[FlowDirection, (int, int)]] + ]: + """ + Args: + vehicle_types: A collection of vehicle types, e.g., passed as a :class:`list` or :class:`set`. + Only the vehicles that correspond to the provided vehicle type(s) are considered in the analysis. + start_date: + The earliest arriving container that is included. Consider all containers if :obj:`None`. + end_date: + The latest departing container that is included. Consider all containers if :obj:`None`. + + Returns: + Grouped by vehicle type and vehicle instance, how many import, export, and transshipment containers are + unloaded and loaded (measured in TEU). + """ + + container_flow_by_vehicle: typing.Dict[ + ModeOfTransport, typing.Dict[VehicleIdentifier, + typing.Dict[FlowDirection, typing.Dict[str, int]]]] = { + vehicle_type: {} + for vehicle_type in ModeOfTransport + } + + vehicle_identifier_cache = {} + + selected_containers: ModelSelect = Container.select().where( + (Container.delivered_by.in_(vehicle_types) | Container.picked_up_by.in_(vehicle_types)) + ) + + container: Container + for container in selected_containers: + if start_date and container.get_arrival_time() < start_date: + continue + if end_date and container.get_departure_time() > end_date: + continue + + if container.delivered_by_large_scheduled_vehicle is not None: # if not transported by truck + + if container.delivered_by_large_scheduled_vehicle not in vehicle_identifier_cache: + vehicle_id_inbound = VehicleIdentifier( + id=container.delivered_by_large_scheduled_vehicle.id, + mode_of_transport=container.delivered_by, + service_name=container.delivered_by_large_scheduled_vehicle.schedule.service_name, + vehicle_name=container.delivered_by_large_scheduled_vehicle.vehicle_name, + vehicle_arrival_time=container.get_arrival_time(), + ) + vehicle_identifier_cache[container.delivered_by_large_scheduled_vehicle] = vehicle_id_inbound + + vehicle_id_inbound = vehicle_identifier_cache[container.delivered_by_large_scheduled_vehicle] + + if vehicle_id_inbound not in container_flow_by_vehicle[container.delivered_by]: + container_flow_by_vehicle[container.delivered_by][vehicle_id_inbound] = { + flow_direction: { + "inbound": 0, + "outbound": 0, + } + for flow_direction in FlowDirection + } + container_flow_by_vehicle[ + container.delivered_by + ][vehicle_id_inbound][container.flow_direction]["inbound"] += container.occupied_teu + + if container.picked_up_by_large_scheduled_vehicle is not None: # if not transported by truck + + if container.picked_up_by_large_scheduled_vehicle not in vehicle_identifier_cache: + vehicle_id_outbound = VehicleIdentifier( + id=container.picked_up_by_large_scheduled_vehicle.id, + mode_of_transport=container.picked_up_by, + service_name=container.picked_up_by_large_scheduled_vehicle.schedule.service_name, + vehicle_name=container.picked_up_by_large_scheduled_vehicle.vehicle_name, + vehicle_arrival_time=container.get_departure_time(), + ) + vehicle_identifier_cache[container.picked_up_by_large_scheduled_vehicle] = vehicle_id_outbound + + vehicle_id_outbound = vehicle_identifier_cache[container.picked_up_by_large_scheduled_vehicle] + + if vehicle_id_outbound not in container_flow_by_vehicle[container.picked_up_by]: + container_flow_by_vehicle[container.picked_up_by][vehicle_id_outbound] = { + flow_direction: { + "inbound": 0, + "outbound": 0, + } + for flow_direction in FlowDirection + } + container_flow_by_vehicle[ + container.picked_up_by][vehicle_id_outbound][container.flow_direction]["outbound"] += 1 + + for skipped_vehicle_type in set(ModeOfTransport) - set(vehicle_types): + assert len(container_flow_by_vehicle[skipped_vehicle_type]) == 0 + del container_flow_by_vehicle[skipped_vehicle_type] + + return container_flow_by_vehicle diff --git a/conflowgen/analyses/container_flow_by_vehicle_instance_analysis_report.py b/conflowgen/analyses/container_flow_by_vehicle_instance_analysis_report.py new file mode 100644 index 0000000..a6327e6 --- /dev/null +++ b/conflowgen/analyses/container_flow_by_vehicle_instance_analysis_report.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +import logging +import typing + +import matplotlib.figure +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd + +from conflowgen.analyses.container_flow_by_vehicle_instance_analysis import ContainerFlowByVehicleInstanceAnalysis +from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport +from conflowgen.reporting import AbstractReportWithMatplotlib +from conflowgen.reporting.no_data_plot import no_data_graph + + +class ContainerFlowByVehicleInstanceAnalysisReport(AbstractReportWithMatplotlib): + """ + This analysis report takes the data structure as generated by :class:`.ContainerFlowByVehicleInstanceAnalysis` + and creates a comprehensible representation for the user, either as text or as a graph. + The visual and table are expected to approximately look like in the + `example ContainerFlowByVehicleInstanceAnalysisReport \ + `_. + """ + + report_description = """ + Analyze how many import, export, and transshipment containers were unloaded and loaded on each vehicle. + """ + + logger = logging.getLogger("conflowgen") + + def __init__(self): + super().__init__() + self._df = None + self.analysis = ContainerFlowByVehicleInstanceAnalysis() + + def get_report_as_text( + self, + vehicle_types: ModeOfTransport | str | typing.Collection = "scheduled vehicles", + **kwargs + ) -> str: + """ + Keyword Args: + vehicle_types (typing.Collection[ModeOfTransport]): A collection of vehicle types, e.g., passed as a + :class:`list` or :class:`set`. + Only the vehicles that correspond to the provided vehicle type(s) are considered in the analysis. + start_date (datetime.datetime): The earliest arriving container that is included. + Consider all containers if :obj:`None`. + end_date (datetime.datetime): The latest departing container that is included. + Consider all containers if :obj:`None`. + + Returns: + The report in human-readable text format + """ + plain_table, vehicle_types = self._get_analysis(kwargs) + + if len(plain_table) > 0: + df = self._get_dataframe_from_plain_table(plain_table) + report = str(df) + else: + report = "(no report feasible because no data is available)" + + return report + + def _get_analysis(self, kwargs: dict) -> (list, list): + start_date = kwargs.pop("start_date", None) + end_date = kwargs.pop("end_date", None) + vehicle_types = kwargs.pop("vehicle_types", ( + ModeOfTransport.train, + ModeOfTransport.feeder, + ModeOfTransport.deep_sea_vessel, + ModeOfTransport.barge + )) + + assert len(kwargs) == 0, f"Keyword(s) {kwargs.keys()} have not been processed" + + container_flow = self.analysis.get_container_flow_by_vehicle( + vehicle_types=vehicle_types, + start_date=start_date, + end_date=end_date, + ) + + vehicle_types = list(container_flow.keys()) + + plain_table = [] + + for mode_of_transport in vehicle_types: + for vehicle_identifier in container_flow[mode_of_transport].keys(): + for flow_direction in container_flow[mode_of_transport][vehicle_identifier]: + for journey_direction in container_flow[mode_of_transport][vehicle_identifier][flow_direction]: + handled_volume = container_flow[ + mode_of_transport][vehicle_identifier][flow_direction][journey_direction] + + plain_table.append( + (mode_of_transport, vehicle_identifier.id, vehicle_identifier.vehicle_name, + vehicle_identifier.service_name, vehicle_identifier.vehicle_arrival_time, + str(flow_direction), journey_direction, handled_volume) + ) + + return plain_table, vehicle_types + + def get_report_as_graph(self, **kwargs) -> matplotlib.figure.Figure: + """ + Keyword Args: + vehicle_types (typing.Collection[ModeOfTransport]): A collection of vehicle types, e.g., passed as a + :class:`list` or :class:`set`. + Only the vehicles that correspond to the provided vehicle type(s) are considered in the analysis. + start_date (datetime.datetime): + The earliest arriving container that is included. Consider all containers if :obj:`None`. + end_date (datetime.datetime): + The latest departing container that is included. Consider all containers if :obj:`None`. + + Returns: + Grouped by vehicle type and vehicle instance, how many import, export, and transshipment containers are + unloaded and loaded (measured in TEU). + """ + plain_table, vehicle_types = self._get_analysis(kwargs) + + plot_title = "Container Flow By Vehicle Instance Analysis Report" + + if len(plain_table) == 0: + fig, ax = no_data_graph() + ax.set_title(plot_title) + return fig + + df = self._get_dataframe_from_plain_table(plain_table) + + number_subplots = 2 * len(vehicle_types) + fig, axes = plt.subplots(nrows=number_subplots, figsize=(7, 4 * number_subplots)) + i = 0 + for mode_of_transport in vehicle_types: + for journey_direction in ["inbound", "outbound"]: + ax = axes[i] + i += 1 + ax.set_title(f"{str(mode_of_transport).replace('_', ' ').capitalize()} - {journey_direction}") + df[(df["mode_of_transport"] == mode_of_transport) + & (df["journey_direction"] == journey_direction) + ].groupby("flow_direction")["handled_volume"].plot(ax=ax, linestyle=":", marker=".") + ax.set_ylabel("") + + plt.legend(loc='center left', bbox_to_anchor=(1, 0.5)) + + plt.tight_layout() + + return fig + + def _get_dataframe_from_plain_table(self, plain_table): + column_names = ("mode_of_transport", "vehicle_id", "vehicle_name", "service_name", "vehicle_arrival_time", + "flow_direction", "journey_direction", "handled_volume") + df = pd.DataFrame(plain_table) + df.columns = column_names + df.set_index("vehicle_arrival_time", inplace=True) + df.replace(0, np.nan, inplace=True) + self._df = df + return df diff --git a/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis.py b/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis.py index 0d5a058..f77ea07 100644 --- a/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis.py +++ b/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis.py @@ -61,7 +61,7 @@ def get_vehicle_type_adjustments_per_vehicle( ) if adjusted_vehicle_type is not None and adjusted_vehicle_type != "all": - selected_containers = self._restrict_container_picked_up_by_vehicle_type( + selected_containers, list_of_vehicle_types = self._restrict_container_picked_up_by_vehicle_type( selected_containers, adjusted_vehicle_type ) @@ -99,6 +99,7 @@ def get_vehicle_type_adjustments_per_vehicle( def _get_vehicle_identifier_for_vehicle_picking_up_the_container(container: Container) -> VehicleIdentifier: if container.picked_up_by == ModeOfTransport.truck: vehicle_identifier = VehicleIdentifier( + id=None, mode_of_transport=ModeOfTransport.truck, vehicle_arrival_time=container.get_departure_time(), service_name=None, @@ -106,6 +107,7 @@ def _get_vehicle_identifier_for_vehicle_picking_up_the_container(container: Cont ) else: vehicle_identifier = VehicleIdentifier( + id=container.picked_up_by_large_scheduled_vehicle.id, mode_of_transport=container.picked_up_by, vehicle_arrival_time=container.get_departure_time(), service_name=container.picked_up_by_large_scheduled_vehicle.schedule.service_name, diff --git a/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis_report.py b/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis_report.py index 3680f26..c3f285a 100644 --- a/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis_report.py +++ b/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis_report.py @@ -3,14 +3,14 @@ import datetime import typing -import matplotlib.axes import pandas as pd +import matplotlib.axes from matplotlib import pyplot as plt from matplotlib.ticker import FuncFormatter from conflowgen.analyses.container_flow_vehicle_type_adjustment_per_vehicle_analysis import \ ContainerFlowVehicleTypeAdjustmentPerVehicleAnalysis -from conflowgen.analyses.inbound_to_outbound_vehicle_capacity_utilization_analysis import VehicleIdentifier +from conflowgen.analyses.outbound_to_inbound_vehicle_capacity_utilization_analysis import VehicleIdentifier from conflowgen.reporting import AbstractReportWithMatplotlib from conflowgen.reporting.no_data_plot import no_data_graph diff --git a/conflowgen/analyses/inbound_and_outbound_vehicle_capacity_analysis.py b/conflowgen/analyses/inbound_and_outbound_vehicle_capacity_analysis.py index 638eab6..46a03e1 100644 --- a/conflowgen/analyses/inbound_and_outbound_vehicle_capacity_analysis.py +++ b/conflowgen/analyses/inbound_and_outbound_vehicle_capacity_analysis.py @@ -109,7 +109,7 @@ def get_outbound_container_volume_by_vehicle_type( large_scheduled_vehicle: LargeScheduledVehicle for large_scheduled_vehicle in LargeScheduledVehicle.select(): maximum_capacity_of_vehicle = min( - int(large_scheduled_vehicle.moved_capacity * (1 + self.transportation_buffer)), + int(large_scheduled_vehicle.inbound_container_volume * (1 + self.transportation_buffer)), large_scheduled_vehicle.capacity_in_teu ) vehicle_type: ModeOfTransport = large_scheduled_vehicle.schedule.vehicle_type diff --git a/conflowgen/analyses/modal_split_analysis_report.py b/conflowgen/analyses/modal_split_analysis_report.py index 48cb36a..6de362a 100644 --- a/conflowgen/analyses/modal_split_analysis_report.py +++ b/conflowgen/analyses/modal_split_analysis_report.py @@ -18,8 +18,8 @@ class ModalSplitAnalysisReport(AbstractReportWithMatplotlib): """ report_description = """ - Analyze the amount of containers dedicated for or coming from the hinterland compared to the amount of containers - that are transshipment. + Analyze how many containers are delivered and picked up by which type of vehicle. + This counts the containers in the yard, i.e., transshipment containers are *not* counted twice. """ def __init__(self): diff --git a/conflowgen/analyses/inbound_to_outbound_vehicle_capacity_utilization_analysis.py b/conflowgen/analyses/outbound_to_inbound_vehicle_capacity_utilization_analysis.py similarity index 95% rename from conflowgen/analyses/inbound_to_outbound_vehicle_capacity_utilization_analysis.py rename to conflowgen/analyses/outbound_to_inbound_vehicle_capacity_utilization_analysis.py index 3362655..f84df5d 100644 --- a/conflowgen/analyses/inbound_to_outbound_vehicle_capacity_utilization_analysis.py +++ b/conflowgen/analyses/outbound_to_inbound_vehicle_capacity_utilization_analysis.py @@ -23,7 +23,7 @@ class InboundAndOutboundCapacity(typing.NamedTuple): outbound_capacity: float -class InboundToOutboundVehicleCapacityUtilizationAnalysis(AbstractAnalysis): +class OutboundToInboundVehicleCapacityUtilizationAnalysis(AbstractAnalysis): """ This analysis can be run after the synthetic data has been generated. The analysis returns a data structure that can be used for generating reports (e.g., in text or as a figure) @@ -66,7 +66,7 @@ def get_inbound_and_outbound_capacity_of_each_vehicle( # vehicle properties vehicle_name = vehicle.vehicle_name vehicle_arrival_time = vehicle.get_arrival_time() - used_capacity_on_inbound_journey = vehicle.moved_capacity + used_capacity_on_inbound_journey = vehicle.inbound_container_volume if start_date and vehicle_arrival_time < start_date: continue @@ -87,6 +87,7 @@ def get_inbound_and_outbound_capacity_of_each_vehicle( used_capacity_on_outbound_journey += container.occupied_teu vehicle_id = VehicleIdentifier( + id=vehicle.id, mode_of_transport=mode_of_transport, service_name=service_name, vehicle_name=vehicle_name, diff --git a/conflowgen/analyses/inbound_to_outbound_vehicle_capacity_utilization_analysis_report.py b/conflowgen/analyses/outbound_to_inbound_vehicle_capacity_utilization_analysis_report.py similarity index 97% rename from conflowgen/analyses/inbound_to_outbound_vehicle_capacity_utilization_analysis_report.py rename to conflowgen/analyses/outbound_to_inbound_vehicle_capacity_utilization_analysis_report.py index 4e82ef4..90f791c 100644 --- a/conflowgen/analyses/inbound_to_outbound_vehicle_capacity_utilization_analysis_report.py +++ b/conflowgen/analyses/outbound_to_inbound_vehicle_capacity_utilization_analysis_report.py @@ -9,8 +9,8 @@ import pandas as pd from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport -from conflowgen.analyses.inbound_to_outbound_vehicle_capacity_utilization_analysis import \ - InboundToOutboundVehicleCapacityUtilizationAnalysis, VehicleIdentifier +from conflowgen.analyses.outbound_to_inbound_vehicle_capacity_utilization_analysis import \ + OutboundToInboundVehicleCapacityUtilizationAnalysis, VehicleIdentifier from conflowgen.reporting import AbstractReportWithMatplotlib from conflowgen.reporting.no_data_plot import no_data_graph @@ -19,13 +19,13 @@ class UnsupportedPlotTypeException(Exception): pass -class InboundToOutboundVehicleCapacityUtilizationAnalysisReport(AbstractReportWithMatplotlib): +class OutboundToInboundVehicleCapacityUtilizationAnalysisReport(AbstractReportWithMatplotlib): """ This analysis report takes the data structure as generated by :class:`.InboundToOutboundCapacityUtilizationAnalysis` and creates a comprehensible representation for the user, either as text or as a graph. The visual and table are expected to approximately look like in the `example InboundToOutboundVehicleCapacityUtilizationAnalysisReport \ - `_. + `_. """ report_description = """ @@ -45,7 +45,7 @@ def __init__(self): self._start_date = None self._vehicle_type_description = None self.vehicle_type_description = None - self.analysis = InboundToOutboundVehicleCapacityUtilizationAnalysis( + self.analysis = OutboundToInboundVehicleCapacityUtilizationAnalysis( transportation_buffer=self.transportation_buffer ) self._df = None diff --git a/conflowgen/api/container_flow_generation_manager.py b/conflowgen/api/container_flow_generation_manager.py index 4cb3e0e..ada21a3 100644 --- a/conflowgen/api/container_flow_generation_manager.py +++ b/conflowgen/api/container_flow_generation_manager.py @@ -8,6 +8,7 @@ ContainerFlowGenerationPropertiesRepository from conflowgen.flow_generator.container_flow_generation_service import \ ContainerFlowGenerationService +from conflowgen.metadata import __version__ class ContainerFlowGenerationManager: @@ -27,7 +28,9 @@ def set_properties( start_date: datetime.date, end_date: datetime.date, name: typing.Optional[str] = None, - transportation_buffer: typing.Optional[float] = None + transportation_buffer: typing.Optional[float] = None, + ramp_up_period: typing.Optional[datetime.timedelta] = None, + ramp_down_period: typing.Optional[datetime.timedelta] = None, ) -> None: """ Args: @@ -38,7 +41,13 @@ def set_properties( name: The name of the generated synthetic container flow which helps to distinguish different scenarios. transportation_buffer: Determines how many percent more of the inbound journey capacity is used at most to transport containers on the outbound journey. + ramp_up_period: The period at the beginning during which yard occupancy gradually increases. + During the ramp-up period, the share of transshipment containers on outbound journey is reduced. + ramp_down_period: The period at the end during which operations gradually fade out. + During the ramp-down period, inbound container volumes are scaled down, reducing the number of new + import, export, and transshipment containers in the yard during this period.. """ + properties = self.container_flow_generation_properties_repository.get_container_flow_generation_properties() if name is not None: @@ -47,9 +56,21 @@ def set_properties( properties.start_date = start_date properties.end_date = end_date + if ramp_up_period: + properties.ramp_up_period = ramp_up_period.total_seconds() / 86400 # in days as float + else: + properties.ramp_up_period = 0 + + if ramp_down_period: + properties.ramp_down_period = ramp_down_period.total_seconds() / 86400 # in days as float + else: + properties.ramp_down_period = 0 + if transportation_buffer is not None: properties.transportation_buffer = transportation_buffer + properties.conflowgen_version = __version__ + self.container_flow_generation_properties_repository.set_container_flow_generation_properties( properties ) @@ -67,6 +88,9 @@ def get_properties(self) -> typing.Dict[str, typing.Union[str, datetime.date, fl 'start_date': properties.start_date, 'end_date': properties.end_date, 'transportation_buffer': properties.transportation_buffer, + 'ramp_up_period': properties.ramp_up_period, + 'ramp_down_period': properties.ramp_down_period, + 'conflowgen_version': properties.conflowgen_version } def container_flow_data_exists(self) -> bool: diff --git a/conflowgen/api/port_call_manager.py b/conflowgen/api/port_call_manager.py index 5da8191..46b8d9e 100644 --- a/conflowgen/api/port_call_manager.py +++ b/conflowgen/api/port_call_manager.py @@ -1,6 +1,7 @@ from __future__ import annotations import datetime import typing +import uuid from conflowgen.data_summaries.data_summaries_cache import DataSummariesCache from conflowgen.domain_models.factories.schedule_factory import ScheduleFactory @@ -22,20 +23,21 @@ class PortCallManager: def __init__(self): self.schedule_factory = ScheduleFactory() - def add_vehicle( + def add_service_that_calls_terminal( self, vehicle_type: ModeOfTransport, service_name: str, vehicle_arrives_at: datetime.date, vehicle_arrives_at_time: datetime.time, average_vehicle_capacity: int, - average_moved_capacity: int, + average_inbound_container_volume: int, next_destinations: typing.Optional[typing.List[typing.Tuple[str, float]]] = None, - vehicle_arrives_every_k_days: typing.Optional[int] = None + vehicle_arrives_every_k_days: int = 7 ) -> None: r""" - Add the schedule of a ship of any size or a train. The concrete vehicle instances are automatically generated - when :meth:`.ContainerFlowGenerationManager.generate` is invoked. + Add a service that frequently calls the terminal, both on the seaside and landside. + The vehicle instances are automatically generated when :meth:`.ContainerFlowGenerationManager.generate` is + invoked. Args: vehicle_type: One of @@ -55,10 +57,10 @@ def add_vehicle( Number of TEU that can be transported with the vehicle at most. The number of moved containers can never exceed this number, no matter what the value for the ``transportation_buffer`` is set to. - average_moved_capacity: - The average moved capacity describes the number of TEU which the vehicle delivers to the terminal on its - inbound journey. - When summing up the TEU factors of each of the loaded containers on the inbound journey, this value is + average_inbound_container_volume: + The average moved container volume describes the number of TEU which the vehicle delivers to the + terminal on its inbound journey. + When summing up the TEU values of each of the loaded containers on the inbound journey, this value is never exceeded but closely approximated. For the outbound journey, the containers are assigned depending on the distribution kept in :class:`.ModeOfTransportDistributionManager`. @@ -67,13 +69,14 @@ def add_vehicle( .. math:: min( - \text{average_moved_capacity} \cdot \text{transportation_buffer},\text{average_vehicle_capacity} + \text{average_inbound_container_volume} \cdot \text{transportation_buffer}, + \text{average_vehicle_capacity} ) If you have calibrated the aforementioned distribution accordingly, the actual number of containers on the outbound journey in TEU should be on average the same as on the inbound journey. - In that case, the vehicle moves ``average_moved_capacity`` number of containers in TEU on its inbound - journey and the same number of containers in TEU again on its outbound journey. + In that case, the vehicle moves ``average_inbound_container_volume`` number of containers in TEU on its + inbound journey and the same number of containers in TEU again on its outbound journey. next_destinations: Pairs of destination and frequency of the destination being chosen. vehicle_arrives_every_k_days: @@ -98,12 +101,81 @@ def add_vehicle( vehicle_arrives_at=vehicle_arrives_at, vehicle_arrives_at_time=vehicle_arrives_at_time, average_vehicle_capacity=average_vehicle_capacity, - average_moved_capacity=average_moved_capacity, + average_inbound_container_volume=average_inbound_container_volume, next_destinations=next_destinations, vehicle_arrives_every_k_days=vehicle_arrives_every_k_days ) DataSummariesCache.reset_cache() + def add_vehicle( + self, + vehicle_type: ModeOfTransport, + vehicle_arrives_at: datetime.date, + vehicle_arrives_at_time: datetime.time, + vehicle_capacity: int, + inbound_container_volume: int, + next_destinations: typing.Optional[typing.List[typing.Tuple[str, float]]] = None, + service_name: typing.Optional[str] = None, + ) -> None: + r""" + Add a service that frequently calls the terminal, both on the seaside and landside. + The vehicle instances are automatically generated when :meth:`.ContainerFlowGenerationManager.generate` is + invoked. + + Args: + vehicle_type: One of + :class:`ModeOfTransport.deep_sea_vessel`, + :class:`ModeOfTransport.feeder`, + :class:`ModeOfTransport.barge`, or + :class:`ModeOfTransport.train` + service_name: + The name of the service, i.e., the shipping line or rail freight line. + Defaults to a randomly generated id. + vehicle_arrives_at: + A date the service would arrive at the terminal. This can, e.g., point at the week day for weekly + services. In any case, this is combined with the parameter ``vehicle_arrives_every_k_days`` and only + arrivals within the time scope between ``start_date`` and ``end_date`` are considered. + vehicle_arrives_at_time: + A time of the day (between 00:00 and 23:59). + vehicle_capacity: + Number of TEU that can be transported with the vehicle at most. + The number of moved containers can never exceed this number, no matter what the value for the + ``transportation_buffer`` is set to. + inbound_container_volume: + This describes the number of TEU which the vehicle delivers to the + terminal on its inbound journey. + When summing up the TEU values of each of the loaded containers on the inbound journey, this value is + never exceeded but closely approximated. + For the outbound journey, the containers are assigned depending on the distribution kept in + :class:`.ModeOfTransportDistributionManager`. + The maximum number of containers in TEU on the outbound journey of the vehicle is bound by + + .. math:: + + min( + \text{average_inbound_container_volume} \cdot \text{transportation_buffer}, + \text{average_vehicle_capacity} + ) + + If you have calibrated the aforementioned distribution accordingly, the actual number of containers on + the outbound journey in TEU should be on average the same as on the inbound journey. + In that case, the vehicle moves ``average_inbound_container_volume`` number of containers in TEU on its + inbound journey and the same number of containers in TEU again on its outbound journey. + next_destinations: + Pairs of destination and frequency of the destination being chosen. + """ + + return self.add_service_that_calls_terminal( + vehicle_type=vehicle_type, + service_name=service_name if service_name is not None else str(uuid.uuid4()), + vehicle_arrives_at=vehicle_arrives_at, + vehicle_arrives_at_time=vehicle_arrives_at_time, + average_vehicle_capacity=vehicle_capacity, + average_inbound_container_volume=inbound_container_volume, + next_destinations=next_destinations, + vehicle_arrives_every_k_days=-1 + ) + def has_schedule( self, service_name: str, diff --git a/conflowgen/application/models/container_flow_generation_properties.py b/conflowgen/application/models/container_flow_generation_properties.py index 82a033d..4651d00 100644 --- a/conflowgen/application/models/container_flow_generation_properties.py +++ b/conflowgen/application/models/container_flow_generation_properties.py @@ -4,6 +4,7 @@ from conflowgen.domain_models.seeders import DEFAULT_TRANSPORTATION_BUFFER from conflowgen.domain_models.base_model import BaseModel +from conflowgen.metadata import __version__ class ContainerFlowGenerationProperties(BaseModel): @@ -14,7 +15,7 @@ class ContainerFlowGenerationProperties(BaseModel): name = CharField( null=True, - help_text="The name of the generated container flow, e.g. a scenario" + help_text="The name of the generated container flow, e.g., describing the scenario" ) start_date = DateField( @@ -27,6 +28,16 @@ class ContainerFlowGenerationProperties(BaseModel): help_text="The last day of the generated container flow" ) + ramp_up_period = FloatField( + default=0, + help_text="Number of days for the ramp-up period" + ) + + ramp_down_period = FloatField( + default=0, + help_text="Number of days for the ramp-down period" + ) + generated_at = DateTimeField( default=lambda: datetime.datetime.now().replace(microsecond=0), help_text="The date the these properties have been created" @@ -39,3 +50,7 @@ class ContainerFlowGenerationProperties(BaseModel): transportation_buffer = FloatField( default=DEFAULT_TRANSPORTATION_BUFFER, ) + + conflowgen_version = CharField( + default=__version__ + ) diff --git a/conflowgen/application/reports/container_flow_statistics_report.py b/conflowgen/application/reports/container_flow_statistics_report.py index b673610..9de3ffb 100644 --- a/conflowgen/application/reports/container_flow_statistics_report.py +++ b/conflowgen/application/reports/container_flow_statistics_report.py @@ -4,6 +4,8 @@ import statistics from typing import List, Type, Dict +from conflowgen.application.services.vehicle_capacity_manager import VehicleCapacityManager +from conflowgen.descriptive_datatypes import FlowDirection from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport from conflowgen.domain_models.repositories.large_scheduled_vehicle_repository import LargeScheduledVehicleRepository from conflowgen.domain_models.vehicle import AbstractLargeScheduledVehicle, LargeScheduledVehicle @@ -12,6 +14,7 @@ class ContainerFlowStatisticsReport: def __init__(self, transportation_buffer=None): self.large_scheduled_vehicle_repository = LargeScheduledVehicleRepository() + self.vehicle_capacity_manager = VehicleCapacityManager() self.logger = logging.getLogger("conflowgen") self.free_capacity_inbound_statistics = {} self.free_capacity_outbound_statistics = {} @@ -19,7 +22,7 @@ def __init__(self, transportation_buffer=None): self.set_transportation_buffer(transportation_buffer=transportation_buffer) def set_transportation_buffer(self, transportation_buffer: float) -> None: - self.large_scheduled_vehicle_repository.set_transportation_buffer(transportation_buffer) + self.vehicle_capacity_manager.set_transportation_buffer(transportation_buffer) self.logger.info(f"Use transportation buffer of {transportation_buffer} for reporting statistics.") def generate(self) -> None: @@ -30,7 +33,7 @@ def generate(self) -> None: self._generate_free_capacity_statistics(vehicles_of_types) def _generate_free_capacity_statistics(self, vehicles_of_types): - buffer_factor = 1 + self.large_scheduled_vehicle_repository.transportation_buffer + buffer_factor = 1 + self.vehicle_capacity_manager.vehicle_container_volume_calculator.transportation_buffer free_capacities_inbound = {} free_capacities_outbound = {} vehicle_type: ModeOfTransport @@ -40,11 +43,12 @@ def _generate_free_capacity_statistics(self, vehicles_of_types): # noinspection PyTypeChecker large_scheduled_vehicle: LargeScheduledVehicle = vehicle.large_scheduled_vehicle - free_capacity_inbound = self.large_scheduled_vehicle_repository.get_free_capacity_for_inbound_journey( + free_capacity_inbound = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( vehicle ) - free_capacity_outbound = self.large_scheduled_vehicle_repository.get_free_capacity_for_outbound_journey( - vehicle + free_capacity_outbound = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + vehicle, + FlowDirection.undefined ) assert free_capacity_inbound <= large_scheduled_vehicle.capacity_in_teu, \ f"A vehicle can only load at maximum its capacity, but for vehicle {vehicle} the free capacity " \ @@ -55,11 +59,11 @@ def _generate_free_capacity_statistics(self, vehicles_of_types): f"of {free_capacity_outbound} for outbound does not match with the capacity of the vehicle of " \ f"{large_scheduled_vehicle.capacity_in_teu} TEU" - assert (free_capacity_inbound <= large_scheduled_vehicle.moved_capacity), \ + assert (free_capacity_inbound <= large_scheduled_vehicle.inbound_container_volume), \ f"A vehicle must not exceed its moved capacity, but for vehicle {vehicle} the free " \ f"capacity of {free_capacity_inbound} TEU for inbound does not match with the moved capacity " \ - f"of {large_scheduled_vehicle.moved_capacity}" - moved_capacity_with_outbound_buffer = (large_scheduled_vehicle.moved_capacity * buffer_factor) + f"of {large_scheduled_vehicle.inbound_container_volume}" + moved_capacity_with_outbound_buffer = (large_scheduled_vehicle.inbound_container_volume * buffer_factor) assert (free_capacity_outbound <= moved_capacity_with_outbound_buffer), \ f"A vehicle must not exceed its transportation buffer, but for vehicle {vehicle} the free " \ f"capacity of {free_capacity_outbound} for outbound does not match with the moved capacity " \ diff --git a/conflowgen/application/services/inbound_and_outbound_vehicle_capacity_calculator_service.py b/conflowgen/application/services/inbound_and_outbound_vehicle_capacity_calculator_service.py index 5b5583f..5d75e61 100644 --- a/conflowgen/application/services/inbound_and_outbound_vehicle_capacity_calculator_service.py +++ b/conflowgen/application/services/inbound_and_outbound_vehicle_capacity_calculator_service.py @@ -62,6 +62,7 @@ def get_inbound_capacity_of_vehicles( at_least_one_schedule_exists: bool = False + schedule: Schedule for schedule in Schedule.select(): at_least_one_schedule_exists = True arrivals = create_arrivals_within_time_range( @@ -72,7 +73,7 @@ def get_inbound_capacity_of_vehicles( schedule.vehicle_arrives_at_time ) moved_inbound_volumes = (len(arrivals) # number of vehicles that are planned - * schedule.average_moved_capacity) # moved TEU capacity of each vehicle + * schedule.average_inbound_container_volume) # moved TEU capacity of each vehicle inbound_container_volume_in_teu[schedule.vehicle_type] += moved_inbound_volumes inbound_container_volume_in_containers[schedule.vehicle_type] += moved_inbound_volumes / \ ContainerLengthDistributionRepository.get_teu_factor() @@ -120,11 +121,15 @@ def get_outbound_capacity_of_vehicles(start_date, end_date, transportation_buffe schedule: Schedule for schedule in Schedule.select(): - assert schedule.average_moved_capacity <= schedule.average_vehicle_capacity, \ - "A vehicle cannot move a larger amount of containers (in TEU) than its capacity, " \ - f"the input data is malformed. Schedule '{schedule.service_name}' of vehicle type " \ - f"{schedule.vehicle_type} has an average moved capacity of {schedule.average_moved_capacity} but an " \ - f"averaged vehicle capacity of {schedule.average_vehicle_capacity}." + assert \ + schedule.average_inbound_container_volume <= schedule.average_vehicle_capacity, \ + ( + "A vehicle cannot move a larger amount of containers (in TEU) than its capacity, " + f"the input data is malformed. Schedule '{schedule.service_name}' of vehicle type " + f"{schedule.vehicle_type} has an average moved capacity of " + f"{schedule.average_inbound_container_volume} but an averaged vehicle capacity of " + f"{schedule.average_vehicle_capacity}." + ) arrivals = create_arrivals_within_time_range( start_date, @@ -135,14 +140,14 @@ def get_outbound_capacity_of_vehicles(start_date, end_date, transportation_buffe ) # If all container flows are balanced, only the average moved capacity is required - container_volume_moved_by_vessels_in_teu = len(arrivals) * schedule.average_moved_capacity + container_volume_moved_by_vessels_in_teu = len(arrivals) * schedule.average_inbound_container_volume outbound_used_capacity_in_teu[schedule.vehicle_type] += container_volume_moved_by_vessels_in_teu outbound_used_containers[schedule.vehicle_type] += container_volume_moved_by_vessels_in_teu / \ ContainerLengthDistributionRepository.get_teu_factor() # If there are unbalanced container flows, a vehicle departs with more containers than it delivered maximum_capacity_of_vehicle_in_teu = min( - schedule.average_moved_capacity * (1 + transportation_buffer), + schedule.average_inbound_container_volume * (1 + transportation_buffer), schedule.average_vehicle_capacity ) total_maximum_capacity_moved_by_vessel = len(arrivals) * maximum_capacity_of_vehicle_in_teu diff --git a/conflowgen/application/services/vehicle_capacity_manager.py b/conflowgen/application/services/vehicle_capacity_manager.py new file mode 100644 index 0000000..23b08a3 --- /dev/null +++ b/conflowgen/application/services/vehicle_capacity_manager.py @@ -0,0 +1,212 @@ +import datetime +import logging +import typing +from typing import Dict, Callable, Type + +from conflowgen.application.services.vehicle_container_volume_calculator import VehicleContainerVolumeCalculator +from conflowgen.descriptive_datatypes import FlowDirection +from conflowgen.domain_models.container import Container +from conflowgen.domain_models.data_types.container_length import ContainerLength +from conflowgen.domain_models.vehicle import LargeScheduledVehicle, AbstractLargeScheduledVehicle + + +class VehicleCapacityManager: + ignored_capacity = ContainerLength.get_teu_factor(ContainerLength.other) + + def __init__(self): + self.vehicle_container_volume_calculator = VehicleContainerVolumeCalculator() + self.occupied_capacity_for_outbound_journey_buffer: ( + Dict)[Type[AbstractLargeScheduledVehicle], float] = {} + self.occupied_capacity_for_inbound_journey_buffer: ( + Dict)[Type[AbstractLargeScheduledVehicle], float] = {} + self.logger = logging.getLogger("conflowgen") + + def set_transportation_buffer(self, transportation_buffer: float): + self.vehicle_container_volume_calculator.set_transportation_buffer(transportation_buffer) + + def set_ramp_up_and_down_times( + self, + ramp_up_period_end: typing.Optional[datetime.datetime] = None, + ramp_down_period_start: typing.Optional[datetime.datetime] = None, + ) -> None: + self.vehicle_container_volume_calculator.set_ramp_up_and_down_times( + ramp_up_period_end=ramp_up_period_end, + ramp_down_period_start=ramp_down_period_start, + ) + + def reset_cache(self): + self.occupied_capacity_for_inbound_journey_buffer = {} + self.occupied_capacity_for_outbound_journey_buffer = {} + + def block_capacity_for_inbound_journey( + self, + vehicle: Type[AbstractLargeScheduledVehicle], + container: Container + ) -> bool: + assert vehicle in self.occupied_capacity_for_inbound_journey_buffer, \ + "First .get_free_capacity_for_inbound_journey(vehicle) must be invoked" + + usable_vessel_capacity = self.vehicle_container_volume_calculator. \ + get_transported_container_volume_on_inbound_journey(vehicle) + + occupied_capacity_in_teu = self.occupied_capacity_for_inbound_journey_buffer[vehicle] + used_capacity_in_teu = ContainerLength.get_teu_factor(container_length=container.length) + new_occupied_capacity_in_teu = occupied_capacity_in_teu + used_capacity_in_teu + + new_free_capacity_in_teu = usable_vessel_capacity - new_occupied_capacity_in_teu + assert \ + new_free_capacity_in_teu >= 0, \ + ( + f"vehicle {vehicle} is overloaded, " + f"usable_vessel_capacity: {usable_vessel_capacity}, " + f"occupied_capacity_in_teu: {occupied_capacity_in_teu}, " + f"used_capacity_in_teu: {used_capacity_in_teu}, " + f"new_free_capacity_in_teu: {new_free_capacity_in_teu}" + ) + + self.occupied_capacity_for_inbound_journey_buffer[vehicle] = new_occupied_capacity_in_teu + vehicle_capacity_is_exhausted = new_free_capacity_in_teu < self.ignored_capacity + return vehicle_capacity_is_exhausted + + def block_capacity_for_outbound_journey( + self, + vehicle: Type[AbstractLargeScheduledVehicle], + container: Container + ) -> bool: + assert vehicle in self.occupied_capacity_for_outbound_journey_buffer, \ + "First .get_free_capacity_for_outbound_journey(vehicle) must be invoked" + + scaled_moved_container_volume, unscaled_moved_container_volume = self.vehicle_container_volume_calculator. \ + get_maximum_transported_container_volume_on_outbound_journey(vehicle, container.flow_direction) + + # calculate new free capacity + occupied_capacity_in_teu = self.occupied_capacity_for_outbound_journey_buffer[vehicle] + used_capacity_in_teu = ContainerLength.get_teu_factor(container_length=container.length) + new_occupied_capacity_in_teu = occupied_capacity_in_teu + used_capacity_in_teu + + new_scaled_free_capacity_in_teu = scaled_moved_container_volume - new_occupied_capacity_in_teu + + new_unscaled_free_capacity_in_teu = unscaled_moved_container_volume - new_occupied_capacity_in_teu + assert \ + new_unscaled_free_capacity_in_teu >= 0, \ + ( + f"vehicle {vehicle} is overloaded, " + f"scaled_moved_container_volume: {scaled_moved_container_volume}, " + f"unscaled_moved_container_volume: {unscaled_moved_container_volume}, " + f"occupied_capacity_in_teu: {occupied_capacity_in_teu}, " + f"used_capacity_in_teu: {used_capacity_in_teu}, " + f"new_scaled_free_capacity_in_teu: {new_scaled_free_capacity_in_teu} " + f"new_unscaled_free_capacity_in_teu: {new_unscaled_free_capacity_in_teu}" + ) + + self.occupied_capacity_for_outbound_journey_buffer[vehicle] = new_occupied_capacity_in_teu + + space_is_exhausted = (new_scaled_free_capacity_in_teu <= self.ignored_capacity) + return space_is_exhausted + + def get_free_capacity_for_inbound_journey(self, vehicle: Type[AbstractLargeScheduledVehicle]) -> float: + """ + Get the free capacity for the inbound journey on a vehicle that moves according to a schedule in TEU. + During the ramp-down period (if existent), all inbound traffic is scaled down, no matter what. + """ + inbound_container_volume = self.vehicle_container_volume_calculator \ + .get_transported_container_volume_on_inbound_journey(vehicle) + + if vehicle in self.occupied_capacity_for_inbound_journey_buffer: + occupied_capacity_in_teu = self.occupied_capacity_for_inbound_journey_buffer[vehicle] + else: + occupied_capacity_in_teu = self._get_occupied_capacity_in_teu( + vehicle=vehicle, + container_counter=self._get_number_containers_for_inbound_journey, + ) + self.occupied_capacity_for_inbound_journey_buffer[vehicle] = occupied_capacity_in_teu + + free_capacity_in_teu = inbound_container_volume - occupied_capacity_in_teu + return free_capacity_in_teu + + def get_free_capacity_for_outbound_journey( + self, + vehicle: Type[AbstractLargeScheduledVehicle], + flow_direction: FlowDirection + ) -> float: + """ + Get the free capacity for the outbound journey on a vehicle that moves according to a schedule in TEU. + During the ramp-up period (if existent), all outbound traffic that constitutes transshipment, is scaled down. + """ + + # During the ramp-up period, the container volume is reduced at this stage + maximum_transported_container_volume, unscaled_moved_container_volume = \ + self.vehicle_container_volume_calculator.get_maximum_transported_container_volume_on_outbound_journey( + vehicle, flow_direction + ) + + if vehicle in self.occupied_capacity_for_outbound_journey_buffer: + occupied_capacity_in_teu = self.occupied_capacity_for_outbound_journey_buffer[vehicle] + else: + occupied_capacity_in_teu = self._get_occupied_capacity_in_teu( + vehicle=vehicle, + container_counter=self._get_number_containers_for_outbound_journey, + ) + self.occupied_capacity_for_outbound_journey_buffer[vehicle] = occupied_capacity_in_teu + + assert \ + unscaled_moved_container_volume - occupied_capacity_in_teu >= 0, \ + ( + f"vehicle {vehicle} is overloaded, " + f"maximum_transported_container_volume: {maximum_transported_container_volume}, " + f"unscaled_moved_container_volume: {unscaled_moved_container_volume}, " + f"occupied_capacity_in_teu: {occupied_capacity_in_teu}" + ) + + free_capacity = max(maximum_transported_container_volume - occupied_capacity_in_teu, 0) + + return free_capacity + + @staticmethod + def _get_occupied_capacity_in_teu( + vehicle: Type[AbstractLargeScheduledVehicle], + container_counter: Callable[[Type[AbstractLargeScheduledVehicle], ContainerLength], int], + ) -> (float, float): + loaded_20_foot_containers = container_counter(vehicle, ContainerLength.twenty_feet) + loaded_40_foot_containers = container_counter(vehicle, ContainerLength.forty_feet) + loaded_45_foot_containers = container_counter(vehicle, ContainerLength.forty_five_feet) + loaded_other_containers = container_counter(vehicle, ContainerLength.other) + occupied_capacity = ( + loaded_20_foot_containers * ContainerLength.get_teu_factor(ContainerLength.twenty_feet) + + loaded_40_foot_containers * ContainerLength.get_teu_factor(ContainerLength.forty_feet) + + loaded_45_foot_containers * ContainerLength.get_teu_factor(ContainerLength.forty_five_feet) + + loaded_other_containers * ContainerLength.get_teu_factor(ContainerLength.other) + ) + return occupied_capacity + + @classmethod + def _get_number_containers_for_outbound_journey( + cls, + vehicle: Type[AbstractLargeScheduledVehicle], + container_length: ContainerLength + ) -> int: + """Returns the number of containers on a specific vehicle of a specific container length that are picked up by + the vehicle""" + # noinspection PyTypeChecker + large_scheduled_vehicle: LargeScheduledVehicle = vehicle.large_scheduled_vehicle + number_loaded_containers = Container.select().where( + (Container.picked_up_by_large_scheduled_vehicle == large_scheduled_vehicle) + & (Container.length == container_length) + ).count() + return number_loaded_containers + + @classmethod + def _get_number_containers_for_inbound_journey( + cls, + vehicle: Type[AbstractLargeScheduledVehicle], + container_length: ContainerLength + ) -> int: + """Returns the number of containers on a specific vehicle of a specific container length that are delivered by + the vehicle""" + + large_scheduled_vehicle: LargeScheduledVehicle = vehicle.large_scheduled_vehicle + number_loaded_containers = Container.select().where( + (Container.delivered_by_large_scheduled_vehicle == large_scheduled_vehicle) + & (Container.length == container_length) + ).count() + return number_loaded_containers diff --git a/conflowgen/application/services/vehicle_container_volume_calculator.py b/conflowgen/application/services/vehicle_container_volume_calculator.py new file mode 100644 index 0000000..ab9a1f1 --- /dev/null +++ b/conflowgen/application/services/vehicle_container_volume_calculator.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +import datetime +import logging +import typing + +from conflowgen.descriptive_datatypes import FlowDirection +from conflowgen.domain_models.vehicle import LargeScheduledVehicle, AbstractLargeScheduledVehicle + + +class VehicleContainerVolumeCalculator: + + downscale_factor_during_ramp_up_period_for_outbound_transshipment = 0.2 + + downscale_factor_during_ramp_down_period_for_inbound_all_kinds = 0.2 + + def __init__(self): + self.transportation_buffer = None + self.ramp_up_period_end = None + self.ramp_down_period_start = None + self.logger = logging.getLogger("conflowgen") + + def set_transportation_buffer( + self, + transportation_buffer: float + ) -> None: + assert -1 < transportation_buffer + self.transportation_buffer = transportation_buffer + + def set_ramp_up_and_down_times( + self, + ramp_up_period_end: typing.Optional[datetime.datetime] = None, + ramp_down_period_start: typing.Optional[datetime.datetime] = None + ) -> None: + self.ramp_up_period_end = ramp_up_period_end + self.ramp_down_period_start = ramp_down_period_start + + def get_transported_container_volume_on_inbound_journey( + self, + large_scheduled_vehicle: LargeScheduledVehicle | typing.Type[AbstractLargeScheduledVehicle], + ) -> float: + """ + + Args: + large_scheduled_vehicle: The vehicle for which the transported container volume is calculated for + + Returns: + The container volume in TEU that are transported by this vehicle on its inbound journey. The volume is + scaled down during the ramp-down period if present. + """ + + # auto-cast vehicles to their LargeScheduledVehicle reference + if hasattr(large_scheduled_vehicle, "large_scheduled_vehicle"): + large_scheduled_vehicle = large_scheduled_vehicle.large_scheduled_vehicle + + # This is our default + moved_container_volume = large_scheduled_vehicle.inbound_container_volume + + if self.ramp_down_period_start is not None: + arrival_time: datetime.datetime = large_scheduled_vehicle.scheduled_arrival + if arrival_time >= self.ramp_down_period_start: + moved_container_volume *= self.downscale_factor_during_ramp_down_period_for_inbound_all_kinds + + return moved_container_volume + + def get_maximum_transported_container_volume_on_outbound_journey( + self, + large_scheduled_vehicle: LargeScheduledVehicle | typing.Type[AbstractLargeScheduledVehicle], + flow_direction: FlowDirection, + ) -> (float, float): + """ + + Args: + large_scheduled_vehicle: The vehicle for which the expected transported container volume is calculated for + flow_direction: The flow direction to consider. + + Returns: + The container volume in TEU that are transported by this vehicle on its outbound journey. The volume is + scaled down during the ramp-down period if present for transshipment containers. + """ + + assert self.transportation_buffer is not None, "Transportation buffer must be set" + assert -1 < self.transportation_buffer, "Transportation buffer must be larger than -1" + + # auto-cast vehicles to their LargeScheduledVehicle reference + if hasattr(large_scheduled_vehicle, "large_scheduled_vehicle"): + large_scheduled_vehicle = large_scheduled_vehicle.large_scheduled_vehicle + + # this is our default + unscaled_moved_container_volume = self._get_maximum_outbound_capacity_in_teu(large_scheduled_vehicle) + scaled_moved_container_volume = unscaled_moved_container_volume + + if self.ramp_up_period_end is not None and flow_direction == FlowDirection.transshipment_flow: + arrival_time: datetime.datetime = large_scheduled_vehicle.scheduled_arrival + if arrival_time <= self.ramp_up_period_end: + scaled_moved_container_volume = ( + unscaled_moved_container_volume + * self.downscale_factor_during_ramp_up_period_for_outbound_transshipment + ) + + return scaled_moved_container_volume, unscaled_moved_container_volume + + def _get_maximum_outbound_capacity_in_teu( + self, + large_scheduled_vehicle: LargeScheduledVehicle + ) -> int: + assert self.transportation_buffer is not None, "Please first set the transportation buffer!" + expected_outbound_capacity_including_transportation_buffer = \ + large_scheduled_vehicle.inbound_container_volume * (1 + self.transportation_buffer) + maximum_capacity_of_vehicle = large_scheduled_vehicle.capacity_in_teu + maximum_outbound_capacity_in_teu = min( + expected_outbound_capacity_including_transportation_buffer, + maximum_capacity_of_vehicle + ) + return maximum_outbound_capacity_in_teu diff --git a/conflowgen/descriptive_datatypes/__init__.py b/conflowgen/descriptive_datatypes/__init__.py index c8d25ad..06430e3 100644 --- a/conflowgen/descriptive_datatypes/__init__.py +++ b/conflowgen/descriptive_datatypes/__init__.py @@ -1,6 +1,7 @@ from __future__ import annotations import datetime +import enum import typing from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport @@ -92,13 +93,16 @@ class VehicleIdentifier(typing.NamedTuple): A vehicle identifier is a composition of the vehicle type, its service name, and the actual vehicle name """ + #: The vehicle identifier as it is used in the CSV export. + id: typing.Optional[int] + #: The vehicle type, e.g., 'deep_sea_vessel' or 'truck'. mode_of_transport: ModeOfTransport #: The service name, such as the name of the container service the vessel operates in. Not set for trucks. service_name: typing.Optional[str] - #: The name of the vehicle if given. + #: The name of the vehicle if given. Not set for trucks. vehicle_name: typing.Optional[str] #: The time of arrival of the vehicle at the terminal. @@ -127,3 +131,22 @@ class ContainersTransportedByTruck(typing.NamedTuple): #: The number of containers moved on the outbound journey outbound: float + + +class FlowDirection(enum.Enum): + """ + Represents the flow direction based on the terminology of + *Handbook of Terminal Planning*, edited by Jürgen W. Böse (https://link.springer.com/book/10.1007/978-3-030-39990-0) + """ + + import_flow = "import" + + export_flow = "export" + + transshipment_flow = "transshipment" + + undefined = "undefined" + + def __str__(self) -> str: + # noinspection PyTypeChecker + return f"{self.value}" diff --git a/conflowgen/domain_models/container.py b/conflowgen/domain_models/container.py index 043c9ad..9da6453 100644 --- a/conflowgen/domain_models/container.py +++ b/conflowgen/domain_models/container.py @@ -14,6 +14,7 @@ from .vehicle import LargeScheduledVehicle from .vehicle import Truck from .data_types.storage_requirement import StorageRequirement +from ..descriptive_datatypes import FlowDirection from ..domain_models.data_types.mode_of_transport import ModeOfTransport @@ -116,6 +117,19 @@ class Container(BaseModel): def occupied_teu(self) -> float: return CONTAINER_LENGTH_TO_OCCUPIED_TEU[self.length] + @property + def flow_direction(self) -> FlowDirection: + if (self.delivered_by in [ModeOfTransport.truck, ModeOfTransport.train, ModeOfTransport.barge] + and self.picked_up_by in [ModeOfTransport.feeder, ModeOfTransport.deep_sea_vessel]): + return FlowDirection.export_flow + if (self.picked_up_by in [ModeOfTransport.truck, ModeOfTransport.train, ModeOfTransport.barge] + and self.delivered_by in [ModeOfTransport.feeder, ModeOfTransport.deep_sea_vessel]): + return FlowDirection.import_flow + if (self.picked_up_by in [ModeOfTransport.feeder, ModeOfTransport.deep_sea_vessel] + and self.delivered_by in [ModeOfTransport.feeder, ModeOfTransport.deep_sea_vessel]): + return FlowDirection.transshipment_flow + return FlowDirection.undefined + def get_arrival_time(self) -> datetime.datetime: if self.cached_arrival_time is not None: diff --git a/conflowgen/domain_models/factories/container_factory.py b/conflowgen/domain_models/factories/container_factory.py index bd3aab8..ad241bf 100644 --- a/conflowgen/domain_models/factories/container_factory.py +++ b/conflowgen/domain_models/factories/container_factory.py @@ -1,8 +1,11 @@ from __future__ import annotations +import datetime import math +import typing from typing import Dict, MutableSequence, Sequence, Type +from conflowgen.application.services.vehicle_capacity_manager import VehicleCapacityManager from conflowgen.domain_models.container import Container from conflowgen.domain_models.distribution_repositories.container_length_distribution_repository import \ ContainerLengthDistributionRepository @@ -15,7 +18,6 @@ from conflowgen.domain_models.data_types.container_length import ContainerLength from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport from conflowgen.domain_models.data_types.storage_requirement import StorageRequirement -from conflowgen.domain_models.repositories.large_scheduled_vehicle_repository import LargeScheduledVehicleRepository from conflowgen.domain_models.vehicle import AbstractLargeScheduledVehicle, LargeScheduledVehicle from conflowgen.tools.distribution_approximator import DistributionApproximator from conflowgen.application.repositories.random_seed_store_repository import get_initialised_random_object @@ -34,7 +36,17 @@ def __init__(self): self.container_length_distribution: dict[ContainerLength, float] | None = None self.container_weight_distribution: dict[ContainerLength, dict[int, float]] | None = None self.storage_requirement_distribution: dict[ContainerLength, dict[StorageRequirement, float]] | None = None - self.large_scheduled_vehicle_repository = LargeScheduledVehicleRepository() + self.vehicle_capacity_manager = VehicleCapacityManager() + + def set_ramp_up_and_down_times( + self, + ramp_up_period_end: typing.Optional[datetime.datetime], + ramp_down_period_start: typing.Optional[datetime.datetime], + ) -> None: + self.vehicle_capacity_manager.set_ramp_up_and_down_times( + ramp_up_period_end=ramp_up_period_end, + ramp_down_period_start=ramp_down_period_start + ) def reload_distributions(self): """The user might change the distributions at any time, so reload them at a meaningful point of time!""" @@ -51,7 +63,7 @@ def create_containers_for_large_scheduled_vehicle( Creates all containers a large vehicle delivers to a terminal. """ - self.large_scheduled_vehicle_repository.reset_cache() + self.vehicle_capacity_manager.reset_cache() created_containers: MutableSequence[Container] = [] @@ -60,7 +72,7 @@ def create_containers_for_large_scheduled_vehicle( # noinspection PyTypeChecker large_scheduled_vehicle: LargeScheduledVehicle = large_scheduled_vehicle_as_subtype.large_scheduled_vehicle - free_capacity_in_teu = self.large_scheduled_vehicle_repository.get_free_capacity_for_inbound_journey( + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( large_scheduled_vehicle_as_subtype ) @@ -68,22 +80,25 @@ def create_containers_for_large_scheduled_vehicle( maximum_number_of_containers = int(math.ceil(free_capacity_in_teu)) self._load_distribution_approximators(maximum_number_of_containers, delivered_by) - while (self.large_scheduled_vehicle_repository.get_free_capacity_for_inbound_journey( - large_scheduled_vehicle_as_subtype - ) > self.ignored_capacity): + while free_capacity_in_teu > self.ignored_capacity: container = self._create_single_container_for_large_scheduled_vehicle( delivered_by_large_scheduled_vehicle_as_subtype=large_scheduled_vehicle_as_subtype ) created_containers.append(container) - is_exhausted = self.large_scheduled_vehicle_repository.block_capacity_for_inbound_journey( + is_exhausted = self.vehicle_capacity_manager.block_capacity_for_inbound_journey( vehicle=large_scheduled_vehicle_as_subtype, container=container ) if is_exhausted and not large_scheduled_vehicle.capacity_exhausted_while_determining_onward_transportation: large_scheduled_vehicle.capacity_exhausted_while_determining_onward_transportation = True large_scheduled_vehicle.save() + break + + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( + large_scheduled_vehicle_as_subtype + ) - free_capacity = self.large_scheduled_vehicle_repository.get_free_capacity_for_inbound_journey( + free_capacity = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( large_scheduled_vehicle_as_subtype ) diff --git a/conflowgen/domain_models/factories/fleet_factory.py b/conflowgen/domain_models/factories/fleet_factory.py index f721fa1..de873ad 100644 --- a/conflowgen/domain_models/factories/fleet_factory.py +++ b/conflowgen/domain_models/factories/fleet_factory.py @@ -77,12 +77,12 @@ def create_feeder_fleet( ) for i, arrival in enumerate(arrivals): - moved_capacity = schedule.average_moved_capacity # here we can add randomness later + inbound_container_volume = schedule.average_inbound_container_volume # here we can add randomness later vehicle_name = f"{i + 1}" feeder = self.vehicle_factory.create_feeder( vehicle_name=vehicle_name, capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=moved_capacity, + inbound_container_volume=inbound_container_volume, scheduled_arrival=arrival, schedule=schedule ) @@ -112,13 +112,13 @@ def create_deep_sea_vessel_fleet( ) for i, arrival in enumerate(arrivals): - moved_capacity = schedule.average_moved_capacity # here we can add randomness later + inbound_container_volume = schedule.average_inbound_container_volume # here we can add randomness later vehicle_name = f"{i + 1}" deep_sea_vessel = self.vehicle_factory.create_deep_sea_vessel( vehicle_name=vehicle_name, capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=moved_capacity, + inbound_container_volume=inbound_container_volume, scheduled_arrival=arrival, schedule=schedule ) @@ -146,13 +146,13 @@ def create_train_fleet( ) for i, arrival in enumerate(arrivals): - moved_capacity = schedule.average_moved_capacity # here we can add randomness later + inbound_container_volume = schedule.average_inbound_container_volume # here we can add randomness later vehicle_name = f"{i + 1}" train = self.vehicle_factory.create_train( vehicle_name=vehicle_name, capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=moved_capacity, + inbound_container_volume=inbound_container_volume, scheduled_arrival=arrival, schedule=schedule ) @@ -180,13 +180,13 @@ def create_barge_fleet( ) for i, arrival in enumerate(arrivals): - moved_capacity = schedule.average_moved_capacity # here we can add randomness later + inbound_container_volume = schedule.average_inbound_container_volume # here we can add randomness later vehicle_name = f"{i + 1}" barge = self.vehicle_factory.create_barge( vehicle_name=vehicle_name, capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=moved_capacity, + inbound_container_volume=inbound_container_volume, scheduled_arrival=arrival, schedule=schedule ) diff --git a/conflowgen/domain_models/factories/schedule_factory.py b/conflowgen/domain_models/factories/schedule_factory.py index 1670c3f..8a41f0d 100644 --- a/conflowgen/domain_models/factories/schedule_factory.py +++ b/conflowgen/domain_models/factories/schedule_factory.py @@ -19,7 +19,7 @@ def add_schedule( vehicle_arrives_at: datetime.date, vehicle_arrives_at_time: datetime.time, average_vehicle_capacity: int, - average_moved_capacity: int, + average_inbound_container_volume: int, next_destinations: Optional[List[Tuple[str, float]]], vehicle_arrives_every_k_days: Optional[int] = None ) -> None: @@ -31,7 +31,7 @@ def add_schedule( vehicle_arrives_at: date of day vehicle_arrives_at_time: time of the day average_vehicle_capacity: in TEU - average_moved_capacity: in TEU + average_inbound_container_volume: in TEU next_destinations: distribution vehicle_arrives_every_k_days: Be aware of special meaning of None and -1! """ @@ -44,7 +44,7 @@ def add_schedule( vehicle_arrives_at=vehicle_arrives_at, vehicle_arrives_at_time=vehicle_arrives_at_time, average_vehicle_capacity=average_vehicle_capacity, - average_moved_capacity=average_moved_capacity + average_inbound_container_volume=average_inbound_container_volume ) # if it is None, use the default set in peewee, otherwise overwrite if vehicle_arrives_every_k_days is not None: diff --git a/conflowgen/domain_models/factories/vehicle_factory.py b/conflowgen/domain_models/factories/vehicle_factory.py index d675c21..cba2da0 100644 --- a/conflowgen/domain_models/factories/vehicle_factory.py +++ b/conflowgen/domain_models/factories/vehicle_factory.py @@ -78,7 +78,7 @@ def create_truck( def _create_large_vehicle( self, capacity_in_teu: int, - moved_capacity: int, + inbound_container_volume: int, scheduled_arrival: datetime.datetime, realized_arrival: datetime.datetime, schedule: Schedule, @@ -91,13 +91,13 @@ def _create_large_vehicle( if capacity_in_teu < 0: raise UnrealisticValuesException(f"Vehicle capacity must be positive but it was {capacity_in_teu}") - if moved_capacity < 0: - raise UnrealisticValuesException(f"Vehicle must move positive amount but it was {moved_capacity}") + if inbound_container_volume < 0: + raise UnrealisticValuesException(f"Vehicle must move positive amount but it was {inbound_container_volume}") - if moved_capacity > capacity_in_teu: + if inbound_container_volume > capacity_in_teu: raise UnrealisticValuesException( f"Vehicle can't move more than its capacity but for the vehicle with an overall capacity of " - f"{capacity_in_teu} the moved capacity was set to {moved_capacity}" + f"{capacity_in_teu} the moved capacity was set to {inbound_container_volume}" ) if vehicle_name is None: @@ -106,7 +106,7 @@ def _create_large_vehicle( lsv = LargeScheduledVehicle.create( vehicle_name=vehicle_name, capacity_in_teu=capacity_in_teu, - moved_capacity=moved_capacity, + inbound_container_volume=inbound_container_volume, scheduled_arrival=scheduled_arrival, realized_arrival=realized_arrival, schedule=schedule @@ -116,7 +116,7 @@ def _create_large_vehicle( def create_feeder( self, capacity_in_teu: int, - moved_capacity: int, + inbound_container_volume: int, scheduled_arrival: datetime.datetime, schedule: Schedule, vehicle_name: Optional[str] = None @@ -126,7 +126,7 @@ def create_feeder( lsv = self._create_large_vehicle( vehicle_name=vehicle_name, capacity_in_teu=capacity_in_teu, - moved_capacity=moved_capacity, + inbound_container_volume=inbound_container_volume, scheduled_arrival=scheduled_arrival, realized_arrival=scheduled_arrival, schedule=schedule @@ -139,7 +139,7 @@ def create_feeder( def create_deep_sea_vessel( self, capacity_in_teu: int, - moved_capacity: int, + inbound_container_volume: int, scheduled_arrival: datetime.datetime, schedule: Schedule, vehicle_name: Optional[str] = None @@ -149,7 +149,7 @@ def create_deep_sea_vessel( lsv = self._create_large_vehicle( vehicle_name=vehicle_name, capacity_in_teu=capacity_in_teu, - moved_capacity=moved_capacity, + inbound_container_volume=inbound_container_volume, scheduled_arrival=scheduled_arrival, realized_arrival=scheduled_arrival, schedule=schedule @@ -162,7 +162,7 @@ def create_deep_sea_vessel( def create_train( self, capacity_in_teu: int, - moved_capacity: int, + inbound_container_volume: int, scheduled_arrival: datetime.datetime, schedule: Schedule, vehicle_name: Optional[str] = None @@ -172,7 +172,7 @@ def create_train( lsv = self._create_large_vehicle( vehicle_name=vehicle_name, capacity_in_teu=capacity_in_teu, - moved_capacity=moved_capacity, + inbound_container_volume=inbound_container_volume, scheduled_arrival=scheduled_arrival, realized_arrival=scheduled_arrival, schedule=schedule @@ -185,7 +185,7 @@ def create_train( def create_barge( self, capacity_in_teu: int, - moved_capacity: int, + inbound_container_volume: int, scheduled_arrival: datetime.datetime, schedule: Schedule, vehicle_name: Optional[str] = None @@ -195,7 +195,7 @@ def create_barge( lsv = self._create_large_vehicle( vehicle_name=vehicle_name, capacity_in_teu=capacity_in_teu, - moved_capacity=moved_capacity, + inbound_container_volume=inbound_container_volume, scheduled_arrival=scheduled_arrival, realized_arrival=scheduled_arrival, schedule=schedule diff --git a/conflowgen/domain_models/large_vehicle_schedule.py b/conflowgen/domain_models/large_vehicle_schedule.py index b54ccd6..d51bfb9 100644 --- a/conflowgen/domain_models/large_vehicle_schedule.py +++ b/conflowgen/domain_models/large_vehicle_schedule.py @@ -26,7 +26,7 @@ class Schedule(BaseModel): "This determines the number of ship-to-shore gantry cranes that can serve the vessel " "or the length of the train that must be served by portal cranes in the subsequent model." ) - average_moved_capacity = IntegerField( + average_inbound_container_volume = IntegerField( null=False, help_text="All vehicles moving according to this schedule move approx. this amount of TEU. " "The actual amount of moved TEU might deviate if necessary to realize the provided modal split." diff --git a/conflowgen/domain_models/repositories/large_scheduled_vehicle_repository.py b/conflowgen/domain_models/repositories/large_scheduled_vehicle_repository.py index 0556368..4bc8a27 100644 --- a/conflowgen/domain_models/repositories/large_scheduled_vehicle_repository.py +++ b/conflowgen/domain_models/repositories/large_scheduled_vehicle_repository.py @@ -1,30 +1,11 @@ -import logging -from typing import Dict, List, Callable, Type +from typing import Dict, List, Type -from conflowgen.domain_models.container import Container -from conflowgen.domain_models.data_types.container_length import ContainerLength from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport from conflowgen.domain_models.vehicle import LargeScheduledVehicle, AbstractLargeScheduledVehicle class LargeScheduledVehicleRepository: - ignored_capacity = ContainerLength.get_teu_factor(ContainerLength.other) - - def __init__(self): - self.transportation_buffer = None - self.free_capacity_for_outbound_journey_buffer: Dict[Type[AbstractLargeScheduledVehicle], float] = {} - self.free_capacity_for_inbound_journey_buffer: Dict[Type[AbstractLargeScheduledVehicle], float] = {} - self.logger = logging.getLogger("conflowgen") - - def set_transportation_buffer(self, transportation_buffer: float): - assert -1 < transportation_buffer - self.transportation_buffer = transportation_buffer - - def reset_cache(self): - self.free_capacity_for_outbound_journey_buffer = {} - self.free_capacity_for_inbound_journey_buffer = {} - @staticmethod def load_all_vehicles() -> Dict[ModeOfTransport, List[Type[AbstractLargeScheduledVehicle]]]: result = {} @@ -33,151 +14,3 @@ def load_all_vehicles() -> Dict[ModeOfTransport, List[Type[AbstractLargeSchedule vehicle_type) result[vehicle_type] = list(large_schedule_vehicle_as_subtype.select().join(LargeScheduledVehicle)) return result - - def block_capacity_for_inbound_journey( - self, - vehicle: Type[AbstractLargeScheduledVehicle], - container: Container - ) -> bool: - assert vehicle in self.free_capacity_for_inbound_journey_buffer, \ - "First .get_free_capacity_for_inbound_journey(vehicle) must be invoked" - - # calculate new free capacity - free_capacity_in_teu = self.free_capacity_for_inbound_journey_buffer[vehicle] - used_capacity_in_teu = ContainerLength.get_teu_factor(container_length=container.length) - new_free_capacity_in_teu = free_capacity_in_teu - used_capacity_in_teu - assert new_free_capacity_in_teu >= 0, f"vehicle {vehicle} is overloaded, " \ - f"free_capacity_in_teu: {free_capacity_in_teu}, " \ - f"used_capacity_in_teu: {used_capacity_in_teu}, " \ - f"new_free_capacity_in_teu: {new_free_capacity_in_teu}" - - self.free_capacity_for_inbound_journey_buffer[vehicle] = new_free_capacity_in_teu - vehicle_capacity_is_exhausted = new_free_capacity_in_teu < self.ignored_capacity - return vehicle_capacity_is_exhausted - - def block_capacity_for_outbound_journey( - self, - vehicle: Type[AbstractLargeScheduledVehicle], - container: Container - ) -> bool: - assert vehicle in self.free_capacity_for_outbound_journey_buffer, \ - "First .get_free_capacity_for_outbound_journey(vehicle) must be invoked" - - # calculate new free capacity - free_capacity_in_teu = self.free_capacity_for_outbound_journey_buffer[vehicle] - used_capacity_in_teu = ContainerLength.get_teu_factor(container_length=container.length) - new_free_capacity_in_teu = free_capacity_in_teu - used_capacity_in_teu - assert new_free_capacity_in_teu >= 0, f"vehicle {vehicle} is overloaded, " \ - f"free_capacity_in_teu: {free_capacity_in_teu}, " \ - f"used_capacity_in_teu: {used_capacity_in_teu}, " \ - f"new_free_capacity_in_teu: {new_free_capacity_in_teu}" - - self.free_capacity_for_outbound_journey_buffer[vehicle] = new_free_capacity_in_teu - return new_free_capacity_in_teu <= self.ignored_capacity - - # noinspection PyTypeChecker - def get_free_capacity_for_inbound_journey(self, vehicle: Type[AbstractLargeScheduledVehicle]) -> float: - """Get the free capacity for the inbound journey on a vehicle that moves according to a schedule in TEU. - """ - if vehicle in self.free_capacity_for_inbound_journey_buffer: - return self.free_capacity_for_inbound_journey_buffer[vehicle] - - large_scheduled_vehicle: LargeScheduledVehicle = vehicle.large_scheduled_vehicle - total_moved_capacity_for_inbound_transportation_in_teu = large_scheduled_vehicle.moved_capacity - free_capacity_in_teu = self._get_free_capacity_in_teu( - vehicle=vehicle, - maximum_capacity=total_moved_capacity_for_inbound_transportation_in_teu, - container_counter=self._get_number_containers_for_inbound_journey - ) - self.free_capacity_for_inbound_journey_buffer[vehicle] = free_capacity_in_teu - return free_capacity_in_teu - - def get_free_capacity_for_outbound_journey(self, vehicle: Type[AbstractLargeScheduledVehicle]) -> float: - """Get the free capacity for the outbound journey on a vehicle that moves according to a schedule in TEU. - """ - assert self.transportation_buffer is not None, "First set the value!" - assert -1 < self.transportation_buffer, "Must be larger than -1" - - if vehicle in self.free_capacity_for_outbound_journey_buffer: - return self.free_capacity_for_outbound_journey_buffer[vehicle] - - # noinspection PyTypeChecker - large_scheduled_vehicle: LargeScheduledVehicle = vehicle.large_scheduled_vehicle - - total_moved_capacity_for_onward_transportation_in_teu = \ - large_scheduled_vehicle.moved_capacity * (1 + self.transportation_buffer) - maximum_capacity_of_vehicle = large_scheduled_vehicle.capacity_in_teu - total_moved_capacity_for_onward_transportation_in_teu = min( - total_moved_capacity_for_onward_transportation_in_teu, - maximum_capacity_of_vehicle - ) - - free_capacity_in_teu = self._get_free_capacity_in_teu( - vehicle=vehicle, - maximum_capacity=total_moved_capacity_for_onward_transportation_in_teu, - container_counter=self._get_number_containers_for_outbound_journey - ) - self.free_capacity_for_outbound_journey_buffer[vehicle] = free_capacity_in_teu - return free_capacity_in_teu - - # noinspection PyUnresolvedReferences - @staticmethod - def _get_free_capacity_in_teu( - vehicle: Type[AbstractLargeScheduledVehicle], - maximum_capacity: int, - container_counter: Callable[[Type[AbstractLargeScheduledVehicle], ContainerLength], int] - ) -> float: - loaded_20_foot_containers = container_counter(vehicle, ContainerLength.twenty_feet) - loaded_40_foot_containers = container_counter(vehicle, ContainerLength.forty_feet) - loaded_45_foot_containers = container_counter(vehicle, ContainerLength.forty_five_feet) - loaded_other_containers = container_counter(vehicle, ContainerLength.other) - free_capacity_in_teu = ( - maximum_capacity - - loaded_20_foot_containers * ContainerLength.get_teu_factor(ContainerLength.twenty_feet) - - loaded_40_foot_containers * ContainerLength.get_teu_factor(ContainerLength.forty_feet) - - loaded_45_foot_containers * ContainerLength.get_teu_factor(ContainerLength.forty_five_feet) - - loaded_other_containers * ContainerLength.get_teu_factor(ContainerLength.other) - ) - vehicle_name = vehicle.large_scheduled_vehicle.vehicle_name - assert free_capacity_in_teu >= 0, f"vehicle {vehicle} of type {vehicle.get_mode_of_transport()} with the " \ - f"name '{vehicle_name}' " \ - f"is overloaded, " \ - f"free_capacity_in_teu: {free_capacity_in_teu} with " \ - f"maximum_capacity: {maximum_capacity}, " \ - f"loaded_20_foot_containers: {loaded_20_foot_containers}, " \ - f"loaded_40_foot_containers: {loaded_40_foot_containers}, " \ - f"loaded_45_foot_containers: {loaded_45_foot_containers} and " \ - f"loaded_other_containers: {loaded_other_containers}" - return free_capacity_in_teu - - @classmethod - def _get_number_containers_for_outbound_journey( - cls, - vehicle: Type[AbstractLargeScheduledVehicle], - container_length: ContainerLength - ) -> int: - """Returns the number of containers on a specific vehicle of a specific container length that are picked up by - the vehicle""" - # noinspection PyTypeChecker - large_scheduled_vehicle: LargeScheduledVehicle = vehicle.large_scheduled_vehicle - number_loaded_containers = Container.select().where( - (Container.picked_up_by_large_scheduled_vehicle == large_scheduled_vehicle) - & (Container.length == container_length) - ).count() - return number_loaded_containers - - @classmethod - def _get_number_containers_for_inbound_journey( - cls, - vehicle: AbstractLargeScheduledVehicle, - container_length: ContainerLength - ) -> int: - """Returns the number of containers on a specific vehicle of a specific container length that are delivered by - the vehicle""" - - large_scheduled_vehicle: LargeScheduledVehicle = vehicle.large_scheduled_vehicle - number_loaded_containers = Container.select().where( - (Container.delivered_by_large_scheduled_vehicle == large_scheduled_vehicle) - & (Container.length == container_length) - ).count() - return number_loaded_containers diff --git a/conflowgen/domain_models/repositories/schedule_repository.py b/conflowgen/domain_models/repositories/schedule_repository.py index b45382a..cca7668 100644 --- a/conflowgen/domain_models/repositories/schedule_repository.py +++ b/conflowgen/domain_models/repositories/schedule_repository.py @@ -1,7 +1,10 @@ import datetime +import typing from typing import List, Type import logging +from conflowgen.application.services.vehicle_capacity_manager import VehicleCapacityManager +from conflowgen.descriptive_datatypes import FlowDirection from conflowgen.domain_models.container import Container from conflowgen.domain_models.data_types.container_length import ContainerLength from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport @@ -14,16 +17,28 @@ class ScheduleRepository: def __init__(self): self.logger = logging.getLogger("conflowgen") self.large_scheduled_vehicle_repository = LargeScheduledVehicleRepository() + self.vehicle_capacity_manager = VehicleCapacityManager() def set_transportation_buffer(self, transportation_buffer: float): - self.large_scheduled_vehicle_repository.set_transportation_buffer(transportation_buffer) + self.vehicle_capacity_manager.set_transportation_buffer(transportation_buffer) + + def set_ramp_up_and_down_times( + self, + ramp_up_period_end: typing.Optional[datetime.datetime] = None, + ramp_down_period_start: typing.Optional[datetime.datetime] = None + ): + self.vehicle_capacity_manager.set_ramp_up_and_down_times( + ramp_up_period_end=ramp_up_period_end, + ramp_down_period_start=ramp_down_period_start + ) def get_departing_vehicles( self, start: datetime.datetime, end: datetime.datetime, vehicle_type: ModeOfTransport, - required_capacity: ContainerLength + required_capacity: ContainerLength, + flow_direction: FlowDirection ) -> List[Type[AbstractLargeScheduledVehicle]]: """Gets the available vehicles for the required capacity of the required type and within the time range. """ @@ -45,9 +60,11 @@ def get_departing_vehicles( vehicles_with_sufficient_capacity = [] vehicle: Type[AbstractLargeScheduledVehicle] for vehicle in vehicles: - free_capacity_in_teu = self.large_scheduled_vehicle_repository.get_free_capacity_for_outbound_journey( - vehicle - ) + free_capacity_in_teu = self.vehicle_capacity_manager.\ + get_free_capacity_for_outbound_journey( + vehicle, flow_direction + ) + if free_capacity_in_teu >= required_capacity_in_teu: vehicles_with_sufficient_capacity.append(vehicle) assert free_capacity_in_teu >= 0, f"Vehicle {vehicle} is overloaded, checking for " \ @@ -60,11 +77,11 @@ def get_departing_vehicles( def block_capacity_for_outbound_journey( self, vehicle: Type[AbstractLargeScheduledVehicle], - container: Container + container: Container, ) -> bool: """Updates the cache for faster execution """ - return self.large_scheduled_vehicle_repository.block_capacity_for_outbound_journey( + return self.vehicle_capacity_manager.block_capacity_for_outbound_journey( vehicle=vehicle, container=container ) diff --git a/conflowgen/domain_models/vehicle.py b/conflowgen/domain_models/vehicle.py index b0b830a..c1a42f5 100644 --- a/conflowgen/domain_models/vehicle.py +++ b/conflowgen/domain_models/vehicle.py @@ -63,7 +63,7 @@ class LargeScheduledVehicle(BaseModel): help_text="This is the vehicle capacity. It can be used, e.g., to determine how many cranes can serve it in " "the subsequent model that reads in this data." ) - moved_capacity = IntegerField( + inbound_container_volume = IntegerField( null=False, help_text="This is the actually moved container volume in TEU for a single terminal visit on the inbound " "journey." diff --git a/conflowgen/flow_generator/allocate_space_for_containers_delivered_by_truck_service.py b/conflowgen/flow_generator/allocate_space_for_containers_delivered_by_truck_service.py index c506dba..231804a 100644 --- a/conflowgen/flow_generator/allocate_space_for_containers_delivered_by_truck_service.py +++ b/conflowgen/flow_generator/allocate_space_for_containers_delivered_by_truck_service.py @@ -3,6 +3,8 @@ from typing import Dict, Type, List from conflowgen.application.repositories.random_seed_store_repository import get_initialised_random_object +from conflowgen.application.services.vehicle_capacity_manager import VehicleCapacityManager +from conflowgen.descriptive_datatypes import FlowDirection from conflowgen.domain_models.container import Container from conflowgen.domain_models.distribution_repositories.mode_of_transport_distribution_repository import \ ModeOfTransportDistributionRepository @@ -24,11 +26,12 @@ def __init__(self): self.mode_of_transport_distribution_repository = ModeOfTransportDistributionRepository() self.mode_of_transport_distribution: Dict[ModeOfTransport, Dict[ModeOfTransport, float]] | None = None self.large_scheduled_vehicle_repository = LargeScheduledVehicleRepository() + self.vehicle_capacity_manager = VehicleCapacityManager() self.container_factory = ContainerFactory() def reload_distribution(self, transportation_buffer: float): self.mode_of_transport_distribution = self.mode_of_transport_distribution_repository.get_distribution() - self.large_scheduled_vehicle_repository.set_transportation_buffer( + self.vehicle_capacity_manager.set_transportation_buffer( transportation_buffer=transportation_buffer ) self.logger.info(f"Use transport buffer of {transportation_buffer} for allocating containers delivered by " @@ -62,7 +65,7 @@ def allocate(self) -> None: if truck_to_other_vehicle_distribution[ModeOfTransport.truck] > 0: raise NotImplementedError("Truck to truck traffic is not supported.") - self.large_scheduled_vehicle_repository.reset_cache() + self.vehicle_capacity_manager.reset_cache() number_containers_to_allocate = self._get_number_containers_to_allocate() @@ -114,8 +117,8 @@ def allocate(self) -> None: del truck_to_other_vehicle_distribution[selected_mode_of_transport] # drop this type continue # try again with another vehicle type (refers to while loop) - free_capacity_of_vehicle = self.large_scheduled_vehicle_repository.\ - get_free_capacity_for_outbound_journey(vehicle) + free_capacity_of_vehicle = self.vehicle_capacity_manager.\ + get_free_capacity_for_outbound_journey(vehicle, flow_direction=FlowDirection.export_flow) if free_capacity_of_vehicle <= self.ignored_capacity: @@ -138,7 +141,7 @@ def allocate(self) -> None: container = self.container_factory.create_container_for_delivering_truck(vehicle) teu_total += ContainerLength.get_teu_factor(container.length) - self.large_scheduled_vehicle_repository.block_capacity_for_outbound_journey(vehicle, container) + self.vehicle_capacity_manager.block_capacity_for_outbound_journey(vehicle, container) successful_assignment += 1 break # success, no further looping to search for a suitable vehicle @@ -175,7 +178,8 @@ def _pick_vehicle( # Make it more likely that a container ends up on a large vessel than on a smaller one vehicle: Type[AbstractLargeScheduledVehicle] vehicle_distribution: Dict[Type[AbstractLargeScheduledVehicle], float] = { - vehicle: self.large_scheduled_vehicle_repository.get_free_capacity_for_outbound_journey(vehicle) + vehicle: self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + vehicle, FlowDirection.export_flow) for vehicle in vehicles_of_type } all_free_capacities = list(vehicle_distribution.values()) diff --git a/conflowgen/flow_generator/container_flow_generation_service.py b/conflowgen/flow_generator/container_flow_generation_service.py index 9a77228..7f695bf 100644 --- a/conflowgen/flow_generator/container_flow_generation_service.py +++ b/conflowgen/flow_generator/container_flow_generation_service.py @@ -43,21 +43,38 @@ def _update_generation_properties_and_distributions(self): self.container_flow_end_date: datetime.date = container_flow_generation_properties.end_date assert self.container_flow_start_date < self.container_flow_end_date + ramp_up_period = container_flow_generation_properties.ramp_up_period + ramp_down_period = container_flow_generation_properties.ramp_down_period + self.ramp_up_period_end = datetime.datetime.combine( + self.container_flow_start_date, datetime.time(hour=0, minute=0, second=0) + ) + datetime.timedelta(days=ramp_up_period) + self.ramp_down_period_start = datetime.datetime.combine( + self.container_flow_end_date, datetime.time(hour=0, minute=0, second=0) + ) - datetime.timedelta(days=ramp_down_period) + assert self.ramp_up_period_end <= self.ramp_down_period_start + self.transportation_buffer: float = container_flow_generation_properties.transportation_buffer assert -1 < self.transportation_buffer self.large_scheduled_vehicle_for_onward_transportation_manager.reload_properties( - transportation_buffer=self.transportation_buffer + transportation_buffer=self.transportation_buffer, + ramp_up_period_end=self.ramp_up_period_end, + ramp_down_period_start=self.ramp_down_period_start, ) self.allocate_space_for_containers_delivered_by_truck_service.reload_distribution( transportation_buffer=self.transportation_buffer ) + self.truck_for_import_containers_manager.reload_distributions() self.truck_for_export_containers_manager.reload_distributions() + self.large_scheduled_vehicle_creation_service.reload_properties( container_flow_start_date=self.container_flow_start_date, - container_flow_end_date=self.container_flow_end_date + container_flow_end_date=self.container_flow_end_date, + ramp_up_period_end=self.ramp_up_period_end, + ramp_down_period_start=self.ramp_down_period_start, ) + self.assign_destination_to_container_service.reload_distributions() @staticmethod diff --git a/conflowgen/flow_generator/large_scheduled_vehicle_creation_service.py b/conflowgen/flow_generator/large_scheduled_vehicle_creation_service.py index 170d6a0..6254578 100644 --- a/conflowgen/flow_generator/large_scheduled_vehicle_creation_service.py +++ b/conflowgen/flow_generator/large_scheduled_vehicle_creation_service.py @@ -1,4 +1,7 @@ +from __future__ import annotations + import datetime +import typing from typing import List, Type import logging @@ -29,11 +32,17 @@ def __init__(self): def reload_properties( self, container_flow_start_date: datetime.date, - container_flow_end_date: datetime.date + container_flow_end_date: datetime.date, + ramp_up_period_end: typing.Optional[datetime.datetime | datetime.date], + ramp_down_period_start: typing.Optional[datetime.datetime | datetime.date], ): assert container_flow_start_date < container_flow_end_date self.container_flow_start_date = container_flow_start_date self.container_flow_end_date = container_flow_end_date + self.container_factory.set_ramp_up_and_down_times( + ramp_up_period_end=ramp_up_period_end, + ramp_down_period_start=ramp_down_period_start + ) self.container_factory.reload_distributions() def create(self) -> None: diff --git a/conflowgen/flow_generator/large_scheduled_vehicle_for_onward_transportation_manager.py b/conflowgen/flow_generator/large_scheduled_vehicle_for_onward_transportation_manager.py index f081989..2b281f0 100644 --- a/conflowgen/flow_generator/large_scheduled_vehicle_for_onward_transportation_manager.py +++ b/conflowgen/flow_generator/large_scheduled_vehicle_for_onward_transportation_manager.py @@ -2,7 +2,7 @@ import datetime import logging import math -from typing import Tuple, List, Dict, Type, Sequence +from typing import Tuple, List, Dict, Type, Sequence, Optional import numpy as np # noinspection PyProtectedMember @@ -31,7 +31,7 @@ def __init__(self): self.logger = logging.getLogger("conflowgen") self.schedule_repository = ScheduleRepository() - self.large_scheduled_vehicle_repository = self.schedule_repository.large_scheduled_vehicle_repository + self.vehicle_capacity_manager = self.schedule_repository.vehicle_capacity_manager self.mode_of_transport_distribution_repository = ModeOfTransportDistributionRepository() self.mode_of_transport_distribution = self.mode_of_transport_distribution_repository.get_distribution() @@ -42,15 +42,23 @@ def __init__(self): def reload_properties( self, - transportation_buffer: float + transportation_buffer: float, + ramp_up_period_end: Optional[datetime.datetime | datetime.date] = None, + ramp_down_period_start: Optional[datetime.datetime | datetime.date] = None, ): assert -1 < transportation_buffer self.schedule_repository.set_transportation_buffer(transportation_buffer) + + self.schedule_repository.set_ramp_up_and_down_times( + ramp_up_period_end=ramp_up_period_end, + ramp_down_period_start=ramp_down_period_start + ) + self.logger.debug(f"Using transportation buffer of {transportation_buffer} when choosing the departing " f"vehicles that adhere a schedule.") self.container_dwell_time_distributions = self.container_dwell_time_distribution_repository.get_distributions() - self.large_scheduled_vehicle_repository = self.schedule_repository.large_scheduled_vehicle_repository + self.vehicle_capacity_manager = self.schedule_repository.vehicle_capacity_manager self.mode_of_transport_distribution = self.mode_of_transport_distribution_repository.get_distribution() def choose_departing_vehicle_for_containers(self) -> None: @@ -66,7 +74,7 @@ def choose_departing_vehicle_for_containers(self) -> None: number_assigned_containers = 0 number_not_assignable_containers = 0 - self.large_scheduled_vehicle_repository.reset_cache() + self.vehicle_capacity_manager.reset_cache() self.logger.info("Assign containers to departing vehicles that move according to a schedule...") @@ -125,7 +133,8 @@ def choose_departing_vehicle_for_containers(self) -> None: start=(container_arrival + datetime.timedelta(hours=minimum_dwell_time_in_hours)), end=(container_arrival + datetime.timedelta(hours=maximum_dwell_time_in_hours)), vehicle_type=initial_departing_vehicle_type, - required_capacity=container.length + required_capacity=container.length, + flow_direction=container.flow_direction ) if len(available_vehicles) > 0: @@ -193,8 +202,12 @@ def _draw_vehicle( return available_vehicles[0] vehicles_and_their_respective_free_capacity = {} + for vehicle in available_vehicles: - free_capacity = self.large_scheduled_vehicle_repository.get_free_capacity_for_outbound_journey(vehicle) + + free_capacity = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + vehicle, container.flow_direction + ) if free_capacity >= ContainerLength.get_teu_factor(ContainerLength.other): vehicles_and_their_respective_free_capacity[vehicle] = free_capacity @@ -313,7 +326,8 @@ def _find_alternative_mode_of_transportation( start=(container_arrival + datetime.timedelta(hours=minimum_dwell_time_in_hours)), end=(container_arrival + datetime.timedelta(hours=maximum_dwell_time_in_hours)), vehicle_type=vehicle_type, - required_capacity=container.length + required_capacity=container.length, + flow_direction=container.flow_direction ) if len(available_vehicles) > 0: # There is a vehicle of a new type available, so it is picked vehicle = self._pick_vehicle_for_container(available_vehicles, container) diff --git a/conflowgen/logger/__init__.py b/conflowgen/logger/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/conflowgen/logging/logging.py b/conflowgen/logger/logger.py similarity index 100% rename from conflowgen/logging/logging.py rename to conflowgen/logger/logger.py diff --git a/conflowgen/tests/analyses/test_container_dwell_time_analysis.py b/conflowgen/tests/analyses/test_container_dwell_time_analysis.py index 55bd85c..eeee565 100644 --- a/conflowgen/tests/analyses/test_container_dwell_time_analysis.py +++ b/conflowgen/tests/analyses/test_container_dwell_time_analysis.py @@ -53,12 +53,12 @@ def test_with_single_container(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -98,12 +98,12 @@ def test_with_two_containers(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -166,12 +166,12 @@ def test_with_two_containers_and_end_time(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -235,12 +235,12 @@ def test_with_two_containers_and_start_and_end_time(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_container_dwell_time_analysis_report.py b/conflowgen/tests/analyses/test_container_dwell_time_analysis_report.py index b0f27b3..29bcc6e 100644 --- a/conflowgen/tests/analyses/test_container_dwell_time_analysis_report.py +++ b/conflowgen/tests/analyses/test_container_dwell_time_analysis_report.py @@ -27,12 +27,12 @@ def setup_feeder_data(): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_container_flow_by_vehicle_type_analysis.py b/conflowgen/tests/analyses/test_container_flow_by_vehicle_type_analysis.py index 6571427..f19dd4b 100644 --- a/conflowgen/tests/analyses/test_container_flow_by_vehicle_type_analysis.py +++ b/conflowgen/tests/analyses/test_container_flow_by_vehicle_type_analysis.py @@ -61,13 +61,13 @@ def test_with_single_feeder(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -105,13 +105,13 @@ def test_with_single_feeder_with_time_window(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=one_week_later, schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_container_flow_by_vehicle_type_analysis_report.py b/conflowgen/tests/analyses/test_container_flow_by_vehicle_type_analysis_report.py index dd3a4e7..0acb779 100644 --- a/conflowgen/tests/analyses/test_container_flow_by_vehicle_type_analysis_report.py +++ b/conflowgen/tests/analyses/test_container_flow_by_vehicle_type_analysis_report.py @@ -23,13 +23,13 @@ def setup_feeder_data(): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_container_flow_vehicle_type_adjustment_per_vehicle_analysis.py b/conflowgen/tests/analyses/test_container_flow_vehicle_type_adjustment_per_vehicle_analysis.py index 36bd2d3..6f30462 100644 --- a/conflowgen/tests/analyses/test_container_flow_vehicle_type_adjustment_per_vehicle_analysis.py +++ b/conflowgen/tests/analyses/test_container_flow_vehicle_type_adjustment_per_vehicle_analysis.py @@ -48,13 +48,13 @@ def test_with_feeder_and_truck_and_no_adjustment(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) inbound_feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -64,7 +64,7 @@ def test_with_feeder_and_truck_and_no_adjustment(self): inbound_feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -72,7 +72,7 @@ def test_with_feeder_and_truck_and_no_adjustment(self): outbound_feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder2", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=vessel_arrival, schedule=schedule ) @@ -91,6 +91,7 @@ def test_with_feeder_and_truck_and_no_adjustment(self): self.assertListEqual(list(no_adjustment.values()), [0]) vehicle_identifier = list(no_adjustment.keys())[0] self.assertEqual(vehicle_identifier, VehicleIdentifier( + id=outbound_feeder_lsv.id, mode_of_transport=ModeOfTransport.feeder, vehicle_arrival_time=vessel_arrival, service_name="TestFeederService", @@ -105,13 +106,13 @@ def test_with_feeder_and_truck_and_one_adjusted_box(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -149,13 +150,13 @@ def test_with_feeder_and_truck_and_some_adjustments(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -203,14 +204,14 @@ def test_with_truck_and_feeder_and_no_adjustment(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_arrival = datetime.datetime.now() feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=feeder_arrival, schedule=schedule ) @@ -243,6 +244,7 @@ def test_with_truck_and_feeder_and_no_adjustment(self): vehicle_identifier = list(no_adjustment.keys())[0] self.assertEqual(vehicle_identifier, VehicleIdentifier( + id=feeder_lsv.id, mode_of_transport=ModeOfTransport.feeder, vehicle_arrival_time=feeder_arrival, service_name="TestFeederService", diff --git a/conflowgen/tests/analyses/test_container_flow_vehicle_type_adjustment_per_vehicle_analysis_report.py b/conflowgen/tests/analyses/test_container_flow_vehicle_type_adjustment_per_vehicle_analysis_report.py index 05e41a3..3751a51 100644 --- a/conflowgen/tests/analyses/test_container_flow_vehicle_type_adjustment_per_vehicle_analysis_report.py +++ b/conflowgen/tests/analyses/test_container_flow_vehicle_type_adjustment_per_vehicle_analysis_report.py @@ -25,13 +25,13 @@ def setup_feeder_data(): vehicle_arrives_at=when.date(), vehicle_arrives_at_time=when.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) inbound_feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -42,7 +42,7 @@ def setup_feeder_data(): outbound_feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder2", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=vessel_arrival, schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_inbound_and_outbound_vehicle_capacity_analysis.py b/conflowgen/tests/analyses/test_inbound_and_outbound_vehicle_capacity_analysis.py index bffa99a..80ab527 100644 --- a/conflowgen/tests/analyses/test_inbound_and_outbound_vehicle_capacity_analysis.py +++ b/conflowgen/tests/analyses/test_inbound_and_outbound_vehicle_capacity_analysis.py @@ -66,13 +66,13 @@ def test_inbound_with_single_feeder(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -110,13 +110,13 @@ def test_outbound_with_single_feeder(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_inbound_and_outbound_vehicle_capacity_analysis_report.py b/conflowgen/tests/analyses/test_inbound_and_outbound_vehicle_capacity_analysis_report.py index 5522c95..923cd77 100644 --- a/conflowgen/tests/analyses/test_inbound_and_outbound_vehicle_capacity_analysis_report.py +++ b/conflowgen/tests/analyses/test_inbound_and_outbound_vehicle_capacity_analysis_report.py @@ -23,14 +23,14 @@ def setup_feeder_data(): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) schedule.save() feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_inbound_to_outbound_capacity_utilization_analysis.py b/conflowgen/tests/analyses/test_inbound_to_outbound_capacity_utilization_analysis.py index 4d17a8f..afd2b1d 100644 --- a/conflowgen/tests/analyses/test_inbound_to_outbound_capacity_utilization_analysis.py +++ b/conflowgen/tests/analyses/test_inbound_to_outbound_capacity_utilization_analysis.py @@ -2,8 +2,8 @@ import unittest from conflowgen.descriptive_datatypes import VehicleIdentifier -from conflowgen.analyses.inbound_to_outbound_vehicle_capacity_utilization_analysis import \ - InboundToOutboundVehicleCapacityUtilizationAnalysis +from conflowgen.analyses.outbound_to_inbound_vehicle_capacity_utilization_analysis import \ + OutboundToInboundVehicleCapacityUtilizationAnalysis from conflowgen.domain_models.container import Container from conflowgen.domain_models.data_types.container_length import ContainerLength from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport @@ -29,7 +29,7 @@ def setUp(self) -> None: Destination ]) mode_of_transport_distribution_seeder.seed() - self.analysis = InboundToOutboundVehicleCapacityUtilizationAnalysis( + self.analysis = OutboundToInboundVehicleCapacityUtilizationAnalysis( transportation_buffer=0.2 ) @@ -53,14 +53,14 @@ def test_inbound_with_single_feeder(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=250, + average_inbound_container_volume=250, vehicle_arrives_every_k_days=-1 ) now = datetime.datetime.now() feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -82,7 +82,7 @@ def test_inbound_with_single_feeder(self): self.assertEqual(len(capacities_with_one_feeder), 1, "There is only one vehicle") key_of_entry: VehicleIdentifier = list(capacities_with_one_feeder.keys())[0] - self.assertEqual(len(key_of_entry), 4, "Key consists of four components") + self.assertEqual(len(key_of_entry), 5, "Key consists of five components") self.assertEqual(key_of_entry.mode_of_transport, ModeOfTransport.feeder) self.assertEqual(key_of_entry.service_name, "TestFeederService") self.assertEqual(key_of_entry.vehicle_name, "TestFeeder1") diff --git a/conflowgen/tests/analyses/test_inbound_to_outbound_capacity_utilization_analysis_report.py b/conflowgen/tests/analyses/test_inbound_to_outbound_capacity_utilization_analysis_report.py index d551351..887474d 100644 --- a/conflowgen/tests/analyses/test_inbound_to_outbound_capacity_utilization_analysis_report.py +++ b/conflowgen/tests/analyses/test_inbound_to_outbound_capacity_utilization_analysis_report.py @@ -1,7 +1,7 @@ import datetime -from conflowgen.analyses.inbound_to_outbound_vehicle_capacity_utilization_analysis_report import \ - InboundToOutboundVehicleCapacityUtilizationAnalysisReport +from conflowgen.analyses.outbound_to_inbound_vehicle_capacity_utilization_analysis_report import \ + OutboundToInboundVehicleCapacityUtilizationAnalysisReport from conflowgen.application.models.container_flow_generation_properties import ContainerFlowGenerationProperties from conflowgen.domain_models.container import Container from conflowgen.domain_models.data_types.container_length import ContainerLength @@ -23,13 +23,13 @@ def setup_feeder_data(): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=250, + average_inbound_container_volume=250, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -66,7 +66,7 @@ def setUp(self) -> None: start_date=datetime.date(2021, 12, 15), end_date=datetime.date(2021, 12, 17) ) - self.report = InboundToOutboundVehicleCapacityUtilizationAnalysisReport() + self.report = OutboundToInboundVehicleCapacityUtilizationAnalysisReport() def test_with_no_data(self): actual_report = self.report.get_report_as_text() diff --git a/conflowgen/tests/analyses/test_modal_split_analysis.py b/conflowgen/tests/analyses/test_modal_split_analysis.py index 6a5bd5a..dc18598 100644 --- a/conflowgen/tests/analyses/test_modal_split_analysis.py +++ b/conflowgen/tests/analyses/test_modal_split_analysis.py @@ -49,13 +49,13 @@ def test_transshipment_share_with_single_feeder(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -84,13 +84,13 @@ def test_outbound_with_single_feeder(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -120,13 +120,13 @@ def test_outbound_with_single_feeder_and_not_affecting_start_and_end(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_modal_split_analysis_report.py b/conflowgen/tests/analyses/test_modal_split_analysis_report.py index 8d090bc..4f34c40 100644 --- a/conflowgen/tests/analyses/test_modal_split_analysis_report.py +++ b/conflowgen/tests/analyses/test_modal_split_analysis_report.py @@ -22,14 +22,14 @@ def setup_feeder_data(): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) schedule.save() feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_quay_side_throughput_analysis.py b/conflowgen/tests/analyses/test_quay_side_throughput_analysis.py index 448b2a0..7f452fe 100644 --- a/conflowgen/tests/analyses/test_quay_side_throughput_analysis.py +++ b/conflowgen/tests/analyses/test_quay_side_throughput_analysis.py @@ -48,12 +48,12 @@ def test_with_single_container(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -92,12 +92,12 @@ def test_with_two_containers(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_quay_side_throughput_analysis_report.py b/conflowgen/tests/analyses/test_quay_side_throughput_analysis_report.py index 22c54ea..a88a612 100644 --- a/conflowgen/tests/analyses/test_quay_side_throughput_analysis_report.py +++ b/conflowgen/tests/analyses/test_quay_side_throughput_analysis_report.py @@ -25,12 +25,12 @@ def setup_feeder_data(): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -142,12 +142,12 @@ def test_with_train(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=90, - average_moved_capacity=90, + average_inbound_container_volume=90, ) train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=90, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_run_all_analyses.py b/conflowgen/tests/analyses/test_run_all_analyses.py index fee4bdf..a36f9d0 100644 --- a/conflowgen/tests/analyses/test_run_all_analyses.py +++ b/conflowgen/tests/analyses/test_run_all_analyses.py @@ -1,5 +1,4 @@ import datetime -import unittest import unittest.mock from conflowgen.analyses import run_all_analyses @@ -23,10 +22,11 @@ def setUp(self) -> None: def test_with_no_data(self): with self.assertLogs('conflowgen', level='INFO') as context: run_all_analyses() - self.assertEqual(len(context.output), 35) + self.assertEqual(len(context.output), 38) def test_with_no_data_as_graph(self): - with unittest.mock.patch('matplotlib.pyplot.show'): + with unittest.mock.patch("matplotlib.pyplot.show"): with self.assertLogs('conflowgen', level='INFO') as context: + print("Before run_all_analyses") run_all_analyses(as_text=False, as_graph=True, static_graphs=True) - self.assertEqual(len(context.output), 27) + self.assertEqual(len(context.output), 29) diff --git a/conflowgen/tests/analyses/test_truck_gate_throughput_analysis.py b/conflowgen/tests/analyses/test_truck_gate_throughput_analysis.py index 57a380b..679935e 100644 --- a/conflowgen/tests/analyses/test_truck_gate_throughput_analysis.py +++ b/conflowgen/tests/analyses/test_truck_gate_throughput_analysis.py @@ -46,12 +46,12 @@ def test_with_single_container(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -90,12 +90,12 @@ def test_with_two_containers(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -153,12 +153,12 @@ def test_with_two_containers_and_end_time(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_truck_gate_throughput_analysis_report.py b/conflowgen/tests/analyses/test_truck_gate_throughput_analysis_report.py index daaf4f0..e75c19e 100644 --- a/conflowgen/tests/analyses/test_truck_gate_throughput_analysis_report.py +++ b/conflowgen/tests/analyses/test_truck_gate_throughput_analysis_report.py @@ -26,12 +26,12 @@ def setup_feeder_data(): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -136,12 +136,12 @@ def test_with_train(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=90, - average_moved_capacity=90, + average_inbound_container_volume=90, ) train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=90, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_yard_capacity_analysis.py b/conflowgen/tests/analyses/test_yard_capacity_analysis.py index 08f55dc..3c7ff8f 100644 --- a/conflowgen/tests/analyses/test_yard_capacity_analysis.py +++ b/conflowgen/tests/analyses/test_yard_capacity_analysis.py @@ -48,12 +48,12 @@ def test_with_single_container(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -96,12 +96,12 @@ def test_with_two_containers(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -163,12 +163,12 @@ def test_with_container_group(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv_1 = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -178,7 +178,7 @@ def test_with_container_group(self): feeder_lsv_2 = LargeScheduledVehicle.create( vehicle_name="TestFeeder2", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now + datetime.timedelta(hours=72), schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_yard_capacity_analysis_report.py b/conflowgen/tests/analyses/test_yard_capacity_analysis_report.py index 9b4c678..76bb2f4 100644 --- a/conflowgen/tests/analyses/test_yard_capacity_analysis_report.py +++ b/conflowgen/tests/analyses/test_yard_capacity_analysis_report.py @@ -24,12 +24,12 @@ def setup_feeder_data(): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) diff --git a/conflowgen/tests/api/test_container_flow_generation_manager.py b/conflowgen/tests/api/test_container_flow_generation_manager.py index 62c03e3..38260ef 100644 --- a/conflowgen/tests/api/test_container_flow_generation_manager.py +++ b/conflowgen/tests/api/test_container_flow_generation_manager.py @@ -66,18 +66,24 @@ class MockedProperties: start_date = datetime.date(2030, 1, 1) end_date = datetime.date(2030, 12, 31) transportation_buffer = 0.2 + ramp_up_period = 10.0 + ramp_down_period = 5.0 minimum_dwell_time_of_import_containers_in_hours = 3 minimum_dwell_time_of_export_containers_in_hours = 4 minimum_dwell_time_of_transshipment_containers_in_hours = 5 maximum_dwell_time_of_import_containers_in_hours = 40 maximum_dwell_time_of_export_containers_in_hours = 50 maximum_dwell_time_of_transshipment_containers_in_hours = 60 + conflowgen_version = '2.1.1' dict_properties = { 'name': "my test data", 'start_date': datetime.date(2030, 1, 1), 'end_date': datetime.date(2030, 12, 31), - 'transportation_buffer': 0.2 + 'transportation_buffer': 0.2, + 'ramp_up_period': 10.0, + 'ramp_down_period': 5.0, + 'conflowgen_version': '2.1.1', } with unittest.mock.patch.object( diff --git a/conflowgen/tests/api/test_port_call_manager.py b/conflowgen/tests/api/test_port_call_manager.py index 04381d3..a626576 100644 --- a/conflowgen/tests/api/test_port_call_manager.py +++ b/conflowgen/tests/api/test_port_call_manager.py @@ -11,7 +11,7 @@ class TestPortCallManager(unittest.TestCase): def setUp(self) -> None: self.port_call_manager = PortCallManager() - def test_add_vehicle(self): + def test_add_service_that_calls_terminal(self): feeder_service_name = "LX050" arrives_at = datetime.date(2021, 7, 9) time_of_the_day = datetime.time(hour=11) @@ -29,13 +29,13 @@ def test_add_vehicle(self): self.port_call_manager, 'has_schedule', return_value=False) as mock_has_schedule: - self.port_call_manager.add_vehicle( + self.port_call_manager.add_service_that_calls_terminal( vehicle_type=ModeOfTransport.feeder, service_name=feeder_service_name, vehicle_arrives_at=arrives_at, vehicle_arrives_at_time=time_of_the_day, average_vehicle_capacity=total_capacity, - average_moved_capacity=moved_capacity, + average_inbound_container_volume=moved_capacity, next_destinations=next_destinations ) mock_has_schedule.assert_called_once_with( @@ -48,9 +48,49 @@ def test_add_vehicle(self): vehicle_arrives_at=arrives_at, vehicle_arrives_at_time=time_of_the_day, average_vehicle_capacity=total_capacity, - average_moved_capacity=moved_capacity, + average_inbound_container_volume=moved_capacity, + next_destinations=next_destinations, + vehicle_arrives_every_k_days=7 + ) + + def test_add_vehicle(self): + arrives_at = datetime.date(2021, 7, 9) + time_of_the_day = datetime.time(hour=11) + total_capacity = 100 + moved_capacity = 3 + next_destinations = [ + ("DEBRV", 0.6), # 60% of the containers (in boxes) go here... + ("RULED", 0.4) # and the other 40% of the containers (in boxes) go here. + ] + with unittest.mock.patch.object( + self.port_call_manager.schedule_factory, + 'add_schedule', + return_value=None) as mock_add_schedule: + with unittest.mock.patch.object( + self.port_call_manager, + 'has_schedule', + return_value=False) as mock_has_schedule: + self.port_call_manager.add_vehicle( + vehicle_type=ModeOfTransport.feeder, + vehicle_arrives_at=arrives_at, + vehicle_arrives_at_time=time_of_the_day, + vehicle_capacity=total_capacity, + inbound_container_volume=moved_capacity, + next_destinations=next_destinations + ) + mock_has_schedule.assert_called_once_with( + vehicle_type=ModeOfTransport.feeder, + service_name=unittest.mock.ANY, + ) + mock_add_schedule.assert_called_once_with( + vehicle_type=ModeOfTransport.feeder, + service_name=unittest.mock.ANY, + vehicle_arrives_at=arrives_at, + vehicle_arrives_at_time=time_of_the_day, + average_vehicle_capacity=total_capacity, + average_inbound_container_volume=moved_capacity, next_destinations=next_destinations, - vehicle_arrives_every_k_days=None + vehicle_arrives_every_k_days=-1 ) def test_get(self): diff --git a/conflowgen/tests/application/reports/test_container_flow_statistics_report.py b/conflowgen/tests/application/reports/test_container_flow_statistics_report.py index 7e53b88..46f9321 100644 --- a/conflowgen/tests/application/reports/test_container_flow_statistics_report.py +++ b/conflowgen/tests/application/reports/test_container_flow_statistics_report.py @@ -46,12 +46,12 @@ def _create_feeder(scheduled_arrival: datetime.datetime, service_name_suffix: st vehicle_arrives_at=scheduled_arrival.date(), vehicle_arrives_at_time=scheduled_arrival.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=scheduled_arrival, schedule=schedule ) @@ -135,7 +135,7 @@ def test_empty_ship_using_capacity_as_maximum(self): def test_empty_ship_using_buffer_as_maximum(self): now = datetime.datetime.now() feeder = self._create_feeder(scheduled_arrival=now) - feeder.large_scheduled_vehicle.moved_capacity = 20 + feeder.large_scheduled_vehicle.inbound_container_volume = 20 feeder.large_scheduled_vehicle.save() truck = self._create_truck(arrival=now) self._create_container_delivered_by_truck(truck) @@ -160,7 +160,7 @@ def test_empty_ship_using_buffer_as_maximum(self): def test_inbound_loaded_ship_using_capacity_as_maximum(self): now = datetime.datetime.now() feeder = self._create_feeder(scheduled_arrival=now) - feeder.large_scheduled_vehicle.moved_capacity = 20 + feeder.large_scheduled_vehicle.inbound_container_volume = 20 feeder.large_scheduled_vehicle.save() self._create_container_delivered_by_large_scheduled_vehicle(feeder) self.report.generate() @@ -184,7 +184,7 @@ def test_inbound_loaded_ship_using_capacity_as_maximum(self): def test_outbound_loaded_ship_using_buffer_as_maximum(self): now = datetime.datetime.now() feeder = self._create_feeder(scheduled_arrival=now) - feeder.large_scheduled_vehicle.moved_capacity = 20 + feeder.large_scheduled_vehicle.inbound_container_volume = 20 feeder.large_scheduled_vehicle.save() truck = self._create_truck(arrival=now) container = self._create_container_delivered_by_truck(truck) @@ -214,7 +214,7 @@ def test_outbound_loaded_ship_using_capacity_as_maximum(self): now = datetime.datetime.now() feeder = self._create_feeder(scheduled_arrival=now) feeder.large_scheduled_vehicle.capacity_in_teu = 20 - feeder.large_scheduled_vehicle.moved_capacity = 20 + feeder.large_scheduled_vehicle.inbound_container_volume = 20 feeder.large_scheduled_vehicle.save() truck = self._create_truck(arrival=now) container = self._create_container_delivered_by_truck(truck) @@ -244,11 +244,11 @@ def test_two_ships_one_with_inbound_traffic(self): now = datetime.datetime.now() feeder_1 = self._create_feeder(scheduled_arrival=now, service_name_suffix="1") feeder_1.large_scheduled_vehicle.capacity_in_teu = 20 - feeder_1.large_scheduled_vehicle.moved_capacity = 20 + feeder_1.large_scheduled_vehicle.inbound_container_volume = 20 feeder_1.large_scheduled_vehicle.save() feeder_2 = self._create_feeder(scheduled_arrival=now, service_name_suffix="2") feeder_2.large_scheduled_vehicle.capacity_in_teu = 20 - feeder_2.large_scheduled_vehicle.moved_capacity = 20 + feeder_2.large_scheduled_vehicle.inbound_container_volume = 20 feeder_2.large_scheduled_vehicle.save() self._create_container_delivered_by_large_scheduled_vehicle(feeder_1) @@ -276,11 +276,11 @@ def test_two_loaded_ships_one_with_outbound_traffic(self): now = datetime.datetime.now() feeder_1 = self._create_feeder(scheduled_arrival=now, service_name_suffix="1") feeder_1.large_scheduled_vehicle.capacity_in_teu = 20 - feeder_1.large_scheduled_vehicle.moved_capacity = 20 + feeder_1.large_scheduled_vehicle.inbound_container_volume = 20 feeder_1.large_scheduled_vehicle.save() feeder_2 = self._create_feeder(scheduled_arrival=now, service_name_suffix="2") feeder_2.large_scheduled_vehicle.capacity_in_teu = 20 - feeder_2.large_scheduled_vehicle.moved_capacity = 20 + feeder_2.large_scheduled_vehicle.inbound_container_volume = 20 feeder_2.large_scheduled_vehicle.save() truck = self._create_truck(arrival=now) diff --git a/conflowgen/tests/application/services/test_vehicle_capacity_manager.py b/conflowgen/tests/application/services/test_vehicle_capacity_manager.py new file mode 100644 index 0000000..cc12a47 --- /dev/null +++ b/conflowgen/tests/application/services/test_vehicle_capacity_manager.py @@ -0,0 +1,274 @@ +import datetime +import unittest + +import parameterized + +from conflowgen.application.services.vehicle_capacity_manager import VehicleCapacityManager +from conflowgen.descriptive_datatypes import FlowDirection +from conflowgen.domain_models.container import Container +from conflowgen.domain_models.data_types.container_length import ContainerLength +from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport +from conflowgen.domain_models.data_types.storage_requirement import StorageRequirement +from conflowgen.domain_models.large_vehicle_schedule import Schedule, Destination +from conflowgen.domain_models.vehicle import Train, LargeScheduledVehicle, Truck, Feeder +from conflowgen.tests.substitute_peewee_database import setup_sqlite_in_memory_db + + +class TestVehicleCapacityManager(unittest.TestCase): + + def setUp(self) -> None: + """Create container database in memory""" + sqlite_db = setup_sqlite_in_memory_db() + sqlite_db.create_tables([ + Container, + Schedule, + LargeScheduledVehicle, + Train, + Destination, + Truck, + Feeder, + ]) + + self.vehicle_capacity_manager = VehicleCapacityManager() + self.vehicle_capacity_manager.set_transportation_buffer(transportation_buffer=0) + + schedule_train = Schedule.create( + vehicle_type=ModeOfTransport.train, + service_name="TestServiceTrain", + vehicle_arrives_at=datetime.date(year=2024, month=5, day=26), + vehicle_arrives_at_time=datetime.time(hour=13, minute=15), + average_vehicle_capacity=90, + average_inbound_container_volume=90, + ) + self.train_lsv = LargeScheduledVehicle.create( + vehicle_name="TestTrain1", + capacity_in_teu=90, + inbound_container_volume=30, + scheduled_arrival=datetime.datetime(year=2024, month=5, day=26, hour=13, minute=15), + schedule=schedule_train + ) + self.train = Train.create( + large_scheduled_vehicle=self.train_lsv + ) + + schedule_feeder = Schedule.create( + vehicle_type=ModeOfTransport.feeder, + service_name="TestServiceFeeder", + vehicle_arrives_at=datetime.date(year=2024, month=5, day=26), + vehicle_arrives_at_time=datetime.time(hour=11, minute=15), + average_vehicle_capacity=1200, + average_inbound_container_volume=600, + ) + self.feeder_lsv = LargeScheduledVehicle.create( + vehicle_name="TestFeeder1", + capacity_in_teu=1200, + inbound_container_volume=600, + scheduled_arrival=datetime.datetime(year=2024, month=5, day=26, hour=11, minute=45), + schedule=schedule_feeder + ) + self.feeder = Feeder.create( + large_scheduled_vehicle=self.feeder_lsv + ) + + @parameterized.parameterized.expand([ + [FlowDirection.export_flow], + [FlowDirection.transshipment_flow] + ]) + def test_free_capacity_on_outbound_journey_without_any_containers_and_no_ramp_up_period( + self, flow_direction: FlowDirection + ): + """ + Independent of the flow direction, the outbound capacity should not change as long as no ramp-up period is + defined. + """ + free_capacity_on_train = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + self.train, flow_direction + ) + self.assertEqual(free_capacity_on_train, 30) + + free_capacity_on_feeder = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + self.feeder, flow_direction + ) + self.assertEqual(free_capacity_on_feeder, 600) + + @parameterized.parameterized.expand([ + [FlowDirection.import_flow, 30, 600], + [FlowDirection.export_flow, 30, 600], + [FlowDirection.transshipment_flow, 6, 120], # downscale by 20% + ]) + def test_free_capacity_on_outbound_journey_without_any_containers_and_a_ramp_up_period( + self, flow_direction: FlowDirection, train_volume: int, feeder_volume: int + ): + """ + The outbound capacity should change for transshipment when a ramp-up period is defined and is applicable. + """ + self.vehicle_capacity_manager.set_ramp_up_and_down_times( + ramp_up_period_end=datetime.datetime(year=2024, month=5, day=27) # one day after the feeder and train + ) + train_volume_calc = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + self.train, flow_direction + ) + self.assertEqual( + train_volume, train_volume_calc, f"The used TEU capacity of the train is {train_volume} TEU." + ) + + feeder_volume_calc = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + self.feeder, flow_direction + ) + self.assertEqual( + feeder_volume, feeder_volume_calc, f"The used TEU capacity of the feeder is {feeder_volume} TEU." + ) + + def test_free_capacity_on_inbound_journey_without_any_containers_and_no_ramp_down_period(self): + """ + Independent of the flow direction, the inbound capacity should not change as long as no ramp-down period is + defined. + """ + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( + self.train + ) + self.assertEqual(free_capacity_in_teu, 30, "The used TEU capacity of the train is 30 TEU.") + + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( + self.feeder + ) + self.assertEqual(free_capacity_in_teu, 600, "The used TEU capacity of the feeder is 600 TEU.") + + def test_free_capacity_on_inbound_journey_without_any_containers_and_a_ramp_down_period(self): + """ + Independent of the flow direction, the inbound capacity is capped during the ramp-down period. + """ + self.vehicle_capacity_manager.set_ramp_up_and_down_times( + ramp_down_period_start=datetime.datetime(year=2024, month=5, day=25) # one day before the feeder and train + ) + + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( + self.train + ) + self.assertEqual( + free_capacity_in_teu, 6, "The used TEU capacity of the train is 20% of 30 TEU." + ) + + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( + self.feeder + ) + self.assertEqual( + free_capacity_in_teu, 120, "The used TEU capacity of the feeder is 20% of 600 TEU." + ) + + def test_free_capacity_for_one_teu(self): + """No ramp-up or ramp-down period applied""" + Container.create( + weight=20, + length=ContainerLength.twenty_feet, + storage_requirement=StorageRequirement.standard, + delivered_by=ModeOfTransport.truck, + picked_up_by=ModeOfTransport.train, + picked_up_by_initial=ModeOfTransport.feeder, + picked_up_by_large_scheduled_vehicle=self.train_lsv, + ) + + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + self.train, FlowDirection.undefined + ) + self.assertEqual( + free_capacity_in_teu, 29, "The used TEU capacity of the train is 30 TEU, and 1 TEU is used by " + "the one container we just created.") + + def test_free_capacity_during_ramp_up_period_for_one_teu(self): + Container.create( + weight=20, + length=ContainerLength.forty_feet, + storage_requirement=StorageRequirement.standard, + delivered_by=ModeOfTransport.truck, + picked_up_by=ModeOfTransport.feeder, + picked_up_by_initial=ModeOfTransport.feeder, + picked_up_by_large_scheduled_vehicle=self.feeder_lsv, + ) + self.vehicle_capacity_manager.set_ramp_up_and_down_times( + # one day after the feeder + ramp_up_period_end=datetime.datetime(year=2024, month=5, day=27) + ) + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + self.feeder, FlowDirection.transshipment_flow + ) + self.assertAlmostEqual(free_capacity_in_teu, 118, msg="20% of 600 TEU is 120, of that 2 TEU minus") + + def test_free_capacity_during_ramp_down_period_for_one_teu(self): + Container.create( + weight=20, + length=ContainerLength.twenty_feet, + storage_requirement=StorageRequirement.standard, + delivered_by=ModeOfTransport.train, + picked_up_by=ModeOfTransport.feeder, + picked_up_by_initial=ModeOfTransport.feeder, + delivered_by_large_scheduled_vehicle=self.train_lsv, + ) + self.vehicle_capacity_manager.set_ramp_up_and_down_times( + # one day before the train + ramp_down_period_start=datetime.datetime(year=2024, month=5, day=25) + ) + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( + self.train + ) + self.assertAlmostEqual( + free_capacity_in_teu, 5.0, msg="20% of 30 TEU is 6 TEU, of that 1 TEU is used " + ) + + def test_free_capacity_during_ramp_up_period_without_load(self): + self.vehicle_capacity_manager.set_ramp_up_and_down_times( + # one day before the train + ramp_down_period_start=datetime.datetime(year=2024, month=5, day=25) + ) + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( + self.train + ) + self.assertAlmostEqual(free_capacity_in_teu, 6) + + def test_free_capacity_for_one_ffe(self): + Container.create( + weight=20, + length=ContainerLength.forty_feet, + storage_requirement=StorageRequirement.standard, + delivered_by=ModeOfTransport.truck, + picked_up_by=ModeOfTransport.train, + picked_up_by_initial=ModeOfTransport.train, + picked_up_by_large_scheduled_vehicle=self.train_lsv, + ) + + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + self.train, FlowDirection.undefined + ) + self.assertEqual(free_capacity_in_teu, 28) # 30 - 2.5 + + def test_free_capacity_for_45_foot_container(self): + Container.create( + weight=20, + length=ContainerLength.forty_five_feet, + storage_requirement=StorageRequirement.standard, + delivered_by=ModeOfTransport.truck, + picked_up_by=ModeOfTransport.train, + picked_up_by_initial=ModeOfTransport.feeder, + picked_up_by_large_scheduled_vehicle=self.train_lsv, + ) + + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + self.train, FlowDirection.undefined + ) + self.assertEqual(free_capacity_in_teu, 27.75, "A 45' container uses 2.25 TEU") + + def test_free_capacity_for_other_container(self): + Container.create( + weight=20, + length=ContainerLength.other, + storage_requirement=StorageRequirement.standard, + delivered_by=ModeOfTransport.truck, + picked_up_by=ModeOfTransport.train, + picked_up_by_initial=ModeOfTransport.feeder, + picked_up_by_large_scheduled_vehicle=self.train_lsv, + ) + + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + self.train, FlowDirection.undefined + ) + self.assertEqual(free_capacity_in_teu, 27.5, "30 TEU minus 2.5 TEU") diff --git a/conflowgen/tests/application/services/test_vehicle_countainer_volume_calculator.py b/conflowgen/tests/application/services/test_vehicle_countainer_volume_calculator.py new file mode 100644 index 0000000..d62c844 --- /dev/null +++ b/conflowgen/tests/application/services/test_vehicle_countainer_volume_calculator.py @@ -0,0 +1,189 @@ +import datetime +import unittest + +import parameterized + +from conflowgen.descriptive_datatypes import FlowDirection +from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport +from conflowgen.application.services.vehicle_container_volume_calculator import VehicleContainerVolumeCalculator +from conflowgen.domain_models.large_vehicle_schedule import Schedule +from conflowgen.domain_models.vehicle import LargeScheduledVehicle, DeepSeaVessel, Feeder +from conflowgen.tests.substitute_peewee_database import setup_sqlite_in_memory_db + + +class TestVehicleContainerVolumeCalculator(unittest.TestCase): + + def setUp(self) -> None: + """Create database in memory""" + self.sqlite_db = setup_sqlite_in_memory_db() + self.sqlite_db.create_tables([ + LargeScheduledVehicle, + Schedule, + DeepSeaVessel, + Feeder, + ]) + self.calculator = VehicleContainerVolumeCalculator() + self.calculator.set_transportation_buffer(0) + + schedule_deep_sea_vessel = Schedule.create( + vehicle_type=ModeOfTransport.deep_sea_vessel, + service_name="TestService-DeepSeaVessel", + vehicle_arrives_at=datetime.date(year=2024, month=8, day=7), + vehicle_arrives_at_time=datetime.time(hour=13, minute=15), + average_vehicle_capacity=12000, + average_inbound_container_volume=3000, + ) + self.lsv_deep_sea_vessel = LargeScheduledVehicle.create( + vehicle_name="TestShip-DeepSeaVessel-1", + capacity_in_teu=12000, + inbound_container_volume=3000, + scheduled_arrival=datetime.datetime(year=2024, month=8, day=7, hour=13, minute=15), + schedule=schedule_deep_sea_vessel + ) + self.deep_sea_vessel = DeepSeaVessel.create( + large_scheduled_vehicle=self.lsv_deep_sea_vessel + ) + + schedule_feeder = Schedule.create( + vehicle_type=ModeOfTransport.feeder, + service_name="TestService-Feeder", + vehicle_arrives_at=datetime.date(year=2024, month=8, day=7), + vehicle_arrives_at_time=datetime.time(hour=13, minute=15), + average_vehicle_capacity=1200, + average_inbound_container_volume=600, + ) + self.lsv_feeder = LargeScheduledVehicle.create( + vehicle_name="TestShip-Feeder1", + capacity_in_teu=1200, + inbound_container_volume=600, + scheduled_arrival=datetime.datetime(year=2024, month=8, day=7, hour=11, minute=9), + schedule=schedule_feeder + ) + self.feeder = Feeder.create( + large_scheduled_vehicle=self.lsv_feeder + ) + + @parameterized.parameterized.expand([ + [flow_direction] for flow_direction in FlowDirection + ]) + def test_get_maximum_transported_container_volume_on_outbound_journey_and_no_ramp_up_period( + self, flow_direction: FlowDirection + ): + """ + Independent of the flow direction, the outbound capacity should not change as long as no ramp-up period is + defined. + """ + feeder_vessel_calc = self.calculator.get_maximum_transported_container_volume_on_outbound_journey( + self.lsv_feeder, flow_direction + ) + scaled_moved_container_volume, unscaled_moved_container_volume = feeder_vessel_calc + self.assertEqual(scaled_moved_container_volume, 600) + + deep_sea_vessel_calc = self.calculator.get_maximum_transported_container_volume_on_outbound_journey( + self.deep_sea_vessel, flow_direction + ) + scaled_moved_container_volume, unscaled_moved_container_volume = deep_sea_vessel_calc + self.assertEqual(scaled_moved_container_volume, 3000) + + @parameterized.parameterized.expand([ + [FlowDirection.import_flow, 600, 3000], # normal values + [FlowDirection.export_flow, 600, 3000], # normal values + [FlowDirection.transshipment_flow, 120, 600], # downscaled by 20% + ]) + def test_get_maximum_transported_container_volume_on_outbound_journey_and_an_applicable_ramp_up_period( + self, flow_direction: FlowDirection, feeder_volume: int, deep_sea_volume: int + ): + """ + The outbound capacity should change for transshipment when a ramp-up period is defined and is applicable. + """ + self.calculator.set_ramp_up_and_down_times( + ramp_up_period_end=datetime.datetime(year=2024, month=8, day=8) # one day after the two vessels + ) + volume_feeder = self.calculator.get_maximum_transported_container_volume_on_outbound_journey( + self.feeder, flow_direction + ) + scaled_moved_container_volume, unscaled_moved_container_volume = volume_feeder + self.assertEqual(scaled_moved_container_volume, feeder_volume) + + volume_deep_sea_vessel = self.calculator.get_maximum_transported_container_volume_on_outbound_journey( + self.deep_sea_vessel, flow_direction + ) + scaled_moved_container_volume, unscaled_moved_container_volume = volume_deep_sea_vessel + self.assertEqual(scaled_moved_container_volume, deep_sea_volume) + + @parameterized.parameterized.expand([ + [FlowDirection.import_flow, 600, 3000], + [FlowDirection.export_flow, 600, 3000], + [FlowDirection.transshipment_flow, 600, 3000], + ]) + def test_get_maximum_transported_container_volume_on_outbound_journey_and_a_non_applicable_ramp_up_period( + self, flow_direction: FlowDirection, feeder_volume: int, deep_sea_volume: int + ): + """ + The outbound capacity should not change for transshipment when a ramp-up period is defined but lies in the past. + """ + self.calculator.set_ramp_up_and_down_times( + ramp_up_period_end=datetime.datetime(year=2024, month=8, day=6) # one day after the two vessels + ) + feeder_volume_calc = self.calculator.get_maximum_transported_container_volume_on_outbound_journey( + self.feeder, flow_direction + ) + scaled_moved_container_volume, unscaled_moved_container_volume = feeder_volume_calc + self.assertEqual(scaled_moved_container_volume, feeder_volume) + + deep_sea_volume_calc = self.calculator.get_maximum_transported_container_volume_on_outbound_journey( + self.deep_sea_vessel, flow_direction + ) + scaled_moved_container_volume, unscaled_moved_container_volume = deep_sea_volume_calc + self.assertEqual(scaled_moved_container_volume, deep_sea_volume) + + def test_get_transported_container_volume_on_inbound_journey_and_no_ramp_down_period(self): + """ + Independent of the flow direction, the inbound capacity should not change as long as no ramp-down period is + defined. + """ + feeder_volume_calc = self.calculator.get_transported_container_volume_on_inbound_journey( + self.feeder + ) + self.assertEqual(feeder_volume_calc, 600) + + deep_sea_vessel_calc = self.calculator.get_transported_container_volume_on_inbound_journey( + self.deep_sea_vessel + ) + self.assertEqual(deep_sea_vessel_calc, 3000) + + def test_get_transported_container_volume_on_inbound_journey_and_an_applicable_ramp_down_period(self): + """ + Independent of the flow direction, the inbound capacity is capped during the ramp-down period. + """ + self.calculator.set_ramp_up_and_down_times( + ramp_down_period_start=datetime.datetime(year=2021, month=8, day=6) # one day before the feeder and train + ) + + feeder_vessel_calc = self.calculator.get_transported_container_volume_on_inbound_journey( + self.lsv_feeder + ) + self.assertEqual(feeder_vessel_calc, 120, "Downscaled by 20%") + + deep_sea_vessel_calc = self.calculator.get_transported_container_volume_on_inbound_journey( + self.lsv_deep_sea_vessel + ) + self.assertEqual(deep_sea_vessel_calc, 600, "Downscaled by 20%") + + def test_get_transported_container_volume_on_inbound_journey_and_a_non_applicable_ramp_down_period(self): + """ + Independent of the flow direction, the inbound capacity is capped during the ramp-down period. + """ + self.calculator.set_ramp_up_and_down_times( + ramp_down_period_start=datetime.datetime(year=2024, month=8, day=8) # one day after + ) + + feeder_volume_calc = self.calculator.get_transported_container_volume_on_inbound_journey( + self.lsv_feeder + ) + self.assertEqual(feeder_volume_calc, 600, "Nothing should be downscaled") + + deep_sea_vessel_volume_calc = self.calculator.get_transported_container_volume_on_inbound_journey( + self.lsv_deep_sea_vessel + ) + self.assertEqual(deep_sea_vessel_volume_calc, 3000, "Nothing should be downscaled") diff --git a/conflowgen/tests/data_summaries/test_data_summaries_cache.py b/conflowgen/tests/data_summaries/test_data_summaries_cache.py index 6448b8d..df41612 100644 --- a/conflowgen/tests/data_summaries/test_data_summaries_cache.py +++ b/conflowgen/tests/data_summaries/test_data_summaries_cache.py @@ -132,7 +132,7 @@ def test_with_preview(self): vehicle_arrives_every_k_days=-1, vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300 + average_inbound_container_volume=300 ) preview = self.preview.get_weekly_truck_arrivals(True, True) self.assertEqual(preview, {3: 12, 4: 48}, "Uncached result is incorrect") @@ -179,7 +179,7 @@ def test_with_adjusted_preview(self): vehicle_arrives_every_k_days=-1, vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300 + average_inbound_container_volume=300 ) preview = self.preview.get_weekly_truck_arrivals(True, True) self.assertEqual(preview, {3: 12, 4: 48}, "Uncached result is incorrect") diff --git a/conflowgen/tests/domain_models/distribution_repositories/test_container_destination_distribution_repository.py b/conflowgen/tests/domain_models/distribution_repositories/test_container_destination_distribution_repository.py index b527c24..09b706d 100644 --- a/conflowgen/tests/domain_models/distribution_repositories/test_container_destination_distribution_repository.py +++ b/conflowgen/tests/domain_models/distribution_repositories/test_container_destination_distribution_repository.py @@ -31,7 +31,7 @@ def test_set_distribution(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) Schedule.create( @@ -40,7 +40,7 @@ def test_set_distribution(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) destination_1 = Destination.create( @@ -77,7 +77,7 @@ def test_save_and_load_correspond_for_single_entry(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) schedule.save() @@ -87,7 +87,7 @@ def test_save_and_load_correspond_for_single_entry(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ).save() destination_1 = Destination.create( @@ -121,7 +121,7 @@ def test_save_and_load_correspond_for_two_entries(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) schedule_2 = Schedule.create( @@ -130,7 +130,7 @@ def test_save_and_load_correspond_for_two_entries(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=12000, - average_moved_capacity=1000, + average_inbound_container_volume=1000, vehicle_arrives_every_k_days=-1 ) destination_1 = Destination.create( @@ -180,7 +180,7 @@ def test_validator(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) Schedule.create( @@ -189,7 +189,7 @@ def test_validator(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) destination_1 = Destination.create( diff --git a/conflowgen/tests/domain_models/factories/test_container_factory__create_for_large_scheduled_vehicle.py b/conflowgen/tests/domain_models/factories/test_container_factory__create_for_large_scheduled_vehicle.py index ccd8495..63c1335 100644 --- a/conflowgen/tests/domain_models/factories/test_container_factory__create_for_large_scheduled_vehicle.py +++ b/conflowgen/tests/domain_models/factories/test_container_factory__create_for_large_scheduled_vehicle.py @@ -14,7 +14,7 @@ from conflowgen.domain_models.factories.fleet_factory import FleetFactory from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport from conflowgen.domain_models.large_vehicle_schedule import Destination -from conflowgen.domain_models.vehicle import Feeder, LargeScheduledVehicle, Schedule, Truck +from conflowgen.domain_models.vehicle import Feeder, LargeScheduledVehicle, Schedule, Truck, DeepSeaVessel from conflowgen.tests.substitute_peewee_database import setup_sqlite_in_memory_db @@ -25,6 +25,7 @@ def setUp(self) -> None: sqlite_db = setup_sqlite_in_memory_db() sqlite_db.create_tables([ Feeder, + DeepSeaVessel, LargeScheduledVehicle, Schedule, Container, @@ -47,7 +48,7 @@ def setUp(self) -> None: vehicle_arrives_at=datetime.date(2021, 7, 9), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=3 + average_inbound_container_volume=3 ) self.feeders = FleetFactory().create_feeder_fleet( schedule=schedule, @@ -81,3 +82,69 @@ def test_create_containers_for_feeder_vessel(self) -> None: feeder_1.large_scheduled_vehicle ) self.assertIsNone(containers[0].delivered_by_truck) + + def test_create_containers_for_single_deep_sea_vessel_during_ramp_up_period(self): + schedule = Schedule.create( + service_name="SunExpress", + vehicle_type=ModeOfTransport.deep_sea_vessel, + vehicle_arrives_at=datetime.date(2021, 7, 9), + vehicle_arrives_at_time=datetime.time(11), + average_vehicle_capacity=24000, + average_inbound_container_volume=3000 + ) + vessels = FleetFactory().create_deep_sea_vessel_fleet( + schedule=schedule, + first_at=datetime.date(2021, 7, 8), + latest_at=datetime.date(2021, 7, 10) + ) + self.assertEqual( + len(vessels), + 1 + ) + vessel = vessels[0] + + self.container_factory.set_ramp_up_and_down_times( + ramp_up_period_end=datetime.datetime(2021, 7, 10), + ramp_down_period_start=None + ) + + # noinspection PyTypeChecker + containers = self.container_factory.create_containers_for_large_scheduled_vehicle(vessel) + + container_volume = sum(c.occupied_teu for c in containers) + + self.assertGreater(container_volume, 2900, "A bit less than 3000 is acceptable but common!") + self.assertLess(container_volume, 3100, "A bit more than 3000 is acceptable but common!") + + def test_create_containers_for_single_deep_sea_vessel_during_ramp_down_period(self): + schedule = Schedule.create( + service_name="SunExpress", + vehicle_type=ModeOfTransport.deep_sea_vessel, + vehicle_arrives_at=datetime.date(2024, 7, 9), + vehicle_arrives_at_time=datetime.time(11), + average_vehicle_capacity=24000, + average_inbound_container_volume=3000 + ) + vessels = FleetFactory().create_deep_sea_vessel_fleet( + schedule=schedule, + first_at=datetime.date(2024, 7, 8), + latest_at=datetime.date(2024, 7, 10) + ) + self.assertEqual( + len(vessels), + 1 + ) + vessel = vessels[0] + + self.container_factory.set_ramp_up_and_down_times( + ramp_up_period_end=None, + ramp_down_period_start=datetime.datetime(2024, 7, 8) + ) + + # noinspection PyTypeChecker + containers = self.container_factory.create_containers_for_large_scheduled_vehicle(vessel) + + container_volume_in_teu = sum(c.occupied_teu for c in containers) + + self.assertGreater(container_volume_in_teu, 500, "A bit less than 600 is acceptable but common!") + self.assertLess(container_volume_in_teu, 700, "A bit more than 600 is acceptable but common!") diff --git a/conflowgen/tests/domain_models/factories/test_container_factory__create_for_truck.py b/conflowgen/tests/domain_models/factories/test_container_factory__create_for_truck.py index b873d5e..d9ed2b8 100644 --- a/conflowgen/tests/domain_models/factories/test_container_factory__create_for_truck.py +++ b/conflowgen/tests/domain_models/factories/test_container_factory__create_for_truck.py @@ -53,7 +53,7 @@ def setUp(self) -> None: vehicle_arrives_at=datetime.date(2021, 7, 9), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=1 + average_inbound_container_volume=1 ) feeders = FleetFactory().create_feeder_fleet( schedule=schedule, diff --git a/conflowgen/tests/domain_models/factories/test_fleet_factory__create_feeder_fleet.py b/conflowgen/tests/domain_models/factories/test_fleet_factory__create_feeder_fleet.py index 03e00bf..315ff44 100644 --- a/conflowgen/tests/domain_models/factories/test_fleet_factory__create_feeder_fleet.py +++ b/conflowgen/tests/domain_models/factories/test_fleet_factory__create_feeder_fleet.py @@ -26,7 +26,7 @@ def test_create_feeder_fleet(self) -> None: vehicle_arrives_at=datetime.date(2021, 7, 9), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=50 + average_inbound_container_volume=50 ) feeders = self.fleet_factory.create_feeder_fleet( schedule=schedule, diff --git a/conflowgen/tests/domain_models/factories/test_schedule_factory.py b/conflowgen/tests/domain_models/factories/test_schedule_factory.py index a40d20a..f4c9d77 100644 --- a/conflowgen/tests/domain_models/factories/test_schedule_factory.py +++ b/conflowgen/tests/domain_models/factories/test_schedule_factory.py @@ -28,7 +28,7 @@ def test_add_schedule(self) -> None: vehicle_arrives_at=datetime.date(2021, 7, 9), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=1, + average_inbound_container_volume=1, next_destinations=[ ("DEBRV", 0.6), ("CNSHG", 0.4) @@ -39,7 +39,7 @@ def test_add_schedule(self) -> None: self.assertEqual(schedule.vehicle_type, ModeOfTransport.feeder) self.assertEqual(schedule.vehicle_arrives_at, datetime.date(2021, 7, 9)) self.assertEqual(schedule.average_vehicle_capacity, 800) - self.assertEqual(schedule.average_moved_capacity, 1) + self.assertEqual(schedule.average_inbound_container_volume, 1) next_destinations = Destination.select().where(Destination.belongs_to_schedule == schedule) next_destinations = list(next_destinations) self.assertEqual(len(next_destinations), 2) @@ -54,7 +54,7 @@ def test_repeated_add_schedule(self) -> None: vehicle_arrives_at=datetime.date(2021, 7, 9), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=1, + average_inbound_container_volume=1, next_destinations=[ ("DEBRV", 0.6), ("CNSHG", 0.4) @@ -67,7 +67,7 @@ def test_repeated_add_schedule(self) -> None: vehicle_arrives_at=datetime.date(2021, 7, 10), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=1, + average_inbound_container_volume=1, next_destinations=[ ("DEBRV", 0.6), ("CNSHG", 0.4) @@ -82,7 +82,7 @@ def test_get_schedule(self) -> None: vehicle_arrives_at=datetime.date(2021, 7, 9), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=1, + average_inbound_container_volume=1, next_destinations=[ ("DEBRV", 0.6), ("CNSHG", 0.4) @@ -93,7 +93,7 @@ def test_get_schedule(self) -> None: self.assertEqual(schedule.vehicle_type, ModeOfTransport.feeder) self.assertEqual(schedule.vehicle_arrives_at, datetime.date(2021, 7, 9)) self.assertEqual(schedule.average_vehicle_capacity, 800) - self.assertEqual(schedule.average_moved_capacity, 1) + self.assertEqual(schedule.average_inbound_container_volume, 1) next_destinations = Destination.select().where(Destination.belongs_to_schedule == schedule) next_destinations = list(next_destinations) self.assertEqual(len(next_destinations), 2) diff --git a/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_barge.py b/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_barge.py index 5daf03f..cf8c426 100644 --- a/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_barge.py +++ b/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_barge.py @@ -25,11 +25,11 @@ def test_create_normal_barge(self) -> None: vehicle_type=ModeOfTransport.barge, vehicle_arrives_at=datetime.datetime.now(), average_vehicle_capacity=60, - average_moved_capacity=30 + average_inbound_container_volume=30 ) self.vehicle_factory.create_barge( capacity_in_teu=60, - moved_capacity=30, + inbound_container_volume=30, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -40,19 +40,19 @@ def test_create_unrealistic_barge(self) -> None: vehicle_type=ModeOfTransport.barge, vehicle_arrives_at=datetime.datetime.now(), average_vehicle_capacity=800, - average_moved_capacity=50 + average_inbound_container_volume=50 ) with self.assertRaises(UnrealisticValuesException): self.vehicle_factory.create_barge( capacity_in_teu=-1, - moved_capacity=1, + inbound_container_volume=1, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) with self.assertRaises(UnrealisticValuesException): self.vehicle_factory.create_barge( capacity_in_teu=1, - moved_capacity=-1, + inbound_container_volume=-1, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) diff --git a/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_deep_sea_vessel.py b/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_deep_sea_vessel.py index d68204c..4704b6e 100644 --- a/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_deep_sea_vessel.py +++ b/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_deep_sea_vessel.py @@ -25,11 +25,11 @@ def test_create_normal_deep_sea_vessel(self) -> None: vehicle_type=ModeOfTransport.deep_sea_vessel, vehicle_arrives_at=datetime.datetime.now(), average_vehicle_capacity=800, - average_moved_capacity=50 + average_inbound_container_volume=50 ) self.vehicle_factory.create_deep_sea_vessel( capacity_in_teu=800, - moved_capacity=50, + inbound_container_volume=50, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -40,26 +40,26 @@ def test_create_unrealistic_deep_sea_vessel(self) -> None: vehicle_type=ModeOfTransport.deep_sea_vessel, vehicle_arrives_at=datetime.datetime.now(), average_vehicle_capacity=800, - average_moved_capacity=50 + average_inbound_container_volume=50 ) with self.assertRaises(UnrealisticValuesException): self.vehicle_factory.create_deep_sea_vessel( capacity_in_teu=-1, - moved_capacity=1, + inbound_container_volume=1, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) with self.assertRaises(UnrealisticValuesException): self.vehicle_factory.create_deep_sea_vessel( capacity_in_teu=1, - moved_capacity=-1, + inbound_container_volume=-1, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) with self.assertRaises(UnrealisticValuesException): self.vehicle_factory.create_deep_sea_vessel( capacity_in_teu=50, - moved_capacity=100, + inbound_container_volume=100, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) diff --git a/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_feeder.py b/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_feeder.py index d7894cf..0951793 100644 --- a/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_feeder.py +++ b/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_feeder.py @@ -25,11 +25,11 @@ def test_create_normal_feeder(self) -> None: vehicle_type=ModeOfTransport.feeder, vehicle_arrives_at=datetime.datetime.now(), average_vehicle_capacity=800, - average_moved_capacity=50 + average_inbound_container_volume=50 ) self.vehicle_factory.create_feeder( capacity_in_teu=800, - moved_capacity=50, + inbound_container_volume=50, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -40,12 +40,12 @@ def test_create_unrealistic_feeder(self) -> None: vehicle_type=ModeOfTransport.feeder, vehicle_arrives_at=datetime.datetime.now(), average_vehicle_capacity=800, - average_moved_capacity=50 + average_inbound_container_volume=50 ) with self.assertRaises(UnrealisticValuesException): self.vehicle_factory.create_feeder( capacity_in_teu=-1, - moved_capacity=1, + inbound_container_volume=1, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -53,7 +53,7 @@ def test_create_unrealistic_feeder(self) -> None: with self.assertRaises(UnrealisticValuesException): self.vehicle_factory.create_feeder( capacity_in_teu=1, - moved_capacity=-1, + inbound_container_volume=-1, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) diff --git a/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_train.py b/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_train.py index d613dbc..2aa44c1 100644 --- a/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_train.py +++ b/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_train.py @@ -25,11 +25,11 @@ def test_create_normal_train(self) -> None: vehicle_type=ModeOfTransport.train, vehicle_arrives_at=datetime.datetime.now(), average_vehicle_capacity=90, - average_moved_capacity=90 + average_inbound_container_volume=90 ) self.vehicle_factory.create_train( capacity_in_teu=90, - moved_capacity=90, + inbound_container_volume=90, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -40,12 +40,12 @@ def test_create_unrealistic_train(self) -> None: vehicle_type=ModeOfTransport.train, vehicle_arrives_at=datetime.datetime.now(), average_vehicle_capacity=800, - average_moved_capacity=50 + average_inbound_container_volume=50 ) with self.assertRaises(UnrealisticValuesException): self.vehicle_factory.create_train( capacity_in_teu=-1, - moved_capacity=1, + inbound_container_volume=1, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) diff --git a/conflowgen/tests/domain_models/reposistories/test_large_scheduled_vehicle_repository.py b/conflowgen/tests/domain_models/reposistories/test_large_scheduled_vehicle_repository.py deleted file mode 100644 index 423db37..0000000 --- a/conflowgen/tests/domain_models/reposistories/test_large_scheduled_vehicle_repository.py +++ /dev/null @@ -1,104 +0,0 @@ -import datetime -import unittest - -from conflowgen.domain_models.container import Container -from conflowgen.domain_models.data_types.container_length import ContainerLength -from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport -from conflowgen.domain_models.data_types.storage_requirement import StorageRequirement -from conflowgen.domain_models.large_vehicle_schedule import Schedule, Destination -from conflowgen.domain_models.repositories.large_scheduled_vehicle_repository import LargeScheduledVehicleRepository -from conflowgen.domain_models.vehicle import Train, LargeScheduledVehicle, Truck -from conflowgen.tests.substitute_peewee_database import setup_sqlite_in_memory_db - - -class TestLargeScheduledVehicleRepository(unittest.TestCase): - - def setUp(self) -> None: - """Create container database in memory""" - sqlite_db = setup_sqlite_in_memory_db() - sqlite_db.create_tables([ - Container, - Schedule, - LargeScheduledVehicle, - Train, - Destination, - Truck - ]) - self.lsv_repository = LargeScheduledVehicleRepository() - self.lsv_repository.set_transportation_buffer(transportation_buffer=0) - schedule = Schedule.create( - vehicle_type=ModeOfTransport.train, - service_name="TestService", - vehicle_arrives_at=datetime.date(year=2021, month=8, day=7), - vehicle_arrives_at_time=datetime.time(hour=13, minute=15), - average_vehicle_capacity=90, - average_moved_capacity=90, - ) - self.train_lsv = LargeScheduledVehicle.create( - vehicle_name="TestTrain1", - capacity_in_teu=90, - moved_capacity=3, - scheduled_arrival=datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15), - schedule=schedule - ) - self.train_lsv.save() - self.train = Train.create( - large_scheduled_vehicle=self.train_lsv - ) - self.train.save() - - def test_free_capacity_for_one_teu(self): - Container.create( - weight=20, - length=ContainerLength.twenty_feet, - storage_requirement=StorageRequirement.standard, - delivered_by=ModeOfTransport.truck, - picked_up_by=ModeOfTransport.train, - picked_up_by_initial=ModeOfTransport.feeder, - picked_up_by_large_scheduled_vehicle=self.train_lsv, - ) - - free_capacity_in_teu = self.lsv_repository.get_free_capacity_for_outbound_journey(self.train) - self.assertEqual(free_capacity_in_teu, 2) - - def test_free_capacity_for_one_ffe(self): - Container.create( - weight=20, - length=ContainerLength.forty_feet, - storage_requirement=StorageRequirement.standard, - delivered_by=ModeOfTransport.truck, - picked_up_by=ModeOfTransport.train, - picked_up_by_initial=ModeOfTransport.train, - picked_up_by_large_scheduled_vehicle=self.train_lsv, - ) - - free_capacity_in_teu = self.lsv_repository.get_free_capacity_for_outbound_journey(self.train) - self.assertEqual(free_capacity_in_teu, 1) - - def test_free_capacity_for_45_foot_container(self): - Container.create( - weight=20, - length=ContainerLength.forty_five_feet, - storage_requirement=StorageRequirement.standard, - delivered_by=ModeOfTransport.truck, - picked_up_by=ModeOfTransport.train, - picked_up_by_initial=ModeOfTransport.feeder, - picked_up_by_large_scheduled_vehicle=self.train_lsv, - ) - - free_capacity_in_teu = self.lsv_repository.get_free_capacity_for_outbound_journey(self.train) - self.assertEqual(free_capacity_in_teu, 0.75) - - def test_free_capacity_for_other_container(self): - Container.create( - weight=20, - length=ContainerLength.other, - storage_requirement=StorageRequirement.standard, - delivered_by=ModeOfTransport.truck, - picked_up_by=ModeOfTransport.train, - picked_up_by_initial=ModeOfTransport.feeder, - picked_up_by_large_scheduled_vehicle=self.train_lsv, - ) - - free_capacity_in_teu = self.lsv_repository.get_free_capacity_for_outbound_journey(self.train) - self.assertEqual(free_capacity_in_teu, 0.5) diff --git a/conflowgen/tests/domain_models/reposistories/test_schedule_repository.py b/conflowgen/tests/domain_models/reposistories/test_schedule_repository.py index d926101..bd323fe 100644 --- a/conflowgen/tests/domain_models/reposistories/test_schedule_repository.py +++ b/conflowgen/tests/domain_models/reposistories/test_schedule_repository.py @@ -2,6 +2,7 @@ import unittest import unittest.mock +from conflowgen.descriptive_datatypes import FlowDirection from conflowgen.domain_models.container import Container from conflowgen.domain_models.data_types.container_length import ContainerLength from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport @@ -36,7 +37,8 @@ def test_empty_schedule_database_throws_no_exception(self): datetime.datetime.now(), datetime.datetime.now() + datetime.timedelta(days=21), vehicle_type=ModeOfTransport.train, - required_capacity=ContainerLength.twenty_feet + required_capacity=ContainerLength.twenty_feet, + flow_direction=FlowDirection.undefined ) self.assertEqual(len(vehicles_and_frequency), 0) @@ -48,27 +50,26 @@ def test_find_vehicle_if_in_time_range(self): vehicle_arrives_at=datetime.date(year=2021, month=8, day=7), vehicle_arrives_at_time=datetime.time(hour=13, minute=15), average_vehicle_capacity=90, - average_moved_capacity=1, + average_inbound_container_volume=1, ) train_moves_this_capacity = 7 train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=90, - moved_capacity=train_moves_this_capacity, + inbound_container_volume=train_moves_this_capacity, scheduled_arrival=datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15), schedule=schedule ) - train_lsv.save() - train = Train.create( + Train.create( large_scheduled_vehicle=train_lsv ) - train.save() vehicles = self.schedule_repository.get_departing_vehicles( start=datetime.datetime(year=2021, month=8, day=5, hour=0, minute=0), end=datetime.datetime(year=2021, month=8, day=10, hour=23, minute=59), vehicle_type=ModeOfTransport.train, - required_capacity=ContainerLength.twenty_feet + required_capacity=ContainerLength.twenty_feet, + flow_direction=FlowDirection.undefined ) self.assertEqual(len(vehicles), 1) @@ -82,27 +83,26 @@ def test_export_buffer_below_capacity(self): vehicle_arrives_at=datetime.date(year=2021, month=8, day=7), vehicle_arrives_at_time=datetime.time(hour=13, minute=15), average_vehicle_capacity=90, - average_moved_capacity=1, + average_inbound_container_volume=1, ) train_moves_this_capacity = 7 train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=90, - moved_capacity=train_moves_this_capacity, + inbound_container_volume=train_moves_this_capacity, scheduled_arrival=datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15), schedule=schedule ) - train_lsv.save() - train = Train.create( + Train.create( large_scheduled_vehicle=train_lsv ) - train.save() vehicles = self.schedule_repository.get_departing_vehicles( start=datetime.datetime(year=2021, month=8, day=5, hour=0, minute=0), end=datetime.datetime(year=2021, month=8, day=10, hour=23, minute=59), vehicle_type=ModeOfTransport.train, - required_capacity=ContainerLength.twenty_feet + required_capacity=ContainerLength.twenty_feet, + flow_direction=FlowDirection.undefined ) self.assertEqual(len(vehicles), 1) @@ -115,27 +115,26 @@ def test_export_buffer_above_capacity(self): vehicle_arrives_at=datetime.date(year=2021, month=8, day=7), vehicle_arrives_at_time=datetime.time(hour=13, minute=15), average_vehicle_capacity=4, - average_moved_capacity=2, + average_inbound_container_volume=2, ) train_moves_this_capacity = 7 train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=8, - moved_capacity=train_moves_this_capacity, + inbound_container_volume=train_moves_this_capacity, scheduled_arrival=datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15), schedule=schedule ) - train_lsv.save() - train = Train.create( + Train.create( large_scheduled_vehicle=train_lsv ) - train.save() vehicles = self.schedule_repository.get_departing_vehicles( start=datetime.datetime(year=2021, month=8, day=5, hour=0, minute=0), end=datetime.datetime(year=2021, month=8, day=10, hour=23, minute=59), vehicle_type=ModeOfTransport.train, - required_capacity=ContainerLength.twenty_feet + required_capacity=ContainerLength.twenty_feet, + flow_direction=FlowDirection.undefined ) self.assertEqual(len(vehicles), 1) @@ -147,23 +146,23 @@ def test_ignore_vehicle_outside_time_range(self): vehicle_arrives_at=datetime.date(year=2021, month=8, day=7), vehicle_arrives_at_time=datetime.time(hour=13, minute=15), average_vehicle_capacity=90, - average_moved_capacity=1, + average_inbound_container_volume=1, ) train_moves_this_capacity = 7 - train = LargeScheduledVehicle.create( + LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=90, - moved_capacity=train_moves_this_capacity, + inbound_container_volume=train_moves_this_capacity, scheduled_arrival=datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15), schedule=schedule ) - train.save() vehicles_and_frequency = self.schedule_repository.get_departing_vehicles( start=datetime.datetime(year=2021, month=8, day=10, hour=13, minute=15), end=datetime.datetime(year=2021, month=8, day=15, hour=13, minute=15), vehicle_type=ModeOfTransport.train, - required_capacity=ContainerLength.forty_feet + required_capacity=ContainerLength.forty_feet, + flow_direction=FlowDirection.undefined ) self.assertEqual(len(vehicles_and_frequency), 0) @@ -175,24 +174,22 @@ def test_check_used_20_foot_containers(self): vehicle_arrives_at=datetime.date(year=2021, month=8, day=7), vehicle_arrives_at_time=datetime.time(hour=13, minute=15), average_vehicle_capacity=90, - average_moved_capacity=90, + average_inbound_container_volume=90, ) train_moves_this_capacity_in_teu = 2 train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=90, - moved_capacity=train_moves_this_capacity_in_teu, + inbound_container_volume=train_moves_this_capacity_in_teu, scheduled_arrival=datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15), schedule=schedule ) - train_lsv.save() train = Train.create( large_scheduled_vehicle=train_lsv ) - train.save() # This container is already loaded on the train - Container.create( + container = Container.create( weight=20, length=ContainerLength.twenty_feet, storage_requirement=StorageRequirement.standard, @@ -205,7 +202,8 @@ def test_check_used_20_foot_containers(self): start=datetime.datetime(year=2021, month=8, day=5, hour=0, minute=0), end=datetime.datetime(year=2021, month=8, day=10, hour=23, minute=59), vehicle_type=ModeOfTransport.train, - required_capacity=ContainerLength.twenty_feet + required_capacity=ContainerLength.twenty_feet, + flow_direction=container.flow_direction ) self.assertEqual(len(available_vehicles), 1) @@ -218,24 +216,22 @@ def test_check_used_40_foot_containers(self): vehicle_arrives_at=datetime.date(year=2021, month=8, day=7), vehicle_arrives_at_time=datetime.time(hour=13, minute=15), average_vehicle_capacity=90, - average_moved_capacity=90, + average_inbound_container_volume=90, ) train_moves_this_capacity_in_teu = 3 train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=90, - moved_capacity=train_moves_this_capacity_in_teu, + inbound_container_volume=train_moves_this_capacity_in_teu, scheduled_arrival=datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15), schedule=schedule ) - train_lsv.save() - train = Train.create( + Train.create( large_scheduled_vehicle=train_lsv ) - train.save() # This container is already loaded on the train - Container.create( + container = Container.create( weight=20, length=ContainerLength.forty_feet, storage_requirement=StorageRequirement.standard, @@ -248,36 +244,35 @@ def test_check_used_40_foot_containers(self): start=datetime.datetime(year=2021, month=8, day=5, hour=0, minute=0), end=datetime.datetime(year=2021, month=8, day=10, hour=23, minute=59), vehicle_type=ModeOfTransport.train, - required_capacity=ContainerLength.twenty_feet + required_capacity=ContainerLength.twenty_feet, + flow_direction=container.flow_direction ) self.assertEqual(len(vehicles), 1) - def test_use_lsv_repository(self): + def test_use_vehicle_capacity_manager(self): schedule = Schedule.create( vehicle_type=ModeOfTransport.train, service_name="TestService", vehicle_arrives_at=datetime.date(year=2021, month=8, day=7), vehicle_arrives_at_time=datetime.time(hour=13, minute=15), average_vehicle_capacity=90, - average_moved_capacity=90, + average_inbound_container_volume=90, ) train_moves_this_capacity_in_teu = 3 train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=90, - moved_capacity=train_moves_this_capacity_in_teu, + inbound_container_volume=train_moves_this_capacity_in_teu, scheduled_arrival=datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15), schedule=schedule ) - train_lsv.save() train = Train.create( large_scheduled_vehicle=train_lsv ) - train.save() # This container is already loaded on the train - Container.create( + container = Container.create( weight=20, length=ContainerLength.forty_feet, storage_requirement=StorageRequirement.standard, @@ -288,13 +283,27 @@ def test_use_lsv_repository(self): ) with unittest.mock.patch.object( - self.schedule_repository.large_scheduled_vehicle_repository, + self.schedule_repository.vehicle_capacity_manager, 'get_free_capacity_for_outbound_journey', return_value=1) as mock_method: self.schedule_repository.get_departing_vehicles( start=datetime.datetime(year=2021, month=8, day=5, hour=0, minute=0), end=datetime.datetime(year=2021, month=8, day=10, hour=23, minute=59), vehicle_type=ModeOfTransport.train, - required_capacity=ContainerLength.twenty_feet + required_capacity=ContainerLength.twenty_feet, + flow_direction=container.flow_direction ) - mock_method.assert_called_once_with(train) + mock_method.assert_called_once_with(train, FlowDirection.undefined) + + def test_set_ramp_up_and_down_period(self): + with unittest.mock.patch.object( + self.schedule_repository.vehicle_capacity_manager, + 'set_ramp_up_and_down_times', + return_value=None) as mock_method: + self.schedule_repository.set_ramp_up_and_down_times( + datetime.datetime(2023, 1, 1), datetime.datetime(2024, 1, 1) + ) + mock_method.assert_called_once_with( + ramp_up_period_end=datetime.datetime(2023, 1, 1), + ramp_down_period_start=datetime.datetime(2024, 1, 1) + ) diff --git a/conflowgen/tests/domain_models/test_container.py b/conflowgen/tests/domain_models/test_container.py index 369f65c..1ac2056 100644 --- a/conflowgen/tests/domain_models/test_container.py +++ b/conflowgen/tests/domain_models/test_container.py @@ -5,8 +5,10 @@ import unittest from dataclasses import dataclass +import parameterized from peewee import IntegrityError +from conflowgen.descriptive_datatypes import FlowDirection from conflowgen.domain_models.container import Container, FaultyDataException, NoPickupVehicleException from conflowgen.domain_models.data_types.container_length import ContainerLength from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport @@ -19,6 +21,7 @@ class TestContainer(unittest.TestCase): """ Rudimentarily check if peewee can handle container entries in the database. + Also check whether the class properties work. """ def setUp(self) -> None: @@ -150,3 +153,63 @@ def __init__(self, value: int, name: str): with self.assertRaises(NoPickupVehicleException): container.get_departure_time() + + @parameterized.parameterized.expand([ + [ContainerLength.twenty_feet, 1], + [ContainerLength.forty_feet, 2], + [ContainerLength.forty_five_feet, 2.25], + [ContainerLength.other, 2.5] + ]) + def test_occupied_teu(self, container_size, teu): + """Test whether the container size is correctly converted to TEU""" + container = Container.create( + weight=10, + delivered_by=ModeOfTransport.barge, + picked_up_by=ModeOfTransport.truck, + picked_up_by_initial=ModeOfTransport.deep_sea_vessel, + length=container_size, + storage_requirement=StorageRequirement.standard + ) + self.assertEqual(teu, container.occupied_teu) + + @parameterized.parameterized.expand([ + [ModeOfTransport.deep_sea_vessel, ModeOfTransport.deep_sea_vessel, FlowDirection.transshipment_flow], + [ModeOfTransport.deep_sea_vessel, ModeOfTransport.feeder, FlowDirection.transshipment_flow], + [ModeOfTransport.feeder, ModeOfTransport.deep_sea_vessel, FlowDirection.transshipment_flow], + [ModeOfTransport.feeder, ModeOfTransport.feeder, FlowDirection.transshipment_flow], + + [ModeOfTransport.deep_sea_vessel, ModeOfTransport.truck, FlowDirection.import_flow], + [ModeOfTransport.deep_sea_vessel, ModeOfTransport.barge, FlowDirection.import_flow], + [ModeOfTransport.deep_sea_vessel, ModeOfTransport.train, FlowDirection.import_flow], + [ModeOfTransport.feeder, ModeOfTransport.truck, FlowDirection.import_flow], + [ModeOfTransport.feeder, ModeOfTransport.barge, FlowDirection.import_flow], + [ModeOfTransport.feeder, ModeOfTransport.train, FlowDirection.import_flow], + + [ModeOfTransport.truck, ModeOfTransport.deep_sea_vessel, FlowDirection.export_flow], + [ModeOfTransport.truck, ModeOfTransport.feeder, FlowDirection.export_flow], + [ModeOfTransport.barge, ModeOfTransport.deep_sea_vessel, FlowDirection.export_flow], + [ModeOfTransport.barge, ModeOfTransport.feeder, FlowDirection.export_flow], + [ModeOfTransport.train, ModeOfTransport.deep_sea_vessel, FlowDirection.export_flow], + [ModeOfTransport.train, ModeOfTransport.feeder, FlowDirection.export_flow], + + [ModeOfTransport.truck, ModeOfTransport.truck, FlowDirection.undefined], + [ModeOfTransport.truck, ModeOfTransport.barge, FlowDirection.undefined], + [ModeOfTransport.truck, ModeOfTransport.train, FlowDirection.undefined], + [ModeOfTransport.barge, ModeOfTransport.truck, FlowDirection.undefined], + [ModeOfTransport.barge, ModeOfTransport.barge, FlowDirection.undefined], + [ModeOfTransport.barge, ModeOfTransport.train, FlowDirection.undefined], + [ModeOfTransport.train, ModeOfTransport.truck, FlowDirection.undefined], + [ModeOfTransport.train, ModeOfTransport.barge, FlowDirection.undefined], + [ModeOfTransport.train, ModeOfTransport.train, FlowDirection.undefined], + ]) + def test_flow_direction(self, delivered_by, picked_up_by, container_flow): + """Test whether all flow directions are detected correctly""" + container: Container = Container.create( + weight=10, + delivered_by=delivered_by, + picked_up_by=picked_up_by, + picked_up_by_initial=ModeOfTransport.truck, + length=ContainerLength.forty_feet, + storage_requirement=StorageRequirement.standard + ) + self.assertEqual(container_flow, container.flow_direction) diff --git a/conflowgen/tests/domain_models/test_vehicle.py b/conflowgen/tests/domain_models/test_vehicle.py index ee046b5..2d11790 100644 --- a/conflowgen/tests/domain_models/test_vehicle.py +++ b/conflowgen/tests/domain_models/test_vehicle.py @@ -74,12 +74,12 @@ def test_save_feeder_to_database(self) -> None: vehicle_type=ModeOfTransport.feeder, vehicle_arrives_at=now, average_vehicle_capacity=1100, - average_moved_capacity=200 + average_inbound_container_volume=200 ) lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=1000, - moved_capacity=200, + inbound_container_volume=200, scheduled_arrival=now, schedule=schedule ) @@ -95,12 +95,12 @@ def test_repr(self) -> None: vehicle_type=ModeOfTransport.feeder, vehicle_arrives_at=now, average_vehicle_capacity=1100, - average_moved_capacity=200 + average_inbound_container_volume=200 ) lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=1000, - moved_capacity=200, + inbound_container_volume=200, scheduled_arrival=now, schedule=schedule ) @@ -132,12 +132,12 @@ def test_save_barge_to_database(self) -> None: vehicle_type=ModeOfTransport.barge, vehicle_arrives_at=now, average_vehicle_capacity=1100, - average_moved_capacity=200 + average_inbound_container_volume=200 ) lsv = LargeScheduledVehicle.create( vehicle_name="TestBarge1", capacity_in_teu=1000, - moved_capacity=200, + inbound_container_volume=200, scheduled_arrival=now, schedule=schedule ) @@ -153,12 +153,12 @@ def test_repr(self) -> None: vehicle_type=ModeOfTransport.barge, vehicle_arrives_at=now, average_vehicle_capacity=1100, - average_moved_capacity=200 + average_inbound_container_volume=200 ) lsv = LargeScheduledVehicle.create( vehicle_name="TestBarge1", capacity_in_teu=1000, - moved_capacity=200, + inbound_container_volume=200, scheduled_arrival=now, schedule=schedule ) @@ -177,13 +177,13 @@ def test_get_mode_of_transport(self) -> None: service_name="MyTestBargeLine", vehicle_type=ModeOfTransport.barge, vehicle_arrives_at=now, - average_vehicle_capacity=1100, - average_moved_capacity=200 + average_vehicle_capacity=110, + average_inbound_container_volume=100 ) lsv = LargeScheduledVehicle.create( vehicle_name="TestBarge1", - capacity_in_teu=1000, - moved_capacity=200, + capacity_in_teu=110, + inbound_container_volume=100, scheduled_arrival=now, schedule=schedule ) diff --git a/conflowgen/tests/flow_generator/test_allocate_space_for_containers_delivered_by_truck_service.py b/conflowgen/tests/flow_generator/test_allocate_space_for_containers_delivered_by_truck_service.py index 4e74955..8f5bbbb 100644 --- a/conflowgen/tests/flow_generator/test_allocate_space_for_containers_delivered_by_truck_service.py +++ b/conflowgen/tests/flow_generator/test_allocate_space_for_containers_delivered_by_truck_service.py @@ -69,21 +69,18 @@ def _create_feeder(scheduled_arrival: datetime.datetime) -> Feeder: vehicle_arrives_at=scheduled_arrival.date(), vehicle_arrives_at_time=scheduled_arrival.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) - schedule.save() feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=scheduled_arrival, schedule=schedule ) - feeder_lsv.save() feeder = Feeder.create( large_scheduled_vehicle=feeder_lsv ) - feeder.save() return feeder @staticmethod @@ -94,21 +91,18 @@ def _create_train(scheduled_arrival: datetime.datetime) -> Train: vehicle_arrives_at=scheduled_arrival.date(), vehicle_arrives_at_time=scheduled_arrival.time(), average_vehicle_capacity=90, - average_moved_capacity=90, + average_inbound_container_volume=90, ) - schedule.save() train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=96, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=scheduled_arrival, schedule=schedule ) - train_lsv.save() train = Train.create( large_scheduled_vehicle=train_lsv ) - train.save() return train @staticmethod @@ -151,7 +145,6 @@ def _create_container_for_large_scheduled_vehicle(vehicle: AbstractLargeSchedule picked_up_by=ModeOfTransport.feeder, picked_up_by_initial=ModeOfTransport.feeder ) - container.save() return container def test_does_nothing_if_no_vehicle_is_available(self): diff --git a/conflowgen/tests/flow_generator/test_assign_destination_to_container_service.py b/conflowgen/tests/flow_generator/test_assign_destination_to_container_service.py index 6818e4c..c7f9717 100644 --- a/conflowgen/tests/flow_generator/test_assign_destination_to_container_service.py +++ b/conflowgen/tests/flow_generator/test_assign_destination_to_container_service.py @@ -48,12 +48,12 @@ def _create_feeder(scheduled_arrival: datetime.datetime) -> Feeder: vehicle_arrives_at=scheduled_arrival.date(), vehicle_arrives_at_time=scheduled_arrival.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=scheduled_arrival, schedule=schedule ) @@ -70,12 +70,12 @@ def _create_train(scheduled_arrival: datetime.datetime) -> Train: vehicle_arrives_at=scheduled_arrival.date(), vehicle_arrives_at_time=scheduled_arrival.time(), average_vehicle_capacity=90, - average_moved_capacity=90, + average_inbound_container_volume=90, ) train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=96, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=scheduled_arrival, schedule=schedule ) diff --git a/conflowgen/tests/flow_generator/test_container_flow_generator_service__generate.py b/conflowgen/tests/flow_generator/test_container_flow_generator_service__generate.py index 1bb2ded..fbdb1d8 100644 --- a/conflowgen/tests/flow_generator/test_container_flow_generator_service__generate.py +++ b/conflowgen/tests/flow_generator/test_container_flow_generator_service__generate.py @@ -7,6 +7,7 @@ from conflowgen.application.repositories.container_flow_generation_properties_repository import \ ContainerFlowGenerationPropertiesRepository from conflowgen.database_connection.create_tables import create_tables +from conflowgen.domain_models.container import Container from conflowgen.domain_models.distribution_models.container_dwell_time_distribution import \ ContainerDwellTimeDistribution from conflowgen.domain_models.distribution_models.mode_of_transport_distribution import ModeOfTransportDistribution @@ -49,22 +50,22 @@ def test_happy_path_no_mocking(self): create_tables(self.sqlite_db) seed_all_distributions() port_call_manager = PortCallManager() - port_call_manager.add_vehicle( + port_call_manager.add_service_that_calls_terminal( vehicle_type=ModeOfTransport.feeder, service_name="TestFeeder", vehicle_arrives_at=datetime.date(2021, 7, 9), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=100, + average_inbound_container_volume=100, next_destinations=None ) - port_call_manager.add_vehicle( + port_call_manager.add_service_that_calls_terminal( vehicle_type=ModeOfTransport.deep_sea_vessel, service_name="TestDeepSeaVessel", vehicle_arrives_at=datetime.date(2021, 7, 9), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=100, + average_inbound_container_volume=100, next_destinations=None ) self.container_flow_generator_service.generate() @@ -73,3 +74,61 @@ def test_nothing_to_do(self): create_tables(self.sqlite_db) seed_all_distributions() self.container_flow_generator_service.generate() + + def test_happy_path_no_mocking_with_ramp_up_and_ramp_down(self): + create_tables(self.sqlite_db) + seed_all_distributions() + + container_flow_generation_properties_manager = ContainerFlowGenerationPropertiesRepository() + properties: ContainerFlowGenerationProperties = (container_flow_generation_properties_manager + .get_container_flow_generation_properties()) + properties.ramp_up_period = 5 + properties.ramp_down_period = 5 + container_flow_generation_properties_manager.set_container_flow_generation_properties(properties) + + port_call_manager = PortCallManager() + port_call_manager.add_service_that_calls_terminal( + vehicle_type=ModeOfTransport.feeder, + service_name="TestFeeder", + vehicle_arrives_at=properties.start_date + datetime.timedelta(days=3), + vehicle_arrives_at_time=datetime.time(11), + average_vehicle_capacity=800, + average_inbound_container_volume=50, + next_destinations=None + ) + port_call_manager.add_service_that_calls_terminal( + vehicle_type=ModeOfTransport.deep_sea_vessel, + service_name="TestDeepSeaVessel2", + vehicle_arrives_at=properties.end_date - datetime.timedelta(days=2), + vehicle_arrives_at_time=datetime.time(11), + average_vehicle_capacity=12000, + average_inbound_container_volume=100, + next_destinations=None + ) + self.container_flow_generator_service.generate() + + # Vehicle 1 - inbound during ramp-up untouched + number_containers_during_ramp_up = Container.select().where( + Container.delivered_by == ModeOfTransport.feeder + ).count() + self.assertLess( + number_containers_during_ramp_up, + 100 + ) + self.assertGreater( + number_containers_during_ramp_up, + 50 + ) + + # Vehicle 2 - inbound volume during ramp-down throttled + number_containers_during_ramp_down = Container.select().where( + Container.delivered_by == ModeOfTransport.deep_sea_vessel + ).count() + self.assertLess( + number_containers_during_ramp_down, + 150 + ) + self.assertGreater( + number_containers_during_ramp_down, + 50 + ) diff --git a/conflowgen/tests/flow_generator/test_export_container_flow_service__container.py b/conflowgen/tests/flow_generator/test_export_container_flow_service__container.py index 3963a61..efdd5c5 100644 --- a/conflowgen/tests/flow_generator/test_export_container_flow_service__container.py +++ b/conflowgen/tests/flow_generator/test_export_container_flow_service__container.py @@ -113,7 +113,7 @@ def test_convert_table_to_pandas_dataframe_with_container_with_destination(self) vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) destination = Destination.create( diff --git a/conflowgen/tests/flow_generator/test_large_scheduled_vehicle_for_onward_transportation_manager.py b/conflowgen/tests/flow_generator/test_large_scheduled_vehicle_for_onward_transportation_manager.py index 55286a7..daf4e8d 100644 --- a/conflowgen/tests/flow_generator/test_large_scheduled_vehicle_for_onward_transportation_manager.py +++ b/conflowgen/tests/flow_generator/test_large_scheduled_vehicle_for_onward_transportation_manager.py @@ -68,21 +68,18 @@ def _create_feeder( vehicle_arrives_at=scheduled_arrival.date(), vehicle_arrives_at_time=scheduled_arrival.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) - schedule.save() feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=scheduled_arrival, schedule=schedule ) - feeder_lsv.save() feeder = Feeder.create( large_scheduled_vehicle=feeder_lsv ) - feeder.save() return feeder @staticmethod @@ -93,12 +90,12 @@ def _create_train(scheduled_arrival: datetime.datetime, service_suffix: str = "" vehicle_arrives_at=scheduled_arrival.date(), vehicle_arrives_at_time=scheduled_arrival.time(), average_vehicle_capacity=90, - average_moved_capacity=90, + average_inbound_container_volume=90, ) train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=96, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=scheduled_arrival, schedule=schedule ) @@ -170,7 +167,7 @@ def test_load_container_from_feeder_to_feeder(self): def test_do_not_overload_feeder_with_truck_traffic(self): truck = self._create_truck(datetime.datetime(year=2021, month=8, day=5, hour=9, minute=0)) feeder = self._create_feeder(datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15)) - feeder.large_scheduled_vehicle.moved_capacity = 10 # in TEU + feeder.large_scheduled_vehicle.inbound_container_volume = 10 # in TEU containers = [self._create_container_for_truck(truck) for _ in range(10)] self.assertEqual(Container.select().count(), 10) teu_generated = sum((ContainerLength.get_teu_factor(container.length) for container in containers)) @@ -194,11 +191,11 @@ def test_do_not_overload_feeder_with_train_traffic(self): train = self._create_train(datetime.datetime(year=2021, month=8, day=5, hour=9, minute=0)) containers = [ self._create_container_for_large_scheduled_vehicle(train) - for _ in range(train.large_scheduled_vehicle.moved_capacity) # here only 20' containers + for _ in range(train.large_scheduled_vehicle.inbound_container_volume) # here only 20' containers ] feeder = self._create_feeder(datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15)) - feeder.large_scheduled_vehicle.moved_capacity = 80 # in TEU + feeder.large_scheduled_vehicle.inbound_container_volume = 80 # in TEU feeder.save() self.assertEqual(Container.select().count(), 90) @@ -222,11 +219,11 @@ def test_do_not_load_if_the_time_span_is_too_long(self): train = self._create_train(datetime.datetime(year=2021, month=8, day=5, hour=9, minute=0)) containers = [ self._create_container_for_large_scheduled_vehicle(train) - for _ in range(train.large_scheduled_vehicle.moved_capacity) # here only 20' containers + for _ in range(train.large_scheduled_vehicle.inbound_container_volume) # here only 20' containers ] feeder = self._create_feeder(datetime.datetime(year=2022, month=8, day=7, hour=13, minute=15)) - feeder.large_scheduled_vehicle.moved_capacity = 80 # in TEU + feeder.large_scheduled_vehicle.inbound_container_volume = 80 # in TEU feeder.save() self.assertEqual(Container.select().count(), 90) @@ -245,16 +242,16 @@ def test_do_not_overload_feeder_with_train_traffic_of_two_vehicles(self): train_2 = self._create_train(datetime.datetime(year=2021, month=8, day=5, hour=15, minute=0), "2") containers_1 = [ self._create_container_for_large_scheduled_vehicle(train_1) - for _ in range(train_1.large_scheduled_vehicle.moved_capacity) # here only 20' containers + for _ in range(train_1.large_scheduled_vehicle.inbound_container_volume) # here only 20' containers ] containers_2 = [ self._create_container_for_large_scheduled_vehicle(train_2) - for _ in range(train_2.large_scheduled_vehicle.moved_capacity) # here only 20' containers + for _ in range(train_2.large_scheduled_vehicle.inbound_container_volume) # here only 20' containers ] containers = containers_1 + containers_2 feeder = self._create_feeder(datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15)) - feeder.large_scheduled_vehicle.moved_capacity = 80 # in TEU + feeder.large_scheduled_vehicle.inbound_container_volume = 80 # in TEU feeder.save() self.assertEqual(Container.select().count(), 180) @@ -269,7 +266,7 @@ def test_do_not_overload_feeder_with_train_traffic_of_two_vehicles(self): self.assertTrue(set(containers_reloaded).issubset(set(containers)), "Feeder must only load generated " "containers") teu_loaded = 0 - for container in containers_reloaded: # pylint: disable=not-an-iterable + for container in containers_reloaded: # pylint: disable=not-an-iterable self.assertEqual(container.picked_up_by_large_scheduled_vehicle, feeder.large_scheduled_vehicle) teu_loaded += ContainerLength.get_teu_factor(container.length) self.assertLessEqual(teu_loaded, 80, "Feeder must not be loaded with more than what it can carry") @@ -279,11 +276,11 @@ def test_do_not_overload_feeder_with_train_traffic_of_two_vehicles_and_changing_ train_2 = self._create_train(datetime.datetime(year=2021, month=8, day=5, hour=15, minute=0), "2") containers_1 = [ self._create_container_for_large_scheduled_vehicle(train_1) - for _ in range(train_1.large_scheduled_vehicle.moved_capacity) # here only 20' containers + for _ in range(train_1.large_scheduled_vehicle.inbound_container_volume) # here only 20' containers ] containers_2 = [ self._create_container_for_large_scheduled_vehicle(train_2) - for _ in range(train_2.large_scheduled_vehicle.moved_capacity) # here only 20' containers + for _ in range(train_2.large_scheduled_vehicle.inbound_container_volume) # here only 20' containers ] for container in containers_2: container.length = ContainerLength.forty_feet @@ -291,7 +288,7 @@ def test_do_not_overload_feeder_with_train_traffic_of_two_vehicles_and_changing_ containers = containers_1 + containers_2 feeder = self._create_feeder(datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15)) - feeder.large_scheduled_vehicle.moved_capacity = 80 # in TEU + feeder.large_scheduled_vehicle.inbound_container_volume = 80 # in TEU feeder.save() self.assertEqual(Container.select().count(), 180) @@ -316,3 +313,135 @@ def test_nothing_to_do(self): self.manager.schedule_repository, "get_departing_vehicles", return_value=None) as get_vehicles_method: self.manager.choose_departing_vehicle_for_containers() get_vehicles_method.assert_not_called() + + def test_behavior_during_ramp_up_period(self): + """During ramp-up, the capacity of vessels is artificially reduced""" + + # the + feeder_1 = self._create_feeder( + datetime.datetime(year=2022, month=8, day=7, hour=13, minute=15), "1" + ) + + feeder_2 = self._create_feeder( + datetime.datetime(year=2021, month=8, day=10, hour=15, minute=0), "2" + ) + + feeder_1.large_scheduled_vehicle.inbound_container_volume = 100 # in TEU + feeder_1.save() + + containers = [ + self._create_container_for_large_scheduled_vehicle(feeder_1) + for _ in range(feeder_1.large_scheduled_vehicle.inbound_container_volume) # here only 20' containers + ] + + self.manager.reload_properties( + transportation_buffer=0, + ramp_up_period_end=datetime.date(2021, 8, 12) + ) + + # run actual function + self.manager.choose_departing_vehicle_for_containers() + + containers_reloaded: Iterable[Container] = Container.select().where( + Container.picked_up_by_large_scheduled_vehicle == feeder_2 + ) + self.assertTrue(set(containers_reloaded).issubset(set(containers)), "Feeder must only load generated " + "containers") + + teu_loaded = 0 + for container in containers_reloaded: # pylint: disable=not-an-iterable + self.assertEqual(container.picked_up_by_large_scheduled_vehicle, feeder_2.large_scheduled_vehicle) + teu_loaded += ContainerLength.get_teu_factor(container.length) + self.assertLessEqual(teu_loaded, 30, "Feeder must have loaded much less containers because this is the" + "ramp-up period!") + + def test_outbound_flow_unaffected_during_ramp_down_period(self): + + # Create feeders (vessels) with specific departure times + feeder_1 = self._create_feeder( + datetime.datetime(year=2024, month=8, day=15, hour=10, minute=0), "1" + ) + + feeder_2 = self._create_feeder( + datetime.datetime(year=2024, month=8, day=16, hour=14, minute=30), "2" + ) + + # Set inbound container volume for feeder_1 + feeder_1.large_scheduled_vehicle.inbound_container_volume = 50 # in TEU + feeder_1.save() + + # Create containers associated with feeder_1 + containers = [ + self._create_container_for_large_scheduled_vehicle(feeder_1) + for _ in range(feeder_1.large_scheduled_vehicle.inbound_container_volume) + ] + + # Set system properties to simulate a ramp-down period + self.manager.reload_properties( + transportation_buffer=0, + ramp_down_period_start=datetime.date(2023, 8, 14), + ) + + # Run the function responsible for choosing the departing vehicle + self.manager.choose_departing_vehicle_for_containers() + + # Query the containers that were picked up by feeder_2 + containers_reloaded: Iterable[Container] = Container.select().where( + Container.picked_up_by_large_scheduled_vehicle == feeder_2 + ) + + # Ensure that all containers from feeder_1 are transshipped to feeder_2 without issues + self.assertTrue(set(containers_reloaded).issubset(set(containers)), + "Transshipment flows should not be affected during ramp-down period!") + + teu_loaded = 0 + for container in containers_reloaded: # pylint: disable=not-an-iterable + self.assertEqual(container.picked_up_by_large_scheduled_vehicle, feeder_2.large_scheduled_vehicle) + teu_loaded += ContainerLength.get_teu_factor(container.length) + + # Assert that the TEU loaded on feeder_2 is as expected, meaning no transshipment disruptions + self.assertEqual(teu_loaded, feeder_1.large_scheduled_vehicle.inbound_container_volume, + "Feeder 2 must have loaded all containers from feeder 1 during the ramp-down period!") + + def test_ramp_down_period_only_10_percent_unloaded(self): + """During ramp-down, only 10% of containers should be unloaded from the incoming vessel.""" + + # Create an incoming deep-sea vessel with a scheduled arrival time + feeder = self._create_feeder( + datetime.datetime(year=2023, month=8, day=15, hour=8, minute=0) + ) + + # Set inbound container volume for the deep-sea vessel + feeder.inbound_container_volume = 100 # in TEU + feeder.save() + + # Create containers associated with the deep-sea vessel + containers = [ + self._create_container_for_large_scheduled_vehicle(feeder) + for _ in range(feeder.inbound_container_volume) + ] + + # Set system properties to simulate a ramp-down period + self.manager.reload_properties( + transportation_buffer=0, + ramp_down_period_start=datetime.date(2023, 8, 15), + ) + + # Run the function responsible for unloading containers during ramp-down + self.manager.choose_departing_vehicle_for_containers() + + # Query the containers that were unloaded + containers_departed: Iterable[Container] = Container.select().where( + Container.picked_up_by_large_scheduled_vehicle == feeder + ) + + # Validate that only the expected containers are unloaded + self.assertTrue(set(containers_departed).issubset(set(containers)), + "Only containers from the deep-sea vessel should be unloaded during ramp-down.") + + # Calculate the expected number of containers to be departed (90% of total) + expected_departed_containers = int(feeder.inbound_container_volume * 0.9) + + # Ensure that the number of departed containers is 90% of the total inbound container volume + self.assertEqual(Container.delivered_by_large_scheduled_vehicle, expected_departed_containers, + "During ramp-down, exactly 10% of containers should be unloaded.") diff --git a/conflowgen/tests/flow_generator/test_truck_for_export_containers_manager.py b/conflowgen/tests/flow_generator/test_truck_for_export_containers_manager.py index 4bb684c..0204345 100644 --- a/conflowgen/tests/flow_generator/test_truck_for_export_containers_manager.py +++ b/conflowgen/tests/flow_generator/test_truck_for_export_containers_manager.py @@ -255,12 +255,12 @@ def test_happy_path(self): vehicle_arrives_at=container_arrival_time.date(), vehicle_arrives_at_time=container_arrival_time.time(), average_vehicle_capacity=5000, - average_moved_capacity=1200, + average_inbound_container_volume=1200, ) lsv = LargeScheduledVehicle.create( vehicle_name="TestDeepSeaVessel", capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=container_arrival_time, schedule=schedule ) diff --git a/conflowgen/tests/flow_generator/test_truck_for_import_containers_manager.py b/conflowgen/tests/flow_generator/test_truck_for_import_containers_manager.py index 515cbf4..a534655 100644 --- a/conflowgen/tests/flow_generator/test_truck_for_import_containers_manager.py +++ b/conflowgen/tests/flow_generator/test_truck_for_import_containers_manager.py @@ -275,12 +275,12 @@ def test_happy_path(self): vehicle_arrives_at=container_arrival_time.date(), vehicle_arrives_at_time=container_arrival_time.time(), average_vehicle_capacity=5000, - average_moved_capacity=1200, + average_inbound_container_volume=1200, ) lsv = LargeScheduledVehicle.create( vehicle_name="TestDeepSeaVessel", capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=container_arrival_time, schedule=schedule ) diff --git a/conflowgen/tests/notebooks/analyses_with_missing_data.ipynb b/conflowgen/tests/notebooks/analyses_with_missing_data.ipynb index 8bf2826..4c03414 100644 --- a/conflowgen/tests/notebooks/analyses_with_missing_data.ipynb +++ b/conflowgen/tests/notebooks/analyses_with_missing_data.ipynb @@ -197,7 +197,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.1" + "version": "3.9.7" } }, "nbformat": 4, diff --git a/conflowgen/tests/previews/test_container_flow_by_vehicle_type_preview.py b/conflowgen/tests/previews/test_container_flow_by_vehicle_type_preview.py index 77fd501..7b2b02a 100644 --- a/conflowgen/tests/previews/test_container_flow_by_vehicle_type_preview.py +++ b/conflowgen/tests/previews/test_container_flow_by_vehicle_type_preview.py @@ -99,7 +99,7 @@ def test_with_single_arrival_schedules(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) schedule.save() diff --git a/conflowgen/tests/previews/test_container_flow_by_vehicle_type_preview_report.py b/conflowgen/tests/previews/test_container_flow_by_vehicle_type_preview_report.py index 3734cf9..93aed9a 100644 --- a/conflowgen/tests/previews/test_container_flow_by_vehicle_type_preview_report.py +++ b/conflowgen/tests/previews/test_container_flow_by_vehicle_type_preview_report.py @@ -123,7 +123,7 @@ def test_inbound_with_single_arrival_schedules(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) actual_report = self.preview_report.get_report_as_text() @@ -172,7 +172,7 @@ def test_report_with_schedules_as_graph(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) fig = self.preview_report.get_report_as_graph() diff --git a/conflowgen/tests/previews/test_inbound_and_outbound_vehicle_capacity_preview.py b/conflowgen/tests/previews/test_inbound_and_outbound_vehicle_capacity_preview.py index 7e7e340..cb45034 100644 --- a/conflowgen/tests/previews/test_inbound_and_outbound_vehicle_capacity_preview.py +++ b/conflowgen/tests/previews/test_inbound_and_outbound_vehicle_capacity_preview.py @@ -82,7 +82,7 @@ def setUp(self) -> None: def test_inbound_with_no_schedules(self): """If no schedules are provided, no capacity is needed""" - empty_capacity = self.preview.get_inbound_capacity_of_vehicles().teu + empty_capacity: dict = self.preview.get_inbound_capacity_of_vehicles().teu self.assertSetEqual(set(ModeOfTransport), set(empty_capacity.keys())) for mode_of_transport in ModeOfTransport: capacity_in_teu = empty_capacity[mode_of_transport] @@ -96,7 +96,7 @@ def test_inbound_with_single_arrival_schedules(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) capacity_with_one_feeder = self.preview.get_inbound_capacity_of_vehicles().teu @@ -124,7 +124,7 @@ def test_inbound_with_several_arrivals_schedules(self): vehicle_arrives_at=two_days_later.date(), vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300 + average_inbound_container_volume=300 ) capacity_with_one_feeder = self.preview.get_inbound_capacity_of_vehicles().teu self.assertSetEqual(set(ModeOfTransport), set(capacity_with_one_feeder.keys())) @@ -151,7 +151,7 @@ def test_outbound_average_capacity_with_several_arrivals_schedules(self): vehicle_arrives_at=two_days_later.date(), vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300 + average_inbound_container_volume=300 ) capacities = self.preview.get_outbound_capacity_of_vehicles() capacity_with_one_feeder = capacities.used.teu @@ -183,7 +183,7 @@ def test_outbound_maximum_capacity_with_several_arrivals_schedules(self): vehicle_arrives_at=two_days_later.date(), vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300 + average_inbound_container_volume=300 ) capacity_with_one_feeder = self.preview.get_outbound_capacity_of_vehicles().maximum.teu self.assertSetEqual(set(ModeOfTransport), set(capacity_with_one_feeder.keys())) diff --git a/conflowgen/tests/previews/test_inbound_and_outbound_vehicle_capacity_preview_report.py b/conflowgen/tests/previews/test_inbound_and_outbound_vehicle_capacity_preview_report.py index 0148dba..6963d7c 100644 --- a/conflowgen/tests/previews/test_inbound_and_outbound_vehicle_capacity_preview_report.py +++ b/conflowgen/tests/previews/test_inbound_and_outbound_vehicle_capacity_preview_report.py @@ -103,7 +103,7 @@ def test_inbound_with_single_arrival_schedules(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) actual_report = self.preview_report.get_report_as_text() @@ -132,7 +132,7 @@ def test_report_with_schedules_as_graph(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) fig = self.preview_report.get_report_as_graph() diff --git a/conflowgen/tests/previews/test_modal_split_preview__get_modal_split_for_hinterland.py b/conflowgen/tests/previews/test_modal_split_preview__get_modal_split_for_hinterland.py index ea168ae..33be55e 100644 --- a/conflowgen/tests/previews/test_modal_split_preview__get_modal_split_for_hinterland.py +++ b/conflowgen/tests/previews/test_modal_split_preview__get_modal_split_for_hinterland.py @@ -97,7 +97,7 @@ def test_with_single_arrival_schedules(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) schedule.save() diff --git a/conflowgen/tests/previews/test_modal_split_preview__get_transshipment.py b/conflowgen/tests/previews/test_modal_split_preview__get_transshipment.py index f6950b4..3a59b87 100644 --- a/conflowgen/tests/previews/test_modal_split_preview__get_transshipment.py +++ b/conflowgen/tests/previews/test_modal_split_preview__get_transshipment.py @@ -92,7 +92,7 @@ def test_with_single_arrival_schedules(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) actual_split = self.preview.get_transshipment_and_hinterland_split() diff --git a/conflowgen/tests/previews/test_modal_split_preview_report.py b/conflowgen/tests/previews/test_modal_split_preview_report.py index 0c84cb4..6035915 100644 --- a/conflowgen/tests/previews/test_modal_split_preview_report.py +++ b/conflowgen/tests/previews/test_modal_split_preview_report.py @@ -113,7 +113,7 @@ def test_inbound_with_single_arrival_schedules(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) actual_report = self.preview_report.get_report_as_text() @@ -154,7 +154,7 @@ def test_report_with_schedules_as_graph(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) axes = self.preview_report.get_report_as_graph() diff --git a/conflowgen/tests/previews/test_quay_side_throughput_preview.py b/conflowgen/tests/previews/test_quay_side_throughput_preview.py index fa070c1..4bbd353 100644 --- a/conflowgen/tests/previews/test_quay_side_throughput_preview.py +++ b/conflowgen/tests/previews/test_quay_side_throughput_preview.py @@ -103,7 +103,7 @@ def test_one_feeder(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=150, + average_inbound_container_volume=150, vehicle_arrives_every_k_days=-1 ) volume = self.preview.get_quay_side_throughput() diff --git a/conflowgen/tests/previews/test_quay_side_throughput_preview_report.py b/conflowgen/tests/previews/test_quay_side_throughput_preview_report.py index 7e89322..a5622db 100644 --- a/conflowgen/tests/previews/test_quay_side_throughput_preview_report.py +++ b/conflowgen/tests/previews/test_quay_side_throughput_preview_report.py @@ -92,7 +92,7 @@ def test_report_with_schedules_as_graph(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) axes = self.preview_report.get_report_as_graph() @@ -108,7 +108,7 @@ def test_text_report(self): vehicle_arrives_every_k_days=-1, vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=24000, - average_moved_capacity=24000 + average_inbound_container_volume=24000 ) report = self.preview_report.get_report_as_text() # flake8: noqa: W291 (ignore trailing whitespace in text report) diff --git a/conflowgen/tests/previews/test_truck_gate_throughput_preview.py b/conflowgen/tests/previews/test_truck_gate_throughput_preview.py index 5244710..ba8fd2c 100644 --- a/conflowgen/tests/previews/test_truck_gate_throughput_preview.py +++ b/conflowgen/tests/previews/test_truck_gate_throughput_preview.py @@ -97,7 +97,7 @@ def test_get_total_trucks(self): vehicle_arrives_every_k_days=-1, vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300 + average_inbound_container_volume=300 ) # pylint: disable=protected-access total_trucks = self.preview._get_total_trucks() @@ -118,7 +118,7 @@ def test_get_weekly_trucks(self): vehicle_arrives_every_k_days=-1, vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300 + average_inbound_container_volume=300 ) weekly_trucks = self.preview._get_number_of_trucks_per_week() # 60 trucks total (from test_get_total_trucks above) @@ -137,7 +137,7 @@ def test_get_truck_distribution(self): vehicle_arrives_every_k_days=-1, vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300 + average_inbound_container_volume=300 ) weekly_truck_distribution = self.preview.get_weekly_truck_arrivals(True, False) self.assertEqual(weekly_truck_distribution, {3: 6, 4: 24}) diff --git a/conflowgen/tests/previews/test_truck_gate_throughput_preview_report.py b/conflowgen/tests/previews/test_truck_gate_throughput_preview_report.py index f1460e9..f94d173 100644 --- a/conflowgen/tests/previews/test_truck_gate_throughput_preview_report.py +++ b/conflowgen/tests/previews/test_truck_gate_throughput_preview_report.py @@ -261,7 +261,7 @@ def test_report_with_schedule(self): vehicle_arrives_every_k_days=-1, vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=24000, - average_moved_capacity=24000 + average_inbound_container_volume=24000 ) report = self.preview_report.get_report_as_graph() self.assertIsNotNone(report) @@ -276,7 +276,7 @@ def test_text_report(self): vehicle_arrives_every_k_days=-1, vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=24000, - average_moved_capacity=24000 + average_inbound_container_volume=24000 ) report = self.preview_report.get_report_as_text() # flake8: noqa: W291 (ignore trailing whitespace in text report) diff --git a/conflowgen/tests/previews/test_vehicle_capacity_exceeded_preview.py b/conflowgen/tests/previews/test_vehicle_capacity_exceeded_preview.py index 6223bd3..1f6e4b5 100644 --- a/conflowgen/tests/previews/test_vehicle_capacity_exceeded_preview.py +++ b/conflowgen/tests/previews/test_vehicle_capacity_exceeded_preview.py @@ -113,7 +113,7 @@ def test_with_single_arrival_schedules(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) schedule.save() diff --git a/conflowgen/tests/previews/test_vehicle_capacity_exceeded_preview_report.py b/conflowgen/tests/previews/test_vehicle_capacity_exceeded_preview_report.py index 48e3e5b..3aae61d 100644 --- a/conflowgen/tests/previews/test_vehicle_capacity_exceeded_preview_report.py +++ b/conflowgen/tests/previews/test_vehicle_capacity_exceeded_preview_report.py @@ -105,7 +105,7 @@ def test_inbound_with_single_arrival_schedules(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) actual_report = self.preview_report.get_report_as_text() @@ -134,7 +134,7 @@ def test_report_with_schedules_as_graph(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) fig = self.preview_report.get_report_as_graph() diff --git a/docs/api.rst b/docs/api.rst index 68cde3e..b0b58ea 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -143,6 +143,12 @@ Running analyses .. autoclass:: conflowgen.ContainerFlowAdjustmentByVehicleTypeAnalysisSummaryReport :members: +.. autoclass:: conflowgen.ContainerFlowByVehicleInstanceAnalysis + :members: + +.. autoclass:: conflowgen.ContainerFlowByVehicleInstanceAnalysisReport + :members: + .. autoclass:: conflowgen.ContainerFlowByVehicleTypeAnalysis :members: @@ -155,10 +161,10 @@ Running analyses .. autoclass:: conflowgen.InboundAndOutboundVehicleCapacityAnalysisReport :members: -.. autoclass:: conflowgen.InboundToOutboundVehicleCapacityUtilizationAnalysis +.. autoclass:: conflowgen.OutboundToInboundVehicleCapacityUtilizationAnalysis :members: -.. autoclass:: conflowgen.InboundToOutboundVehicleCapacityUtilizationAnalysisReport +.. autoclass:: conflowgen.OutboundToInboundVehicleCapacityUtilizationAnalysisReport :members: .. autoclass:: conflowgen.ModalSplitAnalysis diff --git a/docs/background.rst b/docs/background.rst index 497fbc8..0326e22 100644 --- a/docs/background.rst +++ b/docs/background.rst @@ -220,7 +220,8 @@ If you just need a BibTeX entry for your citation software, this one should do t doi = {10.1007/978-3-031-05359-7_11}, month = {2}, pages = {133--143}, - publisher = {Springer Cham}, + publisher = {Springer International Publishing}, + address = {Cham, CH}, series = {Lecture Notes in Logistics}, title = {Container Flow Generation for Maritime Container Terminals}, year = {2022} diff --git a/docs/notebooks/analyses.ipynb b/docs/notebooks/analyses.ipynb index 9b3ff56..a686bd9 100644 --- a/docs/notebooks/analyses.ipynb +++ b/docs/notebooks/analyses.ipynb @@ -289,12 +289,12 @@ "metadata": {}, "outputs": [], "source": [ - "vehicle_capacity_utilization_report = (\n", - " conflowgen.InboundToOutboundVehicleCapacityUtilizationAnalysisReport()\n", + "outbound_to_inbound_vehicle_capacity_utilization_report = (\n", + " conflowgen.OutboundToInboundVehicleCapacityUtilizationAnalysisReport()\n", ")\n", "\n", "print(\n", - " vehicle_capacity_utilization_report.get_report_as_text(\n", + " outbound_to_inbound_vehicle_capacity_utilization_report.get_report_as_text(\n", " vehicle_type={\n", " conflowgen.ModeOfTransport.deep_sea_vessel,\n", " conflowgen.ModeOfTransport.feeder,\n", diff --git a/docs/notebooks/first_steps.ipynb b/docs/notebooks/first_steps.ipynb index 5ececb9..bcc31d3 100644 --- a/docs/notebooks/first_steps.ipynb +++ b/docs/notebooks/first_steps.ipynb @@ -209,7 +209,7 @@ }, "source": [ "By using\n", - ":meth:`.PortCallManager.add_vehicle`,\n", + ":meth:`.PortCallManager.add_service_that_calls_terminal`,\n", "we can define the attributes for our feeder service.\n", "\n", "- ``vehicle_type`` defines, that we deal with a feeder as the mode of transport.\n", @@ -221,7 +221,7 @@ " This parameter must be a :py:obj:`datetime.time`.\n", "- ``average_vehicle_capacity`` defines the average capacity of the vessels utilized on this line.\n", " Parameter must be :py:obj:`int` or :py:obj:`float`.\n", - "- ``average_moved_capacity`` sets the capacity which is in average moved between the feeder and the terminal at each call.\n", + "- ``average_inbound_container_volume`` sets the capacity which is in average moved between the feeder and the terminal at each call.\n", " Parameter must be :py:obj:`int` or :py:obj:`float`.\n", "- ``next_destinations`` can be set, consisting of name and frequency which can, e.g., be used as implication for storage and stacking problems.\n", " A list of name-frequency pairs is expected here." @@ -234,13 +234,13 @@ "metadata": {}, "outputs": [], "source": [ - "port_call_manager.add_vehicle(\n", + "port_call_manager.add_service_that_calls_terminal(\n", " vehicle_type=conflowgen.ModeOfTransport.feeder,\n", " service_name=feeder_service_name,\n", " vehicle_arrives_at=datetime.date(2021, 7, 9),\n", " vehicle_arrives_at_time=datetime.time(11),\n", " average_vehicle_capacity=800,\n", - " average_moved_capacity=100,\n", + " average_inbound_container_volume=100,\n", " next_destinations=[\n", " (\"DEBRV\", 0.4), # 40% of the containers go here...\n", " (\"RULED\", 0.6) # and the other 60% of the containers go here.\n", @@ -263,13 +263,13 @@ "metadata": {}, "outputs": [], "source": [ - "port_call_manager.add_vehicle(\n", + "port_call_manager.add_service_that_calls_terminal(\n", " vehicle_type=conflowgen.ModeOfTransport.train,\n", " service_name=\"JR03A\",\n", " vehicle_arrives_at=datetime.date(2021, 7, 12),\n", " vehicle_arrives_at_time=datetime.time(17),\n", " average_vehicle_capacity=90,\n", - " average_moved_capacity=90,\n", + " average_inbound_container_volume=90,\n", " next_destinations=None # Here we don't have containers that need to be grouped by destination\n", ")" ] @@ -281,13 +281,13 @@ "metadata": {}, "outputs": [], "source": [ - "port_call_manager.add_vehicle(\n", + "port_call_manager.add_service_that_calls_terminal(\n", " vehicle_type=conflowgen.ModeOfTransport.deep_sea_vessel,\n", " service_name=\"LX050\",\n", " vehicle_arrives_at=datetime.date(2021, 7, 10),\n", " vehicle_arrives_at_time=datetime.time(19),\n", " average_vehicle_capacity=16000,\n", - " average_moved_capacity=150, # for faster demo\n", + " average_inbound_container_volume=150, # to speed up the code\n", " next_destinations=[\n", " (\"ZADUR\", 0.3), # 30% of the containers go to ZADUR...\n", " (\"CNSHG\", 0.7) # and the other 70% of the containers go to CNSHG.\n", diff --git a/docs/references.bib b/docs/references.bib index c598277..e81bd73 100644 --- a/docs/references.bib +++ b/docs/references.bib @@ -1,8 +1,8 @@ @online{destatis2021seeschifffahrt, author = {{Statistisches Bundesamt (DESTATIS)}}, title = {{Verkehr : Seeschifffahrt : August 2021}}, - howpublished = {\url{https://www.destatis.de/DE/Themen/Branchen-Unternehmen/Transport-Verkehr/Gueterverkehr/Publikationen/Downloads-Schifffahrt/seeschifffahrt-monat-2080500211084.pdf?__blob=publicationFile}}, - note = {Accessed: 2022-01-22}, + howpublished = {\url{https://www.statistischebibliothek.de/mir/receive/DEHeft_mods_00137854}}, + note = {Accessed: 2024-08-14}, year = 2021 } @@ -50,15 +50,17 @@ @online{macgregor2016containersecuring @inproceedings{kastner2022conflowgen, author = {Kastner, Marvin and Grasse, Ole and Jahn, Carlos}, + editors = {Freitag, Michael and Kinra, Aseem, and Kotzab, Herbert, and Megow, Nicole}, + booktitle = {Dynamics in Logistics. Proceedings of the 8th International Conference {LDIC} 2022, {Bremen, Germany}}, + doi = {10.1007/978-3-031-05359-7_11}, + month = {2}, + pages = {133--143}, + publisher = {Springer International Publishing}, + address = {Cham, CH}, + series = {Lecture Notes in Logistics}, title = {Container Flow Generation for Maritime Container Terminals}, - pages = {133 -- 143}, - booktitle = {Dynamics in Logistics. Proceedings of the 8th International Conference LDIC 2022, Bremen, Germany}, - eventdate = {2022-02-23/2022-02-25}, - year = {2022}, - editor = {Freitag, Michael and Kinra, Aseem and Kotzab, Herbert and Megow, Nicole}, - publisher = {Springer}, - address = {Cham, Switzerland}, - doi = {https://doi.org/10.1007/978-3-031-05359-7_11}, + year = 2022, + eventdate = {2022-02-23/2022-02-25} } @article{exposito2012marshalling, @@ -85,7 +87,7 @@ @Article{briskorn2019generator title={A generator for test instances of scheduling problems concerning cranes in transshipment terminals}, journal={OR Spectrum}, year={2019}, - month={Mar}, + month=3, day={01}, volume={41}, number={1}, @@ -115,7 +117,6 @@ @online{meisel2011unified-software } @inproceedings{edes2024estimating, - abstract = {Vessel delays and increased terminal call sizes negatively impact the ability to properly plan daily operations at seaport container terminals. Such traffic patterns lead to, among others, infrequent peak loads at the seaside of container terminals, complicating terminal operations. Thus, relying on annual or monthly statistics fails to account for these day-to-day fluctuations. When container terminals are planned, be it a greenfield or brownfield terminal, these variations in operations need to be accounted for. The traditional formula-based approach to design terminals uses annual statistics. In this study, it is first used to produce estimates for the required yard capacity for three existing exemplary container terminals. These are then compared to the results of numerical experiments using the synthetic container flow generator ConFlowGen. The findings reveal that yard capacity requirements fluctuate considerably depending on the timing of vessel arrivals and their call sizes. This dynamic modeling proved particularly beneficial for planning gateway traffic, offering more accurate storage capacity predictions. Suggestions are made for how to further develop ConFlowGen for handling transshipment traffic better in future versions.}, author = {Édes, Luc and Kastner, Marvin and Jahn, Carlos}, title = {On Estimating the Required Yard Capacity for Container Terminals}, url = {https://link.springer.com/chapter/10.1007/978-3-031-56826-8_13}, @@ -126,16 +127,16 @@ @inproceedings{edes2024estimating booktitle = {Dynamics in Logistics}, booksubtitle = {Proceedings of the 9th International Conference LDIC 2024, Bremen, Germany}, year = {2024}, - address = {Cham, DE}, + address = {Cham, CH}, doi = {10.1007/978-3-031-56826-8_13}, } @incollection{kastner2023synthetically, - abstract = {More than 80 % of world trade is delivered via sea, making the maritime supply chain a very important backbone for the economy (UNCTAD 2020). Containerized trade regularly outperforms other types of transport in terms of growth, coinciding with consistent increases of average container vessel sizes (UNCTAD 2020). Container terminal operations are heavily affected by this development, since less but larger port calls create unwanted peaks and stress on the terminals and the hinterland. Not all container terminals are affected equally by the described situation. Economic cycles and events such as the global COVID-19 pandemic or the Russian war in Ukraine change the global supply chains, trade characteristics and transport demands between ports in the world. In 2004, Hartmann proposed an approach to create scenarios for simulation and optimization in the sense of container terminal planning and logistics. Due to the significant changes in maritime trade over the years, a new approach for generating synthetic container flow data became practical. In 2021, we introduced a rethought and reworked approach on this topic.The proposed tool, named ConFlowGen, aims to assist planners, scientists, and other maritime experts with providing comprehensive container flow scenarios based on minimal inputs and assumptions of the user. In this paper, we introduce ConFlowGen's general principle of operation in an exemplary use case in the context of container terminal planning.}, author = {Kastner, Marvin and Grasse, Ole}, title = {{Synthetically generating traffic scenarios for simulation-based container terminal planning}}, - booktitle = {{PIANC Yearbook 2023 [Preprint]}}, - year = {2023}, + booktitle = {{PIANC Yearbook 2023}}, + year = {2024}, + pages = {3--17}, doi = {10.15480/882.5156}, - url = {https://tore.tuhh.de/entities/publication/c7ae66d0-4165-44e9-8481-7057af2bc775} + url = {https://pianc.app.box.com/s/vhe61hcxttpakyihjda76pk552itajmc} } diff --git a/examples/Python_Script/demo_DEHAM_CTA.py b/examples/Python_Script/demo_DEHAM_CTA.py index 65b15e3..922db61 100644 --- a/examples/Python_Script/demo_DEHAM_CTA.py +++ b/examples/Python_Script/demo_DEHAM_CTA.py @@ -24,6 +24,7 @@ import os.path import random import sys +import subprocess import pandas as pd try: @@ -120,6 +121,7 @@ conflowgen.ContainerLength.other: 0 }) + # Add vehicles that frequently visit the terminal. port_call_manager = conflowgen.PortCallManager() @@ -165,9 +167,8 @@ service_name=feeder_vehicle_name, vehicle_arrives_at=vessel_arrives_at_as_datetime_type.date(), vehicle_arrives_at_time=vessel_arrives_at_as_datetime_type.time(), - average_vehicle_capacity=capacity, - average_moved_capacity=moved_capacity, - vehicle_arrives_every_k_days=-1, # single arrival, no frequent schedule + vehicle_capacity=capacity, + inbound_container_volume=moved_capacity, next_destinations=next_ports ) logger.info("Feeder vessels are imported") @@ -215,9 +216,8 @@ service_name=deep_sea_vessel_vehicle_name, vehicle_arrives_at=vessel_arrives_at_as_datetime_type.date(), vehicle_arrives_at_time=vessel_arrives_at_as_datetime_type.time(), - average_vehicle_capacity=capacity, - average_moved_capacity=moved_capacity, - vehicle_arrives_every_k_days=-1, # single arrival, no frequent schedule + vehicle_capacity=capacity, + inbound_container_volume=moved_capacity, next_destinations=next_ports ) logger.info("Deep sea vessels are imported") @@ -249,9 +249,8 @@ service_name=barge_vehicle_name, vehicle_arrives_at=vessel_arrives_at_as_datetime_type.date(), vehicle_arrives_at_time=vessel_arrives_at_as_datetime_type.time(), - average_vehicle_capacity=capacity, - average_moved_capacity=moved_capacity, - vehicle_arrives_every_k_days=-1 # single arrival, no frequent schedule + vehicle_capacity=capacity, + inbound_container_volume=moved_capacity, ) logger.info("Barges are imported") @@ -276,14 +275,13 @@ vehicle_arrives_at_time_as_delta = earliest_time_as_delta + datetime.timedelta(hours=0.5 * drawn_slot) vehicle_arrives_at_time = (datetime.datetime.min + vehicle_arrives_at_time_as_delta).time() logger.info(f"Add train '{train_vehicle_name}' to database") - port_call_manager.add_vehicle( + port_call_manager.add_service_that_calls_terminal( vehicle_type=conflowgen.ModeOfTransport.train, service_name=train_vehicle_name, vehicle_arrives_at=vessel_arrives_at_as_datetime_type.date(), vehicle_arrives_at_time=vehicle_arrives_at_time, average_vehicle_capacity=capacity, - average_moved_capacity=capacity, # discharge everything - vehicle_arrives_every_k_days=7 # weekly arrival + average_inbound_container_volume=capacity, ) logger.info("All trains are imported") @@ -324,4 +322,10 @@ # Gracefully close everything database_chooser.close_current_connection() +logger.info(f"ConFlowGen {conflowgen.__version__} from {conflowgen.__file__} was used.") +try: + last_git_commit = str(subprocess.check_output(["git", "log", "-1"]).strip()) # nosec B607 + logger.info("Used git commit: " + last_git_commit[2:-1]) +except: # pylint: disable=bare-except + logger.debug("The last git commit of this repository could not be retrieved, no further version specification.") logger.info("Demo 'demo_DEHAM_CTA' finished successfully.") diff --git a/examples/Python_Script/demo_DEHAM_CTA__with_ramp_up_and_ramp_down_period.py b/examples/Python_Script/demo_DEHAM_CTA__with_ramp_up_and_ramp_down_period.py new file mode 100644 index 0000000..8292bde --- /dev/null +++ b/examples/Python_Script/demo_DEHAM_CTA__with_ramp_up_and_ramp_down_period.py @@ -0,0 +1,335 @@ +""" +Demo for DEHAM CTA with ramp-up and ramp-down period +==================================================== + +This is a demo based on some publicly available figures, some educated guesses, and some random assumptions due to the +lack of information regarding the Container Terminal Altenwerder (CTA) in the port of Hamburg. While this demo only +poorly reflects processes in place, in addition this is only a (poor) snapshot of what has been happening in July +2021. + +No affiliations with the container terminal operators exist. Then why this example was chosen? ConFlowGen is an +extension of the work of Sönke Hartmann [1] and he presented some sample figures. For showing the similarities and +differences, similar assumptions have been made throughout this demo. + +The intention of this script is to provide a demonstration of how ConFlowGen is supposed to be used as a library. +It is, by design, a stateful library that persists all input in an SQL database format to enable reproducibility. +The intention of this demo is further explained in the logs it generated. + + +[1] Hartmann, S.: Generating scenarios for simulation and optimization of container terminal logistics. OR Spectrum, +vol. 26, 171–192 (2004). doi: 10.1007/s00291-003-0150-6 +""" + +import datetime +import os.path +import random +import sys +import subprocess +import pandas as pd + +try: + import conflowgen + install_dir = os.path.abspath( + os.path.join(conflowgen.__file__, os.path.pardir) + ) + print(f"Importing ConFlowGen version {conflowgen.__version__} installed at {install_dir}.") +except ImportError as exc: + print("Please first install ConFlowGen, e.g. with conda or pip") + raise exc + +# The seed of x=1 guarantees that the same traffic data is generated as input data in this script. However, it does not +# affect the container generation or the assignment of containers to vehicles. +seeded_random = random.Random(x=1) + +with_visuals = False +if len(sys.argv) > 2: + if sys.argv[2] == "--with-visuals": + with_visuals = True + +this_dir = os.path.dirname(__file__) + +import_deham_dir = os.path.join( + this_dir, + "data", + "DEHAM", + "CT Altenwerder" +) +df_deep_sea_vessels = pd.read_csv( + os.path.join( + import_deham_dir, + "deep_sea_vessel_input.csv" + ), + index_col=[0] +) +df_feeders = pd.read_csv( + os.path.join( + import_deham_dir, + "feeder_input.csv" + ), + index_col=[0] +) +df_barges = pd.read_csv( + os.path.join( + import_deham_dir, + "barge_input.csv" + ), + index_col=[0] +) +df_trains = pd.read_csv( + os.path.join( + import_deham_dir, + "train_input.csv" + ), + index_col=[0] +) + +# Start logging +logger = conflowgen.setup_logger() +logger.info(__doc__) + +# Pick database +database_chooser = conflowgen.DatabaseChooser( + sqlite_databases_directory=os.path.join(this_dir, "databases") +) +demo_file_name = "demo_deham_cta__with_ramp_up_and_ramp_down_period.sqlite" +database_chooser.create_new_sqlite_database( + demo_file_name, + assume_tas=True, + overwrite=True +) + +# Set settings +container_flow_generation_manager = conflowgen.ContainerFlowGenerationManager() +# Data is available for 01.07.2021 to 31.07.2021 - you can also pick a shorter time period. However, there are some +# artifacts in the generated data in the beginning and in the end of the time range because containers can not continue +# their journey as intended, i.e. they must be delivered by or picked up by a truck since no vehicles that move +# according to a schedule are generated before the start and after the end. +container_flow_start_date = datetime.date(year=2021, month=7, day=1) +container_flow_end_date = datetime.date(year=2021, month=7, day=31) +container_flow_generation_manager.set_properties( + name="Demo DEHAM CTA", + start_date=container_flow_start_date, + end_date=container_flow_end_date, + ramp_up_period=datetime.timedelta(days=10), + ramp_down_period=datetime.timedelta(days=10), +) + +# Set some general assumptions regarding the container properties +container_length_distribution_manager = conflowgen.ContainerLengthDistributionManager() +container_length_distribution_manager.set_container_length_distribution({ + conflowgen.ContainerLength.twenty_feet: 0.33, + conflowgen.ContainerLength.forty_feet: 0.67, + conflowgen.ContainerLength.forty_five_feet: 0, + conflowgen.ContainerLength.other: 0 +}) + +# Add vehicles that frequently visit the terminal. +port_call_manager = conflowgen.PortCallManager() + +logger.info("Start importing feeder vessels...") +for i, row in df_feeders.iterrows(): + feeder_vehicle_name = row["vehicle_name"] + capacity = row["capacity"] + vessel_arrives_at_as_pandas_type = row["arrival (planned)"] + vessel_arrives_at_as_datetime_type = pd.to_datetime(vessel_arrives_at_as_pandas_type) + + if vessel_arrives_at_as_datetime_type.date() < container_flow_start_date: + logger.info(f"Skipping feeder '{feeder_vehicle_name}' because it arrives before the start") + continue + + if vessel_arrives_at_as_datetime_type.date() > container_flow_end_date: + logger.info(f"Skipping feeder '{feeder_vehicle_name}' because it arrives after the end") + continue + + if port_call_manager.has_schedule(feeder_vehicle_name, vehicle_type=conflowgen.ModeOfTransport.feeder): + logger.info(f"Skipping feeder '{feeder_vehicle_name}' because it already exists") + continue + + logger.info(f"Add feeder '{feeder_vehicle_name}' to database") + # The estimate is based on the reported lifts per call in "Tendency toward Mega Containerships and the Constraints + # of Container Terminals" of Park and Suh (2019) J. Mar. Sci. Eng, URL: https://www.mdpi.com/2077-1312/7/5/131/htm + # lifts per call refer to both the inbound and outbound journey of the vessel + moved_capacity = int(round(capacity * seeded_random.triangular(0.3, 0.8) / 2)) # this is only the inbound journey + + # The actual name of the port is not important as it is only used to group containers that are destined for the same + # vessel in the yard for faster loading (less reshuffling). The assumption that the same amount of containers is + # destined for each of the following ports is a simplification. + number_ports = int(round(seeded_random.triangular(low=2, high=4))) + next_ports = [ + (port_name, 1/number_ports) + for port_name in [ + f"port {i + 1} of {feeder_vehicle_name}" + for i in range(number_ports) + ] + ] + + port_call_manager.add_vehicle( + vehicle_type=conflowgen.ModeOfTransport.feeder, + service_name=feeder_vehicle_name, + vehicle_arrives_at=vessel_arrives_at_as_datetime_type.date(), + vehicle_arrives_at_time=vessel_arrives_at_as_datetime_type.time(), + vehicle_capacity=capacity, + inbound_container_volume=moved_capacity, + next_destinations=next_ports + ) +logger.info("Feeder vessels are imported") + +logger.info("Start importing deep sea vessels...") +for i, row in df_deep_sea_vessels.iterrows(): + deep_sea_vessel_vehicle_name = row["vehicle_name"] + capacity = row["capacity"] + vessel_arrives_at_as_pandas_type = row["arrival (planned)"] + vessel_arrives_at_as_datetime_type = pd.to_datetime(vessel_arrives_at_as_pandas_type) + + if vessel_arrives_at_as_datetime_type.date() < container_flow_start_date: + logger.info(f"Skipping deep sea vessel '{deep_sea_vessel_vehicle_name}' because it arrives before the start") + continue + + if vessel_arrives_at_as_datetime_type.date() > container_flow_end_date: + logger.info(f"Skipping deep sea vessel '{deep_sea_vessel_vehicle_name}' because it arrives after the end") + continue + + if port_call_manager.has_schedule(deep_sea_vessel_vehicle_name, + vehicle_type=conflowgen.ModeOfTransport.deep_sea_vessel): + logger.info(f"Skipping deep sea service '{deep_sea_vessel_vehicle_name}' because it already exists") + continue + + logger.info(f"Add deep sea vessel '{deep_sea_vessel_vehicle_name}' to database") + # The estimate is based on the reported lifts per call in "Tendency toward Mega Containerships and the Constraints + # of Container Terminals" of Park and Suh (2019) J. Mar. Sci. Eng, URL: https://www.mdpi.com/2077-1312/7/5/131/htm + # lifts per call refer to both the inbound and outbound journey of the vessel + moved_capacity = int(round(capacity * seeded_random.triangular(0.3, 0.6) / 2)) # this is only the inbound journey + + # The actual name of the port is not important as it is only used to group containers that are destined for the same + # vessel in the yard for faster loading (less reshuffling). The assumption that the same amount of containers is + # destined for each of the following ports is a simplification + number_ports = int(round(seeded_random.triangular(low=3, high=15, mode=7))) + next_ports = [ + (port_name, 1/number_ports) + for port_name in [ + f"port {i + 1} of {deep_sea_vessel_vehicle_name}" + for i in range(number_ports) + ] + ] + + port_call_manager.add_vehicle( + vehicle_type=conflowgen.ModeOfTransport.deep_sea_vessel, + service_name=deep_sea_vessel_vehicle_name, + vehicle_arrives_at=vessel_arrives_at_as_datetime_type.date(), + vehicle_arrives_at_time=vessel_arrives_at_as_datetime_type.time(), + vehicle_capacity=capacity, + inbound_container_volume=moved_capacity, + next_destinations=next_ports + ) +logger.info("Deep sea vessels are imported") + +logger.info("Start importing barges...") +for i, row in df_barges.iterrows(): + barge_vehicle_name = row["vehicle_name"] + capacity = row["capacity"] + vessel_arrives_at_as_pandas_type = row["arrival (planned)"] + vessel_arrives_at_as_datetime_type = pd.to_datetime(vessel_arrives_at_as_pandas_type) + + if vessel_arrives_at_as_datetime_type.date() < container_flow_start_date: + logger.info(f"Skipping barge '{barge_vehicle_name}' because it arrives before the start") + continue + + if vessel_arrives_at_as_datetime_type.date() > container_flow_end_date: + logger.info(f"Skipping barge '{barge_vehicle_name}' because it arrives after the end") + continue + + if port_call_manager.has_schedule(barge_vehicle_name, vehicle_type=conflowgen.ModeOfTransport.barge): + logger.info(f"Skipping barge '{barge_vehicle_name}' because it already exists") + continue + + logger.info(f"Add barge '{barge_vehicle_name}' to database") + # assume that the barge approaches 2 or more terminals, thus not the whole barge is available for CTA + moved_capacity = int(round(capacity * seeded_random.uniform(0.3, 0.6))) + port_call_manager.add_vehicle( + vehicle_type=conflowgen.ModeOfTransport.barge, + service_name=barge_vehicle_name, + vehicle_arrives_at=vessel_arrives_at_as_datetime_type.date(), + vehicle_arrives_at_time=vessel_arrives_at_as_datetime_type.time(), + vehicle_capacity=capacity, + inbound_container_volume=moved_capacity, + ) +logger.info("Barges are imported") + +logger.info("Start importing trains...") +for i, row in df_trains.iterrows(): + train_vehicle_name = row["vehicle_name"] + vessel_arrives_at_as_pandas_type = row["arrival_day"] + vessel_arrives_at_as_datetime_type = pd.to_datetime(vessel_arrives_at_as_pandas_type) + + if port_call_manager.has_schedule(train_vehicle_name, vehicle_type=conflowgen.ModeOfTransport.train): + logger.info(f"Train service '{train_vehicle_name}' already exists") + continue + + capacity = 96 # in TEU, see https://www.intermodal-info.com/verkehrstraeger/ + earliest_time = datetime.time(hour=1, minute=0) + earliest_time_as_delta = datetime.timedelta(hours=earliest_time.hour, minutes=earliest_time.minute) + latest_time = datetime.time(hour=5, minute=30) + latest_time_as_delta = datetime.timedelta(hours=latest_time.hour, minutes=latest_time.minute) + number_slots_minus_one = int((latest_time_as_delta - earliest_time_as_delta) / datetime.timedelta(minutes=30)) + + drawn_slot = seeded_random.randint(0, number_slots_minus_one) + vehicle_arrives_at_time_as_delta = earliest_time_as_delta + datetime.timedelta(hours=0.5 * drawn_slot) + vehicle_arrives_at_time = (datetime.datetime.min + vehicle_arrives_at_time_as_delta).time() + logger.info(f"Add train '{train_vehicle_name}' to database") + port_call_manager.add_service_that_calls_terminal( + vehicle_type=conflowgen.ModeOfTransport.train, + service_name=train_vehicle_name, + vehicle_arrives_at=vessel_arrives_at_as_datetime_type.date(), + vehicle_arrives_at_time=vehicle_arrives_at_time, + average_vehicle_capacity=capacity, + average_inbound_container_volume=capacity, + ) + +logger.info("All trains are imported") + +logger.info("All vehicles have been imported") + +### +# Now, all schedules and input distributions are set up - no further inputs are required +### + +logger.info("Preview the results with some light-weight approaches.") + +conflowgen.run_all_previews( + as_graph=with_visuals +) + +logger.info("Generate all fleets with all vehicles. This is the core of the whole project.") +container_flow_generation_manager.generate() + +logger.info("The container flow data have been generated, run analyses on them.") + +conflowgen.run_all_analyses( + as_graph=with_visuals +) + +logger.info("For a better understanding of the data, it is advised to study the logs and compare the preview with the " + "analysis results.") + +logger.info("Start data export...") + +# Export important entries from SQL to CSV so that it can be further processed, e.g., by a simulation software +export_container_flow_manager = conflowgen.ExportContainerFlowManager() +export_container_flow_manager.export( + folder_name=( + "demo-DEHAM-inc-ramp-up-and-down-of-day--" + + str(datetime.datetime.now()).replace(":", "-").replace(" ", "--").split(".")[0] + ), + path_to_export_folder=os.path.join(this_dir, "export"), + file_format=conflowgen.ExportFileFormat.csv +) + +# Gracefully close everything +database_chooser.close_current_connection() +logger.info(f"ConFlowGen {conflowgen.__version__} from {conflowgen.__file__} was used.") +try: + last_git_commit = str(subprocess.check_output(["git", "log", "-1"]).strip()) # nosec B607 + logger.info("Used git commit: " + last_git_commit[2:-1]) +except: # pylint: disable=bare-except + logger.debug("The last git commit of this repository could not be retrieved, no further version specification.") +logger.info("Demo 'demo_DEHAM_CTA_with_ramp_up_and_down_period' finished successfully.") diff --git a/examples/Python_Script/demo_continental_gateway.py b/examples/Python_Script/demo_continental_gateway.py index 4e9de1e..0560470 100644 --- a/examples/Python_Script/demo_continental_gateway.py +++ b/examples/Python_Script/demo_continental_gateway.py @@ -7,6 +7,7 @@ import os.path import random import sys +import subprocess import pandas as pd try: @@ -168,9 +169,8 @@ service_name=feeder_vehicle_name, vehicle_arrives_at=vessel_arrives_at_as_datetime_type.date(), vehicle_arrives_at_time=vessel_arrives_at_as_datetime_type.time(), - average_vehicle_capacity=capacity, - average_moved_capacity=moved_capacity, - vehicle_arrives_every_k_days=-1, # single arrival, no frequent schedule + vehicle_capacity=capacity, + inbound_container_volume=moved_capacity, next_destinations=next_ports ) logger.info("Feeder vessels are imported") @@ -196,4 +196,10 @@ # Gracefully close everything database_chooser.close_current_connection() +logger.info(f"ConFlowGen {conflowgen.__version__} from {conflowgen.__file__} was used.") +try: + last_git_commit = str(subprocess.check_output(["git", "log", "-1"]).strip()) # nosec B607 + logger.info("Used git commit: " + last_git_commit[2:-1]) +except: # pylint: disable=bare-except + logger.debug("The last git commit of this repository could not be retrieved, no further version specification.") logger.info("Demo 'demo_continental_gateway' finished successfully.") diff --git a/examples/Python_Script/demo_poc.py b/examples/Python_Script/demo_poc.py index fb0a184..10aa4d4 100644 --- a/examples/Python_Script/demo_poc.py +++ b/examples/Python_Script/demo_poc.py @@ -12,8 +12,9 @@ """ import datetime -import os +import os.path import sys +import subprocess try: import conflowgen @@ -25,6 +26,7 @@ print("Please first install ConFlowGen, e.g. with conda or pip") raise exc + this_dir = os.path.dirname(__file__) with_visuals = False @@ -61,13 +63,13 @@ # Add vehicles that frequently visit the terminal. feeder_service_name = "LX050" logger.info(f"Add feeder service '{feeder_service_name}' to database") -port_call_manager.add_vehicle( +port_call_manager.add_service_that_calls_terminal( vehicle_type=conflowgen.ModeOfTransport.feeder, service_name=feeder_service_name, vehicle_arrives_at=datetime.date(2021, 7, 9), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=100, + average_inbound_container_volume=100, next_destinations=[ ("DEBRV", 0.4), # 50% of the containers (in boxes) go here... ("RULED", 0.6) # and the other 50% of the containers (in boxes) go here. @@ -76,25 +78,25 @@ train_service_name = "JR03A" logger.info(f"Add train service '{train_service_name}' to database") -port_call_manager.add_vehicle( +port_call_manager.add_service_that_calls_terminal( vehicle_type=conflowgen.ModeOfTransport.train, service_name=train_service_name, vehicle_arrives_at=datetime.date(2021, 7, 12), vehicle_arrives_at_time=datetime.time(17), average_vehicle_capacity=90, - average_moved_capacity=90, + average_inbound_container_volume=90, next_destinations=None # Here we don't have containers that need to be grouped by destination ) deep_sea_service_name = "LX050" logger.info(f"Add deep sea vessel service '{deep_sea_service_name}' to database") -port_call_manager.add_vehicle( +port_call_manager.add_service_that_calls_terminal( vehicle_type=conflowgen.ModeOfTransport.deep_sea_vessel, service_name=deep_sea_service_name, vehicle_arrives_at=datetime.date(2021, 7, 10), vehicle_arrives_at_time=datetime.time(19), average_vehicle_capacity=16000, - average_moved_capacity=150, # for faster demo + average_inbound_container_volume=150, # for faster demo next_destinations=[ ("ZADUR", 0.3), # 30% of the containers (in boxes) go here... ("CNSHG", 0.7) # and the other 70% of the containers (in boxes) go here. @@ -136,4 +138,10 @@ # Gracefully close everything database_chooser.close_current_connection() +logger.info(f"ConFlowGen {conflowgen.__version__} from {conflowgen.__file__} was used.") +try: + last_git_commit = str(subprocess.check_output(["git", "log", "-1"]).strip()) # nosec B607 + logger.info("Used git commit: " + last_git_commit[2:-1]) +except: # pylint: disable=bare-except + logger.debug("The last git commit of this repository could not be retrieved, skip this.") logger.info("Demo 'demo_poc' finished successfully.") diff --git a/run_ci_light.bat b/run_ci_light.bat index c06d213..ed8e159 100644 --- a/run_ci_light.bat +++ b/run_ci_light.bat @@ -49,6 +49,7 @@ python -m pip install -e .[dev] || ( ) REM run tests +set MPLBACKEND=agg python -m pytest --exitfirst --verbose --failed-first --cov="./conflowgen" --cov-report html || ( ECHO.Tests failed! EXIT /B diff --git a/setup.py b/setup.py index 245d657..9abae58 100644 --- a/setup.py +++ b/setup.py @@ -50,6 +50,7 @@ 'pytest-cov', # create coverage report 'pytest-xdist', # use several processes to speed up the testing process 'pytest-github-actions-annotate-failures', # turns pytest failures into action annotations + 'parameterized', # for parameterized testing 'seaborn', # some visuals in unittests are generated by seaborn 'nbconvert', # used to run tests in Jupyter notebooks, see ./test/notebooks/test_run_notebooks.py From 7969f3aacf037a380232a47697b305769de34865 Mon Sep 17 00:00:00 2001 From: 1kastner Date: Tue, 20 Aug 2024 18:41:47 +0200 Subject: [PATCH 7/7] publish v3.0.0 (#213) * Add Shubhangi as contributor on Zenodo, bump Zenodo version * Fix vulnerability to CVE-2023-25399 * Bump ConFlowGen version as it is used by setup.py / pip, add Shubhangi as contributor in the docstring * Fix qodana issues * add installation of parameterized to CI --- .../workflows/installation-from-remote.yaml | 6 ++-- CITATION.cff | 7 ++++- conflowgen/analyses/abstract_analysis.py | 2 +- ...low_by_vehicle_instance_analysis_report.py | 7 +++-- .../services/export_container_flow_service.py | 2 +- conflowgen/metadata.py | 4 +-- docs/conf.py | 2 +- qodana.yaml | 29 +++++++++++++++++++ setup.py | 3 +- 9 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 qodana.yaml diff --git a/.github/workflows/installation-from-remote.yaml b/.github/workflows/installation-from-remote.yaml index 85939fd..4f3548f 100644 --- a/.github/workflows/installation-from-remote.yaml +++ b/.github/workflows/installation-from-remote.yaml @@ -32,7 +32,7 @@ jobs: conda info conda update conda conda info - conda create -n test-install-conflowgen -c conda-forge conflowgen pytest + conda create -n test-install-conflowgen -c conda-forge conflowgen pytest parameterized - name: Prepare tests run: | @@ -71,7 +71,7 @@ jobs: conda update conda conda info conda activate base - conda create -n test-install-conflowgen -c conda-forge conflowgen pytest + conda create -n test-install-conflowgen -c conda-forge conflowgen pytest parameterized - name: Run tests run: | @@ -102,7 +102,7 @@ jobs: - name: Install ConFlowGen run: | - python -m pip install conflowgen pytest + python -m pip install conflowgen pytest parameterized python -m pip show --verbose conflowgen - name: Run tests diff --git a/CITATION.cff b/CITATION.cff index 7032e81..4fbb4dd 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -24,7 +24,12 @@ authors: email: ole.grasse@tuhh.de affiliation: Hamburg University of Technology (TUHH), Institute of Maritime Logistics orcid: 'https://orcid.org/0000-0003-1982-9436' -version: 2.1.1 + - given-names: Shubhangi + family-names: Gupta + email: shubhangi.gupta@tuhh.de + affiliation: Hamburg University of Technology (TUHH), Institute of Maritime Logistics + orcid: 'https://orcid.org/0009-0003-3574-2899' +version: 3.0.0 repository-code: "https://github.com/1kastner/conflowgen" keywords: - logistics diff --git a/conflowgen/analyses/abstract_analysis.py b/conflowgen/analyses/abstract_analysis.py index c5fbad4..9361a0b 100644 --- a/conflowgen/analyses/abstract_analysis.py +++ b/conflowgen/analyses/abstract_analysis.py @@ -119,7 +119,7 @@ def _restrict_container_picked_up_by_vehicle_type( if container_picked_up_by_vehicle_type == "scheduled vehicles": container_picked_up_by_vehicle_type = ModeOfTransport.get_scheduled_vehicles() list_of_vehicle_types = container_picked_up_by_vehicle_type - if hashable(container_picked_up_by_vehicle_type) \ + elif hashable(container_picked_up_by_vehicle_type) \ and container_picked_up_by_vehicle_type in set(ModeOfTransport): selected_containers = selected_containers.where( Container.picked_up_by == container_picked_up_by_vehicle_type diff --git a/conflowgen/analyses/container_flow_by_vehicle_instance_analysis_report.py b/conflowgen/analyses/container_flow_by_vehicle_instance_analysis_report.py index a6327e6..edbe576 100644 --- a/conflowgen/analyses/container_flow_by_vehicle_instance_analysis_report.py +++ b/conflowgen/analyses/container_flow_by_vehicle_instance_analysis_report.py @@ -1,7 +1,6 @@ from __future__ import annotations import logging -import typing import matplotlib.figure import matplotlib.pyplot as plt @@ -36,7 +35,6 @@ def __init__(self): def get_report_as_text( self, - vehicle_types: ModeOfTransport | str | typing.Collection = "scheduled vehicles", **kwargs ) -> str: """ @@ -99,7 +97,10 @@ def _get_analysis(self, kwargs: dict) -> (list, list): return plain_table, vehicle_types - def get_report_as_graph(self, **kwargs) -> matplotlib.figure.Figure: + def get_report_as_graph( + self, + **kwargs + ) -> matplotlib.figure.Figure: """ Keyword Args: vehicle_types (typing.Collection[ModeOfTransport]): A collection of vehicle types, e.g., passed as a diff --git a/conflowgen/application/services/export_container_flow_service.py b/conflowgen/application/services/export_container_flow_service.py index 251aa17..bdd59ab 100644 --- a/conflowgen/application/services/export_container_flow_service.py +++ b/conflowgen/application/services/export_container_flow_service.py @@ -149,7 +149,7 @@ def _convert_table_to_pandas_dataframe( # extract data from sql database data = list(model.select().dicts()) - if type(model) == ModelSelect: # pylint: disable=unidiomatic-typecheck # TODO: check if isinstance works + if isinstance(model, ModelSelect): model = model.model foreign_keys_to_resolve = {} diff --git a/conflowgen/metadata.py b/conflowgen/metadata.py index 4a94be3..8e12568 100644 --- a/conflowgen/metadata.py +++ b/conflowgen/metadata.py @@ -1,9 +1,9 @@ -__version__ = "2.1.1" +__version__ = "3.0.0" __license__ = "MIT" __description__ = """ A generator for synthetic container flows at maritime container terminals with a focus on yard operations """ -__author__ = "Marvin Kastner and Ole Grasse" +__author__ = "Marvin Kastner, Ole Grasse, and Shubhangi Gupta" __maintainer__ = "Marvin Kastner" __email__ = "marvin.kastner@tuhh.de" diff --git a/docs/conf.py b/docs/conf.py index 6590cf2..efb4a3a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -193,7 +193,7 @@ database_names = ["demo_continental_gateway", "demo_deham_cta", "demo_poc"] # List of database names to download sqlite_databases_directory = "notebooks/data/prepared_dbs/" os.system("echo 'Current directory:'") - os.system("pwd") # Print current directory; we expect to be in the docs folder + os.system("pwd") # Print current directory; we expect to be in the docs folder os.makedirs(sqlite_databases_directory, exist_ok=True) # Create the destination folder if it doesn't exist for database_name in database_names: os.system(f'echo "Fetching {database_name}"') diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..76fb7ad --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,29 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.starter + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-:latest diff --git a/setup.py b/setup.py index 9abae58..6b83f9b 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ python_requires='>=3.8', install_requires=[ # working with distributions and statistics - 'scipy', # used for, e.g., the lognorm distribution + "scipy >=1.10.0-rc1", # used for, e.g., the lognorm distribution, version fixed due to CVE-2023-25399 # data export 'numpy', # used in combination with pandas for column types @@ -53,6 +53,7 @@ 'parameterized', # for parameterized testing 'seaborn', # some visuals in unittests are generated by seaborn 'nbconvert', # used to run tests in Jupyter notebooks, see ./test/notebooks/test_run_notebooks.py + 'nbformat', # used to run tests in Jupyter notebooks # build documentation 'sphinx >=6.2', # build the documentation - restrict version to improve pip version resolution