Skip to content

Commit 8efa8da

Browse files
Merge pull request #977 from Transport-for-the-North/pre-me-tour-props-fix
Fixes for time period splits
2 parents fb67802 + 7ea4c8c commit 8efa8da

File tree

10 files changed

+354
-96
lines changed

10 files changed

+354
-96
lines changed

RELEASE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ Below, a brief summary of patches made since the previous version can be found.
3030
- Renamed the incorrectly named "TemproExtractor" to "NTEM Extractor"
3131
- Updated tool to extract different versions of NTEM data
3232
- Updated tool to extract different scenarios of NTEM data
33+
- TLD Builder
34+
- Fixed a bug where demand on bounds was being dropped
35+
- Added functionality to generate dynamic bands to create a log curve for demand
3336
- Core
3437
- Added Midlands Connect zoning systems as built-ins
3538
- Added functionality to convert matrices into SATURN and CUBE formats
@@ -38,6 +41,7 @@ Below, a brief summary of patches made since the previous version can be found.
3841
- Adapted NoTEM to optionally perform steps needed for Midlands Models
3942
- Including optional localised trip end adjustments
4043
- Added a temporary front end script for `Midlands Distribution Model`
44+
- Fixed a bug in the default distribution model run, running at the incorrect time format
4145
- Created a generic front end for all forecasting functionality
4246
- Can run NTEM based forecasts, or synthetic trip end based forecasts from
4347
the same front end

normits_demand/logging.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
File purpose:
1111
Initialiser for all logging in normits_demand
1212
"""
13+
from __future__ import annotations
14+
1315
# Builtins
1416
import logging
1517
from typing import Any, Dict
@@ -330,3 +332,35 @@ def capture_warnings(
330332
if file_handler_args is not None:
331333
warning_logger.addHandler(get_file_handler(**file_handler_args))
332334

335+
336+
class TemporaryLogFile:
337+
"""Add temporary log file to a logger."""
338+
339+
def __init__(self, logger: logging.Logger, log_file: nd.PathLike, **kwargs) -> None:
340+
"""Add temporary log file handler to `logger`.
341+
Parameters
342+
----------
343+
logger : logging.Logger
344+
Logger to add FileHandler to.
345+
log_file : nd.PathLike
346+
Path to new log file to create.
347+
kwargs : Keyword arguments, optional
348+
Any arguments to pass to `get_file_handler`.
349+
"""
350+
self.logger = logger
351+
self.log_file = log_file
352+
self.logger.debug('Creating temporary log file: "%s"', self.log_file)
353+
self.handler = get_file_handler(log_file, **kwargs)
354+
self.logger.addHandler(self.handler)
355+
356+
def __enter__(self) -> TemporaryLogFile:
357+
"""Initialise TemporaryLogFile."""
358+
return self
359+
360+
def __exit__(self, excepType, excepVal, traceback) -> None:
361+
"""Close temporary log file."""
362+
if excepType is not None or excepVal is not None or traceback is not None:
363+
self.logger.critical("Oh no a critical error occurred", exc_info=True)
364+
365+
self.logger.removeHandler(self.handler)
366+
self.logger.debug('Closed temporary log file: "%s"', self.log_file)

normits_demand/matrices/matrix_processing.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2413,7 +2413,7 @@ def compile_matrices(
24132413
compile_params_path: nd.PathLike,
24142414
factor_pickle_path: str = None,
24152415
round_dp: int = consts.DEFAULT_ROUNDING,
2416-
factors_fname: str = "od_compilation_factors.pickle",
2416+
factors_fname: str = "od_compilation_factors.pkl",
24172417
avoid_zero_splits: bool = False,
24182418
process_count: int = consts.PROCESS_COUNT,
24192419
overwrite: bool = True,
@@ -2470,7 +2470,7 @@ def compile_matrices(
24702470
if pathlib.Path(factor_pickle_path).suffix == "":
24712471
print(
24722472
"WARNING: No filename was given for the pickle factors. "
2473-
"Defaulting to od_compilation_factors.pickle, but this is "
2473+
"Defaulting to od_compilation_factors.pkl, but this is "
24742474
"deprecated and will be removed in future!"
24752475
)
24762476
factor_pickle_path = os.path.join(factor_pickle_path, factors_fname)

normits_demand/matrices/utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
File purpose:
1111
Utility functions specific to matrices
1212
"""
13+
from __future__ import annotations
14+
1315
# builtins
1416
import os
1517
import pathlib
@@ -258,3 +260,14 @@ def split_matrix_by_time_periods(
258260
)
259261

