Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions activitysim/abm/models/__init__.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,3 +27,4 @@
from . import trip_purpose
from . import trip_purpose_and_destination
from . import trip_scheduling
from . import trip_matrices
147 changes: 147 additions & 0 deletions activitysim/abm/models/trip_matrices.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
# ActivitySim
# See full license in LICENSE.txt.

import logging

import openmatrix as omx
import pandas as pd
import numpy as np

from activitysim.core import config
from activitysim.core import inject
from activitysim.core import pipeline

from .util import expressions
from .util.expressions import skim_time_period_label

logger = logging.getLogger(__name__)


@inject.step()
def write_trip_matrices(trips, skim_dict, skim_stack):
"""
Write trip matrices step.

Adds boolean columns to local trips table via annotation expressions,
then aggregates trip counts and writes OD matrices to OMX. Save annotated
trips table to pipeline if desired.
"""

model_settings = config.read_model_settings('write_trip_matrices.yaml')
trips_df = annotate_trips(trips, skim_dict, skim_stack, model_settings)

if bool(model_settings.get('SAVE_TRIPS_TABLE')):
pipeline.replace_table('trips', trips_df)

logger.info('Aggregating trips...')
aggregate_trips = trips_df.groupby(['origin', 'destination'], sort=False).sum()
logger.info('Finished.')

orig_vals = aggregate_trips.index.get_level_values('origin')
dest_vals = aggregate_trips.index.get_level_values('destination')

zone_index = pipeline.get_table('land_use').index
assert all(zone in zone_index for zone in orig_vals)
assert all(zone in zone_index for zone in dest_vals)

_, orig_index = zone_index.reindex(orig_vals)
_, dest_index = zone_index.reindex(dest_vals)

write_matrices(aggregate_trips, zone_index, orig_index, dest_index, model_settings)


def annotate_trips(trips, skim_dict, skim_stack, model_settings):
"""
Add columns to local trips table. The annotator has
access to the origin/destination skims and everything
defined in the model settings CONSTANTS.

Pipeline tables can also be accessed by listing them under
TABLES in the preprocessor settings.
"""

trips_df = trips.to_frame()

trace_label = 'trip_matrices'

# setup skim keys
assert ('trip_period' not in trips_df)
trips_df['trip_period'] = skim_time_period_label(trips_df.depart)
od_skim_wrapper = skim_dict.wrap('origin', 'destination')
odt_skim_stack_wrapper = skim_stack.wrap(left_key='origin', right_key='destination',
skim_key='trip_period')
skims = {
'od_skims': od_skim_wrapper,
"odt_skims": odt_skim_stack_wrapper
}

locals_dict = {}
constants = config.get_model_constants(model_settings)
if constants is not None:
locals_dict.update(constants)

expressions.annotate_preprocessors(
trips_df, locals_dict, skims,
model_settings, trace_label)

# Data will be expanded by an expansion weight column from
# the households pipeline table, if specified in the model settings.
hh_weight_col = model_settings.get('HH_EXPANSION_WEIGHT_COL')

if hh_weight_col and hh_weight_col not in trips_df:
logger.info("adding '%s' from households to trips table" % hh_weight_col)
household_weights = pipeline.get_table('households')[hh_weight_col]
trips_df[hh_weight_col] = trips_df.household_id.map(household_weights)

return trips_df


def write_matrices(aggregate_trips, zone_index, orig_index, dest_index, model_settings):
"""
Write aggregated trips to OMX format.

The MATRICES setting lists the new OMX files to write.
Each file can contain any number of 'tables', each specified by a
table key ('name') and a trips table column ('data_field') to use
for aggregated counts.

Any data type may be used for columns added in the annotation phase,
but the table 'data_field's must be summable types: ints, floats, bools.
"""

matrix_settings = model_settings.get('MATRICES')

if not matrix_settings:
logger.error('Missing MATRICES setting in write_trip_matrices.yaml')

for matrix in matrix_settings:
filename = matrix.get('file_name')
filepath = config.output_file_path(filename)
logger.info('opening %s' % filepath)
file = omx.open_file(filepath, 'w') # possibly overwrite existing file
table_settings = matrix.get('tables')

for table in table_settings:
table_name = table.get('name')
col = table.get('data_field')

if col not in aggregate_trips:
logger.error('missing %s column in %s DataFrame' % (col, aggregate_trips.name))
return

hh_weight_col = model_settings.get('HH_EXPANSION_WEIGHT_COL')
if hh_weight_col:
aggregate_trips[col] = aggregate_trips[col] / aggregate_trips[hh_weight_col]

data = np.zeros((len(zone_index), len(zone_index)))
data[orig_index, dest_index] = aggregate_trips[col]
logger.info('writing %s' % table_name)
file[table_name] = data # write to file

# include the index-to-zone map in the file
logger.info('adding %s mapping for %s zones to %s' %
(zone_index.name, zone_index.size, filename))
file.create_mapping(zone_index.name, zone_index.to_numpy())