260262
return tp_mats
263+
264+
265+
def apply_factor(
266+
input_path: os.PathLike,
267+
output_path: os.PathLike,
268+
factor: int | float,
269+
) -> None:
270+
"""Apply a factor to a matrix"""
271+
mat = file_ops.read_df(input_path, index_col=0)
272+
mat *= factor
273+
file_ops.write_df(mat, output_path)

normits_demand/models/distribution_model.py

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# Built-Ins
1414
import os
1515
import pathlib
16+
import itertools
1617
import functools
1718

1819
from typing import Any
@@ -37,6 +38,7 @@
3738
from normits_demand.matrices import pa_to_od
3839
from normits_demand.matrices import utils as mat_utils
3940
from normits_demand.reports import matrix_reports
41+
from normits_demand.concurrency import multiprocessing
4042

4143
from normits_demand.pathing.distribution_model import DistributionModelExportPaths
4244
from normits_demand.pathing.distribution_model import DMArgumentBuilderBase
@@ -742,7 +744,87 @@ def run_od_matrix_reports(self):
742744
col_name='destinations',
743745
)
744746

745-
def compile_to_assignment_format(self):
747+
def _convert_matrix_time_format(
748+
self,
749+
import_dir: pathlib.Path,
750+
export_dir: pathlib.Path,
751+
from_time_format: nd.core.TimeFormat = None,
752+
to_time_format: nd.core.TimeFormat = None,
753+
) -> None:
754+
"""Converts matrices between time formats"""
755+
# TODO(BT): This function just assumes there's time periods.
756+
# Won't work otherwise
757+
conversion_factors = from_time_format.get_conversion_factors(to_time_format)
758+
759+
# Build matrix naming templates
760+
template = self.running_segmentation.generate_template_file_name(
761+
file_desc='{matrix_format}',
762+
trip_origin=self.trip_origin,
763+
year=str(self.year),
764+
compressed=True,
765+
)
766+
767+
if self.trip_origin == nd.core.TripOrigin.HB.value:
768+
matrix_formats = ["synthetic_od_from", "synthetic_od_to"]
769+
elif self.trip_origin == nd.core.TripOrigin.NHB.value:
770+
matrix_formats = ["synthetic_od"]
771+
else:
772+
raise ValueError(f"Trip origin '{self.trip_origin}' not recognised.")
773+
774+
# Build the multiprocessing kwargs
775+
kwarg_list = list()
776+
777+
# BACKLOG(BT): This is all a really rough kludge to get this working
778+
# NOW. Need to come back and think how to do this properly.
779+
if self.running_segmentation.has_time_period_segments():
780+
iterator = itertools.product(self.running_segmentation, [-1])
781+
naming_order = self.running_segmentation.naming_order
782+
segment_types = self.running_segmentation.segment_types
783+
else:
784+
tps = [1, 2, 3, 4, 5, 6]
785+
iterator = itertools.product(self.running_segmentation, tps)
786+
naming_order = self.running_segmentation.naming_order + ['tp']
787+
segment_types = self.running_segmentation.segment_types | {"tp": int}
788+
789+
for segment_params, tp in iterator:
790+
# Generate filenames
791+
tp_params = segment_params.copy()
792+
if "tp" not in tp_params:
793+
tp_params['tp'] = tp
794+
segment_str = nd.core.SegmentationLevel.generate_template_segment_str(
795+
naming_order=naming_order,
796+
segment_params=tp_params,
797+
segment_types=segment_types,
798+
)
799+
800+
# Build the kwarg list
801+
for mx_format in matrix_formats:
802+
fname = template.format(segment_params=segment_str, matrix_format=mx_format)
803+
kwarg_list.append({
804+
"input_path": import_dir / fname,
805+
"output_path": export_dir / fname,
806+
"factor": conversion_factors[tp_params["tp"]]
807+
})
808+
809+
# MP running
810+
self._logger.info(
811+
f"Converting OD matrix time format from {from_time_format.value} "
812+
f"to {to_time_format.value}."
813+
)
814+
pbar_kwargs = {'desc': "Converting OD matrix time format"}
815+
multiprocessing.multiprocess(
816+
fn=mat_utils.apply_factor,
817+
kwargs=kwarg_list,
818+
process_count=self.process_count,
819+
pbar_kwargs=pbar_kwargs
820+
821+
)
822+
823+
def compile_to_assignment_format(
824+
self,
825+
from_time_format: nd.core.TimeFormat = None,
826+
to_time_format: nd.core.TimeFormat = None,
827+
):
746828
"""TfN Specific helper function to compile outputs into assignment format
747829
748830
This should really be the job of NorMITs Matrix tools! Move there
@@ -752,18 +834,32 @@ def compile_to_assignment_format(self):
752834
-------
753835
754836
"""
755-
# TODO(BT): NEED TO OUTPUT SPLITTING FACTORS
756-
757837
# TODO(BT): UPDATE build_compile_params() to use segmentation levels
758838
m_needed = self.running_segmentation.segments['m'].unique()
759839

760840
# NoHAM should be tp split
761841
tp_needed = [1, 2, 3, 4]
762842

843+
# Covert time periods if factors given
844+
od_mat_dir = self.export_paths.full_od_dir
845+
if (
846+
(from_time_format is not None and to_time_format is not None)
847+
and (from_time_format != to_time_format)
848+
):
849+
new_od_mat_dir = pathlib.Path(self.export_paths.full_od_dir) / "converted time format"
850+
new_od_mat_dir.mkdir(exist_ok=True)
851+
self._convert_matrix_time_format(
852+
import_dir=pathlib.Path(self.export_paths.full_od_dir),
853+
export_dir=pathlib.Path(new_od_mat_dir),
854+
from_time_format=from_time_format,
855+
to_time_format=to_time_format,
856+
)
857+
od_mat_dir = new_od_mat_dir
858+
763859
if self.running_mode in [nd.Mode.CAR, nd.Mode.BUS]:
764860
# Compile to NoHAM format
765861
compile_params_paths = matrix_processing.build_compile_params(
766-
import_dir=self.export_paths.full_od_dir,
862+
import_dir=od_mat_dir,
767863
export_dir=self.export_paths.compiled_od_dir,
768864
matrix_format=self._od_matrix_desc,
769865
years_needed=[self.year],
@@ -772,9 +868,10 @@ def compile_to_assignment_format(self):
772868
)
773869

774870
matrix_processing.compile_matrices(
775-
mat_import=self.export_paths.full_od_dir,
871+
mat_import=od_mat_dir,
776872
mat_export=self.export_paths.compiled_od_dir,
777873
compile_params_path=compile_params_paths[0],
874+
factors_fname="od_compilation_factors.pkl",
778875
)
779876

780877
# TODO(BT): Build in DM imports!
@@ -815,7 +912,7 @@ def compile_to_assignment_format(self):
815912
self._logger.info("Compiling NoRMS VDM Format")
816913
matrix_processing.compile_norms_to_vdm(
817914
mat_pa_import=self.export_paths.full_tp_pa_dir,
818-
mat_od_import=self.export_paths.full_od_dir,
915+
mat_od_import=od_mat_dir,
819916
mat_export=self.export_paths.compiled_pa_dir, # TODO(BT): Rename to NoRMS
820917
params_export=self.export_paths.compiled_pa_dir,
821918
year=self.year,

0 commit comments

Comments
 (0)