logger.info('closing %s' % filepath)
file.close()
2 changes: 1 addition & 1 deletion activitysim/abm/models/trip_scheduling.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -319,7 +319,7 @@ def schedule_trips_in_leg(
trips = trips.sort_index()
trips['next_trip_id'] = np.roll(trips.index, -1 if outbound else 1)
is_final = (trips.trip_num == trips.trip_count) if outbound else (trips.trip_num == 1)
trips.next_trip_id = trips.next_trip_id.where(is_final, NO_TRIP_ID)
trips.next_trip_id = trips.next_trip_id.where(~is_final, NO_TRIP_ID)

# iterate over outbound trips in ascending trip_num order, skipping the initial trip
# iterate over inbound trips in descending trip_num order, skipping the finial trip
Expand Down
18 changes: 14 additions & 4 deletions activitysim/abm/tables/households.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,9 +20,11 @@
def households(households_sample_size, override_hh_ids, trace_hh_id):

df_full = read_input_table("households")
households_sliced = False
tot_households = df_full.shape[0]

logger.info("full household list contains %s households" % tot_households)

logger.info("full household list contains %s households" % df_full.shape[0])
households_sliced = False

# only using households listed in override_hh_ids
if override_hh_ids is not None:
Expand All@@ -48,9 +50,9 @@ def households(households_sample_size, override_hh_ids, trace_hh_id):
households_sliced = True

# if we need a subset of full store
elif households_sample_size > 0 and df_full.shape[0] > households_sample_size:
elif tot_households > households_sample_size > 0:

logger.info("sampling %s of %s households" % (households_sample_size, df_full.shape[0]))
logger.info("sampling %s of %s households" % (households_sample_size, tot_households))

"""
Because random seed is set differently for each step, sampling of households using
Expand DownExpand Up@@ -80,6 +82,14 @@ def households(households_sample_size, override_hh_ids, trace_hh_id):
# persons table
inject.add_injectable('households_sliced', households_sliced)

if 'sample_rate' not in df.columns:
if households_sample_size == 0:
sample_rate = 1
else:
sample_rate = round(households_sample_size / tot_households, 3)

df['sample_rate'] = sample_rate

logger.info("loaded households %s" % (df.shape,))

# FIXME - pathological knowledge of name of chunk_id column used by chunked_choosers_by_chunk_id
Expand Down
21 changes: 20 additions & 1 deletion activitysim/abm/test/test_pipeline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,12 @@
import logging
import pkg_resources

import openmatrix as omx
import numpy as np
import numpy.testing as npt

import pandas as pd
import pandas.util.testing as pdt
import pandas.testing as pdt
import pytest
import yaml

Expand All@@ -17,6 +21,7 @@

# set the max households for all tests (this is to limit memory use on travis)
HOUSEHOLDS_SAMPLE_SIZE = 100
HOUSEHOLDS_SAMPLE_RATE = 0.02 # HOUSEHOLDS_SAMPLE_RATE / 5000 households

# household with mandatory, non mandatory, atwork_subtours, and joint tours
HH_ID = 257341
Expand DownExpand Up@@ -52,6 +57,7 @@ def setup_dirs(configs_dir, data_dir=None):
tracing.delete_output_files('csv')
tracing.delete_output_files('txt')
tracing.delete_output_files('yaml')
tracing.delete_output_files('omx')


def teardown_function(func):
Expand DownExpand Up@@ -433,6 +439,18 @@ def regress():
# should be at least two tours per trip
assert trips_df.shape[0] >= 2*tours_df.shape[0]

# write_trip_matrices
trip_matrices_file = config.output_file_path('trips_md.omx')
assert os.path.exists(trip_matrices_file)
trip_matrices = omx.open_file(trip_matrices_file)
assert trip_matrices.shape() == (25, 25)

assert 'WALK_MD' in trip_matrices.list_matrices()
walk_trips = np.array(trip_matrices['WALK_MD'])
assert walk_trips.dtype == np.dtype('float64')

trip_matrices.close()


def test_full_run1():

Expand DownExpand Up@@ -517,6 +535,7 @@ def test_full_run5_singleton():

if __name__ == "__main__":

from activitysim import abm # register injectables
print("running test_full_run1")
test_full_run1()
# teardown_function(None)
1 change: 1 addition & 0 deletions activitysim/examples/example_mtc/configs/settings.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,6 +86,7 @@ models:
- trip_mode_choice
- write_data_dictionary
- track_skim_usage
- write_trip_matrices
- write_tables

# to resume after last successful checkpoint, specify resume_after: _
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions activitysim/abm/models/__init__.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,3 +27,4 @@
from . import trip_purpose
from . import trip_purpose_and_destination
from . import trip_scheduling
from . import trip_matrices
147 changes: 147 additions & 0 deletions activitysim/abm/models/trip_matrices.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
# ActivitySim
# See full license in LICENSE.txt.

import logging

import openmatrix as omx
import pandas as pd
import numpy as np

from activitysim.core import config
from activitysim.core import inject
from activitysim.core import pipeline

from .util import expressions
from .util.expressions import skim_time_period_label

logger = logging.getLogger(__name__)


@inject.step()
def write_trip_matrices(trips, skim_dict, skim_stack):
"""
Write trip matrices step.

Adds boolean columns to local trips table via annotation expressions,
then aggregates trip counts and writes OD matrices to OMX. Save annotated
trips table to pipeline if desired.
"""

model_settings = config.read_model_settings('write_trip_matrices.yaml')
trips_df = annotate_trips(trips, skim_dict, skim_stack, model_settings)

if bool(model_settings.get('SAVE_TRIPS_TABLE')):
pipeline.replace_table('trips', trips_df)

logger.info('Aggregating trips...')
aggregate_trips = trips_df.groupby(['origin', 'destination'], sort=False).sum()
logger.info('Finished.')

orig_vals = aggregate_trips.index.get_level_values('origin')
dest_vals = aggregate_trips.index.get_level_values('destination')

zone_index = pipeline.get_table('land_use').index
assert all(zone in zone_index for zone in orig_vals)
assert all(zone in zone_index for zone in dest_vals)

_, orig_index = zone_index.reindex(orig_vals)
_, dest_index = zone_index.reindex(dest_vals)

write_matrices(aggregate_trips, zone_index, orig_index, dest_index, model_settings)


def annotate_trips(trips, skim_dict, skim_stack, model_settings):
"""
Add columns to local trips table. The annotator has
access to the origin/destination skims and everything
defined in the model settings CONSTANTS.

Pipeline tables can also be accessed by listing them under
TABLES in the preprocessor settings.
"""

trips_df = trips.to_frame()

trace_label = 'trip_matrices'

# setup skim keys
assert ('trip_period' not in trips_df)
trips_df['trip_period'] = skim_time_period_label(trips_df.depart)
od_skim_wrapper = skim_dict.wrap('origin', 'destination')
odt_skim_stack_wrapper = skim_stack.wrap(left_key='origin', right_key='destination',
skim_key='trip_period')
skims = {
'od_skims': od_skim_wrapper,
"odt_skims": odt_skim_stack_wrapper
}

locals_dict = {}
constants = config.get_model_constants(model_settings)
if constants is not None:
locals_dict.update(constants)

expressions.annotate_preprocessors(
trips_df, locals_dict, skims,
model_settings, trace_label)

# Data will be expanded by an expansion weight column from
# the households pipeline table, if specified in the model settings.
hh_weight_col = model_settings.get('HH_EXPANSION_WEIGHT_COL')

if hh_weight_col and hh_weight_col not in trips_df:
logger.info("adding '%s' from households to trips table" % hh_weight_col)
household_weights = pipeline.get_table('households')[hh_weight_col]
trips_df[hh_weight_col] = trips_df.household_id.map(household_weights)

return trips_df


def write_matrices(aggregate_trips, zone_index, orig_index, dest_index, model_settings):
"""
Write aggregated trips to OMX format.

The MATRICES setting lists the new OMX files to write.
Each file can contain any number of 'tables', each specified by a
table key ('name') and a trips table column ('data_field') to use
for aggregated counts.

Any data type may be used for columns added in the annotation phase,
but the table 'data_field's must be summable types: ints, floats, bools.
"""

matrix_settings = model_settings.get('MATRICES')

if not matrix_settings:
logger.error('Missing MATRICES setting in write_trip_matrices.yaml')

for matrix in matrix_settings:
filename = matrix.get('file_name')
filepath = config.output_file_path(filename)
logger.info('opening %s' % filepath)
file = omx.open_file(filepath, 'w') # possibly overwrite existing file
table_settings = matrix.get('tables')

for table in table_settings:
table_name = table.get('name')
col = table.get('data_field')

if col not in aggregate_trips:
logger.error('missing %s column in %s DataFrame' % (col, aggregate_trips.name))
return

hh_weight_col = model_settings.get('HH_EXPANSION_WEIGHT_COL')
if hh_weight_col:
aggregate_trips[col] = aggregate_trips[col] / aggregate_trips[hh_weight_col]

data = np.zeros((len(zone_index), len(zone_index)))
data[orig_index, dest_index] = aggregate_trips[col]
logger.info('writing %s' % table_name)
file[table_name] = data # write to file

# include the index-to-zone map in the file
logger.info('adding %s mapping for %s zones to %s' %
(zone_index.name, zone_index.size, filename))
file.create_mapping(zone_index.name, zone_index.to_numpy())

logger.info('closing %s' % filepath)
file.close()
2 changes: 1 addition & 1 deletion activitysim/abm/models/trip_scheduling.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -319,7 +319,7 @@ def schedule_trips_in_leg(
trips = trips.sort_index()
trips['next_trip_id'] = np.roll(trips.index, -1 if outbound else 1)
is_final = (trips.trip_num == trips.trip_count) if outbound else (trips.trip_num == 1)
trips.next_trip_id = trips.next_trip_id.where(is_final, NO_TRIP_ID)
trips.next_trip_id = trips.next_trip_id.where(~is_final, NO_TRIP_ID)

# iterate over outbound trips in ascending trip_num order, skipping the initial trip
# iterate over inbound trips in descending trip_num order, skipping the finial trip
Expand Down
18 changes: 14 additions & 4 deletions activitysim/abm/tables/households.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,9 +20,11 @@
def households(households_sample_size, override_hh_ids, trace_hh_id):

df_full = read_input_table("households")
households_sliced = False
tot_households = df_full.shape[0]

logger.info("full household list contains %s households" % tot_households)

logger.info("full household list contains %s households" % df_full.shape[0])
households_sliced = False

# only using households listed in override_hh_ids
if override_hh_ids is not None:
Expand All@@ -48,9 +50,9 @@ def households(households_sample_size, override_hh_ids, trace_hh_id):
households_sliced = True

# if we need a subset of full store
elif households_sample_size > 0 and df_full.shape[0] > households_sample_size:
elif tot_households > households_sample_size > 0:

logger.info("sampling %s of %s households" % (households_sample_size, df_full.shape[0]))
logger.info("sampling %s of %s households" % (households_sample_size, tot_households))

"""
Because random seed is set differently for each step, sampling of households using
Expand DownExpand Up@@ -80,6 +82,14 @@ def households(households_sample_size, override_hh_ids, trace_hh_id):
# persons table
inject.add_injectable('households_sliced', households_sliced)

if 'sample_rate' not in df.columns:
if households_sample_size == 0:
sample_rate = 1
else:
sample_rate = round(households_sample_size / tot_households, 3)

df['sample_rate'] = sample_rate

logger.info("loaded households %s" % (df.shape,))

# FIXME - pathological knowledge of name of chunk_id column used by chunked_choosers_by_chunk_id
Expand Down
21 changes: 20 additions & 1 deletion activitysim/abm/test/test_pipeline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,12 @@
import logging
import pkg_resources

import openmatrix as omx
import numpy as np
import numpy.testing as npt

import pandas as pd
import pandas.util.testing as pdt
import pandas.testing as pdt
import pytest
import yaml

Expand All@@ -17,6 +21,7 @@

# set the max households for all tests (this is to limit memory use on travis)
HOUSEHOLDS_SAMPLE_SIZE = 100
HOUSEHOLDS_SAMPLE_RATE = 0.02 # HOUSEHOLDS_SAMPLE_RATE / 5000 households

# household with mandatory, non mandatory, atwork_subtours, and joint tours
HH_ID = 257341
Expand DownExpand Up@@ -52,6 +57,7 @@ def setup_dirs(configs_dir, data_dir=None):
tracing.delete_output_files('csv')
tracing.delete_output_files('txt')
tracing.delete_output_files('yaml')
tracing.delete_output_files('omx')


def teardown_function(func):
Expand DownExpand Up@@ -433,6 +439,18 @@ def regress():
# should be at least two tours per trip
assert trips_df.shape[0] >= 2*tours_df.shape[0]

# write_trip_matrices
trip_matrices_file = config.output_file_path('trips_md.omx')
assert os.path.exists(trip_matrices_file)
trip_matrices = omx.open_file(trip_matrices_file)
assert trip_matrices.shape() == (25, 25)

assert 'WALK_MD' in trip_matrices.list_matrices()
walk_trips = np.array(trip_matrices['WALK_MD'])
assert walk_trips.dtype == np.dtype('float64')

trip_matrices.close()


def test_full_run1():

Expand DownExpand Up@@ -517,6 +535,7 @@ def test_full_run5_singleton():

if __name__ == "__main__":

from activitysim import abm # register injectables
print("running test_full_run1")
test_full_run1()
# teardown_function(None)
1 change: 1 addition & 0 deletions activitysim/examples/example_mtc/configs/settings.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,6 +86,7 @@ models:
- trip_mode_choice
- write_data_dictionary
- track_skim_usage
- write_trip_matrices
- write_tables

# to resume after last successful checkpoint, specify resume_after: _
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions activitysim/abm/models/__init__.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,3 +27,4 @@
from . import trip_purpose
from . import trip_purpose_and_destination
from . import trip_scheduling
from . import trip_matrices
147 changes: 147 additions & 0 deletions activitysim/abm/models/trip_matrices.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
# ActivitySim
# See full license in LICENSE.txt.

import logging

import openmatrix as omx
import pandas as pd
import numpy as np

from activitysim.core import config
from activitysim.core import inject
from activitysim.core import pipeline

from .util import expressions
from .util.expressions import skim_time_period_label

logger = logging.getLogger(__name__)


@inject.step()
def write_trip_matrices(trips, skim_dict, skim_stack):
"""
Write trip matrices step.

Adds boolean columns to local trips table via annotation expressions,
then aggregates trip counts and writes OD matrices to OMX. Save annotated
trips table to pipeline if desired.
"""

model_settings = config.read_model_settings('write_trip_matrices.yaml')
trips_df = annotate_trips(trips, skim_dict, skim_stack, model_settings)

if bool(model_settings.get('SAVE_TRIPS_TABLE')):
pipeline.replace_table('trips', trips_df)

logger.info('Aggregating trips...')
aggregate_trips = trips_df.groupby(['origin', 'destination'], sort=False).sum()
logger.info('Finished.')

orig_vals = aggregate_trips.index.get_level_values('origin')
dest_vals = aggregate_trips.index.get_level_values('destination')

zone_index = pipeline.get_table('land_use').index
assert all(zone in zone_index for zone in orig_vals)
assert all(zone in zone_index for zone in dest_vals)

_, orig_index = zone_index.reindex(orig_vals)
_, dest_index = zone_index.reindex(dest_vals)

write_matrices(aggregate_trips, zone_index, orig_index, dest_index, model_settings)


def annotate_trips(trips, skim_dict, skim_stack, model_settings):
"""
Add columns to local trips table. The annotator has
access to the origin/destination skims and everything
defined in the model settings CONSTANTS.

Pipeline tables can also be accessed by listing them under
TABLES in the preprocessor settings.
"""

trips_df = trips.to_frame()

trace_label = 'trip_matrices'

# setup skim keys
assert ('trip_period' not in trips_df)
trips_df['trip_period'] = skim_time_period_label(trips_df.depart)
od_skim_wrapper = skim_dict.wrap('origin', 'destination')
odt_skim_stack_wrapper = skim_stack.wrap(left_key='origin', right_key='destination',
skim_key='trip_period')
skims = {
'od_skims': od_skim_wrapper,
"odt_skims": odt_skim_stack_wrapper
}

locals_dict = {}
constants = config.get_model_constants(model_settings)
if constants is not None:
locals_dict.update(constants)

expressions.annotate_preprocessors(
trips_df, locals_dict, skims,
model_settings, trace_label)

# Data will be expanded by an expansion weight column from
# the households pipeline table, if specified in the model settings.
hh_weight_col = model_settings.get('HH_EXPANSION_WEIGHT_COL')

if hh_weight_col and hh_weight_col not in trips_df:
logger.info("adding '%s' from households to trips table" % hh_weight_col)
household_weights = pipeline.get_table('households')[hh_weight_col]
trips_df[hh_weight_col] = trips_df.household_id.map(household_weights)

return trips_df


def write_matrices(aggregate_trips, zone_index, orig_index, dest_index, model_settings):
"""
Write aggregated trips to OMX format.

The MATRICES setting lists the new OMX files to write.
Each file can contain any number of 'tables', each specified by a
table key ('name') and a trips table column ('data_field') to use
for aggregated counts.

Any data type may be used for columns added in the annotation phase,
but the table 'data_field's must be summable types: ints, floats, bools.
"""

matrix_settings = model_settings.get('MATRICES')

if not matrix_settings:
logger.error('Missing MATRICES setting in write_trip_matrices.yaml')

for matrix in matrix_settings:
filename = matrix.get('file_name')
filepath = config.output_file_path(filename)
logger.info('opening %s' % filepath)
file = omx.open_file(filepath, 'w') # possibly overwrite existing file
table_settings = matrix.get('tables')

for table in table_settings:
table_name = table.get('name')
col = table.get('data_field')

if col not in aggregate_trips:
logger.error('missing %s column in %s DataFrame' % (col, aggregate_trips.name))
return

hh_weight_col = model_settings.get('HH_EXPANSION_WEIGHT_COL')
if hh_weight_col:
aggregate_trips[col] = aggregate_trips[col] / aggregate_trips[hh_weight_col]

data = np.zeros((len(zone_index), len(zone_index)))
data[orig_index, dest_index] = aggregate_trips[col]
logger.info('writing %s' % table_name)
file[table_name] = data # write to file

# include the index-to-zone map in the file
logger.info('adding %s mapping for %s zones to %s' %
(zone_index.name, zone_index.size, filename))
file.create_mapping(zone_index.name, zone_index.to_numpy())

logger.info('closing %s' % filepath)
file.close()
2 changes: 1 addition & 1 deletion activitysim/abm/models/trip_scheduling.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -319,7 +319,7 @@ def schedule_trips_in_leg(
trips = trips.sort_index()
trips['next_trip_id'] = np.roll(trips.index, -1 if outbound else 1)
is_final = (trips.trip_num == trips.trip_count) if outbound else (trips.trip_num == 1)
trips.next_trip_id = trips.next_trip_id.where(is_final, NO_TRIP_ID)
trips.next_trip_id = trips.next_trip_id.where(~is_final, NO_TRIP_ID)

# iterate over outbound trips in ascending trip_num order, skipping the initial trip
# iterate over inbound trips in descending trip_num order, skipping the finial trip
Expand Down
18 changes: 14 additions & 4 deletions activitysim/abm/tables/households.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,9 +20,11 @@
def households(households_sample_size, override_hh_ids, trace_hh_id):

df_full = read_input_table("households")
households_sliced = False
tot_households = df_full.shape[0]

logger.info("full household list contains %s households" % tot_households)

logger.info("full household list contains %s households" % df_full.shape[0])
households_sliced = False

# only using households listed in override_hh_ids
if override_hh_ids is not None:
Expand All@@ -48,9 +50,9 @@ def households(households_sample_size, override_hh_ids, trace_hh_id):
households_sliced = True

# if we need a subset of full store
elif households_sample_size > 0 and df_full.shape[0] > households_sample_size:
elif tot_households > households_sample_size > 0:

logger.info("sampling %s of %s households" % (households_sample_size, df_full.shape[0]))
logger.info("sampling %s of %s households" % (households_sample_size, tot_households))

"""
Because random seed is set differently for each step, sampling of households using
Expand DownExpand Up@@ -80,6 +82,14 @@ def households(households_sample_size, override_hh_ids, trace_hh_id):
# persons table
inject.add_injectable('households_sliced', households_sliced)

if 'sample_rate' not in df.columns:
if households_sample_size == 0:
sample_rate = 1
else:
sample_rate = round(households_sample_size / tot_households, 3)

df['sample_rate'] = sample_rate

logger.info("loaded households %s" % (df.shape,))

# FIXME - pathological knowledge of name of chunk_id column used by chunked_choosers_by_chunk_id
Expand Down
21 changes: 20 additions & 1 deletion activitysim/abm/test/test_pipeline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,12 @@
import logging
import pkg_resources

import openmatrix as omx
import numpy as np
import numpy.testing as npt

import pandas as pd
import pandas.util.testing as pdt
import pandas.testing as pdt
import pytest
import yaml

Expand All@@ -17,6 +21,7 @@

# set the max households for all tests (this is to limit memory use on travis)
HOUSEHOLDS_SAMPLE_SIZE = 100
HOUSEHOLDS_SAMPLE_RATE = 0.02 # HOUSEHOLDS_SAMPLE_RATE / 5000 households

# household with mandatory, non mandatory, atwork_subtours, and joint tours
HH_ID = 257341
Expand DownExpand Up@@ -52,6 +57,7 @@ def setup_dirs(configs_dir, data_dir=None):
tracing.delete_output_files('csv')
tracing.delete_output_files('txt')
tracing.delete_output_files('yaml')
tracing.delete_output_files('omx')


def teardown_function(func):
Expand DownExpand Up@@ -433,6 +439,18 @@ def regress():
# should be at least two tours per trip
assert trips_df.shape[0] >= 2*tours_df.shape[0]

# write_trip_matrices
trip_matrices_file = config.output_file_path('trips_md.omx')
assert os.path.exists(trip_matrices_file)
trip_matrices = omx.open_file(trip_matrices_file)
assert trip_matrices.shape() == (25, 25)

assert 'WALK_MD' in trip_matrices.list_matrices()
walk_trips = np.array(trip_matrices['WALK_MD'])
assert walk_trips.dtype == np.dtype('float64')

trip_matrices.close()


def test_full_run1():

Expand DownExpand Up@@ -517,6 +535,7 @@ def test_full_run5_singleton():

if __name__ == "__main__":

from activitysim import abm # register injectables
print("running test_full_run1")
test_full_run1()
# teardown_function(None)
1 change: 1 addition & 0 deletions activitysim/examples/example_mtc/configs/settings.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,6 +86,7 @@ models:
- trip_mode_choice
- write_data_dictionary
- track_skim_usage
- write_trip_matrices
- write_tables

# to resume after last successful checkpoint, specify resume_after: _
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions activitysim/abm/models/__init__.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,3 +27,4 @@
from . import trip_purpose
from . import trip_purpose_and_destination
from . import trip_scheduling
from . import trip_matrices
147 changes: 147 additions & 0 deletions activitysim/abm/models/trip_matrices.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
# ActivitySim
# See full license in LICENSE.txt.

import logging

import openmatrix as omx
import pandas as pd
import numpy as np

from activitysim.core import config
from activitysim.core import inject
from activitysim.core import pipeline

from .util import expressions
from .util.expressions import skim_time_period_label

logger = logging.getLogger(__name__)


@inject.step()
def write_trip_matrices(trips, skim_dict, skim_stack):
"""
Write trip matrices step.

Adds boolean columns to local trips table via annotation expressions,
then aggregates trip counts and writes OD matrices to OMX. Save annotated
trips table to pipeline if desired.
"""

model_settings = config.read_model_settings('write_trip_matrices.yaml')
trips_df = annotate_trips(trips, skim_dict, skim_stack, model_settings)

if bool(model_settings.get('SAVE_TRIPS_TABLE')):
pipeline.replace_table('trips', trips_df)

logger.info('Aggregating trips...')
aggregate_trips = trips_df.groupby(['origin', 'destination'], sort=False).sum()
logger.info('Finished.')

orig_vals = aggregate_trips.index.get_level_values('origin')
dest_vals = aggregate_trips.index.get_level_values('destination')

zone_index = pipeline.get_table('land_use').index
assert all(zone in zone_index for zone in orig_vals)
assert all(zone in zone_index for zone in dest_vals)

_, orig_index = zone_index.reindex(orig_vals)
_, dest_index = zone_index.reindex(dest_vals)

write_matrices(aggregate_trips, zone_index, orig_index, dest_index, model_settings)


def annotate_trips(trips, skim_dict, skim_stack, model_settings):
"""
Add columns to local trips table. The annotator has
access to the origin/destination skims and everything
defined in the model settings CONSTANTS.

Pipeline tables can also be accessed by listing them under
TABLES in the preprocessor settings.
"""

trips_df = trips.to_frame()

trace_label = 'trip_matrices'

# setup skim keys
assert ('trip_period' not in trips_df)
trips_df['trip_period'] = skim_time_period_label(trips_df.depart)
od_skim_wrapper = skim_dict.wrap('origin', 'destination')
odt_skim_stack_wrapper = skim_stack.wrap(left_key='origin', right_key='destination',
skim_key='trip_period')
skims = {
'od_skims': od_skim_wrapper,
"odt_skims": odt_skim_stack_wrapper
}

locals_dict = {}
constants = config.get_model_constants(model_settings)
if constants is not None:
locals_dict.update(constants)

expressions.annotate_preprocessors(
trips_df, locals_dict, skims,
model_settings, trace_label)

# Data will be expanded by an expansion weight column from
# the households pipeline table, if specified in the model settings.
hh_weight_col = model_settings.get('HH_EXPANSION_WEIGHT_COL')

if hh_weight_col and hh_weight_col not in trips_df:
logger.info("adding '%s' from households to trips table" % hh_weight_col)
household_weights = pipeline.get_table('households')[hh_weight_col]
trips_df[hh_weight_col] = trips_df.household_id.map(household_weights)

return trips_df


def write_matrices(aggregate_trips, zone_index, orig_index, dest_index, model_settings):
"""
Write aggregated trips to OMX format.

The MATRICES setting lists the new OMX files to write.
Each file can contain any number of 'tables', each specified by a
table key ('name') and a trips table column ('data_field') to use
for aggregated counts.

Any data type may be used for columns added in the annotation phase,
but the table 'data_field's must be summable types: ints, floats, bools.
"""

matrix_settings = model_settings.get('MATRICES')

if not matrix_settings:
logger.error('Missing MATRICES setting in write_trip_matrices.yaml')

for matrix in matrix_settings:
filename = matrix.get('file_name')
filepath = config.output_file_path(filename)
logger.info('opening %s' % filepath)
file = omx.open_file(filepath, 'w') # possibly overwrite existing file
table_settings = matrix.get('tables')

for table in table_settings:
table_name = table.get('name')
col = table.get('data_field')

if col not in aggregate_trips:
logger.error('missing %s column in %s DataFrame' % (col, aggregate_trips.name))
return

hh_weight_col = model_settings.get('HH_EXPANSION_WEIGHT_COL')
if hh_weight_col:
aggregate_trips[col] = aggregate_trips[col] / aggregate_trips[hh_weight_col]

data = np.zeros((len(zone_index), len(zone_index)))
data[orig_index, dest_index] = aggregate_trips[col]
logger.info('writing %s' % table_name)
file[table_name] = data # write to file

# include the index-to-zone map in the file
logger.info('adding %s mapping for %s zones to %s' %
(zone_index.name, zone_index.size, filename))
file.create_mapping(zone_index.name, zone_index.to_numpy())

logger.info('closing %s' % filepath)
file.close()
2 changes: 1 addition & 1 deletion activitysim/abm/models/trip_scheduling.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -319,7 +319,7 @@ def schedule_trips_in_leg(
trips = trips.sort_index()
trips['next_trip_id'] = np.roll(trips.index, -1 if outbound else 1)
is_final = (trips.trip_num == trips.trip_count) if outbound else (trips.trip_num == 1)
trips.next_trip_id = trips.next_trip_id.where(is_final, NO_TRIP_ID)
trips.next_trip_id = trips.next_trip_id.where(~is_final, NO_TRIP_ID)

# iterate over outbound trips in ascending trip_num order, skipping the initial trip
# iterate over inbound trips in descending trip_num order, skipping the finial trip
Expand Down
18 changes: 14 additions & 4 deletions activitysim/abm/tables/households.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,9 +20,11 @@
def households(households_sample_size, override_hh_ids, trace_hh_id):

df_full = read_input_table("households")
households_sliced = False
tot_households = df_full.shape[0]

logger.info("full household list contains %s households" % tot_households)

logger.info("full household list contains %s households" % df_full.shape[0])
households_sliced = False

# only using households listed in override_hh_ids
if override_hh_ids is not None:
Expand All@@ -48,9 +50,9 @@ def households(households_sample_size, override_hh_ids, trace_hh_id):
households_sliced = True

# if we need a subset of full store
elif households_sample_size > 0 and df_full.shape[0] > households_sample_size:
elif tot_households > households_sample_size > 0:

logger.info("sampling %s of %s households" % (households_sample_size, df_full.shape[0]))
logger.info("sampling %s of %s households" % (households_sample_size, tot_households))

"""
Because random seed is set differently for each step, sampling of households using
Expand DownExpand Up@@ -80,6 +82,14 @@ def households(households_sample_size, override_hh_ids, trace_hh_id):
# persons table
inject.add_injectable('households_sliced', households_sliced)

if 'sample_rate' not in df.columns:
if households_sample_size == 0:
sample_rate = 1
else:
sample_rate = round(households_sample_size / tot_households, 3)

df['sample_rate'] = sample_rate

logger.info("loaded households %s" % (df.shape,))

# FIXME - pathological knowledge of name of chunk_id column used by chunked_choosers_by_chunk_id
Expand Down
21 changes: 20 additions & 1 deletion activitysim/abm/test/test_pipeline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,12 @@
import logging
import pkg_resources

import openmatrix as omx
import numpy as np
import numpy.testing as npt

import pandas as pd
import pandas.util.testing as pdt
import pandas.testing as pdt
import pytest
import yaml

Expand All@@ -17,6 +21,7 @@

# set the max households for all tests (this is to limit memory use on travis)
HOUSEHOLDS_SAMPLE_SIZE = 100
HOUSEHOLDS_SAMPLE_RATE = 0.02 # HOUSEHOLDS_SAMPLE_RATE / 5000 households

# household with mandatory, non mandatory, atwork_subtours, and joint tours
HH_ID = 257341
Expand DownExpand Up@@ -52,6 +57,7 @@ def setup_dirs(configs_dir, data_dir=None):
tracing.delete_output_files('csv')
tracing.delete_output_files('txt')
tracing.delete_output_files('yaml')
tracing.delete_output_files('omx')


def teardown_function(func):
Expand DownExpand Up@@ -433,6 +439,18 @@ def regress():
# should be at least two tours per trip
assert trips_df.shape[0] >= 2*tours_df.shape[0]

# write_trip_matrices
trip_matrices_file = config.output_file_path('trips_md.omx')
assert os.path.exists(trip_matrices_file)
trip_matrices = omx.open_file(trip_matrices_file)
assert trip_matrices.shape() == (25, 25)

assert 'WALK_MD' in trip_matrices.list_matrices()
walk_trips = np.array(trip_matrices['WALK_MD'])
assert walk_trips.dtype == np.dtype('float64')

trip_matrices.close()


def test_full_run1():

Expand DownExpand Up@@ -517,6 +535,7 @@ def test_full_run5_singleton():

if __name__ == "__main__":

from activitysim import abm # register injectables
print("running test_full_run1")
test_full_run1()
# teardown_function(None)
1 change: 1 addition & 0 deletions activitysim/examples/example_mtc/configs/settings.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,6 +86,7 @@ models:
- trip_mode_choice
- write_data_dictionary
- track_skim_usage
- write_trip_matrices
- write_tables

# to resume after last successful checkpoint, specify resume_after: _
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions activitysim/abm/models/__init__.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,3 +27,4 @@
from . import trip_purpose
from . import trip_purpose_and_destination
from . import trip_scheduling
from . import trip_matrices
147 changes: 147 additions & 0 deletions activitysim/abm/models/trip_matrices.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
# ActivitySim
# See full license in LICENSE.txt.

import logging

import openmatrix as omx
import pandas as pd
import numpy as np

from activitysim.core import config
from activitysim.core import inject
from activitysim.core import pipeline

from .util import expressions
from .util.expressions import skim_time_period_label

logger = logging.getLogger(__name__)


@inject.step()
def write_trip_matrices(trips, skim_dict, skim_stack):
"""
Write trip matrices step.

Adds boolean columns to local trips table via annotation expressions,
then aggregates trip counts and writes OD matrices to OMX. Save annotated
trips table to pipeline if desired.
"""

model_settings = config.read_model_settings('write_trip_matrices.yaml')
trips_df = annotate_trips(trips, skim_dict, skim_stack, model_settings)

if bool(model_settings.get('SAVE_TRIPS_TABLE')):
pipeline.replace_table('trips', trips_df)

logger.info('Aggregating trips...')
aggregate_trips = trips_df.groupby(['origin', 'destination'], sort=False).sum()
logger.info('Finished.')

orig_vals = aggregate_trips.index.get_level_values('origin')
dest_vals = aggregate_trips.index.get_level_values('destination')

zone_index = pipeline.get_table('land_use').index
assert all(zone in zone_index for zone in orig_vals)
assert all(zone in zone_index for zone in dest_vals)

_, orig_index = zone_index.reindex(orig_vals)
_, dest_index = zone_index.reindex(dest_vals)

write_matrices(aggregate_trips, zone_index, orig_index, dest_index, model_settings)


def annotate_trips(trips, skim_dict, skim_stack, model_settings):
"""
Add columns to local trips table. The annotator has
access to the origin/destination skims and everything
defined in the model settings CONSTANTS.

Pipeline tables can also be accessed by listing them under
TABLES in the preprocessor settings.
"""

trips_df = trips.to_frame()

trace_label = 'trip_matrices'

# setup skim keys
assert ('trip_period' not in trips_df)
trips_df['trip_period'] = skim_time_period_label(trips_df.depart)
od_skim_wrapper = skim_dict.wrap('origin', 'destination')
odt_skim_stack_wrapper = skim_stack.wrap(left_key='origin', right_key='destination',
skim_key='trip_period')
skims = {
'od_skims': od_skim_wrapper,
"odt_skims": odt_skim_stack_wrapper
}

locals_dict = {}
constants = config.get_model_constants(model_settings)
if constants is not None:
locals_dict.update(constants)

expressions.annotate_preprocessors(
trips_df, locals_dict, skims,
model_settings, trace_label)

# Data will be expanded by an expansion weight column from
# the households pipeline table, if specified in the model settings.
hh_weight_col = model_settings.get('HH_EXPANSION_WEIGHT_COL')

if hh_weight_col and hh_weight_col not in trips_df:
logger.info("adding '%s' from households to trips table" % hh_weight_col)
household_weights = pipeline.get_table('households')[hh_weight_col]
trips_df[hh_weight_col] = trips_df.household_id.map(household_weights)

return trips_df


def write_matrices(aggregate_trips, zone_index, orig_index, dest_index, model_settings):
"""
Write aggregated trips to OMX format.

The MATRICES setting lists the new OMX files to write.
Each file can contain any number of 'tables', each specified by a
table key ('name') and a trips table column ('data_field') to use
for aggregated counts.

Any data type may be used for columns added in the annotation phase,
but the table 'data_field's must be summable types: ints, floats, bools.
"""

matrix_settings = model_settings.get('MATRICES')

if not matrix_settings:
logger.error('Missing MATRICES setting in write_trip_matrices.yaml')

for matrix in matrix_settings:
filename = matrix.get('file_name')
filepath = config.output_file_path(filename)
logger.info('opening %s' % filepath)
file = omx.open_file(filepath, 'w') # possibly overwrite existing file
table_settings = matrix.get('tables')

for table in table_settings:
table_name = table.get('name')
col = table.get('data_field')

if col not in aggregate_trips:
logger.error('missing %s column in %s DataFrame' % (col, aggregate_trips.name))
return

hh_weight_col = model_settings.get('HH_EXPANSION_WEIGHT_COL')
if hh_weight_col:
aggregate_trips[col] = aggregate_trips[col] / aggregate_trips[hh_weight_col]

data = np.zeros((len(zone_index), len(zone_index)))
data[orig_index, dest_index] = aggregate_trips[col]
logger.info('writing %s' % table_name)
file[table_name] = data # write to file

# include the index-to-zone map in the file
logger.info('adding %s mapping for %s zones to %s' %
(zone_index.name, zone_index.size, filename))
file.create_mapping(zone_index.name, zone_index.to_numpy())

logger.info('closing %s' % filepath)
file.close()
2 changes: 1 addition & 1 deletion activitysim/abm/models/trip_scheduling.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -319,7 +319,7 @@ def schedule_trips_in_leg(
trips = trips.sort_index()
trips['next_trip_id'] = np.roll(trips.index, -1 if outbound else 1)
is_final = (trips.trip_num == trips.trip_count) if outbound else (trips.trip_num == 1)
trips.next_trip_id = trips.next_trip_id.where(is_final, NO_TRIP_ID)
trips.next_trip_id = trips.next_trip_id.where(~is_final, NO_TRIP_ID)

# iterate over outbound trips in ascending trip_num order, skipping the initial trip
# iterate over inbound trips in descending trip_num order, skipping the finial trip
Expand Down
18 changes: 14 additions & 4 deletions activitysim/abm/tables/households.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,9 +20,11 @@
def households(households_sample_size, override_hh_ids, trace_hh_id):

df_full = read_input_table("households")
households_sliced = False
tot_households = df_full.shape[0]

logger.info("full household list contains %s households" % tot_households)

logger.info("full household list contains %s households" % df_full.shape[0])
households_sliced = False

# only using households listed in override_hh_ids
if override_hh_ids is not None:
Expand All@@ -48,9 +50,9 @@ def households(households_sample_size, override_hh_ids, trace_hh_id):
households_sliced = True

# if we need a subset of full store
elif households_sample_size > 0 and df_full.shape[0] > households_sample_size:
elif tot_households > households_sample_size > 0:

logger.info("sampling %s of %s households" % (households_sample_size, df_full.shape[0]))
logger.info("sampling %s of %s households" % (households_sample_size, tot_households))

"""
Because random seed is set differently for each step, sampling of households using
Expand DownExpand Up@@ -80,6 +82,14 @@ def households(households_sample_size, override_hh_ids, trace_hh_id):
# persons table
inject.add_injectable('households_sliced', households_sliced)

if 'sample_rate' not in df.columns:
if households_sample_size == 0:
sample_rate = 1
else:
sample_rate = round(households_sample_size / tot_households, 3)

df['sample_rate'] = sample_rate

logger.info("loaded households %s" % (df.shape,))

# FIXME - pathological knowledge of name of chunk_id column used by chunked_choosers_by_chunk_id
Expand Down
21 changes: 20 additions & 1 deletion activitysim/abm/test/test_pipeline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,12 @@
import logging
import pkg_resources

import openmatrix as omx
import numpy as np
import numpy.testing as npt

import pandas as pd
import pandas.util.testing as pdt
import pandas.testing as pdt
import pytest
import yaml

Expand All@@ -17,6 +21,7 @@

# set the max households for all tests (this is to limit memory use on travis)
HOUSEHOLDS_SAMPLE_SIZE = 100
HOUSEHOLDS_SAMPLE_RATE = 0.02 # HOUSEHOLDS_SAMPLE_RATE / 5000 households

# household with mandatory, non mandatory, atwork_subtours, and joint tours
HH_ID = 257341
Expand DownExpand Up@@ -52,6 +57,7 @@ def setup_dirs(configs_dir, data_dir=None):
tracing.delete_output_files('csv')
tracing.delete_output_files('txt')
tracing.delete_output_files('yaml')
tracing.delete_output_files('omx')


def teardown_function(func):
Expand DownExpand Up@@ -433,6 +439,18 @@ def regress():
# should be at least two tours per trip
assert trips_df.shape[0] >= 2*tours_df.shape[0]

# write_trip_matrices
trip_matrices_file = config.output_file_path('trips_md.omx')
assert os.path.exists(trip_matrices_file)
trip_matrices = omx.open_file(trip_matrices_file)
assert trip_matrices.shape() == (25, 25)

assert 'WALK_MD' in trip_matrices.list_matrices()
walk_trips = np.array(trip_matrices['WALK_MD'])
assert walk_trips.dtype == np.dtype('float64')

trip_matrices.close()


def test_full_run1():

Expand DownExpand Up@@ -517,6 +535,7 @@ def test_full_run5_singleton():

if __name__ == "__main__":

from activitysim import abm # register injectables
print("running test_full_run1")
test_full_run1()
# teardown_function(None)
1 change: 1 addition & 0 deletions activitysim/examples/example_mtc/configs/settings.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,6 +86,7 @@ models:
- trip_mode_choice
- write_data_dictionary
- track_skim_usage
- write_trip_matrices
- write_tables

# to resume after last successful checkpoint, specify resume_after: _
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions activitysim/abm/models/__init__.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,3 +27,4 @@
from . import trip_purpose
from . import trip_purpose_and_destination
from . import trip_scheduling
from . import trip_matrices
147 changes: 147 additions & 0 deletions activitysim/abm/models/trip_matrices.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
# ActivitySim
# See full license in LICENSE.txt.

import logging

import openmatrix as omx
import pandas as pd
import numpy as np

from activitysim.core import config
from activitysim.core import inject
from activitysim.core import pipeline

from .util import expressions
from .util.expressions import skim_time_period_label

logger = logging.getLogger(__name__)


@inject.step()
def write_trip_matrices(trips, skim_dict, skim_stack):
"""
Write trip matrices step.

Adds boolean columns to local trips table via annotation expressions,
then aggregates trip counts and writes OD matrices to OMX. Save annotated
trips table to pipeline if desired.
"""

model_settings = config.read_model_settings('write_trip_matrices.yaml')
trips_df = annotate_trips(trips, skim_dict, skim_stack, model_settings)

if bool(model_settings.get('SAVE_TRIPS_TABLE')):
pipeline.replace_table('trips', trips_df)

logger.info('Aggregating trips...')
aggregate_trips = trips_df.groupby(['origin', 'destination'], sort=False).sum()
logger.info('Finished.')

orig_vals = aggregate_trips.index.get_level_values('origin')
dest_vals = aggregate_trips.index.get_level_values('destination')

zone_index = pipeline.get_table('land_use').index
assert all(zone in zone_index for zone in orig_vals)
assert all(zone in zone_index for zone in dest_vals)

_, orig_index = zone_index.reindex(orig_vals)
_, dest_index = zone_index.reindex(dest_vals)

write_matrices(aggregate_trips, zone_index, orig_index, dest_index, model_settings)


def annotate_trips(trips, skim_dict, skim_stack, model_settings):
"""
Add columns to local trips table. The annotator has
access to the origin/destination skims and everything
defined in the model settings CONSTANTS.

Pipeline tables can also be accessed by listing them under
TABLES in the preprocessor settings.
"""

trips_df = trips.to_frame()

trace_label = 'trip_matrices'

# setup skim keys
assert ('trip_period' not in trips_df)
trips_df['trip_period'] = skim_time_period_label(trips_df.depart)
od_skim_wrapper = skim_dict.wrap('origin', 'destination')
odt_skim_stack_wrapper = skim_stack.wrap(left_key='origin', right_key='destination',
skim_key='trip_period')
skims = {
'od_skims': od_skim_wrapper,
"odt_skims": odt_skim_stack_wrapper
}

locals_dict = {}
constants = config.get_model_constants(model_settings)
if constants is not None:
locals_dict.update(constants)

expressions.annotate_preprocessors(
trips_df, locals_dict, skims,
model_settings, trace_label)

# Data will be expanded by an expansion weight column from
# the households pipeline table, if specified in the model settings.
hh_weight_col = model_settings.get('HH_EXPANSION_WEIGHT_COL')

if hh_weight_col and hh_weight_col not in trips_df:
logger.info("adding '%s' from households to trips table" % hh_weight_col)
household_weights = pipeline.get_table('households')[hh_weight_col]
trips_df[hh_weight_col] = trips_df.household_id.map(household_weights)

return trips_df


def write_matrices(aggregate_trips, zone_index, orig_index, dest_index, model_settings):
"""
Write aggregated trips to OMX format.

The MATRICES setting lists the new OMX files to write.
Each file can contain any number of 'tables', each specified by a
table key ('name') and a trips table column ('data_field') to use
for aggregated counts.

Any data type may be used for columns added in the annotation phase,
but the table 'data_field's must be summable types: ints, floats, bools.
"""

matrix_settings = model_settings.get('MATRICES')

if not matrix_settings:
logger.error('Missing MATRICES setting in write_trip_matrices.yaml')

for matrix in matrix_settings:
filename = matrix.get('file_name')
filepath = config.output_file_path(filename)
logger.info('opening %s' % filepath)
file = omx.open_file(filepath, 'w') # possibly overwrite existing file
table_settings = matrix.get('tables')

for table in table_settings:
table_name = table.get('name')
col = table.get('data_field')

if col not in aggregate_trips:
logger.error('missing %s column in %s DataFrame' % (col, aggregate_trips.name))
return

hh_weight_col = model_settings.get('HH_EXPANSION_WEIGHT_COL')
if hh_weight_col:
aggregate_trips[col] = aggregate_trips[col] / aggregate_trips[hh_weight_col]

data = np.zeros((len(zone_index), len(zone_index)))
data[orig_index, dest_index] = aggregate_trips[col]
logger.info('writing %s' % table_name)
file[table_name] = data # write to file

# include the index-to-zone map in the file
logger.info('adding %s mapping for %s zones to %s' %
(zone_index.name, zone_index.size, filename))
file.create_mapping(zone_index.name, zone_index.to_numpy())

logger.info('closing %s' % filepath)
file.close()
2 changes: 1 addition & 1 deletion activitysim/abm/models/trip_scheduling.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -319,7 +319,7 @@ def schedule_trips_in_leg(
trips = trips.sort_index()
trips['next_trip_id'] = np.roll(trips.index, -1 if outbound else 1)
is_final = (trips.trip_num == trips.trip_count) if outbound else (trips.trip_num == 1)
trips.next_trip_id = trips.next_trip_id.where(is_final, NO_TRIP_ID)
trips.next_trip_id = trips.next_trip_id.where(~is_final, NO_TRIP_ID)

# iterate over outbound trips in ascending trip_num order, skipping the initial trip
# iterate over inbound trips in descending trip_num order, skipping the finial trip
Expand Down
18 changes: 14 additions & 4 deletions activitysim/abm/tables/households.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,9 +20,11 @@
def households(households_sample_size, override_hh_ids, trace_hh_id):

df_full = read_input_table("households")
households_sliced = False
tot_households = df_full.shape[0]

logger.info("full household list contains %s households" % tot_households)

logger.info("full household list contains %s households" % df_full.shape[0])
households_sliced = False

# only using households listed in override_hh_ids
if override_hh_ids is not None:
Expand All@@ -48,9 +50,9 @@ def households(households_sample_size, override_hh_ids, trace_hh_id):
households_sliced = True

# if we need a subset of full store
elif households_sample_size > 0 and df_full.shape[0] > households_sample_size:
elif tot_households > households_sample_size > 0:

logger.info("sampling %s of %s households" % (households_sample_size, df_full.shape[0]))
logger.info("sampling %s of %s households" % (households_sample_size, tot_households))

"""
Because random seed is set differently for each step, sampling of households using
Expand DownExpand Up@@ -80,6 +82,14 @@ def households(households_sample_size, override_hh_ids, trace_hh_id):
# persons table
inject.add_injectable('households_sliced', households_sliced)

if 'sample_rate' not in df.columns:
if households_sample_size == 0:
sample_rate = 1
else:
sample_rate = round(households_sample_size / tot_households, 3)

df['sample_rate'] = sample_rate

logger.info("loaded households %s" % (df.shape,))

# FIXME - pathological knowledge of name of chunk_id column used by chunked_choosers_by_chunk_id
Expand Down
21 changes: 20 additions & 1 deletion activitysim/abm/test/test_pipeline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,12 @@
import logging
import pkg_resources

import openmatrix as omx
import numpy as np
import numpy.testing as npt

import pandas as pd
import pandas.util.testing as pdt
import pandas.testing as pdt
import pytest
import yaml

Expand All@@ -17,6 +21,7 @@

# set the max households for all tests (this is to limit memory use on travis)
HOUSEHOLDS_SAMPLE_SIZE = 100
HOUSEHOLDS_SAMPLE_RATE = 0.02 # HOUSEHOLDS_SAMPLE_RATE / 5000 households

# household with mandatory, non mandatory, atwork_subtours, and joint tours
HH_ID = 257341
Expand DownExpand Up@@ -52,6 +57,7 @@ def setup_dirs(configs_dir, data_dir=None):
tracing.delete_output_files('csv')
tracing.delete_output_files('txt')
tracing.delete_output_files('yaml')
tracing.delete_output_files('omx')


def teardown_function(func):
Expand DownExpand Up@@ -433,6 +439,18 @@ def regress():
# should be at least two tours per trip
assert trips_df.shape[0] >= 2*tours_df.shape[0]

# write_trip_matrices
trip_matrices_file = config.output_file_path('trips_md.omx')
assert os.path.exists(trip_matrices_file)
trip_matrices = omx.open_file(trip_matrices_file)
assert trip_matrices.shape() == (25, 25)

assert 'WALK_MD' in trip_matrices.list_matrices()
walk_trips = np.array(trip_matrices['WALK_MD'])
assert walk_trips.dtype == np.dtype('float64')

trip_matrices.close()


def test_full_run1():

Expand DownExpand Up@@ -517,6 +535,7 @@ def test_full_run5_singleton():

if __name__ == "__main__":

from activitysim import abm # register injectables
print("running test_full_run1")
test_full_run1()
# teardown_function(None)
1 change: 1 addition & 0 deletions activitysim/examples/example_mtc/configs/settings.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,6 +86,7 @@ models:
- trip_mode_choice
- write_data_dictionary
- track_skim_usage
- write_trip_matrices
- write_tables

# to resume after last successful checkpoint, specify resume_after: _
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions activitysim/abm/models/__init__.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,3 +27,4 @@
from . import trip_purpose
from . import trip_purpose_and_destination
from . import trip_scheduling
from . import trip_matrices
147 changes: 147 additions & 0 deletions activitysim/abm/models/trip_matrices.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
# ActivitySim
# See full license in LICENSE.txt.

import logging

import openmatrix as omx
import pandas as pd
import numpy as np

from activitysim.core import config
from activitysim.core import inject
from activitysim.core import pipeline

from .util import expressions
from .util.expressions import skim_time_period_label

logger = logging.getLogger(__name__)


@inject.step()
def write_trip_matrices(trips, skim_dict, skim_stack):
"""
Write trip matrices step.

Adds boolean columns to local trips table via annotation expressions,
then aggregates trip counts and writes OD matrices to OMX. Save annotated
trips table to pipeline if desired.
"""

model_settings = config.read_model_settings('write_trip_matrices.yaml')
trips_df = annotate_trips(trips, skim_dict, skim_stack, model_settings)

if bool(model_settings.get('SAVE_TRIPS_TABLE')):
pipeline.replace_table('trips', trips_df)

logger.info('Aggregating trips...')
aggregate_trips = trips_df.groupby(['origin', 'destination'], sort=False).sum()
logger.info('Finished.')

orig_vals = aggregate_trips.index.get_level_values('origin')
dest_vals = aggregate_trips.index.get_level_values('destination')

zone_index = pipeline.get_table('land_use').index
assert all(zone in zone_index for zone in orig_vals)
assert all(zone in zone_index for zone in dest_vals)

_, orig_index = zone_index.reindex(orig_vals)
_, dest_index = zone_index.reindex(dest_vals)

write_matrices(aggregate_trips, zone_index, orig_index, dest_index, model_settings)


def annotate_trips(trips, skim_dict, skim_stack, model_settings):
"""
Add columns to local trips table. The annotator has
access to the origin/destination skims and everything
defined in the model settings CONSTANTS.

Pipeline tables can also be accessed by listing them under
TABLES in the preprocessor settings.
"""

trips_df = trips.to_frame()

trace_label = 'trip_matrices'

# setup skim keys
assert ('trip_period' not in trips_df)
trips_df['trip_period'] = skim_time_period_label(trips_df.depart)
od_skim_wrapper = skim_dict.wrap('origin', 'destination')
odt_skim_stack_wrapper = skim_stack.wrap(left_key='origin', right_key='destination',
skim_key='trip_period')
skims = {
'od_skims': od_skim_wrapper,
"odt_skims": odt_skim_stack_wrapper
}

locals_dict = {}
constants = config.get_model_constants(model_settings)
if constants is not None:
locals_dict.update(constants)

expressions.annotate_preprocessors(
trips_df, locals_dict, skims,
model_settings, trace_label)

# Data will be expanded by an expansion weight column from
# the households pipeline table, if specified in the model settings.
hh_weight_col = model_settings.get('HH_EXPANSION_WEIGHT_COL')

if hh_weight_col and hh_weight_col not in trips_df:
logger.info("adding '%s' from households to trips table" % hh_weight_col)
household_weights = pipeline.get_table('households')[hh_weight_col]
trips_df[hh_weight_col] = trips_df.household_id.map(household_weights)

return trips_df


def write_matrices(aggregate_trips, zone_index, orig_index, dest_index, model_settings):
"""
Write aggregated trips to OMX format.

The MATRICES setting lists the new OMX files to write.
Each file can contain any number of 'tables', each specified by a
table key ('name') and a trips table column ('data_field') to use
for aggregated counts.

Any data type may be used for columns added in the annotation phase,
but the table 'data_field's must be summable types: ints, floats, bools.
"""

matrix_settings = model_settings.get('MATRICES')

if not matrix_settings:
logger.error('Missing MATRICES setting in write_trip_matrices.yaml')

for matrix in matrix_settings:
filename = matrix.get('file_name')
filepath = config.output_file_path(filename)
logger.info('opening %s' % filepath)
file = omx.open_file(filepath, 'w') # possibly overwrite existing file
table_settings = matrix.get('tables')

for table in table_settings:
table_name = table.get('name')
col = table.get('data_field')

if col not in aggregate_trips:
logger.error('missing %s column in %s DataFrame' % (col, aggregate_trips.name))
return

hh_weight_col = model_settings.get('HH_EXPANSION_WEIGHT_COL')
if hh_weight_col:
aggregate_trips[col] = aggregate_trips[col] / aggregate_trips[hh_weight_col]

data = np.zeros((len(zone_index), len(zone_index)))
data[orig_index, dest_index] = aggregate_trips[col]
logger.info('writing %s' % table_name)
file[table_name] = data # write to file

# include the index-to-zone map in the file
logger.info('adding %s mapping for %s zones to %s' %
(zone_index.name, zone_index.size, filename))
file.create_mapping(zone_index.name, zone_index.to_numpy())

logger.info('closing %s' % filepath)
file.close()
2 changes: 1 addition & 1 deletion activitysim/abm/models/trip_scheduling.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -319,7 +319,7 @@ def schedule_trips_in_leg(
trips = trips.sort_index()
trips['next_trip_id'] = np.roll(trips.index, -1 if outbound else 1)
is_final = (trips.trip_num == trips.trip_count) if outbound else (trips.trip_num == 1)
trips.next_trip_id = trips.next_trip_id.where(is_final, NO_TRIP_ID)
trips.next_trip_id = trips.next_trip_id.where(~is_final, NO_TRIP_ID)

# iterate over outbound trips in ascending trip_num order, skipping the initial trip
# iterate over inbound trips in descending trip_num order, skipping the finial trip
Expand Down
18 changes: 14 additions & 4 deletions activitysim/abm/tables/households.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,9 +20,11 @@
def households(households_sample_size, override_hh_ids, trace_hh_id):

df_full = read_input_table("households")
households_sliced = False
tot_households = df_full.shape[0]

logger.info("full household list contains %s households" % tot_households)

logger.info("full household list contains %s households" % df_full.shape[0])
households_sliced = False

# only using households listed in override_hh_ids
if override_hh_ids is not None:
Expand All@@ -48,9 +50,9 @@ def households(households_sample_size, override_hh_ids, trace_hh_id):
households_sliced = True

# if we need a subset of full store
elif households_sample_size > 0 and df_full.shape[0] > households_sample_size:
elif tot_households > households_sample_size > 0:

logger.info("sampling %s of %s households" % (households_sample_size, df_full.shape[0]))
logger.info("sampling %s of %s households" % (households_sample_size, tot_households))

"""
Because random seed is set differently for each step, sampling of households using
Expand DownExpand Up@@ -80,6 +82,14 @@ def households(households_sample_size, override_hh_ids, trace_hh_id):
# persons table
inject.add_injectable('households_sliced', households_sliced)

if 'sample_rate' not in df.columns:
if households_sample_size == 0:
sample_rate = 1
else:
sample_rate = round(households_sample_size / tot_households, 3)

df['sample_rate'] = sample_rate

logger.info("loaded households %s" % (df.shape,))

# FIXME - pathological knowledge of name of chunk_id column used by chunked_choosers_by_chunk_id
Expand Down
21 changes: 20 additions & 1 deletion activitysim/abm/test/test_pipeline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,12 @@
import logging
import pkg_resources

import openmatrix as omx
import numpy as np
import numpy.testing as npt

import pandas as pd
import pandas.util.testing as pdt
import pandas.testing as pdt
import pytest
import yaml

Expand All@@ -17,6 +21,7 @@

# set the max households for all tests (this is to limit memory use on travis)
HOUSEHOLDS_SAMPLE_SIZE = 100
HOUSEHOLDS_SAMPLE_RATE = 0.02 # HOUSEHOLDS_SAMPLE_RATE / 5000 households

# household with mandatory, non mandatory, atwork_subtours, and joint tours
HH_ID = 257341
Expand DownExpand Up@@ -52,6 +57,7 @@ def setup_dirs(configs_dir, data_dir=None):
tracing.delete_output_files('csv')
tracing.delete_output_files('txt')
tracing.delete_output_files('yaml')
tracing.delete_output_files('omx')


def teardown_function(func):
Expand DownExpand Up@@ -433,6 +439,18 @@ def regress():
# should be at least two tours per trip
assert trips_df.shape[0] >= 2*tours_df.shape[0]

# write_trip_matrices
trip_matrices_file = config.output_file_path('trips_md.omx')
assert os.path.exists(trip_matrices_file)
trip_matrices = omx.open_file(trip_matrices_file)
assert trip_matrices.shape() == (25, 25)

assert 'WALK_MD' in trip_matrices.list_matrices()
walk_trips = np.array(trip_matrices['WALK_MD'])
assert walk_trips.dtype == np.dtype('float64')

trip_matrices.close()


def test_full_run1():

Expand DownExpand Up@@ -517,6 +535,7 @@ def test_full_run5_singleton():

if __name__ == "__main__":

from activitysim import abm # register injectables
print("running test_full_run1")
test_full_run1()
# teardown_function(None)
1 change: 1 addition & 0 deletions activitysim/examples/example_mtc/configs/settings.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,6 +86,7 @@ models:
- trip_mode_choice
- write_data_dictionary
- track_skim_usage
- write_trip_matrices
- write_tables

# to resume after last successful checkpoint, specify resume_after: _
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions activitysim/abm/models/__init__.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,3 +27,4 @@
from . import trip_purpose
from . import trip_purpose_and_destination
from . import trip_scheduling
from . import trip_matrices
147 changes: 147 additions & 0 deletions activitysim/abm/models/trip_matrices.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
# ActivitySim
# See full license in LICENSE.txt.

import logging

import openmatrix as omx
import pandas as pd
import numpy as np

from activitysim.core import config
from activitysim.core import inject
from activitysim.core import pipeline

from .util import expressions
from .util.expressions import skim_time_period_label

logger = logging.getLogger(__name__)


@inject.step()
def write_trip_matrices(trips, skim_dict, skim_stack):
"""
Write trip matrices step.

Adds boolean columns to local trips table via annotation expressions,
then aggregates trip counts and writes OD matrices to OMX. Save annotated
trips table to pipeline if desired.
"""

model_settings = config.read_model_settings('write_trip_matrices.yaml')
trips_df = annotate_trips(trips, skim_dict, skim_stack, model_settings)

if bool(model_settings.get('SAVE_TRIPS_TABLE')):
pipeline.replace_table('trips', trips_df)

logger.info('Aggregating trips...')
aggregate_trips = trips_df.groupby(['origin', 'destination'], sort=False).sum()
logger.info('Finished.')

orig_vals = aggregate_trips.index.get_level_values('origin')
dest_vals = aggregate_trips.index.get_level_values('destination')

zone_index = pipeline.get_table('land_use').index
assert all(zone in zone_index for zone in orig_vals)
assert all(zone in zone_index for zone in dest_vals)

_, orig_index = zone_index.reindex(orig_vals)
_, dest_index = zone_index.reindex(dest_vals)

write_matrices(aggregate_trips, zone_index, orig_index, dest_index, model_settings)


def annotate_trips(trips, skim_dict, skim_stack, model_settings):
"""
Add columns to local trips table. The annotator has
access to the origin/destination skims and everything
defined in the model settings CONSTANTS.

Pipeline tables can also be accessed by listing them under
TABLES in the preprocessor settings.
"""

trips_df = trips.to_frame()

trace_label = 'trip_matrices'

# setup skim keys
assert ('trip_period' not in trips_df)
trips_df['trip_period'] = skim_time_period_label(trips_df.depart)
od_skim_wrapper = skim_dict.wrap('origin', 'destination')
odt_skim_stack_wrapper = skim_stack.wrap(left_key='origin', right_key='destination',
skim_key='trip_period')
skims = {
'od_skims': od_skim_wrapper,
"odt_skims": odt_skim_stack_wrapper
}

locals_dict = {}
constants = config.get_model_constants(model_settings)
if constants is not None:
locals_dict.update(constants)

expressions.annotate_preprocessors(
trips_df, locals_dict, skims,
model_settings, trace_label)

# Data will be expanded by an expansion weight column from
# the households pipeline table, if specified in the model settings.
hh_weight_col = model_settings.get('HH_EXPANSION_WEIGHT_COL')

if hh_weight_col and hh_weight_col not in trips_df:
logger.info("adding '%s' from households to trips table" % hh_weight_col)
household_weights = pipeline.get_table('households')[hh_weight_col]
trips_df[hh_weight_col] = trips_df.household_id.map(household_weights)

return trips_df


def write_matrices(aggregate_trips, zone_index, orig_index, dest_index, model_settings):
"""
Write aggregated trips to OMX format.

The MATRICES setting lists the new OMX files to write.
Each file can contain any number of 'tables', each specified by a
table key ('name') and a trips table column ('data_field') to use
for aggregated counts.

Any data type may be used for columns added in the annotation phase,
but the table 'data_field's must be summable types: ints, floats, bools.
"""

matrix_settings = model_settings.get('MATRICES')

if not matrix_settings:
logger.error('Missing MATRICES setting in write_trip_matrices.yaml')

for matrix in matrix_settings:
filename = matrix.get('file_name')
filepath = config.output_file_path(filename)
logger.info('opening %s' % filepath)
file = omx.open_file(filepath, 'w') # possibly overwrite existing file
table_settings = matrix.get('tables')

for table in table_settings:
table_name = table.get('name')
col = table.get('data_field')

if col not in aggregate_trips:
logger.error('missing %s column in %s DataFrame' % (col, aggregate_trips.name))
return

hh_weight_col = model_settings.get('HH_EXPANSION_WEIGHT_COL')
if hh_weight_col:
aggregate_trips[col] = aggregate_trips[col] / aggregate_trips[hh_weight_col]

data = np.zeros((len(zone_index), len(zone_index)))
data[orig_index, dest_index] = aggregate_trips[col]
logger.info('writing %s' % table_name)
file[table_name] = data # write to file

# include the index-to-zone map in the file
logger.info('adding %s mapping for %s zones to %s' %
(zone_index.name, zone_index.size, filename))
file.create_mapping(zone_index.name, zone_index.to_numpy())

logger.info('closing %s' % filepath)
file.close()
2 changes: 1 addition & 1 deletion activitysim/abm/models/trip_scheduling.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -319,7 +319,7 @@ def schedule_trips_in_leg(
trips = trips.sort_index()
trips['next_trip_id'] = np.roll(trips.index, -1 if outbound else 1)
is_final = (trips.trip_num == trips.trip_count) if outbound else (trips.trip_num == 1)
trips.next_trip_id = trips.next_trip_id.where(is_final, NO_TRIP_ID)
trips.next_trip_id = trips.next_trip_id.where(~is_final, NO_TRIP_ID)

# iterate over outbound trips in ascending trip_num order, skipping the initial trip
# iterate over inbound trips in descending trip_num order, skipping the finial trip
Expand Down
18 changes: 14 additions & 4 deletions activitysim/abm/tables/households.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,9 +20,11 @@
def households(households_sample_size, override_hh_ids, trace_hh_id):

df_full = read_input_table("households")
households_sliced = False
tot_households = df_full.shape[0]

logger.info("full household list contains %s households" % tot_households)

logger.info("full household list contains %s households" % df_full.shape[0])
households_sliced = False

# only using households listed in override_hh_ids
if override_hh_ids is not None:
Expand All@@ -48,9 +50,9 @@ def households(households_sample_size, override_hh_ids, trace_hh_id):
households_sliced = True

# if we need a subset of full store
elif households_sample_size > 0 and df_full.shape[0] > households_sample_size:
elif tot_households > households_sample_size > 0:

logger.info("sampling %s of %s households" % (households_sample_size, df_full.shape[0]))
logger.info("sampling %s of %s households" % (households_sample_size, tot_households))

"""
Because random seed is set differently for each step, sampling of households using
Expand DownExpand Up@@ -80,6 +82,14 @@ def households(households_sample_size, override_hh_ids, trace_hh_id):
# persons table
inject.add_injectable('households_sliced', households_sliced)

if 'sample_rate' not in df.columns:
if households_sample_size == 0:
sample_rate = 1
else:
sample_rate = round(households_sample_size / tot_households, 3)

df['sample_rate'] = sample_rate

logger.info("loaded households %s" % (df.shape,))

# FIXME - pathological knowledge of name of chunk_id column used by chunked_choosers_by_chunk_id
Expand Down
21 changes: 20 additions & 1 deletion activitysim/abm/test/test_pipeline.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,12 @@
import logging
import pkg_resources

import openmatrix as omx
import numpy as np
import numpy.testing as npt

import pandas as pd
import pandas.util.testing as pdt
import pandas.testing as pdt
import pytest
import yaml

Expand All@@ -17,6 +21,7 @@

# set the max households for all tests (this is to limit memory use on travis)
HOUSEHOLDS_SAMPLE_SIZE = 100
HOUSEHOLDS_SAMPLE_RATE = 0.02 # HOUSEHOLDS_SAMPLE_RATE / 5000 households

# household with mandatory, non mandatory, atwork_subtours, and joint tours
HH_ID = 257341
Expand DownExpand Up@@ -52,6 +57,7 @@ def setup_dirs(configs_dir, data_dir=None):
tracing.delete_output_files('csv')
tracing.delete_output_files('txt')
tracing.delete_output_files('yaml')
tracing.delete_output_files('omx')


def teardown_function(func):
Expand DownExpand Up@@ -433,6 +439,18 @@ def regress():
# should be at least two tours per trip
assert trips_df.shape[0] >= 2*tours_df.shape[0]

# write_trip_matrices
trip_matrices_file = config.output_file_path('trips_md.omx')
assert os.path.exists(trip_matrices_file)
trip_matrices = omx.open_file(trip_matrices_file)
assert trip_matrices.shape() == (25, 25)

assert 'WALK_MD' in trip_matrices.list_matrices()
walk_trips = np.array(trip_matrices['WALK_MD'])
assert walk_trips.dtype == np.dtype('float64')

trip_matrices.close()


def test_full_run1():

Expand DownExpand Up@@ -517,6 +535,7 @@ def test_full_run5_singleton():

if __name__ == "__main__":

from activitysim import abm # register injectables
print("running test_full_run1")
test_full_run1()
# teardown_function(None)
1 change: 1 addition & 0 deletions activitysim/examples/example_mtc/configs/settings.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,6 +86,7 @@ models:
- trip_mode_choice
- write_data_dictionary
- track_skim_usage
- write_trip_matrices
- write_tables

# to resume after last successful checkpoint, specify resume_after: _
Expand Down
Loading